diff --git a/scripts/MaxYari/LuaPhysics/PhysicsEngineGlobal.lua b/scripts/MaxYari/LuaPhysics/PhysicsEngineGlobal.lua index 6b1c632..dbf4b00 100644 --- a/scripts/MaxYari/LuaPhysics/PhysicsEngineGlobal.lua +++ b/scripts/MaxYari/LuaPhysics/PhysicsEngineGlobal.lua @@ -1,263 +1,251 @@ -local mp = 'scripts/MaxYari/LuaPhysics/' - -local world = require('openmw.world') -local storage = require("openmw.storage") - -local PhysicsObject = require(mp..'PhysicsObject') -local PhysSoundSystem = require(mp..'scripts/physics_sound_system') -local PhysMatSystem = require(mp..'scripts/physics_material_system') -local PhysAiSystem = require(mp..'scripts/physics_ai_system') -local D = require(mp..'scripts/physics_defs') -local gutils = require(mp..'scripts/gutils') - -local settings = storage.globalSection('SettingsLuaPhysics') -local doSelfCollisions = settings:get("SelfCollisions") -local crimeSystemActive = settings:get("CrimeSystemActive") - - --- local physicsObjectScript = mp.."PhysicsEngineLocal.lua" --- if true then return end - --- Defines ----------------- -local frame = 0 -PhysSoundSystem.masterVolume = 2 * settings:get("SFXVolume") - -local physObjectsMap = {} -local objectsToRemove = {} - - --- Grid collision system for dynamic objects ---------------------------------------------- -------------------------------------------------------------------------------------------- -local grid_awake = {} -local grid_sleeping = {} -local gridSize = 150 -local function getGridCellCoord(position) - return math.floor(position.x / gridSize), math.floor(position.y / gridSize), math.floor(position.z / gridSize) -end - -local function updateInGrid(physObject) - -- Remove from previous grid cell if needed - local lastGridCell = physObject.gridCell - if lastGridCell then lastGridCell[physObject.object.id] = nil end - - -- Choose grid based on sleep state - local grid = physObject.isSleeping and grid_sleeping or grid_awake - - local cellX, cellY, cellZ = getGridCellCoord(physObject.position) - if not grid[cellX] then grid[cellX] = {} end - if not grid[cellX][cellY] then grid[cellX][cellY] = {} end - if not grid[cellX][cellY][cellZ] then grid[cellX][cellY][cellZ] = {} end - local gridCell = grid[cellX][cellY][cellZ] - gridCell[physObject.object.id] = physObject - physObject.gridCell = gridCell - physObject.gridType = physObject.isSleeping and "sleeping" or "awake" -end - -local function removeFromGrid(obj) - local physObj = physObjectsMap[obj.id] - if physObj then - if physObj.gridCell then physObj.gridCell[physObj.object.id] = nil end - physObjectsMap[obj.id] = nil - end -end - -local function serialize(physObject) - return { - object = physObject.object, - position = physObject.position, - velocity = physObject.velocity, - mass = physObject.mass, - culprit = physObject.culprit, - bounce = physObject.bounce, - radius = physObject.radius - } -end - --- TO DO: Sleepers shouldnt be checked at all, probably should have their own grid object? But non-sleepers should be checked against sleepers --- TO DO: Unloaded objects should be removed from the grid - event should be sent from onInactive -local function collidePhysObjects(physObj1, physObj2) - physObj1.object:sendEvent(D.e.CollidingWithPhysObj, { other = serialize(physObj2) }) - physObj2.object:sendEvent(D.e.CollidingWithPhysObj, { other = serialize(physObj1) }) -end -local function checkCollisionsInGrid() - local alreadyChecked = {} - for cellX, cellYs in pairs(grid_awake) do - for cellY, cellZs in pairs(cellYs) do - for cellZ, awakeObjects in pairs(cellZs) do - -- Get corresponding sleeping cell (may be nil) - local sleepingObjects = (grid_sleeping[cellX] and grid_sleeping[cellX][cellY] and grid_sleeping[cellX][cellY][cellZ]) or {} - - -- Check awake vs awake - for id1, physObj1 in pairs(awakeObjects) do - for id2, physObj2 in pairs(awakeObjects) do - if physObj1.object == physObj2.object or alreadyChecked[physObj2] then goto continue_awake end - if PhysicsObject.isCollidingWith(physObj1, physObj2) then - collidePhysObjects(physObj1, physObj2) - end - ::continue_awake:: - end - -- Check awake vs sleeping - for id2, physObj2 in pairs(sleepingObjects) do - --if physObj1.object == physObj2.object then goto continue_sleeping end - if PhysicsObject.isCollidingWith(physObj1, physObj2) then - collidePhysObjects(physObj1, physObj2) - end - ::continue_sleeping:: - end - alreadyChecked[physObj1] = true - end - end - end - end -end ---------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------- - - - --- Moving objects and checking grid-optimised collisions with other physics objects ------------------- ---------------------------------------------------------------------------------------------------------- - -local function onPhysObjPropsUpdate(props) - local id = props.object.id - local physObj = physObjectsMap[id] - if not physObj then - physObj = {} - physObjectsMap[id] = physObj - end - - gutils.shallowMergeTables(physObj, props) - -- Move between grids if sleep state changed - - if doSelfCollisions and not physObj.ignorePhysObjectCollisions then - if physObj.position then updateInGrid(physObj) end - end -end - -local function handleUpdateVisPos(pObjData) - - -- print("Global received teleport request from",d.object,"At frame",frame) - local object = pObjData.object - local cell = object.cell - - -- print("Upd vis pos on ",object,cell) - - if objectsToRemove[object.id] then return end - - local physObj = physObjectsMap[object.id] - if not physObj or not physObj.initialized then - --[[ print("ignoring ",physObj) - if physObj then print("Since not initialised",physObj.initialized) end ]] - return - end - - if not physObj.origin then - print("WARNING WARNING, physics object without origin!") - print(gutils.tableToString(physObj)) - end - local position = pObjData.position - pObjData.rotation:apply(physObj.origin) - local rotation = pObjData.rotation - - --local isChunk5 = string.find(object.type.record(object).model:lower(),"misc_com_bottle__chunk_5") - --if isChunk5 then print("Chunk 5 teleport request", pObjData.position) end - - if object and object.count > 0 and cell ~= nil then - object:teleport(cell, position, { rotation = rotation }) - onPhysObjPropsUpdate(pObjData) - end -end - -local function removeObject(obj) - objectsToRemove[obj.id] = obj - removeFromGrid(obj) -end - - - --- onUpdate ----- ------------------ -local function onUpdate(dt) - --print("Global Onupdate frame", frame) - frame = frame + 1 - - -- refetch settings - doSelfCollisions = settings:get("SelfCollisions") - crimeSystemActive = settings:get("CrimeSystemActive") - - -- removal of scheduled objects - for id, obj in pairs(objectsToRemove) do - obj:remove() - end - objectsToRemove = {} - - if not PhysMatSystem.initialized then - PhysMatSystem.init() - end - - if doSelfCollisions then - checkCollisionsInGrid() - end - - if crimeSystemActive then - PhysAiSystem.update() - end -end - - - -return { - engineHandlers = { - onUpdate = onUpdate, - }, - eventHandlers = { - [D.e.UpdateVisPos] = handleUpdateVisPos, - [D.e.PhysPropUpdReport] = function (data) - onPhysObjPropsUpdate(data) - end, - [D.e.InactivationReport] = function (data) - removeFromGrid(data.object) - end, - [D.e.RemoveObject] = function(data) - removeObject(data.object) - end, - [D.e.SpawnCollilsionEffects] = function (data) - PhysMatSystem.spawnCollilsionEffects(data) - end, - [D.e.SpawnMaterialEffect] = function (data) - PhysMatSystem.spawnMaterialEffect(data.material, data.position) - end, - [D.e.PlayCollisionSounds] = function(data) - PhysSoundSystem.playCollisionSounds(data) - end, - [D.e.PlayCrashSound] = function(data) - PhysSoundSystem.playCrashSound(data) - end, - [D.e.PlaySound] = function(data) - PhysSoundSystem.playSound(data) - end, - [D.e.PlayWaterSplashSound] = function(data) - PhysSoundSystem.playWaterSplashSound(data) - end, - [D.e.WhatIsMyPhysicsData] = function(data) - local mat = PhysMatSystem.getMaterialFromObject(data.object) - data.object:sendEvent(D.e.SetMaterial, { material = mat}) - data.object:sendEvent(D.e.SetPhysicsProperties, { player = world.players[1]}) - end, - [D.e.ObjectFenagled] = function(...) - if not crimeSystemActive then return end - PhysAiSystem.onObjectFenagled(...) - end, - [D.e.DetectCulpritResult] = function(...) - if not crimeSystemActive then return end - PhysAiSystem.onDetectCulpritResult(...) - end - }, - interfaceName = "LuaPhysics", - interface = { - version = 1.0, - playCrashSound = PhysSoundSystem.playCrashSound, - playSound = PhysSoundSystem.playSound, - getMaterialFromObject = PhysMatSystem.getMaterialFromObject, - removeObject = removeObject - }, -} +-- PhysicsEngineGlobal.lua +local mp = 'scripts/MaxYari/LuaPhysics/' + +local world = require('openmw.world') +local storage = require("openmw.storage") + +local PhysicsObject = require(mp..'PhysicsObject') +local PhysSoundSystem = require(mp..'scripts/physics_sound_system') +local PhysMatSystem = require(mp..'scripts/physics_material_system') +local PhysAiSystem = require(mp..'scripts/physics_ai_system') +local D = require(mp..'scripts/physics_defs') +local gutils = require(mp..'scripts/gutils') + +local settings = storage.globalSection('SettingsLuaPhysics') +local doSelfCollisions = settings:get("SelfCollisions") +local crimeSystemActive = settings:get("CrimeSystemActive") + +local frame = 0 +PhysSoundSystem.masterVolume = 2 * settings:get("SFXVolume") + +local physObjectsMap = {} +local objectsToRemove = {} + +local grid_awake = {} +local grid_sleeping = {} +local gridSize = 150 + +local function getGridCellCoord(position) + return math.floor(position.x / gridSize), math.floor(position.y / gridSize), math.floor(position.z / gridSize) +end + +local function updateInGrid(physObject) + local lastGridCell = physObject.gridCell + if lastGridCell then lastGridCell[physObject.object.id] = nil end + + local grid = physObject.isSleeping and grid_sleeping or grid_awake + local cellX, cellY, cellZ = getGridCellCoord(physObject.position) + if not grid[cellX] then grid[cellX] = {} end + if not grid[cellX][cellY] then grid[cellX][cellY] = {} end + if not grid[cellX][cellY][cellZ] then grid[cellX][cellY][cellZ] = {} end + local gridCell = grid[cellX][cellY][cellZ] + gridCell[physObject.object.id] = physObject + physObject.gridCell = gridCell + physObject.gridType = physObject.isSleeping and "sleeping" or "awake" +end + +local function removeFromGrid(obj) + local physObj = physObjectsMap[obj.id] + if physObj then + if physObj.gridCell then physObj.gridCell[physObj.object.id] = nil end + physObjectsMap[obj.id] = nil + end +end + +local function serialize(physObject) + return { + object = physObject.object, + position = physObject.position, + velocity = physObject.velocity, + mass = physObject.mass, + culprit = physObject.culprit, + bounce = physObject.bounce, + radius = physObject.radius + } +end + +local function collidePhysObjects(physObj1, physObj2) + physObj1.object:sendEvent(D.e.CollidingWithPhysObj, { other = serialize(physObj2) }) + physObj2.object:sendEvent(D.e.CollidingWithPhysObj, { other = serialize(physObj1) }) +end + +local function checkCollisionsInGrid() + local alreadyChecked = {} + for cellX, cellYs in pairs(grid_awake) do + for cellY, cellZs in pairs(cellYs) do + for cellZ, awakeObjects in pairs(cellZs) do + local sleepingObjects = (grid_sleeping[cellX] and grid_sleeping[cellX][cellY] and grid_sleeping[cellX][cellY][cellZ]) or {} + + for id1, physObj1 in pairs(awakeObjects) do + for id2, physObj2 in pairs(awakeObjects) do + if physObj1.object == physObj2.object or alreadyChecked[physObj2] then goto continue_awake end + if PhysicsObject.isCollidingWith(physObj1, physObj2) then + collidePhysObjects(physObj1, physObj2) + end + ::continue_awake:: + end + for id2, physObj2 in pairs(sleepingObjects) do + if PhysicsObject.isCollidingWith(physObj1, physObj2) then + collidePhysObjects(physObj1, physObj2) + end + ::continue_sleeping:: + end + alreadyChecked[physObj1] = true + end + end + end + end +end + +local function onPhysObjPropsUpdate(props) + local id = props.object.id + local physObj = physObjectsMap[id] + if not physObj then + physObj = {} + physObjectsMap[id] = physObj + end + + gutils.shallowMergeTables(physObj, props) + if doSelfCollisions and not physObj.ignorePhysObjectCollisions then + if physObj.position then updateInGrid(physObj) end + end +end + +local function handleUpdateVisPos(pObjData) + local object = pObjData.object + local cell = object.cell + + if objectsToRemove[object.id] then return end + + local physObj = physObjectsMap[object.id] + if not physObj or not physObj.initialized then + return + end + + if not physObj.origin then + print("WARNING WARNING, physics object without origin!") + print(gutils.tableToString(physObj)) + end + local position = pObjData.position - pObjData.rotation:apply(physObj.origin) + local rotation = pObjData.rotation + + if object and object.count > 0 and cell ~= nil then + object:teleport(cell, position, { rotation = rotation }) + onPhysObjPropsUpdate(pObjData) + end +end + +local function removeObject(obj) + objectsToRemove[obj.id] = obj + removeFromGrid(obj) +end + +local function onUpdate(dt) + frame = frame + 1 + + doSelfCollisions = settings:get("SelfCollisions") + crimeSystemActive = settings:get("CrimeSystemActive") + + for id, obj in pairs(objectsToRemove) do + obj:remove() + end + objectsToRemove = {} + + if not PhysMatSystem.initialized then + PhysMatSystem.init() + end + + if doSelfCollisions then + checkCollisionsInGrid() + end + + if crimeSystemActive then + PhysAiSystem.update() + end +end + +return { + engineHandlers = { + onUpdate = onUpdate, + }, + eventHandlers = { + [D.e.UpdateVisPos] = handleUpdateVisPos, + [D.e.PhysPropUpdReport] = function (data) + onPhysObjPropsUpdate(data) + end, + [D.e.InactivationReport] = function (data) + removeFromGrid(data.object) + end, + [D.e.RemoveObject] = function(data) + removeObject(data.object) + end, + [D.e.SpawnCollilsionEffects] = function (data) + PhysMatSystem.spawnCollilsionEffects(data) + end, + [D.e.SpawnMaterialEffect] = function (data) + PhysMatSystem.spawnMaterialEffect(data.material, data.position) + end, + [D.e.PlayCollisionSounds] = function(data) + PhysSoundSystem.playCollisionSounds(data) + end, + [D.e.PlayCrashSound] = function(data) + PhysSoundSystem.playCrashSound(data) + end, + [D.e.PlaySound] = function(data) + PhysSoundSystem.playSound(data) + end, + [D.e.PlayWaterSplashSound] = function(data) + PhysSoundSystem.playWaterSplashSound(data) + end, + [D.e.WhatIsMyPhysicsData] = function(data) + local mat = PhysMatSystem.getMaterialFromObject(data.object) + data.object:sendEvent(D.e.SetMaterial, { material = mat}) + data.object:sendEvent(D.e.SetPhysicsProperties, { player = world.players[1]}) + end, + [D.e.ObjectFenagled] = function(...) + if not crimeSystemActive then return end + PhysAiSystem.onObjectFenagled(...) + end, + [D.e.DetectCulpritResult] = function(...) + if not crimeSystemActive then return end + PhysAiSystem.onDetectCulpritResult(...) + end, + ["DropItem"] = function(data) + local newObject = data.object + print("Initializing physics for dropped item", newObject.recordId, "at position", newObject.position) + local mat = PhysMatSystem.getMaterialFromObject(newObject) + print("Material:", mat) + newObject:sendEvent(D.e.SetMaterial, { material = mat}) + newObject:sendEvent(D.e.SetPhysicsProperties, { + drag = 0.08, + bounce = 1.2, + isSleeping = false, + culprit = data.culprit, + mass = 1.2, + buoyancy = 0.3, + lockRotation = false, + angularDrag = 0.20, + resetOnLoad = false, + ignoreWorldCollisions = false, + collisionMode = "sphere", + realignWhenRested = false + }) + newObject:sendEvent(D.e.SetPositionUnadjusted, {position = newObject.position}) + local randomHorizontal = util.vector3( + (math.random() - 0.5) * 16, + (math.random() - 0.5) * 16, + -200 + ) + newObject:sendEvent(D.e.ApplyImpulse, {impulse = randomHorizontal, culprit = data.culprit}) + print("Applied impulse, physics initialized") + end + }, + interfaceName = "LuaPhysics", + interface = { + version = 1.0, + playCrashSound = PhysSoundSystem.playCrashSound, + playSound = PhysSoundSystem.playSound, + getMaterialFromObject = PhysMatSystem.getMaterialFromObject, + removeObject = removeObject + }, +} \ No newline at end of file diff --git a/scripts/MaxYari/LuaPhysics/PhysicsEngineLocal.lua b/scripts/MaxYari/LuaPhysics/PhysicsEngineLocal.lua index 74482ad..3774462 100644 --- a/scripts/MaxYari/LuaPhysics/PhysicsEngineLocal.lua +++ b/scripts/MaxYari/LuaPhysics/PhysicsEngineLocal.lua @@ -1,274 +1,315 @@ --- OpenMW Lua Physics - Authors: Maksim Eremenko, GPT-4o (Copilot) - -local mp = 'scripts/MaxYari/LuaPhysics/' - -local core = require('openmw.core') -local util = require('openmw.util') -local vfs = require('openmw.vfs') -local nearby = require('openmw.nearby') - -local omwself = require('openmw.self') - -local PhysicsObject = require(mp..'PhysicsObject') - -local D = require(mp..'scripts/physics_defs') - - --- if omwself.recordId ~= "p_restore_health_s" then return end --- if omwself.recordId ~= "food_kwama_egg_02" then return end --- TO DO: save serialised state of the physics object in onsave - - - --- Main stuff ---------------------------------------------- ------------------------------------------------------------- -local frame = 0 -local lastCell = omwself.cell -local physicsObject = PhysicsObject:new(omwself, { drag = 0.1, bounce = 0.5, isSleeping = true }) -local lastUnderwater = physicsObject.isUnderwater -local crossedWater = false -local lastSleeping = physicsObject.isSleeping -local persistentData = {} - - -local soundPause = 0.23; -local lastSoundTime = 0.0; -local sfxMinSpeed = 50; -local fenagledMinSpeed = 50; - -local function volumeFromVelocity(velocity) - local volume = 0 - if velocity:length() >= sfxMinSpeed then - volume = util.remap(velocity:length(), sfxMinSpeed, 600, 0.33, 1) - -- print("Volume:", volume) - end - return volume -end - -local function tryPlayCollisionSounds(hitResult) - local now = core.getRealTime() - local velocity = physicsObject.velocity - local volume = volumeFromVelocity(velocity) - - local pitch = 0.8 + math.random() * 0.2 - local params = { volume = volume, pitch = pitch, loop = false } - - if volume > 0 and now - lastSoundTime > soundPause then - -- Play sounds - core.sendGlobalEvent(D.e.PlayCollisionSounds, { - object = omwself, - surface = hitResult.hitObject, - params = params - }) - - -- Spawn visual effect - core.sendGlobalEvent(D.e.SpawnCollilsionEffects, { - object = omwself, - surface = hitResult.hitObject, - position = hitResult.hitPos - }) - - lastSoundTime = now - end -end - -local function tryPlayWaterSounds() - local now = core.getRealTime() - if not physicsObject.isSleeping and physicsObject.velocity:length() > 50 and now - lastSoundTime > soundPause then - core.sendGlobalEvent(D.e.SpawnMaterialEffect, { - material = "Water", - position = physicsObject.position - }) - core.sendGlobalEvent(D.e.PlayWaterSplashSound, { - object = omwself, - params = { volume = volumeFromVelocity(physicsObject.velocity), pitch = 0.8 + math.random() * 0.2, loop = false } - }) - lastSoundTime = now - end -end - - -local lastOOBCheck = math.random() -local function checkOutOfBounds() - if physicsObject.isSleeping then return end - local now = core.getRealTime() - if now - lastOOBCheck < 1 then return end - - local position = physicsObject.position - local initialPosition = physicsObject.initialPosition - local isOOB = false - - if omwself.cell and not omwself.cell.isExterior then - -- Interior cell: check if object is 100 meters below its initial position - if position.z < initialPosition.z - 100 * 72 then - print("Interior cell out of bounds detectewd, resetting", omwself) - physicsObject:resetPosition(true) - isOOB = true - end - elseif omwself.cell and omwself.cell.isExterior then - -- Exterior cell: track underwater status and cast ray upwards if status changes - if crossedWater then - if physicsObject.isUnderwater then - local rayStart = position - local rayEnd = position + util.vector3(0, 0, 10000 * 72) - local hitResult = nearby.castRay(rayStart, rayEnd, { collisionType = nearby.COLLISION_TYPE.HeightMap }) - - if hitResult and hitResult.hit then - print("Exterior cell out of bounds detectewd, resetting", omwself) - physicsObject:resetPosition(true) - isOOB = true - end - end - end - end - lastOOBCheck = now - return isOOB -end - - -local function onCollision(hitResult) - tryPlayCollisionSounds(hitResult) - if physicsObject.velocity:length() >= fenagledMinSpeed then - core.sendGlobalEvent(D.e.ObjectFenagled, { - object = omwself, - culprit = physicsObject.culprit, - isOffensive = true - }) - end -end - -physicsObject.onCollision:addEventHandler(onCollision) -physicsObject.onPhysObjectCollision:addEventHandler(onCollision) - -local function onUpdate(dt) - if physicsObject.isSleeping or not omwself.count then return end - - local cell = omwself.cell - - if cell and not lastCell then - -- Reset physics state if was just taken out of the inventory - physicsObject:reInit() - end - lastCell = cell - - -- nil cell == we are in the inventory/container, no physics are needed - if cell == nil then - return - end - - -- Update physics simulation - physicsObject:update(dt) - physicsObject:trySleep(dt) - - -- Sending owner event if just awoken - if physicsObject.isSleeping == false and physicsObject.isSleeping ~= lastSleeping then - lastSleeping = physicsObject.isSleeping - core.sendGlobalEvent(D.e.ObjectFenagled, { - object = omwself, - culprit = physicsObject.culprit - }) - end - - -- Detecting crossing of a water boundary line - if lastUnderwater ~= physicsObject.isUnderwater then - crossedWater = true - else - crossedWater = false - end - lastUnderwater = physicsObject.isUnderwater - - -- Check if object is out of bounds -- should be done after water threshold crossing was detected - local isOOB = checkOutOfBounds() - - -- Water splash sounds - if crossedWater and not isOOB then tryPlayWaterSounds() end - - frame = frame + 1 -end - -onUpdate(0) - - - -local function onSave() - return { - physicsObjectPersistentData = physicsObject:getPersistentData(), - persistentData = persistentData - } -end - -local function onLoad(data) - if not data then return end - if data.persistentData then - persistentData = data.persistentData - core.sendGlobalEvent(D.e.PersistentDataReport, { - source = omwself, - data = persistentData - }) - end - physicsObject:loadPersistentData(data.physicsObjectPersistentData) -end - -local function onInactive(data) - print("Object", omwself.recordId, "is inactive") - core.sendGlobalEvent(D.e.InactivationReport, { - object = omwself - }) -end - - - -return { - engineHandlers = { - onUpdate = onUpdate, - onSave = onSave, - onLoad = onLoad, - onInactive = onInactive - }, - eventHandlers = { - [D.e.MoveTo] = function(e) - local currentVelocity = physicsObject.velocity; - local pushVector = e.position - physicsObject.position - currentVelocity/4; - - if pushVector:length() > e.maxImpulse then - pushVector = pushVector:normalize() * e.maxImpulse - end - - physicsObject:applyImpulse(pushVector, e.culprit) - end, - [D.e.ApplyImpulse] = function(e) - physicsObject:applyImpulse(e.impulse, e.culprit) - end, - [D.e.SetPhysicsProperties] = function(props) - --print("Received physics properties",gutils.tableToString(props)) - physicsObject:updateProperties(props) - end, - [D.e.SetMaterial] = function(e) - physicsObject:updateMaterial(e.material, e.recalcMass, e.recalcBuoyancy) - end, - [D.e.SetPositionUnadjusted] = function(e) - physicsObject:setPositionUnadjusted(e.position) - end, - [D.e.CollidingWithPhysObj] = function(e) - --print("Received phys object collide event",e.other.object.recordId) - physicsObject:handlePhysObjectCollision(e.other) - end, - [D.e.UpdatePersistentData] = function(data) - persistentData = data - core.sendGlobalEvent(D.e.PersistentDataReport, { - source = omwself, - data = persistentData - }) - end - }, - interfaceName = "LuaPhysics", - interface = {version=1.0, physicsObject=physicsObject} - -} - - - - - - - +-- OpenMW Lua Physics - Authors: Maksim Eremenko, GPT-4o (Copilot) + +local mp = 'scripts/MaxYari/LuaPhysics/' + +local core = require('openmw.core') +local util = require('openmw.util') +local vfs = require('openmw.vfs') +local nearby = require('openmw.nearby') + +local omwself = require('openmw.self') +local types = require('openmw.types') + +local PhysicsObject = require(mp..'PhysicsObject') + +local D = require(mp..'scripts/physics_defs') + + +-- if omwself.recordId ~= "p_restore_health_s" then return end +-- if omwself.recordId ~= "food_kwama_egg_02" then return end +-- TO DO: save serialised state of the physics object in onsave + + + +-- Main stuff ---------------------------------------------- +------------------------------------------------------------ +local frame = 0 +local lastCell = omwself.cell +local physicsObject = PhysicsObject:new(omwself, { drag = 0.1, bounce = 0.5, isSleeping = true }) +local lastUnderwater = physicsObject.isUnderwater +local crossedWater = false +local lastSleeping = physicsObject.isSleeping +local persistentData = {} + + +local soundPause = 0.23; +local lastSoundTime = 0.0; +local sfxMinSpeed = 50; +local fenagledMinSpeed = 50; + +local function volumeFromVelocity(velocity) + local volume = 0 + if velocity:length() >= sfxMinSpeed then + volume = util.remap(velocity:length(), sfxMinSpeed, 600, 0.33, 1) + -- print("Volume:", volume) + end + return volume +end + +local function tryPlayCollisionSounds(hitResult) + local now = core.getRealTime() + local velocity = physicsObject.velocity + local volume = volumeFromVelocity(velocity) + + local pitch = 0.8 + math.random() * 0.2 + local params = { volume = volume, pitch = pitch, loop = false } + + if volume > 0 and now - lastSoundTime > soundPause then + -- Play sounds + core.sendGlobalEvent(D.e.PlayCollisionSounds, { + object = omwself, + surface = hitResult.hitObject, + params = params + }) + + -- Spawn visual effect + core.sendGlobalEvent(D.e.SpawnCollilsionEffects, { + object = omwself, + surface = hitResult.hitObject, + position = hitResult.hitPos + }) + + lastSoundTime = now + end +end + +local function tryPlayWaterSounds() + local now = core.getRealTime() + if not physicsObject.isSleeping and physicsObject.velocity:length() > 50 and now - lastSoundTime > soundPause then + core.sendGlobalEvent(D.e.SpawnMaterialEffect, { + material = "Water", + position = physicsObject.position + }) + core.sendGlobalEvent(D.e.PlayWaterSplashSound, { + object = omwself, + params = { volume = volumeFromVelocity(physicsObject.velocity), pitch = 0.8 + math.random() * 0.2, loop = false } + }) + lastSoundTime = now + end +end + + +local lastOOBCheck = math.random() +local function checkOutOfBounds() + if physicsObject.isSleeping then return end + local now = core.getRealTime() + if now - lastOOBCheck < 1 then return end + + local position = physicsObject.position + local initialPosition = physicsObject.initialPosition + local isOOB = false + + if omwself.cell and not omwself.cell.isExterior then + -- Interior cell: check if object is 100 meters below its initial position + if position.z < initialPosition.z - 100 * 72 then + print("Interior cell out of bounds detectewd, resetting", omwself) + physicsObject:resetPosition(true) + isOOB = true + end + elseif omwself.cell and omwself.cell.isExterior then + -- Exterior cell: track underwater status and cast ray upwards if status changes + if crossedWater then + if physicsObject.isUnderwater then + local rayStart = position + local rayEnd = position + util.vector3(0, 0, 10000 * 72) + local hitResult = nearby.castRay(rayStart, rayEnd, { collisionType = nearby.COLLISION_TYPE.HeightMap }) + + if hitResult and hitResult.hit then + print("Exterior cell out of bounds detectewd, resetting", omwself) + physicsObject:resetPosition(true) + isOOB = true + end + end + end + end + lastOOBCheck = now + return isOOB +end + + +local function projectileCollisionFilter(hit) + if not hit or not hit.hitObject then return true end + + local obj = hit.hitObject + if obj.type == types.NPC or obj.type == types.Creature then + local health = types.Actor.stats.dynamic.health(obj).current + if health <= 0 then + -- Dead actor. Check height. + local relZ = hit.hitPos.z - obj.position.z + if relZ > 20 then + -- TOO HIGH. Ignore collision with the ghost upright capsule. + return false + end + end + end + return true +end + +physicsObject.collisionFilter = projectileCollisionFilter + + +local function onCollision(hitResult) + tryPlayCollisionSounds(hitResult) + + -- [[ CUSTOM LUA PROJECTILE PHYSICS HOOK ]] -- + if omwself.type == types.Weapon then + local record = types.Weapon.record(omwself) + if record and (record.type == types.Weapon.TYPE.Arrow or + record.type == types.Weapon.TYPE.Bolt or + record.type == types.Weapon.TYPE.MarksmanThrown) then + + core.sendGlobalEvent('LuaProjectilePhysics_ProjectileHit', { + projectile = omwself, + hitObject = hitResult.hitObject, + hitPos = hitResult.hitPos, + hitNormal = hitResult.hitNormal or hitResult.normal, + velocity = physicsObject.velocity + }) + end + end + -- [[ END CUSTOM HOOK ]] -- + + if physicsObject.velocity:length() >= fenagledMinSpeed then + core.sendGlobalEvent(D.e.ObjectFenagled, { + object = omwself, + culprit = physicsObject.culprit, + isOffensive = true + }) + end +end + +physicsObject.onCollision:addEventHandler(onCollision) +physicsObject.onPhysObjectCollision:addEventHandler(onCollision) + +local function onUpdate(dt) + if physicsObject.isSleeping or not omwself.count then return end + + local cell = omwself.cell + + if cell and not lastCell then + -- Reset physics state if was just taken out of the inventory + physicsObject:reInit() + end + lastCell = cell + + -- nil cell == we are in the inventory/container, no physics are needed + if cell == nil then + return + end + + -- Update physics simulation + physicsObject:update(dt) + physicsObject:trySleep(dt) + + -- Sending owner event if just awoken + if physicsObject.isSleeping == false and physicsObject.isSleeping ~= lastSleeping then + lastSleeping = physicsObject.isSleeping + core.sendGlobalEvent(D.e.ObjectFenagled, { + object = omwself, + culprit = physicsObject.culprit + }) + end + + -- Detecting crossing of a water boundary line + if lastUnderwater ~= physicsObject.isUnderwater then + crossedWater = true + else + crossedWater = false + end + lastUnderwater = physicsObject.isUnderwater + + -- Check if object is out of bounds -- should be done after water threshold crossing was detected + local isOOB = checkOutOfBounds() + + -- Water splash sounds + if crossedWater and not isOOB then tryPlayWaterSounds() end + + frame = frame + 1 +end + +onUpdate(0) + + + +local function onSave() + return { + physicsObjectPersistentData = physicsObject:getPersistentData(), + persistentData = persistentData + } +end + +local function onLoad(data) + if not data then return end + if data.persistentData then + persistentData = data.persistentData + core.sendGlobalEvent(D.e.PersistentDataReport, { + source = omwself, + data = persistentData + }) + end + physicsObject:loadPersistentData(data.physicsObjectPersistentData) +end + +local function onInactive(data) + print("Object", omwself.recordId, "is inactive") + core.sendGlobalEvent(D.e.InactivationReport, { + object = omwself + }) +end + + + +return { + engineHandlers = { + onUpdate = onUpdate, + onSave = onSave, + onLoad = onLoad, + onInactive = onInactive + }, + eventHandlers = { + [D.e.MoveTo] = function(e) + local currentVelocity = physicsObject.velocity; + local pushVector = e.position - physicsObject.position - currentVelocity/4; + + if pushVector:length() > e.maxImpulse then + pushVector = pushVector:normalize() * e.maxImpulse + end + + physicsObject:applyImpulse(pushVector, e.culprit) + end, + [D.e.ApplyImpulse] = function(e) + physicsObject:applyImpulse(e.impulse, e.culprit) + end, + [D.e.SetPhysicsProperties] = function(props) + --print("Received physics properties",gutils.tableToString(props)) + physicsObject:updateProperties(props) + end, + [D.e.SetMaterial] = function(e) + physicsObject:updateMaterial(e.material, e.recalcMass, e.recalcBuoyancy) + end, + [D.e.SetPositionUnadjusted] = function(e) + physicsObject:setPositionUnadjusted(e.position) + end, + [D.e.CollidingWithPhysObj] = function(e) + --print("Received phys object collide event",e.other.object.recordId) + physicsObject:handlePhysObjectCollision(e.other) + end, + [D.e.UpdatePersistentData] = function(data) + persistentData = data + core.sendGlobalEvent(D.e.PersistentDataReport, { + source = omwself, + data = persistentData + }) + end + }, + interfaceName = "LuaPhysics", + interface = {version=1.0, physicsObject=physicsObject} + +} + + + + + + + diff --git a/scripts/MaxYari/LuaPhysics/PhysicsEnginePlayer.lua b/scripts/MaxYari/LuaPhysics/PhysicsEnginePlayer.lua index ea96cab..62b89d7 100644 --- a/scripts/MaxYari/LuaPhysics/PhysicsEnginePlayer.lua +++ b/scripts/MaxYari/LuaPhysics/PhysicsEnginePlayer.lua @@ -1,69 +1,64 @@ -local mp = 'scripts/MaxYari/LuaPhysics/' - -local core = require('openmw.core') -local util = require('openmw.util') -local types = require('openmw.types') -local nearby = require('openmw.nearby') -local ui = require('openmw.ui') -local camera = require('openmw.camera') -local omwself = require('openmw.self') -local input = require('openmw.input') -local I = require('openmw.interfaces') -local async = require("openmw.async") -local storage = require("openmw.storage") - -local gutils = require(mp..'scripts/gutils') -local PhysicsUtils = require(mp..'scripts/physics_utils') -local animManager = require(mp..'scripts/anim_manager') -local D = require(mp..'scripts/physics_defs') - -local selfActor = gutils.Actor:new(omwself) - -local settings = storage.globalSection('SettingsLuaPhysicsAux') -local interface = { - version = 1.0, - defaultThrowEnabled = true -} - - -local frame = 0 - - -local function onUpdate(dt) - frame = frame + 1 - local noColOnShift = settings:get("NoCollisionOnShift") - -- Utilities update loop - PhysicsUtils.HoldGrabbedObject(dt, noColOnShift and input.isShiftPressed()) - - if PhysicsUtils.activeObject and input.getBooleanActionValue("Use") and interface.defaultThrowEnabled then - local throwImpulse = 500 - local direction = camera.viewportToWorldVector(util.vector2(0.5, 0.5)):normalize() - - -- Launching! - PhysicsUtils.activeObject:sendEvent(D.e.ApplyImpulse, {impulse=direction*throwImpulse, culprit = omwself.object }) - PhysicsUtils.DropObject() - end - - if I.impactEffects and I.impactEffects.version < 107 then - return ui.showMessage("LuaPhysics: OpenMW Impact Effects mod detected, but it's an old version. Please update OpenMW Impact Effects.") - end - - -end - -input.registerActionHandler('GrabPhysicsObject', async:callback(function(val) - if val then - PhysicsUtils.GrabObject() - types.Actor.setStance(omwself, types.Actor.STANCE.Nothing) - else - PhysicsUtils.DropObject() - end -end)) - -return { - engineHandlers = { - onUpdate = onUpdate, - }, - interfaceName = "LuaPhysics", - interface = interface -} +-- PhysicsEnginePlayer.lua +local mp = 'scripts/MaxYari/LuaPhysics/' + +local core = require('openmw.core') +local util = require('openmw.util') +local types = require('openmw.types') +local nearby = require('openmw.nearby') +local ui = require('openmw.ui') +local camera = require('openmw.camera') +local omwself = require('openmw.self') +local input = require('openmw.input') +local I = require('openmw.interfaces') +local async = require("openmw.async") +local storage = require("openmw.storage") + +local gutils = require(mp..'scripts/gutils') +local PhysicsUtils = require(mp..'scripts/physics_utils') +local animManager = require(mp..'scripts/anim_manager') +local D = require(mp..'scripts/physics_defs') + +local selfActor = gutils.Actor:new(omwself) + +local settings = storage.globalSection('SettingsLuaPhysicsAux') +local interface = { + version = 1.0, + defaultThrowEnabled = true +} + +local frame = 0 + +local function onUpdate(dt) + frame = frame + 1 + local noColOnShift = settings:get("NoCollisionOnShift") + PhysicsUtils.HoldGrabbedObject(dt, noColOnShift and input.isShiftPressed()) + + if PhysicsUtils.activeObject and input.getBooleanActionValue("Use") and interface.defaultThrowEnabled then + local throwImpulse = 500 + local direction = camera.viewportToWorldVector(util.vector2(0.5, 0.5)):normalize() + + PhysicsUtils.activeObject:sendEvent(D.e.ApplyImpulse, {impulse=direction*throwImpulse, culprit = omwself.object }) + PhysicsUtils.DropObject() + end + + if I.impactEffects and I.impactEffects.version < 107 then + return ui.showMessage("LuaPhysics: OpenMW Impact Effects mod detected, but it's an old version. Please update OpenMW Impact Effects.") + end +end + +input.registerActionHandler('GrabPhysicsObject', async:callback(function(val) + if val then + PhysicsUtils.GrabObject() + types.Actor.setStance(omwself, types.Actor.STANCE.Nothing) + else + PhysicsUtils.DropObject() + end +end)) + +return { + engineHandlers = { + onUpdate = onUpdate, + }, + interfaceName = "LuaPhysics", + interface = interface +} \ No newline at end of file diff --git a/scripts/MaxYari/LuaPhysics/PhysicsObject.lua b/scripts/MaxYari/LuaPhysics/PhysicsObject.lua index 6eb0b91..ecc8b28 100644 --- a/scripts/MaxYari/LuaPhysics/PhysicsObject.lua +++ b/scripts/MaxYari/LuaPhysics/PhysicsObject.lua @@ -1,489 +1,505 @@ --- OpenMW Lua Physics - Authors: Maksim Eremenko, GPT-4o (Copilot) - -local mp = 'scripts/MaxYari/LuaPhysics/' - -local core = require('openmw.core') -local util = require('openmw.util') - -local nstatus, nearby = pcall(require, "openmw.nearby") -local sstatus, omwself = pcall(require, "openmw.self") - -local phUtils = require(mp..'scripts/physics_utils') -local gutils = require(mp..'scripts/gutils') -local EventsManager = require(mp..'scripts/events_manager') -local D = require(mp..'scripts/physics_defs') - - -local Gravity = util.vector3(0, 0, -9.8*D.GUtoM) -local SleepSpeed = 7 -local SleepTime = 1 -local ImpactAngVelDamping = 0.5 -- Multiplier for angular velocity impact -local ImpactAngVelMult = 6 -local MaxAngularVelocity = 5000 -- Optional: Maximum angular velocity limit -local WaterHitVelDamping = 0.5 - ---if omwself.recordId ~= "food_kwama_egg_02" then return end - - --- PhysicsObject class ----------------------------------------------------- ----------------------------------------------------------------------------- -local PhysicsObject = {} - -function PhysicsObject:new(object, properties) - local inst = {} - setmetatable(inst, self) - self.__index = self - - --print("Creating a physics object for: ", object) - - inst:init(object, properties) - - return inst -end - -function PhysicsObject:init(object, properties) - local box = object:getBoundingBox() - - local radius = 10 - local largestHalfExtent = radius - if properties.radius then - radius = properties.radius - elseif object then - radius = math.min(box.halfSize.x, box.halfSize.y, box.halfSize.z) - largestHalfExtent = math.max(box.halfSize.x, box.halfSize.y, box.halfSize.z) - if radius < 2 then radius = 2 end - if largestHalfExtent < 2 then largestHalfExtent = 2 end - end - - local origin = util.vector3(0, 0, 0) - if properties.origin then - origin = properties.origin - elseif object then - origin = object.rotation:inverse():apply(box.center-object.position) - end - - local position = nil - local initialPosition = nil - if properties.position then - position = properties.position - elseif object then - position = object.position + object.rotation:apply(origin) - initialPosition = position - end - - local rotation = util.transform.identity - local initialRotation = nil - if properties.rotation then - rotation = properties.rotation - elseif object then - rotation = object.rotation - initialRotation = rotation - end - - self.object = object - self.position = position - self.initialPosition = initialPosition - self.initialRotation = initialRotation - self.rotation = rotation - self.origin = origin - self.velocity = util.vector3(0, 0, 0) - self.angularVelocity = util.vector3(0, 0, 0) -- Add angular velocity - self.lockRotation = properties.lockRotation or false - self.realignWhenRested = properties.realignWhenRested or false - self.ignoreWorldCollisions = false - self.ignorePhysObjectCollisions = false - self.radius = radius - self.largestHalfExtent = largestHalfExtent - self.drag = properties.drag or 0.33 - self.angularDrag = properties.angularDrag or 0.1 - self.bounce = properties.bounce or 0.5 - self.isSleeping = properties.isSleeping or false - self.sleepTimer = 0 - self.gravity = Gravity - self.resetOnLoad = false - self.collisionMode = properties.collisionMode or "sphere" -- "sphere" or "aabb". "aabb" is experimental, works decent with lockRotation, only aabb can work on objects that have their own collider. - self.isUnderwater = false - - self.cellBounds = nil -- Will be received later from global - self.player = nil -- Will be received later from global - - -- Events - self.onCollision = properties.onCollision or EventsManager:new() - self.onPhysObjectCollision = properties.onPhysObjectCollision or EventsManager:new() - self.onIntersection = properties.onIntersection or EventsManager:new() - self.onMaterialUpdate = properties.onMaterialUpdate or EventsManager:new() - - self:updateMaterial(properties.material or nil) -- Will be received later from global - -- updateMaterial calculates mass and buoyancy, reset them to ones provided to constructor, if necessary - if properties.mass then self.mass = properties.mass end - if properties.buoyancy then self.buoyancy = properties.buoyancy end - - self.initialized = true - - -- This will make global send back an event with material, cellBounds and player - core.sendGlobalEvent(D.e.WhatIsMyPhysicsData, { - object = object - }) - - local serialisedData = self:serialize() - serialisedData.initialized = self.initialized - core.sendGlobalEvent(D.e.PhysPropUpdReport, serialisedData) -end - -function PhysicsObject:updateMaterial(mat, recalcMass, recalcBuoyancy) - if recalcMass == nil then recalcMass = true end - if recalcBuoyancy == nil then recalcBuoyancy = true end - - -- print(omwself,"phys obj update material") - - self.material = mat - - -- Volume-based mass - if recalcMass then - local box = self.object:getBoundingBox() - local volume = (box.halfSize.x/D.GUtoM) * (box.halfSize.y/D.GUtoM) * (box.halfSize.z/D.GUtoM) --In meters^3 - local density = 25 -- In kg/m^3 -- Density is so low since bbox-based volume is very innacurate - if self.material and self.material == "Metal" then density = 50 end - local mass = volume * density -- In kg - if mass < 1 then mass = 1 end - self.mass = mass - end - - -- Material-based boyancy - if recalcBuoyancy then - self.buoyancy = 0.5 - if self.material == "Wood" or self.material == "Glass" or self.material == "Organic" then - self.buoyancy = 1.05 - elseif self.material == "Metal" then - self.buoyancy = 0.25 - end - end - - -- print("emitting mat upd event") - self.onMaterialUpdate:emit(self.material) -end - - -function PhysicsObject:reInit() - --print("Reinitialising a physics object for: ", self.object) - self.position = nil - self.rotation = nil - self.origin = nil - self.radius = nil - self:init(self.object, self) -end - -function PhysicsObject:updateProperties(props) - gutils.shallowMergeTables(self, props) - core.sendGlobalEvent(D.e.PhysPropUpdReport, self:serialize()) -end - - -function PhysicsObject:serialize() - return { - object = self.object, - position = self.position, - velocity = self.velocity, - radius = self.radius, - origin = self.origin, - rotation = self.rotation, - isSleeping = self.isSleeping, - bounce = self.bounce, - mass = self.mass, - culprit = self.culprit, - initialized = self.initialized, - ignorePhysObjectCollisions = self.ignorePhysObjectCollisions - } -end - -function PhysicsObject:serializeMotionData() - return { - object = self.object, - position = self.position, - velocity = self.velocity, - rotation = self.rotation - } -end - -function PhysicsObject:resetPosition(sleep) - if sleep == nil then sleep = true end - if self.initialPosition then self.position = self.initialPosition end - if self.initialRotation then self.rotation = self.initialRotation end - if sleep then self:sleep() end -end - -function PhysicsObject:getPersistentData() - return { - initialPosition = self.initialPosition, - initialRotation = self.initialRotation, - resetOnLoad = self.resetOnLoad, - ignorePhysObjectCollisions = self.ignorePhysObjectCollisions - } -end - -function PhysicsObject:loadPersistentData(data) - self:updateProperties(data) - if self.resetOnLoad then self:resetPosition(false) end -end - -function PhysicsObject:setPositionUnadjusted(position) - self.position = position + self.rotation:apply(self.origin) -end - -function PhysicsObject:sleep() - self.isSleeping = true - self:setCulprit(nil) - core.sendGlobalEvent(D.e.PhysPropUpdReport, {object = self.object, isSleeping = self.isSleeping}) -end - -function PhysicsObject:wakeUp() - self.isSleeping = false - self.sleepTimer = 0 -- Reset sleep timer when waking up - core.sendGlobalEvent(D.e.PhysPropUpdReport, {object = self.object, isSleeping = self.isSleeping}) -end - -function PhysicsObject:setCulprit(culprit) - self.culprit = culprit - core.sendGlobalEvent(D.e.PhysPropUpdReport, {object = self.object, culprit = self.culprit}) -end - -function PhysicsObject:applyImpulse(impulse, culprit) - -- print(self.mass) - self.velocity = self.velocity + impulse / self.mass - self:setCulprit(culprit) - self:wakeUp() -- Wake up the object when an impulse is applied -end - -function PhysicsObject:applyForce(force, culprit, wakeUp) - if wakeUp == nil then wakeUp = true end - if not wakeUp and self.isSleeping then return end - if not self.forceToApply then - self.forceToApply = util.vector3(0, 0, 0) - end - self.forceToApply = self.forceToApply + force - self:setCulprit(culprit) - if wakeUp then self:wakeUp() end -- Wake up the object when a force is applied -end - -function PhysicsObject:isCollidingWith(physObject) - local distance = (self.position - physObject.position):length() - return distance < (self.radius + physObject.radius) -end - -function PhysicsObject:handleCollision(hitResult) - local normal = hitResult.hitNormal - local velocity = self.velocity - local dot = velocity:dot(normal) - - local newInContact = true - local newInContactWith = hitResult.hitObject - local isNewContact = newInContact ~= self.inContact or newInContactWith ~= self.inContactWith - - self.inContact = newInContact - self.inContactWith = newInContactWith - - if dot < 0 then - -- Run collision callback - if isNewContact then - self.onCollision:emit(hitResult) - if hitResult.hitObject then - hitResult.hitObject:sendEvent(D.e.CollidingWithPhysObj, {other = self:serialize()}) - end - end - - -- Reflect velocity and apply bounce factor - self.velocity = -(normal * normal:dot(velocity) * 2 - velocity) - self.velocity = self.velocity * self.bounce - - -- Apply impact torque damping - self.angularVelocity = self.angularVelocity * ImpactAngVelDamping - - if not self.lockRotation then - -- Calculate torque from collision impact - local impactPoint = hitResult.hitPos - local relativePosition = impactPoint - self.position - local torque = relativePosition:cross(-self.velocity) - local tangVelocity = torque - local angularVeloctiy = tangVelocity/self.largestHalfExtent - - -- 6 (ImpactAngVelDamping) is a magic number that makes collistion look fun, essentially its (probably) related to moment of inertia which is not accounted for at all - self.angularVelocity = self.angularVelocity + angularVeloctiy * ImpactAngVelMult - - -- Clamp angular velocity to prevent it from growing uncontrollably - if self.angularVelocity:length() > MaxAngularVelocity then - self.angularVelocity = self.angularVelocity:normalize() * MaxAngularVelocity - end - end - - return true - else - -- Run intersection callback - if isNewContact then self.onIntersection:emit(hitResult) end - return false - end -end - -function PhysicsObject:handlePhysObjectCollision(physObject) - if physObject.culprit then self.culprit = physObject.culprit end - - local normal = (self.position - physObject.position):normalize() - --print(normal, physObject.radius) - local hitPos = physObject.position + normal*physObject.radius - - local relativeVelocity = self.velocity - physObject.velocity - local dot = relativeVelocity:dot(normal) - - if dot < 0 and math.abs(dot) > SleepSpeed then - -- print("Handling object collision", object1, object2, dot) - -- Calculate impulse magnitude - local impulseMagnitude = -(1 + math.min(self.bounce, physObject.bounce)) * dot / (1 / self.mass + 1 / physObject.mass) - - -- Apply impulses to both objects - local impulse = normal * impulseMagnitude - self.velocity = self.velocity + impulse / self.mass - --physObject.velocity = physObject.velocity - impulse / physObject.mass - - self:wakeUp() - self.onPhysObjectCollision:emit({ - hitPos = hitPos, - hitNormal = normal, - hitObject = physObject.object, - hitPhysObject = physObject - }) - - --data2:wakeUp() - end -end - - -local maxDt = 1/20 -function PhysicsObject:update(dt) - if self.disabled then return end - - if dt > maxDt then dt = maxDt end - - if not self.isSleeping then - -- Consume accumulated forces - if self.forceToApply and self.forceToApply:length() > 0 then - self.velocity = self.velocity + (self.forceToApply / self.mass) * dt - self.forceToApply = util.vector3(0, 0, 0) -- Reset accumulated forces after applying - end - - -- Apply Gravity - self.velocity = self.velocity + Gravity * dt - - -- Water Physics - local waterline = self.object.cell.waterLevel - if waterline and not PhysicsObject.isSleeping then - if self.position.z < waterline then - -- We are underwater - if not self.isUnderwater then - -- We just transitioned from above water to underwater - self.velocity = self.velocity * WaterHitVelDamping - end - self.isUnderwater = true - - -- Apply Buoyoncy - local buoyForce = Gravity * self.mass * -1 * self.buoyancy - self.velocity = self.velocity + buoyForce * dt - else - self.isUnderwater = false - end - end - - -- Apply drag - self.velocity = self.velocity * (1 - self.drag * dt) - - -- Calculate displacement - local displacement = self.velocity * dt - - -- Perform collision detection - local collided = false - local hitResult = nil - local rayStart = self.position - local rayEnd = rayStart + displacement - - if not self.ignoreWorldCollisions then - if self.collisionMode == "sphere" then - hitResult = nearby.castRay(rayStart, rayEnd, { radius = self.radius }) - elseif self.collisionMode == "aabb" then - hitResult = phUtils.customRaycastAABB(rayStart, rayEnd, self.radius, self.object:getBoundingBox(), self.rotation, { ignore = self.object }) - end - - if hitResult and hitResult.hit then - collided = self:handleCollision(hitResult) - if self.collisionMode == "sphere" then - -- Update position on collision to get close to a surface without penetrating - if collided then self.position = phUtils.calcSpherePosAtHit(self.position, displacement, hitResult.hitPos, self.radius) end - elseif self.collisionMode == "aabb" then - -- Dont update position on collision, to ensure no penetration - end - end - end - - if not collided then - self.inContact = false - self.inContactWith = nil - self.position = rayEnd - end - - if not self.lastPosition then self.lastPosition = self.position end - self.actualVelocity = (self.position - self.lastPosition) / dt - - -- Apply angular drag to angular velocity - self.angularVelocity = self.angularVelocity * (1 - self.angularDrag * dt) - if self.lockRotation then self.angularVelocity = self.angularVelocity * 0 end -- Lock rotation if specified - - -- Update rotation based on angular velocity - local angularDisplacement = self.angularVelocity * dt * 0.01 - local rotationDelta = util.transform.rotate(angularDisplacement:length(), angularDisplacement:normalize()) - self.rotation = rotationDelta * self.rotation - - -- Realign when close to rest state - if self.realignWhenRested then - -- FIX ME; rest is now based on speed, not angular speed, so realignment will probably not work as intended - if not self.rotationInfluence then self.rotationInfluence = 0 end - local speed = self.actualVelocity:length() - if speed < SleepSpeed then - self.rotationInfluence = self.sleepTimer / SleepTime - if self.rotationInfluence > 0 then - self.rotation = phUtils.slerpRotation(self.rotation, util.transform.identity, self.rotationInfluence) - end - else - self.rotationInfluence = 0 - end - end - end - - if not self.isSleeping then - -- print("Not sleeping",omwself) - core.sendGlobalEvent(D.e.UpdateVisPos, self:serializeMotionData()) - end - - -end - -function PhysicsObject:trySleep(dt) - if self.isSleeping or not self.actualVelocity then return end - - local speed = self.actualVelocity:length() - if speed < SleepSpeed then - self.sleepTimer = self.sleepTimer + dt - if self.sleepTimer >= SleepTime then - self:sleep() - end - else - self.sleepTimer = 0 -- Reset sleep timer if velocity or angular velocity exceeds threshold - end - - self.lastPosition = self.position -end - -return PhysicsObject - - - - - - - +-- OpenMW Lua Physics - Authors: Maksim Eremenko, GPT-4o (Copilot) + +local mp = 'scripts/MaxYari/LuaPhysics/' + +local core = require('openmw.core') +local util = require('openmw.util') + +local nstatus, nearby = pcall(require, "openmw.nearby") +local sstatus, omwself = pcall(require, "openmw.self") + +local phUtils = require(mp..'scripts/physics_utils') +local gutils = require(mp..'scripts/gutils') +local EventsManager = require(mp..'scripts/events_manager') +local D = require(mp..'scripts/physics_defs') + + +local Gravity = util.vector3(0, 0, -9.8*D.GUtoM) +local SleepSpeed = 7 +local SleepTime = 1 +local ImpactAngVelDamping = 0.5 -- Multiplier for angular velocity impact +local ImpactAngVelMult = 6 +local MaxAngularVelocity = 5000 -- Optional: Maximum angular velocity limit +local WaterHitVelDamping = 0.5 + +--if omwself.recordId ~= "food_kwama_egg_02" then return end + + +-- PhysicsObject class ----------------------------------------------------- +---------------------------------------------------------------------------- +local PhysicsObject = {} + +function PhysicsObject:new(object, properties) + local inst = {} + setmetatable(inst, self) + self.__index = self + + --print("Creating a physics object for: ", object) + + inst:init(object, properties) + + return inst +end + +function PhysicsObject:init(object, properties) + local box = object:getBoundingBox() + + local radius = 10 + local largestHalfExtent = radius + if properties.radius then + radius = properties.radius + elseif object then + radius = math.min(box.halfSize.x, box.halfSize.y, box.halfSize.z) + largestHalfExtent = math.max(box.halfSize.x, box.halfSize.y, box.halfSize.z) + if radius < 2 then radius = 2 end + if largestHalfExtent < 2 then largestHalfExtent = 2 end + end + + local origin = util.vector3(0, 0, 0) + if properties.origin then + origin = properties.origin + elseif object then + origin = object.rotation:inverse():apply(box.center-object.position) + end + + local position = nil + local initialPosition = nil + if properties.position then + position = properties.position + elseif object then + position = object.position + object.rotation:apply(origin) + initialPosition = position + end + + local rotation = util.transform.identity + local initialRotation = nil + if properties.rotation then + rotation = properties.rotation + elseif object then + rotation = object.rotation + initialRotation = rotation + end + + self.object = object + self.position = position + self.initialPosition = initialPosition + self.initialRotation = initialRotation + self.rotation = rotation + self.origin = origin + self.velocity = util.vector3(0, 0, 0) + self.angularVelocity = util.vector3(0, 0, 0) -- Add angular velocity + self.lockRotation = properties.lockRotation or false + self.realignWhenRested = properties.realignWhenRested or false + self.ignoreWorldCollisions = false + self.ignorePhysObjectCollisions = false + self.radius = radius + self.largestHalfExtent = largestHalfExtent + self.drag = properties.drag or 0.33 + self.angularDrag = properties.angularDrag or 0.1 + self.bounce = properties.bounce or 0.5 + self.isSleeping = properties.isSleeping or false + self.sleepTimer = 0 + self.gravity = Gravity + self.resetOnLoad = false + self.collisionMode = properties.collisionMode or "sphere" -- "sphere" or "aabb". "aabb" is experimental, works decent with lockRotation, only aabb can work on objects that have their own collider. + self.isUnderwater = false + + self.cellBounds = nil -- Will be received later from global + self.player = nil -- Will be received later from global + + -- Events + self.onCollision = properties.onCollision or EventsManager:new() + self.onPhysObjectCollision = properties.onPhysObjectCollision or EventsManager:new() + self.onIntersection = properties.onIntersection or EventsManager:new() + self.onMaterialUpdate = properties.onMaterialUpdate or EventsManager:new() + + self:updateMaterial(properties.material or nil) -- Will be received later from global + -- updateMaterial calculates mass and buoyancy, reset them to ones provided to constructor, if necessary + if properties.mass then self.mass = properties.mass end + if properties.buoyancy then self.buoyancy = properties.buoyancy end + + self.initialized = true + + -- This will make global send back an event with material, cellBounds and player + core.sendGlobalEvent(D.e.WhatIsMyPhysicsData, { + object = object + }) + + local serialisedData = self:serialize() + serialisedData.initialized = self.initialized + core.sendGlobalEvent(D.e.PhysPropUpdReport, serialisedData) +end + +function PhysicsObject:updateMaterial(mat, recalcMass, recalcBuoyancy) + if recalcMass == nil then recalcMass = true end + if recalcBuoyancy == nil then recalcBuoyancy = true end + + -- print(omwself,"phys obj update material") + + self.material = mat + + -- Volume-based mass + if recalcMass then + local box = self.object:getBoundingBox() + local volume = (box.halfSize.x/D.GUtoM) * (box.halfSize.y/D.GUtoM) * (box.halfSize.z/D.GUtoM) --In meters^3 + local density = 25 -- In kg/m^3 -- Density is so low since bbox-based volume is very innacurate + if self.material and self.material == "Metal" then density = 50 end + local mass = volume * density -- In kg + if mass < 1 then mass = 1 end + self.mass = mass + end + + -- Material-based boyancy + if recalcBuoyancy then + self.buoyancy = 0.5 + if self.material == "Wood" or self.material == "Glass" or self.material == "Organic" then + self.buoyancy = 1.05 + elseif self.material == "Metal" then + self.buoyancy = 0.25 + end + end + + -- print("emitting mat upd event") + self.onMaterialUpdate:emit(self.material) +end + + +function PhysicsObject:reInit() + --print("Reinitialising a physics object for: ", self.object) + self.position = nil + self.rotation = nil + self.origin = nil + self.radius = nil + self:init(self.object, self) +end + +function PhysicsObject:updateProperties(props) + gutils.shallowMergeTables(self, props) + core.sendGlobalEvent(D.e.PhysPropUpdReport, self:serialize()) +end + + +function PhysicsObject:serialize() + return { + object = self.object, + position = self.position, + velocity = self.velocity, + radius = self.radius, + origin = self.origin, + rotation = self.rotation, + isSleeping = self.isSleeping, + bounce = self.bounce, + mass = self.mass, + culprit = self.culprit, + initialized = self.initialized, + ignorePhysObjectCollisions = self.ignorePhysObjectCollisions + } +end + +function PhysicsObject:serializeMotionData() + return { + object = self.object, + position = self.position, + velocity = self.velocity, + rotation = self.rotation + } +end + +function PhysicsObject:resetPosition(sleep) + if sleep == nil then sleep = true end + if self.initialPosition then self.position = self.initialPosition end + if self.initialRotation then self.rotation = self.initialRotation end + if sleep then self:sleep() end +end + +function PhysicsObject:getPersistentData() + return { + initialPosition = self.initialPosition, + initialRotation = self.initialRotation, + resetOnLoad = self.resetOnLoad, + ignorePhysObjectCollisions = self.ignorePhysObjectCollisions + } +end + +function PhysicsObject:loadPersistentData(data) + self:updateProperties(data) + if self.resetOnLoad then self:resetPosition(false) end +end + +function PhysicsObject:setPositionUnadjusted(position) + self.position = position + self.rotation:apply(self.origin) +end + +function PhysicsObject:sleep() + self.isSleeping = true + self:setCulprit(nil) + core.sendGlobalEvent(D.e.PhysPropUpdReport, {object = self.object, isSleeping = self.isSleeping}) +end + +function PhysicsObject:wakeUp() + self.isSleeping = false + self.sleepTimer = 0 -- Reset sleep timer when waking up + core.sendGlobalEvent(D.e.PhysPropUpdReport, {object = self.object, isSleeping = self.isSleeping}) +end + +function PhysicsObject:setCulprit(culprit) + self.culprit = culprit + core.sendGlobalEvent(D.e.PhysPropUpdReport, {object = self.object, culprit = self.culprit}) +end + +function PhysicsObject:applyImpulse(impulse, culprit) + -- print(self.mass) + self.velocity = self.velocity + impulse / self.mass + self:setCulprit(culprit) + self:wakeUp() -- Wake up the object when an impulse is applied +end + +function PhysicsObject:applyForce(force, culprit, wakeUp) + if wakeUp == nil then wakeUp = true end + if not wakeUp and self.isSleeping then return end + if not self.forceToApply then + self.forceToApply = util.vector3(0, 0, 0) + end + self.forceToApply = self.forceToApply + force + self:setCulprit(culprit) + if wakeUp then self:wakeUp() end -- Wake up the object when a force is applied +end + +function PhysicsObject:isCollidingWith(physObject) + local distance = (self.position - physObject.position):length() + return distance < (self.radius + physObject.radius) +end + +function PhysicsObject:handleCollision(hitResult) + local normal = hitResult.hitNormal + local velocity = self.velocity + local dot = velocity:dot(normal) + + local newInContact = true + local newInContactWith = hitResult.hitObject + local isNewContact = newInContact ~= self.inContact or newInContactWith ~= self.inContactWith + + self.inContact = newInContact + self.inContactWith = newInContactWith + + if dot < 0 then + -- Run collision callback + if isNewContact then + self.onCollision:emit(hitResult) + if hitResult.hitObject then + hitResult.hitObject:sendEvent(D.e.CollidingWithPhysObj, {other = self:serialize()}) + end + end + + -- Reflect velocity and apply bounce factor + self.velocity = -(normal * normal:dot(velocity) * 2 - velocity) + self.velocity = self.velocity * self.bounce + + -- Apply impact torque damping + self.angularVelocity = self.angularVelocity * ImpactAngVelDamping + + if not self.lockRotation then + -- Calculate torque from collision impact + local impactPoint = hitResult.hitPos + local relativePosition = impactPoint - self.position + local torque = relativePosition:cross(-self.velocity) + local tangVelocity = torque + local angularVeloctiy = tangVelocity/self.largestHalfExtent + + -- 6 (ImpactAngVelDamping) is a magic number that makes collistion look fun, essentially its (probably) related to moment of inertia which is not accounted for at all + self.angularVelocity = self.angularVelocity + angularVeloctiy * ImpactAngVelMult + + -- Clamp angular velocity to prevent it from growing uncontrollably + if self.angularVelocity:length() > MaxAngularVelocity then + self.angularVelocity = self.angularVelocity:normalize() * MaxAngularVelocity + end + end + + return true + else + -- Run intersection callback + if isNewContact then self.onIntersection:emit(hitResult) end + return false + end +end + +function PhysicsObject:handlePhysObjectCollision(physObject) + if physObject.culprit then self.culprit = physObject.culprit end + + local normal = (self.position - physObject.position):normalize() + --print(normal, physObject.radius) + local hitPos = physObject.position + normal*physObject.radius + + local relativeVelocity = self.velocity - physObject.velocity + local dot = relativeVelocity:dot(normal) + + if dot < 0 and math.abs(dot) > SleepSpeed then + -- print("Handling object collision", object1, object2, dot) + -- Calculate impulse magnitude + local impulseMagnitude = -(1 + math.min(self.bounce, physObject.bounce)) * dot / (1 / self.mass + 1 / physObject.mass) + + -- Apply impulses to both objects + local impulse = normal * impulseMagnitude + self.velocity = self.velocity + impulse / self.mass + --physObject.velocity = physObject.velocity - impulse / physObject.mass + + self:wakeUp() + self.onPhysObjectCollision:emit({ + hitPos = hitPos, + hitNormal = normal, + hitObject = physObject.object, + hitPhysObject = physObject + }) + + --data2:wakeUp() + end +end + + +local maxDt = 1/20 +function PhysicsObject:update(dt) + if self.disabled then return end + + if dt > maxDt then dt = maxDt end + + if not self.isSleeping then + -- Consume accumulated forces + if self.forceToApply and self.forceToApply:length() > 0 then + self.velocity = self.velocity + (self.forceToApply / self.mass) * dt + self.forceToApply = util.vector3(0, 0, 0) -- Reset accumulated forces after applying + end + + -- Apply Gravity + self.velocity = self.velocity + Gravity * dt + + -- Water Physics + local waterline = self.object.cell.waterLevel + if waterline and not PhysicsObject.isSleeping then + if self.position.z < waterline then + -- We are underwater + if not self.isUnderwater then + -- We just transitioned from above water to underwater + self.velocity = self.velocity * WaterHitVelDamping + end + self.isUnderwater = true + + -- Apply Buoyoncy + local buoyForce = Gravity * self.mass * -1 * self.buoyancy + self.velocity = self.velocity + buoyForce * dt + else + self.isUnderwater = false + end + end + + -- Apply drag + self.velocity = self.velocity * (1 - self.drag * dt) + + -- Calculate displacement + local displacement = self.velocity * dt + + -- Perform collision detection + local collided = false + local hitResult = nil + local rayStart = self.position + local rayEnd = rayStart + displacement + + if not self.ignoreWorldCollisions then + if self.collisionMode == "sphere" then + -- Use numeric bitmask 63 (AnyPhysical: World+Door+Actor+HeightMap+Projectile+Water) + hitResult = nearby.castRay(rayStart, rayEnd, { + radius = self.radius, + collisionType = 63 + }) + elseif self.collisionMode == "aabb" then + hitResult = phUtils.customRaycastAABB(rayStart, rayEnd, self.radius, self.object:getBoundingBox(), self.rotation, { ignore = self.object, collisionType = 63 }) + end + + if hitResult and hitResult.hit then + local isHitValid = true + if self.collisionFilter then + isHitValid = self.collisionFilter(hitResult) + end + + if isHitValid then + collided = self:handleCollision(hitResult) + if self.collisionMode == "sphere" then + -- Update position on collision to get close to a surface without penetrating + if collided then self.position = phUtils.calcSpherePosAtHit(self.position, displacement, hitResult.hitPos, self.radius) end + elseif self.collisionMode == "aabb" then + -- Dont update position on collision, to ensure no penetration + end + else + -- Filter rejected this hit (e.g. ghost air above dead actor) + -- We act as if no hit occurred so displacement can continue. + hitResult = nil + collided = false + end + end + end + + if not collided then + self.inContact = false + self.inContactWith = nil + self.position = rayEnd + end + + if not self.lastPosition then self.lastPosition = self.position end + self.actualVelocity = (self.position - self.lastPosition) / dt + + -- Apply angular drag to angular velocity + self.angularVelocity = self.angularVelocity * (1 - self.angularDrag * dt) + if self.lockRotation then self.angularVelocity = self.angularVelocity * 0 end -- Lock rotation if specified + + -- Update rotation based on angular velocity + local angularDisplacement = self.angularVelocity * dt * 0.01 + local rotationDelta = util.transform.rotate(angularDisplacement:length(), angularDisplacement:normalize()) + self.rotation = rotationDelta * self.rotation + + -- Realign when close to rest state + if self.realignWhenRested then + -- FIX ME; rest is now based on speed, not angular speed, so realignment will probably not work as intended + if not self.rotationInfluence then self.rotationInfluence = 0 end + local speed = self.actualVelocity:length() + if speed < SleepSpeed then + self.rotationInfluence = self.sleepTimer / SleepTime + if self.rotationInfluence > 0 then + self.rotation = phUtils.slerpRotation(self.rotation, util.transform.identity, self.rotationInfluence) + end + else + self.rotationInfluence = 0 + end + end + end + + if not self.isSleeping then + -- print("Not sleeping",omwself) + core.sendGlobalEvent(D.e.UpdateVisPos, self:serializeMotionData()) + end + + +end + +function PhysicsObject:trySleep(dt) + if self.isSleeping or not self.actualVelocity then return end + + local speed = self.actualVelocity:length() + if speed < SleepSpeed then + self.sleepTimer = self.sleepTimer + dt + if self.sleepTimer >= SleepTime then + self:sleep() + end + else + self.sleepTimer = 0 -- Reset sleep timer if velocity or angular velocity exceeds threshold + end + + self.lastPosition = self.position +end + +return PhysicsObject + + + + + + +