- JavaScript 57%
- Vue 33.5%
- Java 3.9%
- Shell 2.8%
- Python 1.9%
- Other 0.9%
|
All checks were successful
CI / frontend (push) Successful in 1m6s
Firefox Android's GeckoView always returns Android's cached getLastKnownLocation() instead of requesting a fresh GPS fix. Both enableHighAccuracy modes are broken. No JS workaround exists. See Bugzilla: 1765835, 1824388, 721937. |
||
|---|---|---|
| .forgejo/workflows | ||
| backend | ||
| frontend | ||
| scripts | ||
| testGPS | ||
| tileserver-data | ||
| .gitignore | ||
| Caddyfile | ||
| README.md | ||
| todo.md | ||
Wayfinder — Outdoor Quest RPG
⚠️ Archived — Firefox Android (GeckoView) has a browser-level bug where getCurrentPosition/watchPosition always returns Android's cached getLastKnownLocation() instead of requesting a fresh GPS fix. Both enableHighAccuracy: true and false are affected. The underlying issue is in Mozilla's GeckoAppShell.java — it calls getLastKnownLocation() before requestLocationUpdates(), and this has gone unfixed for years across multiple Bugzilla tickets (1765835, 1824388, 721937).
No JavaScript-level workaround exists. The project relies on continuous location tracking for check-ins, distance tracking, timed quests, and proximity detection — all of which are broken on Firefox Android. A GeckoView wrapper APK or Chromium-based browser would work, but neither is acceptable to the author.
Core Concept
A gamified progressive web app (PWA) designed to encourage users to spend more time outdoors by blending real-world exploration with RPG-style progression and quest systems. Built with a Java Spring Boot backend and a lightweight PWA frontend, the app requires no app store downloads.
Primary Goal
Incentivize outdoor activity through engaging, location-based challenges that reward real-world exploration with in-app progression, achievements, and unlocks — entirely free, no ads, no predatory monetization.
Design Philosophy
- Casual-first — no FOMO, no stamina gates, no leaderboard pressure, no punishment for inactivity
- Trust-based — GPS verification is client-side, photo proof is for the user's own memory, not verification
- Anti-FOMO — nothing permanently missable
- 100% free — no ads, no premium currency, no real-money transactions
Core Game Mechanics
1. Dynamic Quest System
Quests are generated dynamically by matching templates to nearby POIs (parks, cafes, landmarks) fetched from the backend. All quest generation is client-side.
Quest Rarity
| Rarity | Example | XP |
|---|---|---|
| Common | "Visit Oak Park" | 10–15 |
| Uncommon | "Have a 20-minute picnic at Oak Park" | 22–30 |
| Rare | "Take a creative photo of the mural" | 30–35 |
| Epic | Multi-step chain (3+ steps) | varies |
Quest Types
| Type | Description | Status |
|---|---|---|
| Check-In | Visit one specific POI | ✅ |
| Timed | Stay within range of a POI for a duration | ✅ |
| Photo | Capture a photo at a POI | ✅ |
| Multi-Step | Multi-quest chain with ordered steps | ✅ |
| Discovery | Explore N POIs of a category | ✅ |
| Distance | Walk a target distance (1–5 km) | ✅ |
| Time-Based | Check in during a specific window | ✅ |
| Collection | Visit multiple different POI types (multi-category discover) | ✅ |
| Seasonal | Date-gated quests (summer, winter, etc.) | ✅ |
| Holiday | Campaign-style quests for events (Easter, Christmas, Halloween) — separate log, no slot cost | ✅ |
| Speed Trial | Reach a destination POI within a time limit based on your distance × pace | ✅ |
| Compass Navigation | Navigate to a hidden POI using only a bearing on a compass dial — no map marker, no distance | ✅ |
| Radar (Hot & Cold) | Find a hidden POI using only a distance readout — gets warmer as you approach | ✅ |
Quest Slots
- 3 active quest slots base, +1 purchasable (200 tokens, repeatable)
- Quest picker shows 3 options (one of each type when possible), refreshable
- Holiday quests appear in a dedicated log during special dates (Easter, Christmas, Halloween) — no slot cost, auto-generated from nearby POIs
2. RPG Progression
- Logistic XP curve:
L / (1 + e^(-k(level - x0)))with L=1000, k=0.1, x0=32- Early levels (1-10): 43-100 XP — fast, 1-4 quests each
- Mid levels (20-40): 231-690 XP — steady climb
- Late levels (60+): ~940-1000 XP — plateaus at ~40 quests per level
- Never exceeds 1000 XP per level — no unbounded grind
- XP and tokens earned from quests, scaled by rarity
- POI discovery awards +5 XP +1 token per day per POI
Titles & Passive Bonuses
| Title | Bonus |
|---|---|
| Urban Explorer | +10% XP from cafe/landmark quests |
| Park Ranger | +10% XP from park/nature quests |
| Wanderer | +10% XP from distance quests |
| Collector | +1 bonus token per multi-step completed |
3. Tokens & Shop
Tokens are earned by completing quests and exploring POIs. No real-money purchases.
| Item | Cost | Category | |
|---|---|---|---|
| +1 quest slot | 200 tokens | Utility (repeatable) | |
| XP Boost | 50 tokens | Consumable (double XP on next quest) | |
| Express Pass | 100 tokens | Consumable (skip timed quest instantly) |
4. POI Data Pipeline
- OpenStreetMap is the sole POI data source
- Planetiler builds an mbtiles file from OSM data (~30 min)
- POI extraction script reads z14 tiles and writes
pois.sqlite(~2.5 min, 481K unique POIs) - Backend serves POIs via simple haversine + bounding-box queries on SQLite
- All POIs visible from the start — colored markers (green = parks, brown = cafes, gray = landmarks, teal = nature)
5. GPS-Verified Check-Ins
- Client-side Haversine distance calculation (
CHECK_IN_RADIUS = 200m) - Continuous distance tracking with 10m jitter filter
- Photo capture with white border (no upload, no data collection)
- Timer quests run via live
Date.now()computation, auto-pause on zone exit
6. Badges & Achievements
Badges are displayed as circular icons with rarity-colored SVG progress rings. Each badge has a fixed rarity tier that determines its visual style (ring color, background tint, glow effect). Campaign quests can optionally award a badge on completion via the badgeReward field on the campaign template.
| Rarity | Ring | Background | Glow |
|---|---|---|---|
| Bronze | #cd7f32 |
#fdf1e6 |
rgba(205,127,50,0.3) |
| Silver | #a8a8a8 |
#f5f5f5 |
rgba(168,168,168,0.3) |
| Gold | #daa520 |
#fff8e1 |
rgba(218,165,32,0.3) |
| Platinum | #b0b0d0 |
#f5f5ff |
rgba(176,176,208,0.3) |
| Badge | Requirement | Rarity |
|---|---|---|
| First Steps | Complete first quest | Bronze |
| Strider | Walk 1 km total | Bronze |
| Early Bird | Complete a timed quest | Bronze |
| Hat Trick | Open the app 3 days in a row | Bronze |
| Marathoner | Walk 10 km total | Silver |
| Park Goer | Visit 10 different parks | Silver |
| Picnic Master | Complete The Grand Picnic campaign | Silver |
| Weekly Warrior | Open the app 7 days in a row | Silver |
| Globetrotter | Travel 50 km total | Gold |
| Centurion | Complete 100 quests | Gold |
| Dedicated | Open the app 30 days in a row | Platinum |
7. Streak Tracking
- Tracks consecutive days the app is opened
- Best streak persisted alongside current streak
- Streak badges award automatically on login
- Streak resets if a day is skipped
UI Flow
- Map — default view. POI markers, user dot, quests/profile/shop buttons. Active timed quest countdowns and proximity notifications appear as persistent toasts.
- Quest Log — active quests with type-specific progress, "Check In" link per quest
- Quest Picker — 3 random quests from nearby POIs, accept to fill a slot
- Check-In — GPS proximity → type-specific UI (timer, photo, step advance, progress bar) → success animation
- Holiday Log — dedicated view for holiday quests (appears only during active holiday events); no slot cost
- Profile — level, XP bar, stats (quests, POIs, distance, streak, badges), title selector, accent color, save import/export
- Settings — accent color, keyboard pan controls, GPS controls, map attributions, save import/export
- Shop — quest slot upgrades, XP boosts, express passes
Technical Stack
- Frontend: Vue 3 + Pinia + Vue Router + MapLibre GL + Axios + ESLint + Prettier
- Backend: Java 17 + Spring Boot 3.4 + SQLite (org.xerial:sqlite-jdbc)
- Spatial Data: OpenStreetMap → Planetiler → mbtiles → POI extraction script →
pois.sqlite - Quest Generation: Entirely client-side — templates filtered by date range, level, and POI availability. When multiple POIs match a template, one is selected at random.
- Holiday Quests: Campaign-style, date-gated, tracked separately from normal quest slots
- Persistence: Client-side via four per-domain localStorage keys (
wayfinder_player,wayfinder_quests,wayfinder_badges,wayfinder_inventory) with import/export - Tileserver: tileserver-gl serving mbtiles with custom style (maxzoom 14, building layers)
Pipeline
OSM data → Planetiler → sweden.mbtiles
↓
extract_pois.py → pois.sqlite
↓
Backend (SQLite) → /api/pois/nearby
↓
Frontend (MapLibre + quest generator)
Anti-Patterns
- ✗ No ads
- ✗ No premium currency
- ✗ No stamina / energy systems
- ✗ No FOMO timers or limited-time exclusives
- ✗ No leaderboards
- ✗ No real-money purchases
- ✗ No loot boxes
- ✗ No push notification spam
- ✗ No data mining
- ✗ No punishment for inactivity
Development
Linting & Formatting
cd frontend && npm run lint # ESLint
cd frontend && npm run format # Prettier
CI runs npm run lint and npm run format:check on every push.
Running Tests
Backend (Java + JUnit 5):
cd backend && ./gradlew test
21 tests covering POI repository (radius, filters, sorting, clamping), POI controller (params, validation), and config controller.
Frontend (Vitest + Vue Test Utils):
cd frontend && npm run test
132 tests covering player store (XP/leveling, quest lifecycle, streaks, badges, titles, save/reset), quest template generation (seasonal filtering, multi-category discover, date utilities), holiday quest lifecycle, and API service.
XP Curve Parameters
Defined in frontend/src/config/gameSettings.js:
| Param | Value | Effect |
|---|---|---|
XP_LOGISTIC_L |
1000 | Max XP per level (plateau) |
XP_LOGISTIC_K |
0.1 | Steepness of the S-curve |
XP_LOGISTIC_X0 |
32 | Midpoint level (fastest growth) |
Planned
Parking lot: pitch-offset player position
When the map is pitched, offset the player dot toward the bottom of the viewport so more of the forward direction is visible. Abandoned for now due to jitter when re-centering mid-drag. Could be revisited if jumpTo with manual camera-geometry calculation proves smoother.