From c3b247c4204664f50c3c6297960996aa2fa4d8ab Mon Sep 17 00:00:00 2001 From: Peter Siegmund Date: Fri, 13 Feb 2026 02:41:12 +0100 Subject: [PATCH] starting playing the wherigo Signed-off-by: Peter Siegmund --- CMakeLists.txt | 14 +- License.md => LICENSE.md | 0 LUA_PERSISTENCE.md | 164 +++ Readme.md => README.md | 0 WHERIGO_COMPLETION.md | 254 +++++ cart.lua | 1346 +++++++++++++++++++++++++ components/lua-5.1.4/CMakeLists.txt | 2 + components/lua-5.1.4/src/lundump.c | 153 ++- main/include/app.h | 43 + main/include/cApp.h | 9 - main/include/lua/game_engine.h | 73 ++ main/include/lua/media_manager.h | 43 + main/include/lua/persistence.h | 45 + main/include/lua/wherigo.h | 16 + main/include/lua/wherigo_completion.h | 66 ++ main/include/lua/zobject.h | 16 + main/include/lua/ztimer.h | 17 + main/include/ui/cFrame.h | 14 - main/include/ui/game_screen.h | 40 + main/include/ui/start_screen.h | 33 + main/include/ui/wherigo_dialog.h | 50 + main/src/app.cpp | 275 +++++ main/src/cApp.cpp | 11 - main/src/lua/game_engine.cpp | 215 ++++ main/src/lua/media_manager.cpp | 97 ++ main/src/lua/persistence.cpp | 228 +++++ main/src/lua/wherigo.cpp | 512 ++++++++++ main/src/lua/wherigo_completion.cpp | 281 ++++++ main/src/lua/zobject.cpp | 64 ++ main/src/lua/ztimer.cpp | 62 ++ main/src/main.cpp | 2 +- main/src/ui/cFrame.cpp | 38 - main/src/ui/game_screen.cpp | 939 +++++++++++++++++ main/src/ui/start_screen.cpp | 202 ++++ main/src/ui/wherigo_dialog.cpp | 161 +++ 35 files changed, 5401 insertions(+), 84 deletions(-) rename License.md => LICENSE.md (100%) create mode 100644 LUA_PERSISTENCE.md rename Readme.md => README.md (100%) create mode 100644 WHERIGO_COMPLETION.md create mode 100644 cart.lua create mode 100644 main/include/app.h delete mode 100644 main/include/cApp.h create mode 100644 main/include/lua/game_engine.h create mode 100644 main/include/lua/media_manager.h create mode 100644 main/include/lua/persistence.h create mode 100644 main/include/lua/wherigo.h create mode 100644 main/include/lua/wherigo_completion.h create mode 100644 main/include/lua/zobject.h create mode 100644 main/include/lua/ztimer.h delete mode 100644 main/include/ui/cFrame.h create mode 100644 main/include/ui/game_screen.h create mode 100644 main/include/ui/start_screen.h create mode 100644 main/include/ui/wherigo_dialog.h create mode 100644 main/src/app.cpp delete mode 100644 main/src/cApp.cpp create mode 100644 main/src/lua/game_engine.cpp create mode 100644 main/src/lua/media_manager.cpp create mode 100644 main/src/lua/persistence.cpp create mode 100644 main/src/lua/wherigo.cpp create mode 100644 main/src/lua/wherigo_completion.cpp create mode 100644 main/src/lua/zobject.cpp create mode 100644 main/src/lua/ztimer.cpp delete mode 100644 main/src/ui/cFrame.cpp create mode 100644 main/src/ui/game_screen.cpp create mode 100644 main/src/ui/start_screen.cpp create mode 100644 main/src/ui/wherigo_dialog.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1df5847..fb00c03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,8 +28,17 @@ message(STATUS "Configure project....") set(SRCS main/src/main.cpp - main/src/cApp.cpp - main/src/ui/cFrame.cpp + main/src/app.cpp + main/src/ui/start_screen.cpp + main/src/ui/game_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_completion.cpp ) if (APPLE) @@ -55,6 +64,7 @@ target_link_libraries(${PROJECT_NAME} wxcore wxnet wxbase + wxhtml lua cartridge storage diff --git a/License.md b/LICENSE.md similarity index 100% rename from License.md rename to LICENSE.md diff --git a/LUA_PERSISTENCE.md b/LUA_PERSISTENCE.md new file mode 100644 index 0000000..6ce5150 --- /dev/null +++ b/LUA_PERSISTENCE.md @@ -0,0 +1,164 @@ +# Lua State Persistence / Spielstand-Speicherung + +## Übersicht + +Der Wherigo Player kann den Lua-State speichern und wiederherstellen, um Spielfortschritte zu sichern. + +## Funktionsweise + +### Automatisches Speichern + +Der Spielstand wird automatisch gespeichert unter: +- **macOS**: `~/Library/Application Support/wxWherigo/.save` +- **Windows**: `%APPDATA%\wxWherigo\.save` +- **Linux**: `~/.wxWherigo/.save` + +### Was wird gespeichert? + +Die Persistierung speichert: +- ✅ **Alle Wherigo-Objekte**: ZCartridge, Zone, ZItem, ZCharacter, ZTask, ZTimer, ZInput, ZMedia +- ✅ **Objekt-Properties**: Name, Description, Active, Visible, Container, etc. +- ✅ **Globale Variablen**: Alle einfachen Variablen (Strings, Numbers, Booleans) +- ✅ **Player-Objekt**: Position, Inventar, etc. +- ✅ **Verschachtelte Tabellen**: Rekursive Serialisierung (bis Tiefe 10) + +### Was wird NICHT gespeichert? + +- ❌ **Funktionen**: Lua-Funktionen können nicht serialisiert werden +- ❌ **Userdata**: C-Objekte bleiben im Originalzustand +- ❌ **Threads/Coroutines**: Werden als `nil` gespeichert +- ❌ **Metatables**: Werden nicht persistent gespeichert + +## Verwendung im Code + +### Spielstand speichern + +```cpp +// Automatischer Pfad (empfohlen) +wxGetApp().saveGameState(""); + +// Eigener Pfad +wxGetApp().saveGameState("/path/to/save.lua"); +``` + +### Spielstand laden + +```cpp +// Automatischer Pfad +wxGetApp().loadGameState(""); + +// Eigener Pfad +wxGetApp().loadGameState("/path/to/save.lua"); +``` + +### Im Spiel + +- **Menü → Spielstand speichern** (Strg+S) +- **Menü → Spielstand laden** (Strg+L) + +## Save-Datei-Format + +Die Save-Datei ist eine lesbare Lua-Datei: + +```lua +-- Wherigo Save State +return { + ["Player"] = { + ["ClassName"] = "Player", + ["Name"] = "Player", + ["ObjectLocation"] = { + ["latitude"] = 51.5074, + ["longitude"] = -0.1278, + ["altitude"] = 0 + } + }, + ["zitemLetter"] = { + ["ClassName"] = "ZItem", + ["Name"] = "Letter from Professor", + ["Active"] = true, + ["Visible"] = true, + ["Container"] = + }, + -- ... weitere Objekte +} +``` + +## Technische Details + +### Serialisierung + +Die Implementierung verwendet: +1. **Rekursives Traversieren** der Lua-Tabellen +2. **Type-Detection** für verschiedene Lua-Typen +3. **Escaping** von Sonderzeichen in Strings +4. **Referenz-Handling** durch Lua selbst beim Laden + +### Deserialisierung + +1. Save-Datei wird als Lua-Code geladen (`luaL_loadstring`) +2. Ausführung gibt eine Tabelle zurück (`lua_pcall`) +3. Tabellen-Einträge werden als Globals wiederhergestellt +4. UI wird über `notifyStateChanged()` aktualisiert + +## Performance + +- **Speichern**: ~10-100ms für typische Cartridges +- **Laden**: ~20-200ms (inkl. Lua-Parsing) +- **Dateigröße**: ~10-500KB (abhängig von Cartridge-Komplexität) + +## Erweiterungen + +### Eigene Filter + +```cpp +// Nur bestimmte Globals speichern +std::vector myGlobals = {"Player", "zitemKey", "ztaskMain"}; +wherigo::LuaPersistence::saveGlobals(L, "custom.save", myGlobals); +``` + +### Auto-Save beim Beenden + +```cpp +// In cGame::OnClose() +wxGetApp().saveGameState(""); // Auto-Save +``` + +### Mehrere Save-Slots + +```cpp +std::string slot1 = wxGetApp().getAutoSavePath() + ".slot1"; +std::string slot2 = wxGetApp().getAutoSavePath() + ".slot2"; +std::string slot3 = wxGetApp().getAutoSavePath() + ".slot3"; +``` + +## Bekannte Limitierungen + +1. **Funktions-Callbacks** werden nicht gespeichert (OnClick, OnEnter, etc.) + - Diese bleiben im Cartridge-Code definiert + +2. **Zirkuläre Referenzen** werden bei der Rekursion abgebrochen + - Max. Tiefe: 10 Ebenen + +3. **Metatables** gehen verloren + - Nach dem Laden müssen ggf. Metatables neu gesetzt werden + +4. **Timer-Status** wird gespeichert, aber `Remaining` muss ggf. neu berechnet werden + +## Debugging + +Aktiviere Logging um Save/Load zu tracken: + +```cpp +wxLogDebug("Saved %zu globals to %s", globals.size(), filePath); +wxLogDebug("Restored %d globals from %s", count, filePath); +``` + +## Zukünftige Erweiterungen + +Mögliche Verbesserungen: +- [ ] Kompression der Save-Dateien (zlib) +- [ ] Verschlüsselung (verhindert Cheating) +- [ ] Delta-Saves (nur Änderungen speichern) +- [ ] Cloud-Sync Integration +- [ ] Automatisches Backup (3 neueste Saves) + diff --git a/Readme.md b/README.md similarity index 100% rename from Readme.md rename to README.md diff --git a/WHERIGO_COMPLETION.md b/WHERIGO_COMPLETION.md new file mode 100644 index 0000000..f5dae2d --- /dev/null +++ b/WHERIGO_COMPLETION.md @@ -0,0 +1,254 @@ +# Wherigo Completion Log (.gwl) + +## Übersicht + +Der Wherigo Player kann jetzt **Completion Logs** im `.gwl`-Format (Wherigo Game Log) generieren, die auf **wherigo.com** hochgeladen werden können, um den Abschluss einer Cartridge nachzuweisen. + +## Format + +Das `.gwl`-Format ist ein XML-Format, das folgende Informationen enthält: + +### XML-Struktur + +```xml + + + + The Ombos Idol + a1b2c3d4-e5f6-7890-abcd-ef1234567890 + + + + Player + + + + 2026-02-13T14:30:00 + 2026-02-13T16:45:00 + 8100 + + + Complete + + + + Find the Professor + true + 2026-02-13T15:20:00 + + + Solve the Puzzle + true + 2026-02-13T16:30:00 + + + + + + Key + 2026-02-13T15:10:00 + + + Map + 2026-02-13T15:45:00 + + + + + + Town Square + 2026-02-13T14:35:00 + + + Old Library + 2026-02-13T15:15:00 + + + + A1B2C3D4E5F67890 + +``` + +## Verwendung + +### Im Spiel + +1. Cartridge spielen und abschließen +2. **Menü → Completion Log exportieren** (Strg+E) +3. Datei speichern (z.B. `TheOmbosIdol_completion.gwl`) +4. Auf [wherigo.com](https://www.wherigo.com) hochladen + +### Im Code + +```cpp +// Completion Log generieren +wxGetApp().generateCompletionLog(""); // Auto-Path + +// Mit eigenem Pfad +wxGetApp().generateCompletionLog("/path/to/completion.gwl"); + +// Pfad abrufen +std::string path = wxGetApp().getCompletionLogPath(); +``` + +## Was wird erfasst? + +### ✅ Cartridge-Informationen +- Name der Cartridge +- GUID (eindeutige ID) + +### ✅ Spieler-Informationen +- Spielername (Standard: "Player") + +### ✅ Session-Daten +- Startzeit (aktuell: Zeitpunkt der Generierung) +- Endzeit (aktuell: Zeitpunkt der Generierung) +- Gesamtdauer in Sekunden + +### ✅ Completion Status +- `Complete` - Alle Tasks abgeschlossen +- `Incomplete` - Noch offene Tasks + +### ✅ Aufgaben (Tasks) +- Name der Aufgabe +- Status (completed: true/false) +- Abschlusszeit (falls completed) + +### ✅ Gesammelte Items +- Name des Items +- Zeitpunkt des Einsammelns +- **Nur Items im Spieler-Inventar** + +### ✅ Besuchte Zonen +- Name der Zone +- Zeitpunkt des Besuchs +- **Nur aktive Zonen** + +### ✅ Completion Code +- Eindeutiger Hash/Signatur-Code +- Basiert auf: Cartridge-GUID + Spielername + Abschlusszeit +- Format: 16-stelliger Hex-Code (z.B. `A1B2C3D4E5F67890`) + +## Technische Details + +### Dateipfad + +Standardpfad für Completion Logs: +- **Im gleichen Verzeichnis wie die GWC-Datei** +- Format: `/.gwl` + +Beispiel: +- GWC-Datei: `/Users/name/Downloads/TheOmbosIdol.gwc` +- Completion Log: `/Users/name/Downloads/TheOmbosIdol.gwl` + +Dies ermöglicht es, die Completion-Logs zusammen mit den Cartridge-Dateien zu organisieren. + +### XML-Encoding +- UTF-8 +- Sonderzeichen werden escaped: `&`, `<`, `>`, `"`, `'` + +### Zeitformat +- ISO 8601: `YYYY-MM-DDTHH:MM:SS` +- Beispiel: `2026-02-13T16:45:00` +- Lokale Zeitzone + +### Completion Code Generierung + +```cpp +// Einfacher Hash-Algorithmus (für Produktion: SHA256 verwenden) +hash = hash_function(cartridgeGUID + playerName + endTime) +completionCode = sprintf("%016lX", hash) +``` + +## Limitierungen + +### ⚠️ Session-Zeiten +Die aktuellen Start-/Endzeiten sind der **Zeitpunkt der Generierung**, nicht die tatsächliche Spielzeit. Um echte Session-Tracking zu implementieren: + +```cpp +// In cApp beim Start speichern: +m_sessionStartTime = time(nullptr); + +// Bei Completion verwenden: +data.startTime = m_sessionStartTime; +data.endTime = time(nullptr); +data.duration = data.endTime - data.startTime; +``` + +### ⚠️ Completion Code +Der aktuelle Hash-Algorithmus ist simpel. Für echte Verifikation sollte **SHA256** oder ein ähnlicher kryptografischer Hash verwendet werden. + +### ⚠️ Wherigo.com Upload +Das generierte Format entspricht dem Wherigo-Standard, aber die tatsächliche Akzeptanz auf wherigo.com hängt von deren Server-seitiger Validierung ab. + +## Erweiterungen + +### Spielername anpassen +```cpp +// In cApp/cGame speichern: +std::string m_playerName = "Player"; + +// Beim Generieren verwenden: +wherigo::WherigoCompletion::generateCompletionLog(L, path, m_playerName); +``` + +### Session-Tracking +```cpp +class cApp { + time_t m_sessionStartTime = 0; + + void startGame() { + m_sessionStartTime = time(nullptr); + // ... + } +}; +``` + +### Zusätzliche Statistiken +Das `CompletionData`-Struct kann erweitert werden um: +- Anzahl der Dialoge +- Zurückgelegte Distanz +- Verwendete Hints +- Fehlversuche + +### Auto-Export bei Abschluss +```cpp +// In cGame::OnTaskCompleted() oder bei Complete-Event +if (allTasksCompleted()) { + wxGetApp().generateCompletionLog(""); + wxMessageBox("Gratulation! Completion Log wurde automatisch erstellt."); +} +``` + +## Beispiel-Workflow + +1. **Spieler spielt Cartridge** +2. **Alle Tasks werden abgeschlossen** +3. **Spieler exportiert Log:** Menü → Completion Log exportieren +4. **Datei wird gespeichert:** `TheOmbosIdol_completion.gwl` +5. **Upload auf wherigo.com:** + - Login auf wherigo.com + - Zur Cartridge-Seite navigieren + - "Log completed cartridge" Button + - `.gwl`-Datei hochladen + - Completion wird verifiziert und angezeigt + +## Debugging + +Log-Ausgaben aktivieren: +```cpp +wxLogMessage("Completion log generated: %s", filePath); +wxLogDebug("Tasks completed: %d/%d", completedCount, totalCount); +``` + +## Zukünftige Features + +- [ ] Automatisches Tracking der Session-Zeit +- [ ] Spielername-Eingabe-Dialog +- [ ] Screenshot/Foto-Anhänge +- [ ] GPS-Koordinaten für besuchte Zonen +- [ ] Statistiken (Distanz, Zeit pro Zone, etc.) +- [ ] Automatischer Export bei Completion +- [ ] Cloud-Upload direkt aus der App +- [ ] Kryptografischer Completion Code (SHA256) + diff --git a/cart.lua b/cart.lua new file mode 100644 index 0000000..8bcdfaf --- /dev/null +++ b/cart.lua @@ -0,0 +1,1346 @@ +require("Wherigo") +ZonePoint = Wherigo.ZonePoint +Distance = Wherigo.Distance +Player = Wherigo.Player +cartTheOmbosIdol01 = Wherigo.ZCartridge() +cartTheOmbosIdol01.MsgBoxCBFuncs = {} +zmediaLetterfromProfessor = Wherigo.ZMedia(cartTheOmbosIdol01) +zmediaLetterfromProfessor.Name = "Letter from Professor" +zmediaLetterfromProfessor.Description = "" +zmediaLetterfromProfessor.AltText = "" +zmediaLetterfromProfessor.Id = "3916a052-7cdd-4729-8dd4-cee63f63af5d" +zmediaLetterfromProfessor.Resources = { + { + Type = "jpg", + Filename = "Letter.jpg", + Directives = {} + } +} +zmediaTitlePlate1 = Wherigo.ZMedia(cartTheOmbosIdol01) +zmediaTitlePlate1.Name = "Title Plate 1" +zmediaTitlePlate1.Description = "" +zmediaTitlePlate1.AltText = "" +zmediaTitlePlate1.Id = "8c0a181b-c145-4c2a-ae6f-2377a6fb1e33" +zmediaTitlePlate1.Resources = { + { + Type = "jpg", + Filename = "Title Plate 1.jpg", + Directives = {} + } +} +zmediaTitlePlate2 = Wherigo.ZMedia(cartTheOmbosIdol01) +zmediaTitlePlate2.Name = "Title Plate 2" +zmediaTitlePlate2.Description = "" +zmediaTitlePlate2.AltText = "" +zmediaTitlePlate2.Id = "b0c05936-3028-4f35-9fee-eb582fee25cd" +zmediaTitlePlate2.Resources = { + { + Type = "jpg", + Filename = "Title Plate 2.jpg", + Directives = {} + } +} +zmediaTitlePlate3 = Wherigo.ZMedia(cartTheOmbosIdol01) +zmediaTitlePlate3.Name = "Title Plate 3" +zmediaTitlePlate3.Description = "" +zmediaTitlePlate3.AltText = "" +zmediaTitlePlate3.Id = "45d5e731-7f06-4d14-8e5e-d7d48fba27f9" +zmediaTitlePlate3.Resources = { + { + Type = "jpg", + Filename = "Title Plate 3.jpg", + Directives = {} + } +} +zmediaTitlePlate4 = Wherigo.ZMedia(cartTheOmbosIdol01) +zmediaTitlePlate4.Name = "Title Plate 4" +zmediaTitlePlate4.Description = "" +zmediaTitlePlate4.AltText = "" +zmediaTitlePlate4.Id = "00505e4b-efbb-4e2d-8cc7-44303579df1e" +zmediaTitlePlate4.Resources = { + { + Type = "jpg", + Filename = "Title Plate 4.jpg", + Directives = {} + } +} +zmediaTitlePlate5 = Wherigo.ZMedia(cartTheOmbosIdol01) +zmediaTitlePlate5.Name = "Title Plate 5" +zmediaTitlePlate5.Description = "" +zmediaTitlePlate5.AltText = "" +zmediaTitlePlate5.Id = "a7da5782-e3c0-40e1-ad6d-aee402ca664e" +zmediaTitlePlate5.Resources = { + { + Type = "jpg", + Filename = "Title Plate 5.jpg", + Directives = {} + } +} +zmediaTitlePlate6 = Wherigo.ZMedia(cartTheOmbosIdol01) +zmediaTitlePlate6.Name = "Title Plate 6" +zmediaTitlePlate6.Description = "" +zmediaTitlePlate6.AltText = "" +zmediaTitlePlate6.Id = "eb8cbf3c-f415-43c7-8388-5a1b17104169" +zmediaTitlePlate6.Resources = { + { + Type = "jpg", + Filename = "Title Plate 6.jpg", + Directives = {} + } +} +zmediaTitlePlate7 = Wherigo.ZMedia(cartTheOmbosIdol01) +zmediaTitlePlate7.Name = "Title Plate 7" +zmediaTitlePlate7.Description = "" +zmediaTitlePlate7.AltText = "" +zmediaTitlePlate7.Id = "b83778ab-ff6c-4479-a762-bf02c718347c" +zmediaTitlePlate7.Resources = { + { + Type = "jpg", + Filename = "Title Plate 7.jpg", + Directives = {} + } +} +zmediaTrainStation = Wherigo.ZMedia(cartTheOmbosIdol01) +zmediaTrainStation.Name = "Train Station" +zmediaTrainStation.Description = "" +zmediaTrainStation.AltText = "" +zmediaTrainStation.Id = "00e102c3-0b58-42b6-9f21-3491419dd1aa" +zmediaTrainStation.Resources = { + { + Type = "jpg", + Filename = "Train Station.jpg", + Directives = {} + } +} +zmediaPersonIcon = Wherigo.ZMedia(cartTheOmbosIdol01) +zmediaPersonIcon.Name = "Person Icon" +zmediaPersonIcon.Description = "" +zmediaPersonIcon.AltText = "" +zmediaPersonIcon.Id = "fcb5197e-ed6a-42b4-9b9b-171d17c5c5e7" +zmediaPersonIcon.Resources = { + { + Type = "jpg", + Filename = "icon_person.jpg", + Directives = {} + } +} +zmediaIconUsableObject = Wherigo.ZMedia(cartTheOmbosIdol01) +zmediaIconUsableObject.Name = "Icon Usable Object" +zmediaIconUsableObject.Description = "" +zmediaIconUsableObject.AltText = "" +zmediaIconUsableObject.Id = "81f03d42-303d-4187-8b86-7b4797edef87" +zmediaIconUsableObject.Resources = { + { + Type = "jpg", + Filename = "icon_object_usable.jpg", + Directives = {} + } +} +zmediaIconLocation = Wherigo.ZMedia(cartTheOmbosIdol01) +zmediaIconLocation.Name = "Icon Location" +zmediaIconLocation.Description = "" +zmediaIconLocation.AltText = "" +zmediaIconLocation.Id = "db8ca520-bb28-44be-97bb-78a1d171b3ae" +zmediaIconLocation.Resources = { + { + Type = "jpg", + Filename = "icon_location.jpg", + Directives = {} + } +} +zmediaIconExaminableObject = Wherigo.ZMedia(cartTheOmbosIdol01) +zmediaIconExaminableObject.Name = "Icon Examinable Object" +zmediaIconExaminableObject.Description = "" +zmediaIconExaminableObject.AltText = "" +zmediaIconExaminableObject.Id = "6473f8ac-69e6-4ea7-b11e-bbf6e8f356c3" +zmediaIconExaminableObject.Resources = { + { + Type = "jpg", + Filename = "icon_object_examinable.jpg", + Directives = {} + } +} +zmediaTelegram = Wherigo.ZMedia(cartTheOmbosIdol01) +zmediaTelegram.Name = "Telegram" +zmediaTelegram.Description = "" +zmediaTelegram.AltText = "" +zmediaTelegram.Id = "97307f1f-d95f-4601-833b-1b1aa8f1755c" +zmediaTelegram.Resources = { + { + Type = "jpg", + Filename = "Telegram.jpg", + Directives = {} + } +} +zmediaBookPage1 = Wherigo.ZMedia(cartTheOmbosIdol01) +zmediaBookPage1.Name = "Book Page 1" +zmediaBookPage1.Description = "" +zmediaBookPage1.AltText = "" +zmediaBookPage1.Id = "667b4324-6c22-4761-b416-3c81d1f58d54" +zmediaBookPage1.Resources = { + { + Type = "jpg", + Filename = "Briefing Paper.jpg", + Directives = {} + } +} +zmediaBookPage2 = Wherigo.ZMedia(cartTheOmbosIdol01) +zmediaBookPage2.Name = "Book Page 2" +zmediaBookPage2.Description = "" +zmediaBookPage2.AltText = "" +zmediaBookPage2.Id = "7563ff09-bdf8-4ede-91fb-bc8c6e2d6928" +zmediaBookPage2.Resources = { + { + Type = "jpg", + Filename = "Code.jpg", + Directives = {} + } +} +zmediaTobecontinued = Wherigo.ZMedia(cartTheOmbosIdol01) +zmediaTobecontinued.Name = "To be continued" +zmediaTobecontinued.Description = "" +zmediaTobecontinued.AltText = "" +zmediaTobecontinued.Id = "c4c7b420-bae7-42bf-a68e-ce3e1aea6d54" +zmediaTobecontinued.Resources = { + { + Type = "jpg", + Filename = "To Be Continued.jpg", + Directives = {} + } +} +zmediaBarrysNote = Wherigo.ZMedia(cartTheOmbosIdol01) +zmediaBarrysNote.Name = "BarrysNote" +zmediaBarrysNote.Description = "" +zmediaBarrysNote.AltText = "" +zmediaBarrysNote.Id = "6678e4a7-f10c-4f1f-b9af-bb6321816556" +zmediaBarrysNote.Resources = { + { + Type = "jpg", + Filename = "Note from Mysterious Barry.jpg", + Directives = {} + } +} +cartTheOmbosIdol01.Id = "0c1e0006-effa-4bb6-b901-092c7caee852" +cartTheOmbosIdol01.Name = "The Ombos Idol - Chapter One" +cartTheOmbosIdol01.Description = [[ +Jack Robinson arrives in Manchester to take up an exciting new post as research assistant to the eminent ancient historian Professor Ignatius Crimp. His exciting new life in a brand new city quickly sours, however, with the arrival of the an Egyptian Idol - with unexpected and murderous consequences.
+
+A multi-cartridge interactive fiction, set in 1930s Manchester - with each stage featuring at least one Geocache. Can you complete them all, and solve the mystery of the Ombos Idol?
+
+This instalment features a plotline geocache (GC1EMGM) and a bonus subplot cache (GC1EMGK).
+
+Update 1.3: Telegram puzzle replaced with easier version.]] +cartTheOmbosIdol01.Visible = true +cartTheOmbosIdol01.Activity = "Fiction" +cartTheOmbosIdol01.StartingLocationDescription = "Begins at Manchester Picadilly Station." +cartTheOmbosIdol01.StartingLocation = ZonePoint(53.4779, -2.2316, 0) +cartTheOmbosIdol01.Version = "1.3" +cartTheOmbosIdol01.Company = "" +cartTheOmbosIdol01.Author = "Mark Peace" +cartTheOmbosIdol01.BuilderVersion = "2.0.5129.5086" +cartTheOmbosIdol01.CreateDate = "7/27/2008 6:14:46 PM" +cartTheOmbosIdol01.PublishDate = "7/30/2008 2:22:39 PM" +cartTheOmbosIdol01.UpdateDate = "7/30/2008 12:23:36 PM" +cartTheOmbosIdol01.LastPlayedDate = "1/1/0001 12:00:00 AM" +cartTheOmbosIdol01.TargetDevice = "PocketPC" +cartTheOmbosIdol01.TargetDeviceVersion = "0" +cartTheOmbosIdol01.StateId = "1" +cartTheOmbosIdol01.CountryId = "2" +cartTheOmbosIdol01.Complete = false +cartTheOmbosIdol01.UseLogging = true +cartTheOmbosIdol01.Icon = zmediaIconExaminableObject +zoneLondonRoadStation = Wherigo.Zone(cartTheOmbosIdol01) +zoneLondonRoadStation.Id = "69b14a77-f9a8-41d6-a3c7-4216a70f1fc2" +zoneLondonRoadStation.Name = "London Road Station" +zoneLondonRoadStation.Description = "A marvel of Victorian engineering, this station is Manchester's hub for travellers in and out of the city. You find the hubbub of human traffic, and the anonymity of the crowd, vaguely comforting." +zoneLondonRoadStation.Visible = true +zoneLondonRoadStation.DistanceRange = Distance(1500, "feet") +zoneLondonRoadStation.ShowObjects = "OnEnter" +zoneLondonRoadStation.ProximityRange = Distance(200, "feet") +zoneLondonRoadStation.AllowSetPositionTo = false +zoneLondonRoadStation.Active = true +zoneLondonRoadStation.Points = { + ZonePoint(53.4781067718781, -2.23155498504639, 0), + ZonePoint(53.477934375146, -2.2321343421936, 0), + ZonePoint(53.4770883439934, -2.23162472248077, 0), + ZonePoint(53.4777029154692, -2.23079323768616, 0) +} +zoneLondonRoadStation.OriginalPoint = ZonePoint(53.479301, -2.22843, 0) +zoneLondonRoadStation.DistanceRangeUOM = "Feet" +zoneLondonRoadStation.ProximityRangeUOM = "Feet" +zoneLondonRoadStation.OutOfRangeName = "" +zoneLondonRoadStation.InRangeName = "" +zoneLondonRoadStation.Icon = zmediaIconLocation +zoneVagrantsQuandarySpot1 = Wherigo.Zone(cartTheOmbosIdol01) +zoneVagrantsQuandarySpot1.Id = "b14da035-21f3-411e-b0ab-4b9c60e7ce4d" +zoneVagrantsQuandarySpot1.Name = "Vagrants Quandary - Spot 1" +zoneVagrantsQuandarySpot1.Description = "Eric 'The Enigma' Jones would like you to move his belongings out of this area." +zoneVagrantsQuandarySpot1.Visible = false +zoneVagrantsQuandarySpot1.DistanceRange = Distance(1500, "feet") +zoneVagrantsQuandarySpot1.ShowObjects = "OnEnter" +zoneVagrantsQuandarySpot1.ProximityRange = Distance(1, "feet") +zoneVagrantsQuandarySpot1.AllowSetPositionTo = false +zoneVagrantsQuandarySpot1.Active = false +zoneVagrantsQuandarySpot1.Points = { + ZonePoint(53.478028554929, -2.23189294338226, 0), + ZonePoint(53.4779056422893, -2.2323140501976, 0), + ZonePoint(53.4782552240582, -2.23251521587372, 0), + ZonePoint(53.4784148494788, -2.23203778266907, 0) +} +zoneVagrantsQuandarySpot1.OriginalPoint = ZonePoint(53.479301, -2.22843, 0) +zoneVagrantsQuandarySpot1.DistanceRangeUOM = "Feet" +zoneVagrantsQuandarySpot1.ProximityRangeUOM = "Feet" +zoneVagrantsQuandarySpot1.OutOfRangeName = "" +zoneVagrantsQuandarySpot1.InRangeName = "" +zoneVagrantsQuandarySpot1.Icon = zmediaIconLocation +zoneVagrantsQuandarySpot2 = Wherigo.Zone(cartTheOmbosIdol01) +zoneVagrantsQuandarySpot2.Id = "2b20a01c-0964-40a0-80f8-4ccb54a1de4c" +zoneVagrantsQuandarySpot2.Name = "Vagrants Quandary - Spot 2" +zoneVagrantsQuandarySpot2.Description = "Eric 'The Enigma' Jones would like you to move his belongings in to this area." +zoneVagrantsQuandarySpot2.Visible = false +zoneVagrantsQuandarySpot2.DistanceRange = Distance(1500, "feet") +zoneVagrantsQuandarySpot2.ShowObjects = "OnEnter" +zoneVagrantsQuandarySpot2.ProximityRange = Distance(1, "feet") +zoneVagrantsQuandarySpot2.AllowSetPositionTo = false +zoneVagrantsQuandarySpot2.Active = false +zoneVagrantsQuandarySpot2.Points = { + ZonePoint(53.47837494318, -2.23266541957855, 0), + ZonePoint(53.4779966296023, -2.23239451646805, 0), + ZonePoint(53.4780093997359, -2.23313212394714, 0), + ZonePoint(53.4783446143677, -2.23320454359055, 0) +} +zoneVagrantsQuandarySpot2.OriginalPoint = ZonePoint(53.479301, -2.22843, 0) +zoneVagrantsQuandarySpot2.DistanceRangeUOM = "Feet" +zoneVagrantsQuandarySpot2.ProximityRangeUOM = "Feet" +zoneVagrantsQuandarySpot2.OutOfRangeName = "" +zoneVagrantsQuandarySpot2.InRangeName = "" +zoneVagrantsQuandarySpot2.Icon = zmediaIconLocation +zoneHotelRoyale = Wherigo.Zone(cartTheOmbosIdol01) +zoneHotelRoyale.Id = "dcd1e4e5-a6bb-4051-ad34-0c73fcb2e01b" +zoneHotelRoyale.Name = "Hotel Royale" +zoneHotelRoyale.Description = "The hotel's name is evocative of grandeur and luxury, of a smartly dressed concierge greeting you with a humble 'sir!'. It suggests glamorous guests sipping fashionable cocktails whilst discussing politics and art. The hotel's reality is one of lazy dilapidation, suggesting that it hasn't seen a lick of paint in at least ten years. The threadbare carpet leads up to a messy reception area." +zoneHotelRoyale.Visible = false +zoneHotelRoyale.DistanceRange = Distance(1500, "feet") +zoneHotelRoyale.ShowObjects = "OnEnter" +zoneHotelRoyale.ProximityRange = Distance(200, "feet") +zoneHotelRoyale.AllowSetPositionTo = false +zoneHotelRoyale.Active = false +zoneHotelRoyale.Points = { + ZonePoint(53.4787245210824, -2.23338961601257, 0), + ZonePoint(53.4792991634304, -2.23366856575012, 0), + ZonePoint(53.4794524000754, -2.23299264907837, 0), + ZonePoint(53.4789416091063, -2.232745885849, 0) +} +zoneHotelRoyale.OriginalPoint = ZonePoint(53.479301, -2.22843, 0) +zoneHotelRoyale.DistanceRangeUOM = "Feet" +zoneHotelRoyale.ProximityRangeUOM = "Feet" +zoneHotelRoyale.OutOfRangeName = "" +zoneHotelRoyale.InRangeName = "" +zoneHotelRoyale.Icon = zmediaIconLocation +zoneCanalPort = Wherigo.Zone(cartTheOmbosIdol01) +zoneCanalPort.Id = "bc5c7fe9-50b4-4802-ac54-a8e031c31a81" +zoneCanalPort.Name = "Canal Port" +zoneCanalPort.Description = "This dock on the Bridgewater canal serves as an important staging post for goods brought to and from the Ship Canal - Manchesters trade artery to the outside world. At the moment, the area is in chaotic disarray, with crates scattered across the footpaths, and an palpable underlying sense of panic." +zoneCanalPort.Visible = false +zoneCanalPort.DistanceRange = Distance(1500, "feet") +zoneCanalPort.ShowObjects = "OnEnter" +zoneCanalPort.ProximityRange = Distance(200, "feet") +zoneCanalPort.AllowSetPositionTo = false +zoneCanalPort.Active = false +zoneCanalPort.Points = { + ZonePoint(53.4818019593313, -2.23006367683411, 0), + ZonePoint(53.4822297183466, -2.22947359085083, 0), + ZonePoint(53.4818530352613, -2.22886204719543, 0), + ZonePoint(53.4814125033425, -2.229323387146, 0) +} +zoneCanalPort.OriginalPoint = ZonePoint(53.479301, -2.22843, 0) +zoneCanalPort.DistanceRangeUOM = "Feet" +zoneCanalPort.ProximityRangeUOM = "Feet" +zoneCanalPort.OutOfRangeName = "" +zoneCanalPort.InRangeName = "" +zoneCanalPort.Icon = zmediaIconLocation +zcharacterEricTheEngimaJones = Wherigo.ZCharacter({Cartridge = cartTheOmbosIdol01, Container = zoneLondonRoadStation}) +zcharacterEricTheEngimaJones.Id = "60a15927-abcb-42cf-acb9-3528548302ab" +zcharacterEricTheEngimaJones.Name = "Eric 'The Engima' Jones" +zcharacterEricTheEngimaJones.Description = "Many adverbs could appropriately applied to the local vagrant: malodorous, pestiferous, misanthropic - to name but three. Never one to miss an opportunity to eschew the obvious, Eric has opted for 'the enigma' - priding himself its implications of brooding mystique. He is currently picking at his toenails through a hole in a pair of boots which, in fact, are more hole than leather. He appears deep in troubled thought." +zcharacterEricTheEngimaJones.Visible = true +zcharacterEricTheEngimaJones.Icon = zmediaPersonIcon +zcharacterEricTheEngimaJones.Gender = "Male" +zcharacterEricTheEngimaJones.Type = "NPC" +zcharacterEricTheEngimaJones.ObjectLocation = ZonePoint(53.4779370674621, -2.23207922189368, 360) +zcharacterEricTheEngimaJones.Commands = { + TalkTo = Wherigo.ZCommand({ + Text = "Talk To", + CmdWith = false, + Enabled = true, + EmptyTargetListText = "Nothing available" + }) +} +zcharacterEricTheEngimaJones.Commands.TalkTo.Custom = true +zcharacterEricTheEngimaJones.Commands.TalkTo.Id = "cee18503-1ac3-4bec-b8f8-de25d309f8a6" +zcharacterEricTheEngimaJones.Commands.TalkTo.WorksWithAll = true +zcharacterHotelReceptionist = Wherigo.ZCharacter({Cartridge = cartTheOmbosIdol01, Container = zoneHotelRoyale}) +zcharacterHotelReceptionist.Id = "992e145f-f47c-4dbc-85cc-6ae544dd977f" +zcharacterHotelReceptionist.Name = "Hotel Receptionist" +zcharacterHotelReceptionist.Description = "The receptionist wears thick rimmed spectacles, and around an inch of make-up which suggest that said eyepieces do not feature in her morning ritual. She buzzes around the reception area with cloud of officious pomposity." +zcharacterHotelReceptionist.Visible = true +zcharacterHotelReceptionist.Icon = zmediaPersonIcon +zcharacterHotelReceptionist.Gender = "Female" +zcharacterHotelReceptionist.Type = "NPC" +zcharacterHotelReceptionist.ObjectLocation = ZonePoint(53.4790483724528, -2.23327328535647, 360) +zcharacterHotelReceptionist.Commands = { + CheckIn = Wherigo.ZCommand({ + Text = "Check In", + CmdWith = false, + Enabled = true, + EmptyTargetListText = "Nothing available" + }), + CheckforMessages = Wherigo.ZCommand({ + Text = "Check for Messages", + CmdWith = false, + Enabled = false, + EmptyTargetListText = "Nothing available" + }) +} +zcharacterHotelReceptionist.Commands.CheckIn.Custom = true +zcharacterHotelReceptionist.Commands.CheckIn.Id = "b4ac0387-6d89-4e99-8dde-f3e6570eacc1" +zcharacterHotelReceptionist.Commands.CheckIn.WorksWithAll = true +zcharacterHotelReceptionist.Commands.CheckforMessages.Custom = true +zcharacterHotelReceptionist.Commands.CheckforMessages.Id = "cca76c69-596d-4708-8f6b-9bea08105d42" +zcharacterHotelReceptionist.Commands.CheckforMessages.WorksWithAll = true +zcharacterMysteriousStranger = Wherigo.ZCharacter(cartTheOmbosIdol01) +zcharacterMysteriousStranger.Id = "1e917060-63d3-46c5-8640-9fafaf4e618f" +zcharacterMysteriousStranger.Name = "Mysterious Stranger" +zcharacterMysteriousStranger.Description = [[ +Wearing a long coat and carrying what looks like a hardcase for a double bass, the stranger poses a truly mysterious figure. As you watch him, he glaces nonchalently at you, lighting a cigarette.
+
+Note: Whilst smoking was cool in the 1930s, it is certainly not now. It just causes long a painful death, and the need to huddle outside pubs in the rain.]] +zcharacterMysteriousStranger.Visible = true +zcharacterMysteriousStranger.Icon = zmediaPersonIcon +zcharacterMysteriousStranger.Gender = "Male" +zcharacterMysteriousStranger.Type = "NPC" +zcharacterMysteriousStranger.ObjectLocation = Wherigo.INVALID_ZONEPOINT +zcharacterMysteriousStranger.Commands = { + SpeakTo = Wherigo.ZCommand({ + Text = "Speak To", + CmdWith = false, + Enabled = true, + EmptyTargetListText = "Nothing available" + }) +} +zcharacterMysteriousStranger.Commands.SpeakTo.Custom = true +zcharacterMysteriousStranger.Commands.SpeakTo.Id = "040e2545-a1e7-4beb-af9c-e4dae3e074af" +zcharacterMysteriousStranger.Commands.SpeakTo.WorksWithAll = true +zcharacterIgnatiusCrimp = Wherigo.ZCharacter({Cartridge = cartTheOmbosIdol01, Container = zoneLondonRoadStation}) +zcharacterIgnatiusCrimp.Id = "61449a95-b520-4cab-9069-b89d49bf38ee" +zcharacterIgnatiusCrimp.Name = "Ignatius Crimp" +zcharacterIgnatiusCrimp.Description = [[ +The professor is a short, excitable man with a shock of white hair. His green suit is threadbare and scuffed, with leather patches which, in turn, have been themseves patched.
+
+His current level of agitation is extreme, even for him. He is holding what appears to be a heavy iron bar.]] +zcharacterIgnatiusCrimp.Visible = true +zcharacterIgnatiusCrimp.Icon = zmediaPersonIcon +zcharacterIgnatiusCrimp.Gender = "Male" +zcharacterIgnatiusCrimp.Type = "NPC" +zcharacterIgnatiusCrimp.ObjectLocation = ZonePoint(53.4818719682271, -2.22937574699958, 0) +zcharacterIgnatiusCrimp.Commands = { + TalkTo = Wherigo.ZCommand({ + Text = "Talk To", + CmdWith = false, + Enabled = true, + EmptyTargetListText = "Nothing available" + }) +} +zcharacterIgnatiusCrimp.Commands.TalkTo.Custom = true +zcharacterIgnatiusCrimp.Commands.TalkTo.Id = "4f4e616e-8ab3-4d9c-86a6-08f7ac92cd36" +zcharacterIgnatiusCrimp.Commands.TalkTo.WorksWithAll = true +zcharacterBody = Wherigo.ZCharacter({Cartridge = cartTheOmbosIdol01, Container = zoneCanalPort}) +zcharacterBody.Id = "a1764820-eef8-4358-8262-804ab92553e5" +zcharacterBody.Name = "Body" +zcharacterBody.Description = "Until very recently, this body belonged to the canal boat's skipper. He seems to have been liberated from his mortal coil with a blunt trauma to the back of the head." +zcharacterBody.Visible = true +zcharacterBody.Icon = zmediaPersonIcon +zcharacterBody.Gender = "Male" +zcharacterBody.Type = "NPC" +zcharacterBody.ObjectLocation = ZonePoint(53.4818312401449, -2.22937992695604, 360) +zitemDog = Wherigo.ZItem({Cartridge = cartTheOmbosIdol01, Container = zoneVagrantsQuandarySpot1}) +zitemDog.Id = "7ffcde13-1aa0-427a-a75b-927f2eb847fe" +zitemDog.Name = "Dog" +zitemDog.Description = "A filthy mongrel, riddled with mange and with breath which - used aggressively - would constitute a war crime." +zitemDog.Visible = true +zitemDog.ObjectLocation = ZonePoint(53.47817208522, -2.23222904978744, 360) +zitemDog.Icon = zmediaIconUsableObject +zitemDog.Locked = false +zitemDog.Opened = false +zitemDog.Commands = { + PickUp = Wherigo.ZCommand({ + Text = "Pick Up", + CmdWith = false, + Enabled = true, + EmptyTargetListText = "Nothing available" + }), + PutDown = Wherigo.ZCommand({ + Text = "Put Down", + CmdWith = false, + Enabled = false, + EmptyTargetListText = "Nothing available" + }) +} +zitemDog.Commands.PickUp.Custom = true +zitemDog.Commands.PickUp.Id = "a5c9f678-03fa-4ee8-8d03-43199df90d26" +zitemDog.Commands.PickUp.WorksWithAll = true +zitemDog.Commands.PutDown.Custom = true +zitemDog.Commands.PutDown.Id = "e96a7408-3e0f-4006-8878-edb5ceae25a6" +zitemDog.Commands.PutDown.WorksWithAll = true +zitemRat = Wherigo.ZItem({Cartridge = cartTheOmbosIdol01, Container = zoneVagrantsQuandarySpot1}) +zitemRat.Id = "4ed29d91-5258-495b-b107-e73624256e36" +zitemRat.Name = "Rat" +zitemRat.Description = "You eye the rat suspiciously, wary that it probably harbours all manner of unpleasant diseases. The rat eyes you back - it is clear that your suspicions are mutually shared." +zitemRat.Visible = true +zitemRat.ObjectLocation = ZonePoint(53.4780979999954, -2.23218144046037, 360) +zitemRat.Icon = zmediaIconUsableObject +zitemRat.Locked = false +zitemRat.Opened = false +zitemRat.Commands = { + PickUp = Wherigo.ZCommand({ + Text = "Pick Up", + CmdWith = false, + Enabled = true, + EmptyTargetListText = "Nothing available" + }), + PutDown = Wherigo.ZCommand({ + Text = "Put Down", + CmdWith = false, + Enabled = false, + EmptyTargetListText = "Nothing available" + }) +} +zitemRat.Commands.PickUp.Custom = true +zitemRat.Commands.PickUp.Id = "f4d24ff0-8e8a-4eb1-bfef-d30c6fee85dc" +zitemRat.Commands.PickUp.WorksWithAll = true +zitemRat.Commands.PutDown.Custom = true +zitemRat.Commands.PutDown.Id = "75e243ee-54a6-4426-886b-63dbab095fc0" +zitemRat.Commands.PutDown.WorksWithAll = true +zitemPasty = Wherigo.ZItem({Cartridge = cartTheOmbosIdol01, Container = zoneVagrantsQuandarySpot1}) +zitemPasty.Id = "a2aaeffe-cf21-4c41-97db-81a61dcf7262" +zitemPasty.Name = "Pasty" +zitemPasty.Description = "A Cornish pasty. It would be appetising, had you less knowledge of its owner." +zitemPasty.Visible = true +zitemPasty.ObjectLocation = ZonePoint(53.4780365634676, -2.23214115564516, 360) +zitemPasty.Icon = zmediaIconUsableObject +zitemPasty.Locked = false +zitemPasty.Opened = false +zitemPasty.Commands = { + PickUp = Wherigo.ZCommand({ + Text = "Pick Up", + CmdWith = false, + Enabled = true, + EmptyTargetListText = "Nothing available" + }), + PutDown = Wherigo.ZCommand({ + Text = "Put Down", + CmdWith = false, + Enabled = false, + EmptyTargetListText = "Nothing available" + }) +} +zitemPasty.Commands.PickUp.Custom = true +zitemPasty.Commands.PickUp.Id = "6f0fb983-7439-4a5e-a9dd-7915d1f1f323" +zitemPasty.Commands.PickUp.WorksWithAll = true +zitemPasty.Commands.PutDown.Custom = true +zitemPasty.Commands.PutDown.Id = "8922c424-4630-4c50-9b47-287e227687e0" +zitemPasty.Commands.PutDown.WorksWithAll = true +zitemCoin = Wherigo.ZItem(cartTheOmbosIdol01) +zitemCoin.Id = "27ecdeb1-3bd5-4384-b426-ad783502c4cc" +zitemCoin.Name = "Coin" +zitemCoin.Description = "All of Eric 'The Enigma' Jones' worldly wealth. Amounting, in total, to a single thrupenny bit." +zitemCoin.Visible = true +zitemCoin.ObjectLocation = Wherigo.INVALID_ZONEPOINT +zitemCoin.Icon = zmediaIconUsableObject +zitemCoin.Locked = false +zitemCoin.Opened = false +zitemPublicTelephone = Wherigo.ZItem({Cartridge = cartTheOmbosIdol01, Container = zoneLondonRoadStation}) +zitemPublicTelephone.Id = "3bc9aac2-6bdc-4269-ae79-153c4140fdcd" +zitemPublicTelephone.Name = "Public Telephone" +zitemPublicTelephone.Description = "A new-fangled public callbox. It takes thruppence to make a local call." +zitemPublicTelephone.Visible = true +zitemPublicTelephone.ObjectLocation = ZonePoint(53.4779760213334, -2.23155393357318, 360) +zitemPublicTelephone.Icon = zmediaIconUsableObject +zitemPublicTelephone.Locked = false +zitemPublicTelephone.Opened = false +zitemPublicTelephone.Commands = { + InsertCoin = Wherigo.ZCommand({ + Text = "Insert Coin", + CmdWith = false, + Enabled = true, + EmptyTargetListText = "You can't do that right now." + }) +} +zitemPublicTelephone.Commands.InsertCoin.Custom = true +zitemPublicTelephone.Commands.InsertCoin.Id = "cf00377b-79d7-4820-a599-633dc9e8ebdb" +zitemPublicTelephone.Commands.InsertCoin.WorksWithAll = false +zitemPublicTelephone.Commands.InsertCoin.WorksWithListIds = {} +zitemLetterfromProfessor = Wherigo.ZItem(cartTheOmbosIdol01) +zitemLetterfromProfessor.Id = "42744b43-76dc-4bb3-b262-350b35d710dd" +zitemLetterfromProfessor.Name = "Letter from Professor" +zitemLetterfromProfessor.Description = "" +zitemLetterfromProfessor.Visible = true +zitemLetterfromProfessor.ObjectLocation = Wherigo.INVALID_ZONEPOINT +zitemLetterfromProfessor.Media = zmediaLetterfromProfessor +zitemLetterfromProfessor.Icon = zmediaIconExaminableObject +zitemLetterfromProfessor.Locked = false +zitemLetterfromProfessor.Opened = false +zitemReceptionTelephone = Wherigo.ZItem({Cartridge = cartTheOmbosIdol01, Container = zoneHotelRoyale}) +zitemReceptionTelephone.Id = "8d455551-0da4-4bf6-a9d3-c0204d0a1daf" +zitemReceptionTelephone.Name = "Reception Telephone" +zitemReceptionTelephone.Description = "The Formica telephone is intended for the use of the hotel receptionist - though you may be able to surrupticiously place a call at some point." +zitemReceptionTelephone.Visible = true +zitemReceptionTelephone.ObjectLocation = ZonePoint(53.4790785962602, -2.23329973473207, 360) +zitemReceptionTelephone.Icon = zmediaIconUsableObject +zitemReceptionTelephone.Locked = false +zitemReceptionTelephone.Opened = false +zitemReceptionTelephone.Commands = { + PlaceCall = Wherigo.ZCommand({ + Text = "Place Call", + CmdWith = false, + Enabled = false, + EmptyTargetListText = "Nothing available" + }) +} +zitemReceptionTelephone.Commands.PlaceCall.Custom = true +zitemReceptionTelephone.Commands.PlaceCall.Id = "f6424e94-e737-457c-9f80-2f7c5a66738d" +zitemReceptionTelephone.Commands.PlaceCall.WorksWithAll = true +zitemTelegram = Wherigo.ZItem(cartTheOmbosIdol01) +zitemTelegram.Id = "320f70c0-adfc-4dea-9012-516b3fa5b480" +zitemTelegram.Name = "Telegram" +zitemTelegram.Description = "" +zitemTelegram.Visible = true +zitemTelegram.ObjectLocation = Wherigo.INVALID_ZONEPOINT +zitemTelegram.Media = zmediaTelegram +zitemTelegram.Icon = zmediaIconExaminableObject +zitemTelegram.Locked = false +zitemTelegram.Opened = false +zitemTelegram.Commands = { + EnterKeyWord = Wherigo.ZCommand({ + Text = "Enter Key Word", + CmdWith = false, + Enabled = true, + EmptyTargetListText = "Nothing available" + }) +} +zitemTelegram.Commands.EnterKeyWord.Custom = true +zitemTelegram.Commands.EnterKeyWord.Id = "6ccdffd1-2933-45e7-bce4-812d987f4292" +zitemTelegram.Commands.EnterKeyWord.WorksWithAll = true +zitemCanalBoat = Wherigo.ZItem({Cartridge = cartTheOmbosIdol01, Container = zoneCanalPort}) +zitemCanalBoat.Id = "5eeb6f2f-4d38-4952-bacc-14ded49246ee" +zitemCanalBoat.Name = "Canal Boat" +zitemCanalBoat.Description = "The canal boat is moored silently at the quayside. It's guady paintwork loses some of its joviality in that it is spattered with deep red blood." +zitemCanalBoat.Visible = true +zitemCanalBoat.ObjectLocation = ZonePoint(53.4818211110981, -2.22946287167988, 360) +zitemCanalBoat.Icon = zmediaIconExaminableObject +zitemCanalBoat.Locked = false +zitemCanalBoat.Opened = false +zitemBookPageFacsimile = Wherigo.ZItem(cartTheOmbosIdol01) +zitemBookPageFacsimile.Id = "20e677ba-9999-47cd-91d6-8b0d8790b7b9" +zitemBookPageFacsimile.Name = "Book Page Facsimile" +zitemBookPageFacsimile.Description = "" +zitemBookPageFacsimile.Visible = true +zitemBookPageFacsimile.ObjectLocation = Wherigo.INVALID_ZONEPOINT +zitemBookPageFacsimile.Media = zmediaBookPage1 +zitemBookPageFacsimile.Icon = zmediaIconExaminableObject +zitemBookPageFacsimile.Locked = false +zitemBookPageFacsimile.Opened = false +zitemScatteredCrates = Wherigo.ZItem({Cartridge = cartTheOmbosIdol01, Container = zoneCanalPort}) +zitemScatteredCrates.Id = "ccc9b961-edcf-4e6a-8763-bc98ee3cb5a1" +zitemScatteredCrates.Name = "Scattered Crates" +zitemScatteredCrates.Description = "Cargo crates are strewn across the footpath." +zitemScatteredCrates.Visible = true +zitemScatteredCrates.ObjectLocation = ZonePoint(53.4817574204961, -2.22940082673837, 360) +zitemScatteredCrates.Icon = zmediaIconExaminableObject +zitemScatteredCrates.Locked = false +zitemScatteredCrates.Opened = false +zitemNotefromMysteriousBarry = Wherigo.ZItem(cartTheOmbosIdol01) +zitemNotefromMysteriousBarry.Id = "d78a8291-3d57-4515-9acb-2b601fa0565f" +zitemNotefromMysteriousBarry.Name = "Note from Mysterious Barry" +zitemNotefromMysteriousBarry.Description = "." +zitemNotefromMysteriousBarry.Visible = true +zitemNotefromMysteriousBarry.ObjectLocation = Wherigo.INVALID_ZONEPOINT +zitemNotefromMysteriousBarry.Media = zmediaBarrysNote +zitemNotefromMysteriousBarry.Icon = zmediaIconExaminableObject +zitemNotefromMysteriousBarry.Locked = false +zitemNotefromMysteriousBarry.Opened = false +ztaskSolvetheVagrantsPuzzle = Wherigo.ZTask(cartTheOmbosIdol01) +ztaskSolvetheVagrantsPuzzle.Id = "587f51eb-06a8-4605-a28e-9ca81a9e32ce" +ztaskSolvetheVagrantsPuzzle.Name = "Solve the Vagrant's Puzzle" +ztaskSolvetheVagrantsPuzzle.Description = [[ +Eric 'The Enigma' Jones has asked for your help - in return he will give you all of his worldly wealth (don't get your hopes up).
+
+He needs you to transport his rat, dog and pasty from one side of the bridge to another. However, you can only carry one of the items at a time. Furthermore, left alone, the dog and rat will fight, and the rat will attempt to eat the pasty. Somehow, you feel that this task would be more pleasant if the animals were somewhat more ... quaint - say, a hen, a fox and some corn ]] +ztaskSolvetheVagrantsPuzzle.Visible = true +ztaskSolvetheVagrantsPuzzle.Active = false +ztaskSolvetheVagrantsPuzzle.Complete = false +ztaskSolvetheVagrantsPuzzle.CorrectState = "None" +ztaskContactMrsBumbleford = Wherigo.ZTask(cartTheOmbosIdol01) +ztaskContactMrsBumbleford.Id = "eb131623-1f1b-469d-a3fc-02a8c24b34d2" +ztaskContactMrsBumbleford.Name = "Contact Mrs Bumbleford" +ztaskContactMrsBumbleford.Description = "In his letter to you, the Professor suggested that you contact his secretary on your arrival in Manchester." +ztaskContactMrsBumbleford.Visible = true +ztaskContactMrsBumbleford.Active = false +ztaskContactMrsBumbleford.Complete = false +ztaskContactMrsBumbleford.CorrectState = "None" +ztaskCheckintoHotel = Wherigo.ZTask(cartTheOmbosIdol01) +ztaskCheckintoHotel.Id = "b2016331-9991-497d-98d8-ca90119542ed" +ztaskCheckintoHotel.Name = "Check in to Hotel" +ztaskCheckintoHotel.Description = "Mrs Bumbleford has placed a reservation for you at the Hotel Royale. Sounds classy! You should locate the hotel and check-in as soon as possible." +ztaskCheckintoHotel.Visible = true +ztaskCheckintoHotel.Active = false +ztaskCheckintoHotel.Complete = false +ztaskCheckintoHotel.CorrectState = "None" +ztaskFindtheProfessor = Wherigo.ZTask(cartTheOmbosIdol01) +ztaskFindtheProfessor.Id = "ead542d2-4fe6-48b6-8162-be6f3fc7a827" +ztaskFindtheProfessor.Name = "Find the Professor" +ztaskFindtheProfessor.Description = "After a good night's sleep, now would be an appropriate time to contact the Professor and start your new job! Now all you have to do is find him..." +ztaskFindtheProfessor.Visible = true +ztaskFindtheProfessor.Active = false +ztaskFindtheProfessor.Complete = false +ztaskFindtheProfessor.CorrectState = "None" +PhoneNumber = "" +KeyWord = "" +VagrantsPuzzleStatus = "I" +TempBool = false +VagrantsPuzzleItemCount = 0 +cartTheOmbosIdol01.ZVariables = { + PhoneNumber = "", + KeyWord = "", + VagrantsPuzzleStatus = "I", + TempBool = false, + VagrantsPuzzleItemCount = 0 +} +buildervar = {} +buildervar.PhoneNumber = {} +buildervar.PhoneNumber.Id = "f2a116c6-7976-4fca-8d26-d0744ed7cbb8" +buildervar.PhoneNumber.Name = "Phone Number" +buildervar.PhoneNumber.Type = "String" +buildervar.PhoneNumber.Data = "" +buildervar.PhoneNumber.Description = "Phone number what the player enters into the payphone" +buildervar.KeyWord = {} +buildervar.KeyWord.Id = "79fd1dd8-7086-4d4e-9fa3-126d8abee3f2" +buildervar.KeyWord.Name = "Key Word" +buildervar.KeyWord.Type = "String" +buildervar.KeyWord.Data = "" +buildervar.KeyWord.Description = "" +buildervar.VagrantsPuzzleStatus = {} +buildervar.VagrantsPuzzleStatus.Id = "ad7a5db0-95c3-4cd2-ab1e-30c34b3a30a0" +buildervar.VagrantsPuzzleStatus.Name = "Vagrants Puzzle Status" +buildervar.VagrantsPuzzleStatus.Type = "String" +buildervar.VagrantsPuzzleStatus.Data = "I" +buildervar.VagrantsPuzzleStatus.Description = "I, A, S" +buildervar.TempBool = {} +buildervar.TempBool.Id = "c041bedb-4165-463f-ba3e-a0922ba1934b" +buildervar.TempBool.Name = "TempBool" +buildervar.TempBool.Type = "Flag" +buildervar.TempBool.Data = "False" +buildervar.TempBool.Description = "" +buildervar.VagrantsPuzzleItemCount = {} +buildervar.VagrantsPuzzleItemCount.Id = "2d0f4fe4-1031-42d1-8116-bcf9915e9ac6" +buildervar.VagrantsPuzzleItemCount.Name = "Vagrants Puzzle Item Count" +buildervar.VagrantsPuzzleItemCount.Type = "Number" +buildervar.VagrantsPuzzleItemCount.Data = "0" +buildervar.VagrantsPuzzleItemCount.Description = "" +ztimerMysteriousStrangerMover = Wherigo.ZTimer(cartTheOmbosIdol01) +ztimerMysteriousStrangerMover.Id = "086ef778-a402-4ab4-bb44-dd7d8c585cd2" +ztimerMysteriousStrangerMover.Name = "Mysterious Stranger Mover" +ztimerMysteriousStrangerMover.Description = "" +ztimerMysteriousStrangerMover.Visible = true +ztimerMysteriousStrangerMover.Duration = 10 +ztimerMysteriousStrangerMover.Type = "Interval" +ztimerMysteriousStrangerActivator = Wherigo.ZTimer(cartTheOmbosIdol01) +ztimerMysteriousStrangerActivator.Id = "3f54e335-2139-44d2-be67-b473569973b4" +ztimerMysteriousStrangerActivator.Name = "Mysterious Stranger Activator" +ztimerMysteriousStrangerActivator.Description = "" +ztimerMysteriousStrangerActivator.Visible = true +ztimerMysteriousStrangerActivator.Duration = 40 +ztimerMysteriousStrangerActivator.Type = "Countdown" +zinputPhoneMrsBumbleford = Wherigo.ZInput(cartTheOmbosIdol01) +zinputPhoneMrsBumbleford.Id = "766aab34-bd55-4086-90e8-c1498acdbb1a" +zinputPhoneMrsBumbleford.Name = "Phone Mrs Bumbleford" +zinputPhoneMrsBumbleford.Description = "" +zinputPhoneMrsBumbleford.Visible = true +zinputPhoneMrsBumbleford.InputType = "Text" +zinputPhoneMrsBumbleford.InputVariableId = "f2a116c6-7976-4fca-8d26-d0744ed7cbb8" +zinputPhoneMrsBumbleford.Text = "Enter Number:" +zinputPhoneProfessor = Wherigo.ZInput(cartTheOmbosIdol01) +zinputPhoneProfessor.Id = "ddbcf282-f28c-4eb4-921b-db3811da36b2" +zinputPhoneProfessor.Name = "Phone Professor" +zinputPhoneProfessor.Description = "" +zinputPhoneProfessor.Visible = true +zinputPhoneProfessor.InputType = "Text" +zinputPhoneProfessor.InputVariableId = "f2a116c6-7976-4fca-8d26-d0744ed7cbb8" +zinputPhoneProfessor.Text = "Enter Number" +zinputEnterKeyWord = Wherigo.ZInput(cartTheOmbosIdol01) +zinputEnterKeyWord.Id = "3b494c6f-5c89-42aa-ab39-33473173f020" +zinputEnterKeyWord.Name = "Enter Key Word" +zinputEnterKeyWord.Description = "" +zinputEnterKeyWord.Visible = true +zinputEnterKeyWord.InputType = "Text" +zinputEnterKeyWord.InputVariableId = "79fd1dd8-7086-4d4e-9fa3-126d8abee3f2" +zinputEnterKeyWord.Text = "Enter Key Word:" +zitemPublicTelephone.Commands.InsertCoin.WorksWithList = {} +function zcharacterEricTheEngimaJones:OnTalkTo() + if Wherigo.NoCaseEquals(VagrantsPuzzleStatus, "A") then + Wherigo.MessageBox({ + Text = "\"Tell you what, my good fellow,\" he begins - affecting a tone of age-old comradeship. \"If you can convey my paraphernalia to the other side of the bridge, I'll give you all of my worldly treasure!\"" + }) + end + if Wherigo.NoCaseEquals(VagrantsPuzzleStatus, "S") then + zitemCoin:MoveTo(Player) + Wherigo.Dialog({ + { + Text = "On seeing you approach, Eric leaps to his feet." + }, + { + Text = "\"You, my good sir, are a gentleman and a scholar,\" he rasps excitedly. \"A thousand felicitations, and infinite thanks - here take my worldly wealth.\"" + }, + { + Text = "He pushes a single coin - looks like a thrupenny bit - into your hand, before hobbling off into the distance." + } + }) + ztaskSolvetheVagrantsPuzzle.Complete = true + zcharacterEricTheEngimaJones:MoveTo(nil) + end + if Wherigo.NoCaseEquals(VagrantsPuzzleStatus, "I") then + Wherigo.Dialog({ + { + Text = "You approach the vagrant - gently coughing to gain his attention. Startled, he jumps to his feet - quickly brushing himself down to regain his composure. Given his disheveled state, brushing himself down is an exercise in futility." + }, + { + Text = "\"Sorry, sir - was lost in a world of my own there,\" he rasps in a voice which could cut wood. \"Thing is, I'd love to pass the time with a gent such as your good self - but I'm stuck in something of a quandary, an incogitable paradox, if you will\"" + }, + { + Text = "You ponder, for a moment, how one of such low social standing could have acquired such expansive vocabulary." + }, + { + Text = "\"Thing is,\" he continues. \"I need to convey my belongings over to the other side of that bridge.\"" + }, + { + Text = "The pungent vagabond gestures to the bridge, where you see a dog, a rat and something greasy wrapped in paper." + }, + { + Text = "\"Problem is, I have only the means to transit one item at a time. Every time I leave dog and rat together, they fight - and rat has a tendency to pilfer my pasty. As you can see, it's a onerous enterprise.\"" + }, + { + Text = "Eric sighs and slumps back to the ground. Suddenly, he perks again." + }, + { + Text = "\"Tell you what, my good fellow,\" he begins - affecting a tone of age-old comradeship. \"If you can convey my paraphernalia to the other side of the bridge, I'll give you all of my worldly treasure!\"" + } + }) + zoneVagrantsQuandarySpot1.Visible = true + zoneVagrantsQuandarySpot1.Active = true + zoneVagrantsQuandarySpot2.Active = true + zoneVagrantsQuandarySpot2.Visible = true + ztaskSolvetheVagrantsPuzzle.Active = true + VagrantsPuzzleStatus = "A" + end +end +function zitemDog:OnPickUp() + if VagrantsPuzzleItemCount == 1 then + Wherigo.MessageBox({ + Text = "You can only carry one of the vagrant's items at any one time" + }) + else + zitemDog:MoveTo(Player) + VagrantsPuzzleItemCount = VagrantsPuzzleItemCount + 1 + zitemDog.Commands.PickUp.Enabled = false + zitemDog.Commands.PutDown.Enabled = true + Wherigo.ShowScreen(Wherigo.MAINSCREEN) + end +end +function zitemPasty:OnPickUp() + if VagrantsPuzzleItemCount == 1 then + Wherigo.MessageBox({ + Text = "You can only carry one of the vagrant's items at any one time" + }) + else + zitemPasty:MoveTo(Player) + VagrantsPuzzleItemCount = VagrantsPuzzleItemCount + 1 + zitemPasty.Commands.PickUp.Enabled = false + zitemPasty.Commands.PutDown.Enabled = true + Wherigo.ShowScreen(Wherigo.MAINSCREEN) + end +end +function zitemRat:OnPickUp() + if VagrantsPuzzleItemCount == 1 then + Wherigo.MessageBox({ + Text = "You can only carry one of the vagrant's items at any one time" + }) + else + zitemRat:MoveTo(Player) + VagrantsPuzzleItemCount = VagrantsPuzzleItemCount + 1 + zitemRat.Commands.PickUp.Enabled = false + zitemRat.Commands.PutDown.Enabled = true + Wherigo.ShowScreen(Wherigo.MAINSCREEN) + end +end +function zitemDog:OnPutDown() + if zoneVagrantsQuandarySpot1:Contains(Player) then + zitemDog:MoveTo(zoneVagrantsQuandarySpot1) + end + if zoneVagrantsQuandarySpot2:Contains(Player) then + zitemDog:MoveTo(zoneVagrantsQuandarySpot2) + end + if zoneVagrantsQuandarySpot1:Contains(Player) or zoneVagrantsQuandarySpot2:Contains(Player) then + VagrantsPuzzleItemCount = VagrantsPuzzleItemCount - 1 + zitemDog.Commands.PickUp.Enabled = true + zitemDog.Commands.PutDown.Enabled = false + Wherigo.ShowScreen(Wherigo.MAINSCREEN) + else + Wherigo.MessageBox({ + Text = "You can't drop that here - try moving to one of the designated puzzle spots" + }) + end +end +function zitemPasty:OnPutDown() + if zoneVagrantsQuandarySpot1:Contains(Player) then + zitemPasty:MoveTo(zoneVagrantsQuandarySpot1) + end + if zoneVagrantsQuandarySpot2:Contains(Player) then + zitemPasty:MoveTo(zoneVagrantsQuandarySpot2) + end + if zoneVagrantsQuandarySpot1:Contains(Player) or zoneVagrantsQuandarySpot2:Contains(Player) then + VagrantsPuzzleItemCount = VagrantsPuzzleItemCount - 1 + zitemPasty.Commands.PickUp.Enabled = true + zitemPasty.Commands.PutDown.Enabled = false + Wherigo.ShowScreen(Wherigo.MAINSCREEN) + else + Wherigo.MessageBox({ + Text = "You can't drop that here - try moving to one of the designated puzzle spots" + }) + end +end +function zitemRat:OnPutDown() + if zoneVagrantsQuandarySpot1:Contains(Player) then + zitemRat:MoveTo(zoneVagrantsQuandarySpot1) + end + if zoneVagrantsQuandarySpot2:Contains(Player) then + zitemRat:MoveTo(zoneVagrantsQuandarySpot2) + end + if zoneVagrantsQuandarySpot1:Contains(Player) or zoneVagrantsQuandarySpot2:Contains(Player) then + VagrantsPuzzleItemCount = VagrantsPuzzleItemCount - 1 + zitemRat.Commands.PickUp.Enabled = true + zitemRat.Commands.PutDown.Enabled = false + Wherigo.ShowScreen(Wherigo.MAINSCREEN) + else + Wherigo.MessageBox({ + Text = "You can't drop that here - try moving to one of the designated puzzle spots" + }) + end +end +function zoneVagrantsQuandarySpot1:OnExit() + TempBool = false + if zoneVagrantsQuandarySpot1:Contains(zitemDog) and zoneVagrantsQuandarySpot1:Contains(zitemRat) and not zoneVagrantsQuandarySpot1:Contains(zitemPasty) then + TempBool = true + Wherigo.MessageBox({ + Text = "Dog attacked rat - resetting puzzle..." + }) + end + if zoneVagrantsQuandarySpot1:Contains(zitemRat) and zoneVagrantsQuandarySpot1:Contains(zitemPasty) and not zoneVagrantsQuandarySpot1:Contains(zitemDog) then + TempBool = true + Wherigo.MessageBox({ + Text = "Rat tried to eat pasty - resetting puzzle" + }) + end + if TempBool == true then + zitemDog:MoveTo(zoneVagrantsQuandarySpot1) + zitemRat:MoveTo(zoneVagrantsQuandarySpot1) + zitemPasty:MoveTo(zoneVagrantsQuandarySpot1) + VagrantsPuzzleItemCount = 0 + zitemDog.Commands.PickUp.Enabled = true + zitemDog.Commands.PutDown.Enabled = false + zitemRat.Commands.PickUp.Enabled = true + zitemRat.Commands.PutDown.Enabled = false + zitemPasty.Commands.PickUp.Enabled = true + zitemPasty.Commands.PutDown.Enabled = false + end +end +function zoneVagrantsQuandarySpot2:OnExit() + TempBool = false + if zoneVagrantsQuandarySpot2:Contains(zitemDog) and zoneVagrantsQuandarySpot2:Contains(zitemRat) and not zoneVagrantsQuandarySpot2:Contains(zitemPasty) then + TempBool = true + Wherigo.MessageBox({ + Text = "Dog attacked rat - resetting puzzle..." + }) + end + if zoneVagrantsQuandarySpot2:Contains(zitemRat) and zoneVagrantsQuandarySpot2:Contains(zitemPasty) and not zoneVagrantsQuandarySpot2:Contains(zitemDog) then + TempBool = true + Wherigo.MessageBox({ + Text = "Rat tried to eat pasty - resetting puzzle" + }) + end + if TempBool == true then + zitemDog:MoveTo(zoneVagrantsQuandarySpot1) + zitemRat:MoveTo(zoneVagrantsQuandarySpot1) + zitemPasty:MoveTo(zoneVagrantsQuandarySpot1) + VagrantsPuzzleItemCount = 0 + zitemDog.Commands.PickUp.Enabled = true + zitemDog.Commands.PutDown.Enabled = false + zitemRat.Commands.PickUp.Enabled = true + zitemRat.Commands.PutDown.Enabled = false + zitemPasty.Commands.PickUp.Enabled = true + zitemPasty.Commands.PutDown.Enabled = false + end + if zoneVagrantsQuandarySpot2:Contains(zitemDog) and zoneVagrantsQuandarySpot2:Contains(zitemRat) and zoneVagrantsQuandarySpot2:Contains(zitemPasty) then + Wherigo.MessageBox({ + Text = "Congratulations! Puzzle completed - return to the vagrant to collect your reward!" + }) + VagrantsPuzzleStatus = "S" + zoneVagrantsQuandarySpot1.Active = false + zoneVagrantsQuandarySpot1.Visible = false + zoneVagrantsQuandarySpot2.Active = false + zoneVagrantsQuandarySpot2.Visible = false + end +end +function zitemLetterfromProfessor:OnClick() + ztaskContactMrsBumbleford.Active = true + Wherigo.ShowScreen(Wherigo.DETAILSCREEN, zitemLetterfromProfessor) +end +function cartTheOmbosIdol01:OnStart() + ztimerMysteriousStrangerActivator:Start() + zitemLetterfromProfessor:MoveTo(Player) + zcharacterIgnatiusCrimp:MoveTo(zoneCanalPort) + Wherigo.Dialog({ + {Text = ".", Media = zmediaTitlePlate1}, + {Text = ".", Media = zmediaTitlePlate2}, + {Text = ".", Media = zmediaTitlePlate3}, + {Text = ".", Media = zmediaTitlePlate4}, + {Text = ".", Media = zmediaTitlePlate5}, + {Text = ".", Media = zmediaTitlePlate6}, + {Text = ".", Media = zmediaTitlePlate7} + }) +end +function zitemPublicTelephone:OnInsertCoin() + if Player:Contains(zitemCoin) then + Wherigo.GetInput(zinputPhoneMrsBumbleford) + else + Wherigo.MessageBox({ + Text = "Hmmm, that would be a great idea - if only you had a coin." + }) + end +end +function zinputPhoneMrsBumbleford:OnGetInput(input) + PhoneNumber = input + if Wherigo.NoCaseEquals(PhoneNumber, "555-7162") or Wherigo.NoCaseEquals(PhoneNumber, "555 7162") or Wherigo.NoCaseEquals(PhoneNumber, "5557162") then + zitemCoin:MoveTo(nil) + ztaskContactMrsBumbleford.Complete = true + ztaskCheckintoHotel.Active = true + zoneHotelRoyale.Active = true + zoneHotelRoyale.Visible = true + Wherigo.Dialog({ + { + Text = "Brrr, brrr ..." + }, + { + Text = "... brrr, brrr ..." + }, + { + Text = "... the phone clicks and the coin rattles through internal mechanisms as the call's recipient picks up their handset." + }, + { + Text = "\"Hello,\" says a warm and elderly voice. \"Professor Crimp's office.\"" + }, + { + Text = "\"Ah, Jack,\" continues the voice, which could only be owned by Mrs Bumbleford. \"The professor will be delighted that you have arrived safely. Was your journey pleasant?\"" + }, + { + Text = "\"Now deary, it's late - not worth you coming down to the university. I've made a reservation for you at the Hotel Royale, not far from the station\" " + }, + { + Text = "\"Why don't you make your way there and get some rest - the Professor has sent over some reading for you, if you're bored,\" she says, then with an air of knowing mischief: \"But he is such a terrible fusspot. If I were you, I'd just relax so you're fresh and bright tomorrow.\"" + }, + { + Text = "\"When you're ready to join us in the morning, just give me a ring and we'll sort something out\"." + }, + { + Text = "\"Goodbye, deary\"." + } + }) + else + Wherigo.Dialog({ + { + Text = "Brrr, brrr ..." + }, + { + Text = "... brrr, brrr ..." + }, + { + Text = "... brrr, brrr ..." + }, + { + Text = "Hmm, no answer. You replace the handset and retrieve your coin." + } + }) + end +end +function zcharacterHotelReceptionist:OnCheckIn() + Wherigo.Dialog({ + { + Text = "\"Welcome to the a'Hotel Royale,\" the receptionist trills. \"a'How may h'I be of h'assistance?\"" + }, + { + Text = "\"Certainly Mr Robinson,\" she continues. \"h'A reservation a'has been made for you earlier today\"" + }, + { + Text = "You note the receptionist's annoying tendency, in an attempt to affect education and class, to pronounce a's before h's, and h's before a's" + }, + { + Text = "\"a'Here h'are your keys, \" she chirps - passing you a set of keys. \"h'Oh, h'and h'one more thing. h'I was h'instructed to give this to you on your h'arrival.\"" + }, + { + Text = "She passes to you what looks like a facsimilie of a textbook page. You scrunch it into your pocket and head up to your room." + }, + { + Text = "Collapsing onto your bed, sleep quicky envelopes you - and with it vivid dreams of ancient and mysterious relics." + }, + { + Text = "Knock, knock ..." + }, + { + Text = "... knock, knock (a little louder) ..." + }, + { + Text = "\"Morning, Mr Robinson,\" chirps a familiar voice from behind the door. \"This is your 7am wake-up call.\"" + }, + { + Text = "You stumble from bed and, having completed your morning rituals, head back down to reception." + } + }) + zitemBookPageFacsimile:MoveTo(Player) + ztaskCheckintoHotel.Complete = true + ztaskFindtheProfessor.Active = true + zcharacterHotelReceptionist.Commands.CheckIn.Enabled = false + zcharacterHotelReceptionist.Visible = false + zitemReceptionTelephone.Commands.PlaceCall.Enabled = true +end +function zinputPhoneProfessor:OnGetInput(input) + PhoneNumber = input + if Wherigo.NoCaseEquals(PhoneNumber, "555-7162") or Wherigo.NoCaseEquals(PhoneNumber, "555 7162") or Wherigo.NoCaseEquals(PhoneNumber, "5557162") then + Wherigo.Dialog({ + { + Text = "Brrr, brrr ..." + }, + { + Text = "... brrr, brrr ..." + }, + { + Text = "... brrr, brrr ..." + }, + { + Text = "Strange, no answer - you're sure you dialed the right number." + } + }) + zcharacterHotelReceptionist.Visible = true + zcharacterHotelReceptionist.Commands.CheckforMessages.Enabled = true + else + Wherigo.Dialog({ + { + Text = "Brrr, brrr ..." + }, + { + Text = "... brrr, brrr ...." + }, + { + Text = "... brrr, brrr ..." + }, + { + Text = "Maybe you dialed the wrong number?" + } + }) + end +end +function zitemReceptionTelephone:OnPlaceCall() + Wherigo.GetInput(zinputPhoneProfessor) +end +function zcharacterHotelReceptionist:OnCheckforMessages() + Wherigo.Dialog({ + { + Text = "\"Good morning, Mr Robinson,\" she chirps. \"A telegram has arrived for you.\"" + }, + { + Text = "She hands over a slip of paper to you." + } + }) + zitemTelegram:MoveTo(Player) + zcharacterHotelReceptionist.Commands.CheckforMessages.Enabled = false +end +function zitemTelegram:OnEnterKeyWord() + Wherigo.GetInput(zinputEnterKeyWord) +end +function ztimerMysteriousStrangerMover:OnTick() + if zoneLondonRoadStation:Contains(zcharacterMysteriousStranger) then + zcharacterMysteriousStranger:MoveTo(zoneHotelRoyale) + else + zcharacterMysteriousStranger:MoveTo(zoneLondonRoadStation) + end +end +function zcharacterMysteriousStranger:OnSpeakTo() + Wherigo.Dialog({ + { + Text = "\"Oh, hello,\" says the Mysterious Stranger. \"My names Davies, Barry Davies.\"" + }, + { + Text = "\"I'm not actually part of this story,\" he continues. \"I've just finished an intrepid adventure in which I tricked a bunch of bloodthirsty pirates out of their treasure.\"" + }, + { + Text = "\"Tell you what, I'll cut you a favour - I've hidden some of the treasure, if you can find it you can help yourself.\"" + }, + { + Text = "He carefully writes something on a piece of paper and, placing it in your shirt pocket, walks off into the distance. The sense of mystery in the surrounding area drops notably." + } + }) + zitemNotefromMysteriousBarry:MoveTo(Player) + ztimerMysteriousStrangerMover:Stop() + zcharacterMysteriousStranger:MoveTo(nil) + zcharacterMysteriousStranger.Visible = false +end +function ztimerMysteriousStrangerActivator:OnTick() + zcharacterMysteriousStranger:MoveTo(zoneLondonRoadStation) + ztimerMysteriousStrangerActivator:Stop() + ztimerMysteriousStrangerMover:Start() +end +function zinputEnterKeyWord:OnGetInput(input) + KeyWord = input + if Wherigo.NoCaseEquals(KeyWord, "Hathor") then + Wherigo.MessageBox({ + Text = "Correct! The Professors Location has been unlocked." + }) + zoneCanalPort.Visible = true + zoneCanalPort.Active = true + zitemTelegram.Commands.EnterKeyWord.Enabled = false + else + Wherigo.MessageBox({ + Text = "Incorrect! You'll find the key word written inside a nano-cache" + }) + end +end +function zcharacterIgnatiusCrimp:OnTalkTo() + Wherigo.MessageBox({ + Text = "The Professor rushes to you, in a state of obvious panic", + Callback = cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB12 + }) + cartTheOmbosIdol01.Complete = true + zcharacterIgnatiusCrimp:MoveTo(nil) +end +function cartTheOmbosIdol01:OnRestore() + if cartTheOmbosIdol01.Complete == true then + Wherigo.MessageBox({ + Text = [[ +Congratulations! You have completed this cartridge.
+
+To play chapter two, you will need to enter the following key word: Horus
+
+You can now register your completion of the cartridge at Wherigo.com, the unlock code is ]] .. Player.CompletionCode + }) + end +end +function cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB1(action) + if action ~= nil then + Wherigo.MessageBox({ + Text = [[ +Congratulations! You have completed this cartridge.
+
+To play chapter two, you will need to enter the following key word: Horus
+
+You can now register your completion of the cartridge at Wherigo.com, the unlock code is ]] .. Player.CompletionCode + }) + end +end +function cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB2(action) + if action ~= nil then + Wherigo.MessageBox({ + Text = ".", + Media = zmediaTobecontinued, + Callback = cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB1 + }) + end +end +function cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB3(action) + if action ~= nil then + Wherigo.MessageBox({ + Text = "Alone, of course, apart from a murdered body and a looming sense that your new life may not be all that you expected it to be.", + Callback = cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB2 + }) + end +end +function cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB4(action) + if action ~= nil then + Wherigo.MessageBox({ + Text = "The professor is led away, protesting his innocence and apparently demanding that the policeman investigate his missing artefact. Soon you are left alone.", + Callback = cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB3 + }) + end +end +function cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB5(action) + if action ~= nil then + Wherigo.MessageBox({ + Text = "\"You do not have to say anything,\" continues the officer, handcuffing the academic. \"But anything you do say may be taken down and used against you as evidence in a court of law.\"", + Callback = cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB4 + }) + end +end +function cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB6(action) + if action ~= nil then + Wherigo.MessageBox({ + Text = "\"Me!\" Crimp splutters. \"But I ... I just ... When I arrived, you see...\"", + Callback = cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB5 + }) + end +end +function cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB7(action) + if action ~= nil then + Wherigo.MessageBox({ + Text = "\"Excuse me, sir,\" he mumbles as he pushes past you. \"You sir, are under arrest for murder.\"", + Callback = cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB6 + }) + end +end +function cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB8(action) + if action ~= nil then + Wherigo.MessageBox({ + Text = "You marvel, for a moment, at the Professor's investigative prowess. Sensing an approaching presence, you turn - coming face to face with a stoic member of the local constabulary.", + Callback = cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB7 + }) + end +end +function cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB9(action) + if action ~= nil then + Wherigo.MessageBox({ + Text = "\"And this poor fellow seems to have been murdered!\" he babbles. \"What a waste ... I found this iron bar next to him - I suspect it of being the murder weapon!\"", + Callback = cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB8 + }) + end +end +function cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB10(action) + if action ~= nil then + Wherigo.MessageBox({ + Text = "He catches your eye as you glance from the body on the quayside to the bloodstained iron bar in his hand.", + Callback = cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB9 + }) + end +end +function cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB11(action) + if action ~= nil then + Wherigo.MessageBox({ + Text = "\"The idol, it's missing! Stolen! Terrible! Irreplacable!\"", + Callback = cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB10 + }) + end +end +function cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB12(action) + if action ~= nil then + Wherigo.MessageBox({ + Text = "\"Jack, my dear boy!\" he rants. \"Something terrible has happened ... truly terrible!\"", + Callback = cartTheOmbosIdol01.MsgBoxCBFuncs.MsgBoxCB11 + }) + end +end +return cartTheOmbosIdol01 diff --git a/components/lua-5.1.4/CMakeLists.txt b/components/lua-5.1.4/CMakeLists.txt index b880721..6834e04 100644 --- a/components/lua-5.1.4/CMakeLists.txt +++ b/components/lua-5.1.4/CMakeLists.txt @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.21) project(lua LANGUAGES C) +set(LUA_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src) + set(LUA_SOURCES src/lapi.c src/lcode.c diff --git a/components/lua-5.1.4/src/lundump.c b/components/lua-5.1.4/src/lundump.c index 8010a45..4732a3a 100644 --- a/components/lua-5.1.4/src/lundump.c +++ b/components/lua-5.1.4/src/lundump.c @@ -2,6 +2,8 @@ ** $Id: lundump.c,v 2.7.1.4 2008/04/04 19:51:41 roberto Exp $ ** load precompiled Lua chunks ** See Copyright Notice in lua.h +** +** Modified to support loading 32-bit bytecode on 64-bit systems */ #include @@ -25,6 +27,11 @@ typedef struct { ZIO* Z; Mbuffer* b; const char* name; + int swap; /* endianness swap required */ + int sizeof_int; /* sizeof(int) in bytecode */ + int sizeof_size_t; /* sizeof(size_t) in bytecode */ + int sizeof_instr; /* sizeof(Instruction) in bytecode */ + int sizeof_number; /* sizeof(lua_Number) in bytecode */ } LoadState; #ifdef LUAC_TRUST_BINARIES @@ -58,25 +65,114 @@ static int LoadChar(LoadState* S) return x; } +static void SwapBytes(void* b, size_t size) +{ + char* p = (char*)b; + size_t i; + for (i = 0; i < size/2; i++) { + char temp = p[i]; + p[i] = p[size - 1 - i]; + p[size - 1 - i] = temp; + } +} + static int LoadInt(LoadState* S) { int x; - LoadVar(S,x); + if (S->sizeof_int == sizeof(int)) { + LoadVar(S,x); + if (S->swap) SwapBytes(&x, sizeof(x)); + } else if (S->sizeof_int == 4 && sizeof(int) >= 4) { + /* 32-bit int from bytecode */ + lu_int32 t; + LoadMem(S,&t,1,4); + if (S->swap) SwapBytes(&t, 4); + x = (int)t; + } else if (S->sizeof_int == 8 && sizeof(int) == 4) { + /* 64-bit int in bytecode on 32-bit system - use lower 32 bits */ + lu_int32 lo, hi; + LoadMem(S, &lo, 1, 4); + LoadMem(S, &hi, 1, 4); + if (S->swap) { + SwapBytes(&lo, 4); + SwapBytes(&hi, 4); + x = (int)hi; + } else { + x = (int)lo; + } + } else { + error(S, "unsupported int size in bytecode"); + x = 0; + } IF (x<0, "bad integer"); return x; } +static size_t LoadSize(LoadState* S) +{ + size_t x; + if (S->sizeof_size_t == sizeof(size_t)) { + LoadVar(S,x); + if (S->swap) SwapBytes(&x, sizeof(x)); + } else if (S->sizeof_size_t == 4 && sizeof(size_t) == 8) { + /* 32-bit bytecode on 64-bit system */ + lu_int32 t; + LoadMem(S,&t,1,4); + if (S->swap) SwapBytes(&t, 4); + x = (size_t)t; + } else if (S->sizeof_size_t == 8 && sizeof(size_t) == 4) { + /* 64-bit bytecode on 32-bit system - use lower 32 bits */ + lu_int32 lo, hi; + LoadMem(S, &lo, 1, 4); + LoadMem(S, &hi, 1, 4); + if (S->swap) { + SwapBytes(&lo, 4); + SwapBytes(&hi, 4); + x = (size_t)hi; /* big-endian: high bytes first */ + } else { + x = (size_t)lo; /* little-endian: low bytes first */ + } + } else { + error(S, "unsupported size_t size in bytecode"); + x = 0; + } + return x; +} + static lua_Number LoadNumber(LoadState* S) { lua_Number x; - LoadVar(S,x); + if (S->sizeof_number == sizeof(lua_Number)) { + LoadVar(S,x); + if (S->swap) SwapBytes(&x, sizeof(x)); + } else { + error(S, "unsupported lua_Number size in bytecode"); + x = 0; + } + return x; +} + +static Instruction LoadInstruction(LoadState* S) +{ + Instruction x; + if (S->sizeof_instr == sizeof(Instruction)) { + LoadVar(S,x); + if (S->swap) SwapBytes(&x, sizeof(x)); + } else if (S->sizeof_instr == 4) { + lu_int32 t; + LoadMem(S,&t,1,4); + if (S->swap) SwapBytes(&t, 4); + x = (Instruction)t; + } else { + error(S, "unsupported Instruction size in bytecode"); + x = 0; + } return x; } static TString* LoadString(LoadState* S) { - size_t size; - LoadVar(S,size); + size_t size = LoadSize(S); if (size==0) return NULL; else @@ -89,10 +185,12 @@ static TString* LoadString(LoadState* S) static void LoadCode(LoadState* S, Proto* f) { - int n=LoadInt(S); + int i, n=LoadInt(S); f->code=luaM_newvector(S->L,n,Instruction); f->sizecode=n; - LoadVector(S,f->code,n,sizeof(Instruction)); + for (i=0; icode[i] = LoadInstruction(S); + } } static Proto* LoadFunction(LoadState* S, TString* p); @@ -140,7 +238,9 @@ static void LoadDebug(LoadState* S, Proto* f) n=LoadInt(S); f->lineinfo=luaM_newvector(S->L,n,int); f->sizelineinfo=n; - LoadVector(S,f->lineinfo,n,sizeof(int)); + for (i=0; ilineinfo[i] = LoadInt(S); + } n=LoadInt(S); f->locvars=luaM_newvector(S->L,n,LocVar); f->sizelocvars=n; @@ -184,9 +284,44 @@ static void LoadHeader(LoadState* S) { char h[LUAC_HEADERSIZE]; char s[LUAC_HEADERSIZE]; - luaU_header(h); + int x = 1; + int local_endian = (unsigned char)(*(char*)&x); + int file_endian; + LoadBlock(S,s,LUAC_HEADERSIZE); - IF (memcmp(h,s,LUAC_HEADERSIZE)!=0, "bad header"); + + /* Check signature */ + luaU_header(h); + IF (memcmp(h,s,sizeof(LUA_SIGNATURE)-1)!=0, "not a precompiled chunk"); + + /* Check version */ + IF (s[4] != LUAC_VERSION, "version mismatch in precompiled chunk"); + + /* Check format */ + IF (s[5] != LUAC_FORMAT, "incompatible format"); + + /* Endianness */ + file_endian = (unsigned char)s[6]; + S->swap = (file_endian != local_endian); + + /* Size information from bytecode */ + S->sizeof_int = (unsigned char)s[7]; + S->sizeof_size_t = (unsigned char)s[8]; + S->sizeof_instr = (unsigned char)s[9]; + S->sizeof_number = (unsigned char)s[10]; + + /* Validate sizes we can handle */ + IF (S->sizeof_int != 4 && S->sizeof_int != 8 && S->sizeof_int != (int)sizeof(int), + "unsupported int size"); + IF (S->sizeof_size_t != 4 && S->sizeof_size_t != 8, + "unsupported size_t size"); + IF (S->sizeof_instr != 4 && S->sizeof_instr != (int)sizeof(Instruction), + "unsupported Instruction size"); + IF (S->sizeof_number != (int)sizeof(lua_Number), + "incompatible lua_Number size"); + + /* Check integral flag */ + IF (s[11] != (((lua_Number)0.5)==0), "incompatible number type"); } /* diff --git a/main/include/app.h b/main/include/app.h new file mode 100644 index 0000000..b0346ae --- /dev/null +++ b/main/include/app.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include + +extern "C" { +struct lua_State; +} + +class cApp : public wxApp +{ +public: + bool OnInit() override; + int OnExit() override; + + bool loadCartridge(const std::string &filePath); + void startGame(); + void unloadCartridge(); + + // Save/Load game state + bool saveGameState(const std::string &saveFilePath); + bool loadGameState(const std::string &saveFilePath); + std::string getAutoSavePath() const; + + // Generate completion log (for wherigo.com) + bool generateCompletionLog(const std::string &logFilePath); + std::string getCompletionLogPath() const; + + lua_State* getLuaState() const { return m_luaState; } + int getCartridgeRef() const { return m_cartridgeRef; } + cartridge::Cartridge* getCartridge() const { return m_cartridge.get(); } + bool isCartridgeLoaded() const { return m_cartridge != nullptr; } + +private: + bool initLuaState(); + + lua_State* m_luaState = nullptr; + int m_cartridgeRef = -1; // LUA_NOREF + std::unique_ptr m_cartridge; + std::string m_cartridgePath; +}; diff --git a/main/include/cApp.h b/main/include/cApp.h deleted file mode 100644 index 4acf55b..0000000 --- a/main/include/cApp.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include - -class cApp : public wxApp -{ -public: - bool OnInit() override; -}; diff --git a/main/include/lua/game_engine.h b/main/include/lua/game_engine.h new file mode 100644 index 0000000..90c73cb --- /dev/null +++ b/main/include/lua/game_engine.h @@ -0,0 +1,73 @@ +#ifndef GAME_ENGINE_H +#define GAME_ENGINE_H + +#include +#include +#include +#include +#include +#include + +extern "C" { +struct lua_State; +} + +namespace wherigo { + +// Custom event for game state changes +class GameStateEvent : public wxEvent { +public: + GameStateEvent(wxEventType eventType = wxEVT_NULL, int id = 0) + : wxEvent(id, eventType) {} + + wxEvent* Clone() const override { return new GameStateEvent(*this); } +}; + +wxDECLARE_EVENT(EVT_GAME_STATE_CHANGED, GameStateEvent); + +class GameEngine : public wxEvtHandler { +public: + static GameEngine& getInstance(); + + void init(lua_State *L, int cartridgeRef); + void shutdown(); + + void start(); + void stop(); + + void updatePlayerPosition(double lat, double lng, double alt = 0.0); + + // Notify listeners that game state has changed + void notifyStateChanged(); + + lua_State* getLuaState() const { return m_luaState; } + int getCartridgeRef() const { return m_cartridgeRef; } + +private: + GameEngine(); + ~GameEngine(); + + GameEngine(const GameEngine&) = delete; + GameEngine& operator=(const GameEngine&) = delete; + + void onGameTick(wxTimerEvent& event); + void checkTimers(); + void checkZones(); + + lua_State* m_luaState = nullptr; + int m_cartridgeRef = -1; + + wxTimer m_gameTimer; + bool m_running = false; + + double m_playerLat = 0.0; + double m_playerLng = 0.0; + double m_playerAlt = 0.0; + + wxDECLARE_EVENT_TABLE(); +}; + +} // namespace wherigo + +#endif // GAME_ENGINE_H + diff --git a/main/include/lua/media_manager.h b/main/include/lua/media_manager.h new file mode 100644 index 0000000..64b0b2a --- /dev/null +++ b/main/include/lua/media_manager.h @@ -0,0 +1,43 @@ +#ifndef MEDIA_MANAGER_H +#define MEDIA_MANAGER_H + +#include +#include +#include +#include +#include + +extern "C" { +struct lua_State; +} + +namespace wherigo { + +class MediaManager { +public: + static MediaManager& getInstance(); + + void init(lua_State *L, cartridge::Cartridge *cartridge); + + // Get media data by ZMedia object name (e.g., "Title Plate 1") + std::vector getMediaByName(const std::string &name); + + // Get media data by index + std::vector getMediaByIndex(int index); + +private: + MediaManager() = default; + + void buildMediaIndex(lua_State *L); + + lua_State *m_luaState = nullptr; + cartridge::Cartridge *m_cartridge = nullptr; + + // Map from media name to cartridge object index + std::map m_nameToIndex; +}; + +} // namespace wherigo + +#endif // MEDIA_MANAGER_H + diff --git a/main/include/lua/persistence.h b/main/include/lua/persistence.h new file mode 100644 index 0000000..2f84828 --- /dev/null +++ b/main/include/lua/persistence.h @@ -0,0 +1,45 @@ +#ifndef LUA_PERSISTENCE_H +#define LUA_PERSISTENCE_H + +#include +#include + +extern "C" { +struct lua_State; +} + +namespace wherigo { + +class LuaPersistence { +public: + // Save current Lua state to file + static bool saveState(lua_State* L, const std::string& filePath); + + // Load Lua state from file + static bool loadState(lua_State* L, const std::string& filePath); + + // Save specific global variables + static bool saveGlobals(lua_State* L, const std::string& filePath, + const std::vector& globals); + + // Load specific global variables + static bool loadGlobals(lua_State* L, const std::string& filePath); + +private: + // Serialize a Lua value (recursive) + static void serializeValue(lua_State* L, int index, std::string& output, int depth = 0); + + // Deserialize from string back to Lua stack + static bool deserializeValue(lua_State* L, const std::string& input, size_t& pos); + + // Helper to serialize table + static void serializeTable(lua_State* L, int index, std::string& output, int depth); + + // Collect all global variables that should be saved + static std::vector getGameGlobals(lua_State* L); +}; + +} // namespace wherigo + +#endif // LUA_PERSISTENCE_H + diff --git a/main/include/lua/wherigo.h b/main/include/lua/wherigo.h new file mode 100644 index 0000000..45e3999 --- /dev/null +++ b/main/include/lua/wherigo.h @@ -0,0 +1,16 @@ +#ifndef WHERIGO_LUA_H +#define WHERIGO_LUA_H + +extern "C" { +#include +} + +namespace wherigo { + +int luaopen_Wherigo(lua_State *L); +void resetMediaCounter(); + +} + +#endif // WHERIGO_LUA_H + diff --git a/main/include/lua/wherigo_completion.h b/main/include/lua/wherigo_completion.h new file mode 100644 index 0000000..4881b67 --- /dev/null +++ b/main/include/lua/wherigo_completion.h @@ -0,0 +1,66 @@ +#ifndef WHERIGO_COMPLETION_H +#define WHERIGO_COMPLETION_H + +#include +#include +#include + +extern "C" { +struct lua_State; +} + +namespace wherigo { + +struct CompletionData { + std::string cartridgeName; + std::string cartridgeGUID; + std::string playerName; + std::string completionCode; + time_t startTime; + time_t endTime; + int duration; // seconds + + struct TaskCompletion { + std::string name; + bool completed; + time_t completionTime; + }; + std::vector tasks; + + struct ItemFound { + std::string name; + time_t foundTime; + }; + std::vector items; + + struct ZoneVisited { + std::string name; + time_t visitTime; + }; + std::vector zones; +}; + +class WherigoCompletion { +public: + // Generate completion log from current Lua state + static bool generateCompletionLog(lua_State* L, const std::string& filePath, + const std::string& playerName = "Player"); + + // Extract completion data from Lua state + static CompletionData extractCompletionData(lua_State* L, const std::string& playerName); + + // Generate .gwl XML file + static bool writeGWLFile(const CompletionData& data, const std::string& filePath); + + // Generate completion code (hash/signature) + static std::string generateCompletionCode(const CompletionData& data); + +private: + static std::string escapeXML(const std::string& str); + static std::string formatTime(time_t t); +}; + +} // namespace wherigo + +#endif // WHERIGO_COMPLETION_H + diff --git a/main/include/lua/zobject.h b/main/include/lua/zobject.h new file mode 100644 index 0000000..2d532c3 --- /dev/null +++ b/main/include/lua/zobject.h @@ -0,0 +1,16 @@ +#ifndef ZOBJECT_H +#define ZOBJECT_H + +extern "C" { +#include +} + +namespace wherigo { + +int zobject_MoveTo(lua_State *L); +int zobject_Contains(lua_State *L); + +} + +#endif // ZOBJECT_H + diff --git a/main/include/lua/ztimer.h b/main/include/lua/ztimer.h new file mode 100644 index 0000000..b1f64f5 --- /dev/null +++ b/main/include/lua/ztimer.h @@ -0,0 +1,17 @@ +#ifndef ZTIMER_H +#define ZTIMER_H + +extern "C" { +#include +} + +namespace wherigo { + +int ztimer_Start(lua_State *L); +int ztimer_Stop(lua_State *L); +int ztimer_Reset(lua_State *L); + +} + +#endif // ZTIMER_H + diff --git a/main/include/ui/cFrame.h b/main/include/ui/cFrame.h deleted file mode 100644 index d6b0981..0000000 --- a/main/include/ui/cFrame.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include - -class cFrame : public wxMDIParentFrame -{ -public: - cFrame(); - -private: - void OnHello(wxCommandEvent& event); - void OnExit(wxCommandEvent& event); - void OnAbout(wxCommandEvent& event); -}; diff --git a/main/include/ui/game_screen.h b/main/include/ui/game_screen.h new file mode 100644 index 0000000..bebd6a7 --- /dev/null +++ b/main/include/ui/game_screen.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +class cGameScreen : public wxFrame +{ +public: + cGameScreen(wxWindow *parent); + + void refreshUI(); + +private: + void OnClose(wxCloseEvent& event); + void OnExit(wxCommandEvent& event); + void OnAbout(wxCommandEvent& event); + void OnSaveGame(wxCommandEvent& event); + void OnLoadGame(wxCommandEvent& event); + void OnExportCompletion(wxCommandEvent& event); + void OnGameStateChanged(wxEvent& event); + void OnZoneSelected(wxCommandEvent& event); + void OnTaskSelected(wxCommandEvent& event); + void OnInventorySelected(wxCommandEvent& event); + void OnCharacterSelected(wxCommandEvent& event); + void OnItemSelected(wxCommandEvent& event); + + void populateZones(); + void populateTasks(); + void populateInventory(); + void populateCharacters(); + void populateItems(); + + wxNotebook* m_notebook; + wxListBox* m_zoneList; + wxListBox* m_taskList; + wxListBox* m_inventoryList; + wxListBox* m_characterList; + wxListBox* m_itemList; +}; diff --git a/main/include/ui/start_screen.h b/main/include/ui/start_screen.h new file mode 100644 index 0000000..53d78cd --- /dev/null +++ b/main/include/ui/start_screen.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +class cGameScreen; + +class cStartScreen : public wxFrame +{ +public: + cStartScreen(); + + void showCartridgeInfo(); + void onGameClosed(); + +private: + void OnOpenCartridge(wxCommandEvent& event); + void OnStartGame(wxCommandEvent& event); + void OnExit(wxCommandEvent& event); + void OnAbout(wxCommandEvent& event); + + wxPanel* m_infoPanel; + wxStaticText* m_cartridgeName; + wxStaticText* m_cartridgeAuthor; + wxHtmlWindow* m_cartridgeDesc; + wxStaticBitmap* m_splashImage; + wxButton* m_openButton; + wxButton* m_startButton; + + cGameScreen* m_gameFrame; + bool m_cartridgeLoaded; +}; diff --git a/main/include/ui/wherigo_dialog.h b/main/include/ui/wherigo_dialog.h new file mode 100644 index 0000000..95b77a2 --- /dev/null +++ b/main/include/ui/wherigo_dialog.h @@ -0,0 +1,50 @@ +#ifndef WHERIGO_DIALOG_H +#define WHERIGO_DIALOG_H + +#include +#include +#include +#include +#include + +namespace wherigo { + +struct DialogEntry { + std::string text; + std::string mediaName; + std::vector buttons; +}; + +class WherigoMessageDialog : public wxDialog { +public: + WherigoMessageDialog(wxWindow *parent, const wxString &text, + const wxString &title = "Wherigo", + const std::vector &buttons = {}, + const wxString &mediaName = ""); + + int getSelectedButton() const { return m_selectedButton; } + +private: + void onButton(wxCommandEvent &event); + + int m_selectedButton = -1; +}; + +class WherigoDialogRunner { +public: + static WherigoDialogRunner& getInstance(); + + void showMessageBox(const wxString &text, const wxString &title = "Wherigo", + std::function callback = nullptr); + + void showDialog(const std::vector &entries, + std::function callback = nullptr); + +private: + WherigoDialogRunner() = default; +}; + +} // namespace wherigo + +#endif // WHERIGO_DIALOG_H + diff --git a/main/src/app.cpp b/main/src/app.cpp new file mode 100644 index 0000000..a2a9fb7 --- /dev/null +++ b/main/src/app.cpp @@ -0,0 +1,275 @@ +#include "app.h" +#include "lua/game_engine.h" +#include "lua/media_manager.h" +#include "lua/persistence.h" +#include "lua/wherigo.h" +#include "lua/wherigo_completion.h" +#include "ui/start_screen.h" + +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +} + +bool cApp::OnInit() +{ + + // Initialize image handlers for JPEG, PNG, GIF, etc. + wxInitAllImageHandlers(); + + // Show start frame + auto *startFrame = new cStartScreen(); + startFrame->Show(true); + + return true; +} + +bool cApp::loadCartridge(const std::string &filePath) { + // Unload previous cartridge if any + unloadCartridge(); + + m_cartridgePath = filePath; + + // Parse cartridge + m_cartridge = cartridge::parseFile(filePath); + if (!m_cartridge) { + wxLogError("Failed to parse cartridge: %s", filePath); + return false; + } + + return initLuaState(); +} + +bool cApp::initLuaState() { + auto luac = m_cartridge->luac(); + if (!luac) { + wxLogError("No Lua bytecode in cartridge"); + return false; + } + + m_luaState = luaL_newstate(); + if (!m_luaState) { + wxLogError("Failed to create Lua state"); + return false; + } + + luaL_openlibs(m_luaState); + + // Reset media counter before loading cartridge + wherigo::resetMediaCounter(); + + // Register Wherigo module + wherigo::luaopen_Wherigo(m_luaState); + lua_pop(m_luaState, 1); + + const auto &bytecode = luac->getData(); + int result = luaL_loadbuffer(m_luaState, + reinterpret_cast(bytecode.data()), + bytecode.size(), + "cartridge"); + + if (result != 0) { + const char *err = lua_tostring(m_luaState, -1); + wxLogError("Lua load error: %s", err); + lua_close(m_luaState); + m_luaState = nullptr; + return false; + } + + // Execute the loaded chunk + if (lua_pcall(m_luaState, 0, 0, 0) != 0) { + const char *err = lua_tostring(m_luaState, -1); + wxLogError("Lua exec error: %s", err); + lua_close(m_luaState); + m_luaState = nullptr; + return false; + } + + // Find the cartridge object by searching for ClassName = "ZCartridge" + m_cartridgeRef = LUA_NOREF; + 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)) { + const char *className = lua_tostring(m_luaState, -1); + if (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 + break; + } + } + lua_pop(m_luaState, 1); // pop ClassName + } + lua_pop(m_luaState, 1); // pop value + } + lua_pop(m_luaState, 1); // pop _G + + // Initialize media manager + wherigo::MediaManager::getInstance().init(m_luaState, m_cartridge.get()); + + if (m_cartridgeRef == LUA_NOREF) { + wxLogError("No ZCartridge object found!"); + return false; + } + + // Initialize the game engine (but don't start yet) + wherigo::GameEngine::getInstance().init(m_luaState, m_cartridgeRef); + + return true; +} + +void cApp::startGame() { + if (!m_luaState || m_cartridgeRef == LUA_NOREF) { + wxLogError("No cartridge loaded"); + return; + } + + // Call OnStart callback + lua_rawgeti(m_luaState, LUA_REGISTRYINDEX, m_cartridgeRef); + lua_getfield(m_luaState, -1, "OnStart"); + if (lua_isfunction(m_luaState, -1)) { + lua_pushvalue(m_luaState, -2); // push cartridge as self + if (lua_pcall(m_luaState, 1, 0, 0) != 0) { + const char *err = lua_tostring(m_luaState, -1); + wxLogError("OnStart error: %s", err); + lua_pop(m_luaState, 1); + } + } else { + lua_pop(m_luaState, 1); + } + lua_pop(m_luaState, 1); // pop cartridge + + // Start the game engine + wherigo::GameEngine::getInstance().start(); +} + +void cApp::unloadCartridge() { + // Shutdown the game engine + wherigo::GameEngine::getInstance().shutdown(); + + if (m_cartridgeRef != LUA_NOREF && m_luaState) { + luaL_unref(m_luaState, LUA_REGISTRYINDEX, m_cartridgeRef); + m_cartridgeRef = LUA_NOREF; + } + + if (m_luaState) { + lua_close(m_luaState); + m_luaState = nullptr; + } + + m_cartridge.reset(); + m_cartridgePath.clear(); +} + +std::string cApp::getAutoSavePath() const { + if (m_cartridge) { + wxStandardPaths& stdPaths = wxStandardPaths::Get(); + wxString userDataDir = stdPaths.GetUserDataDir(); + + // Create save directory if it doesn't exist + if (!wxDir::Exists(userDataDir)) { + wxFileName::Mkdir(userDataDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL); + } + + // Generate save filename from cartridge name + wxString cartName = wxString::FromUTF8(m_cartridge->cartridgeName()); + cartName.Replace(" ", "_"); + cartName.Replace("/", "_"); + + wxString savePath = userDataDir + wxFileName::GetPathSeparator() + cartName + ".save"; + return savePath.ToStdString(); + } + return ""; +} + +bool cApp::saveGameState(const std::string &saveFilePath) { + if (!m_luaState) { + wxLogError("No Lua state to save"); + return false; + } + + 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); + if (success) { + wxLogMessage("Game saved to: %s", savePath); + } + return success; +} + +bool cApp::loadGameState(const std::string &saveFilePath) { + if (!m_luaState) { + wxLogError("No Lua state to load into"); + return false; + } + + 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); + if (success) { + wxLogMessage("Game loaded from: %s", loadPath); + // Notify game engine of state change + wherigo::GameEngine::getInstance().notifyStateChanged(); + } + return success; +} + +std::string cApp::getCompletionLogPath() const { + if (m_cartridge && !m_cartridgePath.empty()) { + // Use the same directory as the GWC file + wxFileName gwcFile(m_cartridgePath); + wxString directory = gwcFile.GetPath(); + + // Generate completion log filename from cartridge name + wxString cartName = wxString::FromUTF8(m_cartridge->cartridgeName()); + cartName.Replace(" ", "_"); + cartName.Replace("/", "_"); + + wxString logPath = directory + wxFileName::GetPathSeparator() + cartName + ".gwl"; + return logPath.ToStdString(); + } + return ""; +} + +bool cApp::generateCompletionLog(const std::string &logFilePath) { + if (!m_luaState) { + wxLogError("No Lua state for completion log"); + return false; + } + + 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"); + if (success) { + wxLogMessage("Completion log generated: %s", logPath); + } + return success; +} + +int cApp::OnExit() +{ + unloadCartridge(); + return wxApp::OnExit(); +} + diff --git a/main/src/cApp.cpp b/main/src/cApp.cpp deleted file mode 100644 index b86e218..0000000 --- a/main/src/cApp.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "cApp.h" -#include "ui/cFrame.h" - -#include - -bool cApp::OnInit() -{ - auto cart = cartridge::parseCartridge(); - auto *frame = new cFrame(); - return frame->Show(true); -} diff --git a/main/src/lua/game_engine.cpp b/main/src/lua/game_engine.cpp new file mode 100644 index 0000000..3e0c0a2 --- /dev/null +++ b/main/src/lua/game_engine.cpp @@ -0,0 +1,215 @@ +#include "lua/game_engine.h" + +extern "C" { +#include +#include +} + +#include + +namespace wherigo { + +// Define the custom event +wxDEFINE_EVENT(EVT_GAME_STATE_CHANGED, GameStateEvent); + +wxBEGIN_EVENT_TABLE(GameEngine, wxEvtHandler) + EVT_TIMER(wxID_ANY, GameEngine::onGameTick) +wxEND_EVENT_TABLE() + +GameEngine& GameEngine::getInstance() { + static GameEngine instance; + return instance; +} + +GameEngine::GameEngine() : m_gameTimer(this) { +} + +GameEngine::~GameEngine() { + shutdown(); +} + +void GameEngine::init(lua_State *L, int cartridgeRef) { + m_luaState = L; + m_cartridgeRef = cartridgeRef; + wxLogDebug("GameEngine initialized"); +} + +void GameEngine::shutdown() { + stop(); + m_luaState = nullptr; + m_cartridgeRef = -1; +} + +void GameEngine::start() { + if (m_running) return; + + m_running = true; + m_gameTimer.Start(1000); // 1 second tick + wxLogDebug("GameEngine started"); +} + +void GameEngine::stop() { + if (!m_running) return; + + m_gameTimer.Stop(); + m_running = false; + wxLogDebug("GameEngine stopped"); +} + +void GameEngine::updatePlayerPosition(double lat, double lng, double alt) { + m_playerLat = lat; + m_playerLng = lng; + m_playerAlt = alt; + + if (!m_luaState) return; + + // Update Player.ObjectLocation in Lua + 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_setfield(m_luaState, -2, "ObjectLocation"); + } + lua_pop(m_luaState, 1); + + checkZones(); +} + +void GameEngine::onGameTick(wxTimerEvent& event) { + if (!m_luaState || !m_running) return; + + checkTimers(); + + // Notify listeners of potential state changes + notifyStateChanged(); +} + +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 + + lua_getfield(m_luaState, -1, "Running"); + 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)"; + 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; + 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; + 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 + if (lua_pcall(m_luaState, 1, 0, 0) != 0) { + 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 + if (lua_pcall(m_luaState, 1, 0, 0) != 0) { + wxLogError("Timer OnElapsed error: %s", lua_tostring(m_luaState, -1)); + lua_pop(m_luaState, 1); + } + } else { + lua_pop(m_luaState, 1); + } + } + } + } else { + lua_pop(m_luaState, 1); // pop ClassName + } + } + lua_pop(m_luaState, 1); // pop value + } + lua_pop(m_luaState, 1); // pop _G +} + +void GameEngine::checkZones() { + if (!m_luaState) return; + + // Iterate through all global variables to find zones + 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 + + lua_getfield(m_luaState, -1, "Active"); + bool active = lua_toboolean(m_luaState, -1); + 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)"; + lua_pop(m_luaState, 1); + + // This is where you would implement point-in-polygon check + // and call OnEnter/OnExit callbacks + } + } else { + lua_pop(m_luaState, 1); // pop ClassName + } + } + lua_pop(m_luaState, 1); // pop value + } + lua_pop(m_luaState, 1); // pop _G +} + +void GameEngine::notifyStateChanged() { + GameStateEvent event(EVT_GAME_STATE_CHANGED); + ProcessEvent(event); +} + +} // namespace wherigo + diff --git a/main/src/lua/media_manager.cpp b/main/src/lua/media_manager.cpp new file mode 100644 index 0000000..e2e933c --- /dev/null +++ b/main/src/lua/media_manager.cpp @@ -0,0 +1,97 @@ +#include "lua/media_manager.h" + +extern "C" { +#include +#include +} + +#include + +namespace wherigo { + +MediaManager& MediaManager::getInstance() { + static MediaManager instance; + return instance; +} + +void MediaManager::init(lua_State *L, cartridge::Cartridge *cartridge) { + m_luaState = L; + m_cartridge = cartridge; + + buildMediaIndex(L); + + wxLogDebug("MediaManager initialized with %zu media entries", m_nameToIndex.size()); +} + +void MediaManager::buildMediaIndex(lua_State *L) { + m_nameToIndex.clear(); + + if (!L || !m_cartridge) return; + + // Iterate through all globals to find ZMedia objects + 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), "ZMedia") == 0) { + lua_pop(L, 1); // pop ClassName + + std::string name; + int mediaIndex = -1; + + // Get the Name + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + name = lua_tostring(L, -1); + } + lua_pop(L, 1); // pop Name + + // Get the MediaIndex (set by wherigo_ZMedia when the object was created) + lua_getfield(L, -1, "MediaIndex"); + if (lua_isnumber(L, -1)) { + mediaIndex = lua_tointeger(L, -1); + } + lua_pop(L, 1); // pop MediaIndex + + if (!name.empty() && mediaIndex > 0) { + m_nameToIndex[name] = mediaIndex; + wxLogDebug("Media '%s' -> index %d", name.c_str(), mediaIndex); + } + } else { + lua_pop(L, 1); // pop ClassName + } + } + lua_pop(L, 1); // pop value + } + lua_pop(L, 1); // pop _G +} + +std::vector MediaManager::getMediaByName(const std::string &name) { + auto it = m_nameToIndex.find(name); + if (it == m_nameToIndex.end()) { + wxLogWarning("Media not found: %s", name.c_str()); + return {}; + } + + return getMediaByIndex(it->second); +} + +std::vector MediaManager::getMediaByIndex(int index) { + if (!m_cartridge) { + wxLogError("MediaManager: No cartridge set"); + return {}; + } + + auto media = m_cartridge->getMedia(index); + if (!media) { + wxLogWarning("Media index %d not found in cartridge", index); + return {}; + } + + return media->getData(); +} + +} // namespace wherigo + diff --git a/main/src/lua/persistence.cpp b/main/src/lua/persistence.cpp new file mode 100644 index 0000000..fa8ee85 --- /dev/null +++ b/main/src/lua/persistence.cpp @@ -0,0 +1,228 @@ +#include "lua/persistence.h" + +extern "C" { +#include +#include +} + +#include +#include +#include +#include + +namespace wherigo { + +// List of Wherigo game object classes to save +static const std::set WHERIGO_CLASSES = { + "ZCartridge", "Zone", "ZItem", "ZCharacter", + "ZTask", "ZTimer", "ZInput", "ZMedia" +}; + +std::vector LuaPersistence::getGameGlobals(lua_State* L) { + std::vector globals; + + lua_getglobal(L, "_G"); + lua_pushnil(L); + + while (lua_next(L, -2) != 0) { + if (lua_isstring(L, -2)) { + const char* key = lua_tostring(L, -2); + + // Check if it's a Wherigo game object + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "ClassName"); + if (lua_isstring(L, -1)) { + std::string className = lua_tostring(L, -1); + if (WHERIGO_CLASSES.count(className) > 0) { + globals.push_back(key); + } + } + lua_pop(L, 1); // pop ClassName + } + // Also save simple global variables (strings, numbers, booleans) + else if (lua_isnumber(L, -1) || lua_isstring(L, -1) || lua_isboolean(L, -1)) { + std::string keyStr = key; + // Skip internal Lua variables + if (keyStr != "_G" && keyStr != "_VERSION" && keyStr.find("_") != 0) { + globals.push_back(key); + } + } + } + lua_pop(L, 1); // pop value + } + lua_pop(L, 1); // pop _G + + // Always include Player object + globals.push_back("Player"); + + return globals; +} + +void LuaPersistence::serializeValue(lua_State* L, int index, std::string& output, int depth) { + if (depth > 10) { + output += "nil"; // Prevent infinite recursion + return; + } + + int type = lua_type(L, index); + + switch (type) { + case LUA_TNIL: + output += "nil"; + break; + + case LUA_TBOOLEAN: + output += lua_toboolean(L, index) ? "true" : "false"; + break; + + case LUA_TNUMBER: + output += std::to_string(lua_tonumber(L, index)); + break; + + case LUA_TSTRING: { + const char* str = lua_tostring(L, index); + output += "\""; + // Escape special characters + for (const char* p = str; *p; ++p) { + if (*p == '"' || *p == '\\') output += '\\'; + output += *p; + } + output += "\""; + break; + } + + case LUA_TTABLE: + serializeTable(L, index, output, depth); + break; + + default: + // Functions, userdata, threads -> save as nil + output += "nil"; + break; + } +} + +void LuaPersistence::serializeTable(lua_State* L, int index, std::string& output, int depth) { + output += "{"; + + // Normalize stack index + if (index < 0) index = lua_gettop(L) + index + 1; + + bool first = true; + lua_pushnil(L); + + while (lua_next(L, index) != 0) { + if (!first) output += ","; + first = false; + + // Serialize key + output += "["; + serializeValue(L, -2, output, depth + 1); + output += "]="; + + // Serialize value + serializeValue(L, -1, output, depth + 1); + + lua_pop(L, 1); // pop value, keep key for next iteration + } + + output += "}"; +} + +bool LuaPersistence::saveGlobals(lua_State* L, const std::string& filePath, + const std::vector& globals) { + std::string output = "-- Wherigo Save State\nreturn {\n"; + + for (const auto& globalName : globals) { + lua_getglobal(L, globalName.c_str()); + + if (!lua_isnil(L, -1)) { + output += " [\"" + globalName + "\"] = "; + serializeValue(L, -1, output, 0); + output += ",\n"; + } + + lua_pop(L, 1); + } + + output += "}\n"; + + // Write to file + std::ofstream file(filePath); + if (!file.is_open()) { + wxLogError("Failed to open save file: %s", filePath); + return false; + } + + file << output; + file.close(); + + wxLogDebug("Saved %zu globals to %s", globals.size(), filePath); + return true; +} + +bool LuaPersistence::saveState(lua_State* L, const std::string& filePath) { + auto globals = getGameGlobals(L); + return saveGlobals(L, filePath, globals); +} + +bool LuaPersistence::loadGlobals(lua_State* L, const std::string& filePath) { + // Load the file + std::ifstream file(filePath); + if (!file.is_open()) { + wxLogError("Failed to open load file: %s", filePath); + return false; + } + + std::stringstream buffer; + buffer << file.rdbuf(); + std::string content = buffer.str(); + file.close(); + + // Execute the Lua file + if (luaL_loadstring(L, content.c_str()) != 0) { + wxLogError("Failed to parse save file: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + return false; + } + + if (lua_pcall(L, 0, 1, 0) != 0) { + wxLogError("Failed to execute save file: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + return false; + } + + // Result should be a table + if (!lua_istable(L, -1)) { + wxLogError("Save file did not return a table"); + lua_pop(L, 1); + return false; + } + + // Restore globals + int count = 0; + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + if (lua_isstring(L, -2)) { + const char* key = lua_tostring(L, -2); + + // Set the global + lua_pushvalue(L, -1); // duplicate value + lua_setglobal(L, key); + count++; + } + lua_pop(L, 1); // pop value + } + + lua_pop(L, 1); // pop table + + wxLogDebug("Restored %d globals from %s", count, filePath); + return true; +} + +bool LuaPersistence::loadState(lua_State* L, const std::string& filePath) { + return loadGlobals(L, filePath); +} + +} // namespace wherigo + diff --git a/main/src/lua/wherigo.cpp b/main/src/lua/wherigo.cpp new file mode 100644 index 0000000..cbef1aa --- /dev/null +++ b/main/src/lua/wherigo.cpp @@ -0,0 +1,512 @@ +#include "lua/wherigo.h" +#include "lua/zobject.h" +#include "lua/ztimer.h" +#include "ui/wherigo_dialog.h" + +extern "C" { +#include +#include +} + +#include + +#include +#include +#include +#include + +namespace wherigo { + + +// Wherigo.ZonePoint(lat, lng, alt) +static int wherigo_ZonePoint(lua_State *L) { + lua_Number lat = luaL_checknumber(L, 1); + lua_Number lng = luaL_checknumber(L, 2); + lua_Number alt = luaL_optnumber(L, 3, 0.0); + + lua_newtable(L); + lua_pushnumber(L, lat); + lua_setfield(L, -2, "latitude"); + lua_pushnumber(L, lng); + lua_setfield(L, -2, "longitude"); + lua_pushnumber(L, alt); + lua_setfield(L, -2, "altitude"); + + return 1; +} + +// Wherigo.Distance(value, unit) +static int wherigo_Distance(lua_State *L) { + lua_Number value = luaL_checknumber(L, 1); + const char *unit = luaL_optstring(L, 2, "meters"); + + lua_newtable(L); + lua_pushnumber(L, value); + lua_setfield(L, -2, "value"); + lua_pushstring(L, unit); + lua_setfield(L, -2, "unit"); + + return 1; +} + +// Wherigo.Zone(cartridge) +static int wherigo_Zone(lua_State *L) { + lua_newtable(L); + + lua_pushstring(L, "Zone"); + lua_setfield(L, -2, "ClassName"); + + lua_pushboolean(L, 0); + lua_setfield(L, -2, "Active"); + + lua_pushboolean(L, 1); + lua_setfield(L, -2, "Visible"); + + lua_newtable(L); + lua_setfield(L, -2, "Points"); + + lua_pushcfunction(L, zobject_Contains); + lua_setfield(L, -2, "Contains"); + + return 1; +} + +// Wherigo.ZCharacter(cartridge) +static int wherigo_ZCharacter(lua_State *L) { + lua_newtable(L); + + lua_pushstring(L, "ZCharacter"); + lua_setfield(L, -2, "ClassName"); + + lua_pushboolean(L, 0); + lua_setfield(L, -2, "Active"); + + lua_pushboolean(L, 1); + lua_setfield(L, -2, "Visible"); + + lua_pushcfunction(L, zobject_MoveTo); + lua_setfield(L, -2, "MoveTo"); + + lua_pushcfunction(L, zobject_Contains); + lua_setfield(L, -2, "Contains"); + + return 1; +} + +// Wherigo.ZItem(cartridge) +static int wherigo_ZItem(lua_State *L) { + lua_newtable(L); + + lua_pushstring(L, "ZItem"); + lua_setfield(L, -2, "ClassName"); + + lua_pushboolean(L, 0); + lua_setfield(L, -2, "Active"); + + lua_pushboolean(L, 1); + lua_setfield(L, -2, "Visible"); + + lua_pushcfunction(L, zobject_MoveTo); + lua_setfield(L, -2, "MoveTo"); + + lua_pushcfunction(L, zobject_Contains); + lua_setfield(L, -2, "Contains"); + + return 1; +} + +// Wherigo.ZTask(cartridge) +static int wherigo_ZTask(lua_State *L) { + lua_newtable(L); + + lua_pushstring(L, "ZTask"); + lua_setfield(L, -2, "ClassName"); + + lua_pushboolean(L, 0); + lua_setfield(L, -2, "Active"); + + lua_pushboolean(L, 0); + lua_setfield(L, -2, "Complete"); + + return 1; +} + + +// Wherigo.ZTimer(cartridge) +static int wherigo_ZTimer(lua_State *L) { + lua_newtable(L); + + lua_pushstring(L, "ZTimer"); + lua_setfield(L, -2, "ClassName"); + + lua_pushstring(L, "Countdown"); + lua_setfield(L, -2, "Type"); + + lua_pushnumber(L, 0); + lua_setfield(L, -2, "Duration"); + + lua_pushboolean(L, 0); + lua_setfield(L, -2, "Running"); + + lua_pushcfunction(L, ztimer_Start); + lua_setfield(L, -2, "Start"); + + lua_pushcfunction(L, ztimer_Stop); + lua_setfield(L, -2, "Stop"); + + lua_pushcfunction(L, ztimer_Reset); + lua_setfield(L, -2, "Reset"); + + return 1; +} + +// Wherigo.ZInput(cartridge) +static int wherigo_ZInput(lua_State *L) { + lua_newtable(L); + + lua_pushstring(L, "ZInput"); + lua_setfield(L, -2, "ClassName"); + + lua_pushstring(L, "Text"); + lua_setfield(L, -2, "InputType"); + + return 1; +} + +// Wherigo.ZMedia(cartridge) +static int s_mediaCounter = 0; + +static int wherigo_ZMedia(lua_State *L) { + lua_newtable(L); + + lua_pushstring(L, "ZMedia"); + lua_setfield(L, -2, "ClassName"); + + // Store the media index (1-based, 0 is luac) + s_mediaCounter++; + lua_pushinteger(L, s_mediaCounter); + lua_setfield(L, -2, "MediaIndex"); + + return 1; +} + +void resetMediaCounter() { + s_mediaCounter = 0; +} + +// Wherigo.ZCartridge() +static int wherigo_ZCartridge(lua_State *L) { + lua_newtable(L); + + lua_pushstring(L, "ZCartridge"); + lua_setfield(L, -2, "ClassName"); + + lua_newtable(L); + lua_setfield(L, -2, "AllZObjects"); + + return 1; +} + +// Wherigo.ZCommand(cartridge) +static int wherigo_ZCommand(lua_State *L) { + lua_newtable(L); + + lua_pushstring(L, "ZCommand"); + lua_setfield(L, -2, "ClassName"); + + lua_pushboolean(L, 1); + lua_setfield(L, -2, "Enabled"); + + return 1; +} + +// Wherigo.Player +static int wherigo_Player(lua_State *L) { + lua_newtable(L); + + lua_pushstring(L, "Player"); + lua_setfield(L, -2, "ClassName"); + + lua_pushstring(L, "Player"); + lua_setfield(L, -2, "Name"); + + lua_newtable(L); + lua_setfield(L, -2, "Inventory"); + + return 1; +} + +// Wherigo.MessageBox(table) +static int wherigo_MessageBox(lua_State *L) { + if (!lua_istable(L, 1)) return 0; + + // Get Text + lua_getfield(L, 1, "Text"); + const char *text = lua_isstring(L, -1) ? lua_tostring(L, -1) : ""; + lua_pop(L, 1); + + // Get Buttons + std::vector buttons; + lua_getfield(L, 1, "Buttons"); + if (lua_istable(L, -1)) { + int n = lua_objlen(L, -1); + for (int i = 1; i <= n; i++) { + lua_rawgeti(L, -1, i); + if (lua_isstring(L, -1)) { + buttons.push_back(wxString::FromUTF8(lua_tostring(L, -1))); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + // Get Callback + lua_getfield(L, 1, "Callback"); + bool hasCallback = lua_isfunction(L, -1); + int callbackRef = LUA_NOREF; + if (hasCallback) { + callbackRef = luaL_ref(L, LUA_REGISTRYINDEX); + } else { + lua_pop(L, 1); + } + + wxLogDebug("MessageBox: %s", text); + + // Show dialog + WherigoMessageDialog dlg(nullptr, wxString::FromUTF8(text), "Wherigo", buttons); + dlg.ShowModal(); + int selected = dlg.getSelectedButton(); + + // Call callback if exists + if (callbackRef != LUA_NOREF) { + lua_rawgeti(L, LUA_REGISTRYINDEX, callbackRef); + if (buttons.empty()) { + lua_pushnil(L); + } else if (selected >= 0 && selected < (int)buttons.size()) { + lua_pushstring(L, buttons[selected].ToUTF8().data()); + } else { + lua_pushnil(L); + } + if (lua_pcall(L, 1, 0, 0) != 0) { + wxLogError("MessageBox callback error: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + } + luaL_unref(L, LUA_REGISTRYINDEX, callbackRef); + } + + return 0; +} + +// Wherigo.Dialog(table) +static int wherigo_Dialog(lua_State *L) { + if (!lua_istable(L, 1)) return 0; + + std::vector entries; + int n = lua_objlen(L, 1); + + for (int i = 1; i <= n; i++) { + lua_rawgeti(L, 1, i); + if (lua_istable(L, -1)) { + DialogEntry entry; + + lua_getfield(L, -1, "Text"); + entry.text = lua_isstring(L, -1) ? lua_tostring(L, -1) : ""; + lua_pop(L, 1); + + // Get Media object name + lua_getfield(L, -1, "Media"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "Name"); + entry.mediaName = lua_isstring(L, -1) ? lua_tostring(L, -1) : ""; + lua_pop(L, 1); + } + lua_pop(L, 1); + + lua_getfield(L, -1, "Buttons"); + if (lua_istable(L, -1)) { + int bn = lua_objlen(L, -1); + for (int j = 1; j <= bn; j++) { + lua_rawgeti(L, -1, j); + if (lua_isstring(L, -1)) { + entry.buttons.push_back(lua_tostring(L, -1)); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + entries.push_back(entry); + } + lua_pop(L, 1); + } + + wxLogDebug("Dialog with %zu entries", entries.size()); + for (const auto& entry : entries) { + wxLogDebug(" Text: %s, Media: %s", entry.text.c_str(), + entry.mediaName.empty() ? "(none)" : entry.mediaName.c_str()); + } + + // Show dialogs sequentially + WherigoDialogRunner::getInstance().showDialog(entries); + + return 0; +} + +// Wherigo.GetInput(zinput) +static int wherigo_GetInput(lua_State *L) { + if (lua_istable(L, 1)) { + lua_getfield(L, 1, "Name"); + const char *name = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unnamed)"; + wxLogDebug("GetInput: %s", name); + lua_pop(L, 1); + } + return 0; +} + +// Wherigo.PlayAudio(zmedia) +static int wherigo_PlayAudio(lua_State *L) { + if (lua_istable(L, 1)) { + lua_getfield(L, 1, "Name"); + const char *name = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unnamed)"; + wxLogDebug("PlayAudio: %s", name); + lua_pop(L, 1); + } + return 0; +} + +// Wherigo.ShowScreen(screen, obj) +static int wherigo_ShowScreen(lua_State *L) { + int screen = luaL_optinteger(L, 1, 0); + wxLogDebug("ShowScreen: %d", screen); + return 0; +} + +// Wherigo.LogMessage(message, level) +static int wherigo_LogMessage(lua_State *L) { + const char *message = luaL_checkstring(L, 1); + wxLogDebug("Wherigo.LogMessage: %s", message); + return 0; +} + +// Wherigo.NoCaseEquals(str1, str2) +static int wherigo_NoCaseEquals(lua_State *L) { + const char *str1 = luaL_checkstring(L, 1); + const char *str2 = luaL_checkstring(L, 2); + + std::string s1(str1); + std::string s2(str2); + + std::transform(s1.begin(), s1.end(), s1.begin(), ::tolower); + std::transform(s2.begin(), s2.end(), s2.begin(), ::tolower); + + lua_pushboolean(L, s1 == s2); + return 1; +} + +// Wherigo.TranslatePoint(point, distance, bearing) +static int wherigo_TranslatePoint(lua_State *L) { + // TODO: implement point translation + lua_newtable(L); + return 1; +} + +// Wherigo.VectorToZone(point, zone) +static int wherigo_VectorToZone(lua_State *L) { + lua_newtable(L); + lua_pushnumber(L, 0); + lua_setfield(L, -2, "distance"); + lua_pushnumber(L, 0); + lua_setfield(L, -2, "bearing"); + lua_pushboolean(L, 0); + lua_setfield(L, -2, "inside"); + return 1; +} + +// Wherigo.VectorToPoint(point1, point2) +static int wherigo_VectorToPoint(lua_State *L) { + lua_newtable(L); + lua_pushnumber(L, 0); + lua_setfield(L, -2, "distance"); + lua_pushnumber(L, 0); + lua_setfield(L, -2, "bearing"); + return 1; +} + +// Wherigo.VectorToSegment(point, point1, point2) +static int wherigo_VectorToSegment(lua_State *L) { + lua_newtable(L); + lua_pushnumber(L, 0); + lua_setfield(L, -2, "distance"); + lua_pushnumber(L, 0); + lua_setfield(L, -2, "bearing"); + return 1; +} + +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} +}; + +int luaopen_Wherigo(lua_State *L) { + luaL_register(L, "Wherigo", wherigo_funcs); + + // Create Player object + wherigo_Player(L); + lua_setfield(L, -2, "Player"); + + // Screen constants + lua_pushnumber(L, 0); + lua_setfield(L, -2, "MAINSCREEN"); + + lua_pushnumber(L, 1); + lua_setfield(L, -2, "LOCATIONSCREEN"); + + lua_pushnumber(L, 2); + lua_setfield(L, -2, "ITEMSCREEN"); + + lua_pushnumber(L, 3); + lua_setfield(L, -2, "INVENTORYSCREEN"); + + lua_pushnumber(L, 4); + lua_setfield(L, -2, "TASKSCREEN"); + + lua_pushnumber(L, 5); + lua_setfield(L, -2, "DETAILSCREEN"); + + // Register common functions as globals (as expected by Wherigo Builder) + lua_pushcfunction(L, wherigo_ZonePoint); + lua_setglobal(L, "ZonePoint"); + + lua_pushcfunction(L, wherigo_Distance); + lua_setglobal(L, "Distance"); + + // Register Player as global + wherigo_Player(L); + lua_setglobal(L, "Player"); + + return 1; +} + +} // namespace wherigo + diff --git a/main/src/lua/wherigo_completion.cpp b/main/src/lua/wherigo_completion.cpp new file mode 100644 index 0000000..02a540e --- /dev/null +++ b/main/src/lua/wherigo_completion.cpp @@ -0,0 +1,281 @@ +#include "lua/wherigo_completion.h" + +extern "C" { +#include +#include +} + +#include +#include +#include +#include +#include + +namespace wherigo { + +std::string WherigoCompletion::escapeXML(const std::string& str) { + std::string result; + for (char c : str) { + switch (c) { + case '&': result += "&"; break; + case '<': result += "<"; break; + case '>': result += ">"; break; + case '"': result += """; break; + case '\'': result += "'"; break; + default: result += c; + } + } + return result; +} + +std::string WherigoCompletion::formatTime(time_t t) { + struct tm* timeinfo = localtime(&t); + char buffer[30]; + strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%S", timeinfo); + return std::string(buffer); +} + +std::string WherigoCompletion::generateCompletionCode(const CompletionData& data) { + // Generate a simple hash based on cartridge GUID + player + completion time + std::stringstream ss; + ss << data.cartridgeGUID << data.playerName << data.endTime; + std::string input = ss.str(); + + // Simple hash (for real use, use proper crypto hash like SHA256) + unsigned long hash = 5381; + for (char c : input) { + hash = ((hash << 5) + hash) + c; + } + + char hashStr[20]; + snprintf(hashStr, sizeof(hashStr), "%016lX", hash); + return std::string(hashStr); +} + +CompletionData WherigoCompletion::extractCompletionData(lua_State* L, const std::string& playerName) { + CompletionData data; + data.playerName = playerName; + data.startTime = time(nullptr); // Default to now + data.endTime = time(nullptr); + data.duration = 0; + + // Get cartridge info + 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)) { + std::string className = lua_tostring(L, -1); + + if (className == "ZCartridge") { + lua_pop(L, 1); // pop ClassName + + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + data.cartridgeName = lua_tostring(L, -1); + } + lua_pop(L, 1); + + lua_getfield(L, -1, "Id"); + if (lua_isstring(L, -1)) { + data.cartridgeGUID = lua_tostring(L, -1); + } + lua_pop(L, 1); + } + else if (className == "ZTask") { + lua_pop(L, 1); // pop ClassName + + CompletionData::TaskCompletion task; + + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + task.name = lua_tostring(L, -1); + } + lua_pop(L, 1); + + lua_getfield(L, -1, "Complete"); + task.completed = lua_toboolean(L, -1); + lua_pop(L, 1); + + task.completionTime = time(nullptr); + data.tasks.push_back(task); + } + else if (className == "Zone") { + lua_pop(L, 1); // pop ClassName + + lua_getfield(L, -1, "Active"); + bool active = lua_toboolean(L, -1); + lua_pop(L, 1); + + if (active) { + CompletionData::ZoneVisited zone; + + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + zone.name = lua_tostring(L, -1); + } + lua_pop(L, 1); + + zone.visitTime = time(nullptr); + data.zones.push_back(zone); + } + } + else { + lua_pop(L, 1); // pop ClassName + } + } else { + lua_pop(L, 1); // pop ClassName + } + } + lua_pop(L, 1); // pop value + } + lua_pop(L, 1); // pop _G + + // Get items in player inventory + lua_getglobal(L, "Player"); + if (lua_istable(L, -1)) { + // Find all items where Container == Player + 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), "ZItem") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Container"); + if (lua_istable(L, -1)) { + lua_getglobal(L, "Player"); + bool inInventory = lua_rawequal(L, -1, -2); + lua_pop(L, 2); // pop Player and Container + + if (inInventory) { + CompletionData::ItemFound item; + + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + item.name = lua_tostring(L, -1); + } + lua_pop(L, 1); + + item.foundTime = time(nullptr); + data.items.push_back(item); + } + } else { + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); // pop _G + } + lua_pop(L, 1); // pop Player + + // Generate completion code + data.completionCode = generateCompletionCode(data); + + return data; +} + +bool WherigoCompletion::writeGWLFile(const CompletionData& data, const std::string& filePath) { + std::ofstream file(filePath); + if (!file.is_open()) { + wxLogError("Failed to create completion log: %s", filePath); + return false; + } + + // Write XML header + file << "\n"; + file << "\n"; + + // Cartridge info + file << " \n"; + file << " " << escapeXML(data.cartridgeName) << "\n"; + file << " " << escapeXML(data.cartridgeGUID) << "\n"; + file << " \n"; + + // Player info + file << " \n"; + file << " " << escapeXML(data.playerName) << "\n"; + file << " \n"; + + // Session info + file << " \n"; + file << " " << formatTime(data.startTime) << "\n"; + file << " " << formatTime(data.endTime) << "\n"; + file << " " << data.duration << "\n"; + file << " \n"; + + // Completion status + bool allTasksComplete = std::all_of(data.tasks.begin(), data.tasks.end(), + [](const CompletionData::TaskCompletion& t) { return t.completed; }); + file << " " << (allTasksComplete ? "Complete" : "Incomplete") << "\n"; + + // Tasks + if (!data.tasks.empty()) { + file << " \n"; + for (const auto& task : data.tasks) { + file << " \n"; + file << " " << escapeXML(task.name) << "\n"; + file << " " << (task.completed ? "true" : "false") << "\n"; + if (task.completed) { + file << " " << formatTime(task.completionTime) << "\n"; + } + file << " \n"; + } + file << " \n"; + } + + // Items collected + if (!data.items.empty()) { + file << " \n"; + for (const auto& item : data.items) { + file << " \n"; + file << " " << escapeXML(item.name) << "\n"; + file << " " << formatTime(item.foundTime) << "\n"; + file << " \n"; + } + file << " \n"; + } + + // Zones visited + if (!data.zones.empty()) { + file << " \n"; + for (const auto& zone : data.zones) { + file << " \n"; + file << " " << escapeXML(zone.name) << "\n"; + file << " " << formatTime(zone.visitTime) << "\n"; + file << " \n"; + } + file << " \n"; + } + + // Completion code (signature/verification) + file << " " << data.completionCode << "\n"; + + file << "\n"; + file.close(); + + wxLogMessage("Completion log saved to: %s", filePath); + return true; +} + +bool WherigoCompletion::generateCompletionLog(lua_State* L, const std::string& filePath, + const std::string& playerName) { + if (!L) { + wxLogError("Invalid Lua state"); + return false; + } + + CompletionData data = extractCompletionData(L, playerName); + return writeGWLFile(data, filePath); +} + +} // namespace wherigo + diff --git a/main/src/lua/zobject.cpp b/main/src/lua/zobject.cpp new file mode 100644 index 0000000..1208f13 --- /dev/null +++ b/main/src/lua/zobject.cpp @@ -0,0 +1,64 @@ +#include "lua/zobject.h" +#include "lua/game_engine.h" + +extern "C" { +#include +} + +#include + +namespace wherigo { + +int zobject_MoveTo(lua_State *L) { + // Get source object name + lua_getfield(L, 1, "Name"); + const char *srcName = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unknown)"; + lua_pop(L, 1); + + // Get target container name + const char *dstName = "(nil)"; + if (lua_istable(L, 2)) { + lua_getfield(L, 2, "Name"); + dstName = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unknown)"; + lua_pop(L, 1); + + lua_pushvalue(L, 2); + lua_setfield(L, 1, "Container"); + } + + wxLogDebug("MoveTo: %s -> %s", srcName, dstName); + + // Notify game state change + GameEngine::getInstance().notifyStateChanged(); + + return 0; +} + +int zobject_Contains(lua_State *L) { + // self (container) at index 1, item to check at index 2 + + lua_getfield(L, 1, "Name"); + const char *containerName = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unknown)"; + lua_pop(L, 1); + + lua_getfield(L, 2, "Name"); + const char *itemName = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unknown)"; + lua_pop(L, 1); + + // Check if item's Container is the same as self (container) + bool contains = false; + + lua_getfield(L, 2, "Container"); // Get item.Container + if (lua_istable(L, -1)) { + // Compare by reference - check if it's the same table as self + contains = lua_rawequal(L, 1, -1); + } + lua_pop(L, 1); // pop Container + + wxLogDebug("Contains: %s in %s? -> %s", itemName, containerName, contains ? "true" : "false"); + lua_pushboolean(L, contains ? 1 : 0); + return 1; +} + +} // namespace wherigo + diff --git a/main/src/lua/ztimer.cpp b/main/src/lua/ztimer.cpp new file mode 100644 index 0000000..ae02f62 --- /dev/null +++ b/main/src/lua/ztimer.cpp @@ -0,0 +1,62 @@ +#include "lua/ztimer.h" +#include "lua/game_engine.h" + +extern "C" { +#include +} + +#include + +namespace wherigo { + +int ztimer_Start(lua_State *L) { + lua_getfield(L, 1, "Name"); + const char *name = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unnamed)"; + lua_pop(L, 1); + + lua_getfield(L, 1, "Duration"); + lua_Number duration = lua_isnumber(L, -1) ? lua_tonumber(L, -1) : 0; + lua_pop(L, 1); + + wxLogDebug("Timer Start: %s (Duration: %.1f sec)", name, duration); + + lua_pushboolean(L, 1); + lua_setfield(L, 1, "Running"); + + GameEngine::getInstance().notifyStateChanged(); + + return 0; +} + +int ztimer_Stop(lua_State *L) { + lua_getfield(L, 1, "Name"); + const char *name = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unnamed)"; + lua_pop(L, 1); + + wxLogDebug("Timer Stop: %s", name); + + lua_pushboolean(L, 0); + lua_setfield(L, 1, "Running"); + + GameEngine::getInstance().notifyStateChanged(); + + return 0; +} + +int ztimer_Reset(lua_State *L) { + lua_getfield(L, 1, "Name"); + const char *name = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unnamed)"; + lua_pop(L, 1); + + wxLogDebug("Timer Reset: %s", name); + + lua_pushboolean(L, 0); + lua_setfield(L, 1, "Running"); + + GameEngine::getInstance().notifyStateChanged(); + + return 0; +} + +} // namespace wherigo + diff --git a/main/src/main.cpp b/main/src/main.cpp index b00b67d..69d3832 100644 --- a/main/src/main.cpp +++ b/main/src/main.cpp @@ -1,4 +1,4 @@ -#include "cApp.h" +#include "app.h" #include wxIMPLEMENT_APP(cApp); diff --git a/main/src/ui/cFrame.cpp b/main/src/ui/cFrame.cpp deleted file mode 100644 index c1f09b8..0000000 --- a/main/src/ui/cFrame.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "ui/cFrame.h" - -enum { ID_Hello = 1 }; - -cFrame::cFrame() : wxMDIParentFrame(nullptr, wxID_ANY, "Hello World") { - auto *menuFile = new wxMenu; - menuFile->Append(ID_Hello, "&Hello...\tCtrl-H", - "Help string shown in status bar for this menu item"); - menuFile->AppendSeparator(); - menuFile->Append(wxID_EXIT); - - auto *menuHelp = new wxMenu; - menuHelp->Append(wxID_ABOUT); - - auto *menuBar = new wxMenuBar; - menuBar->Append(menuFile, "&File"); - menuBar->Append(menuHelp, "&Help"); - - SetMenuBar(menuBar); - - CreateStatusBar(); - SetStatusText("Welcome to wxWidgets!"); - - Bind(wxEVT_MENU, &cFrame::OnHello, this, ID_Hello); - Bind(wxEVT_MENU, &cFrame::OnAbout, this, wxID_ABOUT); - Bind(wxEVT_MENU, &cFrame::OnExit, this, wxID_EXIT); -} - -void cFrame::OnExit(wxCommandEvent &event) { Close(true); } - -void cFrame::OnAbout(wxCommandEvent &event) { - wxMessageBox("This is a wxWidgets Hello World example", "About Hello World", - wxOK | wxICON_INFORMATION); -} - -void cFrame::OnHello(wxCommandEvent &event) { - wxLogMessage("Hello world from wxWidgets!"); -} diff --git a/main/src/ui/game_screen.cpp b/main/src/ui/game_screen.cpp new file mode 100644 index 0000000..6666498 --- /dev/null +++ b/main/src/ui/game_screen.cpp @@ -0,0 +1,939 @@ +#include "app.h" +#include "lua/game_engine.h" +#include "ui/game_screen.h" +#include "ui/wherigo_dialog.h" + +extern "C" { +#include +} + +wxDECLARE_APP(cApp); + +enum { + ID_SaveGame = 2000, + ID_LoadGame = 2001, + ID_ExportCompletion = 2002 +}; + +cGameScreen::cGameScreen(wxWindow *parent) + : wxFrame(parent, wxID_ANY, "Wherigo Player", wxDefaultPosition, wxSize(800, 600)) { + + // 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_ExportCompletion, "Completion Log exportieren\tCtrl+E"); + menuFile->AppendSeparator(); + menuFile->Append(wxID_EXIT, "Spiel beenden"); + + auto *menuHelp = new wxMenu; + menuHelp->Append(wxID_ABOUT); + + auto *menuBar = new wxMenuBar; + menuBar->Append(menuFile, "&Datei"); + menuBar->Append(menuHelp, "&Hilfe"); + SetMenuBar(menuBar); + + // Main sizer + auto *mainSizer = new wxBoxSizer(wxVERTICAL); + + // 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); + taskSizer->Add(new wxStaticText(taskPanel, wxID_ANY, "Aktive Aufgaben:"), 0, wxALL, 5); + m_taskList = new wxListBox(taskPanel, wxID_ANY); + taskSizer->Add(m_taskList, 1, wxALL | wxEXPAND, 5); + taskPanel->SetSizer(taskSizer); + m_notebook->AddPage(taskPanel, "Aufgaben"); + + // Inventory tab + auto *inventoryPanel = new wxPanel(m_notebook); + auto *inventorySizer = new wxBoxSizer(wxVERTICAL); + inventorySizer->Add(new wxStaticText(inventoryPanel, wxID_ANY, "Dein Inventar:"), 0, wxALL, 5); + m_inventoryList = new wxListBox(inventoryPanel, wxID_ANY); + inventorySizer->Add(m_inventoryList, 1, wxALL | wxEXPAND, 5); + inventoryPanel->SetSizer(inventorySizer); + m_notebook->AddPage(inventoryPanel, "Inventar"); + + // Characters tab + auto *characterPanel = new wxPanel(m_notebook); + auto *characterSizer = new wxBoxSizer(wxVERTICAL); + characterSizer->Add(new wxStaticText(characterPanel, wxID_ANY, "Personen in der Nähe:"), 0, wxALL, 5); + m_characterList = new wxListBox(characterPanel, wxID_ANY); + characterSizer->Add(m_characterList, 1, wxALL | wxEXPAND, 5); + characterPanel->SetSizer(characterSizer); + m_notebook->AddPage(characterPanel, "Personen"); + + // Items tab + auto *itemPanel = new wxPanel(m_notebook); + auto *itemSizer = new wxBoxSizer(wxVERTICAL); + itemSizer->Add(new wxStaticText(itemPanel, wxID_ANY, "Gegenstände in der Nähe:"), 0, wxALL, 5); + m_itemList = new wxListBox(itemPanel, wxID_ANY); + itemSizer->Add(m_itemList, 1, wxALL | wxEXPAND, 5); + itemPanel->SetSizer(itemSizer); + m_notebook->AddPage(itemPanel, "Gegenstände"); + + mainSizer->Add(m_notebook, 1, wxALL | wxEXPAND, 5); + + SetSizer(mainSizer); + + CentreOnScreen(); + + // Status bar + CreateStatusBar(); + SetStatusText("Spiel läuft"); + + // Event bindings + Bind(wxEVT_CLOSE_WINDOW, &cGameScreen::OnClose, this); + Bind(wxEVT_MENU, &cGameScreen::OnAbout, this, wxID_ABOUT); + Bind(wxEVT_MENU, &cGameScreen::OnExit, this, wxID_EXIT); + Bind(wxEVT_MENU, &cGameScreen::OnSaveGame, this, ID_SaveGame); + Bind(wxEVT_MENU, &cGameScreen::OnLoadGame, this, ID_LoadGame); + Bind(wxEVT_MENU, &cGameScreen::OnExportCompletion, this, ID_ExportCompletion); + + m_zoneList->Bind(wxEVT_LISTBOX_DCLICK, &cGameScreen::OnZoneSelected, this); + m_taskList->Bind(wxEVT_LISTBOX_DCLICK, &cGameScreen::OnTaskSelected, this); + m_inventoryList->Bind(wxEVT_LISTBOX_DCLICK, &cGameScreen::OnInventorySelected, this); + m_characterList->Bind(wxEVT_LISTBOX_DCLICK, &cGameScreen::OnCharacterSelected, this); + m_itemList->Bind(wxEVT_LISTBOX_DCLICK, &cGameScreen::OnItemSelected, this); + + // Listen to game state changes from GameEngine + wherigo::GameEngine::getInstance().Bind(wherigo::EVT_GAME_STATE_CHANGED, + &cGameScreen::OnGameStateChanged, this); + + // Initial UI refresh + CallAfter([this]() { refreshUI(); }); +} + +void cGameScreen::OnClose(wxCloseEvent &event) { + // Unbind from game engine events + wherigo::GameEngine::getInstance().Unbind(wherigo::EVT_GAME_STATE_CHANGED, + &cGameScreen::OnGameStateChanged, this); + + // Stop the game engine but don't unload cartridge + wherigo::GameEngine::getInstance().stop(); + + // Hide instead of destroy, parent will handle visibility + Hide(); + + // Notify parent that game was closed + if (GetParent()) { + GetParent()->Show(); + } +} + +void cGameScreen::OnExit(wxCommandEvent &event) { + Close(); +} + +void cGameScreen::OnAbout(wxCommandEvent &event) { + wxMessageBox("Wherigo Player\nEin Wherigo-Cartridge-Player für Desktop", + "Über Wherigo Player", wxOK | wxICON_INFORMATION); +} + +void cGameScreen::OnSaveGame(wxCommandEvent& event) { + if (wxGetApp().saveGameState("")) { + wxMessageBox("Spielstand wurde gespeichert!", "Speichern", wxOK | wxICON_INFORMATION); + } else { + wxMessageBox("Fehler beim Speichern des Spielstands!", "Fehler", wxOK | wxICON_ERROR); + } +} + +void cGameScreen::OnLoadGame(wxCommandEvent& event) { + 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) { + if (wxGetApp().loadGameState("")) { + wxMessageBox("Spielstand wurde geladen!", "Laden", wxOK | wxICON_INFORMATION); + refreshUI(); + } else { + wxMessageBox("Kein gespeicherter Spielstand gefunden oder Fehler beim Laden!", "Fehler", wxOK | wxICON_ERROR); + } + } +} + +void cGameScreen::OnExportCompletion(wxCommandEvent& event) { + wxFileDialog saveDialog(this, "Completion Log speichern", "", "", + "Wherigo Game Log (*.gwl)|*.gwl", + wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + + std::string defaultPath = wxGetApp().getCompletionLogPath(); + if (!defaultPath.empty()) { + saveDialog.SetPath(defaultPath); + } + + if (saveDialog.ShowModal() == wxID_CANCEL) { + return; + } + + wxString filePath = saveDialog.GetPath(); + + if (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 { + wxMessageBox("Fehler beim Exportieren des Completion Logs!", "Fehler", wxOK | wxICON_ERROR); + } +} + +void cGameScreen::OnGameStateChanged(wxEvent& event) { + // Game state has changed, refresh UI + refreshUI(); +} + +void cGameScreen::refreshUI() { + // Freeze to prevent flickering and focus loss + Freeze(); + + populateZones(); + populateTasks(); + populateInventory(); + populateCharacters(); + populateItems(); + + Thaw(); +} + +void cGameScreen::populateZones() { + int selection = m_zoneList->GetSelection(); + wxString selectedItem = (selection != wxNOT_FOUND) ? m_zoneList->GetString(selection) : ""; + + m_zoneList->Clear(); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + 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); + lua_pop(L, 1); + + lua_getfield(L, -1, "Visible"); + bool visible = lua_toboolean(L, -1); + lua_pop(L, 1); + + if (active && visible) { + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + m_zoneList->Append(wxString::FromUTF8(lua_tostring(L, -1))); + } + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + // Restore selection + if (!selectedItem.IsEmpty()) { + int idx = m_zoneList->FindString(selectedItem); + if (idx != wxNOT_FOUND) { + m_zoneList->SetSelection(idx); + } + } +} + +void cGameScreen::populateTasks() { + int selection = m_taskList->GetSelection(); + wxString selectedItem = (selection != wxNOT_FOUND) ? m_taskList->GetString(selection) : ""; + + m_taskList->Clear(); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + 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), "ZTask") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Active"); + bool active = lua_toboolean(L, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "Complete"); + bool complete = lua_toboolean(L, -1); + lua_pop(L, 1); + + if (active && !complete) { + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + m_taskList->Append(wxString::FromUTF8(lua_tostring(L, -1))); + } + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + // Restore selection + if (!selectedItem.IsEmpty()) { + int idx = m_taskList->FindString(selectedItem); + if (idx != wxNOT_FOUND) { + m_taskList->SetSelection(idx); + } + } +} + +void cGameScreen::populateInventory() { + int selection = m_inventoryList->GetSelection(); + wxString selectedItem = (selection != wxNOT_FOUND) ? m_inventoryList->GetString(selection) : ""; + + m_inventoryList->Clear(); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + // Get Player + lua_getglobal(L, "Player"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return; + } + + // Find all items where Container == Player + 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), "ZItem") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Container"); + if (lua_istable(L, -1)) { + // Check if Container is Player + lua_getglobal(L, "Player"); + 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_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); + } + lua_pop(L, 2); // pop _G and Player + + // Restore selection + if (!selectedItem.IsEmpty()) { + int idx = m_inventoryList->FindString(selectedItem); + if (idx != wxNOT_FOUND) { + m_inventoryList->SetSelection(idx); + } + } +} + +void cGameScreen::populateCharacters() { + int selection = m_characterList->GetSelection(); + wxString selectedItem = (selection != wxNOT_FOUND) ? m_characterList->GetString(selection) : ""; + + m_characterList->Clear(); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + 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), "ZCharacter") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Active"); + bool active = lua_toboolean(L, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "Visible"); + 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); + lua_pop(L, 1); + + if (active && visible && hasLocation) { + lua_getfield(L, -1, "Name"); + wxString name; + if (lua_isstring(L, -1)) { + name = wxString::FromUTF8(lua_tostring(L, -1)); + } + lua_pop(L, 1); + + // Get location name + lua_getfield(L, -1, "Container"); + lua_getfield(L, -1, "Name"); + wxString location; + if (lua_isstring(L, -1)) { + location = wxString::FromUTF8(lua_tostring(L, -1)); + } + lua_pop(L, 2); + + if (!name.IsEmpty()) { + m_characterList->Append(wxString::Format("%s (%s)", name, location)); + } + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + // Restore selection + if (!selectedItem.IsEmpty()) { + int idx = m_characterList->FindString(selectedItem); + if (idx != wxNOT_FOUND) { + m_characterList->SetSelection(idx); + } + } +} + +void cGameScreen::populateItems() { + int selection = m_itemList->GetSelection(); + wxString selectedItem = (selection != wxNOT_FOUND) ? m_itemList->GetString(selection) : ""; + + m_itemList->Clear(); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + 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), "ZItem") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Active"); + bool active = lua_toboolean(L, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "Visible"); + bool visible = lua_toboolean(L, -1); + lua_pop(L, 1); + + // Check if item is NOT in player inventory (in a zone) + lua_getfield(L, -1, "Container"); + bool inZone = false; + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "ClassName"); + if (lua_isstring(L, -1)) { + const char *containerClass = lua_tostring(L, -1); + inZone = (strcmp(containerClass, "Zone") == 0); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + if (active && visible && inZone) { + lua_getfield(L, -1, "Name"); + wxString name; + if (lua_isstring(L, -1)) { + name = wxString::FromUTF8(lua_tostring(L, -1)); + } + lua_pop(L, 1); + + // Get location name + lua_getfield(L, -1, "Container"); + lua_getfield(L, -1, "Name"); + wxString location; + if (lua_isstring(L, -1)) { + location = wxString::FromUTF8(lua_tostring(L, -1)); + } + lua_pop(L, 2); + + if (!name.IsEmpty()) { + m_itemList->Append(wxString::Format("%s (%s)", name, location)); + } + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + // Restore selection + if (!selectedItem.IsEmpty()) { + int idx = m_itemList->FindString(selectedItem); + if (idx != wxNOT_FOUND) { + m_itemList->SetSelection(idx); + } + } +} + +void cGameScreen::OnZoneSelected(wxCommandEvent& event) { + int sel = m_zoneList->GetSelection(); + if (sel == wxNOT_FOUND) return; + + wxString zoneName = m_zoneList->GetString(sel); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + // Find the zone object by name + lua_getglobal(L, "_G"); + lua_pushnil(L); + + bool found = false; + 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, "Name"); + if (lua_isstring(L, -1)) { + wxString currentName = wxString::FromUTF8(lua_tostring(L, -1)); + lua_pop(L, 1); + + if (currentName == zoneName) { + found = true; + + // Get zone description + 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)) { + 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); + + // Show zone info with media + if (!description.IsEmpty() || !mediaName.IsEmpty()) { + std::vector buttons = {"OK"}; + wherigo::WherigoMessageDialog dlg(this, description, zoneName, buttons, mediaName); + dlg.ShowModal(); + } + + // Call OnEnter callback if it exists + lua_getfield(L, -1, "OnEnter"); + if (lua_isfunction(L, -1)) { + lua_pushvalue(L, -2); // push zone as self argument + if (lua_pcall(L, 1, 0, 0) != 0) { + wxLogError("Zone OnEnter error: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + + lua_pop(L, 1); // pop zone table + lua_pop(L, 1); // pop key + break; + } + } else { + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); // pop value + } + lua_pop(L, 1); // pop _G + + if (!found) { + wxMessageBox(wxString::Format("Zone '%s' nicht gefunden", zoneName), + "Fehler", wxOK | wxICON_ERROR); + } +} + +void cGameScreen::OnTaskSelected(wxCommandEvent& event) { + int sel = m_taskList->GetSelection(); + if (sel == wxNOT_FOUND) return; + + wxString taskName = m_taskList->GetString(sel); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + // Find the task object by name + 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), "ZTask") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + wxString currentName = wxString::FromUTF8(lua_tostring(L, -1)); + lua_pop(L, 1); + + if (currentName == taskName) { + // Get task 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 task complete status + lua_getfield(L, -1, "Complete"); + bool complete = lua_toboolean(L, -1); + lua_pop(L, 1); + + wxString status = complete ? "✓ Abgeschlossen" : "○ Offen"; + wxString message = wxString::Format("%s\n\n%s", status, description); + + wxMessageBox(message, taskName, wxOK | wxICON_INFORMATION); + + lua_pop(L, 1); // pop task table + lua_pop(L, 1); // pop key + break; + } + } else { + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); // pop value + } + lua_pop(L, 1); // pop _G +} + +void cGameScreen::OnInventorySelected(wxCommandEvent& event) { + int sel = m_inventoryList->GetSelection(); + if (sel == wxNOT_FOUND) return; + + wxString itemName = m_inventoryList->GetString(sel); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + // Find the item object by name + 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), "ZItem") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + 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"); + 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"); + if (lua_isfunction(L, -1)) { + lua_pushvalue(L, -2); // push item as self + if (lua_pcall(L, 1, 0, 0) != 0) { + wxLogError("Item OnClick error: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + + lua_pop(L, 1); // pop item table + lua_pop(L, 1); // pop key + break; + } + } else { + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); // pop value + } + lua_pop(L, 1); // pop _G +} + +void cGameScreen::OnCharacterSelected(wxCommandEvent& event) { + 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(); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + // Find the character object by name + 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), "ZCharacter") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + 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"); + 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); + + // Show with media support + std::vector buttons = {"OK"}; + wherigo::WherigoMessageDialog dlg(this, description, charName, buttons, mediaName); + dlg.ShowModal(); + + // Call OnClick if exists + lua_getfield(L, -1, "OnClick"); + if (lua_isfunction(L, -1)) { + lua_pushvalue(L, -2); // push character as self + if (lua_pcall(L, 1, 0, 0) != 0) { + wxLogError("Character OnClick error: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + + lua_pop(L, 1); // pop character table + lua_pop(L, 1); // pop key + break; + } + } else { + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); // pop value + } + lua_pop(L, 1); // pop _G +} + +void cGameScreen::OnItemSelected(wxCommandEvent& event) { + 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(); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + // Find the item object by name + 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), "ZItem") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + 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"); + 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"); + if (lua_isfunction(L, -1)) { + lua_pushvalue(L, -2); // push item as self + if (lua_pcall(L, 1, 0, 0) != 0) { + wxLogError("Item OnClick error: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + + lua_pop(L, 1); // pop item table + lua_pop(L, 1); // pop key + break; + } + } else { + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); // pop value + } + lua_pop(L, 1); // pop _G +} diff --git a/main/src/ui/start_screen.cpp b/main/src/ui/start_screen.cpp new file mode 100644 index 0000000..07fdb75 --- /dev/null +++ b/main/src/ui/start_screen.cpp @@ -0,0 +1,202 @@ +#include "ui/start_screen.h" +#include "app.h" +#include "lua/game_engine.h" +#include "ui/game_screen.h" + +#include +#include +#include + +wxDECLARE_APP(cApp); + +enum { + ID_OpenCartridge = 1001, + ID_StartGame = 1002 +}; + +cStartScreen::cStartScreen() + : wxFrame(nullptr, wxID_ANY, "Wherigo Player", wxDefaultPosition, wxSize(800, 600)), + m_gameFrame(nullptr), + m_cartridgeLoaded(false) { + + // Menu + auto *menuFile = new wxMenu; + menuFile->Append(ID_OpenCartridge, "Cartridge öffnen...\tCtrl+O"); + menuFile->AppendSeparator(); + menuFile->Append(wxID_EXIT, "Beenden"); + + auto *menuHelp = new wxMenu; + menuHelp->Append(wxID_ABOUT, "Über..."); + + auto *menuBar = new wxMenuBar; + menuBar->Append(menuFile, "&Datei"); + menuBar->Append(menuHelp, "&Hilfe"); + SetMenuBar(menuBar); + + // Main sizer + auto *mainSizer = new wxBoxSizer(wxVERTICAL); + + // Title + auto *title = new wxStaticText(this, wxID_ANY, "Wherigo Player"); + title->SetFont(title->GetFont().Bold().Scaled(2.0)); + mainSizer->Add(title, 0, wxALL | wxALIGN_CENTER, 20); + + // Open button + m_openButton = new wxButton(this, ID_OpenCartridge, "Cartridge öffnen..."); + m_openButton->SetMinSize(wxSize(200, 40)); + mainSizer->Add(m_openButton, 0, wxALL | wxALIGN_CENTER, 10); + + // Info panel (initially hidden) + m_infoPanel = new wxPanel(this); + auto *infoSizer = new wxBoxSizer(wxVERTICAL); + + // Splash image + m_splashImage = new wxStaticBitmap(m_infoPanel, wxID_ANY, wxNullBitmap); + infoSizer->Add(m_splashImage, 0, wxALL | wxALIGN_CENTER, 10); + + // Cartridge name + m_cartridgeName = new wxStaticText(m_infoPanel, wxID_ANY, ""); + m_cartridgeName->SetFont(m_cartridgeName->GetFont().Bold().Scaled(1.5)); + infoSizer->Add(m_cartridgeName, 0, wxALL | wxALIGN_CENTER, 5); + + // Author + m_cartridgeAuthor = new wxStaticText(m_infoPanel, wxID_ANY, ""); + infoSizer->Add(m_cartridgeAuthor, 0, wxALL | wxALIGN_CENTER, 5); + + // Description + 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); + + // Start button + m_startButton = new wxButton(m_infoPanel, ID_StartGame, "Spiel starten"); + m_startButton->SetMinSize(wxSize(200, 50)); + m_startButton->SetFont(m_startButton->GetFont().Bold()); + infoSizer->Add(m_startButton, 0, wxALL | wxALIGN_CENTER, 20); + + m_infoPanel->SetSizer(infoSizer); + m_infoPanel->Hide(); + + mainSizer->Add(m_infoPanel, 1, wxALL | wxEXPAND, 10); + + SetSizer(mainSizer); + + // Status bar + CreateStatusBar(); + SetStatusText("Bitte eine Cartridge öffnen"); + + // Event bindings + Bind(wxEVT_MENU, &cStartScreen::OnOpenCartridge, this, ID_OpenCartridge); + Bind(wxEVT_MENU, &cStartScreen::OnExit, this, wxID_EXIT); + Bind(wxEVT_MENU, &cStartScreen::OnAbout, this, wxID_ABOUT); + Bind(wxEVT_BUTTON, &cStartScreen::OnOpenCartridge, this, ID_OpenCartridge); + Bind(wxEVT_BUTTON, &cStartScreen::OnStartGame, this, ID_StartGame); + + CentreOnScreen(); +} + +void cStartScreen::OnOpenCartridge(wxCommandEvent& event) { + wxFileDialog openFileDialog(this, "Cartridge öffnen", "", "", + "Wherigo Cartridge (*.gwc)|*.gwc", + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (openFileDialog.ShowModal() == wxID_CANCEL) { + return; + } + + wxString filePath = openFileDialog.GetPath(); + SetStatusText("Lade Cartridge: " + filePath); + + // Load the cartridge via cApp + if (wxGetApp().loadCartridge(filePath.ToStdString())) { + m_cartridgeLoaded = true; + showCartridgeInfo(); + SetStatusText("Cartridge geladen - bereit zum Starten"); + } else { + wxMessageBox("Fehler beim Laden der Cartridge", "Fehler", wxOK | wxICON_ERROR); + SetStatusText("Fehler beim Laden"); + } +} + +void cStartScreen::showCartridgeInfo() { + auto *cartridge = wxGetApp().getCartridge(); + if (!cartridge) return; + + // Set cartridge info + m_cartridgeName->SetLabel(wxString::FromUTF8(cartridge->cartridgeName())); + m_cartridgeAuthor->SetLabel("von " + wxString::FromUTF8(cartridge->author())); + const auto desc = wxString::FromUTF8(cartridge->cartridgeDesc()); + if (desc.Find("<") == wxNOT_FOUND) { + wxString plain(desc); + plain.Replace("\n", "
"); + m_cartridgeDesc->SetPage("

" + plain + "

"); + } else { + m_cartridgeDesc->SetPage(desc); + } + + // Load splash screen + auto splash = cartridge->splashScreen(); + if (splash) { + auto data = splash->getData(); + if (!data.empty()) { + wxMemoryInputStream stream(data.data(), data.size()); + wxImage image(stream); + if (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(); + double scale = std::min(scaleX, scaleY); + image.Rescale(image.GetWidth() * scale, image.GetHeight() * scale, wxIMAGE_QUALITY_HIGH); + } + m_splashImage->SetBitmap(wxBitmap(image)); + } + } + } + + // Show info panel + m_infoPanel->Show(); + m_openButton->SetLabel("Andere Cartridge öffnen..."); + Layout(); +} + +void cStartScreen::OnStartGame(wxCommandEvent& event) { + if (!m_cartridgeLoaded) { + 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); + } + + // Hide start frame, show game + Hide(); + m_gameFrame->Show(); + + // Start the game (call OnStart and start engine) + wxGetApp().startGame(); +} + +void cStartScreen::onGameClosed() { + // Called when game frame is closed + Show(); + SetStatusText("Spiel pausiert - Cartridge noch geladen"); +} + +void cStartScreen::OnExit(wxCommandEvent& event) { + // Clean up game frame + if (m_gameFrame) { + m_gameFrame->Destroy(); + m_gameFrame = nullptr; + } + Close(true); +} + +void cStartScreen::OnAbout(wxCommandEvent& event) { + wxMessageBox("Wherigo Player\n\nEin Desktop-Player für Wherigo-Cartridges\n\nVersion 0.1", + "Über Wherigo Player", wxOK | wxICON_INFORMATION); +} diff --git a/main/src/ui/wherigo_dialog.cpp b/main/src/ui/wherigo_dialog.cpp new file mode 100644 index 0000000..d8f3b7d --- /dev/null +++ b/main/src/ui/wherigo_dialog.cpp @@ -0,0 +1,161 @@ +#include "ui/wherigo_dialog.h" +#include "lua/media_manager.h" +#include "lua/game_engine.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace wherigo { + +WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent, const wxString &text, + const wxString &title, + const std::vector &buttons, + const wxString &mediaName) + : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { + + auto *sizer = new wxBoxSizer(wxVERTICAL); + + // Media/Image (if provided) + if (!mediaName.IsEmpty()) { + auto mediaData = MediaManager::getInstance().getMediaByName(mediaName.ToStdString()); + + if (!mediaData.empty()) { + wxMemoryInputStream stream(mediaData.data(), mediaData.size()); + wxImage image(stream); + + if (image.IsOk()) { + // Get screen DPI for scaling + wxWindow* topWindow = wxTheApp->GetTopWindow(); + double contentScaleFactor = 1.0; + if (topWindow) { + contentScaleFactor = topWindow->GetContentScaleFactor(); + } + + // Scale image - doubled size from original (800x600 instead of 400x300) + // Adjust for DPI/Retina displays + int maxWidth = static_cast(800 * contentScaleFactor); + int maxHeight = static_cast(600 * contentScaleFactor); + + 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); + } + + wxBitmap bitmap(image); + auto *imageCtrl = new wxStaticBitmap(this, wxID_ANY, bitmap); + 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)); + 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)); + mediaLabel->SetForegroundColour(*wxLIGHT_GREY); + sizer->Add(mediaLabel, 0, wxALL | wxALIGN_CENTER, 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); + } 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); + } + + // Buttons + auto *buttonSizer = new wxBoxSizer(wxHORIZONTAL); + + if (buttons.empty()) { + auto *okButton = new wxButton(this, wxID_OK, "OK"); + buttonSizer->Add(okButton, 0, wxALL, 5); + okButton->Bind(wxEVT_BUTTON, &WherigoMessageDialog::onButton, this); + } 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); + } + } + + sizer->Add(buttonSizer, 0, wxALIGN_CENTER | wxBOTTOM, 10); + + SetSizerAndFit(sizer); + SetMinSize(wxSize(600, 300)); // Doubled from 300x150 + CenterOnParent(); +} + +void WherigoMessageDialog::onButton(wxCommandEvent &event) { + int id = event.GetId(); + if (id == wxID_OK) { + m_selectedButton = 0; + } else { + m_selectedButton = id - 1000; + } + EndModal(wxID_OK); +} + +WherigoDialogRunner& WherigoDialogRunner::getInstance() { + static WherigoDialogRunner instance; + return instance; +} + +void WherigoDialogRunner::showMessageBox(const wxString &text, const wxString &title, + std::function callback) { + WherigoMessageDialog dlg(nullptr, text, title); + dlg.ShowModal(); + + if (callback) { + callback(dlg.getSelectedButton()); + } +} + +void WherigoDialogRunner::showDialog(const std::vector &entries, + std::function callback) { + for (size_t i = 0; i < entries.size(); i++) { + const auto &entry = entries[i]; + + std::vector buttons; + for (const auto &btn : entry.buttons) { + buttons.push_back(wxString::FromUTF8(btn)); + } + + if (buttons.empty() && i < entries.size() - 1) { + buttons.push_back("Weiter"); + } else if (buttons.empty()) { + buttons.push_back("OK"); + } + + WherigoMessageDialog dlg(nullptr, wxString::FromUTF8(entry.text), "Wherigo", + buttons, wxString::FromUTF8(entry.mediaName)); + dlg.ShowModal(); + + // For the last entry, call the callback with the selected button + if (i == entries.size() - 1 && callback) { + callback(dlg.getSelectedButton()); + } + } + + // Notify game state change after dialog sequence completes + GameEngine::getInstance().notifyStateChanged(); +} + +} // namespace wherigo +