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/
assets/icon.iconset/
*.lua
*.local.json
+4 -4
View File
@@ -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; }
+3
View File
@@ -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();
};
+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
+34 -7
View File
@@ -1,25 +1,52 @@
#pragma once
#include <wx/frame.h>
#include <wx/timer.h>
#include <wx/webview.h>
#include <vector>
#include <string>
#include <utility>
#include <vector>
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<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 StartSimulation();
private:
wxWebView* m_webView;
std::vector<SimPoint> m_route;
std::vector<std::pair<double, double>> m_zoneCoords;
void OnWebViewEvent(wxWebViewEvent& event);
void OnPlay(wxCommandEvent& event);
wxWebView* m_webView = nullptr;
wxTimer m_simTimer;
size_t m_simIndex = 0;
std::vector<SimPoint> m_route;
std::vector<ZoneInfo> 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();
};
+1 -1
View File
@@ -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;
};
+42 -14
View File
@@ -6,6 +6,11 @@
#include "lua/wherigo_completion.h"
#include "ui/start_screen.h"
#ifdef __APPLE__
#include <objc/message.h>
#include <objc/objc.h>
#endif
#include <cartridge/parser.h>
#include <wx/dir.h>
#include <wx/filename.h>
@@ -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<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;
}
@@ -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<const char *>(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);
}
+241 -62
View File
@@ -1,15 +1,18 @@
#include "lua/game_engine.h"
#include "lua/geo_utils.h"
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lua.h>
}
#include <wx/log.h>
#include <string>
#include <vector>
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<int> 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
+2 -2
View File
@@ -69,7 +69,7 @@ void MediaManager::buildMediaIndex(lua_State *L) {
}
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()) {
wxLogWarning("Media not found: %s", name.c_str());
return {};
@@ -84,7 +84,7 @@ std::vector<uint8_t> 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 {};
+718 -503
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);
// Notify game state change
GameEngine::getInstance().rebuildPlayerInventory();
GameEngine::getInstance().notifyStateChanged();
return 0;
+100 -89
View File
@@ -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<wxString> buttons = {"OK"};
const std::vector<wxString> 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<std::pair<double, double>> zoneCoords;
std::vector<ZoneInfo> 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();
}
+362 -62
View File
@@ -1,79 +1,379 @@
#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/webview.h>
#include <wx/log.h>
#include <wx/msgdlg.h>
#include <thread>
#include <chrono>
#include <wx/sizer.h>
#include <wx/webview.h>
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<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);
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 << "<!DOCTYPE html><html><head><meta charset='utf-8'><title>MapSim</title>"
"<link rel='stylesheet' href='https://unpkg.com/leaflet/dist/leaflet.css'/>"
"<script src='https://unpkg.com/leaflet/dist/leaflet.js'></script>"
"<style>html,body,#map{height:100%%;margin:0;padding:0;}#map{height:600px;}</style>"
"</head><body><div id='map'></div>"
"<script>"
"var map = L.map('map').setView([" << wxString::Format("%.8f, %.8f", centerLat, centerLon) << "], 16);"
"L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {maxZoom: 19}).addTo(map);"
"var markers = [];";
// Marker für alle Zonen
for (const auto& z : zoneCoords) {
html << wxString::Format("var m = L.marker([%.8f, %.8f]).addTo(map); markers.push(m);\n", z.first, z.second);
}
html <<
"map.on('click', function(e) {"
" var marker = L.marker(e.latlng).addTo(map);"
" markers.push(marker);"
" window.wx.postMessage(JSON.stringify({lat: e.latlng.lat, lon: e.latlng.lng}));"
"});"
"</script></body></html>";
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<char>(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<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);
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 << "<!DOCTYPE html><html><head>"
"<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.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>"
<< wxString::Format("var map=L.map('map').setView([%.8f,%.8f],16);",
centerLat, centerLon)
<< "L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',"
" {maxZoom:19,attribution:'&copy; OpenStreetMap'}).addTo(map);"
"var routeMarkers=[];"
"var routeLine=null;"
// Zone cluster group with custom cluster icon
"var zoneCluster=L.markerClusterGroup({"
" maxClusterRadius:60,"
" iconCreateFunction:function(c){"
" 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>";
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<int>(key.length());
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) {
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);
}
+48 -40
View File
@@ -4,20 +4,17 @@
#include "ui/game_screen.h"
#include <wx/filedlg.h>
#include <wx/mstream.h>
#include <wx/image.h>
#include <wx/mstream.h>
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<double>(maxWidth) / image.GetWidth();
double scaleY = static_cast<double>(maxHeight) / image.GetHeight();
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));
}
@@ -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);
}
+62 -53
View File
@@ -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 <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/statbmp.h>
#include <wx/button.h>
#include <wx/html/htmlwin.h>
#include <wx/image.h>
#include <wx/log.h>
#include <wx/mstream.h>
#include <wx/image.h>
#include <wx/html/htmlwin.h>
#include <wx/sizer.h>
#include <wx/statbmp.h>
#include <wx/stattext.h>
namespace wherigo {
WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent, const wxString &text,
WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent,
const wxString &text,
const wxString &title,
const std::vector<wxString> &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<int>(800 * contentScaleFactor);
int maxHeight = static_cast<int>(600 * contentScaleFactor);
const int maxWidth = static_cast<int>(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<int>(600 * contentScaleFactor);
image.GetWidth() > maxWidth || image.GetHeight() > maxHeight) {
const double scaleX =
static_cast<double>(maxWidth) / image.GetWidth();
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);
@@ -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(
"<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>",
bgColor.Red(), bgColor.Green(), bgColor.Blue(),
fgColor.Red(), fgColor.Green(), fgColor.Blue(),
textToHtml(text)
);
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>",
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<void(int)> callback) {
void WherigoDialogRunner::showMessageBox(const wxString &text,
const wxString &title,
std::function<void(int)> 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<DialogEntry> &entries,
std::function<void(int)> callback) {
for (size_t i = 0; i < entries.size(); i++) {
std::function<void(int)> callback) {
for (size_t i = 0; i < entries.size(); ++i) {
const auto &entry = entries[i];
std::vector<wxString> 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());
}