Something went wrong. Try again.
OpenMW is an open implementation of the Morrowind engine
Something went wrong. Try again.
21 kB · 472 lines
Lua
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473local testing = require('testing_util')local self = require('openmw.self')local util = require('openmw.util')local core = require('openmw.core')local input = require('openmw.input')local types = require('openmw.types')local nearby = require('openmw.nearby')local camera = require('openmw.camera')local matchers = require('matchers')
types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Controls, false)types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Fighting, false)types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Jumping, false)types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Looking, false)types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Magic, false)types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.VanityMode, false)types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.ViewMode, false)
local function rotate(object, targetPitch, targetYaw) local endTime = core.getSimulationTime() + 1 while core.getSimulationTime() < endTime do object.controls.jump = false object.controls.run = true object.controls.movement = 0 object.controls.sideMovement = 0 if targetPitch ~= nil then object.controls.pitchChange = util.normalizeAngle(targetPitch - object.rotation:getPitch()) * 0.5 end if targetYaw ~= nil then object.controls.yawChange = util.normalizeAngle(targetYaw - object.rotation:getYaw()) * 0.5 end coroutine.yield() endend
local function rotateByYaw(object, target) rotate(object, nil, target)end
local function rotateByPitch(object, target) rotate(object, target, nil)end
testing.registerLocalTest('rotating player with controls.yawChange should change rotation', function() local initialAlphaXZ, initialGammaXZ = self.rotation:getAnglesXZ() local initialAlphaZYX, initialBetaZYX, initialGammaZYX = self.rotation:getAnglesZYX()
local targetYaw = math.rad(90) rotateByYaw(self, targetYaw)
testing.expectEqualWithDelta(self.rotation:getYaw(), targetYaw, 0.05, 'Incorrect yaw rotation')
local alpha1, gamma1 = self.rotation:getAnglesXZ() testing.expectEqualWithDelta(alpha1, initialAlphaXZ, 0.05, 'Alpha rotation in XZ convention should not change') testing.expectEqualWithDelta(gamma1, targetYaw, 0.05, 'Incorrect gamma rotation in XZ convention')
local alpha2, beta2, gamma2 = self.rotation:getAnglesZYX() testing.expectEqualWithDelta(alpha2, targetYaw, 0.05, 'Incorrect alpha rotation in ZYX convention') testing.expectEqualWithDelta(beta2, initialBetaZYX, 0.05, 'Beta rotation in ZYX convention should not change') testing.expectEqualWithDelta(gamma2, initialGammaZYX, 0.05, 'Gamma rotation in ZYX convention should not change') end)
testing.registerLocalTest('rotating player with controls.pitchChange should change rotation', function() local initialAlphaXZ, initialGammaXZ = self.rotation:getAnglesXZ() local initialAlphaZYX, initialBetaZYX, initialGammaZYX = self.rotation:getAnglesZYX()
local targetPitch = math.rad(90) rotateByPitch(self, targetPitch)
testing.expectEqualWithDelta(self.rotation:getPitch(), targetPitch, 0.05, 'Incorrect pitch rotation')
local alpha1, gamma1 = self.rotation:getAnglesXZ() testing.expectEqualWithDelta(alpha1, targetPitch, 0.05, 'Incorrect alpha rotation in XZ convention') testing.expectEqualWithDelta(gamma1, initialGammaXZ, 0.05, 'Gamma rotation in XZ convention should not change')
local alpha2, beta2, gamma2 = self.rotation:getAnglesZYX() testing.expectEqualWithDelta(alpha2, initialAlphaZYX, 0.05, 'Alpha rotation in ZYX convention should not change') testing.expectEqualWithDelta(beta2, initialBetaZYX, 0.05, 'Beta rotation in ZYX convention should not change') testing.expectEqualWithDelta(gamma2, targetPitch, 0.05, 'Incorrect gamma rotation in ZYX convention') end)
testing.registerLocalTest('rotating player with controls.pitchChange and controls.yawChange should change rotation', function() local targetPitch = math.rad(-30) local targetYaw = math.rad(-60) rotate(self, targetPitch, targetYaw)
testing.expectEqualWithDelta(self.rotation:getPitch(), targetPitch, 0.05, 'Incorrect pitch rotation') testing.expectEqualWithDelta(self.rotation:getYaw(), targetYaw, 0.05, 'Incorrect yaw rotation')
local alpha1, gamma1 = self.rotation:getAnglesXZ() testing.expectEqualWithDelta(alpha1, targetPitch, 0.05, 'Incorrect alpha rotation in XZ convention') testing.expectEqualWithDelta(gamma1, targetYaw, 0.05, 'Incorrect gamma rotation in XZ convention')
local alpha2, beta2, gamma2 = self.rotation:getAnglesZYX() testing.expectEqualWithDelta(alpha2, math.rad(-56), 0.05, 'Incorrect alpha rotation in ZYX convention') testing.expectEqualWithDelta(beta2, math.rad(-25), 0.05, 'Incorrect beta rotation in ZYX convention') testing.expectEqualWithDelta(gamma2, math.rad(-16), 0.05, 'Incorrect gamma rotation in ZYX convention') end)
testing.registerLocalTest('rotating player should not lead to nan rotation', function() local rotation = math.sqrt(2) local endTime = core.getSimulationTime() + 3 while core.getSimulationTime() < endTime do self.controls.jump = false self.controls.run = true self.controls.movement = 0 self.controls.sideMovement = 0 self.controls.pitchChange = rotation self.controls.yawChange = rotation coroutine.yield()
local alpha1, gamma1 = self.rotation:getAnglesXZ() testing.expectThat(alpha1, matchers.isNotNan(), 'Alpha rotation in XZ convention is nan') testing.expectThat(gamma1, matchers.isNotNan(), 'Gamma rotation in XZ convention is nan')
local alpha2, beta2, gamma2 = self.rotation:getAnglesZYX() testing.expectThat(alpha2, matchers.isNotNan(), 'Alpha rotation in ZYX convention is nan') testing.expectThat(beta2, matchers.isNotNan(), 'Beta rotation in ZYX convention is nan') testing.expectThat(gamma2, matchers.isNotNan(), 'Gamma rotation in ZYX convention is nan') end end)
testing.registerLocalTest('player forward running', function() local startPos = self.position local endTime = core.getSimulationTime() + 1 while core.getSimulationTime() < endTime do self.controls.jump = false self.controls.run = true self.controls.movement = 1 self.controls.sideMovement = 0 self.controls.yawChange = 0 coroutine.yield() end local direction, distance = (self.position - startPos):normalize() testing.expectGreaterThan(distance, 0, 'Run forward, distance') testing.expectEqualWithDelta(direction.x, 0, 0.1, 'Run forward, X coord') testing.expectEqualWithDelta(direction.y, 1, 0.1, 'Run forward, Y coord') end)
testing.registerLocalTest('player diagonal walking', function() local startPos = self.position local endTime = core.getSimulationTime() + 1 while core.getSimulationTime() < endTime do self.controls.jump = false self.controls.run = false self.controls.movement = -1 self.controls.sideMovement = -1 self.controls.yawChange = 0 coroutine.yield() end local direction, distance = (self.position - startPos):normalize() testing.expectGreaterThan(distance, 0, 'Walk diagonally, distance') testing.expectEqualWithDelta(direction.x, -0.707, 0.1, 'Walk diagonally, X coord') testing.expectEqualWithDelta(direction.y, -0.707, 0.1, 'Walk diagonally, Y coord') end)
testing.registerLocalTest('findPath', function() local src = util.vector3(4096, 4096, 1745) local dst = util.vector3(4500, 4500, 1745.95263671875) local options = { agentBounds = types.Actor.getPathfindingAgentBounds(self), includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.Swim, areaCosts = { water = 1, door = 2, ground = 1, pathgrid = 1, }, destinationTolerance = 1, } local status, path = nearby.findPath(src, dst, options) testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Status') testing.expectLessOrEqual((path[#path] - dst):length(), 1, 'Last path point ' .. testing.formatActualExpected(path[#path], dst)) testing.expectThat(path, matchers.equalTo({ matchers.closeToVector(util.vector3(4096, 4096, 1746.27099609375), 1e-1), matchers.closeToVector(util.vector3(4500, 4500, 1745.95263671875), 1e-1), })) end)
testing.registerLocalTest('findPath with checkpoints', function() local src = util.vector3(4096, 4096, 1745) local dst = util.vector3(4500, 4500, 1745.95263671875) local options = { agentBounds = types.Actor.getPathfindingAgentBounds(self), includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.Swim, areaCosts = { water = 1, door = 2, ground = 1, pathgrid = 1, }, destinationTolerance = 1, checkpoints = { util.vector3(4200, 4100, 1750), }, } local status, path = nearby.findPath(src, dst, options) testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Status') testing.expectLessOrEqual((path[#path] - dst):length(), 1, 'Last path point ' .. testing.formatActualExpected(path[#path], dst)) testing.expectThat(path, matchers.equalTo({ matchers.closeToVector(util.vector3(4096, 4096, 1746.27099609375), 1e-1), matchers.closeToVector(util.vector3(4200, 4100, 1749.5076904296875), 1e-1), matchers.closeToVector(util.vector3(4500, 4500, 1745.95263671875), 1e-1), })) end)
testing.registerLocalTest('findRandomPointAroundCircle', function() local position = util.vector3(4096, 4096, 1745.95263671875) local maxRadius = 100 local options = { agentBounds = types.Actor.getPathfindingAgentBounds(self), includeFlags = nearby.NAVIGATOR_FLAGS.Walk, } local result = nearby.findRandomPointAroundCircle(position, maxRadius, options) testing.expectGreaterThan((result - position):length(), 1, 'Random point ' .. testing.formatActualExpected(result, position)) end)
testing.registerLocalTest('castNavigationRay', function() local src = util.vector3(4096, 4096, 1745) local dst = util.vector3(4500, 4500, 1745.95263671875) local options = { agentBounds = types.Actor.getPathfindingAgentBounds(self), includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.Swim, } local result = nearby.castNavigationRay(src, dst, options) testing.expectLessOrEqual((result - dst):length(), 1, 'Navigation hit point ' .. testing.formatActualExpected(result, dst)) end)
testing.registerLocalTest('findNearestNavMeshPosition', function() local position = util.vector3(4096, 4096, 2000) local options = { agentBounds = types.Actor.getPathfindingAgentBounds(self), includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.Swim, searchAreaHalfExtents = util.vector3(1000, 1000, 1000), } local result = nearby.findNearestNavMeshPosition(position, options) local expected = util.vector3(4096, 4096, 1746.27099609375) testing.expectLessOrEqual((result - expected):length(), 1, 'Navigation mesh position ' .. testing.formatActualExpected(result, expected)) end)
testing.registerLocalTestStep('camera.getFocusRay should report the object under the crosshair', function() local target = nil for _, actor in ipairs(nearby.actors) do if actor.id ~= self.id and (actor.position - self.position):length() < 200 then target = actor end end testing.expect(target, 'An NPC should stand ahead of the player')
local ray = camera.getFocusRay() testing.expect(ray.hit, 'Focus ray should hit the NPC ahead') testing.expect(ray.hitObject, 'Focus ray should report the NPC it hits') testing.expectEqual(ray.hitObject.id, target.id, 'Focus ray should hit the NPC ahead') testing.expectLessOrEqual((ray.hitPos - target.position):length(), 200, 'Focus ray hit position ' .. testing.formatActualExpected(ray.hitPos, target.position)) testing.expectGreaterThan((ray.hitPos - self.position):length(), 50, 'Focus ray hit position ' .. testing.formatActualExpected(ray.hitPos, self.position)) end)
testing.registerLocalTest('player memory limit', function() local ok, err = pcall(function() local str = 'a' while true do str = str .. str end end) testing.expectEqual(ok, false, 'Script reaching memory limit should fail') testing.expectEqual(err, 'not enough memory') end)
testing.registerLocalTestStep('player approaches and attacks a creature', function() camera.setMode(camera.MODE.ThirdPerson)
local options = { agentBounds = types.Actor.getPathfindingAgentBounds(self), } local duration = 10 local endTime = core.getSimulationTime() + duration local nextTime = 0 local use = self.ATTACK_TYPE.NoAttack
local attributes = {} for k, v in pairs(types.Actor.stats.attributes) do attributes[k] = v(self).base end
types.Actor.stats.attributes.speed(self).base = 100 types.Actor.stats.attributes.strength(self).base = 1000 types.Actor.stats.attributes.agility(self).base = 1000
local weaponId = 'basic_dagger1h' local weapon = types.Actor.inventory(self):find(weaponId)
local isWeapon = function(actual) if actual == nil then return weaponId .. ' is not found' end if actual.recordId ~= weaponId then return 'found weapon recordId does not match expected: actual=' .. tostring(actual.id) .. ', expected=' .. weaponId end return '' end testing.expectThat(weapon, isWeapon)
types.Actor.setEquipment(self, {[types.Actor.EQUIPMENT_SLOT.CarriedRight] = weapon})
coroutine.yield()
testing.expectThat(types.Actor.getEquipment(self, types.Actor.EQUIPMENT_SLOT.CarriedRight), isWeapon)
types.Actor.setStance(self, types.Actor.STANCE.Weapon)
local previousHealth = nil local targetActor = nil
while true do local time = core.getSimulationTime() testing.expectLessOrEqual(time, endTime, 'Did not damage any targets in ' .. duration .. ' seconds')
if targetActor ~= nil and types.Actor.stats.dynamic.health(targetActor).current < previousHealth then print('Dealt ' .. (previousHealth - types.Actor.stats.dynamic.health(targetActor).current) .. ' damage to ' .. tostring(targetActor)) break end
local minDistance = nil for i, actor in ipairs(nearby.actors) do if actor.id ~= self.id then local distance = (actor.position - self.position):length() if minDistance == nil or minDistance > distance then minDistance = distance targetActor = actor end end end testing.expectNotEqual(targetActor, nil, 'No attack targets found')
previousHealth = types.Actor.stats.dynamic.health(targetActor).current
local destination = nil if minDistance > 100 then local status, path = nearby.findPath(self.position, targetActor.position, options)
testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Failed to find path from ' .. tostring(self.position) .. ' to ' .. tostring(targetActor.position))
destination = path[2] use = self.ATTACK_TYPE.NoAttack
self.controls.run = true self.controls.movement = 1 else local halfExtents = types.Actor.getPathfindingAgentBounds(targetActor).halfExtents destination = targetActor.position - util.vector3(0, 0, halfExtents.z)
if nextTime < time then if use == 0 then use = self.ATTACK_TYPE.Any nextTime = time + 0.5 else use = self.ATTACK_TYPE.NoAttack end end end self.controls.use = use
local direction = destination - self.position direction = direction:normalize() self.controls.yawChange = util.normalizeAngle(math.atan2(direction.x, direction.y) - self.rotation:getYaw()) self.controls.pitchChange = util.normalizeAngle(math.asin(util.clamp(-direction.z, -1, 1)) - self.rotation:getPitch())
coroutine.yield() end
for k, v in pairs(types.Actor.stats.attributes) do v(self).base = attributes[k] end end)
testing.registerLocalTestStep('equipped weapon damages a stationary target', function(targetActor) camera.setMode(camera.MODE.ThirdPerson) self.controls.movement = 0 self.controls.sideMovement = 0 self.controls.use = self.ATTACK_TYPE.NoAttack
local attributes = types.Actor.stats.attributes local strength = attributes.strength(self).base local agility = attributes.agility(self).base attributes.strength(self).base = 1000 attributes.agility(self).base = 1000
local weaponId = 'basic_dagger1h' local setupDeadline = core.getRealTime() + 10 local weapon while weapon == nil do testing.expectLessOrEqual(core.getRealTime(), setupDeadline, 'Attack setup timed out') weapon = types.Actor.inventory(self):find(weaponId) coroutine.yield() end
types.Actor.setEquipment(self, {[types.Actor.EQUIPMENT_SLOT.CarriedRight] = weapon}) types.Actor.setStance(self, types.Actor.STANCE.Weapon)
while not targetActor:isValid() or not types.Actor.isOnGround(self) or not types.Actor.isOnGround(targetActor) or types.Actor.getEquipment(self, types.Actor.EQUIPMENT_SLOT.CarriedRight) ~= weapon or types.Actor.getStance(self) ~= types.Actor.STANCE.Weapon do testing.expectLessOrEqual(core.getRealTime(), setupDeadline, 'Attack setup timed out') coroutine.yield() end
local previousHealth = types.Actor.stats.dynamic.health(targetActor).current local endTime = core.getSimulationTime() + 10 local nextTime = 0 local use = self.ATTACK_TYPE.NoAttack while types.Actor.stats.dynamic.health(targetActor).current >= previousHealth do local time = core.getSimulationTime() testing.expectLessOrEqual(time, endTime, 'Attack did not damage the target') testing.expectLessOrEqual((targetActor.position - self.position):length(), 100, 'Target left melee reach')
if nextTime < time then if use == self.ATTACK_TYPE.NoAttack then use = self.ATTACK_TYPE.Any nextTime = time + 0.5 else use = self.ATTACK_TYPE.NoAttack end end self.controls.use = use
local halfExtents = types.Actor.getPathfindingAgentBounds(targetActor).halfExtents local destination = targetActor.position - util.vector3(0, 0, halfExtents.z) local direction = destination - self.position direction = direction:normalize() self.controls.yawChange = util.normalizeAngle(math.atan2(direction.x, direction.y) - self.rotation:getYaw()) self.controls.pitchChange = util.normalizeAngle(math.asin(util.clamp(-direction.z, -1, 1)) - self.rotation:getPitch())
coroutine.yield() end
self.controls.use = self.ATTACK_TYPE.NoAttack attributes.strength(self).base = strength attributes.agility(self).base = agility end)
return { engineHandlers = { onFrame = testing.updateLocal, }, eventHandlers = testing.localEventHandlers,}