Working on stuff

This commit is contained in:
Vladislav Khorev 2026-07-10 14:09:31 +03:00
parent 17d903fd52
commit 07f45ad1e1
11 changed files with 176 additions and 70 deletions

View File

@ -109,7 +109,7 @@ std::unique_ptr<Character> Character::createFromState(CharacterState st, Rendere
return c; return c;
} }
void Character::setTarget(const Eigen::Vector3f& target, std::function<void()> onArrived) { void Character::setTarget(const Eigen::Vector3f& target, std::function<void()> onArrived, std::string callbackName) {
Eigen::Vector3f normalizedTarget(target.x(), 0.f, target.z()); Eigen::Vector3f normalizedTarget(target.x(), 0.f, target.z());
const bool sameRequestedTarget = const bool sameRequestedTarget =
@ -123,8 +123,9 @@ void Character::setTarget(const Eigen::Vector3f& target, std::function<void()> o
return; return;
} }
state.requestedWalkTarget = normalizedTarget; state.requestedWalkTarget = normalizedTarget;
state.onArrivedCallback = std::move(onArrived); state.onArrivedCallback = std::move(onArrived);
state.onArrivedCallbackName = std::move(callbackName);
if (pathPlanner) { if (pathPlanner) {
state.pathWaypoints = pathPlanner(state.position, normalizedTarget); state.pathWaypoints = pathPlanner(state.position, normalizedTarget);
@ -140,6 +141,7 @@ void Character::setTarget(const Eigen::Vector3f& target, std::function<void()> o
state.walkTarget = Eigen::Vector3f(state.position.x(), 0.f, state.position.z()); state.walkTarget = Eigen::Vector3f(state.position.x(), 0.f, state.position.z());
state.onArrivedCallback = nullptr; state.onArrivedCallback = nullptr;
state.onArrivedCallbackName.clear();
return; return;
} }
@ -178,6 +180,7 @@ void Character::forceReplan() {
state.walkTarget = Eigen::Vector3f(state.position.x(), 0.f, state.position.z()); state.walkTarget = Eigen::Vector3f(state.position.x(), 0.f, state.position.z());
state.onArrivedCallback = nullptr; state.onArrivedCallback = nullptr;
state.onArrivedCallbackName.clear();
} }
void Character::setTexture(std::shared_ptr<ZL::Texture> texture) { void Character::setTexture(std::shared_ptr<ZL::Texture> texture) {
@ -294,6 +297,7 @@ void Character::update(int64_t deltaMs, const CharacterResolver& resolver) {
if (!hasNextWaypoint && state.onArrivedCallback) { if (!hasNextWaypoint && state.onArrivedCallback) {
auto cb = std::move(state.onArrivedCallback); auto cb = std::move(state.onArrivedCallback);
state.onArrivedCallback = nullptr; state.onArrivedCallback = nullptr;
state.onArrivedCallbackName.clear();
cb(); cb();
} }

View File

@ -52,7 +52,9 @@ public:
void setPathPlanner(PathPlanner planner); void setPathPlanner(PathPlanner planner);
// ---- Navigation (path data lives in state; pathPlanner lives here) ---- // ---- Navigation (path data lives in state; pathPlanner lives here) ----
void setTarget(const Eigen::Vector3f& target, std::function<void()> onArrived = nullptr); // callbackName, if given, is the global Lua function name backing onArrived —
// stored in state.onArrivedCallbackName so it survives save/load (see ScriptEngine::rehydrateNamedCallback).
void setTarget(const Eigen::Vector3f& target, std::function<void()> onArrived = nullptr, std::string callbackName = "");
void forceReplan(); void forceReplan();
void stopInPlace() { state.stopInPlace(); } void stopInPlace() { state.stopInPlace(); }

View File

@ -21,6 +21,7 @@ void CharacterState::stopInPlace() {
pathWaypoints.clear(); pathWaypoints.clear();
currentWaypointIndex = 0; currentWaypointIndex = 0;
onArrivedCallback = nullptr; onArrivedCallback = nullptr;
onArrivedCallbackName.clear();
} }
void CharacterState::setHp(float newHp) { void CharacterState::setHp(float newHp) {
@ -52,6 +53,8 @@ void CharacterState::save(nlohmann::json& out) const
out["pathWaypoints"] = std::move(wpArr); out["pathWaypoints"] = std::move(wpArr);
out["currentWaypointIndex"] = currentWaypointIndex; out["currentWaypointIndex"] = currentWaypointIndex;
out["homeDriftCheckTimer"] = homeDriftCheckTimer; out["homeDriftCheckTimer"] = homeDriftCheckTimer;
if (!onArrivedCallbackName.empty())
out["onArrivedCallbackName"] = onArrivedCallbackName;
} }
void CharacterState::load(const nlohmann::json& in) void CharacterState::load(const nlohmann::json& in)
@ -84,6 +87,7 @@ void CharacterState::load(const nlohmann::json& in)
} }
currentWaypointIndex = in.value("currentWaypointIndex", currentWaypointIndex); currentWaypointIndex = in.value("currentWaypointIndex", currentWaypointIndex);
homeDriftCheckTimer = in.value("homeDriftCheckTimer", homeDriftCheckTimer); homeDriftCheckTimer = in.value("homeDriftCheckTimer", homeDriftCheckTimer);
onArrivedCallbackName = in.value("onArrivedCallbackName", std::string());
} }
} // namespace ZL } // namespace ZL

View File

@ -116,6 +116,11 @@ public:
std::function<void()> onDeathAnimComplete; std::function<void()> onDeathAnimComplete;
std::function<void(float, float)> onHpChanged; std::function<void(float, float)> onHpChanged;
// Global Lua function name backing onArrivedCallback, if it was sourced from a
// script (see ScriptEngine::rehydrateNamedCallback). Serialized so the callback
// can be re-looked-up by name after a save/load; empty if none or non-Lua.
std::string onArrivedCallbackName;
// --- Asset / creation info (used to reload visuals from a saved state) --- // --- Asset / creation info (used to reload visuals from a saved state) ---
CharacterCreationInfo creationInfo; CharacterCreationInfo creationInfo;

View File

@ -20,6 +20,7 @@
#include <emscripten.h> #include <emscripten.h>
#endif #endif
#include "GameConstants.h" #include "GameConstants.h"
namespace ZL namespace ZL

View File

@ -1995,6 +1995,24 @@ namespace ZL
if (in.contains("scriptGlobals")) { if (in.contains("scriptGlobals")) {
scriptEngine.loadScriptGlobals(in["scriptGlobals"]); scriptEngine.loadScriptGlobals(in["scriptGlobals"]);
} }
// Re-wire in-flight, one-shot Lua callbacks (walk-arrival / animation-complete)
// that were serialized by name. Must run after scriptEngine has already run the
// location's script (guaranteed: Location::setup() always runs before load())
// and after the states above are restored.
if (player && !player->state.onArrivedCallbackName.empty()) {
scriptEngine.rehydrateNamedCallback(player->state.onArrivedCallbackName, player->state.onArrivedCallback);
}
for (auto& npc : npcs) {
if (npc && !npc->state.onArrivedCallbackName.empty()) {
scriptEngine.rehydrateNamedCallback(npc->state.onArrivedCallbackName, npc->state.onArrivedCallback);
}
}
for (auto& intObj : interactiveObjects) {
if (intObj.state.isAnimating && intObj.state.animTask && !intObj.state.animTask->onCompleteName.empty()) {
scriptEngine.rehydrateNamedCallback(intObj.state.animTask->onCompleteName, intObj.state.animTask->onComplete);
}
}
} }
} // namespace ZL } // namespace ZL

View File

@ -11,6 +11,54 @@
namespace ZL { namespace ZL {
namespace {
// Wraps a sol::protected_function into a std::function<void()>, invoking it and
// logging any Lua-side error to stderr. Shared by every binding that stores a
// callback for later, one-shot invocation (npc_walk_to, move_object, etc.) and by
// rehydrateNamedCallback, which re-wraps a callback looked up by name after load.
std::function<void()> wrapLuaCallback(sol::protected_function fn) {
return [fn]() mutable {
auto result = fn();
if (!result.valid()) {
sol::error err = result;
std::cerr << "[script] callback error: " << err.what() << "\n";
}
};
}
// Reverse-looks-up the global name a Lua function is registered under, by
// comparing raw function pointers via lua_topointer. Returns "" if fn isn't
// (or is no longer) reachable as a plain global — e.g. an anonymous function,
// or one stored only in a local/upvalue. This lets callback-accepting bindings
// remember a callback "by name" for save/load without changing the Lua-facing
// API (scripts still just pass a function reference).
std::string findGlobalFunctionName(lua_State* L, const sol::protected_function& fn) {
if (!fn.valid()) return {};
fn.push(L);
const void* target = lua_topointer(L, -1);
lua_pop(L, 1);
if (!target) return {};
std::string found;
lua_pushglobaltable(L); // stack: [_G]
lua_pushnil(L); // stack: [_G, nil]
while (lua_next(L, -2) != 0) { // stack: [_G, key, value]
if (lua_type(L, -2) == LUA_TSTRING && lua_isfunction(L, -1) &&
lua_topointer(L, -1) == target) {
found = lua_tostring(L, -2);
lua_pop(L, 2); // pop value, key
break;
}
lua_pop(L, 1); // pop value, keep key for next iteration
}
lua_pop(L, 1); // pop _G
return found;
}
} // anonymous namespace
struct ScriptEngine::Impl { struct ScriptEngine::Impl {
sol::state lua; sol::state lua;
std::unordered_map<std::string, sol::protected_function> triggerEnterCallbacks; std::unordered_map<std::string, sol::protected_function> triggerEnterCallbacks;
@ -45,7 +93,7 @@ namespace ZL {
// on_arrived is an optional Lua function called when the NPC reaches the target. // on_arrived is an optional Lua function called when the NPC reaches the target.
// It can call npc_walk_to again (or anything else) to chain behaviour. // It can call npc_walk_to again (or anything else) to chain behaviour.
api.set_function("npc_walk_to", api.set_function("npc_walk_to",
[loc](int index, float x, float y, float z, sol::object on_arrived) { [loc, this_impl = impl.get()](int index, float x, float y, float z, sol::object on_arrived) {
auto& npcs = loc->npcs; auto& npcs = loc->npcs;
if (index < 0 || index >= static_cast<int>(npcs.size())) { if (index < 0 || index >= static_cast<int>(npcs.size())) {
std::cerr << "[script] npc_walk_to: index " << index std::cerr << "[script] npc_walk_to: index " << index
@ -53,18 +101,16 @@ namespace ZL {
return; return;
} }
std::function<void()> cb; std::function<void()> cb;
std::string cbName;
if (on_arrived.is<sol::protected_function>()) { if (on_arrived.is<sol::protected_function>()) {
sol::protected_function fn = on_arrived.as<sol::protected_function>(); sol::protected_function fn = on_arrived.as<sol::protected_function>();
cb = [fn]() mutable { cb = wrapLuaCallback(fn);
auto result = fn(); cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
if (!result.valid()) { if (cbName.empty())
sol::error err = result; std::cerr << "[script] npc_walk_to: on_arrived callback has no global name, will not survive save/load\n";
std::cerr << "[script] on_arrived error: " << err.what() << "\n";
}
};
} }
npcs[index]->state.homePosition = Eigen::Vector3f(x, 0.f, z); npcs[index]->state.homePosition = Eigen::Vector3f(x, 0.f, z);
npcs[index]->setTarget(Eigen::Vector3f(x, y, z), std::move(cb)); npcs[index]->setTarget(Eigen::Vector3f(x, y, z), std::move(cb), std::move(cbName));
}); });
@ -83,21 +129,19 @@ namespace ZL {
}); });
api.set_function("player_walk_to", api.set_function("player_walk_to",
[loc](float x, float y, float z, sol::object on_arrived) { [loc, this_impl = impl.get()](float x, float y, float z, sol::object on_arrived) {
std::function<void()> cb; std::function<void()> cb;
std::string cbName;
if (on_arrived.is<sol::protected_function>()) { if (on_arrived.is<sol::protected_function>()) {
sol::protected_function fn = on_arrived.as<sol::protected_function>(); sol::protected_function fn = on_arrived.as<sol::protected_function>();
cb = [fn]() mutable { cb = wrapLuaCallback(fn);
auto result = fn(); cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
if (!result.valid()) { if (cbName.empty())
sol::error err = result; std::cerr << "[script] player_walk_to: on_arrived callback has no global name, will not survive save/load\n";
std::cerr << "[script] on_arrived error: " << err.what() << "\n";
}
};
} }
loc->player->state.homePosition = Eigen::Vector3f(x, 0.f, z); loc->player->state.homePosition = Eigen::Vector3f(x, 0.f, z);
loc->player->setTarget(Eigen::Vector3f(x, y, z), std::move(cb)); loc->player->setTarget(Eigen::Vector3f(x, y, z), std::move(cb), std::move(cbName));
}); });
@ -556,7 +600,7 @@ namespace ZL {
// move_object(name, x, y, z, duration_sec [, on_complete]) // move_object(name, x, y, z, duration_sec [, on_complete])
api.set_function("move_object", api.set_function("move_object",
[loc](const std::string& name, float x, float y, float z, [loc, this_impl = impl.get()](const std::string& name, float x, float y, float z,
float durationSec, sol::object onComplete) { float durationSec, sol::object onComplete) {
for (auto& intObj : loc->interactiveObjects) { for (auto& intObj : loc->interactiveObjects) {
if (intObj.loadedObject.name != name) continue; if (intObj.loadedObject.name != name) continue;
@ -565,17 +609,15 @@ namespace ZL {
return; return;
} }
std::function<void()> cb; std::function<void()> cb;
std::string cbName;
if (onComplete.is<sol::protected_function>()) { if (onComplete.is<sol::protected_function>()) {
sol::protected_function fn = onComplete.as<sol::protected_function>(); sol::protected_function fn = onComplete.as<sol::protected_function>();
cb = [fn]() mutable { cb = wrapLuaCallback(fn);
auto res = fn(); cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
if (!res.valid()) { if (cbName.empty())
sol::error err = res; std::cerr << "[script] move_object: on_complete callback has no global name, will not survive save/load\n";
std::cerr << "[script] move_object on_complete error: " << err.what() << "\n";
}
};
} }
intObj.moveTo(Eigen::Vector3f(x, y, z), durationSec, std::move(cb)); intObj.moveTo(Eigen::Vector3f(x, y, z), durationSec, std::move(cb), std::move(cbName));
return; return;
} }
std::cerr << "[script] move_object: object '" << name << "' not found\n"; std::cerr << "[script] move_object: object '" << name << "' not found\n";
@ -583,7 +625,7 @@ namespace ZL {
// rotate_object(name, angle_deg, duration_sec [, on_complete]) // rotate_object(name, angle_deg, duration_sec [, on_complete])
api.set_function("rotate_object", api.set_function("rotate_object",
[loc](const std::string& name, float angleDeg, [loc, this_impl = impl.get()](const std::string& name, float angleDeg,
float durationSec, sol::object onComplete) { float durationSec, sol::object onComplete) {
for (auto& intObj : loc->interactiveObjects) { for (auto& intObj : loc->interactiveObjects) {
if (intObj.loadedObject.name != name) continue; if (intObj.loadedObject.name != name) continue;
@ -592,18 +634,16 @@ namespace ZL {
return; return;
} }
std::function<void()> cb; std::function<void()> cb;
std::string cbName;
if (onComplete.is<sol::protected_function>()) { if (onComplete.is<sol::protected_function>()) {
sol::protected_function fn = onComplete.as<sol::protected_function>(); sol::protected_function fn = onComplete.as<sol::protected_function>();
cb = [fn]() mutable { cb = wrapLuaCallback(fn);
auto res = fn(); cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
if (!res.valid()) { if (cbName.empty())
sol::error err = res; std::cerr << "[script] rotate_object: on_complete callback has no global name, will not survive save/load\n";
std::cerr << "[script] rotate_object on_complete error: " << err.what() << "\n";
}
};
} }
const float angleRad = angleDeg * static_cast<float>(M_PI) / 180.f; const float angleRad = angleDeg * static_cast<float>(M_PI) / 180.f;
intObj.rotateTo(intObj.state.rotationY + angleRad, durationSec, std::move(cb)); intObj.rotateTo(intObj.state.rotationY + angleRad, durationSec, std::move(cb), std::move(cbName));
return; return;
} }
std::cerr << "[script] rotate_object: object '" << name << "' not found\n"; std::cerr << "[script] rotate_object: object '" << name << "' not found\n";
@ -611,7 +651,7 @@ namespace ZL {
// fade_object(name, target_alpha, duration_sec [, on_complete]) // fade_object(name, target_alpha, duration_sec [, on_complete])
api.set_function("fade_object", api.set_function("fade_object",
[loc](const std::string& name, float targetAlpha, [loc, this_impl = impl.get()](const std::string& name, float targetAlpha,
float durationSec, sol::object onComplete) { float durationSec, sol::object onComplete) {
for (auto& intObj : loc->interactiveObjects) { for (auto& intObj : loc->interactiveObjects) {
if (intObj.loadedObject.name != name) continue; if (intObj.loadedObject.name != name) continue;
@ -620,17 +660,15 @@ namespace ZL {
return; return;
} }
std::function<void()> cb; std::function<void()> cb;
std::string cbName;
if (onComplete.is<sol::protected_function>()) { if (onComplete.is<sol::protected_function>()) {
sol::protected_function fn = onComplete.as<sol::protected_function>(); sol::protected_function fn = onComplete.as<sol::protected_function>();
cb = [fn]() mutable { cb = wrapLuaCallback(fn);
auto res = fn(); cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
if (!res.valid()) { if (cbName.empty())
sol::error err = res; std::cerr << "[script] fade_object: on_complete callback has no global name, will not survive save/load\n";
std::cerr << "[script] fade_object on_complete error: " << err.what() << "\n";
}
};
} }
intObj.fadeTo(targetAlpha, durationSec, std::move(cb)); intObj.fadeTo(targetAlpha, durationSec, std::move(cb), std::move(cbName));
return; return;
} }
std::cerr << "[script] fade_object: object '" << name << "' not found\n"; std::cerr << "[script] fade_object: object '" << name << "' not found\n";
@ -638,7 +676,7 @@ namespace ZL {
// scale_object(name, target_scale, duration_sec [, on_complete]) // scale_object(name, target_scale, duration_sec [, on_complete])
api.set_function("scale_object", api.set_function("scale_object",
[loc](const std::string& name, float targetScale, [loc, this_impl = impl.get()](const std::string& name, float targetScale,
float durationSec, sol::object onComplete) { float durationSec, sol::object onComplete) {
for (auto& intObj : loc->interactiveObjects) { for (auto& intObj : loc->interactiveObjects) {
if (intObj.loadedObject.name != name) continue; if (intObj.loadedObject.name != name) continue;
@ -647,17 +685,15 @@ namespace ZL {
return; return;
} }
std::function<void()> cb; std::function<void()> cb;
std::string cbName;
if (onComplete.is<sol::protected_function>()) { if (onComplete.is<sol::protected_function>()) {
sol::protected_function fn = onComplete.as<sol::protected_function>(); sol::protected_function fn = onComplete.as<sol::protected_function>();
cb = [fn]() mutable { cb = wrapLuaCallback(fn);
auto res = fn(); cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
if (!res.valid()) { if (cbName.empty())
sol::error err = res; std::cerr << "[script] scale_object: on_complete callback has no global name, will not survive save/load\n";
std::cerr << "[script] scale_object on_complete error: " << err.what() << "\n";
}
};
} }
intObj.scaleTo(targetScale, durationSec, std::move(cb)); intObj.scaleTo(targetScale, durationSec, std::move(cb), std::move(cbName));
return; return;
} }
std::cerr << "[script] scale_object: object '" << name << "' not found\n"; std::cerr << "[script] scale_object: object '" << name << "' not found\n";
@ -1065,4 +1101,18 @@ namespace ZL {
} }
} }
void ScriptEngine::rehydrateNamedCallback(const std::string& name, std::function<void()>& outCallback) const
{
outCallback = nullptr;
if (!impl || name.empty()) return;
sol::protected_function fn = impl->lua[name];
if (!fn.valid()) {
std::cerr << "[SCRIPT] rehydrateNamedCallback: global function '" << name
<< "' not found (script changed since save?)\n";
return;
}
outCallback = wrapLuaCallback(fn);
}
} // namespace ZL } // namespace ZL

View File

@ -2,6 +2,7 @@
#include <string> #include <string>
#include <memory> #include <memory>
#include <unordered_map> #include <unordered_map>
#include <functional>
#include "quest/QuestJournal.h" #include "quest/QuestJournal.h"
#include "external/nlohmann/json.hpp" #include "external/nlohmann/json.hpp"
#include "ISaveable.h" #include "ISaveable.h"
@ -62,6 +63,14 @@ public:
// has already run and registered all callbacks. // has already run and registered all callbacks.
void loadScriptGlobals(const nlohmann::json& in); void loadScriptGlobals(const nlohmann::json& in);
// Re-wires an in-flight, one-shot callback (NPC/player walk-arrival, object
// animation-complete) that was serialized by name rather than by value.
// Looks up `name` as a global Lua function and assigns a wrapped invoker to
// outCallback; leaves outCallback empty and logs an error if not found.
// Call after init() (the script must already have run) and after the owning
// state (CharacterState / InteractiveObjectState) has been loaded.
void rehydrateNamedCallback(const std::string& name, std::function<void()>& outCallback) const;
void save(nlohmann::json& out) const override { saveScriptGlobals(out); } void save(nlohmann::json& out) const override { saveScriptGlobals(out); }
void load(const nlohmann::json& in) override { loadScriptGlobals(in); } void load(const nlohmann::json& in) override { loadScriptGlobals(in); }

View File

@ -71,7 +71,7 @@ namespace ZL {
} }
void InteractiveObject::moveTo(const Eigen::Vector3f& target, float durationSec, std::function<void()> onComplete) { void InteractiveObject::moveTo(const Eigen::Vector3f& target, float durationSec, std::function<void()> onComplete, std::string callbackName) {
if (state.isAnimating) return; if (state.isAnimating) return;
AnimTask task; AnimTask task;
task.type = AnimTask::Type::Move; task.type = AnimTask::Type::Move;
@ -83,12 +83,13 @@ namespace ZL {
task.targetScale = state.scale; task.targetScale = state.scale;
task.durationMs = durationSec * 1000.f; task.durationMs = durationSec * 1000.f;
task.elapsedMs = 0.f; task.elapsedMs = 0.f;
task.onComplete = std::move(onComplete); task.onComplete = std::move(onComplete);
task.onCompleteName = std::move(callbackName);
state.animTask = std::move(task); state.animTask = std::move(task);
state.isAnimating = true; state.isAnimating = true;
} }
void InteractiveObject::rotateTo(float targetRotY, float durationSec, std::function<void()> onComplete) { void InteractiveObject::rotateTo(float targetRotY, float durationSec, std::function<void()> onComplete, std::string callbackName) {
if (state.isAnimating) return; if (state.isAnimating) return;
AnimTask task; AnimTask task;
task.type = AnimTask::Type::Rotate; task.type = AnimTask::Type::Rotate;
@ -100,12 +101,13 @@ namespace ZL {
task.targetScale = state.scale; task.targetScale = state.scale;
task.durationMs = durationSec * 1000.f; task.durationMs = durationSec * 1000.f;
task.elapsedMs = 0.f; task.elapsedMs = 0.f;
task.onComplete = std::move(onComplete); task.onComplete = std::move(onComplete);
task.onCompleteName = std::move(callbackName);
state.animTask = std::move(task); state.animTask = std::move(task);
state.isAnimating = true; state.isAnimating = true;
} }
void InteractiveObject::scaleTo(float targetScale, float durationSec, std::function<void()> onComplete) { void InteractiveObject::scaleTo(float targetScale, float durationSec, std::function<void()> onComplete, std::string callbackName) {
if (state.isAnimating) return; if (state.isAnimating) return;
AnimTask task; AnimTask task;
task.type = AnimTask::Type::Scale; task.type = AnimTask::Type::Scale;
@ -117,12 +119,13 @@ namespace ZL {
task.targetScale = targetScale; task.targetScale = targetScale;
task.durationMs = durationSec * 1000.f; task.durationMs = durationSec * 1000.f;
task.elapsedMs = 0.f; task.elapsedMs = 0.f;
task.onComplete = std::move(onComplete); task.onComplete = std::move(onComplete);
task.onCompleteName = std::move(callbackName);
state.animTask = std::move(task); state.animTask = std::move(task);
state.isAnimating = true; state.isAnimating = true;
} }
void InteractiveObject::fadeTo(float targetAlpha, float durationSec, std::function<void()> onComplete) { void InteractiveObject::fadeTo(float targetAlpha, float durationSec, std::function<void()> onComplete, std::string callbackName) {
if (state.isAnimating) return; if (state.isAnimating) return;
AnimTask task; AnimTask task;
task.type = AnimTask::Type::Fade; task.type = AnimTask::Type::Fade;
@ -134,7 +137,8 @@ namespace ZL {
task.targetScale = targetAlpha; // reuse targetScale to hold target alpha task.targetScale = targetAlpha; // reuse targetScale to hold target alpha
task.durationMs = durationSec * 1000.f; task.durationMs = durationSec * 1000.f;
task.elapsedMs = 0.f; task.elapsedMs = 0.f;
task.onComplete = std::move(onComplete); task.onComplete = std::move(onComplete);
task.onCompleteName = std::move(callbackName);
state.animTask = std::move(task); state.animTask = std::move(task);
state.isAnimating = true; state.isAnimating = true;
} }
@ -256,6 +260,8 @@ namespace ZL {
at["targetScale"] = animTask->targetScale; at["targetScale"] = animTask->targetScale;
at["durationMs"] = animTask->durationMs; at["durationMs"] = animTask->durationMs;
at["elapsedMs"] = animTask->elapsedMs; at["elapsedMs"] = animTask->elapsedMs;
if (!animTask->onCompleteName.empty())
at["onCompleteName"] = animTask->onCompleteName;
out["animTask"] = std::move(at); out["animTask"] = std::move(at);
} }
} }
@ -284,6 +290,7 @@ namespace ZL {
task.targetScale = at.value("targetScale", 1.f); task.targetScale = at.value("targetScale", 1.f);
task.durationMs = at.value("durationMs", 1000.f); task.durationMs = at.value("durationMs", 1000.f);
task.elapsedMs = at.value("elapsedMs", 0.f); task.elapsedMs = at.value("elapsedMs", 0.f);
task.onCompleteName = at.value("onCompleteName", std::string());
// onComplete is non-serializable; re-wired at runtime by Lua script callbacks. // onComplete is non-serializable; re-wired at runtime by Lua script callbacks.
animTask = std::move(task); animTask = std::move(task);
} }

View File

@ -31,10 +31,12 @@ namespace ZL {
Renderer& renderer, Renderer& renderer,
const std::string& zipPath = ""); const std::string& zipPath = "");
void moveTo(const Eigen::Vector3f& target, float durationSec, std::function<void()> onComplete = {}); // callbackName, if given, is the global Lua function name backing onComplete —
void rotateTo(float targetRotY, float durationSec, std::function<void()> onComplete = {}); // stored in the AnimTask so it survives save/load (see ScriptEngine::rehydrateNamedCallback).
void scaleTo(float targetScale, float durationSec, std::function<void()> onComplete = {}); void moveTo(const Eigen::Vector3f& target, float durationSec, std::function<void()> onComplete = {}, std::string callbackName = "");
void fadeTo(float targetAlpha, float durationSec, std::function<void()> onComplete = {}); void rotateTo(float targetRotY, float durationSec, std::function<void()> onComplete = {}, std::string callbackName = "");
void scaleTo(float targetScale, float durationSec, std::function<void()> onComplete = {}, std::string callbackName = "");
void fadeTo(float targetAlpha, float durationSec, std::function<void()> onComplete = {}, std::string callbackName = "");
void update(int64_t deltaMs); void update(int64_t deltaMs);
void draw(Renderer& renderer) const; void draw(Renderer& renderer) const;
void drawDarklands(Renderer& renderer) const; void drawDarklands(Renderer& renderer) const;

View File

@ -22,6 +22,10 @@ struct AnimTask {
float durationMs = 1000.f; float durationMs = 1000.f;
float elapsedMs = 0.f; float elapsedMs = 0.f;
std::function<void()> onComplete; // non-serializable; re-wired at runtime by Lua callbacks std::function<void()> onComplete; // non-serializable; re-wired at runtime by Lua callbacks
// Global Lua function name backing onComplete, if it was sourced from a script
// (see ScriptEngine::rehydrateNamedCallback). Serialized so the callback can be
// re-looked-up by name after a save/load; empty if none or non-Lua.
std::string onCompleteName;
}; };
// Paths and baked mesh transforms needed to recreate a LoadedGameObject from disk. // Paths and baked mesh transforms needed to recreate a LoadedGameObject from disk.