948 lines
41 KiB
C++
948 lines
41 KiB
C++
#include "ScriptEngine.h"
|
|
#include "Game.h"
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
#include <unordered_map>
|
|
#include "Location.h"
|
|
#include "items/ItemRegistry.h"
|
|
|
|
#define SOL_ALL_SAFETIES_ON 1
|
|
#include <sol/sol.hpp>
|
|
|
|
namespace ZL {
|
|
|
|
struct ScriptEngine::Impl {
|
|
sol::state lua;
|
|
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, int>* globalInts = nullptr;
|
|
std::unordered_map<std::string, float>* globalFloats = nullptr;
|
|
Quest::QuestJournal* questJournal = nullptr;
|
|
sol::protected_function locationEnterCallback;
|
|
sol::protected_function locationExitCallback;
|
|
sol::protected_function darklandsEnterCallback;
|
|
sol::protected_function darklandsExitCallback;
|
|
sol::protected_function triggerNightEnterCallback;
|
|
sol::protected_function callTaxiCallback;
|
|
sol::protected_function chatOpenCallbacks[3];
|
|
};
|
|
|
|
ScriptEngine::ScriptEngine() = default;
|
|
ScriptEngine::~ScriptEngine() = default;
|
|
|
|
void ScriptEngine::init(Location* loc, Inventory* inventory, const std::string& scriptPath) {
|
|
impl = std::make_unique<Impl>();
|
|
sol::state& lua = impl->lua;
|
|
|
|
lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string, sol::lib::table);
|
|
|
|
auto api = lua.create_named_table("game_api");
|
|
|
|
// npc_walk_to(index, 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](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";
|
|
return;
|
|
}
|
|
std::function<void()> cb;
|
|
if (on_arrived.is<sol::protected_function>()) {
|
|
sol::protected_function fn = on_arrived.as<sol::protected_function>();
|
|
cb = [fn]() mutable {
|
|
auto result = fn();
|
|
if (!result.valid()) {
|
|
sol::error err = result;
|
|
std::cerr << "[script] on_arrived error: " << err.what() << "\n";
|
|
}
|
|
};
|
|
}
|
|
npcs[index]->homePosition = Eigen::Vector3f(x, 0.f, z);
|
|
npcs[index]->setTarget(Eigen::Vector3f(x, y, z), std::move(cb));
|
|
});
|
|
|
|
api.set_function("player_walk_to",
|
|
[loc](float x, float y, float z, sol::object on_arrived) {
|
|
|
|
std::function<void()> cb;
|
|
if (on_arrived.is<sol::protected_function>()) {
|
|
sol::protected_function fn = on_arrived.as<sol::protected_function>();
|
|
cb = [fn]() mutable {
|
|
auto result = fn();
|
|
if (!result.valid()) {
|
|
sol::error err = result;
|
|
std::cerr << "[script] on_arrived error: " << err.what() << "\n";
|
|
}
|
|
};
|
|
}
|
|
loc->player->homePosition = Eigen::Vector3f(x, 0.f, z);
|
|
loc->player->setTarget(Eigen::Vector3f(x, y, z), std::move(cb));
|
|
});
|
|
|
|
|
|
api.set_function("has_item", [inventory](const std::string& itemId) {
|
|
const Item* item = ItemRegistry::instance().findById(itemId);
|
|
if (item) {
|
|
if (inventory->hasItem(itemId)) {
|
|
std::cout << "[script] has_item: " << item->name << " returns true" << std::endl;
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
std::cout << "[script] has_item: " << item->name << " returns false" << std::endl;
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
std::cerr << "[script] has_item: item '" << itemId << "' not found in ItemRegistry\n";
|
|
return false;
|
|
}
|
|
});
|
|
|
|
// pickup_item(item_id)
|
|
api.set_function("pickup_item", [inventory](const std::string& itemId) {
|
|
const Item* item = ItemRegistry::instance().findById(itemId);
|
|
if (item) {
|
|
inventory->addItem(*item);
|
|
std::cout << "[script] pickup_item: " << item->name << std::endl;
|
|
} else {
|
|
std::cerr << "[script] pickup_item: item '" << itemId << "' not found in ItemRegistry\n";
|
|
}
|
|
});
|
|
|
|
// remove_item(item_id)
|
|
api.set_function("remove_item", [loc, inventory](const std::string& id) {
|
|
std::cout << "[script] remove_item: " << id << std::endl;
|
|
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.isActive = false;
|
|
std::cout << "[script] deactivate_interactive_object: " << objectName << std::endl;
|
|
return;
|
|
}
|
|
}
|
|
std::cerr << "[script] deactivate_interactive_object: not found: " << objectName << 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.isActive = true;
|
|
std::cout << "[script] activate_interactive_object: " << objectName << std::endl;
|
|
return;
|
|
}
|
|
}
|
|
std::cerr << "[script] activate_interactive_object: not found: " << objectName << 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.rotationY = value * static_cast<float>(M_PI) / 180.f;
|
|
std::cout << "[script] set_object_rotation: " << objectName << " " << value << std::endl;
|
|
return;
|
|
}
|
|
}
|
|
std::cerr << "[script] set_object_rotation: not found: " << objectName << 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.alpha = value;
|
|
std::cout << "[script] set_object_alpha: " <<objectName << " " << value << std::endl;
|
|
return;
|
|
}
|
|
}
|
|
std::cerr << "[script] set_object_alpha: not found: " << objectName << std::endl;
|
|
});
|
|
|
|
// get_inventory_count()
|
|
api.set_function("get_inventory_count", [inventory]() {
|
|
return inventory->getCount();
|
|
});
|
|
|
|
// has_item(item_id)
|
|
api.set_function("has_item", [inventory](const std::string& id) {
|
|
return inventory->hasItem(id);
|
|
});
|
|
|
|
api.set_function("start_dialogue",
|
|
[loc](const std::string& dialogueId) {
|
|
if (!loc->requestDialogueStart(dialogueId)) {
|
|
std::cerr << "[script] start_dialogue failed for id: " << dialogueId << "\n";
|
|
}
|
|
});
|
|
|
|
// start_cutscene(cutscene_id)
|
|
api.set_function("start_cutscene",
|
|
[loc](const std::string& cutsceneId) {
|
|
if (!loc->requestCutsceneStart(cutsceneId))
|
|
std::cerr << "[script] start_cutscene failed for id: " << cutsceneId << "\n";
|
|
});
|
|
|
|
// set_cutscene_callback(cutscene_id, on_complete)
|
|
// on_complete() is called with no arguments when the named cutscene finishes.
|
|
api.set_function("set_cutscene_callback",
|
|
[this_impl = impl.get()](const std::string& cutsceneId, sol::object onComplete) {
|
|
if (onComplete.is<sol::protected_function>())
|
|
this_impl->cutsceneCompleteCallbacks[cutsceneId] = onComplete.as<sol::protected_function>();
|
|
});
|
|
|
|
api.set_function("set_dialogue_flag",
|
|
[loc](const std::string& flag, int value) {
|
|
loc->setDialogueFlag(flag, value);
|
|
});
|
|
|
|
api.set_function("get_dialogue_flag",
|
|
[loc](const std::string& flag) {
|
|
return loc->getDialogueFlag(flag);
|
|
});
|
|
|
|
api.set_function("set_navigation_area_available",
|
|
[loc](const std::string& areaName, bool available) {
|
|
if (!loc->setNavigationAreaAvailable(areaName, available)) {
|
|
std::cerr << "[script] set_navigation_area_available: area not found: "
|
|
<< areaName << "\n";
|
|
}
|
|
});
|
|
|
|
api.set_function("switch_navigation",
|
|
[loc](int index) {
|
|
if (!loc->switchNavigation(index)) {
|
|
std::cerr << "[script] switch_navigation: index " << index << " out of range\n";
|
|
}
|
|
});
|
|
|
|
// set_trigger_zone_callbacks(zone_id, on_enter, on_exit)
|
|
// 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) {
|
|
if (onEnter.is<sol::protected_function>())
|
|
this_impl->triggerEnterCallbacks[zoneId] = onEnter.as<sol::protected_function>();
|
|
if (onExit.is<sol::protected_function>())
|
|
this_impl->triggerExitCallbacks[zoneId] = onExit.as<sol::protected_function>();
|
|
});
|
|
|
|
// start_darklands_transition()
|
|
// Triggers the white-flash transition to toggle darklands mode.
|
|
// Does nothing if a transition is already in progress.
|
|
api.set_function("start_darklands_transition",
|
|
[loc]() {
|
|
if (loc->requestDarklandsTransition)
|
|
loc->requestDarklandsTransition();
|
|
});
|
|
|
|
|
|
api.set_function("set_day",
|
|
[loc]() {
|
|
loc->requestNightDayTransition(false, false);
|
|
});
|
|
|
|
api.set_function("set_night",
|
|
[loc]() {
|
|
loc->requestNightDayTransition(true, false);
|
|
std::cout << "Set night called" << std::endl;
|
|
});
|
|
|
|
api.set_function("set_dawn",
|
|
[loc]() {
|
|
loc->requestNightDayTransition(true, true);
|
|
std::cout << "Set dawn called" << std::endl;
|
|
});
|
|
|
|
|
|
api.set_function("is_night",
|
|
[loc]() {
|
|
return loc->isNight;
|
|
});
|
|
|
|
api.set_function("is_dawn",
|
|
[loc]() {
|
|
return loc->isDawn;
|
|
});
|
|
|
|
// advance_darklands_hud()
|
|
// Advances the uni_interior darklands HUD from step12 to step13.
|
|
// Call when the player enters the ghost trigger zone in darklands.
|
|
api.set_function("advance_darklands_hud",
|
|
[loc]() {
|
|
if (loc->requestAdvanceDarklandsHud)
|
|
loc->requestAdvanceDarklandsHud();
|
|
});
|
|
|
|
// set_location_callbacks(on_enter, on_exit)
|
|
// 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) {
|
|
if (onEnter.is<sol::protected_function>())
|
|
this_impl->locationEnterCallback = onEnter.as<sol::protected_function>();
|
|
if (onExit.is<sol::protected_function>())
|
|
this_impl->locationExitCallback = onExit.as<sol::protected_function>();
|
|
});
|
|
|
|
// is_darklands() → bool
|
|
api.set_function("is_darklands",
|
|
[loc]() { return loc->isDarklands; });
|
|
|
|
api.set_function("setFloatValue",
|
|
[this_impl = impl.get()](const std::string& key, float value) {
|
|
if (this_impl->globalFloats)
|
|
(*this_impl->globalFloats)[key] = value;
|
|
});
|
|
api.set_function("getFloatValue",
|
|
[this_impl = impl.get()](const std::string& key) -> float {
|
|
if (!this_impl->globalFloats) return 0.0f;
|
|
auto it = this_impl->globalFloats->find(key);
|
|
return it != this_impl->globalFloats->end() ? it->second : 0.0f;
|
|
});
|
|
|
|
api.set_function("get_player_x",
|
|
[loc]() -> float {
|
|
return loc->player ? loc->player->position.x() : 0.0f;
|
|
});
|
|
api.set_function("get_player_z",
|
|
[loc]() -> float {
|
|
return loc->player ? loc->player->position.z() : 0.0f;
|
|
});
|
|
|
|
// set_player_hp(value) — sets the player's current HP directly.
|
|
api.set_function("set_player_hp",
|
|
[loc](float value) {
|
|
if (loc->player)
|
|
{
|
|
loc->player->hp = value;
|
|
if (loc->player->currentState == AnimationState::ACTION_TO_DEATH || loc->player->currentState == AnimationState::DEATH_IDLE)
|
|
{
|
|
loc->player->currentState = AnimationState::STAND;
|
|
loc->player->resetAnim = true;
|
|
}
|
|
}
|
|
});
|
|
// get_player_hp() → float (0 if no player)
|
|
api.set_function("get_player_hp",
|
|
[loc]() -> float {
|
|
return loc->player ? loc->player->hp : 0.0f;
|
|
});
|
|
|
|
// Global integer store — shared across all location scripts.
|
|
// setIntValue(key, value) / getIntValue(key) → int (0 if not set)
|
|
api.set_function("setIntValue",
|
|
[this_impl = impl.get()](const std::string& key, int value) {
|
|
if (this_impl->globalInts)
|
|
(*this_impl->globalInts)[key] = value;
|
|
});
|
|
api.set_function("getIntValue",
|
|
[this_impl = impl.get()](const std::string& key) -> int {
|
|
if (!this_impl->globalInts) return 0;
|
|
auto it = this_impl->globalInts->find(key);
|
|
return it != this_impl->globalInts->end() ? it->second : 0;
|
|
});
|
|
|
|
// set_darklands_callbacks(on_enter, on_exit)
|
|
// on_enter() called when the player switches into Darklands mode.
|
|
// on_exit() called when the player switches back to normal mode.
|
|
// Pass nil to omit either callback.
|
|
api.set_function("set_darklands_callbacks",
|
|
[this_impl = impl.get()](sol::object onEnter, sol::object onExit) {
|
|
if (onEnter.is<sol::protected_function>())
|
|
this_impl->darklandsEnterCallback = onEnter.as<sol::protected_function>();
|
|
if (onExit.is<sol::protected_function>())
|
|
this_impl->darklandsExitCallback = onExit.as<sol::protected_function>();
|
|
});
|
|
|
|
api.set_function("close_phone",
|
|
[loc]() {
|
|
if (loc->requestClosePhone)
|
|
loc->requestClosePhone();
|
|
});
|
|
|
|
api.set_function("return_to_main_menu",
|
|
[loc]() {
|
|
if (loc->requestReturnToMainMenu)
|
|
loc->requestReturnToMainMenu();
|
|
});
|
|
|
|
// set_chat_unread(chat_index, unread [, preview_msg])
|
|
// chat_index: 0=first contact, 1=second, 2=third
|
|
// unread: true to mark as unread, false to mark as read
|
|
// preview_msg: optional string to show in the chat list preview (trimmed to 22 chars)
|
|
// When unread=false and preview_msg is omitted, preview auto-updates from chat history.
|
|
api.set_function("set_chat_unread",
|
|
[loc](int chatIndex, bool unread, sol::object previewMsg) {
|
|
if (!loc->requestSetChatUnread) {
|
|
std::cerr << "[script] set_chat_unread: callback not wired\n";
|
|
return;
|
|
}
|
|
const std::string msg = previewMsg.is<std::string>()
|
|
? previewMsg.as<std::string>() : "";
|
|
loc->requestSetChatUnread(chatIndex, unread, msg);
|
|
});
|
|
|
|
api.set_function("set_chat_callbacks",
|
|
[this_impl = impl.get()](sol::object cb0, sol::object cb1, sol::object cb2) {
|
|
if (cb0.is<sol::protected_function>()) this_impl->chatOpenCallbacks[0] = cb0.as<sol::protected_function>();
|
|
if (cb1.is<sol::protected_function>()) this_impl->chatOpenCallbacks[1] = cb1.as<sol::protected_function>();
|
|
if (cb2.is<sol::protected_function>()) this_impl->chatOpenCallbacks[2] = cb2.as<sol::protected_function>();
|
|
});
|
|
|
|
api.set_function("set_enter_night_callback",
|
|
[this_impl = impl.get()](sol::object onEnter) {
|
|
if (onEnter.is<sol::protected_function>())
|
|
this_impl->triggerNightEnterCallback = onEnter.as<sol::protected_function>();
|
|
});
|
|
|
|
api.set_function("set_call_taxi_callback",
|
|
[this_impl = impl.get()](sol::object onTaxi) {
|
|
if (onTaxi.is<sol::protected_function>())
|
|
this_impl->callTaxiCallback = onTaxi.as<sol::protected_function>();
|
|
});
|
|
|
|
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";
|
|
return;
|
|
}
|
|
triggerZones[index].enabled = value;
|
|
});
|
|
|
|
// set_teleport_active(index, active) — activates or deactivates a teleport zone by index.
|
|
// 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";
|
|
return;
|
|
}
|
|
zones[index].setActive(value);
|
|
});
|
|
|
|
// is_teleport_active(index) → 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;
|
|
});
|
|
|
|
|
|
// npc_set_hp(index, value) — sets an NPC's current HP directly.
|
|
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";
|
|
return;
|
|
}
|
|
npcs[index]->hp = value;
|
|
});
|
|
|
|
// npc_set_position(index, x, y, z) — teleports an NPC instantly, no walking.
|
|
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";
|
|
return;
|
|
}
|
|
Eigen::Vector3f pos(x, y, z);
|
|
npcs[index]->position = pos;
|
|
npcs[index]->homePosition = pos;
|
|
npcs[index]->setTarget(pos);
|
|
});
|
|
|
|
// npc_set_rotation(index, angle) — sets NPC facing angle around Y axis (degrees).
|
|
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";
|
|
return;
|
|
}
|
|
const float rad = angle * static_cast<float>(M_PI) / 180.f;
|
|
npcs[index]->facingAngle = rad;
|
|
npcs[index]->targetFacingAngle = rad;
|
|
});
|
|
|
|
// set_npc_enabled(index, 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";
|
|
return;
|
|
}
|
|
npcs[index]->enabled = value;
|
|
npcs[index]->attack = 0;
|
|
npcs[index]->attack_cooldown = 0;
|
|
npcs[index]->currentState = AnimationState::STAND;
|
|
if (npcs[index]->canAttack)
|
|
{
|
|
npcs[index]->attackTarget = loc->player.get();
|
|
}
|
|
npcs[index]->battle_state = 0;
|
|
});
|
|
|
|
// move_object(name, x, y, z, duration_sec [, on_complete])
|
|
api.set_function("move_object",
|
|
[loc](const std::string& name, float x, float y, float z,
|
|
float durationSec, sol::object onComplete) {
|
|
for (auto& intObj : loc->interactiveObjects) {
|
|
if (intObj.loadedObject.name != name) continue;
|
|
if (intObj.isAnimating) {
|
|
std::cerr << "[script] move_object: '" << name << "' is already animating\n";
|
|
return;
|
|
}
|
|
std::function<void()> cb;
|
|
if (onComplete.is<sol::protected_function>()) {
|
|
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
|
cb = [fn]() mutable {
|
|
auto res = fn();
|
|
if (!res.valid()) {
|
|
sol::error err = res;
|
|
std::cerr << "[script] move_object on_complete error: " << err.what() << "\n";
|
|
}
|
|
};
|
|
}
|
|
intObj.moveTo(Eigen::Vector3f(x, y, z), durationSec, std::move(cb));
|
|
return;
|
|
}
|
|
std::cerr << "[script] move_object: object '" << name << "' not found\n";
|
|
});
|
|
|
|
// rotate_object(name, angle_deg, duration_sec [, on_complete])
|
|
api.set_function("rotate_object",
|
|
[loc](const std::string& name, float angleDeg,
|
|
float durationSec, sol::object onComplete) {
|
|
for (auto& intObj : loc->interactiveObjects) {
|
|
if (intObj.loadedObject.name != name) continue;
|
|
if (intObj.isAnimating) {
|
|
std::cerr << "[script] rotate_object: '" << name << "' is already animating\n";
|
|
return;
|
|
}
|
|
std::function<void()> cb;
|
|
if (onComplete.is<sol::protected_function>()) {
|
|
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
|
cb = [fn]() mutable {
|
|
auto res = fn();
|
|
if (!res.valid()) {
|
|
sol::error err = res;
|
|
std::cerr << "[script] rotate_object on_complete error: " << err.what() << "\n";
|
|
}
|
|
};
|
|
}
|
|
const float angleRad = angleDeg * static_cast<float>(M_PI) / 180.f;
|
|
intObj.rotateTo(intObj.rotationY + angleRad, durationSec, std::move(cb));
|
|
return;
|
|
}
|
|
std::cerr << "[script] rotate_object: object '" << name << "' not found\n";
|
|
});
|
|
|
|
// fade_object(name, target_alpha, duration_sec [, on_complete])
|
|
api.set_function("fade_object",
|
|
[loc](const std::string& name, float targetAlpha,
|
|
float durationSec, sol::object onComplete) {
|
|
for (auto& intObj : loc->interactiveObjects) {
|
|
if (intObj.loadedObject.name != name) continue;
|
|
if (intObj.isAnimating) {
|
|
std::cerr << "[script] fade_object: '" << name << "' is already animating\n";
|
|
return;
|
|
}
|
|
std::function<void()> cb;
|
|
if (onComplete.is<sol::protected_function>()) {
|
|
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
|
cb = [fn]() mutable {
|
|
auto res = fn();
|
|
if (!res.valid()) {
|
|
sol::error err = res;
|
|
std::cerr << "[script] fade_object on_complete error: " << err.what() << "\n";
|
|
}
|
|
};
|
|
}
|
|
intObj.fadeTo(targetAlpha, durationSec, std::move(cb));
|
|
return;
|
|
}
|
|
std::cerr << "[script] fade_object: object '" << name << "' not found\n";
|
|
});
|
|
|
|
// scale_object(name, target_scale, duration_sec [, on_complete])
|
|
api.set_function("scale_object",
|
|
[loc](const std::string& name, float targetScale,
|
|
float durationSec, sol::object onComplete) {
|
|
for (auto& intObj : loc->interactiveObjects) {
|
|
if (intObj.loadedObject.name != name) continue;
|
|
if (intObj.isAnimating) {
|
|
std::cerr << "[script] scale_object: '" << name << "' is already animating\n";
|
|
return;
|
|
}
|
|
std::function<void()> cb;
|
|
if (onComplete.is<sol::protected_function>()) {
|
|
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
|
cb = [fn]() mutable {
|
|
auto res = fn();
|
|
if (!res.valid()) {
|
|
sol::error err = res;
|
|
std::cerr << "[script] scale_object on_complete error: " << err.what() << "\n";
|
|
}
|
|
};
|
|
}
|
|
intObj.scaleTo(targetScale, durationSec, std::move(cb));
|
|
return;
|
|
}
|
|
std::cerr << "[script] scale_object: object '" << name << "' not found\n";
|
|
});
|
|
|
|
api.set_function("resetPlayerAfterDeath",
|
|
[loc]() {
|
|
if (loc->player) loc->player->resetPlayerAfterDeath();
|
|
});
|
|
|
|
// 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();
|
|
});
|
|
|
|
// npc_stop(index) — cancels an NPC's current walk target and stops them immediately.
|
|
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";
|
|
return;
|
|
}
|
|
npcs[index]->stopInPlace();
|
|
});
|
|
|
|
// set_npc_bump_callbacks(npc_index, 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) {
|
|
if (onPlayerBumpsNpc.is<sol::protected_function>())
|
|
this_impl->npcBumpedByPlayerCallbacks[index] = onPlayerBumpsNpc.as<sol::protected_function>();
|
|
if (onNpcBumpsPlayer.is<sol::protected_function>())
|
|
this_impl->npcBumpsPlayerCallbacks[index] = onNpcBumpsPlayer.as<sol::protected_function>();
|
|
});
|
|
|
|
// quest_unlock(quest_id)
|
|
api.set_function("quest_unlock",
|
|
[this_impl = impl.get()](const std::string& questId) -> bool {
|
|
if (!this_impl->questJournal) {
|
|
std::cerr << "[script] quest_unlock: QuestJournal not set\n";
|
|
return false;
|
|
}
|
|
return this_impl->questJournal->unlockQuest(questId);
|
|
});
|
|
|
|
// quest_complete(quest_id)
|
|
api.set_function("quest_complete",
|
|
[this_impl = impl.get()](const std::string& questId) -> bool {
|
|
if (!this_impl->questJournal) {
|
|
std::cerr << "[script] quest_complete: QuestJournal not set\n";
|
|
return false;
|
|
}
|
|
return this_impl->questJournal->completeQuest(questId);
|
|
});
|
|
|
|
// quest_fail(quest_id)
|
|
api.set_function("quest_fail",
|
|
[this_impl = impl.get()](const std::string& questId) -> bool {
|
|
if (!this_impl->questJournal) {
|
|
std::cerr << "[script] quest_fail: QuestJournal not set\n";
|
|
return false;
|
|
}
|
|
return this_impl->questJournal->failQuest(questId);
|
|
});
|
|
|
|
// quest_set_objective_completed(quest_id, objective_id [, completed])
|
|
api.set_function("quest_set_objective_completed",
|
|
[this_impl = impl.get()](const std::string& questId, const std::string& objId, sol::object completed) -> bool {
|
|
if (!this_impl->questJournal) {
|
|
std::cerr << "[script] quest_set_objective_completed: QuestJournal not set\n";
|
|
return false;
|
|
}
|
|
const bool val = completed.is<bool>() ? completed.as<bool>() : true;
|
|
return this_impl->questJournal->setObjectiveCompleted(questId, objId, val);
|
|
});
|
|
|
|
// quest_set_objective_visible(quest_id, objective_id [, visible])
|
|
api.set_function("quest_set_objective_visible",
|
|
[this_impl = impl.get()](const std::string& questId, const std::string& objId, sol::object visible) -> bool {
|
|
if (!this_impl->questJournal) {
|
|
std::cerr << "[script] quest_set_objective_visible: QuestJournal not set\n";
|
|
return false;
|
|
}
|
|
const bool val = visible.is<bool>() ? visible.as<bool>() : true;
|
|
return this_impl->questJournal->setObjectiveVisible(questId, objId, val);
|
|
});
|
|
|
|
// quest_set_active_objective(quest_id, objective_index)
|
|
api.set_function("quest_set_active_objective",
|
|
[this_impl = impl.get()](const std::string& questId, int index) -> bool {
|
|
if (!this_impl->questJournal) {
|
|
std::cerr << "[script] quest_set_active_objective: QuestJournal not set\n";
|
|
return false;
|
|
}
|
|
return this_impl->questJournal->setActiveObjective(questId, index);
|
|
});
|
|
|
|
api.set_function("call_tutorial_taxi_required",
|
|
[loc]() -> void {
|
|
if (loc->onPlayerTaxiRequired)
|
|
{
|
|
loc->onPlayerTaxiRequired();
|
|
loc->onPlayerTaxiRequired = nullptr;
|
|
}
|
|
else {
|
|
std::cerr << "[script] call_tutorial_taxi_required: function is empty\n";
|
|
}
|
|
});
|
|
|
|
lua.script_file(scriptPath);
|
|
}
|
|
|
|
void ScriptEngine::callNpcInteractCallback(int npcIndex) {
|
|
if (!impl) {
|
|
std::cerr << "[SCRIPT] Engine not initialized!" << std::endl;
|
|
return;
|
|
}
|
|
sol::state& lua = impl->lua;
|
|
sol::function fn = lua["on_npc_interact"];
|
|
if (fn.valid()) {
|
|
auto result = fn(npcIndex);
|
|
if (!result.valid()) {
|
|
sol::error err = result;
|
|
std::cerr << "[SCRIPT] on_npc_interact error: " << err.what() << "\n";
|
|
}
|
|
else {
|
|
std::cout << "[SCRIPT] on_npc_interact called with index " << npcIndex << std::endl;
|
|
}
|
|
}
|
|
else {
|
|
std::cerr << "[SCRIPT] Lua function 'on_npc_interact' not found!" << std::endl;
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::runScript(const std::string& path) {
|
|
auto result = impl->lua.safe_script_file(path, sol::script_pass_on_error);
|
|
if (!result.valid()) {
|
|
sol::error err = result;
|
|
std::cerr << "[script] Error in " << path << ": " << err.what() << "\n";
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::callActivateFunction(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!");
|
|
}
|
|
|
|
sol::state& lua = impl->lua;
|
|
std::cout << "[SCRIPT] Looking for activate function: " << functionName << std::endl;
|
|
|
|
sol::function activateFunc = lua[functionName];
|
|
|
|
if (!activateFunc.valid()) {
|
|
throw std::runtime_error("[SCRIPT] Lua function not found: " + functionName);
|
|
}
|
|
|
|
std::cout << "[SCRIPT] Found function! Calling: " << functionName << std::endl;
|
|
auto result = activateFunc();
|
|
|
|
if (!result.valid()) {
|
|
sol::error err = result;
|
|
throw std::runtime_error("[SCRIPT] Error executing " + functionName + ": " + std::string(err.what()));
|
|
}
|
|
|
|
std::cout << "[SCRIPT] Function executed successfully!" << std::endl;
|
|
}
|
|
|
|
void ScriptEngine::callItemPickupCallback(const std::string& objectName) {
|
|
if (!impl) {
|
|
std::cerr << "[SCRIPT] impl is null!" << std::endl;
|
|
return;
|
|
}
|
|
|
|
sol::state& lua = impl->lua;
|
|
|
|
// Try to find custom activate function first
|
|
sol::function activateFunc = lua["on_item_pickup"];
|
|
|
|
if (activateFunc.valid()) {
|
|
std::cout << "[SCRIPT] Callback found! Calling with argument: " << objectName << std::endl;
|
|
auto result = activateFunc(objectName);
|
|
if (!result.valid()) {
|
|
sol::error err = result;
|
|
std::cerr << "[SCRIPT] on_item_pickup callback error: " << err.what() << "\n";
|
|
}
|
|
else {
|
|
std::cout << "[SCRIPT] Callback executed successfully!" << std::endl;
|
|
}
|
|
}
|
|
else {
|
|
std::cout << "[SCRIPT] Fallback: on_item_pickup not found" << std::endl;
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::callTriggerEnterCallback(const std::string& zoneId) {
|
|
if (!impl) return;
|
|
auto it = impl->triggerEnterCallbacks.find(zoneId);
|
|
if (it == impl->triggerEnterCallbacks.end()) return;
|
|
auto result = it->second();
|
|
if (!result.valid()) {
|
|
sol::error err = result;
|
|
std::cerr << "[SCRIPT] trigger enter callback error for '" << zoneId << "': " << err.what() << "\n";
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::callTriggerExitCallback(const std::string& zoneId) {
|
|
if (!impl) return;
|
|
auto it = impl->triggerExitCallbacks.find(zoneId);
|
|
if (it == impl->triggerExitCallbacks.end()) return;
|
|
auto result = it->second();
|
|
if (!result.valid()) {
|
|
sol::error err = result;
|
|
std::cerr << "[SCRIPT] trigger exit callback error for '" << zoneId << "': " << err.what() << "\n";
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::setGlobalStore(std::unordered_map<std::string, int>* store) {
|
|
if (impl) impl->globalInts = store;
|
|
}
|
|
|
|
void ScriptEngine::setGlobalFloatStore(std::unordered_map<std::string, float>* store) {
|
|
if (impl) impl->globalFloats = store;
|
|
}
|
|
|
|
void ScriptEngine::setQuestJournal(Quest::QuestJournal* journal) {
|
|
if (impl) impl->questJournal = journal;
|
|
}
|
|
|
|
void ScriptEngine::callLocationEnterCallback() {
|
|
if (!impl || !impl->locationEnterCallback.valid()) return;
|
|
auto result = impl->locationEnterCallback();
|
|
if (!result.valid()) {
|
|
sol::error err = result;
|
|
std::cerr << "[SCRIPT] location enter callback error: " << err.what() << "\n";
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::callLocationExitCallback() {
|
|
if (!impl || !impl->locationExitCallback.valid()) return;
|
|
auto result = impl->locationExitCallback();
|
|
if (!result.valid()) {
|
|
sol::error err = result;
|
|
std::cerr << "[SCRIPT] location exit callback error: " << err.what() << "\n";
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::callDarklandsEnterCallback() {
|
|
if (!impl || !impl->darklandsEnterCallback.valid()) return;
|
|
auto result = impl->darklandsEnterCallback();
|
|
if (!result.valid()) {
|
|
sol::error err = result;
|
|
std::cerr << "[SCRIPT] darklands enter callback error: " << err.what() << "\n";
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::callTriggerNightEnterCallback() {
|
|
if (!impl || !impl->triggerNightEnterCallback.valid()) return;
|
|
auto result = impl->triggerNightEnterCallback();
|
|
if (!result.valid()) {
|
|
sol::error err = result;
|
|
std::cerr << "[SCRIPT] trigger night enter callback error: " << err.what() << "\n";
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::callDarklandsExitCallback() {
|
|
if (!impl || !impl->darklandsExitCallback.valid()) return;
|
|
auto result = impl->darklandsExitCallback();
|
|
if (!result.valid()) {
|
|
sol::error err = result;
|
|
std::cerr << "[SCRIPT] darklands exit callback error: " << err.what() << "\n";
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::callCallTaxiCallback() {
|
|
if (!impl || !impl->callTaxiCallback.valid()) return;
|
|
auto result = impl->callTaxiCallback();
|
|
if (!result.valid()) {
|
|
sol::error err = result;
|
|
std::cerr << "[SCRIPT] call_taxi callback error: " << err.what() << "\n";
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::callCutsceneCompleteCallback(const std::string& cutsceneId) {
|
|
if (!impl) return;
|
|
auto it = impl->cutsceneCompleteCallbacks.find(cutsceneId);
|
|
if (it == impl->cutsceneCompleteCallbacks.end()) return;
|
|
auto result = it->second();
|
|
if (!result.valid()) {
|
|
sol::error err = result;
|
|
std::cerr << "[SCRIPT] cutscene complete callback error for '"
|
|
<< cutsceneId << "': " << err.what() << "\n";
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::callNpcBumpedByPlayerCallback(int npcIndex) {
|
|
if (!impl) return;
|
|
auto it = impl->npcBumpedByPlayerCallbacks.find(npcIndex);
|
|
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";
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::callChatOpenCallback(int chatIndex) {
|
|
if (!impl) return;
|
|
if (chatIndex < 0 || chatIndex > 2) return;
|
|
auto& fn = impl->chatOpenCallbacks[chatIndex];
|
|
if (!fn.valid()) return;
|
|
auto result = fn();
|
|
if (!result.valid()) {
|
|
sol::error err = result;
|
|
std::cerr << "[SCRIPT] chat open callback error for chat " << chatIndex << ": " << err.what() << "\n";
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::callNpcBumpsPlayerCallback(int npcIndex) {
|
|
if (!impl) return;
|
|
auto it = impl->npcBumpsPlayerCallbacks.find(npcIndex);
|
|
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";
|
|
}
|
|
}
|
|
|
|
} // namespace ZL
|