some tweaks

still non playable cartridges

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
This commit is contained in:
2026-06-02 23:21:56 +02:00
parent 6e29dde558
commit 92045ec6df
15 changed files with 1802 additions and 838 deletions
+1
View File
@@ -113,3 +113,4 @@ CMakeUserPresets.json
cartridges/ cartridges/
assets/icon.iconset/ assets/icon.iconset/
*.lua *.lua
*.local.json
+4 -4
View File
@@ -16,16 +16,16 @@ public:
int OnExit() override; int OnExit() override;
bool loadCartridge(const std::string &filePath); bool loadCartridge(const std::string &filePath);
void startGame(); void startGame() const;
void unloadCartridge(); void unloadCartridge();
// Save/Load game state // Save/Load game state
bool saveGameState(const std::string &saveFilePath); bool saveGameState(const std::string &saveFilePath) const;
bool loadGameState(const std::string &saveFilePath); bool loadGameState(const std::string &saveFilePath) const;
std::string getAutoSavePath() const; std::string getAutoSavePath() const;
// Generate completion log (for wherigo.com) // Generate completion log (for wherigo.com)
bool generateCompletionLog(const std::string &logFilePath); bool generateCompletionLog(const std::string &logFilePath) const;
std::string getCompletionLogPath() const; std::string getCompletionLogPath() const;
lua_State* getLuaState() const { return m_luaState; } lua_State* getLuaState() const { return m_luaState; }
+3
View File
@@ -39,6 +39,7 @@ public:
// Notify listeners that game state has changed // Notify listeners that game state has changed
void notifyStateChanged(); void notifyStateChanged();
void rebuildPlayerInventory();
lua_State* getLuaState() const { return m_luaState; } lua_State* getLuaState() const { return m_luaState; }
int getCartridgeRef() const { return m_cartridgeRef; } int getCartridgeRef() const { return m_cartridgeRef; }
@@ -64,6 +65,8 @@ private:
double m_playerLng = 0.0; double m_playerLng = 0.0;
double m_playerAlt = 0.0; double m_playerAlt = 0.0;
bool m_isProcessing = false;
wxDECLARE_EVENT_TABLE(); wxDECLARE_EVENT_TABLE();
}; };
+183
View File
@@ -0,0 +1,183 @@
#pragma once
#include <cmath>
#include <cstring>
#include <limits>
#include <utility>
#include <vector>
extern "C" {
#include <lauxlib.h>
#include <lua.h>
}
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<double,double> 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<double,double> 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<std::pair<double,double>>& pts) {
const int n = static_cast<int>(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<double,double> nearestZoneEdge(
double plat, double plon,
const std::vector<std::pair<double,double>>& pts) {
double minDist = std::numeric_limits<double>::max(), minBrg = 0.0;
const int n = static_cast<int>(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<std::pair<double,double>> readZonePoints(lua_State *L, int zoneIdx) {
std::vector<std::pair<double,double>> pts;
lua_getfield(L, zoneIdx, "Points");
if (!lua_istable(L, -1)) { lua_pop(L, 1); return pts; }
const int n = static_cast<int>(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
+33 -6
View File
@@ -1,25 +1,52 @@
#pragma once #pragma once
#include <wx/frame.h> #include <wx/frame.h>
#include <wx/timer.h>
#include <wx/webview.h> #include <wx/webview.h>
#include <vector> #include <string>
#include <utility> #include <utility>
#include <vector>
struct SimPoint { struct SimPoint {
double lat; double lat;
double lon; double lon;
}; };
struct ZoneInfo {
double lat;
double lon;
std::string name;
};
class MapSimFrame : public wxFrame { class MapSimFrame : public wxFrame {
public: public:
MapSimFrame(wxWindow* parent, double centerLat = 53.3, double centerLon = 10.39, const std::vector<std::pair<double, double>>& zoneCoords = {}); MapSimFrame(wxWindow* parent,
double centerLat = 53.3, double centerLon = 10.39,
const std::vector<ZoneInfo>& zoneInfos = {});
void AddSimPoint(double lat, double lon); void AddSimPoint(double lat, double lon);
void StartSimulation(); void StartSimulation();
private: private:
wxWebView* m_webView; wxWebView* m_webView = nullptr;
wxTimer m_simTimer;
size_t m_simIndex = 0;
std::vector<SimPoint> m_route; std::vector<SimPoint> m_route;
std::vector<std::pair<double, double>> m_zoneCoords; std::vector<ZoneInfo> m_zoneInfos;
void OnWebViewEvent(wxWebViewEvent& event); bool m_pageLoaded = false;
void OnPlay(wxCommandEvent& event);
void SendPositionToEngine(double lat, double lon); 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(); wxDECLARE_EVENT_TABLE();
}; };
+1 -1
View File
@@ -25,7 +25,7 @@ public:
int getSelectedButton() const { return m_selectedButton; } int getSelectedButton() const { return m_selectedButton; }
private: private:
void onButton(wxCommandEvent &event); void onButton(const wxCommandEvent &event);
int m_selectedButton = -1; int m_selectedButton = -1;
}; };
+42 -14
View File
@@ -6,6 +6,11 @@
#include "lua/wherigo_completion.h" #include "lua/wherigo_completion.h"
#include "ui/start_screen.h" #include "ui/start_screen.h"
#ifdef __APPLE__
#include <objc/message.h>
#include <objc/objc.h>
#endif
#include <cartridge/parser.h> #include <cartridge/parser.h>
#include <wx/dir.h> #include <wx/dir.h>
#include <wx/filename.h> #include <wx/filename.h>
@@ -26,6 +31,19 @@ bool cApp::OnInit()
auto *startFrame = new cStartScreen(); auto *startFrame = new cStartScreen();
startFrame->Show(true); 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<MsgFn>(objc_msgSend)(
reinterpret_cast<id>(objc_getClass("NSApplication")),
sel_registerName("sharedApplication"));
reinterpret_cast<ActivateFn>(objc_msgSend)(
nsApp, sel_registerName("activateIgnoringOtherApps:"), YES);
#else
startFrame->Raise();
#endif
return true; return true;
} }
@@ -46,7 +64,7 @@ bool cApp::loadCartridge(const std::string &filePath) {
} }
bool cApp::initLuaState() { bool cApp::initLuaState() {
auto luac = m_cartridge->luac(); const auto luac = m_cartridge->luac();
if (!luac) { if (!luac) {
wxLogError("No Lua bytecode in cartridge"); wxLogError("No Lua bytecode in cartridge");
return false; return false;
@@ -68,7 +86,7 @@ bool cApp::initLuaState() {
lua_pop(m_luaState, 1); lua_pop(m_luaState, 1);
const auto &bytecode = luac->getData(); const auto &bytecode = luac->getData();
int result = luaL_loadbuffer(m_luaState, const int result = luaL_loadbuffer(m_luaState,
reinterpret_cast<const char *>(bytecode.data()), reinterpret_cast<const char *>(bytecode.data()),
bytecode.size(), bytecode.size(),
"cartridge"); "cartridge");
@@ -98,8 +116,8 @@ bool cApp::initLuaState() {
if (lua_istable(m_luaState, -1)) { if (lua_istable(m_luaState, -1)) {
lua_getfield(m_luaState, -1, "ClassName"); lua_getfield(m_luaState, -1, "ClassName");
if (lua_isstring(m_luaState, -1)) { if (lua_isstring(m_luaState, -1)) {
const char *className = lua_tostring(m_luaState, -1); if (const char *className = lua_tostring(m_luaState, -1);
if (strcmp(className, "ZCartridge") == 0) { strcmp(className, "ZCartridge") == 0) {
lua_pop(m_luaState, 1); // pop ClassName lua_pop(m_luaState, 1); // pop ClassName
m_cartridgeRef = luaL_ref(m_luaState, LUA_REGISTRYINDEX); // store reference, pops table m_cartridgeRef = luaL_ref(m_luaState, LUA_REGISTRYINDEX); // store reference, pops table
lua_pop(m_luaState, 1); // pop key lua_pop(m_luaState, 1); // pop key
@@ -115,6 +133,16 @@ bool cApp::initLuaState() {
// Initialize media manager // Initialize media manager
wherigo::MediaManager::getInstance().init(m_luaState, m_cartridge.get()); 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) { if (m_cartridgeRef == LUA_NOREF) {
wxLogError("No ZCartridge object found!"); wxLogError("No ZCartridge object found!");
return false; return false;
@@ -126,7 +154,7 @@ bool cApp::initLuaState() {
return true; return true;
} }
void cApp::startGame() { void cApp::startGame() const {
if (!m_luaState || m_cartridgeRef == LUA_NOREF) { if (!m_luaState || m_cartridgeRef == LUA_NOREF) {
wxLogError("No cartridge loaded"); wxLogError("No cartridge loaded");
return; return;
@@ -190,38 +218,38 @@ std::string cApp::getAutoSavePath() const {
return ""; return "";
} }
bool cApp::saveGameState(const std::string &saveFilePath) { bool cApp::saveGameState(const std::string &saveFilePath) const {
if (!m_luaState) { if (!m_luaState) {
wxLogError("No Lua state to save"); wxLogError("No Lua state to save");
return false; return false;
} }
std::string savePath = saveFilePath.empty() ? getAutoSavePath() : saveFilePath; const std::string savePath = saveFilePath.empty() ? getAutoSavePath() : saveFilePath;
if (savePath.empty()) { if (savePath.empty()) {
wxLogError("No save path available"); wxLogError("No save path available");
return false; return false;
} }
bool success = wherigo::LuaPersistence::saveState(m_luaState, savePath); const bool success = wherigo::LuaPersistence::saveState(m_luaState, savePath);
if (success) { if (success) {
wxLogMessage("Game saved to: %s", savePath); wxLogMessage("Game saved to: %s", savePath);
} }
return success; return success;
} }
bool cApp::loadGameState(const std::string &saveFilePath) { bool cApp::loadGameState(const std::string &saveFilePath) const {
if (!m_luaState) { if (!m_luaState) {
wxLogError("No Lua state to load into"); wxLogError("No Lua state to load into");
return false; return false;
} }
std::string loadPath = saveFilePath.empty() ? getAutoSavePath() : saveFilePath; const std::string loadPath = saveFilePath.empty() ? getAutoSavePath() : saveFilePath;
if (loadPath.empty() || !wxFileExists(loadPath)) { if (loadPath.empty() || !wxFileExists(loadPath)) {
wxLogError("Save file not found: %s", loadPath); wxLogError("Save file not found: %s", loadPath);
return false; return false;
} }
bool success = wherigo::LuaPersistence::loadState(m_luaState, loadPath); const bool success = wherigo::LuaPersistence::loadState(m_luaState, loadPath);
if (success) { if (success) {
wxLogMessage("Game loaded from: %s", loadPath); wxLogMessage("Game loaded from: %s", loadPath);
// Notify game engine of state change // Notify game engine of state change
@@ -247,19 +275,19 @@ std::string cApp::getCompletionLogPath() const {
return ""; return "";
} }
bool cApp::generateCompletionLog(const std::string &logFilePath) { bool cApp::generateCompletionLog(const std::string &logFilePath) const {
if (!m_luaState) { if (!m_luaState) {
wxLogError("No Lua state for completion log"); wxLogError("No Lua state for completion log");
return false; return false;
} }
std::string logPath = logFilePath.empty() ? getCompletionLogPath() : logFilePath; const std::string logPath = logFilePath.empty() ? getCompletionLogPath() : logFilePath;
if (logPath.empty()) { if (logPath.empty()) {
wxLogError("No completion log path available"); wxLogError("No completion log path available");
return false; return false;
} }
bool success = wherigo::WherigoCompletion::generateCompletionLog(m_luaState, logPath, "Player"); const bool success = wherigo::WherigoCompletion::generateCompletionLog(m_luaState, logPath, "Player");
if (success) { if (success) {
wxLogMessage("Completion log generated: %s", logPath); wxLogMessage("Completion log generated: %s", logPath);
} }
+243 -64
View File
@@ -1,15 +1,18 @@
#include "lua/game_engine.h" #include "lua/game_engine.h"
#include "lua/geo_utils.h"
extern "C" { extern "C" {
#include <lua.h>
#include <lauxlib.h> #include <lauxlib.h>
#include <lua.h>
} }
#include <wx/log.h> #include <wx/log.h>
#include <string>
#include <vector>
namespace wherigo { namespace wherigo {
// Define the custom event
wxDEFINE_EVENT(EVT_GAME_STATE_CHANGED, GameStateEvent); wxDEFINE_EVENT(EVT_GAME_STATE_CHANGED, GameStateEvent);
wxBEGIN_EVENT_TABLE(GameEngine, wxEvtHandler) wxBEGIN_EVENT_TABLE(GameEngine, wxEvtHandler)
@@ -21,12 +24,8 @@ GameEngine& GameEngine::getInstance() {
return instance; return instance;
} }
GameEngine::GameEngine() : m_gameTimer(this) { GameEngine::GameEngine() : m_gameTimer(this) {}
} GameEngine::~GameEngine() { shutdown(); }
GameEngine::~GameEngine() {
shutdown();
}
void GameEngine::init(lua_State *L, int cartridgeRef) { void GameEngine::init(lua_State *L, int cartridgeRef) {
m_luaState = L; m_luaState = L;
@@ -42,15 +41,13 @@ void GameEngine::shutdown() {
void GameEngine::start() { void GameEngine::start() {
if (m_running) return; if (m_running) return;
m_running = true; m_running = true;
m_gameTimer.Start(1000); // 1 second tick m_gameTimer.Start(1000);
wxLogDebug("GameEngine started"); wxLogDebug("GameEngine started");
} }
void GameEngine::stop() { void GameEngine::stop() {
if (!m_running) return; if (!m_running) return;
m_gameTimer.Stop(); m_gameTimer.Stop();
m_running = false; m_running = false;
wxLogDebug("GameEngine stopped"); wxLogDebug("GameEngine stopped");
@@ -67,93 +64,89 @@ void GameEngine::updatePlayerPosition(double lat, double lng, double alt) {
lua_getglobal(m_luaState, "Player"); lua_getglobal(m_luaState, "Player");
if (lua_istable(m_luaState, -1)) { if (lua_istable(m_luaState, -1)) {
lua_newtable(m_luaState); lua_newtable(m_luaState);
lua_pushnumber(m_luaState, lat); lua_pushnumber(m_luaState, lat); lua_setfield(m_luaState, -2, "latitude");
lua_setfield(m_luaState, -2, "latitude"); lua_pushnumber(m_luaState, lng); lua_setfield(m_luaState, -2, "longitude");
lua_pushnumber(m_luaState, lng); lua_pushnumber(m_luaState, alt); lua_setfield(m_luaState, -2, "altitude");
lua_setfield(m_luaState, -2, "longitude");
lua_pushnumber(m_luaState, alt);
lua_setfield(m_luaState, -2, "altitude");
lua_setfield(m_luaState, -2, "ObjectLocation"); lua_setfield(m_luaState, -2, "ObjectLocation");
} }
lua_pop(m_luaState, 1); lua_pop(m_luaState, 1);
if (!m_isProcessing) {
m_isProcessing = true;
checkZones(); checkZones();
}
void GameEngine::onGameTick(wxTimerEvent& event) {
if (!m_luaState || !m_running) return;
checkTimers();
// Notify listeners of potential state changes
notifyStateChanged(); notifyStateChanged();
m_isProcessing = false;
}
} }
void GameEngine::onGameTick(wxTimerEvent&) {
if (!m_luaState || !m_running || m_isProcessing) return;
m_isProcessing = true;
checkTimers();
notifyStateChanged();
m_isProcessing = false;
}
// ── Timer processing ──────────────────────────────────────────────────────────
void GameEngine::checkTimers() { void GameEngine::checkTimers() {
if (!m_luaState) return; if (!m_luaState) return;
// Iterate through all global variables to find running timers
lua_getglobal(m_luaState, "_G"); lua_getglobal(m_luaState, "_G");
lua_pushnil(m_luaState); lua_pushnil(m_luaState);
while (lua_next(m_luaState, -2) != 0) { while (lua_next(m_luaState, -2) != 0) {
if (lua_istable(m_luaState, -1)) { if (lua_istable(m_luaState, -1)) {
lua_getfield(m_luaState, -1, "ClassName"); lua_getfield(m_luaState, -1, "ClassName");
if (lua_isstring(m_luaState, -1) && strcmp(lua_tostring(m_luaState, -1), "ZTimer") == 0) { const bool isTimer = lua_isstring(m_luaState, -1) &&
lua_pop(m_luaState, 1); // pop ClassName !strcmp(lua_tostring(m_luaState, -1), "ZTimer");
lua_pop(m_luaState, 1);
if (isTimer) {
lua_getfield(m_luaState, -1, "Running"); 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); lua_pop(m_luaState, 1);
if (running) { if (running) {
// Get timer info
lua_getfield(m_luaState, -1, "Name"); 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_pop(m_luaState, 1);
lua_getfield(m_luaState, -1, "Remaining"); 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); lua_pop(m_luaState, 1);
// If Remaining not set, initialize from Duration
if (remaining < 0) { if (remaining < 0) {
lua_getfield(m_luaState, -1, "Duration"); 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); lua_pop(m_luaState, 1);
} }
// Decrement remaining time
remaining -= 1.0; remaining -= 1.0;
lua_pushnumber(m_luaState, remaining); lua_pushnumber(m_luaState, remaining);
lua_setfield(m_luaState, -2, "Remaining"); lua_setfield(m_luaState, -2, "Remaining");
// Call OnTick if exists
lua_getfield(m_luaState, -1, "OnTick"); lua_getfield(m_luaState, -1, "OnTick");
if (lua_isfunction(m_luaState, -1)) { 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) { 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); lua_pop(m_luaState, 1);
} }
} else { } else {
lua_pop(m_luaState, 1); lua_pop(m_luaState, 1);
} }
// Check if elapsed
if (remaining <= 0) { if (remaining <= 0) {
wxLogDebug("Timer elapsed: %s", name); wxLogDebug("Timer elapsed: %s", name);
// Stop the timer
lua_pushboolean(m_luaState, 0); lua_pushboolean(m_luaState, 0);
lua_setfield(m_luaState, -2, "Running"); lua_setfield(m_luaState, -2, "Running");
// Call OnElapsed if exists
lua_getfield(m_luaState, -1, "OnElapsed"); lua_getfield(m_luaState, -1, "OnElapsed");
if (lua_isfunction(m_luaState, -1)) { 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) { 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); lua_pop(m_luaState, 1);
} }
} else { } else {
@@ -161,49 +154,236 @@ void GameEngine::checkTimers() {
} }
} }
} }
}
}
lua_pop(m_luaState, 1);
}
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 { } else {
lua_pop(m_luaState, 1); // pop ClassName 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_pop(m_luaState, 1); // pop value
} lua_pushstring(L, newState.c_str()); lua_setfield(L, zoneIdx, "State");
lua_pop(m_luaState, 1); // pop _G lua_pushstring(L, newState.c_str()); lua_setfield(L, zoneIdx, "_state");
return stateChanged;
} }
void GameEngine::checkZones() { void GameEngine::checkZones() {
if (!m_luaState) return; 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<int> zoneRefs;
lua_getglobal(m_luaState, "_G"); lua_getglobal(m_luaState, "_G");
lua_pushnil(m_luaState); lua_pushnil(m_luaState);
while (lua_next(m_luaState, -2) != 0) { while (lua_next(m_luaState, -2) != 0) {
if (lua_istable(m_luaState, -1)) { if (lua_istable(m_luaState, -1)) {
lua_getfield(m_luaState, -1, "ClassName"); lua_getfield(m_luaState, -1, "ClassName");
if (lua_isstring(m_luaState, -1) && strcmp(lua_tostring(m_luaState, -1), "Zone") == 0) { const bool isZone = lua_isstring(m_luaState,-1) &&
lua_pop(m_luaState, 1); // pop ClassName !strcmp(lua_tostring(m_luaState,-1), "Zone");
lua_pop(m_luaState, 1);
if (isZone) {
lua_getfield(m_luaState, -1, "Active"); 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); lua_pop(m_luaState, 1);
if (active) { if (active) {
// TODO: Check if player is inside zone using Points lua_pushvalue(m_luaState, -1);
// For now, just log that we would check zoneRefs.push_back(luaL_ref(m_luaState, LUA_REGISTRYINDEX));
lua_getfield(m_luaState, -1, "Name"); }
const char *name = lua_isstring(m_luaState, -1) ? lua_tostring(m_luaState, -1) : "(unnamed)"; }
}
lua_pop(m_luaState, 1);
}
lua_pop(m_luaState, 1); lua_pop(m_luaState, 1);
// This is where you would implement point-in-polygon check // Process each zone
// and call OnEnter/OnExit callbacks 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 (inInventory) {
lua_getfield(m_luaState, -1, "Visible");
const bool visible = lua_toboolean(m_luaState, -1);
lua_pop(m_luaState, 1);
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 value
} }
lua_pop(m_luaState, 1); // pop _G 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() { void GameEngine::notifyStateChanged() {
@@ -212,4 +392,3 @@ void GameEngine::notifyStateChanged() {
} }
} // namespace wherigo } // namespace wherigo
+2 -2
View File
@@ -69,7 +69,7 @@ void MediaManager::buildMediaIndex(lua_State *L) {
} }
std::vector<uint8_t> MediaManager::getMediaByName(const std::string &name) { std::vector<uint8_t> MediaManager::getMediaByName(const std::string &name) {
auto it = m_nameToIndex.find(name); const auto it = m_nameToIndex.find(name);
if (it == m_nameToIndex.end()) { if (it == m_nameToIndex.end()) {
wxLogWarning("Media not found: %s", name.c_str()); wxLogWarning("Media not found: %s", name.c_str());
return {}; return {};
@@ -84,7 +84,7 @@ std::vector<uint8_t> MediaManager::getMediaByIndex(int index) {
return {}; return {};
} }
auto media = m_cartridge->getMedia(index); const auto media = m_cartridge->getMedia(index);
if (!media) { if (!media) {
wxLogWarning("Media index %d not found in cartridge", index); wxLogWarning("Media index %d not found in cartridge", index);
return {}; return {};
+558 -343
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -28,7 +28,7 @@ int zobject_MoveTo(lua_State *L) {
wxLogDebug("MoveTo: %s -> %s", srcName, dstName); wxLogDebug("MoveTo: %s -> %s", srcName, dstName);
// Notify game state change GameEngine::getInstance().rebuildPlayerInventory();
GameEngine::getInstance().notifyStateChanged(); GameEngine::getInstance().notifyStateChanged();
return 0; return 0;
+92 -81
View File
@@ -37,7 +37,7 @@ cGameScreen::cGameScreen(wxWindow *parent)
auto *menuBar = new wxMenuBar; auto *menuBar = new wxMenuBar;
menuBar->Append(menuFile, "&Datei"); menuBar->Append(menuFile, "&Datei");
menuBar->Append(menuHelp, "&Hilfe"); menuBar->Append(menuHelp, "&Hilfe");
SetMenuBar(menuBar); wxFrameBase::SetMenuBar(menuBar);
// Main sizer // Main sizer
auto *mainSizer = new wxBoxSizer(wxVERTICAL); auto *mainSizer = new wxBoxSizer(wxVERTICAL);
@@ -45,15 +45,6 @@ cGameScreen::cGameScreen(wxWindow *parent)
// Notebook with tabs // Notebook with tabs
m_notebook = new wxNotebook(this, wxID_ANY); 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 // Tasks tab
auto *taskPanel = new wxPanel(m_notebook); auto *taskPanel = new wxPanel(m_notebook);
auto *taskSizer = new wxBoxSizer(wxVERTICAL); auto *taskSizer = new wxBoxSizer(wxVERTICAL);
@@ -63,6 +54,15 @@ cGameScreen::cGameScreen(wxWindow *parent)
taskPanel->SetSizer(taskSizer); taskPanel->SetSizer(taskSizer);
m_notebook->AddPage(taskPanel, "Aufgaben"); 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 // Inventory tab
auto *inventoryPanel = new wxPanel(m_notebook); auto *inventoryPanel = new wxPanel(m_notebook);
auto *inventorySizer = new wxBoxSizer(wxVERTICAL); auto *inventorySizer = new wxBoxSizer(wxVERTICAL);
@@ -97,8 +97,8 @@ cGameScreen::cGameScreen(wxWindow *parent)
CentreOnScreen(); CentreOnScreen();
// Status bar // Status bar
CreateStatusBar(); wxFrameBase::CreateStatusBar();
SetStatusText("Spiel läuft"); wxFrameBase::SetStatusText("Spiel läuft");
// Event bindings // Event bindings
Bind(wxEVT_CLOSE_WINDOW, &cGameScreen::OnClose, this); Bind(wxEVT_CLOSE_WINDOW, &cGameScreen::OnClose, this);
@@ -158,7 +158,7 @@ void cGameScreen::OnSaveGame(wxCommandEvent& event) {
} }
void cGameScreen::OnLoadGame(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); "Laden", wxYES_NO | wxICON_QUESTION);
if (result == wxYES) { if (result == wxYES) {
@@ -185,9 +185,8 @@ void cGameScreen::OnExportCompletion(wxCommandEvent& event) {
return; return;
} }
wxString filePath = saveDialog.GetPath(); if (const wxString filePath = saveDialog.GetPath();
wxGetApp().generateCompletionLog(filePath.ToStdString())) {
if (wxGetApp().generateCompletionLog(filePath.ToStdString())) {
wxMessageBox("Completion Log wurde erfolgreich exportiert!\n\nDiese Datei kann auf wherigo.com hochgeladen werden, um den Abschluss nachzuweisen.", wxMessageBox("Completion Log wurde erfolgreich exportiert!\n\nDiese Datei kann auf wherigo.com hochgeladen werden, um den Abschluss nachzuweisen.",
"Export erfolgreich", wxOK | wxICON_INFORMATION); "Export erfolgreich", wxOK | wxICON_INFORMATION);
} else { } else {
@@ -201,6 +200,9 @@ void cGameScreen::OnGameStateChanged(wxEvent& event) {
} }
void cGameScreen::refreshUI() { void cGameScreen::refreshUI() {
if (wxGetApp().isCartridgeLoaded())
SetTitle(wxString::FromUTF8(wxGetApp().getCartridge()->cartridgeName()));
// Freeze to prevent flickering and focus loss // Freeze to prevent flickering and focus loss
Freeze(); Freeze();
@@ -214,8 +216,8 @@ void cGameScreen::refreshUI() {
} }
void cGameScreen::populateZones() { void cGameScreen::populateZones() {
int selection = m_zoneList->GetSelection(); const int selection = m_zoneList->GetSelection();
wxString selectedItem = (selection != wxNOT_FOUND) ? m_zoneList->GetString(selection) : ""; const wxString selectedItem = (selection != wxNOT_FOUND) ? m_zoneList->GetString(selection) : "";
m_zoneList->Clear(); m_zoneList->Clear();
@@ -232,11 +234,11 @@ void cGameScreen::populateZones() {
lua_pop(L, 1); lua_pop(L, 1);
lua_getfield(L, -1, "Active"); lua_getfield(L, -1, "Active");
bool active = lua_toboolean(L, -1); const bool active = lua_toboolean(L, -1);
lua_pop(L, 1); lua_pop(L, 1);
lua_getfield(L, -1, "Visible"); lua_getfield(L, -1, "Visible");
bool visible = lua_toboolean(L, -1); const bool visible = lua_toboolean(L, -1);
lua_pop(L, 1); lua_pop(L, 1);
if (active && visible) { if (active && visible) {
@@ -256,16 +258,15 @@ void cGameScreen::populateZones() {
// Restore selection // Restore selection
if (!selectedItem.IsEmpty()) { if (!selectedItem.IsEmpty()) {
int idx = m_zoneList->FindString(selectedItem); if (const int idx = m_zoneList->FindString(selectedItem); idx != wxNOT_FOUND) {
if (idx != wxNOT_FOUND) {
m_zoneList->SetSelection(idx); m_zoneList->SetSelection(idx);
} }
} }
} }
void cGameScreen::populateTasks() { void cGameScreen::populateTasks() {
int selection = m_taskList->GetSelection(); const int selection = m_taskList->GetSelection();
wxString selectedItem = (selection != wxNOT_FOUND) ? m_taskList->GetString(selection) : ""; const wxString selectedItem = (selection != wxNOT_FOUND) ? m_taskList->GetString(selection) : "";
m_taskList->Clear(); m_taskList->Clear();
@@ -286,7 +287,7 @@ void cGameScreen::populateTasks() {
lua_pop(L, 1); lua_pop(L, 1);
lua_getfield(L, -1, "Complete"); lua_getfield(L, -1, "Complete");
bool complete = lua_toboolean(L, -1); const bool complete = lua_toboolean(L, -1);
lua_pop(L, 1); lua_pop(L, 1);
if (active && !complete) { if (active && !complete) {
@@ -306,16 +307,15 @@ void cGameScreen::populateTasks() {
// Restore selection // Restore selection
if (!selectedItem.IsEmpty()) { if (!selectedItem.IsEmpty()) {
int idx = m_taskList->FindString(selectedItem); if (const int idx = m_taskList->FindString(selectedItem); idx != wxNOT_FOUND) {
if (idx != wxNOT_FOUND) {
m_taskList->SetSelection(idx); m_taskList->SetSelection(idx);
} }
} }
} }
void cGameScreen::populateInventory() { void cGameScreen::populateInventory() {
int selection = m_inventoryList->GetSelection(); const int selection = m_inventoryList->GetSelection();
wxString selectedItem = (selection != wxNOT_FOUND) ? m_inventoryList->GetString(selection) : ""; const wxString selectedItem = (selection != wxNOT_FOUND) ? m_inventoryList->GetString(selection) : "";
m_inventoryList->Clear(); m_inventoryList->Clear();
@@ -343,16 +343,22 @@ void cGameScreen::populateInventory() {
if (lua_istable(L, -1)) { if (lua_istable(L, -1)) {
// Check if Container is Player // Check if Container is Player
lua_getglobal(L, "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 lua_pop(L, 2); // pop Player and Container
if (isInInventory) { if (isInInventory) {
lua_getfield(L, -1, "Visible");
const bool visible = lua_toboolean(L, -1);
lua_pop(L, 1);
if (visible) {
lua_getfield(L, -1, "Name"); lua_getfield(L, -1, "Name");
if (lua_isstring(L, -1)) { if (lua_isstring(L, -1)) {
m_inventoryList->Append(wxString::FromUTF8(lua_tostring(L, -1))); m_inventoryList->Append(wxString::FromUTF8(lua_tostring(L, -1)));
} }
lua_pop(L, 1); lua_pop(L, 1);
} }
}
} else { } else {
lua_pop(L, 1); lua_pop(L, 1);
} }
@@ -366,16 +372,16 @@ void cGameScreen::populateInventory() {
// Restore selection // Restore selection
if (!selectedItem.IsEmpty()) { if (!selectedItem.IsEmpty()) {
int idx = m_inventoryList->FindString(selectedItem); if (const int idx = m_inventoryList->FindString(selectedItem);
if (idx != wxNOT_FOUND) { idx != wxNOT_FOUND) {
m_inventoryList->SetSelection(idx); m_inventoryList->SetSelection(idx);
} }
} }
} }
void cGameScreen::populateCharacters() { void cGameScreen::populateCharacters() {
int selection = m_characterList->GetSelection(); const int selection = m_characterList->GetSelection();
wxString selectedItem = (selection != wxNOT_FOUND) ? m_characterList->GetString(selection) : ""; const wxString selectedItem = (selection != wxNOT_FOUND) ? m_characterList->GetString(selection) : "";
m_characterList->Clear(); m_characterList->Clear();
@@ -392,16 +398,16 @@ void cGameScreen::populateCharacters() {
lua_pop(L, 1); lua_pop(L, 1);
lua_getfield(L, -1, "Active"); lua_getfield(L, -1, "Active");
bool active = lua_toboolean(L, -1); const bool active = lua_toboolean(L, -1);
lua_pop(L, 1); lua_pop(L, 1);
lua_getfield(L, -1, "Visible"); lua_getfield(L, -1, "Visible");
bool visible = lua_toboolean(L, -1); const bool visible = lua_toboolean(L, -1);
lua_pop(L, 1); lua_pop(L, 1);
// Check if character has a container (is somewhere) // Check if character has a container (is somewhere)
lua_getfield(L, -1, "Container"); lua_getfield(L, -1, "Container");
bool hasLocation = lua_istable(L, -1); const bool hasLocation = lua_istable(L, -1);
lua_pop(L, 1); lua_pop(L, 1);
if (active && visible && hasLocation) { if (active && visible && hasLocation) {
@@ -435,16 +441,16 @@ void cGameScreen::populateCharacters() {
// Restore selection // Restore selection
if (!selectedItem.IsEmpty()) { if (!selectedItem.IsEmpty()) {
int idx = m_characterList->FindString(selectedItem); if (const int idx = m_characterList->FindString(selectedItem);
if (idx != wxNOT_FOUND) { idx != wxNOT_FOUND) {
m_characterList->SetSelection(idx); m_characterList->SetSelection(idx);
} }
} }
} }
void cGameScreen::populateItems() { void cGameScreen::populateItems() {
int selection = m_itemList->GetSelection(); const int selection = m_itemList->GetSelection();
wxString selectedItem = (selection != wxNOT_FOUND) ? m_itemList->GetString(selection) : ""; const wxString selectedItem = (selection != wxNOT_FOUND) ? m_itemList->GetString(selection) : "";
m_itemList->Clear(); m_itemList->Clear();
@@ -461,11 +467,11 @@ void cGameScreen::populateItems() {
lua_pop(L, 1); lua_pop(L, 1);
lua_getfield(L, -1, "Active"); lua_getfield(L, -1, "Active");
bool active = lua_toboolean(L, -1); const bool active = lua_toboolean(L, -1);
lua_pop(L, 1); lua_pop(L, 1);
lua_getfield(L, -1, "Visible"); lua_getfield(L, -1, "Visible");
bool visible = lua_toboolean(L, -1); const bool visible = lua_toboolean(L, -1);
lua_pop(L, 1); lua_pop(L, 1);
// Check if item is NOT in player inventory (in a zone) // Check if item is NOT in player inventory (in a zone)
@@ -512,8 +518,7 @@ void cGameScreen::populateItems() {
// Restore selection // Restore selection
if (!selectedItem.IsEmpty()) { if (!selectedItem.IsEmpty()) {
int idx = m_itemList->FindString(selectedItem); if (const int idx = m_itemList->FindString(selectedItem); idx != wxNOT_FOUND) {
if (idx != wxNOT_FOUND) {
m_itemList->SetSelection(idx); m_itemList->SetSelection(idx);
} }
} }
@@ -658,10 +663,10 @@ void cGameScreen::OnTaskSelected(wxCommandEvent& event) {
} }
void cGameScreen::OnInventorySelected(wxCommandEvent& event) { void cGameScreen::OnInventorySelected(wxCommandEvent& event) {
int sel = m_inventoryList->GetSelection(); const int sel = m_inventoryList->GetSelection();
if (sel == wxNOT_FOUND) return; if (sel == wxNOT_FOUND) return;
wxString itemName = m_inventoryList->GetString(sel); const wxString itemName = m_inventoryList->GetString(sel);
lua_State *L = wxGetApp().getLuaState(); lua_State *L = wxGetApp().getLuaState();
if (!L) return; if (!L) return;
@@ -712,12 +717,12 @@ void cGameScreen::OnInventorySelected(wxCommandEvent& event) {
} }
void cGameScreen::OnCharacterSelected(wxCommandEvent& event) { void cGameScreen::OnCharacterSelected(wxCommandEvent& event) {
int sel = m_characterList->GetSelection(); const int sel = m_characterList->GetSelection();
if (sel == wxNOT_FOUND) return; if (sel == wxNOT_FOUND) return;
// Extract name from "Name (Location)" format // Extract name from "Name (Location)" format
wxString fullText = m_characterList->GetString(sel); const wxString fullText = m_characterList->GetString(sel);
wxString charName = fullText.BeforeFirst('(').Trim(); const wxString charName = fullText.BeforeFirst('(').Trim();
lua_State *L = wxGetApp().getLuaState(); lua_State *L = wxGetApp().getLuaState();
if (!L) return; if (!L) return;
@@ -734,13 +739,13 @@ void cGameScreen::OnCharacterSelected(wxCommandEvent& event) {
lua_getfield(L, -1, "Name"); lua_getfield(L, -1, "Name");
if (lua_isstring(L, -1)) { 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); lua_pop(L, 1);
if (currentName == charName) { if (currentName == charName) {
// Get character description // Get character description
lua_getfield(L, -1, "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); lua_pop(L, 1);
// Get media if exists // Get media if exists
@@ -789,12 +794,12 @@ void cGameScreen::OnCharacterSelected(wxCommandEvent& event) {
} }
void cGameScreen::OnItemSelected(wxCommandEvent& event) { void cGameScreen::OnItemSelected(wxCommandEvent& event) {
int sel = m_itemList->GetSelection(); const int sel = m_itemList->GetSelection();
if (sel == wxNOT_FOUND) return; if (sel == wxNOT_FOUND) return;
// Extract name from "Name (Location)" format // Extract name from "Name (Location)" format
wxString fullText = m_itemList->GetString(sel); const wxString fullText = m_itemList->GetString(sel);
wxString itemName = fullText.BeforeFirst('(').Trim(); const wxString itemName = fullText.BeforeFirst('(').Trim();
lua_State *L = wxGetApp().getLuaState(); lua_State *L = wxGetApp().getLuaState();
if (!L) return; if (!L) return;
@@ -811,13 +816,13 @@ void cGameScreen::OnItemSelected(wxCommandEvent& event) {
lua_getfield(L, -1, "Name"); lua_getfield(L, -1, "Name");
if (lua_isstring(L, -1)) { 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); lua_pop(L, 1);
if (currentName == itemName) { if (currentName == itemName) {
// Get item description // Get item description
lua_getfield(L, -1, "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); lua_pop(L, 1);
// Get media if exists // Get media if exists
@@ -836,7 +841,7 @@ void cGameScreen::OnItemSelected(wxCommandEvent& event) {
lua_getfield(L, -1, "Commands"); lua_getfield(L, -1, "Commands");
wxArrayString commands; wxArrayString commands;
if (lua_istable(L, -1)) { 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++) { for (int i = 1; i <= n; i++) {
lua_rawgeti(L, -1, i); lua_rawgeti(L, -1, i);
if (lua_istable(L, -1)) { if (lua_istable(L, -1)) {
@@ -860,7 +865,7 @@ void cGameScreen::OnItemSelected(wxCommandEvent& event) {
} }
// Use WherigoMessageDialog to show with media support // Use WherigoMessageDialog to show with media support
std::vector<wxString> buttons = {"OK"}; const std::vector<wxString> buttons = {"OK"};
wherigo::WherigoMessageDialog dlg(this, message, itemName, buttons, mediaName); wherigo::WherigoMessageDialog dlg(this, message, itemName, buttons, mediaName);
dlg.ShowModal(); dlg.ShowModal();
@@ -893,8 +898,9 @@ void cGameScreen::OnItemSelected(wxCommandEvent& event) {
} }
void cGameScreen::OnMapSim(wxCommandEvent&) { void cGameScreen::OnMapSim(wxCommandEvent&) {
std::vector<std::pair<double, double>> zoneCoords; std::vector<ZoneInfo> zoneInfos;
double lat = 53.3, lon = 10.39; double lat = 53.3, lon = 10.39;
lua_State *L = wxGetApp().getLuaState(); lua_State *L = wxGetApp().getLuaState();
if (L) { if (L) {
lua_getglobal(L, "_G"); lua_getglobal(L, "_G");
@@ -903,41 +909,46 @@ void cGameScreen::OnMapSim(wxCommandEvent&) {
if (lua_istable(L, -1)) { if (lua_istable(L, -1)) {
lua_getfield(L, -1, "ClassName"); lua_getfield(L, -1, "ClassName");
if (lua_isstring(L, -1) && strcmp(lua_tostring(L, -1), "Zone") == 0) { 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"); lua_getfield(L, -1, "Active");
bool active = lua_toboolean(L, -1) || true; const bool active = lua_toboolean(L, -1);
lua_pop(L, 1); lua_pop(L, 1);
lua_getfield(L, -1, "Visible"); if (!active) { lua_pop(L, 1); continue; }
bool visible = lua_toboolean(L, -1) || true;
// Read name
lua_getfield(L, -1, "Name");
std::string zoneName = lua_isstring(L, -1) ? lua_tostring(L, -1) : "";
lua_pop(L, 1); lua_pop(L, 1);
if (active && visible) {
// Read OriginalPoint (set by Wherigo Builder on each zone)
lua_getfield(L, -1, "OriginalPoint"); lua_getfield(L, -1, "OriginalPoint");
if (lua_istable(L, -1)) { if (lua_istable(L, -1)) {
lua_getfield(L, -1, "latitude"); lua_getfield(L, -1, "latitude");
lua_getfield(L, -2, "longitude"); lua_getfield(L, -2, "longitude");
if (lua_isnumber(L, -2) && lua_isnumber(L, -1)) { if (lua_isnumber(L, -2) && lua_isnumber(L, -1)) {
double zlat = lua_tonumber(L, -2); zoneInfos.push_back({
double zlon = lua_tonumber(L, -1); lua_tonumber(L, -2),
zoneCoords.emplace_back(zlat, zlon); lua_tonumber(L, -1),
zoneName
});
} }
lua_pop(L, 2); lua_pop(L, 2); // pop lat, lon
}
lua_pop(L, 1); // pop OriginalPoint (table or nil)
} else { } else {
lua_pop(L, 1); lua_pop(L, 1); // pop ClassName
}
lua_pop(L, 1); // pop OriginalPoint
}
} else {
lua_pop(L, 1);
} }
} }
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; if (!zoneInfos.empty()) {
lon = zoneCoords[0].second; 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(); mapSim->Show();
} }
+352 -52
View File
@@ -1,79 +1,379 @@
#include "ui/map_sim_frame.h" #include "ui/map_sim_frame.h"
#include <wx/sizer.h> #include "lua/game_engine.h"
extern "C" {
#include <lua.h>
}
#include <wx/button.h> #include <wx/button.h>
#include <wx/webview.h> #include <wx/log.h>
#include <wx/msgdlg.h> #include <wx/msgdlg.h>
#include <thread> #include <wx/sizer.h>
#include <chrono> #include <wx/webview.h>
enum { ID_PlayBtn = wxID_HIGHEST + 100, ID_ClearBtn, ID_SimTimer };
wxBEGIN_EVENT_TABLE(MapSimFrame, wxFrame) wxBEGIN_EVENT_TABLE(MapSimFrame, wxFrame)
EVT_WEBVIEW_NAVIGATED(wxID_ANY, MapSimFrame::OnWebViewEvent) EVT_BUTTON(ID_PlayBtn, MapSimFrame::OnPlay)
EVT_BUTTON(wxID_ANY, MapSimFrame::OnPlay) EVT_BUTTON(ID_ClearBtn, MapSimFrame::OnClear)
wxEND_EVENT_TABLE() EVT_TIMER(ID_SimTimer, MapSimFrame::OnSimTimer) wxEND_EVENT_TABLE()
// 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<char>(c);
}
return out;
}
MapSimFrame::MapSimFrame(wxWindow *parent, double centerLat, double centerLon,
const std::vector<ZoneInfo> &zoneInfos)
: wxFrame(parent, wxID_ANY, "GPS-Simulation", wxDefaultPosition,
wxSize(900, 700)),
m_simTimer(this, ID_SimTimer), m_zoneInfos(zoneInfos) {
auto *sizer = new wxBoxSizer(wxVERTICAL);
MapSimFrame::MapSimFrame(wxWindow* parent, double centerLat, double centerLon, const std::vector<std::pair<double, double>>& 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); m_webView = wxWebView::New(this, wxID_ANY);
sizer->Add(m_webView, 1, wxEXPAND); sizer->Add(m_webView, 1, wxEXPAND);
auto* playBtn = new wxButton(this, wxID_ANY, "Simulation starten");
sizer->Add(playBtn, 0, wxALL | wxALIGN_CENTER, 8); 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); SetSizer(sizer);
// Leaflet-Karte laden (dynamisch zentriert und Marker)
// 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; wxString html;
html << "<!DOCTYPE html><html><head><meta charset='utf-8'><title>MapSim</title>" html << "<!DOCTYPE html><html><head>"
"<link rel='stylesheet' href='https://unpkg.com/leaflet/dist/leaflet.css'/>" "<meta charset='utf-8'><title>GPS-Sim</title>"
"<link rel='stylesheet' "
"href='https://unpkg.com/leaflet/dist/leaflet.css'/>"
"<link rel='stylesheet' "
"href='https://unpkg.com/leaflet.markercluster/dist/"
"MarkerCluster.css'/>"
"<link rel='stylesheet' "
"href='https://unpkg.com/leaflet.markercluster/dist/"
"MarkerCluster.Default.css'/>"
"<script src='https://unpkg.com/leaflet/dist/leaflet.js'></script>" "<script src='https://unpkg.com/leaflet/dist/leaflet.js'></script>"
"<style>html,body,#map{height:100%%;margin:0;padding:0;}#map{height:600px;}</style>" "<script "
"</head><body><div id='map'></div>" "src='https://unpkg.com/leaflet.markercluster/dist/"
"leaflet.markercluster.js'></script>"
"<style>"
" html,body,#map{height:100%;margin:0;padding:0;}"
" .zone-dot{"
" width:14px;height:14px;border-radius:50%;"
" background:#e44;border:2px solid #900;"
" box-shadow:0 1px 3px rgba(0,0,0,.4);"
" }"
" .zone-cluster{"
" background:#e44;border:3px solid #900;border-radius:50%;"
" color:#fff;font-weight:bold;font-size:13px;"
" display:flex;align-items:center;justify-content:center;"
" box-shadow:0 2px 5px rgba(0,0,0,.4);"
" }"
" .zone-label{"
" background:transparent;border:none;font-weight:bold;"
" font-size:12px;color:#900;"
" text-shadow:0 0 3px #fff,0 0 3px #fff,0 0 3px #fff;"
" white-space:nowrap;pointer-events:none;"
" }"
"</style>"
"</head><body>"
"<div id='map' style='height:calc(100vh - 50px);'></div>"
"<script>" "<script>"
"var map = L.map('map').setView([" << wxString::Format("%.8f, %.8f", centerLat, centerLon) << "], 16);" << wxString::Format("var map=L.map('map').setView([%.8f,%.8f],16);",
"L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {maxZoom: 19}).addTo(map);" centerLat, centerLon)
"var markers = [];"; << "L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',"
// Marker für alle Zonen " {maxZoom:19,attribution:'&copy; OpenStreetMap'}).addTo(map);"
for (const auto& z : zoneCoords) { "var routeMarkers=[];"
html << wxString::Format("var m = L.marker([%.8f, %.8f]).addTo(map); markers.push(m);\n", z.first, z.second); "var routeLine=null;"
}
html << // Zone cluster group with custom cluster icon
"map.on('click', function(e) {" "var zoneCluster=L.markerClusterGroup({"
" var marker = L.marker(e.latlng).addTo(map);" " maxClusterRadius:60,"
" markers.push(marker);" " iconCreateFunction:function(c){"
" window.wx.postMessage(JSON.stringify({lat: e.latlng.lat, lon: e.latlng.lng}));" " var n=c.getChildCount();"
" var s=n<10?32:n<100?38:44;"
" return L.divIcon({"
" html:'<div class=\"zone-cluster\" "
"style=\"width:'+s+'px;height:'+s+'px;\">'+n+'</div>',"
" className:'',"
" iconSize:[s,s],iconAnchor:[s/2,s/2]"
" });"
" }"
"});";
html << "map.addLayer(zoneCluster);"
// Dynamic zone sync: adds new active zones, removes deactivated ones
"var zoneMarkerMap={};"
"function syncZones(zones){"
" var active={};"
" zones.forEach(function(z){"
" active[z.name]=true;"
" if(!zoneMarkerMap[z.name]){"
" var zm=L.marker([z.lat,z.lon],{"
" icon:L.divIcon({"
" html:'<div class=\"zone-dot\"></div>',"
" className:'',"
" iconSize:[14,14],iconAnchor:[7,7]"
" })"
" });"
" zm.bindTooltip(z.name,{"
" permanent:true,direction:'top',"
" offset:[0,-10],className:'zone-label'"
" });"
" zoneCluster.addLayer(zm);"
" zoneMarkerMap[z.name]=zm;"
" }"
" });"
" Object.keys(zoneMarkerMap).forEach(function(name){"
" if(!active[name]){"
" zoneCluster.removeLayer(zoneMarkerMap[name]);"
" delete zoneMarkerMap[name];"
" }"
" });"
"}"
// Player position marker (hidden until first position update)
"var playerMarker=null;"
"function updatePlayerPos(lat,lon){"
" if(!playerMarker){"
" playerMarker=L.marker([lat,lon],{"
" icon:L.divIcon({"
" html:'<div style=\"width:18px;height:18px;border-radius:50%;"
" background:#0077cc;border:3px solid #fff;"
" box-shadow:0 0 0 2px #0077cc,0 2px 6px "
"rgba(0,0,0,.5);\"></div>',"
" className:'',"
" iconSize:[18,18],iconAnchor:[9,9]"
" })"
" }).addTo(map);"
" "
"playerMarker.bindTooltip('Spieler',{permanent:false,direction:'top'}"
");"
" } else {"
" playerMarker.setLatLng([lat,lon]);"
" }"
" map.panTo([lat,lon]);"
"}"
// Click handler: add numbered route waypoint (NOT in cluster)
"function updateLine(){"
" if(routeLine){map.removeLayer(routeLine);routeLine=null;}"
" if(routeMarkers.length>1){"
" var lls=routeMarkers.map(function(m){return m.getLatLng();});"
" "
"routeLine=L.polyline(lls,{color:'#0077cc',weight:2,dashArray:'6,4'})"
".addTo(map);"
" }"
"}"
"map.on('click',function(e){"
" var n=routeMarkers.length+1;"
" var m=L.marker(e.latlng,{"
" icon:L.divIcon({"
" html:'<div "
"style=\"background:#0077cc;color:#fff;border-radius:50%;width:20px;"
" "
"height:20px;line-height:20px;text-align:center;font-size:11px;"
" font-weight:bold;border:2px solid "
"#005299;\">'+n+'</div>',"
" className:'',"
" iconSize:[20,20],iconAnchor:[10,10]"
" })"
" }).addTo(map);"
" routeMarkers.push(m);"
" updateLine();"
" "
"window.wx.postMessage(JSON.stringify({lat:e.latlng.lat,lon:e.latlng."
"lng}));"
"});" "});"
"</script></body></html>"; "</script></body></html>";
m_webView->SetPage(html, ""); m_webView->SetPage(html, "");
} }
void MapSimFrame::OnWebViewEvent(wxWebViewEvent& event) { // ── JS → C++ message
// Empfange Marker-Koordinaten von JS // ──────────────────────────────────────────────────────────
wxString msg = event.GetString();
double lat = 0, lon = 0; static bool parseLatLon(const wxString &json, double &lat, double &lon) {
if (msg.ToDouble(&lat)) { auto extractNum = [&](const wxString &key, double &val) -> bool {
// Not used, see below int pos = json.Find(key);
} if (pos == wxNOT_FOUND)
// In wxWidgets 3.1+ kann man wxWebView::RegisterHandler für JS->C++ nutzen return false;
// Hier: Marker werden über postMessage als JSON gesendet pos += static_cast<int>(key.length());
// TODO: JSON parsen und AddSimPoint aufrufen while (pos < static_cast<int>(json.length()) &&
(json[pos] == ' ' || json[pos] == ':'))
++pos;
const int start = pos;
while (pos < static_cast<int>(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) { void MapSimFrame::AddSimPoint(double lat, double lon) {
m_route.push_back({lat, lon}); m_route.push_back({lat, lon});
} }
void MapSimFrame::OnPlay(wxCommandEvent&) { void MapSimFrame::StartSimulation() {
if (m_route.empty()) { if (m_route.empty())
wxMessageBox("Bitte zuerst Marker setzen.", "Hinweis", wxOK | wxICON_INFORMATION);
return; return;
} m_simIndex = 0;
// Simuliere Bewegung entlang der Route m_simTimer.Start(500);
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::SendPositionToEngine(double lat, double lon) { // ── Button handlers
// TODO: Engine-Integration: GPS-Position setzen // ───────────────────────────────────────────────────────────
// Beispiel: wxGetApp().setSimulatedPosition(lat, lon);
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) {
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);
} }
+48 -40
View File
@@ -4,20 +4,17 @@
#include "ui/game_screen.h" #include "ui/game_screen.h"
#include <wx/filedlg.h> #include <wx/filedlg.h>
#include <wx/mstream.h>
#include <wx/image.h> #include <wx/image.h>
#include <wx/mstream.h>
wxDECLARE_APP(cApp); wxDECLARE_APP(cApp);
enum { enum { ID_OpenCartridge = 1001, ID_StartGame = 1002 };
ID_OpenCartridge = 1001,
ID_StartGame = 1002
};
cStartScreen::cStartScreen() cStartScreen::cStartScreen()
: wxFrame(nullptr, wxID_ANY, "Wherigo Player", wxDefaultPosition, wxSize(800, 800)), : wxFrame(nullptr, wxID_ANY, "Wherigo Player", wxDefaultPosition,
m_gameFrame(nullptr), wxSize(800, 800)),
m_cartridgeLoaded(false) { m_gameFrame(nullptr), m_cartridgeLoaded(false) {
// Menu // Menu
auto *menuFile = new wxMenu; auto *menuFile = new wxMenu;
@@ -31,7 +28,7 @@ cStartScreen::cStartScreen()
auto *menuBar = new wxMenuBar; auto *menuBar = new wxMenuBar;
menuBar->Append(menuFile, "&Datei"); menuBar->Append(menuFile, "&Datei");
menuBar->Append(menuHelp, "&Hilfe"); menuBar->Append(menuHelp, "&Hilfe");
SetMenuBar(menuBar); wxFrameBase::SetMenuBar(menuBar);
// Main sizer // Main sizer
auto *mainSizer = new wxBoxSizer(wxVERTICAL); auto *mainSizer = new wxBoxSizer(wxVERTICAL);
@@ -64,7 +61,8 @@ cStartScreen::cStartScreen()
infoSizer->Add(m_cartridgeAuthor, 0, wxALL | wxALIGN_CENTER, 5); infoSizer->Add(m_cartridgeAuthor, 0, wxALL | wxALIGN_CENTER, 5);
// Description // 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)); m_cartridgeDesc->SetMinSize(wxSize(500, 200));
infoSizer->Add(m_cartridgeDesc, 1, wxALL | wxEXPAND, 10); infoSizer->Add(m_cartridgeDesc, 1, wxALL | wxEXPAND, 10);
@@ -82,8 +80,8 @@ cStartScreen::cStartScreen()
SetSizer(mainSizer); SetSizer(mainSizer);
// Status bar // Status bar
CreateStatusBar(); wxFrameBase::CreateStatusBar();
SetStatusText("Bitte eine Cartridge öffnen"); wxFrameBase::SetStatusText("Bitte eine Cartridge öffnen");
// Event bindings // Event bindings
Bind(wxEVT_MENU, &cStartScreen::OnOpenCartridge, this, ID_OpenCartridge); Bind(wxEVT_MENU, &cStartScreen::OnOpenCartridge, this, ID_OpenCartridge);
@@ -95,7 +93,7 @@ cStartScreen::cStartScreen()
CentreOnScreen(); CentreOnScreen();
} }
void cStartScreen::OnOpenCartridge(wxCommandEvent& event) { void cStartScreen::OnOpenCartridge(wxCommandEvent &event) {
wxFileDialog openFileDialog(this, "Cartridge öffnen", "", "", wxFileDialog openFileDialog(this, "Cartridge öffnen", "", "",
"Wherigo Cartridge (*.gwc)|*.gwc", "Wherigo Cartridge (*.gwc)|*.gwc",
wxFD_OPEN | wxFD_FILE_MUST_EXIST); wxFD_OPEN | wxFD_FILE_MUST_EXIST);
@@ -104,15 +102,19 @@ void cStartScreen::OnOpenCartridge(wxCommandEvent& event) {
return; return;
} }
wxString filePath = openFileDialog.GetPath(); const wxString filePath = openFileDialog.GetPath();
SetStatusText("Lade Cartridge: " + filePath); wxFrameBase::SetStatusText("Lade Cartridge: " + filePath);
// Vor dem Laden: Info-Panel zurücksetzen // Vor dem Laden: Info-Panel zurücksetzen
if (m_infoPanel) { if (m_infoPanel) {
if (m_cartridgeName) m_cartridgeName->SetLabel(""); if (m_cartridgeName)
if (m_cartridgeAuthor) m_cartridgeAuthor->SetLabel(""); m_cartridgeName->SetLabel("");
if (m_cartridgeDesc) m_cartridgeDesc->SetPage(""); if (m_cartridgeAuthor)
if (m_splashImage) m_splashImage->SetBitmap(wxNullBitmap); m_cartridgeAuthor->SetLabel("");
if (m_cartridgeDesc)
m_cartridgeDesc->SetPage("");
if (m_splashImage)
m_splashImage->SetBitmap(wxNullBitmap);
} }
// ggf. alten GameScreen schließen // ggf. alten GameScreen schließen
@@ -125,16 +127,18 @@ void cStartScreen::OnOpenCartridge(wxCommandEvent& event) {
if (wxGetApp().loadCartridge(filePath.ToStdString())) { if (wxGetApp().loadCartridge(filePath.ToStdString())) {
m_cartridgeLoaded = true; m_cartridgeLoaded = true;
showCartridgeInfo(); showCartridgeInfo();
SetStatusText("Cartridge geladen - bereit zum Starten"); wxFrameBase::SetStatusText("Cartridge geladen - bereit zum Starten");
} else { } else {
wxMessageBox("Fehler beim Laden der Cartridge", "Fehler", wxOK | wxICON_ERROR); wxMessageBox("Fehler beim Laden der Cartridge", "Fehler",
SetStatusText("Fehler beim Laden"); wxOK | wxICON_ERROR);
wxFrameBase::SetStatusText("Fehler beim Laden");
} }
} }
void cStartScreen::showCartridgeInfo() { void cStartScreen::showCartridgeInfo() {
auto *cartridge = wxGetApp().getCartridge(); auto *cartridge = wxGetApp().getCartridge();
if (!cartridge) return; if (!cartridge)
return;
// Set cartridge info // Set cartridge info
m_cartridgeName->SetLabel(wxString::FromUTF8(cartridge->cartridgeName())); m_cartridgeName->SetLabel(wxString::FromUTF8(cartridge->cartridgeName()));
@@ -149,21 +153,21 @@ void cStartScreen::showCartridgeInfo() {
} }
// Load splash screen // Load splash screen
auto splash = cartridge->splashScreen(); if (const auto splash = cartridge->splashScreen()) {
if (splash) { const auto data = splash->getData();
auto data = splash->getData();
if (!data.empty()) { if (!data.empty()) {
wxMemoryInputStream stream(data.data(), data.size()); wxMemoryInputStream stream(data.data(), data.size());
wxImage image(stream); if (wxImage image(stream); image.IsOk()) {
if (image.IsOk()) {
// Scale if too large // Scale if too large
int maxWidth = 300; const int maxHeight = 200;
int maxHeight = 200; if (int maxWidth = 300;
if (image.GetWidth() > maxWidth || image.GetHeight() > maxHeight) { image.GetWidth() > maxWidth || image.GetHeight() > maxHeight) {
double scaleX = (double)maxWidth / image.GetWidth(); double scaleX = static_cast<double>(maxWidth) / image.GetWidth();
double scaleY = (double)maxHeight / image.GetHeight(); double scaleY = static_cast<double>(maxHeight) / image.GetHeight();
double scale = std::min(scaleX, scaleY); double scale = std::min(scaleX, scaleY);
image.Rescale(image.GetWidth() * scale, image.GetHeight() * scale, wxIMAGE_QUALITY_HIGH); image.Rescale(static_cast<int>(image.GetWidth() * scale),
static_cast<int>(image.GetHeight() * scale),
wxIMAGE_QUALITY_HIGH);
} }
m_splashImage->SetBitmap(wxBitmap(image)); m_splashImage->SetBitmap(wxBitmap(image));
} }
@@ -176,15 +180,18 @@ void cStartScreen::showCartridgeInfo() {
Layout(); Layout();
} }
void cStartScreen::OnStartGame(wxCommandEvent& event) { void cStartScreen::OnStartGame(wxCommandEvent &event) {
if (!m_cartridgeLoaded) { if (!m_cartridgeLoaded) {
wxMessageBox("Bitte zuerst eine Cartridge laden", "Hinweis", wxOK | wxICON_INFORMATION); wxMessageBox("Bitte zuerst eine Cartridge laden", "Hinweis",
wxOK | wxICON_INFORMATION);
return; return;
} }
// Create game frame if not exists // Create game frame if not exists
if (!m_gameFrame) { if (!m_gameFrame) {
m_gameFrame = new cGameScreen(this); m_gameFrame = new cGameScreen(this);
m_gameFrame->SetPosition(GetPosition());
m_gameFrame->SetSize(GetSize());
} }
// Hide start frame, show game // Hide start frame, show game
@@ -198,10 +205,10 @@ void cStartScreen::OnStartGame(wxCommandEvent& event) {
void cStartScreen::onGameClosed() { void cStartScreen::onGameClosed() {
// Called when game frame is closed // Called when game frame is closed
Show(); 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 // Clean up game frame
if (m_gameFrame) { if (m_gameFrame) {
m_gameFrame->Destroy(); m_gameFrame->Destroy();
@@ -210,7 +217,8 @@ void cStartScreen::OnExit(wxCommandEvent& event) {
Close(true); Close(true);
} }
void cStartScreen::OnAbout(wxCommandEvent& event) { void cStartScreen::OnAbout(wxCommandEvent &event) {
wxMessageBox("Wherigo Player\n\nEin Desktop-Player für Wherigo-Cartridges\n\nVersion 0.1", wxMessageBox("Wherigo Player\n\nEin Desktop-Player für "
"Wherigo-Cartridges\n\nVersion 0.1",
"Über Wherigo Player", wxOK | wxICON_INFORMATION); "Über Wherigo Player", wxOK | wxICON_INFORMATION);
} }
+59 -50
View File
@@ -1,38 +1,39 @@
#include "ui/wherigo_dialog.h" #include "ui/wherigo_dialog.h"
#include "lua/media_manager.h"
#include "lua/game_engine.h" #include "lua/game_engine.h"
#include "lua/media_manager.h"
#include <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/statbmp.h>
#include <wx/button.h> #include <wx/button.h>
#include <wx/html/htmlwin.h>
#include <wx/image.h>
#include <wx/log.h> #include <wx/log.h>
#include <wx/mstream.h> #include <wx/mstream.h>
#include <wx/image.h> #include <wx/sizer.h>
#include <wx/html/htmlwin.h> #include <wx/statbmp.h>
#include <wx/stattext.h>
namespace wherigo { namespace wherigo {
WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent, const wxString &text, WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent,
const wxString &text,
const wxString &title, const wxString &title,
const std::vector<wxString> &buttons, const std::vector<wxString> &buttons,
const wxString &mediaName) const wxString &mediaName)
: wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize,
wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER ) { wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) {
auto *sizer = new wxBoxSizer(wxVERTICAL); auto *sizer = new wxBoxSizer(wxVERTICAL);
// Media/Image (if provided) // Media/Image (if provided)
if (!mediaName.IsEmpty()) { if (!mediaName.IsEmpty()) {
auto mediaData = MediaManager::getInstance().getMediaByName(mediaName.ToStdString()); const auto mediaData =
MediaManager::getInstance().getMediaByName(mediaName.ToStdString());
if (!mediaData.empty()) { if (!mediaData.empty()) {
wxMemoryInputStream stream(mediaData.data(), mediaData.size()); wxMemoryInputStream stream(mediaData.data(), mediaData.size());
wxImage image(stream);
if (image.IsOk()) { if (wxImage image(stream); image.IsOk()) {
// Get screen DPI for scaling // Get screen DPI for scaling
wxWindow* topWindow = wxTheApp->GetTopWindow(); const wxWindow *topWindow = wxTheApp->GetTopWindow();
double contentScaleFactor = 1.0; double contentScaleFactor = 1.0;
if (topWindow) { if (topWindow) {
contentScaleFactor = topWindow->GetContentScaleFactor(); contentScaleFactor = topWindow->GetContentScaleFactor();
@@ -40,14 +41,18 @@ WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent, const wxString &tex
// Scale image - doubled size from original (800x600 instead of 400x300) // Scale image - doubled size from original (800x600 instead of 400x300)
// Adjust for DPI/Retina displays // Adjust for DPI/Retina displays
int maxWidth = static_cast<int>(800 * contentScaleFactor); const int maxWidth = static_cast<int>(800 * contentScaleFactor);
int maxHeight = static_cast<int>(600 * contentScaleFactor);
if (image.GetWidth() > maxWidth || image.GetHeight() > maxHeight) { if (const int maxHeight = static_cast<int>(600 * contentScaleFactor);
double scaleX = (double)maxWidth / image.GetWidth(); image.GetWidth() > maxWidth || image.GetHeight() > maxHeight) {
double scaleY = (double)maxHeight / image.GetHeight(); const double scaleX =
double scale = std::min(scaleX, scaleY); static_cast<double>(maxWidth) / image.GetWidth();
image.Rescale(image.GetWidth() * scale, image.GetHeight() * scale, wxIMAGE_QUALITY_HIGH); const double scaleY =
static_cast<double>(maxHeight) / image.GetHeight();
const double scale = std::min(scaleX, scaleY);
image.Rescale(static_cast<int>(image.GetWidth() * scale),
static_cast<int>(image.GetHeight() * scale),
wxIMAGE_QUALITY_HIGH);
} }
wxBitmap bitmap(image); wxBitmap bitmap(image);
@@ -55,22 +60,22 @@ WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent, const wxString &tex
sizer->Add(imageCtrl, 0, wxALL | wxALIGN_CENTER, 10); sizer->Add(imageCtrl, 0, wxALL | wxALIGN_CENTER, 10);
} else { } else {
wxLogDebug("Failed to load image: %s", mediaName); wxLogDebug("Failed to load image: %s", mediaName);
auto *mediaLabel = new wxStaticText(this, wxID_ANY, auto *mediaLabel = new wxStaticText(
wxString::Format("[Media: %s]", mediaName)); this, wxID_ANY, wxString::Format("[Media: %s]", mediaName));
mediaLabel->SetForegroundColour(*wxLIGHT_GREY); mediaLabel->SetForegroundColour(*wxLIGHT_GREY);
sizer->Add(mediaLabel, 0, wxALL | wxALIGN_CENTER, 10); sizer->Add(mediaLabel, 0, wxALL | wxALIGN_CENTER, 10);
} }
} else { } else {
wxLogDebug("No media data for: %s", mediaName); wxLogDebug("No media data for: %s", mediaName);
auto *mediaLabel = new wxStaticText(this, wxID_ANY, auto *mediaLabel = new wxStaticText(
wxString::Format("[Media: %s]", mediaName)); this, wxID_ANY, wxString::Format("[Media: %s]", mediaName));
mediaLabel->SetForegroundColour(*wxLIGHT_GREY); mediaLabel->SetForegroundColour(*wxLIGHT_GREY);
sizer->Add(mediaLabel, 0, wxALL | wxALIGN_CENTER, 10); sizer->Add(mediaLabel, 0, wxALL | wxALIGN_CENTER, 10);
} }
} }
// Helper lambda to convert plain text to HTML // Helper lambda to convert plain text to HTML
auto textToHtml = [](const wxString& plainText) -> wxString { auto textToHtml = [](const wxString &plainText) -> wxString {
wxString result = plainText; wxString result = plainText;
// Check if this looks like HTML (has tags) // Check if this looks like HTML (has tags)
if (plainText.Find("<") == wxNOT_FOUND) { if (plainText.Find("<") == wxNOT_FOUND) {
@@ -104,29 +109,34 @@ WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent, const wxString &tex
wxColour fgColor = GetForegroundColour(); wxColour fgColor = GetForegroundColour();
// Helper lambda to create and configure HTML window with auto-sizing // 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); auto *htmlCtrl = new wxHtmlWindow(this, wxID_ANY);
htmlCtrl->SetBorders(0); htmlCtrl->SetBorders(0);
wxString htmlContent = wxString::Format( const wxString htmlContent = wxString::Format(
"<html><body style=\"margin:0; padding:5px; background-color:#%02x%02x%02x; color:#%02x%02x%02x; font-family:sans-serif; font-size:11pt; line-height:1.5; user-select:none; -webkit-user-select:none; -moz-user-select:none;\">%s</body></html>", "<html><body style=\"margin:0; padding:5px; "
bgColor.Red(), bgColor.Green(), bgColor.Blue(), "background-color:#%02x%02x%02x; color:#%02x%02x%02x; "
fgColor.Red(), fgColor.Green(), fgColor.Blue(), "font-family:sans-serif; font-size:11pt; line-height:1.5; "
textToHtml(text) "user-select:none; -webkit-user-select:none; "
); "-moz-user-select:none;\">%s</body></html>",
bgColor.Red(), bgColor.Green(), bgColor.Blue(), fgColor.Red(),
fgColor.Green(), fgColor.Blue(), textToHtml(message));
htmlCtrl->SetPage(htmlContent); 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 // Set a reasonable fixed size - will expand based on content
// Width: fixed at 600px for consistent layout // Width: fixed at 600px for consistent layout
// Height: let wxHtmlWindow calculate based on content, with limits // 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) // Calculate approximate height based on line count (rough estimate)
int lineCount = 1; int lineCount = 1;
for (size_t i = 0; i < text.length(); i++) { for (size_t i = 0; i < message.length(); i++) {
if (text[i] == '\n') lineCount++; if (message[i] == '\n')
lineCount++;
} }
// Estimate: ~20px per line + 40px padding // Estimate: ~20px per line + 40px padding
@@ -182,17 +192,16 @@ WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent, const wxString &tex
wxSize minSize = sizer->GetMinSize(); wxSize minSize = sizer->GetMinSize();
// Set reasonable dialog size // Set reasonable dialog size
int dialogWidth = std::clamp(minSize.GetWidth() + 40, 500, 800); const int dialogWidth = std::clamp(minSize.GetWidth() + 40, 500, 800);
int dialogHeight = std::clamp(minSize.GetHeight() + 40, 300, 700); const int dialogHeight = std::clamp(minSize.GetHeight() + 40, 300, 700);
SetSize(dialogWidth, dialogHeight); SetSize(dialogWidth, dialogHeight);
SetMinSize(wxSize(400, 250)); wxTopLevelWindowBase::SetMinSize(wxSize(400, 250));
CenterOnParent(); CenterOnParent();
} }
void WherigoMessageDialog::onButton(wxCommandEvent &event) { void WherigoMessageDialog::onButton(const wxCommandEvent &event) {
int id = event.GetId(); if (const int id = event.GetId(); id == wxID_OK) {
if (id == wxID_OK) {
m_selectedButton = 0; m_selectedButton = 0;
} else { } else {
m_selectedButton = id - 1000; m_selectedButton = id - 1000;
@@ -200,12 +209,13 @@ void WherigoMessageDialog::onButton(wxCommandEvent &event) {
EndModal(wxID_OK); EndModal(wxID_OK);
} }
WherigoDialogRunner& WherigoDialogRunner::getInstance() { WherigoDialogRunner &WherigoDialogRunner::getInstance() {
static WherigoDialogRunner instance; static WherigoDialogRunner instance;
return instance; return instance;
} }
void WherigoDialogRunner::showMessageBox(const wxString &text, const wxString &title, void WherigoDialogRunner::showMessageBox(const wxString &text,
const wxString &title,
std::function<void(int)> callback) { std::function<void(int)> callback) {
WherigoMessageDialog dlg(nullptr, text, title); WherigoMessageDialog dlg(nullptr, text, title);
dlg.ShowModal(); dlg.ShowModal();
@@ -217,18 +227,17 @@ void WherigoDialogRunner::showMessageBox(const wxString &text, const wxString &t
void WherigoDialogRunner::showDialog(const std::vector<DialogEntry> &entries, void WherigoDialogRunner::showDialog(const std::vector<DialogEntry> &entries,
std::function<void(int)> callback) { std::function<void(int)> callback) {
for (size_t i = 0; i < entries.size(); i++) { for (size_t i = 0; i < entries.size(); ++i) {
const auto &entry = entries[i]; const auto &entry = entries[i];
std::vector<wxString> buttons; std::vector<wxString> buttons;
buttons.reserve(entry.buttons.size() + 1);
for (const auto &btn : entry.buttons) { 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) { if (buttons.empty()) {
buttons.push_back("Weiter"); buttons.emplace_back(i + 1 < entries.size() ? "Weiter" : "OK");
} else if (buttons.empty()) {
buttons.push_back("OK");
} }
WherigoMessageDialog dlg(nullptr, wxString::FromUTF8(entry.text), "Wherigo", WherigoMessageDialog dlg(nullptr, wxString::FromUTF8(entry.text), "Wherigo",
@@ -236,7 +245,7 @@ void WherigoDialogRunner::showDialog(const std::vector<DialogEntry> &entries,
dlg.ShowModal(); dlg.ShowModal();
// For the last entry, call the callback with the selected button // 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()); callback(dlg.getSelectedButton());
} }