mirror of https://github.com/OpenMW/openmw.git
Lua: Animation bindings
parent
d1e79028e9
commit
a94add741e
@ -0,0 +1,365 @@
|
||||
#include <components/esm3/loadmgef.hpp>
|
||||
#include <components/esm3/loadstat.hpp>
|
||||
#include <components/lua/asyncpackage.hpp>
|
||||
#include <components/lua/luastate.hpp>
|
||||
#include <components/lua/utilpackage.hpp>
|
||||
#include <components/misc/resourcehelpers.hpp>
|
||||
#include <components/settings/values.hpp>
|
||||
|
||||
#include "../mwbase/environment.hpp"
|
||||
#include "../mwbase/mechanicsmanager.hpp"
|
||||
#include "../mwbase/world.hpp"
|
||||
|
||||
#include "../mwmechanics/character.hpp"
|
||||
|
||||
#include "../mwworld/esmstore.hpp"
|
||||
|
||||
#include "context.hpp"
|
||||
#include "luamanagerimp.hpp"
|
||||
#include "objectvariant.hpp"
|
||||
|
||||
#include "animationbindings.hpp"
|
||||
#include <array>
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
struct AnimationGroup;
|
||||
struct TextKeyCallback;
|
||||
}
|
||||
|
||||
namespace sol
|
||||
{
|
||||
template <>
|
||||
struct is_automagical<MWLua::AnimationGroup> : std::false_type
|
||||
{
|
||||
};
|
||||
template <>
|
||||
struct is_automagical<std::shared_ptr<MWLua::TextKeyCallback>> : std::false_type
|
||||
{
|
||||
};
|
||||
}
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
using BlendMask = MWRender::Animation::BlendMask;
|
||||
using BoneGroup = MWRender::Animation::BoneGroup;
|
||||
using Priority = MWMechanics::Priority;
|
||||
using AnimationPriorities = MWRender::Animation::AnimPriority;
|
||||
|
||||
MWWorld::Ptr getMutablePtrOrThrow(const ObjectVariant& variant)
|
||||
{
|
||||
if (variant.isLObject())
|
||||
throw std::runtime_error("Local scripts can only modify animations of the object they are attached to.");
|
||||
|
||||
MWWorld::Ptr ptr = variant.ptr();
|
||||
if (ptr.isEmpty())
|
||||
throw std::runtime_error("Invalid object");
|
||||
if (!ptr.getRefData().isEnabled())
|
||||
throw std::runtime_error("Can't use a disabled object");
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
MWWorld::Ptr getPtrOrThrow(const ObjectVariant& variant)
|
||||
{
|
||||
MWWorld::Ptr ptr = variant.ptr();
|
||||
if (ptr.isEmpty())
|
||||
throw std::runtime_error("Invalid object");
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
MWRender::Animation* getMutableAnimationOrThrow(const ObjectVariant& variant)
|
||||
{
|
||||
MWWorld::Ptr ptr = getMutablePtrOrThrow(variant);
|
||||
auto world = MWBase::Environment::get().getWorld();
|
||||
MWRender::Animation* anim = world->getAnimation(ptr);
|
||||
if (!anim)
|
||||
throw std::runtime_error("Object has no animation");
|
||||
return anim;
|
||||
}
|
||||
|
||||
const MWRender::Animation* getConstAnimationOrThrow(const ObjectVariant& variant)
|
||||
{
|
||||
MWWorld::Ptr ptr = getPtrOrThrow(variant);
|
||||
auto world = MWBase::Environment::get().getWorld();
|
||||
const MWRender::Animation* anim = world->getAnimation(ptr);
|
||||
if (!anim)
|
||||
throw std::runtime_error("Object has no animation");
|
||||
return anim;
|
||||
}
|
||||
|
||||
const ESM::Static* getStatic(const sol::object& staticOrID)
|
||||
{
|
||||
if (staticOrID.is<ESM::Static>())
|
||||
return staticOrID.as<const ESM::Static*>();
|
||||
else
|
||||
{
|
||||
ESM::RefId id = ESM::RefId::deserializeText(LuaUtil::cast<std::string_view>(staticOrID));
|
||||
return MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find(id);
|
||||
}
|
||||
}
|
||||
|
||||
std::string getStaticModelOrThrow(const sol::object& staticOrID)
|
||||
{
|
||||
const ESM::Static* static_ = getStatic(staticOrID);
|
||||
if (!static_)
|
||||
throw std::runtime_error("Invalid static");
|
||||
|
||||
return Misc::ResourceHelpers::correctMeshPath(static_->mModel);
|
||||
}
|
||||
|
||||
static AnimationPriorities getPriorityArgument(const sol::table& args)
|
||||
{
|
||||
auto asPriorityEnum = args.get<sol::optional<Priority>>("priority");
|
||||
if (asPriorityEnum)
|
||||
return asPriorityEnum.value();
|
||||
|
||||
auto asTable = args.get<sol::optional<sol::table>>("priority");
|
||||
if (asTable)
|
||||
{
|
||||
AnimationPriorities priorities = AnimationPriorities(Priority::Priority_Default);
|
||||
for (auto entry : asTable.value())
|
||||
{
|
||||
if (!entry.first.is<BoneGroup>() || !entry.second.is<Priority>())
|
||||
throw std::runtime_error("Priority table must consist of BoneGroup-Priority pairs only");
|
||||
auto group = entry.first.as<BoneGroup>();
|
||||
auto priority = entry.second.as<Priority>();
|
||||
if (group < 0 || group >= BoneGroup::Num_BoneGroups)
|
||||
throw std::runtime_error("Invalid bonegroup: " + std::to_string(group));
|
||||
priorities[group] = priority;
|
||||
}
|
||||
|
||||
return priorities;
|
||||
}
|
||||
|
||||
return Priority::Priority_Default;
|
||||
}
|
||||
|
||||
sol::table initAnimationPackage(const Context& context)
|
||||
{
|
||||
auto* lua = context.mLua;
|
||||
auto mechanics = MWBase::Environment::get().getMechanicsManager();
|
||||
auto world = MWBase::Environment::get().getWorld();
|
||||
|
||||
sol::table api(lua->sol(), sol::create);
|
||||
|
||||
api["PRIORITY"]
|
||||
= LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs<std::string_view, MWMechanics::Priority>({
|
||||
{ "Default", MWMechanics::Priority::Priority_Default },
|
||||
{ "WeaponLowerBody", MWMechanics::Priority::Priority_WeaponLowerBody },
|
||||
{ "SneakIdleLowerBody", MWMechanics::Priority::Priority_SneakIdleLowerBody },
|
||||
{ "SwimIdle", MWMechanics::Priority::Priority_SwimIdle },
|
||||
{ "Jump", MWMechanics::Priority::Priority_Jump },
|
||||
{ "Movement", MWMechanics::Priority::Priority_Movement },
|
||||
{ "Hit", MWMechanics::Priority::Priority_Hit },
|
||||
{ "Weapon", MWMechanics::Priority::Priority_Weapon },
|
||||
{ "Block", MWMechanics::Priority::Priority_Block },
|
||||
{ "Knockdown", MWMechanics::Priority::Priority_Knockdown },
|
||||
{ "Torch", MWMechanics::Priority::Priority_Torch },
|
||||
{ "Storm", MWMechanics::Priority::Priority_Storm },
|
||||
{ "Death", MWMechanics::Priority::Priority_Death },
|
||||
{ "Scripted", MWMechanics::Priority::Priority_Scripted },
|
||||
}));
|
||||
|
||||
api["BLEND_MASK"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs<std::string_view, BlendMask>({
|
||||
{ "LowerBody", BlendMask::BlendMask_LowerBody },
|
||||
{ "Torso", BlendMask::BlendMask_Torso },
|
||||
{ "LeftArm", BlendMask::BlendMask_LeftArm },
|
||||
{ "RightArm", BlendMask::BlendMask_RightArm },
|
||||
{ "UpperBody", BlendMask::BlendMask_UpperBody },
|
||||
{ "All", BlendMask::BlendMask_All },
|
||||
}));
|
||||
|
||||
api["BONE_GROUP"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs<std::string_view, BoneGroup>({
|
||||
{ "LowerBody", BoneGroup::BoneGroup_LowerBody },
|
||||
{ "Torso", BoneGroup::BoneGroup_Torso },
|
||||
{ "LeftArm", BoneGroup::BoneGroup_LeftArm },
|
||||
{ "RightArm", BoneGroup::BoneGroup_RightArm },
|
||||
}));
|
||||
|
||||
api["hasAnimation"] = [world](const sol::object& object) -> bool {
|
||||
return world->getAnimation(getPtrOrThrow(ObjectVariant(object))) != nullptr;
|
||||
};
|
||||
|
||||
// equivalent to MWScript's SkipAnim
|
||||
api["skipAnimationThisFrame"] = [mechanics](const sol::object& object) {
|
||||
MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object));
|
||||
// This sets a flag that is only used during the update pass, so
|
||||
// there's no need to queue
|
||||
mechanics->skipAnimation(ptr);
|
||||
};
|
||||
|
||||
api["getTextKeyTime"] = [](const sol::object& object, std::string_view key) -> sol::optional<float> {
|
||||
float time = getConstAnimationOrThrow(ObjectVariant(object))->getTextKeyTime(key);
|
||||
if (time >= 0.f)
|
||||
return time;
|
||||
return sol::nullopt;
|
||||
};
|
||||
api["isPlaying"] = [](const sol::object& object, std::string_view groupname) {
|
||||
return getConstAnimationOrThrow(ObjectVariant(object))->isPlaying(groupname);
|
||||
};
|
||||
api["getCurrentTime"] = [](const sol::object& object, std::string_view groupname) -> sol::optional<float> {
|
||||
float time = getConstAnimationOrThrow(ObjectVariant(object))->getCurrentTime(groupname);
|
||||
if (time >= 0.f)
|
||||
return time;
|
||||
return sol::nullopt;
|
||||
};
|
||||
api["isLoopingAnimation"] = [](const sol::object& object, std::string_view groupname) {
|
||||
return getConstAnimationOrThrow(ObjectVariant(object))->isLoopingAnimation(groupname);
|
||||
};
|
||||
api["cancel"] = [](const sol::object& object, std::string_view groupname) {
|
||||
return getMutableAnimationOrThrow(ObjectVariant(object))->disable(groupname);
|
||||
};
|
||||
api["setLoopingEnabled"] = [](const sol::object& object, std::string_view groupname, bool enabled) {
|
||||
return getMutableAnimationOrThrow(ObjectVariant(object))->setLoopingEnabled(groupname, enabled);
|
||||
};
|
||||
// MWRender::Animation::getInfo can also return the current speed multiplier, but this is never used.
|
||||
api["getCompletion"] = [](const sol::object& object, std::string_view groupname) -> sol::optional<float> {
|
||||
float completion = 0.f;
|
||||
if (getConstAnimationOrThrow(ObjectVariant(object))->getInfo(groupname, &completion))
|
||||
return completion;
|
||||
return sol::nullopt;
|
||||
};
|
||||
api["getLoopCount"] = [](const sol::object& object, std::string groupname) -> sol::optional<size_t> {
|
||||
size_t loops = 0;
|
||||
if (getConstAnimationOrThrow(ObjectVariant(object))->getInfo(groupname, nullptr, nullptr, &loops))
|
||||
return loops;
|
||||
return sol::nullopt;
|
||||
};
|
||||
api["getSpeed"] = [](const sol::object& object, std::string groupname) -> sol::optional<float> {
|
||||
float speed = 0.f;
|
||||
if (getConstAnimationOrThrow(ObjectVariant(object))->getInfo(groupname, nullptr, &speed, nullptr))
|
||||
return speed;
|
||||
return sol::nullopt;
|
||||
};
|
||||
api["setSpeed"] = [](const sol::object& object, std::string groupname, float speed) {
|
||||
getMutableAnimationOrThrow(ObjectVariant(object))->adjustSpeedMult(groupname, speed);
|
||||
};
|
||||
api["getActiveGroup"] = [](const sol::object& object, MWRender::BoneGroup boneGroup) -> std::string_view {
|
||||
return getConstAnimationOrThrow(ObjectVariant(object))->getActiveGroup(boneGroup);
|
||||
};
|
||||
|
||||
// Clears out the animation queue, and cancel any animation currently playing from the queue
|
||||
api["clearAnimationQueue"] = [mechanics](const sol::object& object, bool clearScripted) {
|
||||
MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object));
|
||||
mechanics->clearAnimationQueue(ptr, clearScripted);
|
||||
};
|
||||
|
||||
// Extended variant of MWScript's PlayGroup and LoopGroup
|
||||
api["playQueued"] = sol::overload(
|
||||
[mechanics](const sol::object& object, const std::string& groupname, const sol::table& options) {
|
||||
int numberOfLoops = options.get_or("loops", std::numeric_limits<int>::max());
|
||||
float speed = options.get_or("speed", 1.f);
|
||||
std::string startKey = options.get_or<std::string>("startkey", "start");
|
||||
std::string stopKey = options.get_or<std::string>("stopkey", "stop");
|
||||
bool forceLoop = options.get_or("forceloop", false);
|
||||
|
||||
MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object));
|
||||
mechanics->playAnimationGroupLua(ptr, groupname, numberOfLoops, speed, startKey, stopKey, forceLoop);
|
||||
},
|
||||
[mechanics](const sol::object& object, const std::string& groupname) {
|
||||
MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object));
|
||||
mechanics->playAnimationGroupLua(
|
||||
ptr, groupname, std::numeric_limits<int>::max(), 1, "start", "stop", false);
|
||||
});
|
||||
|
||||
api["playBlended"] = [](const sol::object& object, std::string_view groupname, const sol::table& options) {
|
||||
int loops = options.get_or("loops", 0);
|
||||
MWRender::Animation::AnimPriority priority = getPriorityArgument(options);
|
||||
BlendMask blendMask = options.get_or("blendmask", BlendMask::BlendMask_All);
|
||||
bool autoDisable = options.get_or("autodisable", true);
|
||||
float speed = options.get_or("speed", 1.0f);
|
||||
std::string start = options.get_or<std::string>("startkey", "start");
|
||||
std::string stop = options.get_or<std::string>("stopkey", "stop");
|
||||
float startpoint = options.get_or("startpoint", 0.0f);
|
||||
bool forceLoop = options.get_or("forceloop", false);
|
||||
|
||||
auto animation = getMutableAnimationOrThrow(ObjectVariant(object));
|
||||
animation->play(groupname, priority, blendMask, autoDisable, speed, start, stop, startpoint, loops,
|
||||
forceLoop || animation->isLoopingAnimation(groupname));
|
||||
};
|
||||
|
||||
api["hasGroup"] = [](const sol::object& object, std::string_view groupname) -> bool {
|
||||
const MWRender::Animation* anim = getConstAnimationOrThrow(ObjectVariant(object));
|
||||
return anim->hasAnimation(groupname);
|
||||
};
|
||||
|
||||
// Note: This checks the nodemap, and does not read the scene graph itself, and so should be thread safe.
|
||||
api["hasBone"] = [](const sol::object& object, std::string_view bonename) -> bool {
|
||||
const MWRender::Animation* anim = getConstAnimationOrThrow(ObjectVariant(object));
|
||||
return anim->getNode(bonename) != nullptr;
|
||||
};
|
||||
|
||||
api["addVfx"] = sol::overload(
|
||||
[context](const sol::object& object, const sol::object& staticOrID) {
|
||||
context.mLuaManager->addAction(
|
||||
[object = ObjectVariant(object), model = getStaticModelOrThrow(staticOrID)] {
|
||||
MWRender::Animation* anim = getMutableAnimationOrThrow(object);
|
||||
anim->addEffect(model, "");
|
||||
},
|
||||
"addVfxAction");
|
||||
},
|
||||
[context](const sol::object& object, const sol::object& staticOrID, const sol::table& options) {
|
||||
context.mLuaManager->addAction(
|
||||
[object = ObjectVariant(object), model = getStaticModelOrThrow(staticOrID),
|
||||
effectId = options.get_or<std::string>("vfxId", ""), loop = options.get_or("loop", false),
|
||||
bonename = options.get_or<std::string>("bonename", ""),
|
||||
particleTexture = options.get_or<std::string>("particleTextureOverride", "")] {
|
||||
MWRender::Animation* anim = getMutableAnimationOrThrow(ObjectVariant(object));
|
||||
|
||||
anim->addEffect(model, effectId, loop, bonename, particleTexture);
|
||||
},
|
||||
"addVfxAction");
|
||||
});
|
||||
|
||||
api["removeVfx"] = [context](const sol::object& object, std::string_view effectId) {
|
||||
context.mLuaManager->addAction(
|
||||
[object = ObjectVariant(object), effectId = std::string(effectId)] {
|
||||
MWRender::Animation* anim = getMutableAnimationOrThrow(object);
|
||||
anim->removeEffect(effectId);
|
||||
},
|
||||
"removeVfxAction");
|
||||
};
|
||||
|
||||
api["removeAllVfx"] = [context](const sol::object& object) {
|
||||
context.mLuaManager->addAction(
|
||||
[object = ObjectVariant(object)] {
|
||||
MWRender::Animation* anim = getMutableAnimationOrThrow(object);
|
||||
anim->removeEffects();
|
||||
},
|
||||
"removeVfxAction");
|
||||
};
|
||||
|
||||
return LuaUtil::makeReadOnly(api);
|
||||
}
|
||||
|
||||
sol::table initCoreVfxBindings(const Context& context)
|
||||
{
|
||||
sol::state_view& lua = context.mLua->sol();
|
||||
sol::table api(lua, sol::create);
|
||||
auto world = MWBase::Environment::get().getWorld();
|
||||
|
||||
api["spawn"] = sol::overload(
|
||||
[world, context](const sol::object& staticOrID, const osg::Vec3f& worldPos) {
|
||||
auto model = getStaticModelOrThrow(staticOrID);
|
||||
context.mLuaManager->addAction(
|
||||
[world, model, worldPos]() { world->spawnEffect(model, "", worldPos); }, "openmw.vfx.spawn");
|
||||
},
|
||||
[world, context](const sol::object& staticOrID, const osg::Vec3f& worldPos, const sol::table& options) {
|
||||
auto model = getStaticModelOrThrow(staticOrID);
|
||||
|
||||
bool magicVfx = options.get_or("mwMagicVfx", true);
|
||||
std::string textureOverride = options.get_or<std::string>("particleTextureOverride", "");
|
||||
float scale = options.get_or("scale", 1.f);
|
||||
|
||||
context.mLuaManager->addAction(
|
||||
[world, model, textureOverride, worldPos, scale, magicVfx]() {
|
||||
world->spawnEffect(model, textureOverride, worldPos, scale, magicVfx);
|
||||
},
|
||||
"openmw.vfx.spawn");
|
||||
});
|
||||
|
||||
return api;
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
#ifndef MWLUA_ANIMATIONBINDINGS_H
|
||||
#define MWLUA_ANIMATIONBINDINGS_H
|
||||
|
||||
#include <sol/forward.hpp>
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
sol::table initAnimationPackage(const Context& context);
|
||||
sol::table initCoreVfxBindings(const Context& context);
|
||||
}
|
||||
|
||||
#endif // MWLUA_ANIMATIONBINDINGS_H
|
@ -0,0 +1,42 @@
|
||||
#ifndef GAME_RENDER_ANIMATIONPRIORITY_H
|
||||
#define GAME_RENDER_ANIMATIONPRIORITY_H
|
||||
|
||||
#include "blendmask.hpp"
|
||||
#include "bonegroup.hpp"
|
||||
|
||||
namespace MWRender
|
||||
{
|
||||
/// Holds an animation priority value for each BoneGroup.
|
||||
struct AnimPriority
|
||||
{
|
||||
/// Convenience constructor, initialises all priorities to the same value.
|
||||
AnimPriority(int priority)
|
||||
{
|
||||
for (unsigned int i = 0; i < sNumBlendMasks; ++i)
|
||||
mPriority[i] = priority;
|
||||
}
|
||||
|
||||
bool operator==(const AnimPriority& other) const
|
||||
{
|
||||
for (unsigned int i = 0; i < sNumBlendMasks; ++i)
|
||||
if (other.mPriority[i] != mPriority[i])
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
int& operator[](BoneGroup n) { return mPriority[n]; }
|
||||
|
||||
const int& operator[](BoneGroup n) const { return mPriority[n]; }
|
||||
|
||||
bool contains(int priority) const
|
||||
{
|
||||
for (unsigned int i = 0; i < sNumBlendMasks; ++i)
|
||||
if (priority == mPriority[i])
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
int mPriority[sNumBlendMasks];
|
||||
};
|
||||
}
|
||||
#endif
|
@ -0,0 +1,22 @@
|
||||
#ifndef GAME_RENDER_BLENDMASK_H
|
||||
#define GAME_RENDER_BLENDMASK_H
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace MWRender
|
||||
{
|
||||
enum BlendMask
|
||||
{
|
||||
BlendMask_LowerBody = 1 << 0,
|
||||
BlendMask_Torso = 1 << 1,
|
||||
BlendMask_LeftArm = 1 << 2,
|
||||
BlendMask_RightArm = 1 << 3,
|
||||
|
||||
BlendMask_UpperBody = BlendMask_Torso | BlendMask_LeftArm | BlendMask_RightArm,
|
||||
|
||||
BlendMask_All = BlendMask_LowerBody | BlendMask_UpperBody
|
||||
};
|
||||
/* This is the number of *discrete* blend masks. */
|
||||
static constexpr size_t sNumBlendMasks = 4;
|
||||
}
|
||||
#endif
|
@ -0,0 +1,16 @@
|
||||
#ifndef GAME_RENDER_BONEGROUP_H
|
||||
#define GAME_RENDER_BONEGROUP_H
|
||||
|
||||
namespace MWRender
|
||||
{
|
||||
enum BoneGroup
|
||||
{
|
||||
BoneGroup_LowerBody = 0,
|
||||
BoneGroup_Torso,
|
||||
BoneGroup_LeftArm,
|
||||
BoneGroup_RightArm,
|
||||
|
||||
Num_BoneGroups
|
||||
};
|
||||
}
|
||||
#endif
|
@ -0,0 +1,8 @@
|
||||
Interface AnimationController
|
||||
=============================
|
||||
|
||||
.. include:: version.rst
|
||||
|
||||
.. raw:: html
|
||||
:file: generated_html/scripts_omw_mechanics_animationcontroller.html
|
||||
|
@ -0,0 +1,7 @@
|
||||
Package openmw.animation
|
||||
========================
|
||||
|
||||
.. include:: version.rst
|
||||
|
||||
.. raw:: html
|
||||
:file: generated_html/openmw_animation.html
|
@ -0,0 +1,145 @@
|
||||
local anim = require('openmw.animation')
|
||||
local self = require('openmw.self')
|
||||
|
||||
local playBlendedHandlers = {}
|
||||
local function onPlayBlendedAnimation(groupname, options)
|
||||
for i = #playBlendedHandlers, 1, -1 do
|
||||
if playBlendedHandlers[i](groupname, options) == false then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function playBlendedAnimation(groupname, options)
|
||||
onPlayBlendedAnimation(groupname, options)
|
||||
if options.skip then
|
||||
return
|
||||
end
|
||||
anim.playBlended(self, groupname, options)
|
||||
end
|
||||
|
||||
local textKeyHandlers = {}
|
||||
local function onAnimationTextKey(groupname, key)
|
||||
local handlers = textKeyHandlers[groupname]
|
||||
if handlers then
|
||||
for i = #handlers, 1, -1 do
|
||||
if handlers[i](groupname, key) == false then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
handlers = textKeyHandlers['']
|
||||
if handlers then
|
||||
for i = #handlers, 1, -1 do
|
||||
if handlers[i](groupname, key) == false then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local initialized = false
|
||||
|
||||
local function onUpdate(dt)
|
||||
-- The script is loaded before the actor's CharacterController object is initialized, therefore
|
||||
-- we have to delay this initialization step or the call won't have any effect.
|
||||
if not initialized then
|
||||
self:_enableLuaAnimations(true)
|
||||
initialized = true
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
engineHandlers = {
|
||||
_onPlayAnimation = playBlendedAnimation,
|
||||
_onAnimationTextKey = onAnimationTextKey,
|
||||
onUpdate = onUpdate,
|
||||
},
|
||||
|
||||
interfaceName = 'AnimationController',
|
||||
---
|
||||
-- Animation controller interface
|
||||
-- @module AnimationController
|
||||
-- @usage local anim = require('openmw.animation')
|
||||
-- local I = require('openmw.interfaces')
|
||||
--
|
||||
-- -- play spellcast animation
|
||||
-- I.AnimationController.playBlendedAnimation('spellcast', { startkey = 'self start', stopkey = 'self stop', priority = {
|
||||
-- [anim.BONE_GROUP.RightArm] = anim.PRIORITY.Weapon,
|
||||
-- [anim.BONE_GROUP.LeftArm] = anim.PRIORITY.Weapon,
|
||||
-- [anim.BONE_GROUP.Torso] = anim.PRIORITY.Weapon,
|
||||
-- [anim.BONE_GROUP.LowerBody] = anim.PRIORITY.WeaponLowerBody
|
||||
-- } })
|
||||
--
|
||||
-- @usage -- react to the spellcast release textkey
|
||||
-- I.AnimationController.addTextKeyHandler('spellcast', function(groupname, key)
|
||||
-- -- Note, Lua is 1-indexed so have to subtract 1 less than the length of 'release'
|
||||
-- if key.sub(key, #key - 6) == 'release' then
|
||||
-- print('Abra kadabra!')
|
||||
-- end
|
||||
-- end)
|
||||
--
|
||||
-- @usage -- Add a text key handler that will react to all keys
|
||||
-- I.AnimationController.addTextKeyHandler('', function(groupname, key)
|
||||
-- if key.sub(key, #key - 2) == 'hit' and not key.sub(key, #key - 7) == ' min hit' then
|
||||
-- print('Hit!')
|
||||
-- end
|
||||
-- end)
|
||||
--
|
||||
-- @usage -- Make a handler that changes player attack speed based on current fatigue
|
||||
-- I.AnimationController.addPlayBlendedAnimationHandler(function (groupname, options)
|
||||
-- local stop = options.stopkey
|
||||
-- if #stop > 10 and stop.sub(stop, #stop - 10) == ' max attack' then
|
||||
-- -- This is an attack wind up animation, scale its speed by attack
|
||||
-- local fatigue = Actor.stats.dynamic.fatigue(self)
|
||||
-- local factor = 1 - fatigue.current / fatigue.base
|
||||
-- speed = 1 - factor * 0.8
|
||||
-- options.speed = speed
|
||||
-- end
|
||||
-- end)
|
||||
--
|
||||
|
||||
interface = {
|
||||
--- Interface version
|
||||
-- @field [parent=#AnimationController] #number version
|
||||
version = 0,
|
||||
|
||||
--- AnimationController Package
|
||||
-- @type Package
|
||||
|
||||
--- Make this actor play an animation. Makes a call to @{openmw.animation#playBlended}, after invoking handlers added through addPlayBlendedAnimationHandler
|
||||
-- @function [parent=#AnimationController] playBlendedAnimation
|
||||
-- @param #string groupname The animation group to be played
|
||||
-- @param #table options The table of play options that will be passed to @{openmw.animation#playBlended}
|
||||
playBlendedAnimation = playBlendedAnimation,
|
||||
|
||||
--- Add new playBlendedAnimation handler for this actor
|
||||
-- If `handler(groupname, options)` returns false, other handlers for
|
||||
-- the call will be skipped.
|
||||
-- @function [parent=#AnimationController] addPlayBlendedAnimationHandler
|
||||
-- @param #function handler The handler.
|
||||
addPlayBlendedAnimationHandler = function(handler)
|
||||
playBlendedHandlers[#playBlendedHandlers + 1] = handler
|
||||
end,
|
||||
|
||||
--- Add new text key handler for this actor
|
||||
-- While playing, some animations emit text key events. Register a handle to listen for all
|
||||
-- text key events associated with this actor's animations.
|
||||
-- If `handler(groupname, key)` returns false, other handlers for
|
||||
-- the call will be skipped.
|
||||
-- @function [parent=#AnimationController] addTextKeyHandler
|
||||
-- @param #string groupname Name of the animation group to listen to keys for. If the empty string or nil, all keys will be received
|
||||
-- @param #function handler The handler.
|
||||
addTextKeyHandler = function(groupname, handler)
|
||||
if not groupname then
|
||||
groupname = ""
|
||||
end
|
||||
local handlers = textKeyHandlers[groupname]
|
||||
if handlers == nil then
|
||||
handlers = {}
|
||||
textKeyHandlers[groupname] = handlers
|
||||
end
|
||||
handlers[#handlers + 1] = handler
|
||||
end,
|
||||
}
|
||||
}
|
@ -0,0 +1,255 @@
|
||||
---
|
||||
-- `openmw.animation` defines functions that allow control of character animations
|
||||
-- Note that for some methods, such as @{openmw.animation#playBlended} you should use the associated methods on the
|
||||
-- [AnimationController](interface_animation.html) interface rather than invoking this API directly.
|
||||
-- @module animation
|
||||
-- @usage local anim = require('openmw.animation')
|
||||
|
||||
--- Possible @{#Priority} values
|
||||
-- @field [parent=#animation] #Priority PRIORITY
|
||||
|
||||
--- `animation.PRIORITY`
|
||||
-- @type Priority
|
||||
-- @field #number Default "0"
|
||||
-- @field #number WeaponLowerBody "1"
|
||||
-- @field #number SneakIdleLowerBody "2"
|
||||
-- @field #number SwimIdle "3"
|
||||
-- @field #number Jump "4"
|
||||
-- @field #number Movement "5"
|
||||
-- @field #number Hit "6"
|
||||
-- @field #number Weapon "7"
|
||||
-- @field #number Block "8"
|
||||
-- @field #number Knockdown "9"
|
||||
-- @field #number Torch "10"
|
||||
-- @field #number Storm "11"
|
||||
-- @field #number Death "12"
|
||||
-- @field #number Scripted "13" Special priority used by scripted animations. When any animation with this priority is present, all animations without this priority are paused.
|
||||
|
||||
--- Possible @{#BlendMask} values
|
||||
-- @field [parent=#animation] #BlendMask BLEND_MASK
|
||||
|
||||
--- `animation.BLEND_MASK`
|
||||
-- @type BlendMask
|
||||
-- @field #number LowerBody "1" All bones from 'Bip01 pelvis' and below
|
||||
-- @field #number Torso "2" All bones from 'Bip01 Spine1' and up, excluding arms
|
||||
-- @field #number LeftArm "4" All bones from 'Bip01 L Clavicle' and out
|
||||
-- @field #number RightArm "8" All bones from 'Bip01 R Clavicle' and out
|
||||
-- @field #number UpperBody "14" All bones from 'Bip01 Spine1' and up, including arms
|
||||
-- @field #number All "15" All bones
|
||||
|
||||
--- Possible @{#BoneGroup} values
|
||||
-- @field [parent=#animation] #BoneGroup BONE_GROUP
|
||||
|
||||
--- `animation.BONE_GROUP`
|
||||
-- @type BoneGroup
|
||||
-- @field #number LowerBody "1" All bones from 'Bip01 pelvis' and below
|
||||
-- @field #number Torso "2" All bones from 'Bip01 Spine1' and up, excluding arms
|
||||
-- @field #number LeftArm "3" All bones from 'Bip01 L Clavicle' and out
|
||||
-- @field #number RightArm "4" All bones from 'Bip01 R Clavicle' and out
|
||||
|
||||
|
||||
---
|
||||
-- Check if the object has an animation object or not
|
||||
-- @function [parent=#animation] hasAnimation
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @return #boolean
|
||||
|
||||
---
|
||||
-- Skips animations for one frame, equivalent to mwscript's SkipAnim
|
||||
-- Can be used only in local scripts on self.
|
||||
-- @function [parent=#animation] skipAnimationThisFrame
|
||||
-- @param openmw.core#GameObject actor
|
||||
|
||||
---
|
||||
-- Get the absolute position within the animation track of the given text key
|
||||
-- @function [parent=#animation] getTextKeyTime
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @param #string text key
|
||||
-- @return #number
|
||||
|
||||
---
|
||||
-- Check if the given animation group is currently playing
|
||||
-- @function [parent=#animation] isPlaying
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @param #string groupname
|
||||
-- @return #boolean
|
||||
|
||||
---
|
||||
-- Get the current absolute time of the given animation group if it is playing, or -1 if it is not playing.
|
||||
-- @function [parent=#animation] getCurrentTime
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @param #string groupname
|
||||
-- @return #number
|
||||
|
||||
---
|
||||
-- Check whether the animation is a looping animation or not. This is determined by a combination
|
||||
-- of groupname, some of which are hardcoded to be looping, and the presence of loop start/stop keys.
|
||||
-- The groupnames that are hardcoded as looping are the following, as well as per-weapon-type suffixed variants of each.
|
||||
-- "walkforward", "walkback", "walkleft", "walkright", "swimwalkforward", "swimwalkback", "swimwalkleft", "swimwalkright",
|
||||
-- "runforward", "runback", "runleft", "runright", "swimrunforward", "swimrunback", "swimrunleft", "swimrunright",
|
||||
-- "sneakforward", "sneakback", "sneakleft", "sneakright", "turnleft", "turnright", "swimturnleft", "swimturnright",
|
||||
-- "spellturnleft", "spellturnright", "torch", "idle", "idle2", "idle3", "idle4", "idle5", "idle6", "idle7", "idle8",
|
||||
-- "idle9", "idlesneak", "idlestorm", "idleswim", "jump", "inventoryhandtohand", "inventoryweapononehand",
|
||||
-- "inventoryweapontwohand", "inventoryweapontwowide"
|
||||
-- @function [parent=#animation] isLoopingAnimation
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @param #string groupname
|
||||
-- @return #boolean
|
||||
|
||||
|
||||
---
|
||||
-- Cancels and removes the animation group from the list of active animations
|
||||
-- Can be used only in local scripts on self.
|
||||
-- @function [parent=#animation] cancel
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @param #string groupname
|
||||
|
||||
---
|
||||
-- Enables or disables looping for the given animation group. Looping is enabled by default.
|
||||
-- Can be used only in local scripts on self.
|
||||
-- @function [parent=#animation] setLoopingEnabled
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @param #string groupname
|
||||
-- @param #boolean enabled
|
||||
|
||||
---
|
||||
-- Returns the completion of the animation, or nil if the animation group is not active.
|
||||
-- @function [parent=#animation] getCompletion
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @param #string groupname
|
||||
-- @return #number, #nil
|
||||
|
||||
---
|
||||
-- Returns the remaining number of loops, not counting the current loop, or nil if the animation group is not active.
|
||||
-- @function [parent=#animation] getLoopCount
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @param #string groupname
|
||||
-- @return #number, #nil
|
||||
|
||||
---
|
||||
-- Get the current playback speed of an animation group, or nil if the animation group is not active.
|
||||
-- @function [parent=#animation] getSpeed
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @param #string groupname
|
||||
-- @return #number, #nil
|
||||
|
||||
---
|
||||
-- Modifies the playback speed of an animation group.
|
||||
-- Note that this is not sticky and only affects the speed until the currently playing sequence ends.
|
||||
-- Can be used only in local scripts on self.
|
||||
-- @function [parent=#animation] setSpeed
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @param #string groupname
|
||||
-- @param #number speed The new animation speed, where speed=1 is normal speed.
|
||||
|
||||
---
|
||||
-- Clears all animations currently in the animation queue. This affects animations played by mwscript, @{openmw.animation#playQueued}, and ai packages, but does not affect animations played using @{openmw.animation#playBlended}.
|
||||
-- Can be used only in local scripts on self.
|
||||
-- @function [parent=#animation] clearAnimationQueue
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @param #boolean clearScripted whether to keep animation with priority Scripted or not.
|
||||
|
||||
---
|
||||
-- Acts as a slightly extended version of MWScript's LoopGroup. Plays this animation exclusively
|
||||
-- until it ends, or the queue is cleared using #clearAnimationQueue. Use #clearAnimationQueue and the `startkey` option
|
||||
-- to imitate the behavior of LoopGroup's play modes.
|
||||
-- Can be used only in local scripts on self.
|
||||
-- @function [parent=#animation] playQueued
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @param #string groupname
|
||||
-- @param #table options A table of play options. Can contain:
|
||||
--
|
||||
-- * `loops` - a number >= 0, the number of times the animation should loop after the first play (default: infinite).
|
||||
-- * `speed` - a floating point number >= 0, the speed at which the animation should play (default: 1);
|
||||
-- * `startkey` - the animation key at which the animation should start (default: "start")
|
||||
-- * `stopkey` - the animation key at which the animation should end (default: "stop")
|
||||
-- * `forceloop` - a boolean, to set if the animation should loop even if it's not a looping animation (default: false)
|
||||
--
|
||||
-- @usage -- Play death1 without waiting. Equivalent to playgroup, death1, 1
|
||||
-- anim.clearAnimationQueue(self, false)
|
||||
-- anim.playQueued(self, 'death1')
|
||||
--
|
||||
-- @usage -- Play an animation group with custom start/stop keys
|
||||
-- anim.clearAnimationQueue(self, false)
|
||||
-- anim.playQueued(self, 'spellcast', { startkey = 'self start', stopkey = 'self stop' })
|
||||
--
|
||||
|
||||
---
|
||||
-- Play an animation directly. You probably want to use the [AnimationController](interface_animation.html) interface, which will trigger relevant handlers,
|
||||
-- instead of calling this directly. Note that the still hardcoded character controller may at any time and for any reason alter
|
||||
-- or cancel currently playing animations, so making your own calls to this function either directly or through the [AnimationController](interface_animation.html)
|
||||
-- interface may be of limited utility. For now, use openmw.animation#playQueued to script your own animations.
|
||||
-- Can be used only in local scripts on self.
|
||||
-- @function [parent=#animation] playBlended
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @param #string groupname
|
||||
-- @param #table options A table of play options. Can contain:
|
||||
--
|
||||
-- * `loops` - a number >= 0, the number of times the animation should loop after the first play (default: 0).
|
||||
-- * `priority` - Either a single #Priority value that will be assigned to all bone groups. Or a table mapping bone groups to its priority (default: PRIORITY.Default).
|
||||
-- * `blendMask` - A mask of which bone groups to include in the animation (Default: BLEND_MASK.All.
|
||||
-- * `autodisable` - If true, the animation will be immediately removed upon finishing, which means information will not be possible to query once completed. (Default: true)
|
||||
-- * `speed` - a floating point number >= 0, the speed at which the animation should play (default: 1)
|
||||
-- * `startkey` - the animation key at which the animation should start (default: "start")
|
||||
-- * `stopkey` - the animation key at which the animation should end (default: "stop")
|
||||
-- * `startpoint` - a floating point number 0 <= value <= 1, starting completion of the animation (default: 0)
|
||||
-- * `forceloop` - a boolean, to set if the animation should loop even if it's not a looping animation (default: false)
|
||||
|
||||
---
|
||||
-- Check if the actor's animation has the given animation group or not.
|
||||
-- @function [parent=#animation] hasGroup
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @param #string groupname
|
||||
-- @return #boolean
|
||||
|
||||
---
|
||||
-- Check if the actor's skeleton has the given bone or not
|
||||
-- @function [parent=#animation] hasBone
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @param #string bonename
|
||||
-- @return #boolean
|
||||
|
||||
---
|
||||
-- Get the current active animation for a bone group
|
||||
-- @function [parent=#animation] getActiveGroup
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @param #number bonegroup Bone group enum, see @{openmw.animation#BONE_GROUP}
|
||||
-- @return #string
|
||||
|
||||
---
|
||||
-- Plays a VFX on the actor.
|
||||
-- Can be used only in local scripts on self.
|
||||
-- @function [parent=#animation] addVfx
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @param #any static @{openmw.core#StaticRecord} or #string ID
|
||||
-- @param #table options optional table of parameters. Can contain:
|
||||
--
|
||||
-- * `loop` - boolean, if true the effect will loop until removed (default: 0).
|
||||
-- * `bonename` - name of the bone to attach the vfx to. (default: "")
|
||||
-- * `particle` - name of the particle texture to use. (default: "")
|
||||
-- * `vfxId` - a string ID that can be used to remove the effect later, using #removeVfx, and to avoid duplicate effects. The default value of "" can have duplicates. To avoid interaction with the engine, use unique identifiers unrelated to magic effect IDs. The engine uses this identifier to add and remove magic effects based on what effects are active on the actor. If this is set equal to the @{openmw.core#MagicEffectId} identifier of the magic effect being added, for example core.magic.EFFECT_TYPE.FireDamage, then the engine will remove it once the fire damage effect on the actor reaches 0. (Default: "").
|
||||
--
|
||||
-- @usage local mgef = core.magic.effects[myEffectName]
|
||||
-- anim.addVfx(self, 'VFX_Hands', {bonename = 'Bip01 L Hand', particle = mgef.particle, loop = mgef.continuousVfx, vfxId = mgef.id..'_myuniquenamehere'})
|
||||
-- -- later:
|
||||
-- anim.removeVfx(self, mgef.id..'_myuniquenamehere')
|
||||
--
|
||||
|
||||
---
|
||||
-- Removes a specific VFX
|
||||
-- Can be used only in local scripts on self.
|
||||
-- @function [parent=#animation] removeVfx
|
||||
-- @param openmw.core#GameObject actor
|
||||
-- @param #number vfxId an integer ID that uniquely identifies the VFX to remove
|
||||
|
||||
---
|
||||
-- Removes all vfx from the actor
|
||||
-- Can be used only in local scripts on self.
|
||||
-- @function [parent=#animation] removeAllVfx
|
||||
-- @param openmw.core#GameObject actor
|
||||
|
||||
|
||||
|
||||
|
||||
return nil
|
||||
|
Loading…
Reference in New Issue