Major refactoring - internal server state machine implemented
This commit is contained in:
1 parent
71c654e5b6
commit
66e93d46f4
48 files changed
+3948
-3696
No files matched your search
+294
-270
@@ -1,5 +1,6 @@
|
||||
#include "ScriptEngine.h"
|
||||
#include "ScriptEngine.h"
|
||||
#include "Game.h"
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
@@ -14,26 +15,11 @@ namespace FRG {
|
||||
|
||||
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).
|
||||
// remember a callback by name while scripts continue to pass function references.
|
||||
std::string findGlobalFunctionName(lua_State* L, const sol::protected_function& fn) {
|
||||
if (!fn.valid()) return {};
|
||||
|
||||
@@ -58,6 +44,19 @@ namespace FRG {
|
||||
return found;
|
||||
}
|
||||
|
||||
Character* findNpc(Location* location, const std::string& id) {
|
||||
for (auto& npc : location->npcs)
|
||||
if (npc && npc->entityId() == id) return npc.get();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
InteractiveObject* findInteractiveObject(
|
||||
Location* location, const std::string& id) {
|
||||
for (auto& object : location->interactiveObjects)
|
||||
if (object.entityId() == id) return &object;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
struct ScriptEngine::Impl {
|
||||
@@ -65,11 +64,12 @@ namespace FRG {
|
||||
std::unordered_map<std::string, sol::protected_function> triggerEnterCallbacks;
|
||||
std::unordered_map<std::string, sol::protected_function> triggerExitCallbacks;
|
||||
std::unordered_map<std::string, sol::protected_function> cutsceneCompleteCallbacks;
|
||||
std::unordered_map<int, sol::protected_function> npcBumpedByPlayerCallbacks;
|
||||
std::unordered_map<int, sol::protected_function> npcBumpsPlayerCallbacks;
|
||||
std::unordered_map<std::string, sol::protected_function> npcBumpedByPlayerCallbacks;
|
||||
std::unordered_map<std::string, sol::protected_function> npcBumpsPlayerCallbacks;
|
||||
std::unordered_map<std::string, int>* globalInts = nullptr;
|
||||
std::unordered_map<std::string, float>* globalFloats = nullptr;
|
||||
Quest::QuestJournal* questJournal = nullptr;
|
||||
sol::protected_function locationInitCallback;
|
||||
sol::protected_function locationEnterCallback;
|
||||
sol::protected_function locationExitCallback;
|
||||
sol::protected_function darklandsEnterCallback;
|
||||
@@ -110,82 +110,92 @@ namespace FRG {
|
||||
|
||||
auto api = lua.create_named_table("game_api");
|
||||
|
||||
// npc_walk_to(index, x, y, z [, on_arrived])
|
||||
// npc_walk_to(npc_id, x, y, z [, on_arrived])
|
||||
// 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.
|
||||
api.set_function("npc_walk_to",
|
||||
[loc, this_impl = impl.get()](int index, float x, float y, float z, sol::object on_arrived) {
|
||||
auto& npcs = loc->npcs;
|
||||
if (index < 0 || index >= static_cast<int>(npcs.size())) {
|
||||
std::cerr << "[script] npc_walk_to: index " << index
|
||||
<< " out of range (0.." << npcs.size() - 1 << ")\n";
|
||||
[loc, this_impl = impl.get()](const std::string& npcId, float x, float y, float z, sol::object on_arrived) {
|
||||
Character* npc = findNpc(loc, npcId);
|
||||
if (!npc) {
|
||||
std::cerr << "[script] npc_walk_to: unknown NPC id '" << npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
std::function<void()> cb;
|
||||
std::string cbName;
|
||||
if (on_arrived.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = on_arrived.as<sol::protected_function>();
|
||||
cb = wrapLuaCallback(fn);
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] npc_walk_to: on_arrived callback has no global name, will not survive save/load\n";
|
||||
std::cerr << "[script] npc_walk_to: on_arrived must be a global Lua function\n";
|
||||
}
|
||||
npcs[index]->state.homePosition = Eigen::Vector3f(x, 0.f, z);
|
||||
npcs[index]->setTarget(Eigen::Vector3f(x, y, z), std::move(cb), std::move(cbName));
|
||||
loc->updateCharacterState(*npc, [x, z](CharacterState& workingState) {
|
||||
workingState.homePosition = Eigen::Vector3f(x, 0.f, z);
|
||||
});
|
||||
loc->setCharacterTarget(
|
||||
*npc, Eigen::Vector3f(x, y, z), std::move(cbName));
|
||||
});
|
||||
|
||||
|
||||
api.set_function("npc_rotate_to",
|
||||
[loc](int index, float angle) {
|
||||
auto& npcs = loc->npcs;
|
||||
if (index < 0 || index >= static_cast<int>(npcs.size())) {
|
||||
std::cerr << "[script] npc_rotate_to: index " << index
|
||||
<< " out of range (0.." << npcs.size() - 1 << ")\n";
|
||||
[loc](const std::string& npcId, float angle) {
|
||||
Character* npc = findNpc(loc, npcId);
|
||||
if (!npc) {
|
||||
std::cerr << "[script] npc_rotate_to: unknown NPC id '" << npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
|
||||
const float rad = angle * static_cast<float>(M_PI) / 180.f;
|
||||
logger() << "[script] npc_rotate_to: index " << index << " angle " << angle << " degrees (" << rad << " radians)" << std::endl;
|
||||
npcs[index]->state.targetFacingAngle = rad;
|
||||
logger() << "[script] npc_rotate_to: " << npcId << " angle " << angle << " degrees (" << rad << " radians)" << std::endl;
|
||||
loc->updateCharacterState(*npc, [rad](CharacterState& workingState) {
|
||||
workingState.targetFacingAngle = rad;
|
||||
});
|
||||
});
|
||||
|
||||
api.set_function("player_rotate_to",
|
||||
[loc](float angle) {
|
||||
const float rad = angle * static_cast<float>(M_PI) / 180.f;
|
||||
logger() << "[script] player_rotate_to: angle " << angle << " degrees (" << rad << " radians)" << std::endl;
|
||||
loc->player->state.targetFacingAngle = rad;
|
||||
loc->updateCharacterState(*loc->player, [rad](CharacterState& workingState) {
|
||||
workingState.targetFacingAngle = rad;
|
||||
});
|
||||
});
|
||||
|
||||
api.set_function("player_stop_and_rotate_to_npc",
|
||||
[loc](int index) {
|
||||
loc->player->stopAndRotateToTarget(index);
|
||||
[loc](const std::string& npcId) {
|
||||
Character* npc = findNpc(loc, npcId);
|
||||
if (!npc) {
|
||||
std::cerr << "[script] player_stop_and_rotate_to_npc: unknown NPC id '"
|
||||
<< npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
loc->faceCharacter(*loc->player, npc->entityId());
|
||||
});
|
||||
|
||||
api.set_function("npc_stop_and_rotate_to_player",
|
||||
[loc](int index) {
|
||||
auto& npcs = loc->npcs;
|
||||
if (index < 0 || index >= static_cast<int>(npcs.size())) {
|
||||
std::cerr << "[script] npc_stop_and_rotate_to_player: index " << index
|
||||
<< " out of range (0.." << npcs.size() - 1 << ")\n";
|
||||
[loc](const std::string& npcId) {
|
||||
Character* npc = findNpc(loc, npcId);
|
||||
if (!npc) {
|
||||
std::cerr << "[script] npc_stop_and_rotate_to_player: unknown NPC id '"
|
||||
<< npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
npcs[index]->stopAndRotateToTarget(CharacterState::kPlayerIndex);
|
||||
loc->faceCharacter(*npc, "player");
|
||||
});
|
||||
|
||||
api.set_function("player_walk_to",
|
||||
[loc, this_impl = impl.get()](float x, float y, float z, sol::object on_arrived) {
|
||||
|
||||
std::function<void()> cb;
|
||||
std::string cbName;
|
||||
if (on_arrived.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = on_arrived.as<sol::protected_function>();
|
||||
cb = wrapLuaCallback(fn);
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] player_walk_to: on_arrived callback has no global name, will not survive save/load\n";
|
||||
std::cerr << "[script] player_walk_to: on_arrived must be a global Lua function\n";
|
||||
}
|
||||
loc->player->state.homePosition = Eigen::Vector3f(x, 0.f, z);
|
||||
loc->player->setTarget(Eigen::Vector3f(x, y, z), std::move(cb), std::move(cbName));
|
||||
loc->updateCharacterState(*loc->player, [x, z](CharacterState& workingState) {
|
||||
workingState.homePosition = Eigen::Vector3f(x, 0.f, z);
|
||||
});
|
||||
loc->setCharacterTarget(
|
||||
*loc->player, Eigen::Vector3f(x, y, z), std::move(cbName));
|
||||
});
|
||||
|
||||
|
||||
@@ -225,50 +235,44 @@ namespace FRG {
|
||||
inventory->removeItem(id);
|
||||
});
|
||||
|
||||
// deactivate_interactive_object(object_name)
|
||||
api.set_function("deactivate_interactive_object", [loc](const std::string& objectName) {
|
||||
for (auto& intObj : loc->interactiveObjects) {
|
||||
if (intObj.loadedObject.name == objectName) {
|
||||
intObj.state.isActive = false;
|
||||
logger() << "[script] deactivate_interactive_object: " << objectName << std::endl;
|
||||
return;
|
||||
}
|
||||
api.set_function("deactivate_interactive_object", [loc](const std::string& objectId) {
|
||||
InteractiveObject* object = findInteractiveObject(loc, objectId);
|
||||
if (object) {
|
||||
object->state.isActive = false;
|
||||
logger() << "[script] deactivate_interactive_object: " << objectId << std::endl;
|
||||
return;
|
||||
}
|
||||
std::cerr << "[script] deactivate_interactive_object: not found: " << objectName << std::endl;
|
||||
std::cerr << "[script] deactivate_interactive_object: unknown object id: " << objectId << std::endl;
|
||||
});
|
||||
|
||||
// activate_interactive_object(object_name)
|
||||
api.set_function("activate_interactive_object", [loc](const std::string& objectName) {
|
||||
for (auto& intObj : loc->interactiveObjects) {
|
||||
if (intObj.loadedObject.name == objectName) {
|
||||
intObj.state.isActive = true;
|
||||
logger() << "[script] activate_interactive_object: " << objectName << std::endl;
|
||||
return;
|
||||
}
|
||||
api.set_function("activate_interactive_object", [loc](const std::string& objectId) {
|
||||
InteractiveObject* object = findInteractiveObject(loc, objectId);
|
||||
if (object) {
|
||||
object->state.isActive = true;
|
||||
logger() << "[script] activate_interactive_object: " << objectId << std::endl;
|
||||
return;
|
||||
}
|
||||
std::cerr << "[script] activate_interactive_object: not found: " << objectName << std::endl;
|
||||
std::cerr << "[script] activate_interactive_object: unknown object id: " << objectId << std::endl;
|
||||
});
|
||||
|
||||
api.set_function("set_object_rotation", [loc](const std::string& objectName, float value) {
|
||||
for (auto& intObj : loc->interactiveObjects) {
|
||||
if (intObj.loadedObject.name == objectName) {
|
||||
intObj.state.rotationY = value * static_cast<float>(M_PI) / 180.f;
|
||||
logger() << "[script] set_object_rotation: " << objectName << " " << value << std::endl;
|
||||
return;
|
||||
}
|
||||
api.set_function("set_object_rotation", [loc](const std::string& objectId, float value) {
|
||||
InteractiveObject* object = findInteractiveObject(loc, objectId);
|
||||
if (object) {
|
||||
object->state.rotationY = value * static_cast<float>(M_PI) / 180.f;
|
||||
logger() << "[script] set_object_rotation: " << objectId << " " << value << std::endl;
|
||||
return;
|
||||
}
|
||||
std::cerr << "[script] set_object_rotation: not found: " << objectName << std::endl;
|
||||
std::cerr << "[script] set_object_rotation: unknown object id: " << objectId << std::endl;
|
||||
});
|
||||
|
||||
api.set_function("set_object_alpha", [loc](const std::string& objectName, float value) {
|
||||
for (auto& intObj : loc->interactiveObjects) {
|
||||
if (intObj.loadedObject.name == objectName) {
|
||||
intObj.state.alpha = value;
|
||||
logger() << "[script] set_object_alpha: " <<objectName << " " << value << std::endl;
|
||||
return;
|
||||
}
|
||||
api.set_function("set_object_alpha", [loc](const std::string& objectId, float value) {
|
||||
InteractiveObject* object = findInteractiveObject(loc, objectId);
|
||||
if (object) {
|
||||
object->state.alpha = value;
|
||||
logger() << "[script] set_object_alpha: " << objectId << " " << value << std::endl;
|
||||
return;
|
||||
}
|
||||
std::cerr << "[script] set_object_alpha: not found: " << objectName << std::endl;
|
||||
std::cerr << "[script] set_object_alpha: unknown object id: " << objectId << std::endl;
|
||||
});
|
||||
|
||||
// get_inventory_count()
|
||||
@@ -332,7 +336,15 @@ namespace FRG {
|
||||
// on_enter and on_exit are optional Lua functions (pass nil to omit).
|
||||
// Called when the player enters or exits the named trigger zone.
|
||||
api.set_function("set_trigger_zone_callbacks",
|
||||
[this_impl = impl.get()](const std::string& zoneId, sol::object onEnter, sol::object onExit) {
|
||||
[loc, this_impl = impl.get()](const std::string& zoneId, sol::object onEnter, sol::object onExit) {
|
||||
const bool exists = std::any_of(
|
||||
loc->triggerZones.begin(), loc->triggerZones.end(),
|
||||
[&zoneId](const TriggerZone& zone) { return zone.id == zoneId; });
|
||||
if (!exists) {
|
||||
std::cerr << "[script] set_trigger_zone_callbacks: unknown zone id '"
|
||||
<< zoneId << "'\n";
|
||||
return;
|
||||
}
|
||||
if (onEnter.is<sol::protected_function>())
|
||||
this_impl->triggerEnterCallbacks[zoneId] = onEnter.as<sol::protected_function>();
|
||||
if (onExit.is<sol::protected_function>())
|
||||
@@ -386,11 +398,15 @@ namespace FRG {
|
||||
loc->requestAdvanceDarklandsHud();
|
||||
});
|
||||
|
||||
// set_location_callbacks(on_enter, on_exit)
|
||||
// set_location_callbacks(on_init, on_enter, on_exit)
|
||||
// on_init() is called only when this location has no retained state.
|
||||
// on_enter() called once when the player arrives at this location.
|
||||
// on_exit() called once just before the player leaves this location.
|
||||
api.set_function("set_location_callbacks",
|
||||
[this_impl = impl.get()](sol::object onEnter, sol::object onExit) {
|
||||
[this_impl = impl.get()](
|
||||
sol::object onInit, sol::object onEnter, sol::object onExit) {
|
||||
if (onInit.is<sol::protected_function>())
|
||||
this_impl->locationInitCallback = onInit.as<sol::protected_function>();
|
||||
if (onEnter.is<sol::protected_function>())
|
||||
this_impl->locationEnterCallback = onEnter.as<sol::protected_function>();
|
||||
if (onExit.is<sol::protected_function>())
|
||||
@@ -415,11 +431,11 @@ namespace FRG {
|
||||
|
||||
api.set_function("get_player_x",
|
||||
[loc]() -> float {
|
||||
return loc->player ? loc->player->state.position.x() : 0.0f;
|
||||
return loc->player ? loc->player->characterState.position.x() : 0.0f;
|
||||
});
|
||||
api.set_function("get_player_z",
|
||||
[loc]() -> float {
|
||||
return loc->player ? loc->player->state.position.z() : 0.0f;
|
||||
return loc->player ? loc->player->characterState.position.z() : 0.0f;
|
||||
});
|
||||
|
||||
// set_player_hp(value) — sets the player's current HP directly.
|
||||
@@ -427,12 +443,14 @@ namespace FRG {
|
||||
[loc](float value) {
|
||||
if (loc->player)
|
||||
{
|
||||
loc->player->state.setHp(value);
|
||||
if (loc->player->state.currentState == AnimationState::ACTION_TO_DEATH || loc->player->state.currentState == AnimationState::DEATH_IDLE)
|
||||
{
|
||||
loc->player->state.currentState = AnimationState::STAND;
|
||||
loc->player->state.resetAnim = true;
|
||||
}
|
||||
loc->setCharacterHp(*loc->player, value);
|
||||
loc->updateCharacterState(*loc->player, [](CharacterState& workingState) {
|
||||
if (workingState.currentState == AnimationState::ACTION_TO_DEATH ||
|
||||
workingState.currentState == AnimationState::DEATH_IDLE) {
|
||||
workingState.currentState = AnimationState::STAND;
|
||||
workingState.resetAnim = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// get_player_hp() → float (0 if no player)
|
||||
@@ -537,246 +555,253 @@ namespace FRG {
|
||||
});
|
||||
|
||||
api.set_function("set_trigger_zone_enabled",
|
||||
[loc](int index, bool value) {
|
||||
auto& triggerZones = loc->triggerZones;
|
||||
if (index < 0 || index >= static_cast<int>(triggerZones.size())) {
|
||||
std::cerr << "[script] set_trigger_zone_enabled: index " << index << " out of range\n";
|
||||
[loc](const std::string& zoneId, bool value) {
|
||||
for (auto& zone : loc->triggerZones) {
|
||||
if (zone.id != zoneId) continue;
|
||||
zone.enabled = value;
|
||||
return;
|
||||
}
|
||||
triggerZones[index].enabled = value;
|
||||
std::cerr << "[script] set_trigger_zone_enabled: unknown zone id '"
|
||||
<< zoneId << "'\n";
|
||||
});
|
||||
|
||||
// set_teleport_active(index, active) — activates or deactivates a teleport zone by index.
|
||||
// set_teleport_active(zone_id, active)
|
||||
// Swaps the particle effect texture to match the new state.
|
||||
api.set_function("set_teleport_active",
|
||||
[loc](int index, bool value) {
|
||||
auto& zones = loc->teleportZones;
|
||||
if (index < 0 || index >= static_cast<int>(zones.size())) {
|
||||
std::cerr << "[script] set_teleport_active: index " << index << " out of range\n";
|
||||
[loc](const std::string& zoneId, bool value) {
|
||||
for (auto& zone : loc->teleportZones) {
|
||||
if (zone.id != zoneId) continue;
|
||||
zone.setActive(value);
|
||||
return;
|
||||
}
|
||||
zones[index].setActive(value);
|
||||
std::cerr << "[script] set_teleport_active: unknown zone id '"
|
||||
<< zoneId << "'\n";
|
||||
});
|
||||
|
||||
// is_teleport_active(index) → bool
|
||||
// is_teleport_active(zone_id) → bool
|
||||
api.set_function("is_teleport_active",
|
||||
[loc](int index) -> bool {
|
||||
auto& zones = loc->teleportZones;
|
||||
if (index < 0 || index >= static_cast<int>(zones.size())) {
|
||||
std::cerr << "[script] is_teleport_active: index " << index << " out of range\n";
|
||||
return false;
|
||||
}
|
||||
return zones[index].active;
|
||||
[loc](const std::string& zoneId) -> bool {
|
||||
for (const auto& zone : loc->teleportZones)
|
||||
if (zone.id == zoneId) return zone.active;
|
||||
std::cerr << "[script] is_teleport_active: unknown zone id '"
|
||||
<< zoneId << "'\n";
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
// npc_set_hp(index, value) — sets an NPC's current HP directly.
|
||||
// npc_set_hp(npc_id, value)
|
||||
api.set_function("npc_set_hp",
|
||||
[loc](int index, float value) {
|
||||
auto& npcs = loc->npcs;
|
||||
if (index < 0 || index >= static_cast<int>(npcs.size())) {
|
||||
std::cerr << "[script] npc_set_hp: index " << index << " out of range\n";
|
||||
[loc](const std::string& npcId, float value) {
|
||||
Character* npc = findNpc(loc, npcId);
|
||||
if (!npc) {
|
||||
std::cerr << "[script] npc_set_hp: unknown NPC id '" << npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
npcs[index]->setHp(value);
|
||||
loc->setCharacterHp(*npc, value);
|
||||
});
|
||||
|
||||
// npc_set_position(index, x, y, z) — teleports an NPC instantly, no walking.
|
||||
// npc_set_position(npc_id, x, y, z)
|
||||
api.set_function("npc_set_position",
|
||||
[loc](int index, float x, float y, float z) {
|
||||
auto& npcs = loc->npcs;
|
||||
if (index < 0 || index >= static_cast<int>(npcs.size())) {
|
||||
std::cerr << "[script] npc_set_position: index " << index << " out of range\n";
|
||||
[loc](const std::string& npcId, float x, float y, float z) {
|
||||
Character* npc = findNpc(loc, npcId);
|
||||
if (!npc) {
|
||||
std::cerr << "[script] npc_set_position: unknown NPC id '" << npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
Eigen::Vector3f pos(x, y, z);
|
||||
npcs[index]->state.position = pos;
|
||||
npcs[index]->state.homePosition = pos;
|
||||
npcs[index]->setTarget(pos);
|
||||
loc->updateCharacterState(*npc, [&pos](CharacterState& workingState) {
|
||||
workingState.position = pos;
|
||||
workingState.homePosition = pos;
|
||||
});
|
||||
loc->setCharacterTarget(*npc, pos);
|
||||
});
|
||||
|
||||
// npc_set_rotation(index, angle) — sets NPC facing angle around Y axis (degrees).
|
||||
// npc_set_rotation(npc_id, angle)
|
||||
api.set_function("npc_set_rotation",
|
||||
[loc](int index, float angle) {
|
||||
auto& npcs = loc->npcs;
|
||||
if (index < 0 || index >= static_cast<int>(npcs.size())) {
|
||||
std::cerr << "[script] npc_set_rotation: index " << index << " out of range\n";
|
||||
[loc](const std::string& npcId, float angle) {
|
||||
Character* npc = findNpc(loc, npcId);
|
||||
if (!npc) {
|
||||
std::cerr << "[script] npc_set_rotation: unknown NPC id '" << npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
const float rad = angle * static_cast<float>(M_PI) / 180.f;
|
||||
npcs[index]->state.facingAngle = rad;
|
||||
npcs[index]->state.targetFacingAngle = rad;
|
||||
loc->updateCharacterState(*npc, [rad](CharacterState& workingState) {
|
||||
workingState.facingAngle = rad;
|
||||
workingState.targetFacingAngle = rad;
|
||||
});
|
||||
});
|
||||
|
||||
// set_npc_enabled(index, enabled)
|
||||
// set_npc_enabled(npc_id, enabled)
|
||||
api.set_function("set_npc_enabled",
|
||||
[loc](int index, bool value) {
|
||||
auto& npcs = loc->npcs;
|
||||
if (index < 0 || index >= static_cast<int>(npcs.size())) {
|
||||
std::cerr << "[script] set_npc_enabled: index " << index << " out of range\n";
|
||||
[loc](const std::string& npcId, bool value) {
|
||||
Character* npc = findNpc(loc, npcId);
|
||||
if (!npc) {
|
||||
std::cerr << "[script] set_npc_enabled: unknown NPC id '" << npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
npcs[index]->state.enabled = value;
|
||||
npcs[index]->state.attack = 0;
|
||||
npcs[index]->state.attack_cooldown = 0;
|
||||
npcs[index]->state.currentState = AnimationState::STAND;
|
||||
if (npcs[index]->state.canAttack)
|
||||
{
|
||||
npcs[index]->state.attackTargetIndex = CharacterState::kPlayerIndex;
|
||||
}
|
||||
npcs[index]->state.battle_state = 0;
|
||||
loc->updateCharacterState(*npc, [value](CharacterState& workingState) {
|
||||
workingState.enabled = value;
|
||||
workingState.attack = 0;
|
||||
workingState.attack_cooldown = 0;
|
||||
workingState.currentState = AnimationState::STAND;
|
||||
if (workingState.canAttack)
|
||||
workingState.attackTargetId = "player";
|
||||
workingState.battle_state = 0;
|
||||
});
|
||||
});
|
||||
|
||||
api.set_function("player_set_position",
|
||||
[loc](float x, float y, float z) {
|
||||
Eigen::Vector3f pos(x, y, z);
|
||||
loc->player->state.position = pos;
|
||||
loc->player->state.homePosition = pos;
|
||||
loc->player->setTarget(pos);
|
||||
loc->updateCharacterState(*loc->player, [&pos](CharacterState& workingState) {
|
||||
workingState.position = pos;
|
||||
workingState.homePosition = pos;
|
||||
});
|
||||
loc->setCharacterTarget(*loc->player, pos);
|
||||
});
|
||||
|
||||
api.set_function("player_set_rotation",
|
||||
[loc](float angle) {
|
||||
const float rad = angle * static_cast<float>(M_PI) / 180.f;
|
||||
loc->player->state.facingAngle = rad;
|
||||
loc->player->state.targetFacingAngle = rad;
|
||||
loc->updateCharacterState(*loc->player, [rad](CharacterState& workingState) {
|
||||
workingState.facingAngle = rad;
|
||||
workingState.targetFacingAngle = rad;
|
||||
});
|
||||
});
|
||||
|
||||
// move_object(name, x, y, z, duration_sec [, on_complete])
|
||||
api.set_function("move_object",
|
||||
[loc, this_impl = impl.get()](const std::string& name, float x, float y, float z,
|
||||
[loc, this_impl = impl.get()](const std::string& objectId, float x, float y, float z,
|
||||
float durationSec, sol::object onComplete) {
|
||||
for (auto& intObj : loc->interactiveObjects) {
|
||||
if (intObj.loadedObject.name != name) continue;
|
||||
if (intObj.state.isAnimating) {
|
||||
std::cerr << "[script] move_object: '" << name << "' is already animating\n";
|
||||
return;
|
||||
}
|
||||
std::function<void()> cb;
|
||||
std::string cbName;
|
||||
if (onComplete.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
||||
cb = wrapLuaCallback(fn);
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] move_object: on_complete callback has no global name, will not survive save/load\n";
|
||||
}
|
||||
intObj.moveTo(Eigen::Vector3f(x, y, z), durationSec, std::move(cb), std::move(cbName));
|
||||
InteractiveObject* object = findInteractiveObject(loc, objectId);
|
||||
if (!object) {
|
||||
std::cerr << "[script] move_object: unknown object id '" << objectId << "'\n";
|
||||
return;
|
||||
}
|
||||
std::cerr << "[script] move_object: object '" << name << "' not found\n";
|
||||
if (object->state.isAnimating) {
|
||||
std::cerr << "[script] move_object: '" << objectId << "' is already animating\n";
|
||||
return;
|
||||
}
|
||||
std::string cbName;
|
||||
if (onComplete.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] move_object: on_complete must be a global Lua function\n";
|
||||
}
|
||||
object->state.moveTo(
|
||||
Eigen::Vector3f(x, y, z), durationSec, std::move(cbName));
|
||||
});
|
||||
|
||||
// rotate_object(name, angle_deg, duration_sec [, on_complete])
|
||||
api.set_function("rotate_object",
|
||||
[loc, this_impl = impl.get()](const std::string& name, float angleDeg,
|
||||
[loc, this_impl = impl.get()](const std::string& objectId, float angleDeg,
|
||||
float durationSec, sol::object onComplete) {
|
||||
for (auto& intObj : loc->interactiveObjects) {
|
||||
if (intObj.loadedObject.name != name) continue;
|
||||
if (intObj.state.isAnimating) {
|
||||
std::cerr << "[script] rotate_object: '" << name << "' is already animating\n";
|
||||
return;
|
||||
}
|
||||
std::function<void()> cb;
|
||||
std::string cbName;
|
||||
if (onComplete.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
||||
cb = wrapLuaCallback(fn);
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] rotate_object: on_complete callback has no global name, will not survive save/load\n";
|
||||
}
|
||||
const float angleRad = angleDeg * static_cast<float>(M_PI) / 180.f;
|
||||
intObj.rotateTo(intObj.state.rotationY + angleRad, durationSec, std::move(cb), std::move(cbName));
|
||||
InteractiveObject* object = findInteractiveObject(loc, objectId);
|
||||
if (!object) {
|
||||
std::cerr << "[script] rotate_object: unknown object id '" << objectId << "'\n";
|
||||
return;
|
||||
}
|
||||
std::cerr << "[script] rotate_object: object '" << name << "' not found\n";
|
||||
if (object->state.isAnimating) {
|
||||
std::cerr << "[script] rotate_object: '" << objectId << "' is already animating\n";
|
||||
return;
|
||||
}
|
||||
std::string cbName;
|
||||
if (onComplete.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] rotate_object: on_complete must be a global Lua function\n";
|
||||
}
|
||||
const float angleRad = angleDeg * static_cast<float>(M_PI) / 180.f;
|
||||
object->state.rotateTo(
|
||||
object->state.rotationY + angleRad, durationSec, std::move(cbName));
|
||||
});
|
||||
|
||||
// fade_object(name, target_alpha, duration_sec [, on_complete])
|
||||
api.set_function("fade_object",
|
||||
[loc, this_impl = impl.get()](const std::string& name, float targetAlpha,
|
||||
[loc, this_impl = impl.get()](const std::string& objectId, float targetAlpha,
|
||||
float durationSec, sol::object onComplete) {
|
||||
for (auto& intObj : loc->interactiveObjects) {
|
||||
if (intObj.loadedObject.name != name) continue;
|
||||
if (intObj.state.isAnimating) {
|
||||
std::cerr << "[script] fade_object: '" << name << "' is already animating\n";
|
||||
return;
|
||||
}
|
||||
std::function<void()> cb;
|
||||
std::string cbName;
|
||||
if (onComplete.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
||||
cb = wrapLuaCallback(fn);
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] fade_object: on_complete callback has no global name, will not survive save/load\n";
|
||||
}
|
||||
intObj.fadeTo(targetAlpha, durationSec, std::move(cb), std::move(cbName));
|
||||
InteractiveObject* object = findInteractiveObject(loc, objectId);
|
||||
if (!object) {
|
||||
std::cerr << "[script] fade_object: unknown object id '" << objectId << "'\n";
|
||||
return;
|
||||
}
|
||||
std::cerr << "[script] fade_object: object '" << name << "' not found\n";
|
||||
if (object->state.isAnimating) {
|
||||
std::cerr << "[script] fade_object: '" << objectId << "' is already animating\n";
|
||||
return;
|
||||
}
|
||||
std::string cbName;
|
||||
if (onComplete.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] fade_object: on_complete must be a global Lua function\n";
|
||||
}
|
||||
object->state.fadeTo(targetAlpha, durationSec, std::move(cbName));
|
||||
});
|
||||
|
||||
// scale_object(name, target_scale, duration_sec [, on_complete])
|
||||
api.set_function("scale_object",
|
||||
[loc, this_impl = impl.get()](const std::string& name, float targetScale,
|
||||
[loc, this_impl = impl.get()](const std::string& objectId, float targetScale,
|
||||
float durationSec, sol::object onComplete) {
|
||||
for (auto& intObj : loc->interactiveObjects) {
|
||||
if (intObj.loadedObject.name != name) continue;
|
||||
if (intObj.state.isAnimating) {
|
||||
std::cerr << "[script] scale_object: '" << name << "' is already animating\n";
|
||||
return;
|
||||
}
|
||||
std::function<void()> cb;
|
||||
std::string cbName;
|
||||
if (onComplete.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
||||
cb = wrapLuaCallback(fn);
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] scale_object: on_complete callback has no global name, will not survive save/load\n";
|
||||
}
|
||||
intObj.scaleTo(targetScale, durationSec, std::move(cb), std::move(cbName));
|
||||
InteractiveObject* object = findInteractiveObject(loc, objectId);
|
||||
if (!object) {
|
||||
std::cerr << "[script] scale_object: unknown object id '" << objectId << "'\n";
|
||||
return;
|
||||
}
|
||||
std::cerr << "[script] scale_object: object '" << name << "' not found\n";
|
||||
if (object->state.isAnimating) {
|
||||
std::cerr << "[script] scale_object: '" << objectId << "' is already animating\n";
|
||||
return;
|
||||
}
|
||||
std::string cbName;
|
||||
if (onComplete.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] scale_object: on_complete must be a global Lua function\n";
|
||||
}
|
||||
object->state.scaleTo(targetScale, durationSec, std::move(cbName));
|
||||
});
|
||||
|
||||
api.set_function("resetPlayerAfterDeath",
|
||||
[loc]() {
|
||||
if (loc->player) loc->player->resetPlayerAfterDeath();
|
||||
if (loc->player) loc->resetCharacterAfterDeath(*loc->player);
|
||||
});
|
||||
|
||||
// player_stop() — cancels the player's current walk target and stops them immediately.
|
||||
api.set_function("player_stop",
|
||||
[loc]() {
|
||||
if (loc->player) loc->player->stopInPlace();
|
||||
if (loc->player) loc->stopCharacter(*loc->player);
|
||||
});
|
||||
|
||||
// npc_stop(index) — cancels an NPC's current walk target and stops them immediately.
|
||||
// npc_stop(npc_id)
|
||||
api.set_function("npc_stop",
|
||||
[loc](int index) {
|
||||
auto& npcs = loc->npcs;
|
||||
if (index < 0 || index >= static_cast<int>(npcs.size())) {
|
||||
std::cerr << "[script] npc_stop: index " << index
|
||||
<< " out of range (0.." << npcs.size() - 1 << ")\n";
|
||||
[loc](const std::string& npcId) {
|
||||
Character* npc = findNpc(loc, npcId);
|
||||
if (!npc) {
|
||||
std::cerr << "[script] npc_stop: unknown NPC id '" << npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
npcs[index]->stopInPlace();
|
||||
loc->stopCharacter(*npc);
|
||||
});
|
||||
|
||||
// set_npc_bump_callbacks(npc_index, on_player_bumps_npc, on_npc_bumps_player)
|
||||
// set_npc_bump_callbacks(npc_id, on_player_bumps_npc, on_npc_bumps_player)
|
||||
// Both callbacks are optional (pass nil to omit).
|
||||
// on_player_bumps_npc() — player was moving and walked into the NPC.
|
||||
// on_npc_bumps_player() — the NPC was moving and walked into the standing player.
|
||||
// Each callback fires at most once every 5 seconds per NPC (cooldown managed by Location).
|
||||
api.set_function("set_npc_bump_callbacks",
|
||||
[this_impl = impl.get()](int index, sol::object onPlayerBumpsNpc, sol::object onNpcBumpsPlayer) {
|
||||
[loc, this_impl = impl.get()](const std::string& npcId, sol::object onPlayerBumpsNpc, sol::object onNpcBumpsPlayer) {
|
||||
if (!findNpc(loc, npcId)) {
|
||||
std::cerr << "[script] set_npc_bump_callbacks: unknown NPC id '"
|
||||
<< npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
if (onPlayerBumpsNpc.is<sol::protected_function>())
|
||||
this_impl->npcBumpedByPlayerCallbacks[index] = onPlayerBumpsNpc.as<sol::protected_function>();
|
||||
this_impl->npcBumpedByPlayerCallbacks[npcId] = onPlayerBumpsNpc.as<sol::protected_function>();
|
||||
if (onNpcBumpsPlayer.is<sol::protected_function>())
|
||||
this_impl->npcBumpsPlayerCallbacks[index] = onNpcBumpsPlayer.as<sol::protected_function>();
|
||||
this_impl->npcBumpsPlayerCallbacks[npcId] = onNpcBumpsPlayer.as<sol::protected_function>();
|
||||
});
|
||||
|
||||
// quest_unlock(quest_id)
|
||||
@@ -896,7 +921,7 @@ namespace FRG {
|
||||
#endif
|
||||
}
|
||||
|
||||
void ScriptEngine::callNpcInteractCallback(int npcIndex) {
|
||||
void ScriptEngine::callNpcInteractCallback(const std::string& npcId) {
|
||||
if (!impl) {
|
||||
std::cerr << "[SCRIPT] Engine not initialized!" << std::endl;
|
||||
return;
|
||||
@@ -904,13 +929,13 @@ namespace FRG {
|
||||
sol::state& lua = impl->lua;
|
||||
sol::function fn = lua["on_npc_interact"];
|
||||
if (fn.valid()) {
|
||||
auto result = fn(npcIndex);
|
||||
auto result = fn(npcId);
|
||||
if (!result.valid()) {
|
||||
sol::error err = result;
|
||||
std::cerr << "[SCRIPT] on_npc_interact error: " << err.what() << "\n";
|
||||
}
|
||||
else {
|
||||
logger() << "[SCRIPT] on_npc_interact called with index " << npcIndex << std::endl;
|
||||
logger() << "[SCRIPT] on_npc_interact called with id " << npcId << std::endl;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -926,26 +951,26 @@ namespace FRG {
|
||||
}
|
||||
}*/
|
||||
|
||||
void ScriptEngine::callActivateFunction(const std::string& functionName) {
|
||||
void ScriptEngine::callFunction(const std::string& functionName) {
|
||||
if (!impl) {
|
||||
throw std::runtime_error("[SCRIPT] Engine not initialized!");
|
||||
}
|
||||
|
||||
if (functionName.empty()) {
|
||||
throw std::runtime_error("[SCRIPT] Activate function name is empty!");
|
||||
throw std::runtime_error("[SCRIPT] Function name is empty!");
|
||||
}
|
||||
|
||||
sol::state& lua = impl->lua;
|
||||
logger() << "[SCRIPT] Looking for activate function: " << functionName << std::endl;
|
||||
logger() << "[SCRIPT] Looking for function: " << functionName << std::endl;
|
||||
|
||||
sol::function activateFunc = lua[functionName];
|
||||
sol::protected_function function = lua[functionName];
|
||||
|
||||
if (!activateFunc.valid()) {
|
||||
if (!function.valid()) {
|
||||
throw std::runtime_error("[SCRIPT] Lua function not found: " + functionName);
|
||||
}
|
||||
|
||||
logger() << "[SCRIPT] Found function! Calling: " << functionName << std::endl;
|
||||
auto result = activateFunc();
|
||||
auto result = function();
|
||||
|
||||
if (!result.valid()) {
|
||||
sol::error err = result;
|
||||
@@ -955,6 +980,10 @@ namespace FRG {
|
||||
logger() << "[SCRIPT] Function executed successfully!" << std::endl;
|
||||
}
|
||||
|
||||
void ScriptEngine::callActivateFunction(const std::string& functionName) {
|
||||
callFunction(functionName);
|
||||
}
|
||||
|
||||
void ScriptEngine::callItemPickupCallback(const std::string& objectName) {
|
||||
if (!impl) {
|
||||
std::cerr << "[SCRIPT] impl is null!" << std::endl;
|
||||
@@ -1016,6 +1045,15 @@ namespace FRG {
|
||||
if (impl) impl->questJournal = journal;
|
||||
}
|
||||
|
||||
void ScriptEngine::callLocationInitCallback() {
|
||||
if (!impl || !impl->locationInitCallback.valid()) return;
|
||||
auto result = impl->locationInitCallback();
|
||||
if (!result.valid()) {
|
||||
sol::error err = result;
|
||||
std::cerr << "[SCRIPT] location init callback error: " << err.what() << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptEngine::callLocationEnterCallback() {
|
||||
if (!impl || !impl->locationEnterCallback.valid()) return;
|
||||
auto result = impl->locationEnterCallback();
|
||||
@@ -1082,14 +1120,14 @@ namespace FRG {
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptEngine::callNpcBumpedByPlayerCallback(int npcIndex) {
|
||||
void ScriptEngine::callNpcBumpedByPlayerCallback(const std::string& npcId) {
|
||||
if (!impl) return;
|
||||
auto it = impl->npcBumpedByPlayerCallbacks.find(npcIndex);
|
||||
auto it = impl->npcBumpedByPlayerCallbacks.find(npcId);
|
||||
if (it == impl->npcBumpedByPlayerCallbacks.end()) return;
|
||||
auto result = it->second();
|
||||
if (!result.valid()) {
|
||||
sol::error err = result;
|
||||
std::cerr << "[SCRIPT] on_player_bumps_npc error for NPC " << npcIndex << ": " << err.what() << "\n";
|
||||
std::cerr << "[SCRIPT] on_player_bumps_npc error for NPC " << npcId << ": " << err.what() << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1105,14 +1143,14 @@ namespace FRG {
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptEngine::callNpcBumpsPlayerCallback(int npcIndex) {
|
||||
void ScriptEngine::callNpcBumpsPlayerCallback(const std::string& npcId) {
|
||||
if (!impl) return;
|
||||
auto it = impl->npcBumpsPlayerCallbacks.find(npcIndex);
|
||||
auto it = impl->npcBumpsPlayerCallbacks.find(npcId);
|
||||
if (it == impl->npcBumpsPlayerCallbacks.end()) return;
|
||||
auto result = it->second();
|
||||
if (!result.valid()) {
|
||||
sol::error err = result;
|
||||
std::cerr << "[SCRIPT] on_npc_bumps_player error for NPC " << npcIndex << ": " << err.what() << "\n";
|
||||
std::cerr << "[SCRIPT] on_npc_bumps_player error for NPC " << npcId << ": " << err.what() << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1177,18 +1215,4 @@ namespace FRG {
|
||||
}
|
||||
}
|
||||
|
||||
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 FRG
|
||||
Reference in new issue
Block a user