No description
This repository has been archived on 2026-05-24. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
  • JavaScript 57%
  • Vue 33.5%
  • Java 3.9%
  • Shell 2.8%
  • Python 1.9%
  • Other 0.9%
Find a file
Jimmy Berglund 66a71dc5fc
All checks were successful
CI / frontend (push) Successful in 1m6s
Archive project due to unfixable Firefox Android geolocation bug
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.
2026-05-24 16:08:01 +02:00
.forgejo/workflows chore: add ESLint, Prettier, and Forgejo CI workflow 2026-05-21 18:44:39 +02:00
backend Add Caddy reverse proxy for phone testing, bind services to 0.0.0.0 2026-05-24 13:00:58 +02:00
frontend Archive project due to unfixable Firefox Android geolocation bug 2026-05-24 16:08:01 +02:00
scripts Add /gpstest GPS test page under Caddy 2026-05-24 14:33:07 +02:00
testGPS Add /gpstest GPS test page under Caddy 2026-05-24 14:33:07 +02:00
tileserver-data Fix tile CORS: add cors:true to tileserver, clarify .env.example 2026-05-24 13:26:40 +02:00
.gitignore chore: updated gitignore 2026-05-24 13:40:28 +02:00
Caddyfile Add /gpstest GPS test page under Caddy 2026-05-24 14:33:07 +02:00
README.md Archive project due to unfixable Firefox Android geolocation bug 2026-05-24 16:08:01 +02:00
todo.md Add Caddy reverse proxy for phone testing, bind services to 0.0.0.0 2026-05-24 13:00:58 +02:00

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" 1015
Uncommon "Have a 20-minute picnic at Oak Park" 2230
Rare "Take a creative photo of the mural" 3035
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 (15 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

  1. Map — default view. POI markers, user dot, quests/profile/shop buttons. Active timed quest countdowns and proximity notifications appear as persistent toasts.
  2. Quest Log — active quests with type-specific progress, "Check In" link per quest
  3. Quest Picker — 3 random quests from nearby POIs, accept to fill a slot
  4. Check-In — GPS proximity → type-specific UI (timer, photo, step advance, progress bar) → success animation
  5. Holiday Log — dedicated view for holiday quests (appears only during active holiday events); no slot cost
  6. Profile — level, XP bar, stats (quests, POIs, distance, streak, badges), title selector, accent color, save import/export
  7. Settings — accent color, keyboard pan controls, GPS controls, map attributions, save import/export
  8. 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.