From 92045ec6df457b33e6c68717b57363d28bbc6755 Mon Sep 17 00:00:00 2001 From: Peter Siegmund Date: Tue, 2 Jun 2026 23:21:56 +0200 Subject: [PATCH] some tweaks still non playable cartridges Signed-off-by: Peter Siegmund --- .gitignore | 1 + main/include/app.h | 8 +- main/include/lua/game_engine.h | 3 + main/include/lua/geo_utils.h | 183 +++++ main/include/ui/map_sim_frame.h | 41 +- main/include/ui/wherigo_dialog.h | 2 +- main/src/app.cpp | 56 +- main/src/lua/game_engine.cpp | 303 ++++++-- main/src/lua/media_manager.cpp | 4 +- main/src/lua/wherigo.cpp | 1221 ++++++++++++++++++------------ main/src/lua/zobject.cpp | 2 +- main/src/ui/game_screen.cpp | 189 ++--- main/src/ui/map_sim_frame.cpp | 424 +++++++++-- main/src/ui/start_screen.cpp | 88 ++- main/src/ui/wherigo_dialog.cpp | 115 +-- 15 files changed, 1802 insertions(+), 838 deletions(-) create mode 100644 main/include/lua/geo_utils.h diff --git a/.gitignore b/.gitignore index 8f5b82c..2b88654 100644 --- a/.gitignore +++ b/.gitignore @@ -113,3 +113,4 @@ CMakeUserPresets.json cartridges/ assets/icon.iconset/ *.lua +*.local.json diff --git a/main/include/app.h b/main/include/app.h index a46d95f..f1a4085 100644 --- a/main/include/app.h +++ b/main/include/app.h @@ -16,16 +16,16 @@ public: int OnExit() override; bool loadCartridge(const std::string &filePath); - void startGame(); + void startGame() const; void unloadCartridge(); // Save/Load game state - bool saveGameState(const std::string &saveFilePath); - bool loadGameState(const std::string &saveFilePath); + bool saveGameState(const std::string &saveFilePath) const; + bool loadGameState(const std::string &saveFilePath) const; std::string getAutoSavePath() const; // Generate completion log (for wherigo.com) - bool generateCompletionLog(const std::string &logFilePath); + bool generateCompletionLog(const std::string &logFilePath) const; std::string getCompletionLogPath() const; lua_State* getLuaState() const { return m_luaState; } diff --git a/main/include/lua/game_engine.h b/main/include/lua/game_engine.h index 90c73cb..a982bfc 100644 --- a/main/include/lua/game_engine.h +++ b/main/include/lua/game_engine.h @@ -39,6 +39,7 @@ public: // Notify listeners that game state has changed void notifyStateChanged(); + void rebuildPlayerInventory(); lua_State* getLuaState() const { return m_luaState; } int getCartridgeRef() const { return m_cartridgeRef; } @@ -64,6 +65,8 @@ private: double m_playerLng = 0.0; double m_playerAlt = 0.0; + bool m_isProcessing = false; + wxDECLARE_EVENT_TABLE(); }; diff --git a/main/include/lua/geo_utils.h b/main/include/lua/geo_utils.h new file mode 100644 index 0000000..cc9ee38 --- /dev/null +++ b/main/include/lua/geo_utils.h @@ -0,0 +1,183 @@ +#pragma once + +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +} + +namespace wherigo { + +// ── Unit conversion ─────────────────────────────────────────────────────────── + +inline double distanceToMeters(double value, const char *units) { + if (!units || !strcmp(units,"meters") || !strcmp(units,"metres") || !strcmp(units,"m")) + return value; + if (!strcmp(units,"kilometers") || !strcmp(units,"kilometres") || !strcmp(units,"km")) + return value * 1000.0; + if (!strcmp(units,"feet") || !strcmp(units,"ft")) + return value * 0.3048; + if (!strcmp(units,"miles") || !strcmp(units,"mi")) + return value * 1609.344; + if (!strcmp(units,"nauticalmiles")) + return value * 1852.0; + return value; +} + +inline double metersToUnit(double meters, const char *units) { + if (!units || !strcmp(units,"meters") || !strcmp(units,"metres") || !strcmp(units,"m")) + return meters; + if (!strcmp(units,"kilometers") || !strcmp(units,"kilometres") || !strcmp(units,"km")) + return meters / 1000.0; + if (!strcmp(units,"feet") || !strcmp(units,"ft")) + return meters / 0.3048; + if (!strcmp(units,"miles") || !strcmp(units,"mi")) + return meters / 1609.344; + if (!strcmp(units,"nauticalmiles")) + return meters / 1852.0; + return meters; +} + +// ── Geodesic calculations ───────────────────────────────────────────────────── + +// Haversine formula → {distance_meters, bearing_degrees [0,360)} +inline std::pair haversine(double lat1, double lon1, + double lat2, double lon2) { + constexpr double R = 6371000.0; + constexpr double D2R = M_PI / 180.0; + const double rl1 = lat1*D2R, rl2 = lat2*D2R; + const double dl = (lat2-lat1)*D2R, dl2 = (lon2-lon1)*D2R; + const double a = std::sin(dl/2)*std::sin(dl/2) + + std::cos(rl1)*std::cos(rl2)*std::sin(dl2/2)*std::sin(dl2/2); + const double dist = R * 2.0 * std::asin(std::min(1.0, std::sqrt(a))); + const double brg = std::atan2(std::sin(dl2)*std::cos(rl2), + std::cos(rl1)*std::sin(rl2) - + std::sin(rl1)*std::cos(rl2)*std::cos(dl2)); + return {dist, std::fmod(brg*(180.0/M_PI)+360.0, 360.0)}; +} + +// Destination point given start, distance (m), bearing (deg) +inline std::pair translatePoint(double lat, double lon, + double distMeters, double bearingDeg) { + constexpr double R = 6371000.0; + constexpr double D2R = M_PI / 180.0; + constexpr double R2D = 180.0 / M_PI; + const double d = distMeters / R; + const double b = bearingDeg * D2R; + const double r1 = lat * D2R; + const double r2 = std::asin(std::sin(r1)*std::cos(d) + + std::cos(r1)*std::sin(d)*std::cos(b)); + const double dl = std::atan2(std::sin(b)*std::sin(d)*std::cos(r1), + std::cos(d) - std::sin(r1)*std::sin(r2)); + return {r2*R2D, lon + dl*R2D}; +} + +// Ray-casting point-in-polygon. pts = {lat, lon} pairs. +inline bool pointInPolygon(double playerLat, double playerLon, + const std::vector>& pts) { + const int n = static_cast(pts.size()); + if (n < 3) return false; + bool inside = false; + for (int i = 0, j = n-1; i < n; j = i++) { + const double lati = pts[i].first, loni = pts[i].second; + const double latj = pts[j].first, lonj = pts[j].second; + if (((lati > playerLat) != (latj > playerLat)) && + (playerLon < (lonj-loni)*(playerLat-lati)/(latj-lati) + loni)) + inside = !inside; + } + return inside; +} + +// Distance (m) from point to nearest point on segment; optionally returns that point +inline double distToSegment(double plat, double plon, + double lat1, double lon1, + double lat2, double lon2, + double *nearLatOut = nullptr, + double *nearLonOut = nullptr) { + const double dlat = lat2-lat1, dlon = lon2-lon1; + const double len2 = dlat*dlat + dlon*dlon; + const double t = (len2 > 1e-12) + ? std::max(0.0, std::min(1.0, + ((plat-lat1)*dlat + (plon-lon1)*dlon) / len2)) + : 0.0; + const double nLat = lat1 + t*dlat, nLon = lon1 + t*dlon; + if (nearLatOut) *nearLatOut = nLat; + if (nearLonOut) *nearLonOut = nLon; + return haversine(plat, plon, nLat, nLon).first; +} + +// Nearest edge of zone polygon → {distance_m, bearing_deg} +inline std::pair nearestZoneEdge( + double plat, double plon, + const std::vector>& pts) { + double minDist = std::numeric_limits::max(), minBrg = 0.0; + const int n = static_cast(pts.size()); + for (int i = 0, j = n-1; i < n; j = i++) { + double nLat, nLon; + const double d = distToSegment(plat, plon, + pts[j].first, pts[j].second, pts[i].first, pts[i].second, + &nLat, &nLon); + if (d < minDist) { + minDist = d; + minBrg = haversine(plat, plon, nLat, nLon).second; + } + } + return {minDist, minBrg}; +} + +// ── Lua table helpers ───────────────────────────────────────────────────────── + +// Push a Distance object (value in meters) with __call metatable +inline void pushDistance(lua_State *L, double meters) { + lua_newtable(L); + lua_pushnumber(L, meters); + lua_setfield(L, -2, "value"); + lua_pushstring(L, "Distance"); + lua_setfield(L, -2, "_classname"); + luaL_getmetatable(L, "Wherigo.Distance"); + if (lua_istable(L, -1)) + lua_setmetatable(L, -2); + else + lua_pop(L, 1); +} + +// Push a Bearing object (value in degrees [0,360)) with __call metatable +inline void pushBearing(lua_State *L, double degrees) { + lua_newtable(L); + lua_pushnumber(L, std::fmod(degrees+360.0, 360.0)); + lua_setfield(L, -2, "value"); + lua_pushstring(L, "Bearing"); + lua_setfield(L, -2, "_classname"); + luaL_getmetatable(L, "Wherigo.Bearing"); + if (lua_istable(L, -1)) + lua_setmetatable(L, -2); + else + lua_pop(L, 1); +} + +// Read zone Points array from Lua table at absolute stack index +inline std::vector> readZonePoints(lua_State *L, int zoneIdx) { + std::vector> pts; + lua_getfield(L, zoneIdx, "Points"); + if (!lua_istable(L, -1)) { lua_pop(L, 1); return pts; } + const int n = static_cast(lua_objlen(L, -1)); + pts.reserve(n); + for (int i = 1; i <= n; ++i) { + lua_rawgeti(L, -1, i); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "latitude"); double lat = lua_tonumber(L,-1); lua_pop(L,1); + lua_getfield(L, -1, "longitude"); double lon = lua_tonumber(L,-1); lua_pop(L,1); + pts.push_back({lat, lon}); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + return pts; +} + +} // namespace wherigo diff --git a/main/include/ui/map_sim_frame.h b/main/include/ui/map_sim_frame.h index f4a60af..a54734c 100644 --- a/main/include/ui/map_sim_frame.h +++ b/main/include/ui/map_sim_frame.h @@ -1,25 +1,52 @@ #pragma once + #include +#include #include -#include +#include #include +#include struct SimPoint { double lat; double lon; }; +struct ZoneInfo { + double lat; + double lon; + std::string name; +}; + class MapSimFrame : public wxFrame { public: - MapSimFrame(wxWindow* parent, double centerLat = 53.3, double centerLon = 10.39, const std::vector>& zoneCoords = {}); + MapSimFrame(wxWindow* parent, + double centerLat = 53.3, double centerLon = 10.39, + const std::vector& zoneInfos = {}); + void AddSimPoint(double lat, double lon); void StartSimulation(); + private: - wxWebView* m_webView; - std::vector m_route; - std::vector> m_zoneCoords; - void OnWebViewEvent(wxWebViewEvent& event); - void OnPlay(wxCommandEvent& event); + wxWebView* m_webView = nullptr; + wxTimer m_simTimer; + size_t m_simIndex = 0; + + std::vector m_route; + std::vector m_zoneInfos; + bool m_pageLoaded = false; + void SendPositionToEngine(double lat, double lon); + + void syncZonesOnMap(); + + void OnScriptMessage(wxWebViewEvent& event); + void OnWebViewLoaded(wxWebViewEvent& event); + void OnGameStateChanged(wxEvent& event); + void OnClose(wxCloseEvent& event); + void OnPlay(wxCommandEvent& event); + void OnClear(wxCommandEvent& event); + void OnSimTimer(wxTimerEvent& event); + wxDECLARE_EVENT_TABLE(); }; diff --git a/main/include/ui/wherigo_dialog.h b/main/include/ui/wherigo_dialog.h index 95b77a2..36d73c2 100644 --- a/main/include/ui/wherigo_dialog.h +++ b/main/include/ui/wherigo_dialog.h @@ -25,7 +25,7 @@ public: int getSelectedButton() const { return m_selectedButton; } private: - void onButton(wxCommandEvent &event); + void onButton(const wxCommandEvent &event); int m_selectedButton = -1; }; diff --git a/main/src/app.cpp b/main/src/app.cpp index 83dd650..76016cf 100644 --- a/main/src/app.cpp +++ b/main/src/app.cpp @@ -6,6 +6,11 @@ #include "lua/wherigo_completion.h" #include "ui/start_screen.h" +#ifdef __APPLE__ +#include +#include +#endif + #include #include #include @@ -26,6 +31,19 @@ bool cApp::OnInit() auto *startFrame = new cStartScreen(); startFrame->Show(true); +#ifdef __APPLE__ + // Force the app to process itself into the foreground (Raise() alone is not enough on macOS) + using MsgFn = id(*)(id, SEL); + using ActivateFn = void(*)(id, SEL, BOOL); + id nsApp = reinterpret_cast(objc_msgSend)( + reinterpret_cast(objc_getClass("NSApplication")), + sel_registerName("sharedApplication")); + reinterpret_cast(objc_msgSend)( + nsApp, sel_registerName("activateIgnoringOtherApps:"), YES); +#else + startFrame->Raise(); +#endif + return true; } @@ -46,7 +64,7 @@ bool cApp::loadCartridge(const std::string &filePath) { } bool cApp::initLuaState() { - auto luac = m_cartridge->luac(); + const auto luac = m_cartridge->luac(); if (!luac) { wxLogError("No Lua bytecode in cartridge"); return false; @@ -68,7 +86,7 @@ bool cApp::initLuaState() { lua_pop(m_luaState, 1); const auto &bytecode = luac->getData(); - int result = luaL_loadbuffer(m_luaState, + const int result = luaL_loadbuffer(m_luaState, reinterpret_cast(bytecode.data()), bytecode.size(), "cartridge"); @@ -98,8 +116,8 @@ bool cApp::initLuaState() { if (lua_istable(m_luaState, -1)) { lua_getfield(m_luaState, -1, "ClassName"); if (lua_isstring(m_luaState, -1)) { - const char *className = lua_tostring(m_luaState, -1); - if (strcmp(className, "ZCartridge") == 0) { + if (const char *className = lua_tostring(m_luaState, -1); + strcmp(className, "ZCartridge") == 0) { lua_pop(m_luaState, 1); // pop ClassName m_cartridgeRef = luaL_ref(m_luaState, LUA_REGISTRYINDEX); // store reference, pops table lua_pop(m_luaState, 1); // pop key @@ -115,6 +133,16 @@ bool cApp::initLuaState() { // Initialize media manager wherigo::MediaManager::getInstance().init(m_luaState, m_cartridge.get()); + // Set Player.CompletionCode from the cartridge binary so Lua can access it + lua_getglobal(m_luaState, "Player"); + if (lua_istable(m_luaState, -1)) { + std::string cc = m_cartridge->completionCode(); + if (cc.length() > 15) cc = cc.substr(0, 15); + lua_pushstring(m_luaState, cc.c_str()); + lua_setfield(m_luaState, -2, "CompletionCode"); + } + lua_pop(m_luaState, 1); + if (m_cartridgeRef == LUA_NOREF) { wxLogError("No ZCartridge object found!"); return false; @@ -126,7 +154,7 @@ bool cApp::initLuaState() { return true; } -void cApp::startGame() { +void cApp::startGame() const { if (!m_luaState || m_cartridgeRef == LUA_NOREF) { wxLogError("No cartridge loaded"); return; @@ -190,38 +218,38 @@ std::string cApp::getAutoSavePath() const { return ""; } -bool cApp::saveGameState(const std::string &saveFilePath) { +bool cApp::saveGameState(const std::string &saveFilePath) const { if (!m_luaState) { wxLogError("No Lua state to save"); return false; } - std::string savePath = saveFilePath.empty() ? getAutoSavePath() : saveFilePath; + const std::string savePath = saveFilePath.empty() ? getAutoSavePath() : saveFilePath; if (savePath.empty()) { wxLogError("No save path available"); return false; } - bool success = wherigo::LuaPersistence::saveState(m_luaState, savePath); + const bool success = wherigo::LuaPersistence::saveState(m_luaState, savePath); if (success) { wxLogMessage("Game saved to: %s", savePath); } return success; } -bool cApp::loadGameState(const std::string &saveFilePath) { +bool cApp::loadGameState(const std::string &saveFilePath) const { if (!m_luaState) { wxLogError("No Lua state to load into"); return false; } - std::string loadPath = saveFilePath.empty() ? getAutoSavePath() : saveFilePath; + const std::string loadPath = saveFilePath.empty() ? getAutoSavePath() : saveFilePath; if (loadPath.empty() || !wxFileExists(loadPath)) { wxLogError("Save file not found: %s", loadPath); return false; } - bool success = wherigo::LuaPersistence::loadState(m_luaState, loadPath); + const bool success = wherigo::LuaPersistence::loadState(m_luaState, loadPath); if (success) { wxLogMessage("Game loaded from: %s", loadPath); // Notify game engine of state change @@ -247,19 +275,19 @@ std::string cApp::getCompletionLogPath() const { return ""; } -bool cApp::generateCompletionLog(const std::string &logFilePath) { +bool cApp::generateCompletionLog(const std::string &logFilePath) const { if (!m_luaState) { wxLogError("No Lua state for completion log"); return false; } - std::string logPath = logFilePath.empty() ? getCompletionLogPath() : logFilePath; + const std::string logPath = logFilePath.empty() ? getCompletionLogPath() : logFilePath; if (logPath.empty()) { wxLogError("No completion log path available"); return false; } - bool success = wherigo::WherigoCompletion::generateCompletionLog(m_luaState, logPath, "Player"); + const bool success = wherigo::WherigoCompletion::generateCompletionLog(m_luaState, logPath, "Player"); if (success) { wxLogMessage("Completion log generated: %s", logPath); } diff --git a/main/src/lua/game_engine.cpp b/main/src/lua/game_engine.cpp index 3e0c0a2..4f1d5be 100644 --- a/main/src/lua/game_engine.cpp +++ b/main/src/lua/game_engine.cpp @@ -1,15 +1,18 @@ #include "lua/game_engine.h" +#include "lua/geo_utils.h" extern "C" { -#include #include +#include } #include +#include +#include + namespace wherigo { -// Define the custom event wxDEFINE_EVENT(EVT_GAME_STATE_CHANGED, GameStateEvent); wxBEGIN_EVENT_TABLE(GameEngine, wxEvtHandler) @@ -21,12 +24,8 @@ GameEngine& GameEngine::getInstance() { return instance; } -GameEngine::GameEngine() : m_gameTimer(this) { -} - -GameEngine::~GameEngine() { - shutdown(); -} +GameEngine::GameEngine() : m_gameTimer(this) {} +GameEngine::~GameEngine() { shutdown(); } void GameEngine::init(lua_State *L, int cartridgeRef) { m_luaState = L; @@ -42,15 +41,13 @@ void GameEngine::shutdown() { void GameEngine::start() { if (m_running) return; - m_running = true; - m_gameTimer.Start(1000); // 1 second tick + m_gameTimer.Start(1000); wxLogDebug("GameEngine started"); } void GameEngine::stop() { if (!m_running) return; - m_gameTimer.Stop(); m_running = false; wxLogDebug("GameEngine stopped"); @@ -67,93 +64,89 @@ void GameEngine::updatePlayerPosition(double lat, double lng, double alt) { lua_getglobal(m_luaState, "Player"); if (lua_istable(m_luaState, -1)) { lua_newtable(m_luaState); - lua_pushnumber(m_luaState, lat); - lua_setfield(m_luaState, -2, "latitude"); - lua_pushnumber(m_luaState, lng); - lua_setfield(m_luaState, -2, "longitude"); - lua_pushnumber(m_luaState, alt); - lua_setfield(m_luaState, -2, "altitude"); + lua_pushnumber(m_luaState, lat); lua_setfield(m_luaState, -2, "latitude"); + lua_pushnumber(m_luaState, lng); lua_setfield(m_luaState, -2, "longitude"); + lua_pushnumber(m_luaState, alt); lua_setfield(m_luaState, -2, "altitude"); lua_setfield(m_luaState, -2, "ObjectLocation"); } lua_pop(m_luaState, 1); - checkZones(); + if (!m_isProcessing) { + m_isProcessing = true; + checkZones(); + notifyStateChanged(); + m_isProcessing = false; + } } -void GameEngine::onGameTick(wxTimerEvent& event) { - if (!m_luaState || !m_running) return; - +void GameEngine::onGameTick(wxTimerEvent&) { + if (!m_luaState || !m_running || m_isProcessing) return; + m_isProcessing = true; checkTimers(); - - // Notify listeners of potential state changes notifyStateChanged(); + m_isProcessing = false; } +// ── Timer processing ────────────────────────────────────────────────────────── + void GameEngine::checkTimers() { if (!m_luaState) return; - // Iterate through all global variables to find running timers lua_getglobal(m_luaState, "_G"); lua_pushnil(m_luaState); while (lua_next(m_luaState, -2) != 0) { if (lua_istable(m_luaState, -1)) { lua_getfield(m_luaState, -1, "ClassName"); - if (lua_isstring(m_luaState, -1) && strcmp(lua_tostring(m_luaState, -1), "ZTimer") == 0) { - lua_pop(m_luaState, 1); // pop ClassName + const bool isTimer = lua_isstring(m_luaState, -1) && + !strcmp(lua_tostring(m_luaState, -1), "ZTimer"); + lua_pop(m_luaState, 1); + if (isTimer) { lua_getfield(m_luaState, -1, "Running"); - bool running = lua_toboolean(m_luaState, -1); + const bool running = lua_toboolean(m_luaState, -1); lua_pop(m_luaState, 1); if (running) { - // Get timer info lua_getfield(m_luaState, -1, "Name"); - const char *name = lua_isstring(m_luaState, -1) ? lua_tostring(m_luaState, -1) : "(unnamed)"; + const char *name = lua_isstring(m_luaState,-1) ? lua_tostring(m_luaState,-1) : "(unnamed)"; lua_pop(m_luaState, 1); lua_getfield(m_luaState, -1, "Remaining"); - lua_Number remaining = lua_isnumber(m_luaState, -1) ? lua_tonumber(m_luaState, -1) : -1; + double remaining = lua_isnumber(m_luaState,-1) ? lua_tonumber(m_luaState,-1) : -1.0; lua_pop(m_luaState, 1); - // If Remaining not set, initialize from Duration if (remaining < 0) { lua_getfield(m_luaState, -1, "Duration"); - remaining = lua_isnumber(m_luaState, -1) ? lua_tonumber(m_luaState, -1) : 0; + remaining = lua_isnumber(m_luaState,-1) ? lua_tonumber(m_luaState,-1) : 0.0; lua_pop(m_luaState, 1); } - // Decrement remaining time remaining -= 1.0; lua_pushnumber(m_luaState, remaining); lua_setfield(m_luaState, -2, "Remaining"); - // Call OnTick if exists lua_getfield(m_luaState, -1, "OnTick"); if (lua_isfunction(m_luaState, -1)) { - lua_pushvalue(m_luaState, -2); // push timer as self + lua_pushvalue(m_luaState, -2); if (lua_pcall(m_luaState, 1, 0, 0) != 0) { - wxLogError("Timer OnTick error: %s", lua_tostring(m_luaState, -1)); + wxLogError("Timer OnTick error: %s", lua_tostring(m_luaState,-1)); lua_pop(m_luaState, 1); } } else { lua_pop(m_luaState, 1); } - // Check if elapsed if (remaining <= 0) { wxLogDebug("Timer elapsed: %s", name); - - // Stop the timer lua_pushboolean(m_luaState, 0); lua_setfield(m_luaState, -2, "Running"); - // Call OnElapsed if exists lua_getfield(m_luaState, -1, "OnElapsed"); if (lua_isfunction(m_luaState, -1)) { - lua_pushvalue(m_luaState, -2); // push timer as self + lua_pushvalue(m_luaState, -2); if (lua_pcall(m_luaState, 1, 0, 0) != 0) { - wxLogError("Timer OnElapsed error: %s", lua_tostring(m_luaState, -1)); + wxLogError("Timer OnElapsed error: %s", lua_tostring(m_luaState,-1)); lua_pop(m_luaState, 1); } } else { @@ -161,49 +154,236 @@ void GameEngine::checkTimers() { } } } - } else { - lua_pop(m_luaState, 1); // pop ClassName } } - lua_pop(m_luaState, 1); // pop value + lua_pop(m_luaState, 1); } - lua_pop(m_luaState, 1); // pop _G + lua_pop(m_luaState, 1); +} + +// ── Zone processing ─────────────────────────────────────────────────────────── + +// Fire a named callback on a zone (absolute index zoneIdx). Stack-neutral. +static void callZoneCallback(lua_State *L, int zoneIdx, + const char *cbName, const char *zoneName) { + lua_getfield(L, zoneIdx, cbName); + if (lua_isfunction(L, -1)) { + wxLogDebug("Zone <%s>: START %s", zoneName, cbName); + lua_pushvalue(L, zoneIdx); + if (lua_pcall(L, 1, 0, 0) != 0) { + wxLogError("Zone %s error: %s", cbName, lua_tostring(L,-1)); + lua_pop(L, 1); + } + wxLogDebug("Zone <%s>: END__ %s", zoneName, cbName); + } else { + lua_pop(L, 1); + } +} + +// Process one zone table at absolute stack index. Returns true if state changed. +static bool updateZone(lua_State *L, int zoneIdx, double playerLat, double playerLng) { + // Zone name (for logging) + lua_getfield(L, zoneIdx, "Name"); + const std::string zoneName = lua_isstring(L,-1) ? lua_tostring(L,-1) : "(unnamed)"; + lua_pop(L, 1); + + // Need at least 3 points for a polygon + const auto pts = readZonePoints(L, zoneIdx); + if (pts.size() < 3) return false; + + // Previous state + lua_getfield(L, zoneIdx, "_inside"); + bool wasInside = lua_toboolean(L, -1); + lua_pop(L, 1); + + lua_getfield(L, zoneIdx, "_state"); + const std::string prevState = lua_isstring(L,-1) ? lua_tostring(L,-1) : "NotInRange"; + lua_pop(L, 1); + + // Proximity / distance range thresholds + double proximityRange = 60.0; + lua_getfield(L, zoneIdx, "ProximityRange"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "value"); + if (lua_isnumber(L,-1)) proximityRange = lua_tonumber(L,-1); + lua_pop(L, 1); + } + lua_pop(L, 1); + + double distanceRange = -0.3048; + lua_getfield(L, zoneIdx, "DistanceRange"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "value"); + if (lua_isnumber(L,-1)) distanceRange = lua_tonumber(L,-1); + lua_pop(L, 1); + } + lua_pop(L, 1); + + // Current position in zone? + bool inside = pointInPolygon(playerLat, playerLng, pts); + + // Distance / bearing to zone boundary + double currentDist = 0.0, currentBrg = 0.0; + if (!inside) { + const auto [d, b] = nearestZoneEdge(playerLat, playerLng, pts); + currentDist = d; + currentBrg = b; + } + + // Update zone fields + pushDistance(L, inside ? 0.0 : currentDist); + lua_setfield(L, zoneIdx, "CurrentDistance"); + pushBearing(L, inside ? 0.0 : currentBrg); + lua_setfield(L, zoneIdx, "CurrentBearing"); + + constexpr double COMFORT = 7.0; // meters hysteresis buffer + bool stateChanged = false; + + // ── Handle enter / exit transitions ──────────────────────────────────────── + if (inside != wasInside) { + if (inside) { + // Entering the zone + if (prevState == "NotInRange") + callZoneCallback(L, zoneIdx, "OnDistant", zoneName.c_str()); + if (prevState != "Proximity" && prevState != "Inside") + callZoneCallback(L, zoneIdx, "OnProximity", zoneName.c_str()); + callZoneCallback(L, zoneIdx, "OnEnter", zoneName.c_str()); + stateChanged = true; + } else if (currentDist > COMFORT) { + // Confirmed exit (beyond hysteresis buffer) + callZoneCallback(L, zoneIdx, "OnExit", zoneName.c_str()); + stateChanged = true; + } else { + // Within comfort zone – stay inside + inside = true; + } + lua_pushboolean(L, inside ? 1 : 0); + lua_setfield(L, zoneIdx, "_inside"); + } + + // ── Determine new state string ────────────────────────────────────────────── + std::string newState; + if (inside) { + newState = "Inside"; + } else if (currentDist < (proximityRange + COMFORT)) { + newState = "Proximity"; + } else if (distanceRange < 0.0 || currentDist < (distanceRange + COMFORT)) { + newState = "Distant"; + } else { + newState = "NotInRange"; + } + + // Fire state-change callbacks for non-inside transitions + if (!inside && newState != prevState) { + stateChanged = true; + if (prevState == "Inside") { + callZoneCallback(L, zoneIdx, "OnProximity", zoneName.c_str()); + if (newState == "NotInRange") + callZoneCallback(L, zoneIdx, "OnDistant", zoneName.c_str()); + } else if (prevState == "Proximity" && newState == "NotInRange") { + callZoneCallback(L, zoneIdx, "OnDistant", zoneName.c_str()); + } else if (prevState == "NotInRange" && newState == "Proximity") { + callZoneCallback(L, zoneIdx, "OnDistant", zoneName.c_str()); + } + } + + lua_pushstring(L, newState.c_str()); lua_setfield(L, zoneIdx, "State"); + lua_pushstring(L, newState.c_str()); lua_setfield(L, zoneIdx, "_state"); + + return stateChanged; } void GameEngine::checkZones() { if (!m_luaState) return; - // Iterate through all global variables to find zones + // Collect all active zone references first (to avoid lua_next invalidation) + std::vector zoneRefs; lua_getglobal(m_luaState, "_G"); lua_pushnil(m_luaState); - while (lua_next(m_luaState, -2) != 0) { if (lua_istable(m_luaState, -1)) { lua_getfield(m_luaState, -1, "ClassName"); - if (lua_isstring(m_luaState, -1) && strcmp(lua_tostring(m_luaState, -1), "Zone") == 0) { - lua_pop(m_luaState, 1); // pop ClassName - + const bool isZone = lua_isstring(m_luaState,-1) && + !strcmp(lua_tostring(m_luaState,-1), "Zone"); + lua_pop(m_luaState, 1); + if (isZone) { lua_getfield(m_luaState, -1, "Active"); - bool active = lua_toboolean(m_luaState, -1); + const bool active = lua_toboolean(m_luaState, -1); + lua_pop(m_luaState, 1); + if (active) { + lua_pushvalue(m_luaState, -1); + zoneRefs.push_back(luaL_ref(m_luaState, LUA_REGISTRYINDEX)); + } + } + } + lua_pop(m_luaState, 1); + } + lua_pop(m_luaState, 1); + + // Process each zone + bool anyChanged = false; + for (const int ref : zoneRefs) { + lua_rawgeti(m_luaState, LUA_REGISTRYINDEX, ref); + if (updateZone(m_luaState, lua_gettop(m_luaState), m_playerLat, m_playerLng)) + anyChanged = true; + lua_pop(m_luaState, 1); + luaL_unref(m_luaState, LUA_REGISTRYINDEX, ref); + } + + if (anyChanged) { + rebuildPlayerInventory(); + notifyStateChanged(); + } +} + +void GameEngine::rebuildPlayerInventory() { + if (!m_luaState) return; + + // Player table + lua_getglobal(m_luaState, "Player"); + if (!lua_istable(m_luaState, -1)) { lua_pop(m_luaState, 1); return; } + const int playerIdx = lua_gettop(m_luaState); + + // New inventory array (only visible items with Container == Player) + lua_newtable(m_luaState); + const int invIdx = lua_gettop(m_luaState); + int count = 0; + + lua_getglobal(m_luaState, "_G"); + const int gIdx = lua_gettop(m_luaState); + lua_pushnil(m_luaState); + + while (lua_next(m_luaState, gIdx) != 0) { + if (lua_istable(m_luaState, -1)) { + lua_getfield(m_luaState, -1, "ClassName"); + const bool isItem = lua_isstring(m_luaState, -1) && + !strcmp(lua_tostring(m_luaState, -1), "ZItem"); + lua_pop(m_luaState, 1); + + if (isItem) { + lua_getfield(m_luaState, -1, "Container"); + const bool inInventory = lua_rawequal(m_luaState, -1, playerIdx); lua_pop(m_luaState, 1); - if (active) { - // TODO: Check if player is inside zone using Points - // For now, just log that we would check - lua_getfield(m_luaState, -1, "Name"); - const char *name = lua_isstring(m_luaState, -1) ? lua_tostring(m_luaState, -1) : "(unnamed)"; + if (inInventory) { + lua_getfield(m_luaState, -1, "Visible"); + const bool visible = lua_toboolean(m_luaState, -1); lua_pop(m_luaState, 1); - // This is where you would implement point-in-polygon check - // and call OnEnter/OnExit callbacks + if (visible) { + lua_pushvalue(m_luaState, -1); + lua_rawseti(m_luaState, invIdx, ++count); + } } - } else { - lua_pop(m_luaState, 1); // pop ClassName } } lua_pop(m_luaState, 1); // pop value } lua_pop(m_luaState, 1); // pop _G + + lua_pushvalue(m_luaState, invIdx); + lua_setfield(m_luaState, playerIdx, "Inventory"); + lua_pop(m_luaState, 2); // pop inventory + Player } void GameEngine::notifyStateChanged() { @@ -212,4 +392,3 @@ void GameEngine::notifyStateChanged() { } } // namespace wherigo - diff --git a/main/src/lua/media_manager.cpp b/main/src/lua/media_manager.cpp index e2e933c..76cbf4c 100644 --- a/main/src/lua/media_manager.cpp +++ b/main/src/lua/media_manager.cpp @@ -69,7 +69,7 @@ void MediaManager::buildMediaIndex(lua_State *L) { } std::vector MediaManager::getMediaByName(const std::string &name) { - auto it = m_nameToIndex.find(name); + const auto it = m_nameToIndex.find(name); if (it == m_nameToIndex.end()) { wxLogWarning("Media not found: %s", name.c_str()); return {}; @@ -84,7 +84,7 @@ std::vector MediaManager::getMediaByIndex(int index) { return {}; } - auto media = m_cartridge->getMedia(index); + const auto media = m_cartridge->getMedia(index); if (!media) { wxLogWarning("Media index %d not found in cartridge", index); return {}; diff --git a/main/src/lua/wherigo.cpp b/main/src/lua/wherigo.cpp index 652d7c1..ee2c2ab 100644 --- a/main/src/lua/wherigo.cpp +++ b/main/src/lua/wherigo.cpp @@ -1,6 +1,8 @@ #include "lua/wherigo.h" #include "app.h" #include "lua/game_engine.h" +#include "lua/geo_utils.h" +#include "lua/media_manager.h" #include "lua/zobject.h" #include "lua/ztimer.h" #include "ui/wherigo_dialog.h" @@ -10,10 +12,13 @@ extern "C" { #include } -#include +#include +#include #include #include -#include +#include +#include +#include #include #include @@ -22,598 +27,808 @@ extern "C" { namespace wherigo { -std::string getCompletionCode(lua_State *L) { - lua_getglobal(L, "Player"); - if (!lua_istable(L, -1)) { +// ── Distance / Bearing metatables ───────────────────────────────────────────── + +// __call on Distance: distance("m") → number +static int distance_call(lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + const char *units = luaL_optstring(L, 2, "meters"); + lua_getfield(L, 1, "value"); + const double meters = lua_tonumber(L, -1); lua_pop(L, 1); - return ""; - } - - lua_getfield(L, -1, "CompletionCode"); - std::string completionCode; - if (lua_isstring(L, -1)) { - completionCode = lua_tostring(L, -1); - if (completionCode.length() > 15) { - completionCode = completionCode.substr(0, 15); - } - } - lua_pop(L, 2); - - return completionCode; + lua_pushnumber(L, metersToUnit(meters, units)); + return 1; } -// Wherigo.RequestSync() +// GetValue closure: captured Distance table as upvalue 1 +static int distance_getvalue_upval(lua_State *L) { + const char *units = luaL_optstring(L, 1, "meters"); + lua_pushvalue(L, lua_upvalueindex(1)); // Distance table + lua_getfield(L, -1, "value"); + const double meters = lua_tonumber(L, -1); + lua_pop(L, 2); + lua_pushnumber(L, metersToUnit(meters, units)); + return 1; +} + +// __call on Bearing: bearing() → number (degrees) +static int bearing_call(lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + lua_getfield(L, 1, "value"); + const double val = lua_tonumber(L, -1); + lua_pop(L, 1); + lua_pushnumber(L, std::fmod(val + 360.0, 360.0)); + return 1; +} + +static void setupMetatables(lua_State *L) { + luaL_newmetatable(L, "Wherigo.Distance"); + lua_pushcfunction(L, distance_call); + lua_setfield(L, -2, "__call"); + lua_pop(L, 1); + + luaL_newmetatable(L, "Wherigo.Bearing"); + lua_pushcfunction(L, bearing_call); + lua_setfield(L, -2, "__call"); + lua_pop(L, 1); +} + +// ── App-level helpers ───────────────────────────────────────────────────────── + +// Returns the loaded cartridge's name, or "Wherigo" as fallback. +static wxString cartridgeTitle() { + if (auto *app = dynamic_cast(wxTheApp); + app && app->isCartridgeLoaded()) + return wxString::FromUTF8(app->getCartridge()->cartridgeName()); + return "Wherigo"; +} + +// ── Completion code helper ──────────────────────────────────────────────────── + +std::string getCompletionCode(lua_State *L) { + lua_getglobal(L, "Player"); + if (!lua_istable(L, -1)) { lua_pop(L, 1); return ""; } + lua_getfield(L, -1, "CompletionCode"); + std::string code; + if (lua_isstring(L, -1)) { + code = lua_tostring(L, -1); + if (code.length() > 15) code = code.substr(0, 15); + } + lua_pop(L, 2); + return code; +} + +// ── Wherigo API functions ───────────────────────────────────────────────────── + static int wherigo_RequestSync(lua_State *L) { - // RequestSync shows the completion code from the cartridge in a MessageBox - wxLogDebug( - "Wherigo.RequestSync() called - showing completion code from cartridge"); - - auto completionCode = getCompletionCode(L); - if (completionCode.empty()) { - wxApp *app = wxTheApp; - if (!app) { - wxLogWarning("Cannot sync: no app instance"); - return 0; + auto completionCode = getCompletionCode(L); + if (completionCode.empty()) { + if (auto *app = dynamic_cast(wxTheApp); + app && app->isCartridgeLoaded()) + completionCode = app->getCartridge()->completionCode().substr(0, 15); } - - cApp *wherigApp = dynamic_cast(app); - if (!wherigApp || !wherigApp->isCartridgeLoaded()) { - wxLogWarning("Cannot sync: no cartridge loaded"); - return 0; - } - - auto cartridge = wherigApp->getCartridge(); - - completionCode = cartridge->completionCode().substr(0, 15); - } - - wxString message = - wxString::Format("Completion Code:\n\n%s\n\nEnter this code on " - "wherigo.com to verify your completion.", - completionCode.c_str()); - - std::vector buttons = {"OK"}; - wherigo::WherigoMessageDialog dlg(nullptr, message, "Wherigo Completion", - buttons); - dlg.ShowModal(); - - return 0; + const wxString msg = wxString::Format( + "Completion Code:\n\n%s\n\nEnter this code on wherigo.com to verify your completion.", + completionCode.c_str()); + wherigo::WherigoMessageDialog dlg(nullptr, msg, cartridgeTitle() + " – Abschluss", {"OK"}); + dlg.ShowModal(); + return 0; } -// Wherigo.ZonePoint(lat, lng, alt) static int wherigo_ZonePoint(lua_State *L) { - lua_Number lat = luaL_checknumber(L, 1); - lua_Number lng = luaL_checknumber(L, 2); - lua_Number alt = luaL_optnumber(L, 3, 0.0); - - lua_newtable(L); - lua_pushnumber(L, lat); - lua_setfield(L, -2, "latitude"); - lua_pushnumber(L, lng); - lua_setfield(L, -2, "longitude"); - lua_pushnumber(L, alt); - lua_setfield(L, -2, "altitude"); - - return 1; + const lua_Number lat = luaL_checknumber(L, 1); + const lua_Number lng = luaL_checknumber(L, 2); + const lua_Number alt = luaL_optnumber(L, 3, 0.0); + lua_newtable(L); + lua_pushnumber(L, lat); lua_setfield(L, -2, "latitude"); + lua_pushnumber(L, lng); lua_setfield(L, -2, "longitude"); + lua_pushnumber(L, alt); lua_setfield(L, -2, "altitude"); + lua_pushstring(L, "ZonePoint"); lua_setfield(L, -2, "_classname"); + return 1; } -// Wherigo.Distance(value, unit) static int wherigo_Distance(lua_State *L) { - lua_Number value = luaL_checknumber(L, 1); - const char *unit = luaL_optstring(L, 2, "meters"); + const lua_Number value = luaL_checknumber(L, 1); + const char *unit = luaL_optstring(L, 2, "meters"); + const double meters = distanceToMeters(value, unit); - lua_newtable(L); - lua_pushnumber(L, value); - lua_setfield(L, -2, "value"); - lua_pushstring(L, unit); - lua_setfield(L, -2, "unit"); + lua_newtable(L); + lua_pushnumber(L, meters); lua_setfield(L, -2, "value"); + lua_pushstring(L, "Distance"); lua_setfield(L, -2, "_classname"); - return 1; + // GetValue closure: captures this Distance table as upvalue + lua_pushvalue(L, -1); + lua_pushcclosure(L, distance_getvalue_upval, 1); + lua_setfield(L, -2, "GetValue"); + + luaL_getmetatable(L, "Wherigo.Distance"); + lua_setmetatable(L, -2); + return 1; } -// Wherigo.Zone(cartridge) +static int wherigo_Bearing(lua_State *L) { + const double deg = luaL_optnumber(L, 1, 0.0); + pushBearing(L, deg); + return 1; +} + +// If arg1 is a plain properties table (no _classname), use it as the object base +// (mirrors the reference: "if type(cartridge)=='table' and not cartridge._classname then self=cartridge"). +// Otherwise create a fresh table. +static void pushObjectBase(lua_State *L) { + if (lua_istable(L, 1)) { + lua_getfield(L, 1, "ClassName"); // our objects use "ClassName" (not "_classname") + const bool isPlainProps = lua_isnil(L, -1); + lua_pop(L, 1); + if (isPlainProps) { lua_pushvalue(L, 1); return; } + } + lua_newtable(L); +} + +// Set field to value only if currently nil/unset — preserves props from caller's table +#define SET_DEFAULT_BOOL(key, val) do { \ + lua_getfield(L, -1, key); \ + if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_pushboolean(L, val); lua_setfield(L, -2, key); } \ + else { lua_pop(L, 1); } } while(0) + +#define SET_DEFAULT_STR(key, val) do { \ + lua_getfield(L, -1, key); \ + if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_pushstring(L, val); lua_setfield(L, -2, key); } \ + else { lua_pop(L, 1); } } while(0) + +#define SET_DEFAULT_NUM(key, val) do { \ + lua_getfield(L, -1, key); \ + if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_pushnumber(L, val); lua_setfield(L, -2, key); } \ + else { lua_pop(L, 1); } } while(0) + +#define SET_DEFAULT_TABLE(key) do { \ + lua_getfield(L, -1, key); \ + if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_newtable(L); lua_setfield(L, -2, key); } \ + else { lua_pop(L, 1); } } while(0) + static int wherigo_Zone(lua_State *L) { - lua_newtable(L); - - lua_pushstring(L, "Zone"); - lua_setfield(L, -2, "ClassName"); - - lua_pushboolean(L, 0); - lua_setfield(L, -2, "Active"); - - lua_pushboolean(L, 1); - lua_setfield(L, -2, "Visible"); - - lua_newtable(L); - lua_setfield(L, -2, "Points"); - - lua_pushcfunction(L, zobject_Contains); - lua_setfield(L, -2, "Contains"); - - return 1; + pushObjectBase(L); + lua_pushstring(L, "Zone"); lua_setfield(L, -2, "ClassName"); + SET_DEFAULT_BOOL("Active", 0); + SET_DEFAULT_BOOL("Visible", 1); + SET_DEFAULT_TABLE("Points"); + // State machine fields – always reset to initial state + lua_pushstring(L, "NotInRange"); lua_setfield(L, -2, "State"); + lua_pushstring(L, "NotInRange"); lua_setfield(L, -2, "_state"); + lua_pushboolean(L, 0); lua_setfield(L, -2, "_inside"); + SET_DEFAULT_STR("ShowObjects", "OnEnter"); + lua_getfield(L, -1, "ProximityRange"); + if (lua_isnil(L, -1)) { lua_pop(L, 1); pushDistance(L, 60.0); lua_setfield(L, -2, "ProximityRange"); } + else { lua_pop(L, 1); } + lua_getfield(L, -1, "DistanceRange"); + if (lua_isnil(L, -1)) { lua_pop(L, 1); pushDistance(L, -0.3048); lua_setfield(L, -2, "DistanceRange"); } + else { lua_pop(L, 1); } + pushDistance(L, 0.0); lua_setfield(L, -2, "CurrentDistance"); + pushBearing(L, 0.0); lua_setfield(L, -2, "CurrentBearing"); + lua_pushcfunction(L, zobject_Contains); lua_setfield(L, -2, "Contains"); + lua_pushcfunction(L, zobject_MoveTo); lua_setfield(L, -2, "MoveTo"); + return 1; } -// Wherigo.ZCharacter(cartridge) static int wherigo_ZCharacter(lua_State *L) { - lua_newtable(L); - - lua_pushstring(L, "ZCharacter"); - lua_setfield(L, -2, "ClassName"); - - lua_pushboolean(L, 0); - lua_setfield(L, -2, "Active"); - - lua_pushboolean(L, 1); - lua_setfield(L, -2, "Visible"); - - lua_pushcfunction(L, zobject_MoveTo); - lua_setfield(L, -2, "MoveTo"); - - lua_pushcfunction(L, zobject_Contains); - lua_setfield(L, -2, "Contains"); - - return 1; + pushObjectBase(L); + lua_pushstring(L, "ZCharacter"); lua_setfield(L, -2, "ClassName"); + SET_DEFAULT_BOOL("Active", 0); + SET_DEFAULT_BOOL("Visible", 1); + lua_pushcfunction(L, zobject_MoveTo); lua_setfield(L, -2, "MoveTo"); + lua_pushcfunction(L, zobject_Contains); lua_setfield(L, -2, "Contains"); + return 1; } -// Wherigo.ZItem(cartridge) static int wherigo_ZItem(lua_State *L) { - lua_newtable(L); - - lua_pushstring(L, "ZItem"); - lua_setfield(L, -2, "ClassName"); - - lua_pushboolean(L, 0); - lua_setfield(L, -2, "Active"); - - lua_pushboolean(L, 1); - lua_setfield(L, -2, "Visible"); - - lua_pushcfunction(L, zobject_MoveTo); - lua_setfield(L, -2, "MoveTo"); - - lua_pushcfunction(L, zobject_Contains); - lua_setfield(L, -2, "Contains"); - - return 1; + pushObjectBase(L); + lua_pushstring(L, "ZItem"); lua_setfield(L, -2, "ClassName"); + SET_DEFAULT_BOOL("Active", 0); + SET_DEFAULT_BOOL("Visible", 1); + lua_pushcfunction(L, zobject_MoveTo); lua_setfield(L, -2, "MoveTo"); + lua_pushcfunction(L, zobject_Contains); lua_setfield(L, -2, "Contains"); + return 1; } -// Wherigo.ZTask(cartridge) static int wherigo_ZTask(lua_State *L) { - lua_newtable(L); - - lua_pushstring(L, "ZTask"); - lua_setfield(L, -2, "ClassName"); - - lua_pushboolean(L, 0); - lua_setfield(L, -2, "Active"); - - lua_pushboolean(L, 0); - lua_setfield(L, -2, "Complete"); - - return 1; + pushObjectBase(L); + lua_pushstring(L, "ZTask"); lua_setfield(L, -2, "ClassName"); + SET_DEFAULT_BOOL("Active", 0); + SET_DEFAULT_BOOL("Complete", 0); + return 1; } -// Wherigo.ZTimer(cartridge) static int wherigo_ZTimer(lua_State *L) { - lua_newtable(L); - - lua_pushstring(L, "ZTimer"); - lua_setfield(L, -2, "ClassName"); - - lua_pushstring(L, "Countdown"); - lua_setfield(L, -2, "Type"); - - lua_pushnumber(L, 0); - lua_setfield(L, -2, "Duration"); - - lua_pushboolean(L, 0); - lua_setfield(L, -2, "Running"); - - lua_pushcfunction(L, ztimer_Start); - lua_setfield(L, -2, "Start"); - - lua_pushcfunction(L, ztimer_Stop); - lua_setfield(L, -2, "Stop"); - - lua_pushcfunction(L, ztimer_Reset); - lua_setfield(L, -2, "Reset"); - - return 1; + pushObjectBase(L); + lua_pushstring(L, "ZTimer"); lua_setfield(L, -2, "ClassName"); + SET_DEFAULT_STR("Type", "Countdown"); + SET_DEFAULT_NUM("Duration", 0); + lua_pushboolean(L, 0); lua_setfield(L, -2, "Running"); + lua_pushcfunction(L, ztimer_Start); lua_setfield(L, -2, "Start"); + lua_pushcfunction(L, ztimer_Stop); lua_setfield(L, -2, "Stop"); + lua_pushcfunction(L, ztimer_Reset); lua_setfield(L, -2, "Reset"); + return 1; } -// Wherigo.ZInput(cartridge) static int wherigo_ZInput(lua_State *L) { - lua_newtable(L); - - lua_pushstring(L, "ZInput"); - lua_setfield(L, -2, "ClassName"); - - lua_pushstring(L, "Text"); - lua_setfield(L, -2, "InputType"); - - return 1; + pushObjectBase(L); + lua_pushstring(L, "ZInput"); lua_setfield(L, -2, "ClassName"); + SET_DEFAULT_STR("InputType", "Text"); + SET_DEFAULT_TABLE("Choices"); + return 1; } -// Wherigo.ZMedia(cartridge) static int s_mediaCounter = 0; static int wherigo_ZMedia(lua_State *L) { - lua_newtable(L); - - lua_pushstring(L, "ZMedia"); - lua_setfield(L, -2, "ClassName"); - - // Store the media index (1-based, 0 is luac) - s_mediaCounter++; - lua_pushinteger(L, s_mediaCounter); - lua_setfield(L, -2, "MediaIndex"); - - return 1; + lua_newtable(L); + lua_pushstring(L, "ZMedia"); lua_setfield(L, -2, "ClassName"); + s_mediaCounter++; + lua_pushinteger(L, s_mediaCounter); lua_setfield(L, -2, "MediaIndex"); + return 1; } void resetMediaCounter() { s_mediaCounter = 0; } -// Wherigo.ZCartridge() static int wherigo_ZCartridge(lua_State *L) { - lua_newtable(L); - - lua_pushstring(L, "ZCartridge"); - lua_setfield(L, -2, "ClassName"); - - lua_newtable(L); - lua_setfield(L, -2, "AllZObjects"); - - lua_pushcfunction(L, wherigo_RequestSync); - lua_setfield(L, -2, "RequestSync"); - - return 1; + lua_newtable(L); + lua_pushstring(L, "ZCartridge"); lua_setfield(L, -2, "ClassName"); + lua_newtable(L); lua_setfield(L, -2, "AllZObjects"); + lua_pushcfunction(L, wherigo_RequestSync); lua_setfield(L, -2, "RequestSync"); + return 1; } -// Wherigo.ZCommand(cartridge) static int wherigo_ZCommand(lua_State *L) { - lua_newtable(L); - - lua_pushstring(L, "ZCommand"); - lua_setfield(L, -2, "ClassName"); - - lua_pushboolean(L, 1); - lua_setfield(L, -2, "Enabled"); - - return 1; + lua_newtable(L); + lua_pushstring(L, "ZCommand"); lua_setfield(L, -2, "ClassName"); + lua_pushboolean(L, 1); lua_setfield(L, -2, "Enabled"); + return 1; } -// Wherigo.Player static int wherigo_Player(lua_State *L) { - lua_newtable(L); + lua_newtable(L); + lua_pushstring(L, "Player"); lua_setfield(L, -2, "ClassName"); + lua_pushstring(L, "Player"); lua_setfield(L, -2, "Name"); + lua_newtable(L); lua_setfield(L, -2, "Inventory"); + lua_newtable(L); lua_setfield(L, -2, "InsideOfZones"); - lua_pushstring(L, "Player"); - lua_setfield(L, -2, "ClassName"); + // ObjectLocation: ZonePoint(0,0,0) + lua_newtable(L); + lua_pushnumber(L, 0.0); lua_setfield(L, -2, "latitude"); + lua_pushnumber(L, 0.0); lua_setfield(L, -2, "longitude"); + lua_pushnumber(L, 0.0); lua_setfield(L, -2, "altitude"); + lua_setfield(L, -2, "ObjectLocation"); - lua_pushstring(L, "Player"); - lua_setfield(L, -2, "Name"); - - lua_newtable(L); - lua_setfield(L, -2, "Inventory"); - - return 1; + pushDistance(L, 5.0); lua_setfield(L, -2, "PositionAccuracy"); + return 1; } -// Wherigo.MessageBox(table) +// ── UI functions ────────────────────────────────────────────────────────────── + static int wherigo_MessageBox(lua_State *L) { - if (!lua_istable(L, 1)) - return 0; + if (!lua_istable(L, 1)) return 0; - // Get Text - lua_getfield(L, 1, "Text"); - const char *text = lua_isstring(L, -1) ? lua_tostring(L, -1) : ""; - lua_pop(L, 1); - - // Get Buttons - std::vector buttons; - lua_getfield(L, 1, "Buttons"); - if (lua_istable(L, -1)) { - int n = lua_objlen(L, -1); - for (int i = 1; i <= n; i++) { - lua_rawgeti(L, -1, i); - if (lua_isstring(L, -1)) { - buttons.push_back(wxString::FromUTF8(lua_tostring(L, -1))); - } - lua_pop(L, 1); - } - } - lua_pop(L, 1); - - // Get Callback - lua_getfield(L, 1, "Callback"); - bool hasCallback = lua_isfunction(L, -1); - int callbackRef = LUA_NOREF; - if (hasCallback) { - callbackRef = luaL_ref(L, LUA_REGISTRYINDEX); - } else { + lua_getfield(L, 1, "Text"); + const char *text = lua_isstring(L, -1) ? lua_tostring(L, -1) : ""; lua_pop(L, 1); - } - wxLogDebug("MessageBox: %s", text); - - // Show dialog - WherigoMessageDialog dlg(nullptr, wxString::FromUTF8(text), "Wherigo", - buttons); - dlg.ShowModal(); - int selected = dlg.getSelectedButton(); - - // Call callback if exists - if (callbackRef != LUA_NOREF) { - lua_rawgeti(L, LUA_REGISTRYINDEX, callbackRef); - if (buttons.empty()) { - lua_pushnil(L); - } else if (selected >= 0 && selected < (int)buttons.size()) { - lua_pushstring(L, buttons[selected].ToUTF8().data()); - } else { - lua_pushnil(L); - } - if (lua_pcall(L, 1, 0, 0) != 0) { - wxLogError("MessageBox callback error: %s", lua_tostring(L, -1)); - lua_pop(L, 1); - } - luaL_unref(L, LUA_REGISTRYINDEX, callbackRef); - } - - // Notify game state change after MessageBox - // This ensures UI updates if state was modified during the MessageBox - GameEngine::getInstance().notifyStateChanged(); - - return 0; -} - -// Wherigo.Dialog(table) -static int wherigo_Dialog(lua_State *L) { - if (!lua_istable(L, 1)) - return 0; - - std::vector entries; - int n = lua_objlen(L, 1); - - for (int i = 1; i <= n; i++) { - lua_rawgeti(L, 1, i); - if (lua_istable(L, -1)) { - DialogEntry entry; - - lua_getfield(L, -1, "Text"); - entry.text = lua_isstring(L, -1) ? lua_tostring(L, -1) : ""; - lua_pop(L, 1); - - // Get Media object name - lua_getfield(L, -1, "Media"); - if (lua_istable(L, -1)) { - lua_getfield(L, -1, "Name"); - entry.mediaName = lua_isstring(L, -1) ? lua_tostring(L, -1) : ""; - lua_pop(L, 1); - } - lua_pop(L, 1); - - lua_getfield(L, -1, "Buttons"); - if (lua_istable(L, -1)) { - int bn = lua_objlen(L, -1); - for (int j = 1; j <= bn; j++) { - lua_rawgeti(L, -1, j); - if (lua_isstring(L, -1)) { - entry.buttons.push_back(lua_tostring(L, -1)); - } - lua_pop(L, 1); - } - } - lua_pop(L, 1); - - entries.push_back(entry); - } - lua_pop(L, 1); - } - - wxLogDebug("Dialog with %zu entries", entries.size()); - for (const auto &entry : entries) { - wxLogDebug(" Text: %s, Media: %s", entry.text.c_str(), - entry.mediaName.empty() ? "(none)" : entry.mediaName.c_str()); - } - - // Show dialogs sequentially - WherigoDialogRunner::getInstance().showDialog(entries); - - return 0; -} - -// Wherigo.GetInput(zinput) -static int wherigo_GetInput(lua_State *L) { - if (lua_istable(L, 1)) { - lua_getfield(L, 1, "Name"); - const char *name = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unnamed)"; - wxLogDebug("GetInput: %s", name); - lua_pop(L, 1); - } - return 0; -} - -// Wherigo.PlayAudio(zmedia) -static int wherigo_PlayAudio(lua_State *L) { - if (lua_istable(L, 1)) { - lua_getfield(L, 1, "Name"); - const char *name = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unnamed)"; - wxLogDebug("PlayAudio: %s", name); - lua_pop(L, 1); - } - return 0; -} - -// Wherigo.ShowScreen(screen, obj) -static int wherigo_ShowScreen(lua_State *L) { - int screen = luaL_optinteger(L, 1, 0); - wxLogDebug("ShowScreen: %d", screen); - - // Show details dialog for DETAILSCREEN - if (screen == 5 && lua_gettop(L) >= 2 && lua_istable(L, 2)) { - // Get Name - lua_getfield(L, 2, "Name"); - std::string name = lua_isstring(L, -1) ? lua_tostring(L, -1) : "Details"; - lua_pop(L, 1); - // Get Description - lua_getfield(L, 2, "Description"); - std::string desc = lua_isstring(L, -1) ? lua_tostring(L, -1) : ""; - lua_pop(L, 1); - // Get Media name (prefer Media, fallback to Icon) + // Media: ZMedia table → Name field (used for image display) std::string mediaName; - lua_getfield(L, 2, "Media"); + lua_getfield(L, 1, "Media"); if (lua_istable(L, -1)) { - lua_getfield(L, -1, "Name"); - if (lua_isstring(L, -1)) { - mediaName = lua_tostring(L, -1); - } - lua_pop(L, 1); + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) mediaName = lua_tostring(L, -1); + lua_pop(L, 1); } lua_pop(L, 1); - if (mediaName.empty()) { - lua_getfield(L, 2, "Icon"); - if (lua_istable(L, -1)) { - lua_getfield(L, -1, "Name"); - if (lua_isstring(L, -1)) { - mediaName = lua_tostring(L, -1); + + std::vector buttons; + lua_getfield(L, 1, "Buttons"); + if (lua_istable(L, -1)) { + const int n = lua_objlen(L, -1); + for (int i = 1; i <= n; i++) { + lua_rawgeti(L, -1, i); + if (lua_isstring(L, -1)) + buttons.push_back(wxString::FromUTF8(lua_tostring(L, -1))); + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + lua_getfield(L, 1, "Callback"); + const bool hasCallback = lua_isfunction(L, -1); + int callbackRef = LUA_NOREF; + if (hasCallback) + callbackRef = luaL_ref(L, LUA_REGISTRYINDEX); + else + lua_pop(L, 1); + + wxLogDebug("MessageBox: %s", text); + WherigoMessageDialog dlg(nullptr, wxString::FromUTF8(text), cartridgeTitle(), buttons, + wxString::FromUTF8(mediaName)); + dlg.ShowModal(); + const int selected = dlg.getSelectedButton(); + + if (callbackRef != LUA_NOREF) { + lua_rawgeti(L, LUA_REGISTRYINDEX, callbackRef); + if (!buttons.empty() && selected >= 0 && selected < static_cast(buttons.size())) + lua_pushstring(L, buttons[selected].ToUTF8().data()); + else if (buttons.empty() && selected >= 0) + lua_pushstring(L, ""); // OK clicked → "" keeps chain alive + else + lua_pushnil(L); // window closed with X → nil breaks the chain + if (lua_pcall(L, 1, 0, 0) != 0) { + wxLogError("MessageBox callback error: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + } + luaL_unref(L, LUA_REGISTRYINDEX, callbackRef); + } + + GameEngine::getInstance().notifyStateChanged(); + return 0; +} + +static int wherigo_Dialog(lua_State *L) { + if (!lua_istable(L, 1)) return 0; + + std::vector entries; + const int n = lua_objlen(L, 1); + for (int i = 1; i <= n; i++) { + lua_rawgeti(L, 1, i); + if (lua_istable(L, -1)) { + DialogEntry entry; + lua_getfield(L, -1, "Text"); + entry.text = lua_isstring(L, -1) ? lua_tostring(L, -1) : ""; + lua_pop(L, 1); + + lua_getfield(L, -1, "Media"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "Name"); + entry.mediaName = lua_isstring(L, -1) ? lua_tostring(L, -1) : ""; + lua_pop(L, 1); + } + lua_pop(L, 1); + + lua_getfield(L, -1, "Buttons"); + if (lua_istable(L, -1)) { + const int bn = lua_objlen(L, -1); + for (int j = 1; j <= bn; j++) { + lua_rawgeti(L, -1, j); + if (lua_isstring(L, -1)) entry.buttons.push_back(lua_tostring(L, -1)); + lua_pop(L, 1); + } + } + lua_pop(L, 1); + entries.push_back(entry); } lua_pop(L, 1); - } - lua_pop(L, 1); } - // Show dialog with media if available - std::vector buttons = {"OK"}; - wherigo::WherigoMessageDialog dlg(nullptr, wxString::FromUTF8(desc.c_str()), wxString::FromUTF8(name.c_str()), buttons, wxString::FromUTF8(mediaName.c_str())); - dlg.ShowModal(); + + for (const auto &e : entries) + wxLogDebug("Dialog: %s", e.text.c_str()); + + WherigoDialogRunner::getInstance().showDialog(entries); + return 0; +} + +static int wherigo_GetInput(lua_State *L) { + if (!lua_istable(L, 1)) return 0; + + lua_getfield(L, 1, "InputType"); + const std::string inputType = lua_isstring(L,-1) ? lua_tostring(L,-1) : "Text"; + lua_pop(L, 1); + + lua_getfield(L, 1, "Text"); + const wxString prompt = lua_isstring(L,-1) + ? wxString::FromUTF8(lua_tostring(L,-1)) : wxString("Eingabe:"); + lua_pop(L, 1); + + lua_getfield(L, 1, "Name"); + const wxString inputName = lua_isstring(L,-1) + ? wxString::FromUTF8(lua_tostring(L,-1)) : wxString("(unnamed)"); + lua_pop(L, 1); + + // Debug: log question text + wxLogDebug("GetInput[%s] Frage: %s", inputType, prompt); + + std::string response; + + if (inputType == "MultipleChoice") { + wxArrayString choices; + lua_getfield(L, 1, "Choices"); + if (lua_istable(L, -1)) { + const int n = lua_objlen(L, -1); + for (int i = 1; i <= n; i++) { + lua_rawgeti(L, -1, i); + if (lua_isstring(L, -1)) { + wxString c = wxString::FromUTF8(lua_tostring(L,-1)); + choices.Add(c); + wxLogDebug("GetInput Option: %s", c); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + if (!choices.IsEmpty()) { + wxSingleChoiceDialog dlg(nullptr, prompt, cartridgeTitle(), choices); + if (dlg.ShowModal() == wxID_OK) + response = dlg.GetStringSelection().ToUTF8().data(); + } + } else { + wxLogDebug("GetInput Freitext (Name: %s)", inputName); + wxTextEntryDialog dlg(nullptr, prompt, cartridgeTitle(), ""); + if (dlg.ShowModal() == wxID_OK) + response = dlg.GetValue().ToUTF8().data(); + } + + lua_getfield(L, 1, "OnGetInput"); + if (lua_isfunction(L, -1)) { + lua_pushvalue(L, 1); + lua_pushstring(L, response.c_str()); + if (lua_pcall(L, 2, 0, 0) != 0) { + wxLogError("GetInput OnGetInput error: %s", lua_tostring(L,-1)); + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + + // OnGetInput may have changed item visibility → rebuild inventory + GameEngine::getInstance().rebuildPlayerInventory(); + GameEngine::getInstance().notifyStateChanged(); + return 0; +} + +// ── Audio playback ──────────────────────────────────────────────────────────── + +static long s_audioPid = 0; +static wxString s_audioTemp; + +static void stopAudio() { + if (s_audioPid > 0) { + wxProcess::Kill(s_audioPid, wxSIGKILL, wxKILL_CHILDREN); + s_audioPid = 0; + } + if (!s_audioTemp.empty()) { + wxRemoveFile(s_audioTemp); + s_audioTemp.clear(); + } +} + +static void playAudio(const std::vector& data, const std::string& ext) { + stopAudio(); + + static int s_counter = 0; + s_audioTemp = wxFileName::GetTempDir() + + wxFileName::GetPathSeparator() + + wxString::Format("wherigo_%d.%s", s_counter++, + wxString::FromUTF8(ext).Lower()); + + { + wxFile f(s_audioTemp, wxFile::write); + if (!f.IsOpened()) { + wxLogWarning("PlayAudio: cannot write temp file %s", s_audioTemp); + s_audioTemp.clear(); + return; + } + f.Write(data.data(), data.size()); + } + +#if defined(__APPLE__) + const wxString cmd = wxString::Format("afplay \"%s\"", s_audioTemp); +#elif defined(__linux__) + const wxString cmd = wxString::Format( + "mpg123 -q \"%s\" 2>/dev/null || " + "ffplay -nodisp -autoexit -loglevel quiet \"%s\"", + s_audioTemp, s_audioTemp); +#else + const wxString cmd = wxString::Format( + "powershell -c \"Add-Type -AssemblyName presentationCore;" + "$mp=[Windows.Media.Playback.MediaPlayer]::new();" + "$mp.Source=[Windows.Media.Core.MediaSource]::CreateFromUri('%s');" + "$mp.Play();Start-Sleep -s 300\"", + s_audioTemp); +#endif + + wxLogDebug("PlayAudio: launching %s", cmd); + s_audioPid = wxExecute(cmd, wxEXEC_ASYNC); + if (s_audioPid <= 0) { + wxLogWarning("PlayAudio: failed to start player"); + wxRemoveFile(s_audioTemp); + s_audioTemp.clear(); + s_audioPid = 0; + } +} + +static int wherigo_PlayAudio(lua_State *L) { + if (!lua_istable(L, 1)) return 0; + + lua_getfield(L, 1, "Name"); + wxLogDebug("PlayAudio: %s", lua_isstring(L,-1) ? lua_tostring(L,-1) : "(unnamed)"); + lua_pop(L, 1); + + // MediaIndex assigned by wherigo_ZMedia at construction time + lua_getfield(L, 1, "MediaIndex"); + const int mediaIndex = lua_isnumber(L,-1) ? lua_tointeger(L,-1) : -1; + lua_pop(L, 1); + if (mediaIndex <= 0) return 0; + + // File extension from Resources[1].Type (set by cartridge code) + std::string ext = "mp3"; + lua_getfield(L, 1, "Resources"); + if (lua_istable(L, -1)) { + lua_rawgeti(L, -1, 1); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "Type"); + if (lua_isstring(L, -1)) ext = lua_tostring(L, -1); + lua_pop(L, 1); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + const auto data = MediaManager::getInstance().getMediaByIndex(mediaIndex); + if (data.empty()) { + wxLogWarning("PlayAudio: no data for MediaIndex %d", mediaIndex); + return 0; + } + + playAudio(data, ext); + return 0; +} + +static int wherigo_ShowScreen(lua_State *L) { + const int screen = luaL_optinteger(L, 1, 0); + wxLogDebug("ShowScreen: %d", screen); + + if (screen == 5 && lua_gettop(L) >= 2 && lua_istable(L, 2)) { + lua_getfield(L, 2, "Name"); + const std::string name = lua_isstring(L,-1) ? lua_tostring(L,-1) : "Details"; + lua_pop(L, 1); + lua_getfield(L, 2, "Description"); + const std::string desc = lua_isstring(L,-1) ? lua_tostring(L,-1) : ""; + lua_pop(L, 1); + + std::string mediaName; + lua_getfield(L, 2, "Media"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) mediaName = lua_tostring(L, -1); + lua_pop(L, 1); + } + lua_pop(L, 1); + if (mediaName.empty()) { + lua_getfield(L, 2, "Icon"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) mediaName = lua_tostring(L, -1); + lua_pop(L, 1); + } + lua_pop(L, 1); + } + WherigoMessageDialog dlg(nullptr, + wxString::FromUTF8(desc), wxString::FromUTF8(name), + {"OK"}, wxString::FromUTF8(mediaName)); + dlg.ShowModal(); + } + return 0; +} + +static int wherigo_ShowStatusText(lua_State *L) { + const char *text = luaL_checkstring(L, 1); + wxLogDebug("ShowStatusText: %s", text); return 0; - } - return 0; } -// Wherigo.LogMessage(message, level) static int wherigo_LogMessage(lua_State *L) { - const char *message = luaL_checkstring(L, 1); - wxLogDebug("Wherigo.LogMessage: %s", message); - return 0; + const char *msg = luaL_checkstring(L, 1); + wxLogDebug("Wherigo.LogMessage: %s", msg); + return 0; } -// Wherigo.NoCaseEquals(str1, str2) +static int wherigo_Command(lua_State *L) { + const char *text = luaL_checkstring(L, 1); + wxLogDebug("Wherigo.Command: %s", text); + if (!strcmp(text, "SaveClose")) { + wxLogMessage("Wherigo.Command: SaveClose – exiting"); + wxTheApp->ExitMainLoop(); + } else if (!strcmp(text, "StopSound")) { + stopAudio(); + } else if (!strcmp(text, "Alert")) { + wxBell(); + } + return 0; +} + +// ── String utilities ────────────────────────────────────────────────────────── + static int wherigo_NoCaseEquals(lua_State *L) { - const char *str1 = luaL_checkstring(L, 1); - const char *str2 = luaL_checkstring(L, 2); - - std::string s1(str1); - std::string s2(str2); - - std::transform(s1.begin(), s1.end(), s1.begin(), ::tolower); - std::transform(s2.begin(), s2.end(), s2.begin(), ::tolower); - - lua_pushboolean(L, s1 == s2); - return 1; + const char *s1 = luaL_checkstring(L, 1); + const char *s2 = luaL_checkstring(L, 2); + std::string a(s1), b(s2); + std::transform(a.begin(), a.end(), a.begin(), ::tolower); + std::transform(b.begin(), b.end(), b.begin(), ::tolower); + lua_pushboolean(L, a == b); + return 1; } -// Wherigo.TranslatePoint(point, distance, bearing) -static int wherigo_TranslatePoint(lua_State *L) { - // TODO: implement point translation - lua_newtable(L); - return 1; +// ── Geo functions ───────────────────────────────────────────────────────────── + +static int wherigo_IsPointInZone(lua_State *L) { + if (!lua_istable(L, 1) || !lua_istable(L, 2)) { + lua_pushboolean(L, 0); + return 1; + } + lua_getfield(L, 1, "latitude"); const double lat = lua_tonumber(L,-1); lua_pop(L,1); + lua_getfield(L, 1, "longitude"); const double lon = lua_tonumber(L,-1); lua_pop(L,1); + const auto pts = readZonePoints(L, 2); + lua_pushboolean(L, pointInPolygon(lat, lon, pts) ? 1 : 0); + return 1; } -// Wherigo.VectorToZone(point, zone) -static int wherigo_VectorToZone(lua_State *L) { - lua_newtable(L); - lua_pushnumber(L, 0); - lua_setfield(L, -2, "distance"); - lua_pushnumber(L, 0); - lua_setfield(L, -2, "bearing"); - lua_pushboolean(L, 0); - lua_setfield(L, -2, "inside"); - return 1; -} - -// Wherigo.VectorToPoint(point1, point2) static int wherigo_VectorToPoint(lua_State *L) { - lua_newtable(L); - lua_pushnumber(L, 0); - lua_setfield(L, -2, "distance"); - lua_pushnumber(L, 0); - lua_setfield(L, -2, "bearing"); - return 1; + if (!lua_istable(L, 1) || !lua_istable(L, 2)) { + pushDistance(L, 0.0); pushBearing(L, 0.0); return 2; + } + lua_getfield(L, 1, "latitude"); const double lat1 = lua_tonumber(L,-1); lua_pop(L,1); + lua_getfield(L, 1, "longitude"); const double lon1 = lua_tonumber(L,-1); lua_pop(L,1); + lua_getfield(L, 2, "latitude"); const double lat2 = lua_tonumber(L,-1); lua_pop(L,1); + lua_getfield(L, 2, "longitude"); const double lon2 = lua_tonumber(L,-1); lua_pop(L,1); + const auto [dist, brg] = haversine(lat1, lon1, lat2, lon2); + pushDistance(L, dist); + pushBearing(L, brg); + return 2; } -// Wherigo.VectorToSegment(point, point1, point2) static int wherigo_VectorToSegment(lua_State *L) { - lua_newtable(L); - lua_pushnumber(L, 0); - lua_setfield(L, -2, "distance"); - lua_pushnumber(L, 0); - lua_setfield(L, -2, "bearing"); - return 1; + if (!lua_istable(L, 1) || !lua_istable(L, 2) || !lua_istable(L, 3)) { + pushDistance(L, 0.0); pushBearing(L, 0.0); return 2; + } + lua_getfield(L, 1, "latitude"); const double plat = lua_tonumber(L,-1); lua_pop(L,1); + lua_getfield(L, 1, "longitude"); const double plon = lua_tonumber(L,-1); lua_pop(L,1); + lua_getfield(L, 2, "latitude"); const double lat1 = lua_tonumber(L,-1); lua_pop(L,1); + lua_getfield(L, 2, "longitude"); const double lon1 = lua_tonumber(L,-1); lua_pop(L,1); + lua_getfield(L, 3, "latitude"); const double lat2 = lua_tonumber(L,-1); lua_pop(L,1); + lua_getfield(L, 3, "longitude"); const double lon2 = lua_tonumber(L,-1); lua_pop(L,1); + double nLat, nLon; + const double dist = distToSegment(plat, plon, lat1, lon1, lat2, lon2, &nLat, &nLon); + const double brg = haversine(plat, plon, nLat, nLon).second; + pushDistance(L, dist); + pushBearing(L, brg); + return 2; } +static int wherigo_VectorToZone(lua_State *L) { + if (!lua_istable(L, 1) || !lua_istable(L, 2)) { + pushDistance(L, 0.0); pushBearing(L, 0.0); return 2; + } + lua_getfield(L, 1, "latitude"); const double plat = lua_tonumber(L,-1); lua_pop(L,1); + lua_getfield(L, 1, "longitude"); const double plon = lua_tonumber(L,-1); lua_pop(L,1); + const auto pts = readZonePoints(L, 2); + if (pointInPolygon(plat, plon, pts)) { + pushDistance(L, 0.0); pushBearing(L, 0.0); return 2; + } + const auto [dist, brg] = nearestZoneEdge(plat, plon, pts); + pushDistance(L, dist); + pushBearing(L, brg); + return 2; +} + +static int wherigo_TranslatePoint(lua_State *L) { + if (!lua_istable(L, 1)) { lua_pushnil(L); return 1; } + + lua_getfield(L, 1, "latitude"); const double lat = lua_tonumber(L,-1); lua_pop(L,1); + lua_getfield(L, 1, "longitude"); const double lon = lua_tonumber(L,-1); lua_pop(L,1); + lua_getfield(L, 1, "altitude"); + const double alt = lua_isnumber(L,-1) ? lua_tonumber(L,-1) : 0.0; + lua_pop(L, 1); + + double distMeters = 0.0; + if (lua_istable(L, 2)) { + lua_getfield(L, 2, "value"); distMeters = lua_tonumber(L,-1); lua_pop(L,1); + } else if (lua_isnumber(L, 2)) { + distMeters = lua_tonumber(L, 2); + } + + double bearingDeg = 0.0; + if (lua_istable(L, 3)) { + lua_getfield(L, 3, "value"); bearingDeg = lua_tonumber(L,-1); lua_pop(L,1); + } else if (lua_isnumber(L, 3)) { + bearingDeg = lua_tonumber(L, 3); + } + + const auto [newLat, newLon] = translatePoint(lat, lon, distMeters, bearingDeg); + lua_newtable(L); + lua_pushnumber(L, newLat); lua_setfield(L, -2, "latitude"); + lua_pushnumber(L, newLon); lua_setfield(L, -2, "longitude"); + lua_pushnumber(L, alt); lua_setfield(L, -2, "altitude"); + lua_pushstring(L, "ZonePoint"); lua_setfield(L, -2, "_classname"); + return 1; +} + +// ── Function table & module init ────────────────────────────────────────────── + static const luaL_Reg wherigo_funcs[] = { - {"ZonePoint", wherigo_ZonePoint}, - {"Distance", wherigo_Distance}, - {"Zone", wherigo_Zone}, - {"ZCharacter", wherigo_ZCharacter}, - {"ZItem", wherigo_ZItem}, - {"ZTask", wherigo_ZTask}, - {"ZTimer", wherigo_ZTimer}, - {"ZInput", wherigo_ZInput}, - {"ZMedia", wherigo_ZMedia}, - {"ZCartridge", wherigo_ZCartridge}, - {"ZCommand", wherigo_ZCommand}, - {"MessageBox", wherigo_MessageBox}, - {"Dialog", wherigo_Dialog}, - {"GetInput", wherigo_GetInput}, - {"PlayAudio", wherigo_PlayAudio}, - {"ShowScreen", wherigo_ShowScreen}, - {"LogMessage", wherigo_LogMessage}, - {"NoCaseEquals", wherigo_NoCaseEquals}, + {"ZonePoint", wherigo_ZonePoint}, + {"Distance", wherigo_Distance}, + {"Bearing", wherigo_Bearing}, + {"Zone", wherigo_Zone}, + {"ZCharacter", wherigo_ZCharacter}, + {"ZItem", wherigo_ZItem}, + {"ZTask", wherigo_ZTask}, + {"ZTimer", wherigo_ZTimer}, + {"ZInput", wherigo_ZInput}, + {"ZMedia", wherigo_ZMedia}, + {"ZCartridge", wherigo_ZCartridge}, + {"ZCommand", wherigo_ZCommand}, + {"MessageBox", wherigo_MessageBox}, + {"Dialog", wherigo_Dialog}, + {"GetInput", wherigo_GetInput}, + {"PlayAudio", wherigo_PlayAudio}, + {"ShowScreen", wherigo_ShowScreen}, + {"ShowStatusText", wherigo_ShowStatusText}, + {"LogMessage", wherigo_LogMessage}, + {"Command", wherigo_Command}, + {"NoCaseEquals", wherigo_NoCaseEquals}, + {"IsPointInZone", wherigo_IsPointInZone}, {"TranslatePoint", wherigo_TranslatePoint}, - {"VectorToZone", wherigo_VectorToZone}, - {"VectorToPoint", wherigo_VectorToPoint}, - {"VectorToSegment", wherigo_VectorToSegment}, - {"RequestSync", wherigo_RequestSync}, - {nullptr, nullptr}}; + {"VectorToZone", wherigo_VectorToZone}, + {"VectorToPoint", wherigo_VectorToPoint}, + {"VectorToSegment",wherigo_VectorToSegment}, + {"RequestSync", wherigo_RequestSync}, + {nullptr, nullptr} +}; int luaopen_Wherigo(lua_State *L) { - luaL_register(L, "Wherigo", wherigo_funcs); + // Register metatables before any object constructors are used + setupMetatables(L); - // Create Player object - wherigo_Player(L); - lua_setfield(L, -2, "Player"); + luaL_register(L, "Wherigo", wherigo_funcs); - // Screen constants - lua_pushnumber(L, 0); - lua_setfield(L, -2, "MAINSCREEN"); + // Player object + // One Player table, referenced by both Wherigo.Player and the global Player + wherigo_Player(L); // push Player table + lua_pushvalue(L, -1); // duplicate + lua_setfield(L, -3, "Player"); // Wherigo.Player = table (pops duplicate) + lua_setglobal(L, "Player"); // Player (global) = same table (pops original) - lua_pushnumber(L, 1); - lua_setfield(L, -2, "LOCATIONSCREEN"); + // Screen constants (numbers matching original Groundspeak spec) + lua_pushnumber(L, 0); lua_setfield(L, -2, "MAINSCREEN"); + lua_pushnumber(L, 1); lua_setfield(L, -2, "LOCATIONSCREEN"); + lua_pushnumber(L, 2); lua_setfield(L, -2, "ITEMSCREEN"); + lua_pushnumber(L, 3); lua_setfield(L, -2, "INVENTORYSCREEN"); + lua_pushnumber(L, 4); lua_setfield(L, -2, "TASKSCREEN"); + lua_pushnumber(L, 5); lua_setfield(L, -2, "DETAILSCREEN"); - lua_pushnumber(L, 2); - lua_setfield(L, -2, "ITEMSCREEN"); + // Log level constants + lua_pushnumber(L, 150); lua_setfield(L, -2, "LOGDEBUG"); + lua_pushnumber(L, 151); lua_setfield(L, -2, "LOGCARTRIDGE"); + lua_pushnumber(L, 152); lua_setfield(L, -2, "LOGINFO"); + lua_pushnumber(L, 153); lua_setfield(L, -2, "LOGWARNING"); + lua_pushnumber(L, 154); lua_setfield(L, -2, "LOGERROR"); - lua_pushnumber(L, 3); - lua_setfield(L, -2, "INVENTORYSCREEN"); + // Class name constants + lua_pushstring(L, "Zone"); lua_setfield(L, -2, "CLASS_ZONE"); + lua_pushstring(L, "ZMedia"); lua_setfield(L, -2, "CLASS_ZMEDIA"); + lua_pushstring(L, "ZCartridge"); lua_setfield(L, -2, "CLASS_ZCARTRIDGE"); + lua_pushstring(L, "ZCharacter"); lua_setfield(L, -2, "CLASS_ZCHARACTER"); + lua_pushstring(L, "ZCommand"); lua_setfield(L, -2, "CLASS_ZCOMMAND"); + lua_pushstring(L, "ZInput"); lua_setfield(L, -2, "CLASS_ZINPUT"); + lua_pushstring(L, "ZTask"); lua_setfield(L, -2, "CLASS_ZTASK"); + lua_pushstring(L, "ZItem"); lua_setfield(L, -2, "CLASS_ZITEM"); + lua_pushstring(L, "ZTimer"); lua_setfield(L, -2, "CLASS_ZTIMER"); + lua_pushstring(L, "Distance"); lua_setfield(L, -2, "CLASS_DISTANCE"); + lua_pushstring(L, "Bearing"); lua_setfield(L, -2, "CLASS_BEARING"); + lua_pushstring(L, "ZonePoint"); lua_setfield(L, -2, "CLASS_ZONEPOINT"); - lua_pushnumber(L, 4); - lua_setfield(L, -2, "TASKSCREEN"); + // INVALID_ZONEPOINT + lua_pushnil(L); lua_setfield(L, -2, "INVALID_ZONEPOINT"); - lua_pushnumber(L, 5); - lua_setfield(L, -2, "DETAILSCREEN"); + // Register globals expected by Wherigo Builder output + lua_pushcfunction(L, wherigo_ZonePoint); lua_setglobal(L, "ZonePoint"); + lua_pushcfunction(L, wherigo_Distance); lua_setglobal(L, "Distance"); - // Register common functions as globals (as expected by Wherigo Builder) - lua_pushcfunction(L, wherigo_ZonePoint); - lua_setglobal(L, "ZonePoint"); - - lua_pushcfunction(L, wherigo_Distance); - lua_setglobal(L, "Distance"); - - // Register Player as global - wherigo_Player(L); - lua_setglobal(L, "Player"); - - return 1; + return 1; } } // namespace wherigo diff --git a/main/src/lua/zobject.cpp b/main/src/lua/zobject.cpp index 1208f13..ed660f0 100644 --- a/main/src/lua/zobject.cpp +++ b/main/src/lua/zobject.cpp @@ -28,7 +28,7 @@ int zobject_MoveTo(lua_State *L) { wxLogDebug("MoveTo: %s -> %s", srcName, dstName); - // Notify game state change + GameEngine::getInstance().rebuildPlayerInventory(); GameEngine::getInstance().notifyStateChanged(); return 0; diff --git a/main/src/ui/game_screen.cpp b/main/src/ui/game_screen.cpp index aae0942..57e64da 100644 --- a/main/src/ui/game_screen.cpp +++ b/main/src/ui/game_screen.cpp @@ -37,7 +37,7 @@ cGameScreen::cGameScreen(wxWindow *parent) auto *menuBar = new wxMenuBar; menuBar->Append(menuFile, "&Datei"); menuBar->Append(menuHelp, "&Hilfe"); - SetMenuBar(menuBar); + wxFrameBase::SetMenuBar(menuBar); // Main sizer auto *mainSizer = new wxBoxSizer(wxVERTICAL); @@ -45,15 +45,6 @@ cGameScreen::cGameScreen(wxWindow *parent) // Notebook with tabs m_notebook = new wxNotebook(this, wxID_ANY); - // Locations/Zones tab - auto *zonePanel = new wxPanel(m_notebook); - auto *zoneSizer = new wxBoxSizer(wxVERTICAL); - zoneSizer->Add(new wxStaticText(zonePanel, wxID_ANY, "Orte in der Nähe:"), 0, wxALL, 5); - m_zoneList = new wxListBox(zonePanel, wxID_ANY); - zoneSizer->Add(m_zoneList, 1, wxALL | wxEXPAND, 5); - zonePanel->SetSizer(zoneSizer); - m_notebook->AddPage(zonePanel, "Orte"); - // Tasks tab auto *taskPanel = new wxPanel(m_notebook); auto *taskSizer = new wxBoxSizer(wxVERTICAL); @@ -63,6 +54,15 @@ cGameScreen::cGameScreen(wxWindow *parent) taskPanel->SetSizer(taskSizer); m_notebook->AddPage(taskPanel, "Aufgaben"); + // Locations/Zones tab + auto *zonePanel = new wxPanel(m_notebook); + auto *zoneSizer = new wxBoxSizer(wxVERTICAL); + zoneSizer->Add(new wxStaticText(zonePanel, wxID_ANY, "Orte in der Nähe:"), 0, wxALL, 5); + m_zoneList = new wxListBox(zonePanel, wxID_ANY); + zoneSizer->Add(m_zoneList, 1, wxALL | wxEXPAND, 5); + zonePanel->SetSizer(zoneSizer); + m_notebook->AddPage(zonePanel, "Orte"); + // Inventory tab auto *inventoryPanel = new wxPanel(m_notebook); auto *inventorySizer = new wxBoxSizer(wxVERTICAL); @@ -97,8 +97,8 @@ cGameScreen::cGameScreen(wxWindow *parent) CentreOnScreen(); // Status bar - CreateStatusBar(); - SetStatusText("Spiel läuft"); + wxFrameBase::CreateStatusBar(); + wxFrameBase::SetStatusText("Spiel läuft"); // Event bindings Bind(wxEVT_CLOSE_WINDOW, &cGameScreen::OnClose, this); @@ -158,7 +158,7 @@ void cGameScreen::OnSaveGame(wxCommandEvent& event) { } void cGameScreen::OnLoadGame(wxCommandEvent& event) { - int result = wxMessageBox("Möchten Sie den gespeicherten Spielstand laden?\n\nAchtung: Der aktuelle Fortschritt geht verloren!", + const int result = wxMessageBox("Möchten Sie den gespeicherten Spielstand laden?\n\nAchtung: Der aktuelle Fortschritt geht verloren!", "Laden", wxYES_NO | wxICON_QUESTION); if (result == wxYES) { @@ -185,9 +185,8 @@ void cGameScreen::OnExportCompletion(wxCommandEvent& event) { return; } - wxString filePath = saveDialog.GetPath(); - - if (wxGetApp().generateCompletionLog(filePath.ToStdString())) { + if (const wxString filePath = saveDialog.GetPath(); + wxGetApp().generateCompletionLog(filePath.ToStdString())) { wxMessageBox("Completion Log wurde erfolgreich exportiert!\n\nDiese Datei kann auf wherigo.com hochgeladen werden, um den Abschluss nachzuweisen.", "Export erfolgreich", wxOK | wxICON_INFORMATION); } else { @@ -201,6 +200,9 @@ void cGameScreen::OnGameStateChanged(wxEvent& event) { } void cGameScreen::refreshUI() { + if (wxGetApp().isCartridgeLoaded()) + SetTitle(wxString::FromUTF8(wxGetApp().getCartridge()->cartridgeName())); + // Freeze to prevent flickering and focus loss Freeze(); @@ -214,8 +216,8 @@ void cGameScreen::refreshUI() { } void cGameScreen::populateZones() { - int selection = m_zoneList->GetSelection(); - wxString selectedItem = (selection != wxNOT_FOUND) ? m_zoneList->GetString(selection) : ""; + const int selection = m_zoneList->GetSelection(); + const wxString selectedItem = (selection != wxNOT_FOUND) ? m_zoneList->GetString(selection) : ""; m_zoneList->Clear(); @@ -232,11 +234,11 @@ void cGameScreen::populateZones() { lua_pop(L, 1); lua_getfield(L, -1, "Active"); - bool active = lua_toboolean(L, -1); + const bool active = lua_toboolean(L, -1); lua_pop(L, 1); lua_getfield(L, -1, "Visible"); - bool visible = lua_toboolean(L, -1); + const bool visible = lua_toboolean(L, -1); lua_pop(L, 1); if (active && visible) { @@ -256,16 +258,15 @@ void cGameScreen::populateZones() { // Restore selection if (!selectedItem.IsEmpty()) { - int idx = m_zoneList->FindString(selectedItem); - if (idx != wxNOT_FOUND) { + if (const int idx = m_zoneList->FindString(selectedItem); idx != wxNOT_FOUND) { m_zoneList->SetSelection(idx); } } } void cGameScreen::populateTasks() { - int selection = m_taskList->GetSelection(); - wxString selectedItem = (selection != wxNOT_FOUND) ? m_taskList->GetString(selection) : ""; + const int selection = m_taskList->GetSelection(); + const wxString selectedItem = (selection != wxNOT_FOUND) ? m_taskList->GetString(selection) : ""; m_taskList->Clear(); @@ -286,7 +287,7 @@ void cGameScreen::populateTasks() { lua_pop(L, 1); lua_getfield(L, -1, "Complete"); - bool complete = lua_toboolean(L, -1); + const bool complete = lua_toboolean(L, -1); lua_pop(L, 1); if (active && !complete) { @@ -306,16 +307,15 @@ void cGameScreen::populateTasks() { // Restore selection if (!selectedItem.IsEmpty()) { - int idx = m_taskList->FindString(selectedItem); - if (idx != wxNOT_FOUND) { + if (const int idx = m_taskList->FindString(selectedItem); idx != wxNOT_FOUND) { m_taskList->SetSelection(idx); } } } void cGameScreen::populateInventory() { - int selection = m_inventoryList->GetSelection(); - wxString selectedItem = (selection != wxNOT_FOUND) ? m_inventoryList->GetString(selection) : ""; + const int selection = m_inventoryList->GetSelection(); + const wxString selectedItem = (selection != wxNOT_FOUND) ? m_inventoryList->GetString(selection) : ""; m_inventoryList->Clear(); @@ -343,15 +343,21 @@ void cGameScreen::populateInventory() { if (lua_istable(L, -1)) { // Check if Container is Player lua_getglobal(L, "Player"); - bool isInInventory = lua_rawequal(L, -1, -2); + const bool isInInventory = lua_rawequal(L, -1, -2); lua_pop(L, 2); // pop Player and Container if (isInInventory) { - lua_getfield(L, -1, "Name"); - if (lua_isstring(L, -1)) { - m_inventoryList->Append(wxString::FromUTF8(lua_tostring(L, -1))); - } + lua_getfield(L, -1, "Visible"); + const bool visible = lua_toboolean(L, -1); lua_pop(L, 1); + + if (visible) { + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + m_inventoryList->Append(wxString::FromUTF8(lua_tostring(L, -1))); + } + lua_pop(L, 1); + } } } else { lua_pop(L, 1); @@ -366,16 +372,16 @@ void cGameScreen::populateInventory() { // Restore selection if (!selectedItem.IsEmpty()) { - int idx = m_inventoryList->FindString(selectedItem); - if (idx != wxNOT_FOUND) { + if (const int idx = m_inventoryList->FindString(selectedItem); + idx != wxNOT_FOUND) { m_inventoryList->SetSelection(idx); } } } void cGameScreen::populateCharacters() { - int selection = m_characterList->GetSelection(); - wxString selectedItem = (selection != wxNOT_FOUND) ? m_characterList->GetString(selection) : ""; + const int selection = m_characterList->GetSelection(); + const wxString selectedItem = (selection != wxNOT_FOUND) ? m_characterList->GetString(selection) : ""; m_characterList->Clear(); @@ -392,16 +398,16 @@ void cGameScreen::populateCharacters() { lua_pop(L, 1); lua_getfield(L, -1, "Active"); - bool active = lua_toboolean(L, -1); + const bool active = lua_toboolean(L, -1); lua_pop(L, 1); lua_getfield(L, -1, "Visible"); - bool visible = lua_toboolean(L, -1); + const bool visible = lua_toboolean(L, -1); lua_pop(L, 1); // Check if character has a container (is somewhere) lua_getfield(L, -1, "Container"); - bool hasLocation = lua_istable(L, -1); + const bool hasLocation = lua_istable(L, -1); lua_pop(L, 1); if (active && visible && hasLocation) { @@ -435,16 +441,16 @@ void cGameScreen::populateCharacters() { // Restore selection if (!selectedItem.IsEmpty()) { - int idx = m_characterList->FindString(selectedItem); - if (idx != wxNOT_FOUND) { + if (const int idx = m_characterList->FindString(selectedItem); + idx != wxNOT_FOUND) { m_characterList->SetSelection(idx); } } } void cGameScreen::populateItems() { - int selection = m_itemList->GetSelection(); - wxString selectedItem = (selection != wxNOT_FOUND) ? m_itemList->GetString(selection) : ""; + const int selection = m_itemList->GetSelection(); + const wxString selectedItem = (selection != wxNOT_FOUND) ? m_itemList->GetString(selection) : ""; m_itemList->Clear(); @@ -461,11 +467,11 @@ void cGameScreen::populateItems() { lua_pop(L, 1); lua_getfield(L, -1, "Active"); - bool active = lua_toboolean(L, -1); + const bool active = lua_toboolean(L, -1); lua_pop(L, 1); lua_getfield(L, -1, "Visible"); - bool visible = lua_toboolean(L, -1); + const bool visible = lua_toboolean(L, -1); lua_pop(L, 1); // Check if item is NOT in player inventory (in a zone) @@ -512,8 +518,7 @@ void cGameScreen::populateItems() { // Restore selection if (!selectedItem.IsEmpty()) { - int idx = m_itemList->FindString(selectedItem); - if (idx != wxNOT_FOUND) { + if (const int idx = m_itemList->FindString(selectedItem); idx != wxNOT_FOUND) { m_itemList->SetSelection(idx); } } @@ -658,10 +663,10 @@ void cGameScreen::OnTaskSelected(wxCommandEvent& event) { } void cGameScreen::OnInventorySelected(wxCommandEvent& event) { - int sel = m_inventoryList->GetSelection(); + const int sel = m_inventoryList->GetSelection(); if (sel == wxNOT_FOUND) return; - wxString itemName = m_inventoryList->GetString(sel); + const wxString itemName = m_inventoryList->GetString(sel); lua_State *L = wxGetApp().getLuaState(); if (!L) return; @@ -712,12 +717,12 @@ void cGameScreen::OnInventorySelected(wxCommandEvent& event) { } void cGameScreen::OnCharacterSelected(wxCommandEvent& event) { - int sel = m_characterList->GetSelection(); + const int sel = m_characterList->GetSelection(); if (sel == wxNOT_FOUND) return; // Extract name from "Name (Location)" format - wxString fullText = m_characterList->GetString(sel); - wxString charName = fullText.BeforeFirst('(').Trim(); + const wxString fullText = m_characterList->GetString(sel); + const wxString charName = fullText.BeforeFirst('(').Trim(); lua_State *L = wxGetApp().getLuaState(); if (!L) return; @@ -734,13 +739,13 @@ void cGameScreen::OnCharacterSelected(wxCommandEvent& event) { lua_getfield(L, -1, "Name"); if (lua_isstring(L, -1)) { - wxString currentName = wxString::FromUTF8(lua_tostring(L, -1)); + const wxString currentName = wxString::FromUTF8(lua_tostring(L, -1)); lua_pop(L, 1); if (currentName == charName) { // Get character description lua_getfield(L, -1, "Description"); - wxString description = lua_isstring(L, -1) ? wxString::FromUTF8(lua_tostring(L, -1)) : ("Keine Beschreibung verfügbar"); + const wxString description = lua_isstring(L, -1) ? wxString::FromUTF8(lua_tostring(L, -1)) : ("Keine Beschreibung verfügbar"); lua_pop(L, 1); // Get media if exists @@ -789,12 +794,12 @@ void cGameScreen::OnCharacterSelected(wxCommandEvent& event) { } void cGameScreen::OnItemSelected(wxCommandEvent& event) { - int sel = m_itemList->GetSelection(); + const int sel = m_itemList->GetSelection(); if (sel == wxNOT_FOUND) return; // Extract name from "Name (Location)" format - wxString fullText = m_itemList->GetString(sel); - wxString itemName = fullText.BeforeFirst('(').Trim(); + const wxString fullText = m_itemList->GetString(sel); + const wxString itemName = fullText.BeforeFirst('(').Trim(); lua_State *L = wxGetApp().getLuaState(); if (!L) return; @@ -811,13 +816,13 @@ void cGameScreen::OnItemSelected(wxCommandEvent& event) { lua_getfield(L, -1, "Name"); if (lua_isstring(L, -1)) { - wxString currentName = wxString::FromUTF8(lua_tostring(L, -1)); + const wxString currentName = wxString::FromUTF8(lua_tostring(L, -1)); lua_pop(L, 1); if (currentName == itemName) { // Get item description lua_getfield(L, -1, "Description"); - wxString description = lua_isstring(L, -1) ? wxString::FromUTF8(lua_tostring(L, -1)) : ("Keine Beschreibung verfügbar"); + const wxString description = lua_isstring(L, -1) ? wxString::FromUTF8(lua_tostring(L, -1)) : ("Keine Beschreibung verfügbar"); lua_pop(L, 1); // Get media if exists @@ -836,7 +841,7 @@ void cGameScreen::OnItemSelected(wxCommandEvent& event) { lua_getfield(L, -1, "Commands"); wxArrayString commands; if (lua_istable(L, -1)) { - int n = lua_objlen(L, -1); + const auto n = lua_objlen(L, -1); for (int i = 1; i <= n; i++) { lua_rawgeti(L, -1, i); if (lua_istable(L, -1)) { @@ -860,7 +865,7 @@ void cGameScreen::OnItemSelected(wxCommandEvent& event) { } // Use WherigoMessageDialog to show with media support - std::vector buttons = {"OK"}; + const std::vector buttons = {"OK"}; wherigo::WherigoMessageDialog dlg(this, message, itemName, buttons, mediaName); dlg.ShowModal(); @@ -893,8 +898,9 @@ void cGameScreen::OnItemSelected(wxCommandEvent& event) { } void cGameScreen::OnMapSim(wxCommandEvent&) { - std::vector> zoneCoords; + std::vector zoneInfos; double lat = 53.3, lon = 10.39; + lua_State *L = wxGetApp().getLuaState(); if (L) { lua_getglobal(L, "_G"); @@ -903,41 +909,46 @@ void cGameScreen::OnMapSim(wxCommandEvent&) { if (lua_istable(L, -1)) { lua_getfield(L, -1, "ClassName"); if (lua_isstring(L, -1) && strcmp(lua_tostring(L, -1), "Zone") == 0) { - lua_pop(L, 1); + lua_pop(L, 1); // pop ClassName + lua_getfield(L, -1, "Active"); - bool active = lua_toboolean(L, -1) || true; + const bool active = lua_toboolean(L, -1); lua_pop(L, 1); - lua_getfield(L, -1, "Visible"); - bool visible = lua_toboolean(L, -1) || true; + if (!active) { lua_pop(L, 1); continue; } + + // Read name + lua_getfield(L, -1, "Name"); + std::string zoneName = lua_isstring(L, -1) ? lua_tostring(L, -1) : ""; lua_pop(L, 1); - if (active && visible) { - lua_getfield(L, -1, "OriginalPoint"); - if (lua_istable(L, -1)) { - lua_getfield(L, -1, "latitude"); - lua_getfield(L, -2, "longitude"); - if (lua_isnumber(L, -2) && lua_isnumber(L, -1)) { - double zlat = lua_tonumber(L, -2); - double zlon = lua_tonumber(L, -1); - zoneCoords.emplace_back(zlat, zlon); - } - lua_pop(L, 2); - } else { - lua_pop(L, 1); + + // Read OriginalPoint (set by Wherigo Builder on each zone) + lua_getfield(L, -1, "OriginalPoint"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "latitude"); + lua_getfield(L, -2, "longitude"); + if (lua_isnumber(L, -2) && lua_isnumber(L, -1)) { + zoneInfos.push_back({ + lua_tonumber(L, -2), + lua_tonumber(L, -1), + zoneName + }); } - lua_pop(L, 1); // pop OriginalPoint + lua_pop(L, 2); // pop lat, lon } + lua_pop(L, 1); // pop OriginalPoint (table or nil) } else { - lua_pop(L, 1); + lua_pop(L, 1); // pop ClassName } } - lua_pop(L, 1); + lua_pop(L, 1); // pop value } - lua_pop(L, 1); + lua_pop(L, 1); // pop _G } - if (!zoneCoords.empty()) { - lat = zoneCoords[0].first; - lon = zoneCoords[0].second; + + if (!zoneInfos.empty()) { + lat = zoneInfos[0].lat; + lon = zoneInfos[0].lon; } - auto *mapSim = new MapSimFrame(this, lat, lon, zoneCoords); + auto *mapSim = new MapSimFrame(this, lat, lon, zoneInfos); mapSim->Show(); } diff --git a/main/src/ui/map_sim_frame.cpp b/main/src/ui/map_sim_frame.cpp index bf73ebb..7e573b1 100644 --- a/main/src/ui/map_sim_frame.cpp +++ b/main/src/ui/map_sim_frame.cpp @@ -1,79 +1,379 @@ #include "ui/map_sim_frame.h" -#include +#include "lua/game_engine.h" + +extern "C" { +#include +} + #include -#include +#include #include -#include -#include +#include +#include + +enum { ID_PlayBtn = wxID_HIGHEST + 100, ID_ClearBtn, ID_SimTimer }; wxBEGIN_EVENT_TABLE(MapSimFrame, wxFrame) - EVT_WEBVIEW_NAVIGATED(wxID_ANY, MapSimFrame::OnWebViewEvent) - EVT_BUTTON(wxID_ANY, MapSimFrame::OnPlay) -wxEND_EVENT_TABLE() + EVT_BUTTON(ID_PlayBtn, MapSimFrame::OnPlay) + EVT_BUTTON(ID_ClearBtn, MapSimFrame::OnClear) + EVT_TIMER(ID_SimTimer, MapSimFrame::OnSimTimer) wxEND_EVENT_TABLE() -MapSimFrame::MapSimFrame(wxWindow* parent, double centerLat, double centerLon, const std::vector>& zoneCoords) - : wxFrame(parent, wxID_ANY, "Karten-Simulation", wxDefaultPosition, wxSize(900, 700)), m_zoneCoords(zoneCoords) { - auto* sizer = new wxBoxSizer(wxVERTICAL); - m_webView = wxWebView::New(this, wxID_ANY); - sizer->Add(m_webView, 1, wxEXPAND); - auto* playBtn = new wxButton(this, wxID_ANY, "Simulation starten"); - sizer->Add(playBtn, 0, wxALL | wxALIGN_CENTER, 8); - SetSizer(sizer); - // Leaflet-Karte laden (dynamisch zentriert und Marker) - wxString html; - html << "MapSim" - "" - "" - "" - "
" - ""; - m_webView->SetPage(html, ""); + // Escape a string for safe embedding inside a JS single-quoted string + static wxString jsEscape(const std::string &s) { + wxString out; + for (const unsigned char c : s) { + if (c == '\'') + out += "\\'"; + else if (c == '\\') + out += "\\\\"; + else if (c == '\n') + out += "\\n"; + else if (c == '\r') + out += "\\r"; + else + out += static_cast(c); + } + return out; } -void MapSimFrame::OnWebViewEvent(wxWebViewEvent& event) { - // Empfange Marker-Koordinaten von JS - wxString msg = event.GetString(); - double lat = 0, lon = 0; - if (msg.ToDouble(&lat)) { - // Not used, see below - } - // In wxWidgets 3.1+ kann man wxWebView::RegisterHandler für JS->C++ nutzen - // Hier: Marker werden über postMessage als JSON gesendet - // TODO: JSON parsen und AddSimPoint aufrufen +MapSimFrame::MapSimFrame(wxWindow *parent, double centerLat, double centerLon, + const std::vector &zoneInfos) + : wxFrame(parent, wxID_ANY, "GPS-Simulation", wxDefaultPosition, + wxSize(900, 700)), + m_simTimer(this, ID_SimTimer), m_zoneInfos(zoneInfos) { + auto *sizer = new wxBoxSizer(wxVERTICAL); + + m_webView = wxWebView::New(this, wxID_ANY); + sizer->Add(m_webView, 1, wxEXPAND); + + auto *btnSizer = new wxBoxSizer(wxHORIZONTAL); + btnSizer->Add(new wxButton(this, ID_PlayBtn, "▶ Route abspielen"), 0, wxALL, + 5); + btnSizer->Add(new wxButton(this, ID_ClearBtn, "✕ Wegpunkte löschen"), 0, + wxALL, 5); + sizer->Add(btnSizer, 0, wxALIGN_CENTER); + SetSizer(sizer); + + // Register JS → C++ message handler (wxWidgets 3.1.5+) + m_webView->AddScriptMessageHandler("wx"); + m_webView->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, + &MapSimFrame::OnScriptMessage, this); + m_webView->Bind(wxEVT_WEBVIEW_LOADED, &MapSimFrame::OnWebViewLoaded, this); + + wherigo::GameEngine::getInstance().Bind( + wherigo::EVT_GAME_STATE_CHANGED, &MapSimFrame::OnGameStateChanged, this); + + Bind(wxEVT_CLOSE_WINDOW, &MapSimFrame::OnClose, this); + + // ── Build Leaflet page ─────────────────────────────────────────────────── + wxString html; + html << "" + "GPS-Sim" + "" + "" + "" + "" + "" + "" + "" + "
" + ""; + + m_webView->SetPage(html, ""); } +// ── JS → C++ message +// ────────────────────────────────────────────────────────── + +static bool parseLatLon(const wxString &json, double &lat, double &lon) { + auto extractNum = [&](const wxString &key, double &val) -> bool { + int pos = json.Find(key); + if (pos == wxNOT_FOUND) + return false; + pos += static_cast(key.length()); + while (pos < static_cast(json.length()) && + (json[pos] == ' ' || json[pos] == ':')) + ++pos; + const int start = pos; + while (pos < static_cast(json.length()) && + (wxIsdigit(json[pos]) || json[pos] == '.' || json[pos] == '-')) + ++pos; + return json.Mid(start, pos - start).ToDouble(&val); + }; + return extractNum("\"lat\"", lat) && extractNum("\"lon\"", lon); +} + +void MapSimFrame::OnScriptMessage(wxWebViewEvent &event) { + double lat = 0, lon = 0; + if (parseLatLon(event.GetString(), lat, lon)) { + AddSimPoint(lat, lon); + wxLogDebug("MapSim: waypoint %zu added (%.6f, %.6f)", m_route.size(), lat, + lon); + } +} + +// ── Route management +// ────────────────────────────────────────────────────────── + void MapSimFrame::AddSimPoint(double lat, double lon) { - m_route.push_back({lat, lon}); + m_route.push_back({lat, lon}); } -void MapSimFrame::OnPlay(wxCommandEvent&) { - if (m_route.empty()) { - wxMessageBox("Bitte zuerst Marker setzen.", "Hinweis", wxOK | wxICON_INFORMATION); - return; - } - // Simuliere Bewegung entlang der Route - for (const auto& pt : m_route) { - SendPositionToEngine(pt.lat, pt.lon); - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } - wxMessageBox("Simulation beendet.", "Info", wxOK | wxICON_INFORMATION); +void MapSimFrame::StartSimulation() { + if (m_route.empty()) + return; + m_simIndex = 0; + m_simTimer.Start(500); } +// ── Button handlers +// ─────────────────────────────────────────────────────────── + +void MapSimFrame::OnPlay(wxCommandEvent &) { + if (m_route.empty()) { + wxMessageBox("Bitte zuerst Wegpunkte auf der Karte setzen.", "Hinweis", + wxOK | wxICON_INFORMATION); + return; + } + m_simIndex = 0; + m_simTimer.Start(500); + wxLogDebug("MapSim: starting playback of %zu waypoints", m_route.size()); +} + +void MapSimFrame::OnClear(wxCommandEvent &) { + m_simTimer.Stop(); + m_route.clear(); + m_simIndex = 0; + // Remove all route markers and the connecting line from the map + m_webView->RunScript( + "routeMarkers.forEach(function(m){map.removeLayer(m);});" + "routeMarkers=[];" + "if(routeLine){map.removeLayer(routeLine);routeLine=null;}", + nullptr); + wxLogDebug("MapSim: route cleared"); +} + +void MapSimFrame::OnSimTimer(wxTimerEvent &) { + if (m_simIndex >= m_route.size()) { + wxCommandEvent dummy; + OnClear(dummy); + return; + } + const auto &pt = m_route[m_simIndex++]; + SendPositionToEngine(pt.lat, pt.lon); +} + +// ── Zone sync +// ───────────────────────────────────────────────────────────────── + +void MapSimFrame::syncZonesOnMap() { + if (!m_pageLoaded) + return; + lua_State *L = wherigo::GameEngine::getInstance().getLuaState(); + if (!L) + return; + + wxString js = "syncZones(["; + bool first = true; + + lua_getglobal(L, "_G"); + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "ClassName"); + const bool isZone = + lua_isstring(L, -1) && !strcmp(lua_tostring(L, -1), "Zone"); + lua_pop(L, 1); + if (isZone) { + lua_getfield(L, -1, "Active"); + const bool active = lua_toboolean(L, -1); + lua_pop(L, 1); + if (active) { + lua_getfield(L, -1, "Name"); + const std::string name = + lua_isstring(L, -1) ? lua_tostring(L, -1) : ""; + lua_pop(L, 1); + + lua_getfield(L, -1, "OriginalPoint"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "latitude"); + lua_getfield(L, -2, "longitude"); + const double lat = lua_tonumber(L, -2); + const double lon = lua_tonumber(L, -1); + lua_pop(L, 2); + + if (!first) + js += ","; + js += wxString::Format("{name:'%s',lat:%.8f,lon:%.8f}", + jsEscape(name), lat, lon); + first = false; + } + lua_pop(L, 1); // pop OriginalPoint + } + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); // pop _G + + js += "]);"; + m_webView->RunScript(js, nullptr); +} + +void MapSimFrame::OnWebViewLoaded(wxWebViewEvent &) { + m_pageLoaded = true; + syncZonesOnMap(); +} + +void MapSimFrame::OnGameStateChanged(wxEvent &) { syncZonesOnMap(); } + +void MapSimFrame::OnClose(wxCloseEvent &event) { + wherigo::GameEngine::getInstance().Unbind( + wherigo::EVT_GAME_STATE_CHANGED, &MapSimFrame::OnGameStateChanged, this); + event.Skip(); +} + +// ── Engine integration +// ──────────────────────────────────────────────────────── + void MapSimFrame::SendPositionToEngine(double lat, double lon) { - // TODO: Engine-Integration: GPS-Position setzen - // Beispiel: wxGetApp().setSimulatedPosition(lat, lon); + wxLogDebug("MapSim: position → engine (%.6f, %.6f)", lat, lon); + wherigo::GameEngine::getInstance().updatePlayerPosition(lat, lon, 0.0); + + m_webView->RunScript( + wxString::Format("updatePlayerPos(%.8f,%.8f);", lat, lon), nullptr); } diff --git a/main/src/ui/start_screen.cpp b/main/src/ui/start_screen.cpp index 072de90..161c9b8 100644 --- a/main/src/ui/start_screen.cpp +++ b/main/src/ui/start_screen.cpp @@ -4,20 +4,17 @@ #include "ui/game_screen.h" #include -#include #include +#include wxDECLARE_APP(cApp); -enum { - ID_OpenCartridge = 1001, - ID_StartGame = 1002 -}; +enum { ID_OpenCartridge = 1001, ID_StartGame = 1002 }; cStartScreen::cStartScreen() - : wxFrame(nullptr, wxID_ANY, "Wherigo Player", wxDefaultPosition, wxSize(800, 800)), - m_gameFrame(nullptr), - m_cartridgeLoaded(false) { + : wxFrame(nullptr, wxID_ANY, "Wherigo Player", wxDefaultPosition, + wxSize(800, 800)), + m_gameFrame(nullptr), m_cartridgeLoaded(false) { // Menu auto *menuFile = new wxMenu; @@ -31,7 +28,7 @@ cStartScreen::cStartScreen() auto *menuBar = new wxMenuBar; menuBar->Append(menuFile, "&Datei"); menuBar->Append(menuHelp, "&Hilfe"); - SetMenuBar(menuBar); + wxFrameBase::SetMenuBar(menuBar); // Main sizer auto *mainSizer = new wxBoxSizer(wxVERTICAL); @@ -64,7 +61,8 @@ cStartScreen::cStartScreen() infoSizer->Add(m_cartridgeAuthor, 0, wxALL | wxALIGN_CENTER, 5); // Description - m_cartridgeDesc = new wxHtmlWindow(m_infoPanel, wxID_ANY, wxDefaultPosition, wxSize(500, 200)); + m_cartridgeDesc = new wxHtmlWindow(m_infoPanel, wxID_ANY, wxDefaultPosition, + wxSize(500, 200)); m_cartridgeDesc->SetMinSize(wxSize(500, 200)); infoSizer->Add(m_cartridgeDesc, 1, wxALL | wxEXPAND, 10); @@ -82,8 +80,8 @@ cStartScreen::cStartScreen() SetSizer(mainSizer); // Status bar - CreateStatusBar(); - SetStatusText("Bitte eine Cartridge öffnen"); + wxFrameBase::CreateStatusBar(); + wxFrameBase::SetStatusText("Bitte eine Cartridge öffnen"); // Event bindings Bind(wxEVT_MENU, &cStartScreen::OnOpenCartridge, this, ID_OpenCartridge); @@ -95,7 +93,7 @@ cStartScreen::cStartScreen() CentreOnScreen(); } -void cStartScreen::OnOpenCartridge(wxCommandEvent& event) { +void cStartScreen::OnOpenCartridge(wxCommandEvent &event) { wxFileDialog openFileDialog(this, "Cartridge öffnen", "", "", "Wherigo Cartridge (*.gwc)|*.gwc", wxFD_OPEN | wxFD_FILE_MUST_EXIST); @@ -104,15 +102,19 @@ void cStartScreen::OnOpenCartridge(wxCommandEvent& event) { return; } - wxString filePath = openFileDialog.GetPath(); - SetStatusText("Lade Cartridge: " + filePath); + const wxString filePath = openFileDialog.GetPath(); + wxFrameBase::SetStatusText("Lade Cartridge: " + filePath); // Vor dem Laden: Info-Panel zurücksetzen if (m_infoPanel) { - if (m_cartridgeName) m_cartridgeName->SetLabel(""); - if (m_cartridgeAuthor) m_cartridgeAuthor->SetLabel(""); - if (m_cartridgeDesc) m_cartridgeDesc->SetPage(""); - if (m_splashImage) m_splashImage->SetBitmap(wxNullBitmap); + if (m_cartridgeName) + m_cartridgeName->SetLabel(""); + if (m_cartridgeAuthor) + m_cartridgeAuthor->SetLabel(""); + if (m_cartridgeDesc) + m_cartridgeDesc->SetPage(""); + if (m_splashImage) + m_splashImage->SetBitmap(wxNullBitmap); } // ggf. alten GameScreen schließen @@ -125,16 +127,18 @@ void cStartScreen::OnOpenCartridge(wxCommandEvent& event) { if (wxGetApp().loadCartridge(filePath.ToStdString())) { m_cartridgeLoaded = true; showCartridgeInfo(); - SetStatusText("Cartridge geladen - bereit zum Starten"); + wxFrameBase::SetStatusText("Cartridge geladen - bereit zum Starten"); } else { - wxMessageBox("Fehler beim Laden der Cartridge", "Fehler", wxOK | wxICON_ERROR); - SetStatusText("Fehler beim Laden"); + wxMessageBox("Fehler beim Laden der Cartridge", "Fehler", + wxOK | wxICON_ERROR); + wxFrameBase::SetStatusText("Fehler beim Laden"); } } void cStartScreen::showCartridgeInfo() { auto *cartridge = wxGetApp().getCartridge(); - if (!cartridge) return; + if (!cartridge) + return; // Set cartridge info m_cartridgeName->SetLabel(wxString::FromUTF8(cartridge->cartridgeName())); @@ -149,21 +153,21 @@ void cStartScreen::showCartridgeInfo() { } // Load splash screen - auto splash = cartridge->splashScreen(); - if (splash) { - auto data = splash->getData(); + if (const auto splash = cartridge->splashScreen()) { + const auto data = splash->getData(); if (!data.empty()) { wxMemoryInputStream stream(data.data(), data.size()); - wxImage image(stream); - if (image.IsOk()) { + if (wxImage image(stream); image.IsOk()) { // Scale if too large - int maxWidth = 300; - int maxHeight = 200; - if (image.GetWidth() > maxWidth || image.GetHeight() > maxHeight) { - double scaleX = (double)maxWidth / image.GetWidth(); - double scaleY = (double)maxHeight / image.GetHeight(); + const int maxHeight = 200; + if (int maxWidth = 300; + image.GetWidth() > maxWidth || image.GetHeight() > maxHeight) { + double scaleX = static_cast(maxWidth) / image.GetWidth(); + double scaleY = static_cast(maxHeight) / image.GetHeight(); double scale = std::min(scaleX, scaleY); - image.Rescale(image.GetWidth() * scale, image.GetHeight() * scale, wxIMAGE_QUALITY_HIGH); + image.Rescale(static_cast(image.GetWidth() * scale), + static_cast(image.GetHeight() * scale), + wxIMAGE_QUALITY_HIGH); } m_splashImage->SetBitmap(wxBitmap(image)); } @@ -176,15 +180,18 @@ void cStartScreen::showCartridgeInfo() { Layout(); } -void cStartScreen::OnStartGame(wxCommandEvent& event) { +void cStartScreen::OnStartGame(wxCommandEvent &event) { if (!m_cartridgeLoaded) { - wxMessageBox("Bitte zuerst eine Cartridge laden", "Hinweis", wxOK | wxICON_INFORMATION); + wxMessageBox("Bitte zuerst eine Cartridge laden", "Hinweis", + wxOK | wxICON_INFORMATION); return; } // Create game frame if not exists if (!m_gameFrame) { m_gameFrame = new cGameScreen(this); + m_gameFrame->SetPosition(GetPosition()); + m_gameFrame->SetSize(GetSize()); } // Hide start frame, show game @@ -198,10 +205,10 @@ void cStartScreen::OnStartGame(wxCommandEvent& event) { void cStartScreen::onGameClosed() { // Called when game frame is closed Show(); - SetStatusText("Spiel pausiert - Cartridge noch geladen"); + wxFrameBase::SetStatusText("Spiel pausiert - Cartridge noch geladen"); } -void cStartScreen::OnExit(wxCommandEvent& event) { +void cStartScreen::OnExit(wxCommandEvent &event) { // Clean up game frame if (m_gameFrame) { m_gameFrame->Destroy(); @@ -210,7 +217,8 @@ void cStartScreen::OnExit(wxCommandEvent& event) { Close(true); } -void cStartScreen::OnAbout(wxCommandEvent& event) { - wxMessageBox("Wherigo Player\n\nEin Desktop-Player für Wherigo-Cartridges\n\nVersion 0.1", +void cStartScreen::OnAbout(wxCommandEvent &event) { + wxMessageBox("Wherigo Player\n\nEin Desktop-Player für " + "Wherigo-Cartridges\n\nVersion 0.1", "Über Wherigo Player", wxOK | wxICON_INFORMATION); } diff --git a/main/src/ui/wherigo_dialog.cpp b/main/src/ui/wherigo_dialog.cpp index 2d787cb..e4c86c2 100644 --- a/main/src/ui/wherigo_dialog.cpp +++ b/main/src/ui/wherigo_dialog.cpp @@ -1,38 +1,39 @@ #include "ui/wherigo_dialog.h" -#include "lua/media_manager.h" #include "lua/game_engine.h" +#include "lua/media_manager.h" -#include -#include -#include #include +#include +#include #include #include -#include -#include +#include +#include +#include namespace wherigo { -WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent, const wxString &text, +WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent, + const wxString &text, const wxString &title, const std::vector &buttons, const wxString &mediaName) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, - wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER ) { + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { auto *sizer = new wxBoxSizer(wxVERTICAL); // Media/Image (if provided) if (!mediaName.IsEmpty()) { - auto mediaData = MediaManager::getInstance().getMediaByName(mediaName.ToStdString()); + const auto mediaData = + MediaManager::getInstance().getMediaByName(mediaName.ToStdString()); if (!mediaData.empty()) { wxMemoryInputStream stream(mediaData.data(), mediaData.size()); - wxImage image(stream); - if (image.IsOk()) { + if (wxImage image(stream); image.IsOk()) { // Get screen DPI for scaling - wxWindow* topWindow = wxTheApp->GetTopWindow(); + const wxWindow *topWindow = wxTheApp->GetTopWindow(); double contentScaleFactor = 1.0; if (topWindow) { contentScaleFactor = topWindow->GetContentScaleFactor(); @@ -40,14 +41,18 @@ WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent, const wxString &tex // Scale image - doubled size from original (800x600 instead of 400x300) // Adjust for DPI/Retina displays - int maxWidth = static_cast(800 * contentScaleFactor); - int maxHeight = static_cast(600 * contentScaleFactor); + const int maxWidth = static_cast(800 * contentScaleFactor); - if (image.GetWidth() > maxWidth || image.GetHeight() > maxHeight) { - double scaleX = (double)maxWidth / image.GetWidth(); - double scaleY = (double)maxHeight / image.GetHeight(); - double scale = std::min(scaleX, scaleY); - image.Rescale(image.GetWidth() * scale, image.GetHeight() * scale, wxIMAGE_QUALITY_HIGH); + if (const int maxHeight = static_cast(600 * contentScaleFactor); + image.GetWidth() > maxWidth || image.GetHeight() > maxHeight) { + const double scaleX = + static_cast(maxWidth) / image.GetWidth(); + const double scaleY = + static_cast(maxHeight) / image.GetHeight(); + const double scale = std::min(scaleX, scaleY); + image.Rescale(static_cast(image.GetWidth() * scale), + static_cast(image.GetHeight() * scale), + wxIMAGE_QUALITY_HIGH); } wxBitmap bitmap(image); @@ -55,22 +60,22 @@ WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent, const wxString &tex sizer->Add(imageCtrl, 0, wxALL | wxALIGN_CENTER, 10); } else { wxLogDebug("Failed to load image: %s", mediaName); - auto *mediaLabel = new wxStaticText(this, wxID_ANY, - wxString::Format("[Media: %s]", mediaName)); + auto *mediaLabel = new wxStaticText( + this, wxID_ANY, wxString::Format("[Media: %s]", mediaName)); mediaLabel->SetForegroundColour(*wxLIGHT_GREY); sizer->Add(mediaLabel, 0, wxALL | wxALIGN_CENTER, 10); } } else { wxLogDebug("No media data for: %s", mediaName); - auto *mediaLabel = new wxStaticText(this, wxID_ANY, - wxString::Format("[Media: %s]", mediaName)); + auto *mediaLabel = new wxStaticText( + this, wxID_ANY, wxString::Format("[Media: %s]", mediaName)); mediaLabel->SetForegroundColour(*wxLIGHT_GREY); sizer->Add(mediaLabel, 0, wxALL | wxALIGN_CENTER, 10); } } // Helper lambda to convert plain text to HTML - auto textToHtml = [](const wxString& plainText) -> wxString { + auto textToHtml = [](const wxString &plainText) -> wxString { wxString result = plainText; // Check if this looks like HTML (has tags) if (plainText.Find("<") == wxNOT_FOUND) { @@ -104,29 +109,34 @@ WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent, const wxString &tex wxColour fgColor = GetForegroundColour(); // Helper lambda to create and configure HTML window with auto-sizing - auto createHtmlWindow = [this, &sizer, &textToHtml, bgColor, fgColor](const wxString& text) { + auto createHtmlWindow = [this, &sizer, &textToHtml, bgColor, + fgColor](const wxString &message) { auto *htmlCtrl = new wxHtmlWindow(this, wxID_ANY); htmlCtrl->SetBorders(0); - wxString htmlContent = wxString::Format( - "%s", - bgColor.Red(), bgColor.Green(), bgColor.Blue(), - fgColor.Red(), fgColor.Green(), fgColor.Blue(), - textToHtml(text) - ); + const wxString htmlContent = wxString::Format( + "%s", + bgColor.Red(), bgColor.Green(), bgColor.Blue(), fgColor.Red(), + fgColor.Green(), fgColor.Blue(), textToHtml(message)); htmlCtrl->SetPage(htmlContent); - // htmlCtrl->SetBackgroundColour(bgColor); // Removed as it might not exist or be needed + // htmlCtrl->SetBackgroundColour(bgColor); // Removed as it might not exist + // or be needed // Set a reasonable fixed size - will expand based on content // Width: fixed at 600px for consistent layout // Height: let wxHtmlWindow calculate based on content, with limits - int contentWidth = 600; + constexpr int contentWidth = 600; // Calculate approximate height based on line count (rough estimate) int lineCount = 1; - for (size_t i = 0; i < text.length(); i++) { - if (text[i] == '\n') lineCount++; + for (size_t i = 0; i < message.length(); i++) { + if (message[i] == '\n') + lineCount++; } // Estimate: ~20px per line + 40px padding @@ -182,17 +192,16 @@ WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent, const wxString &tex wxSize minSize = sizer->GetMinSize(); // Set reasonable dialog size - int dialogWidth = std::clamp(minSize.GetWidth() + 40, 500, 800); - int dialogHeight = std::clamp(minSize.GetHeight() + 40, 300, 700); + const int dialogWidth = std::clamp(minSize.GetWidth() + 40, 500, 800); + const int dialogHeight = std::clamp(minSize.GetHeight() + 40, 300, 700); SetSize(dialogWidth, dialogHeight); - SetMinSize(wxSize(400, 250)); + wxTopLevelWindowBase::SetMinSize(wxSize(400, 250)); CenterOnParent(); } -void WherigoMessageDialog::onButton(wxCommandEvent &event) { - int id = event.GetId(); - if (id == wxID_OK) { +void WherigoMessageDialog::onButton(const wxCommandEvent &event) { + if (const int id = event.GetId(); id == wxID_OK) { m_selectedButton = 0; } else { m_selectedButton = id - 1000; @@ -200,13 +209,14 @@ void WherigoMessageDialog::onButton(wxCommandEvent &event) { EndModal(wxID_OK); } -WherigoDialogRunner& WherigoDialogRunner::getInstance() { +WherigoDialogRunner &WherigoDialogRunner::getInstance() { static WherigoDialogRunner instance; return instance; } -void WherigoDialogRunner::showMessageBox(const wxString &text, const wxString &title, - std::function callback) { +void WherigoDialogRunner::showMessageBox(const wxString &text, + const wxString &title, + std::function callback) { WherigoMessageDialog dlg(nullptr, text, title); dlg.ShowModal(); @@ -216,27 +226,26 @@ void WherigoDialogRunner::showMessageBox(const wxString &text, const wxString &t } void WherigoDialogRunner::showDialog(const std::vector &entries, - std::function callback) { - for (size_t i = 0; i < entries.size(); i++) { + std::function callback) { + for (size_t i = 0; i < entries.size(); ++i) { const auto &entry = entries[i]; std::vector buttons; + buttons.reserve(entry.buttons.size() + 1); for (const auto &btn : entry.buttons) { - buttons.push_back(wxString::FromUTF8(btn)); + buttons.emplace_back(wxString::FromUTF8(btn)); } - if (buttons.empty() && i < entries.size() - 1) { - buttons.push_back("Weiter"); - } else if (buttons.empty()) { - buttons.push_back("OK"); + if (buttons.empty()) { + buttons.emplace_back(i + 1 < entries.size() ? "Weiter" : "OK"); } WherigoMessageDialog dlg(nullptr, wxString::FromUTF8(entry.text), "Wherigo", - buttons, wxString::FromUTF8(entry.mediaName)); + buttons, wxString::FromUTF8(entry.mediaName)); dlg.ShowModal(); // For the last entry, call the callback with the selected button - if (i == entries.size() - 1 && callback) { + if (i + 1 == entries.size() && callback) { callback(dlg.getSelectedButton()); }