diff --git a/.gitignore b/.gitignore index 5be4c69e..8f5b82c5 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,5 @@ CMakeUserPresets.json # End of https://www.toptal.com/developers/gitignore/api/cmake,clion+all cartridges/ +assets/icon.iconset/ +*.lua diff --git a/CMakeLists.txt b/CMakeLists.txt index fb00c03f..7c3f16ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,21 +27,25 @@ FetchContent_MakeAvailable(wxWidgets) message(STATUS "Configure project....") set(SRCS - main/src/main.cpp main/src/app.cpp - main/src/ui/start_screen.cpp + main/src/main.cpp main/src/ui/game_screen.cpp + main/src/ui/map_sim_frame.cpp + main/src/ui/start_screen.cpp main/src/ui/wherigo_dialog.cpp - main/src/lua/wherigo.cpp - main/src/lua/zobject.cpp - main/src/lua/ztimer.cpp main/src/lua/game_engine.cpp main/src/lua/media_manager.cpp main/src/lua/persistence.cpp + main/src/lua/wherigo.cpp main/src/lua/wherigo_completion.cpp + main/src/lua/zobject.cpp + main/src/lua/ztimer.cpp ) if (APPLE) + configure_file(${CMAKE_SOURCE_DIR}/assets/Info.plist.in + ${CMAKE_BINARY_DIR}/Info.plist @ONLY) + set(MACOSX_BUNDLE_ICON_FILE icon.icns) add_executable(${PROJECT_NAME} MACOSX_BUNDLE ${SRCS}) set_target_properties(${PROJECT_NAME} PROPERTIES BUNDLE True @@ -49,7 +53,8 @@ if (APPLE) MACOSX_BUNDLE_BUNDLE_NAME ${PROJECT_NAME} MACOSX_BUNDLE_BUNDLE_VERSION "0.1" MACOSX_BUNDLE_SHORT_VERSION_STRING "0.1" - #MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/cmake/customtemplate.plist.in + MACOSX_BUNDLE_ICON_FILE icon.icns + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_BINARY_DIR}/Info.plist INSTALL_RPATH "@executable_path" ) else () @@ -61,14 +66,14 @@ include_directories(main/include) target_link_libraries(${PROJECT_NAME} PRIVATE - wxcore - wxnet - wxbase - wxhtml + wx::net + wx::html + wx::webview lua cartridge storage ) +target_include_directories(${PROJECT_NAME} PRIVATE ${wxWidgets_INCLUDE_DIRS}) # Kopiere die .dylib-Dateien ins Bundle if (APPLE) @@ -78,5 +83,11 @@ if (APPLE) $ $ "$/Contents/MacOS/" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_SOURCE_DIR}/assets/icon.icns + "$/Contents/Resources/icon.icns" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_BINARY_DIR}/Info.plist + "$/Contents/Info.plist" ) endif () diff --git a/assets/Info.plist.in b/assets/Info.plist.in new file mode 100644 index 00000000..3bc8861a --- /dev/null +++ b/assets/Info.plist.in @@ -0,0 +1,22 @@ + + + + + CFBundleIconFile + icon.icns + CFBundleName + @CMAKE_PROJECT_NAME@ + CFBundleIdentifier + dev.mars3142.@CMAKE_PROJECT_NAME@ + CFBundleVersion + @PROJECT_VERSION@ + CFBundleShortVersionString + @PROJECT_VERSION@ + CFBundlePackageType + APPL + CFBundleSignature + ???? + LSMinimumSystemVersion + 10.13 + + diff --git a/assets/icon.icns b/assets/icon.icns new file mode 100644 index 00000000..7b9ff825 Binary files /dev/null and b/assets/icon.icns differ diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 00000000..53a45a93 --- /dev/null +++ b/assets/icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72b281b40c663e7fdb11e0096bfe0ad52c74bfdd1bf433092beb018f2b5e0894 +size 7440945 diff --git a/assets/make_icns.sh b/assets/make_icns.sh new file mode 100755 index 00000000..0ab59f20 --- /dev/null +++ b/assets/make_icns.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +ICONSET="icon.iconset" +PNG="$1" + +if [ -z "$PNG" ]; then + echo "Usage: $0 icon.png" + exit 1 +fi + +mkdir -p $ICONSET + +# Erzeuge alle Größen +sips -z 16 16 "$PNG" --out $ICONSET/icon_16x16.png +sips -z 32 32 "$PNG" --out $ICONSET/icon_16x16@2x.png +sips -z 32 32 "$PNG" --out $ICONSET/icon_32x32.png +sips -z 64 64 "$PNG" --out $ICONSET/icon_32x32@2x.png +sips -z 128 128 "$PNG" --out $ICONSET/icon_128x128.png +sips -z 256 256 "$PNG" --out $ICONSET/icon_128x128@2x.png +sips -z 256 256 "$PNG" --out $ICONSET/icon_256x256.png +sips -z 512 512 "$PNG" --out $ICONSET/icon_256x256@2x.png +sips -z 512 512 "$PNG" --out $ICONSET/icon_512x512.png +sips -z 1024 1024 "$PNG" --out $ICONSET/icon_512x512@2x.png + +iconutil -c icns icon.iconset diff --git a/components/cartridge/include/cartridge/parser.h b/components/cartridge/include/cartridge/parser.h index 382b5063..9ca77356 100644 --- a/components/cartridge/include/cartridge/parser.h +++ b/components/cartridge/include/cartridge/parser.h @@ -23,6 +23,4 @@ std::unique_ptr parseData(const std::vector& bytes); std::unique_ptr parseFile(const std::string& filePath); -std::unique_ptr parseCartridge(); - } // namespace cartridge diff --git a/components/cartridge/src/parser.cpp b/components/cartridge/src/parser.cpp index 6d870f54..725e3e50 100644 --- a/components/cartridge/src/parser.cpp +++ b/components/cartridge/src/parser.cpp @@ -57,45 +57,4 @@ std::unique_ptr parseFile(const std::string &filePath) { return parseData(*result); } -std::unique_ptr parseCartridge() { - auto cartridge = parseFile("/Volumes/Coding/git.mars3142.dev/mars3142/" - "wx_wherigo/cartridges/the_ombos_idol_-_c.gwc"); - if (!cartridge) { - std::println(std::cerr, "Cartridge konnte nicht geladen werden."); - return nullptr; - } - - const int count = cartridge->mediaCount(); - const std::filesystem::path outDir = - "/Volumes/Coding/git.mars3142.dev/mars3142/wx_wherigo/cartridges"; - storage::Storage storage; - - for (const int i : std::views::iota(0, count)) { - auto media = cartridge->getMedia(i); - if (!media) { - std::println(std::cerr, "Media-Objekt an Index {} ist nullptr.", i); - continue; - } - - const auto &data = media->getData(); - if (data.empty()) { - std::println(std::cerr, "Media-Daten an Index {} sind leer.", i); - continue; - } - - std::string ext = media->getObjectType(); - if (ext.empty()) { - std::println(std::cerr, "Media-Extension an Index {} ist leer.", i); - ext = "bin"; - } - - const std::filesystem::path outFile = outDir / std::format("file{}.{}", i, ext); - - if (auto writeResult = storage.writeFile(outFile.string(), data); !writeResult) { - std::println(std::cerr, "Fehler beim Schreiben der Datei: {}", outFile.string()); - } - } - return cartridge; -} - } // namespace cartridge diff --git a/main/include/app.h b/main/include/app.h index b0346ae0..a46d95f4 100644 --- a/main/include/app.h +++ b/main/include/app.h @@ -32,6 +32,7 @@ public: int getCartridgeRef() const { return m_cartridgeRef; } cartridge::Cartridge* getCartridge() const { return m_cartridge.get(); } bool isCartridgeLoaded() const { return m_cartridge != nullptr; } + std::string getCartridgePath() const { return m_cartridgePath; } private: bool initLuaState(); diff --git a/main/include/ui/game_screen.h b/main/include/ui/game_screen.h index bebd6a73..9bba1de6 100644 --- a/main/include/ui/game_screen.h +++ b/main/include/ui/game_screen.h @@ -3,6 +3,7 @@ #include #include #include +#include "ui/map_sim_frame.h" class cGameScreen : public wxFrame { @@ -24,6 +25,7 @@ private: void OnInventorySelected(wxCommandEvent& event); void OnCharacterSelected(wxCommandEvent& event); void OnItemSelected(wxCommandEvent& event); + void OnMapSim(wxCommandEvent& event); void populateZones(); void populateTasks(); diff --git a/main/include/ui/map_sim_frame.h b/main/include/ui/map_sim_frame.h new file mode 100644 index 00000000..f4a60af9 --- /dev/null +++ b/main/include/ui/map_sim_frame.h @@ -0,0 +1,25 @@ +#pragma once +#include +#include +#include +#include + +struct SimPoint { + double lat; + double lon; +}; + +class MapSimFrame : public wxFrame { +public: + MapSimFrame(wxWindow* parent, double centerLat = 53.3, double centerLon = 10.39, const std::vector>& zoneCoords = {}); + void AddSimPoint(double lat, double lon); + void StartSimulation(); +private: + wxWebView* m_webView; + std::vector m_route; + std::vector> m_zoneCoords; + void OnWebViewEvent(wxWebViewEvent& event); + void OnPlay(wxCommandEvent& event); + void SendPositionToEngine(double lat, double lon); + wxDECLARE_EVENT_TABLE(); +}; diff --git a/main/include/ui/start_screen.h b/main/include/ui/start_screen.h index 53d78cd3..b61fca04 100644 --- a/main/include/ui/start_screen.h +++ b/main/include/ui/start_screen.h @@ -3,6 +3,7 @@ #include #include #include +#include "ui/map_sim_frame.h" class cGameScreen; diff --git a/main/src/app.cpp b/main/src/app.cpp index a2a9fb7a..83dd6506 100644 --- a/main/src/app.cpp +++ b/main/src/app.cpp @@ -19,7 +19,6 @@ extern "C" { bool cApp::OnInit() { - // Initialize image handlers for JPEG, PNG, GIF, etc. wxInitAllImageHandlers(); diff --git a/main/src/lua/persistence.cpp b/main/src/lua/persistence.cpp index fa8ee854..0701d675 100644 --- a/main/src/lua/persistence.cpp +++ b/main/src/lua/persistence.cpp @@ -9,6 +9,7 @@ extern "C" { #include #include #include +#include namespace wherigo { @@ -82,10 +83,42 @@ void LuaPersistence::serializeValue(lua_State* L, int index, std::string& output case LUA_TSTRING: { const char* str = lua_tostring(L, index); output += "\""; - // Escape special characters + // Properly escape all special characters for Lua strings for (const char* p = str; *p; ++p) { - if (*p == '"' || *p == '\\') output += '\\'; - output += *p; + unsigned char c = (unsigned char)*p; + switch (c) { + case '"': + output += "\\\""; + break; + case '\\': + output += "\\\\"; + break; + case '\n': + output += "\\n"; + break; + case '\r': + output += "\\r"; + break; + case '\t': + output += "\\t"; + break; + case '\b': + output += "\\b"; + break; + case '\f': + output += "\\f"; + break; + default: + // For control characters and high bytes, use decimal escape + if (c < 32 || c >= 127) { + char hex[6]; + snprintf(hex, sizeof(hex), "\\%d", c); + output += hex; + } else { + output += c; + } + break; + } } output += "\""; break; @@ -199,23 +232,120 @@ bool LuaPersistence::loadGlobals(lua_State* L, const std::string& filePath) { return false; } - // Restore globals + // Restore globals - but for tables (Wherigo objects), merge instead of replace + // Strategy: For each saved object, find the matching existing object and merge int count = 0; lua_pushnil(L); while (lua_next(L, -2) != 0) { - if (lua_isstring(L, -2)) { - const char* key = lua_tostring(L, -2); + if (lua_isstring(L, -2) && lua_istable(L, -1)) { + const char* savedKey = lua_tostring(L, -2); + wxLogDebug("Restoring global: %s", savedKey); + int stackBefore = lua_gettop(L); - // Set the global - lua_pushvalue(L, -1); // duplicate value - lua_setglobal(L, key); + // Get the Name field of the saved object (if it exists) + lua_getfield(L, -1, "Name"); + std::string savedName; + if (lua_isstring(L, -1)) { + savedName = lua_tostring(L, -1); + } + lua_pop(L, 1); + + // Try to find matching existing object + bool merged = false; + + if (!savedName.empty()) { + // Search for existing object with same Name + lua_getglobal(L, "_G"); + lua_pushnil(L); + + while (lua_next(L, -2) != 0) { + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + if (savedName == lua_tostring(L, -1)) { + lua_pop(L, 1); // pop Name + // Found matching object! Merge data + lua_pushnil(L); + while (lua_next(L, -3) != 0) { + if (lua_isstring(L, -2)) { + const char* field = lua_tostring(L, -2); + if (!lua_isfunction(L, -1)) { + lua_pushvalue(L, -1); + lua_setfield(L, -4, field); + } + } + lua_pop(L, 1); + } + merged = true; + lua_pop(L, 2); // pop iterator and object + break; + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); + } + lua_pop(L, 1); // pop _G + } + + if (!merged) { + // No match found or no Name field - use key matching + lua_getglobal(L, savedKey); + if (lua_istable(L, -1)) { + // Object exists by key - merge it + lua_pushnil(L); + while (lua_next(L, -3) != 0) { + if (lua_isstring(L, -2)) { + const char* field = lua_tostring(L, -2); + if (!lua_isfunction(L, -1)) { + lua_getfield(L, -3, field); // get existing value + if (lua_istable(L, -1) && lua_istable(L, -2)) { + // Both are tables: merge recursively + lua_pushnil(L); + while (lua_next(L, -3) != 0) { + if (lua_isstring(L, -2)) { + const char* subfield = lua_tostring(L, -2); + if (!lua_isfunction(L, -1)) { + lua_pushvalue(L, -1); + lua_setfield(L, -5, subfield); // merge into existing table + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); // pop existing value + } else { + lua_pop(L, 1); // pop existing value + lua_pushvalue(L, -1); + lua_setfield(L, -3, field); + } + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); // pop existing object + merged = true; + } else { + lua_pop(L, 1); // pop nil + } + } + if (!merged) { + // No existing object found - create new global + lua_pushvalue(L, -1); + lua_setglobal(L, savedKey); + } + int stackAfter = lua_gettop(L); + if (stackBefore != stackAfter) { + wxLogError("Stack imbalance detected for global %s: before=%d after=%d", savedKey, stackBefore, stackAfter); + } count++; } - lua_pop(L, 1); // pop value + lua_pop(L, 1); // pop value from lua_next } lua_pop(L, 1); // pop table + lua_pop(L, 1); // pop table + wxLogDebug("Restored %d globals from %s", count, filePath); return true; } diff --git a/main/src/lua/wherigo.cpp b/main/src/lua/wherigo.cpp index cbef1aad..652d7c18 100644 --- a/main/src/lua/wherigo.cpp +++ b/main/src/lua/wherigo.cpp @@ -1,14 +1,19 @@ #include "lua/wherigo.h" +#include "app.h" +#include "lua/game_engine.h" #include "lua/zobject.h" #include "lua/ztimer.h" #include "ui/wherigo_dialog.h" extern "C" { -#include #include +#include } +#include +#include #include +#include #include #include @@ -17,6 +22,63 @@ extern "C" { namespace wherigo { +std::string getCompletionCode(lua_State *L) { + lua_getglobal(L, "Player"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return ""; + } + + lua_getfield(L, -1, "CompletionCode"); + std::string completionCode; + if (lua_isstring(L, -1)) { + completionCode = lua_tostring(L, -1); + if (completionCode.length() > 15) { + completionCode = completionCode.substr(0, 15); + } + } + lua_pop(L, 2); + + return completionCode; +} + +// Wherigo.RequestSync() +static int wherigo_RequestSync(lua_State *L) { + // RequestSync shows the completion code from the cartridge in a MessageBox + wxLogDebug( + "Wherigo.RequestSync() called - showing completion code from cartridge"); + + auto completionCode = getCompletionCode(L); + if (completionCode.empty()) { + wxApp *app = wxTheApp; + if (!app) { + wxLogWarning("Cannot sync: no app instance"); + return 0; + } + + cApp *wherigApp = dynamic_cast(app); + if (!wherigApp || !wherigApp->isCartridgeLoaded()) { + wxLogWarning("Cannot sync: no cartridge loaded"); + return 0; + } + + auto cartridge = wherigApp->getCartridge(); + + completionCode = cartridge->completionCode().substr(0, 15); + } + + wxString message = + wxString::Format("Completion Code:\n\n%s\n\nEnter this code on " + "wherigo.com to verify your completion.", + completionCode.c_str()); + + std::vector buttons = {"OK"}; + wherigo::WherigoMessageDialog dlg(nullptr, message, "Wherigo Completion", + buttons); + dlg.ShowModal(); + + return 0; +} // Wherigo.ZonePoint(lat, lng, alt) static int wherigo_ZonePoint(lua_State *L) { @@ -131,7 +193,6 @@ static int wherigo_ZTask(lua_State *L) { return 1; } - // Wherigo.ZTimer(cartridge) static int wherigo_ZTimer(lua_State *L) { lua_newtable(L); @@ -190,9 +251,7 @@ static int wherigo_ZMedia(lua_State *L) { return 1; } -void resetMediaCounter() { - s_mediaCounter = 0; -} +void resetMediaCounter() { s_mediaCounter = 0; } // Wherigo.ZCartridge() static int wherigo_ZCartridge(lua_State *L) { @@ -204,6 +263,9 @@ static int wherigo_ZCartridge(lua_State *L) { lua_newtable(L); lua_setfield(L, -2, "AllZObjects"); + lua_pushcfunction(L, wherigo_RequestSync); + lua_setfield(L, -2, "RequestSync"); + return 1; } @@ -238,7 +300,8 @@ static int wherigo_Player(lua_State *L) { // Wherigo.MessageBox(table) static int wherigo_MessageBox(lua_State *L) { - if (!lua_istable(L, 1)) return 0; + if (!lua_istable(L, 1)) + return 0; // Get Text lua_getfield(L, 1, "Text"); @@ -273,7 +336,8 @@ static int wherigo_MessageBox(lua_State *L) { wxLogDebug("MessageBox: %s", text); // Show dialog - WherigoMessageDialog dlg(nullptr, wxString::FromUTF8(text), "Wherigo", buttons); + WherigoMessageDialog dlg(nullptr, wxString::FromUTF8(text), "Wherigo", + buttons); dlg.ShowModal(); int selected = dlg.getSelectedButton(); @@ -294,12 +358,17 @@ static int wherigo_MessageBox(lua_State *L) { luaL_unref(L, LUA_REGISTRYINDEX, callbackRef); } + // Notify game state change after MessageBox + // This ensures UI updates if state was modified during the MessageBox + GameEngine::getInstance().notifyStateChanged(); + return 0; } // Wherigo.Dialog(table) static int wherigo_Dialog(lua_State *L) { - if (!lua_istable(L, 1)) return 0; + if (!lua_istable(L, 1)) + return 0; std::vector entries; int n = lua_objlen(L, 1); @@ -341,7 +410,7 @@ static int wherigo_Dialog(lua_State *L) { } wxLogDebug("Dialog with %zu entries", entries.size()); - for (const auto& entry : entries) { + for (const auto &entry : entries) { wxLogDebug(" Text: %s, Media: %s", entry.text.c_str(), entry.mediaName.empty() ? "(none)" : entry.mediaName.c_str()); } @@ -378,6 +447,45 @@ static int wherigo_PlayAudio(lua_State *L) { static int wherigo_ShowScreen(lua_State *L) { int screen = luaL_optinteger(L, 1, 0); wxLogDebug("ShowScreen: %d", screen); + + // Show details dialog for DETAILSCREEN + if (screen == 5 && lua_gettop(L) >= 2 && lua_istable(L, 2)) { + // Get Name + lua_getfield(L, 2, "Name"); + std::string name = lua_isstring(L, -1) ? lua_tostring(L, -1) : "Details"; + lua_pop(L, 1); + // Get Description + lua_getfield(L, 2, "Description"); + std::string desc = lua_isstring(L, -1) ? lua_tostring(L, -1) : ""; + lua_pop(L, 1); + // Get Media name (prefer Media, fallback to Icon) + std::string mediaName; + lua_getfield(L, 2, "Media"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + mediaName = lua_tostring(L, -1); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + if (mediaName.empty()) { + lua_getfield(L, 2, "Icon"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + mediaName = lua_tostring(L, -1); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + } + // Show dialog with media if available + std::vector buttons = {"OK"}; + wherigo::WherigoMessageDialog dlg(nullptr, wxString::FromUTF8(desc.c_str()), wxString::FromUTF8(name.c_str()), buttons, wxString::FromUTF8(mediaName.c_str())); + dlg.ShowModal(); + return 0; + } return 0; } @@ -443,30 +551,30 @@ static int wherigo_VectorToSegment(lua_State *L) { } static const luaL_Reg wherigo_funcs[] = { - {"ZonePoint", wherigo_ZonePoint}, - {"Distance", wherigo_Distance}, - {"Zone", wherigo_Zone}, - {"ZCharacter", wherigo_ZCharacter}, - {"ZItem", wherigo_ZItem}, - {"ZTask", wherigo_ZTask}, - {"ZTimer", wherigo_ZTimer}, - {"ZInput", wherigo_ZInput}, - {"ZMedia", wherigo_ZMedia}, - {"ZCartridge", wherigo_ZCartridge}, - {"ZCommand", wherigo_ZCommand}, - {"MessageBox", wherigo_MessageBox}, - {"Dialog", wherigo_Dialog}, - {"GetInput", wherigo_GetInput}, - {"PlayAudio", wherigo_PlayAudio}, - {"ShowScreen", wherigo_ShowScreen}, - {"LogMessage", wherigo_LogMessage}, - {"NoCaseEquals", wherigo_NoCaseEquals}, - {"TranslatePoint", wherigo_TranslatePoint}, - {"VectorToZone", wherigo_VectorToZone}, - {"VectorToPoint", wherigo_VectorToPoint}, - {"VectorToSegment", wherigo_VectorToSegment}, - {nullptr, nullptr} -}; + {"ZonePoint", wherigo_ZonePoint}, + {"Distance", wherigo_Distance}, + {"Zone", wherigo_Zone}, + {"ZCharacter", wherigo_ZCharacter}, + {"ZItem", wherigo_ZItem}, + {"ZTask", wherigo_ZTask}, + {"ZTimer", wherigo_ZTimer}, + {"ZInput", wherigo_ZInput}, + {"ZMedia", wherigo_ZMedia}, + {"ZCartridge", wherigo_ZCartridge}, + {"ZCommand", wherigo_ZCommand}, + {"MessageBox", wherigo_MessageBox}, + {"Dialog", wherigo_Dialog}, + {"GetInput", wherigo_GetInput}, + {"PlayAudio", wherigo_PlayAudio}, + {"ShowScreen", wherigo_ShowScreen}, + {"LogMessage", wherigo_LogMessage}, + {"NoCaseEquals", wherigo_NoCaseEquals}, + {"TranslatePoint", wherigo_TranslatePoint}, + {"VectorToZone", wherigo_VectorToZone}, + {"VectorToPoint", wherigo_VectorToPoint}, + {"VectorToSegment", wherigo_VectorToSegment}, + {"RequestSync", wherigo_RequestSync}, + {nullptr, nullptr}}; int luaopen_Wherigo(lua_State *L) { luaL_register(L, "Wherigo", wherigo_funcs); @@ -509,4 +617,3 @@ int luaopen_Wherigo(lua_State *L) { } } // namespace wherigo - diff --git a/main/src/lua/wherigo_completion.cpp b/main/src/lua/wherigo_completion.cpp index 02a540eb..2a3bcac6 100644 --- a/main/src/lua/wherigo_completion.cpp +++ b/main/src/lua/wherigo_completion.cpp @@ -262,7 +262,6 @@ bool WherigoCompletion::writeGWLFile(const CompletionData& data, const std::stri file << "\n"; file.close(); - wxLogMessage("Completion log saved to: %s", filePath); return true; } diff --git a/main/src/ui/game_screen.cpp b/main/src/ui/game_screen.cpp index 6666498d..aae0942e 100644 --- a/main/src/ui/game_screen.cpp +++ b/main/src/ui/game_screen.cpp @@ -2,6 +2,7 @@ #include "lua/game_engine.h" #include "ui/game_screen.h" #include "ui/wherigo_dialog.h" +#include extern "C" { #include @@ -12,17 +13,20 @@ wxDECLARE_APP(cApp); enum { ID_SaveGame = 2000, ID_LoadGame = 2001, - ID_ExportCompletion = 2002 + ID_ExportCompletion = 2002, + ID_MapView = 2003 }; cGameScreen::cGameScreen(wxWindow *parent) - : wxFrame(parent, wxID_ANY, "Wherigo Player", wxDefaultPosition, wxSize(800, 600)) { + : wxFrame(parent, wxID_ANY, "Wherigo Player", wxDefaultPosition, wxSize(800, 800)) { // Menu auto *menuFile = new wxMenu; menuFile->Append(ID_SaveGame, "Spielstand speichern\tCtrl+S"); menuFile->Append(ID_LoadGame, "Spielstand laden\tCtrl+L"); menuFile->AppendSeparator(); + menuFile->Append(ID_MapView, "Karten-Simulation..."); // Menüpunkt für Simulation + menuFile->AppendSeparator(); menuFile->Append(ID_ExportCompletion, "Completion Log exportieren\tCtrl+E"); menuFile->AppendSeparator(); menuFile->Append(wxID_EXIT, "Spiel beenden"); @@ -103,6 +107,7 @@ cGameScreen::cGameScreen(wxWindow *parent) Bind(wxEVT_MENU, &cGameScreen::OnSaveGame, this, ID_SaveGame); Bind(wxEVT_MENU, &cGameScreen::OnLoadGame, this, ID_LoadGame); Bind(wxEVT_MENU, &cGameScreen::OnExportCompletion, this, ID_ExportCompletion); + Bind(wxEVT_MENU, &cGameScreen::OnMapSim, this, ID_MapView); m_zoneList->Bind(wxEVT_LISTBOX_DCLICK, &cGameScreen::OnZoneSelected, this); m_taskList->Bind(wxEVT_LISTBOX_DCLICK, &cGameScreen::OnTaskSelected, this); @@ -542,12 +547,11 @@ void cGameScreen::OnZoneSelected(wxCommandEvent& event) { if (currentName == zoneName) { found = true; - // Get zone description + // Zone Info mit Media anzeigen lua_getfield(L, -1, "Description"); wxString description = lua_isstring(L, -1) ? wxString::FromUTF8(lua_tostring(L, -1)) : ""; lua_pop(L, 1); - // Get media if exists wxString mediaName; lua_getfield(L, -1, "Media"); if (lua_istable(L, -1)) { @@ -559,7 +563,6 @@ void cGameScreen::OnZoneSelected(wxCommandEvent& event) { } lua_pop(L, 1); - // Show zone info with media if (!description.IsEmpty() || !mediaName.IsEmpty()) { std::vector buttons = {"OK"}; wherigo::WherigoMessageDialog dlg(this, description, zoneName, buttons, mediaName); @@ -589,9 +592,9 @@ void cGameScreen::OnZoneSelected(wxCommandEvent& event) { lua_pop(L, 1); } } - lua_pop(L, 1); // pop value + lua_pop(L, 1); } - lua_pop(L, 1); // pop _G + lua_pop(L, 1); if (!found) { wxMessageBox(wxString::Format("Zone '%s' nicht gefunden", zoneName), @@ -624,12 +627,11 @@ void cGameScreen::OnTaskSelected(wxCommandEvent& event) { lua_pop(L, 1); if (currentName == taskName) { - // Get task description + // Bestehende MessageBox mit Status und Beschreibung lua_getfield(L, -1, "Description"); wxString description = lua_isstring(L, -1) ? wxString::FromUTF8(lua_tostring(L, -1)) : "Keine Beschreibung verfügbar"; lua_pop(L, 1); - // Get task complete status lua_getfield(L, -1, "Complete"); bool complete = lua_toboolean(L, -1); lua_pop(L, 1); @@ -680,54 +682,6 @@ void cGameScreen::OnInventorySelected(wxCommandEvent& event) { 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"); - lua_pop(L, 1); - - // Get media if exists - wxString mediaName; - lua_getfield(L, -1, "Media"); - if (lua_istable(L, -1)) { - lua_getfield(L, -1, "Name"); - if (lua_isstring(L, -1)) { - mediaName = wxString::FromUTF8(lua_tostring(L, -1)); - } - lua_pop(L, 1); - } - lua_pop(L, 1); - - // Check for commands - lua_getfield(L, -1, "Commands"); - wxArrayString commands; - if (lua_istable(L, -1)) { - int n = lua_objlen(L, -1); - for (int i = 1; i <= n; i++) { - lua_rawgeti(L, -1, i); - if (lua_istable(L, -1)) { - lua_getfield(L, -1, "Text"); - if (lua_isstring(L, -1)) { - commands.Add(wxString::FromUTF8(lua_tostring(L, -1))); - } - lua_pop(L, 1); - } - lua_pop(L, 1); - } - } - lua_pop(L, 1); - - wxString message = description; - if (!commands.IsEmpty()) { - message += ("\n\nVerfügbare Aktionen:\n"); - for (const auto& cmd : commands) { - message += "• " + cmd + "\n"; - } - } - - // Use WherigoMessageDialog to show with media support - std::vector buttons = {"OK"}; - wherigo::WherigoMessageDialog dlg(this, message, itemName, buttons, mediaName); - dlg.ShowModal(); // Call OnClick if exists lua_getfield(L, -1, "OnClick"); @@ -937,3 +891,53 @@ void cGameScreen::OnItemSelected(wxCommandEvent& event) { } lua_pop(L, 1); // pop _G } + +void cGameScreen::OnMapSim(wxCommandEvent&) { + std::vector> zoneCoords; + double lat = 53.3, lon = 10.39; + lua_State *L = wxGetApp().getLuaState(); + if (L) { + lua_getglobal(L, "_G"); + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + 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_getfield(L, -1, "Active"); + bool active = lua_toboolean(L, -1) || true; + lua_pop(L, 1); + lua_getfield(L, -1, "Visible"); + bool visible = lua_toboolean(L, -1) || true; + 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); + } + lua_pop(L, 1); // pop OriginalPoint + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); + } + if (!zoneCoords.empty()) { + lat = zoneCoords[0].first; + lon = zoneCoords[0].second; + } + auto *mapSim = new MapSimFrame(this, lat, lon, zoneCoords); + mapSim->Show(); +} diff --git a/main/src/ui/map_sim_frame.cpp b/main/src/ui/map_sim_frame.cpp new file mode 100644 index 00000000..bf73ebb0 --- /dev/null +++ b/main/src/ui/map_sim_frame.cpp @@ -0,0 +1,79 @@ +#include "ui/map_sim_frame.h" +#include +#include +#include +#include +#include +#include + +wxBEGIN_EVENT_TABLE(MapSimFrame, wxFrame) + EVT_WEBVIEW_NAVIGATED(wxID_ANY, MapSimFrame::OnWebViewEvent) + EVT_BUTTON(wxID_ANY, MapSimFrame::OnPlay) +wxEND_EVENT_TABLE() + +MapSimFrame::MapSimFrame(wxWindow* parent, double centerLat, double centerLon, const std::vector>& zoneCoords) + : wxFrame(parent, wxID_ANY, "Karten-Simulation", wxDefaultPosition, wxSize(900, 700)), m_zoneCoords(zoneCoords) { + auto* sizer = new wxBoxSizer(wxVERTICAL); + m_webView = wxWebView::New(this, wxID_ANY); + sizer->Add(m_webView, 1, wxEXPAND); + auto* playBtn = new wxButton(this, wxID_ANY, "Simulation starten"); + sizer->Add(playBtn, 0, wxALL | wxALIGN_CENTER, 8); + SetSizer(sizer); + // Leaflet-Karte laden (dynamisch zentriert und Marker) + wxString html; + html << "MapSim" + "" + "" + "" + "
" + ""; + m_webView->SetPage(html, ""); +} + +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 +} + +void MapSimFrame::AddSimPoint(double lat, double 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::SendPositionToEngine(double lat, double lon) { + // TODO: Engine-Integration: GPS-Position setzen + // Beispiel: wxGetApp().setSimulatedPosition(lat, lon); +} diff --git a/main/src/ui/start_screen.cpp b/main/src/ui/start_screen.cpp index 07fdb750..072de90d 100644 --- a/main/src/ui/start_screen.cpp +++ b/main/src/ui/start_screen.cpp @@ -15,7 +15,7 @@ enum { }; cStartScreen::cStartScreen() - : wxFrame(nullptr, wxID_ANY, "Wherigo Player", wxDefaultPosition, wxSize(800, 600)), + : wxFrame(nullptr, wxID_ANY, "Wherigo Player", wxDefaultPosition, wxSize(800, 800)), m_gameFrame(nullptr), m_cartridgeLoaded(false) { @@ -107,6 +107,20 @@ void cStartScreen::OnOpenCartridge(wxCommandEvent& event) { wxString filePath = openFileDialog.GetPath(); 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); + } + + // ggf. alten GameScreen schließen + if (m_gameFrame) { + m_gameFrame->Destroy(); + m_gameFrame = nullptr; + } + // Load the cartridge via cApp if (wxGetApp().loadCartridge(filePath.ToStdString())) { m_cartridgeLoaded = true; diff --git a/main/src/ui/wherigo_dialog.cpp b/main/src/ui/wherigo_dialog.cpp index d8f3b7da..2d787cb6 100644 --- a/main/src/ui/wherigo_dialog.cpp +++ b/main/src/ui/wherigo_dialog.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace wherigo { @@ -17,7 +18,7 @@ WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent, const wxString &tex const std::vector &buttons, const wxString &mediaName) : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, - wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER ) { auto *sizer = new wxBoxSizer(wxVERTICAL); @@ -68,37 +69,124 @@ WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent, const wxString &tex } } + // Helper lambda to convert plain text to HTML + auto textToHtml = [](const wxString& plainText) -> wxString { + wxString result = plainText; + // Check if this looks like HTML (has tags) + if (plainText.Find("<") == wxNOT_FOUND) { + // Plain text - need to be careful with & to avoid breaking HTML entities + // Replace & but preserve HTML entities like   & etc. + wxString escaped; + for (size_t i = 0; i < plainText.length(); i++) { + if (plainText[i] == '&') { + // Look ahead to see if this is an HTML entity + size_t end = plainText.find(';', i); + if (end != wxString::npos && end - i < 12) { + // Likely an entity, copy as-is + escaped += plainText.substr(i, end - i + 1); + i = end; + } else { + // Not an entity, escape the & + escaped += "&"; + } + } else if (plainText[i] == '\n') { + escaped += "
"; + } else { + escaped += plainText[i]; + } + } + result = escaped; + } + return result; + }; + + wxColour bgColor = GetBackgroundColour(); + 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 *htmlCtrl = new wxHtmlWindow(this, wxID_ANY); + htmlCtrl->SetBorders(0); + + wxString htmlContent = wxString::Format( + "%s", + bgColor.Red(), bgColor.Green(), bgColor.Blue(), + fgColor.Red(), fgColor.Green(), fgColor.Blue(), + textToHtml(text) + ); + + htmlCtrl->SetPage(htmlContent); + // 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; + + // 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++; + } + + // Estimate: ~20px per line + 40px padding + int estimatedHeight = std::max(80, lineCount * 20 + 40); + estimatedHeight = std::clamp(estimatedHeight, 80, 400); + + htmlCtrl->SetMinSize(wxSize(contentWidth, estimatedHeight)); + + sizer->Add(htmlCtrl, 1, wxALL | wxEXPAND, 10); + }; + // Text (only show if not just a placeholder like ".") if (!text.IsEmpty() && text != ".") { - auto *textCtrl = new wxStaticText(this, wxID_ANY, text); - textCtrl->Wrap(800); // Also doubled wrap width - sizer->Add(textCtrl, 1, wxALL | wxEXPAND, 15); + createHtmlWindow(text); } else if (mediaName.IsEmpty()) { // No media and empty/placeholder text - show something - auto *textCtrl = new wxStaticText(this, wxID_ANY, text); - textCtrl->Wrap(800); // Also doubled wrap width - sizer->Add(textCtrl, 1, wxALL | wxEXPAND, 15); + createHtmlWindow(text); } // Buttons auto *buttonSizer = new wxBoxSizer(wxHORIZONTAL); + wxButton *defaultButton = nullptr; if (buttons.empty()) { auto *okButton = new wxButton(this, wxID_OK, "OK"); buttonSizer->Add(okButton, 0, wxALL, 5); okButton->Bind(wxEVT_BUTTON, &WherigoMessageDialog::onButton, this); + defaultButton = okButton; } else { for (size_t i = 0; i < buttons.size(); i++) { auto *btn = new wxButton(this, 1000 + i, buttons[i]); buttonSizer->Add(btn, 0, wxALL, 5); btn->Bind(wxEVT_BUTTON, &WherigoMessageDialog::onButton, this); + + // Set first button as default + if (i == 0) { + defaultButton = btn; + } } } + // Set the default button (will be activated by Enter) + if (defaultButton) { + defaultButton->SetDefault(); + } + sizer->Add(buttonSizer, 0, wxALIGN_CENTER | wxBOTTOM, 10); - SetSizerAndFit(sizer); - SetMinSize(wxSize(600, 300)); // Doubled from 300x150 + SetSizer(sizer); + sizer->Layout(); + + // Calculate dialog size based on content + 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); + + SetSize(dialogWidth, dialogHeight); + SetMinSize(wxSize(400, 250)); CenterOnParent(); } @@ -151,11 +239,11 @@ void WherigoDialogRunner::showDialog(const std::vector &entries, if (i == entries.size() - 1 && callback) { callback(dlg.getSelectedButton()); } - } - // Notify game state change after dialog sequence completes - GameEngine::getInstance().notifyStateChanged(); + // Notify state change after each dialog to ensure UI updates + // This is important for goto events triggered during dialogs + GameEngine::getInstance().notifyStateChanged(); + } } } // namespace wherigo -