GeoMon

Quang Minh Dinh, Brandon Arai, Duc Long Nguyen, Yizhang Zhu,
{qmd, baa45, dln3, yza673}@sfu.ca,

GeoMon is a lightweight location-based RPG where players explore real-world maps, tap icons to battle enemies, and earn rewards. Unlike AR-heavy games, it offers quick, 2D turn-based fights for fun, accessible on-the-go gameplay.

Demo 2 video

Demo 1 video

Threaded Design Diagram

                          ┌─────────────────────────────────────────────────────┐
                          │                    UI THREAD                        │
                          │                (Main / Looper Main)                 │
                          └─────────────────────────────────────────────────────┘
                                          ▲                 ▲
                                          │                 │
                                          │                 │
                               UI Updates │                 │ LiveData Observers
                                          │                 │
┌─────────────────────────┐               │                 │             ┌─────────────────────────┐
│       MainActivity      │───────────────┘                 └────────────>│   TrackingViewModel     │
│ - onCreate              │                                               │ - LiveData      │
│ - onResume              │                                               │ - serviceStarted        │
│ - onMapReady            │                                               │ - TrackingHandler       │
│ - updateMap             │                                               │   (Looper.Main)         │
│ - displayOtherPlayers   │                                               └─────────────────────────┘
│ - handlePlayerClick     │
│ - setupDuelListener     │
│ - showDuelDialog        │
│ - createBattleState     │
│ - launchBattle          │
│ - marker clicks         │
│ - heartbeat handler     │
│   (30s intervals)       │
└─────────────────────────┘
          ▲
          │ UI interactions
          │
┌─────────────────────────┐   ┌─────────────────────────┐   ┌─────────────────────────┐
│     BattleActivity      │   │  MonsterChatDialog      │   │   BagActivity           │
│ - onCreate              │   │ - onCreate              │   │ - loadUserBag           │
│ - setupMoveButtons      │   │ - sendMessage           │   │ - onItemClicked         │
│ - onPlayerMoveSelected  │   │ - GeminiAPI calls       │   │ - openHealTarget        │
│ - handlePvPMove         │   │ - chat UI updates       │   │ - openLevelUpTarget     │
│ - handlePvEMove         │   └─────────────────────────┘   └─────────────────────────┘
│ - performAttack         │
│ - checkBattleEnd        │   ┌─────────────────────────┐   ┌─────────────────────────┐
│ - setupBattleListener   │   │  HealTargetActivity     │   │  AllTargetActivity      │
│ - updateHpFromState     │   │ - loadDamagedMonsters   │   │ - loadAllMonsters       │
│ - coroutine delays      │   │ - onMonsterClicked      │   │ - onMonsterClicked      │
│ - lastMoveTimestamp     │   │ - heal & Firebase sync  │   │ - levelUp & sync        │
└─────────────────────────┘   └─────────────────────────┘   └─────────────────────────┘

┌─────────────────────────┐   ┌─────────────────────────┐   ┌─────────────────────────┐
│   PokedexActivity       │   │  MonsterInfoActivity    │   │  MenuDialogFragment     │
│ - loadUserMonsters      │   │ - fetchById (coroutine) │   │ - openMonsterChat       │
│ - launch MonsterInfo    │   │ - bindMonster           │   │ - launch activities     │
└─────────────────────────┘   │ - setBattle button      │   └─────────────────────────┘
                              └─────────────────────────┘
┌─────────────────────────┐   ┌─────────────────────────┐
│ ChangeAvatarDialog      │   │  PlayerStatsDialog      │
│ - image picker          │   │ - fetchById user        │
│ - Firebase Storage      │   │ - fetchById monster     │
│ - Glide avatar load     │   │ - Glide avatar load     │
└─────────────────────────┘   └─────────────────────────┘

───────────────────────────────────────────────────────────────────────────────
                 BACKGROUND THREADS (COROUTINES / ROOM / FIREBASE)
───────────────────────────────────────────────────────────────────────────────

            ┌─────────────────────────────┐
            │      Dispatchers.IO         │
            │ (Coroutine thread pool)     │
            └─────────────────────────────┘
                    ▲                          ▲                    ▲
                    │                          │                    │
    MainActivity.onCreate background work      │                    │
    ┌─────────────────────────────┐        Room DB                  │
    │ lifecycleScope.launch(IO)   │       auto-threading            │
    │ - signInAnonymously()       │        ┌──────────────────────────┐
    │ - Seeder.run()              │◄──────►│ SpeciesDao (suspend)     │
    │ - load stats.json           │        │ - upsertAll()            │
    │ - load moves.json           │        │ - upsertMoves()          │
    │ - load items.json           │        │ - upsertItems()          │
    │ - parse species/moves/items │        │ - getById()              │
    │ - User.fetchById()          │        │ - getAll()               │
    │ - Monster.fetchById()       │        │ - getByIdNow()           │
    │ - Monster.initializeByName()│        │ - getMoveByIdNow()       │
    │ - User.createOrUpdate()     │        │ - getItemByDisplayName() │
    │ - generateAndUploadMonsters │        │ - countSpecies()         │
    │ - fetchNearbyMonsters       │        └──────────────────────────┘
    └─────────────────────────────┘

SpeciesRepository
┌──────────────────────────────────────────┐
│ clear() → CoroutineScope(IO).launch      │──► IO THREAD
│ getAllSpecies() (Flow)                   │◄── ROOM THREAD
│ upsertAll(), upsertMoves() (suspend)     │──► ROOM THREAD
│ upsertItems() (suspend)                  │──► ROOM THREAD
│ countSpecies() (suspend)                 │──► ROOM THREAD
└──────────────────────────────────────────┘

Seeder
┌─────────────────────────────────────────────┐
│ run(context, repo)                          │
│ - withContext(IO)                           │──► JSON parsing thread
│ - load raw/stats.json                       │
│ - load raw/moves.json                       │
│ - load raw/items.json                       │
│ - construct Species/Move/Item objects       │
│ - insert via repository (Room → IO thread)  │
└─────────────────────────────────────────────┘

Monster Companion Object
┌──────────────────────────────────────────┐
│ initializeByName(name)                   │
│   - DB lookup (Room on IO)               │
│   - fallback to default                  │
│   - levelScaling calculation             │
│   - Firebase push & setValue             │──► Firebase thread
│ fetchById(id)                            │
│   - suspendCoroutine                     │
│   - Firebase .get()                      │──► Firebase threadpool
│   - fromSnapshot parsing                 │
│   - levelScaling on result               │
│   - continuation.resume() on main thread │──► MAIN THREAD
│ setOwner(monsterId, ownerId)             │──► Firebase thread
│ takeDamage(damage)                       │
│   - syncHpToFirebase()                   │──► Firebase thread
│ healDamage(amount)                       │
│   - syncHpToFirebase()                   │──► Firebase thread
│ levelUp(amount)                          │
│   - levelScaling(this)                   │
│   - syncHpToFirebase()                   │──► Firebase thread
└──────────────────────────────────────────┘

Move Companion Object
┌──────────────────────────────────────────┐
│ initializeByName(context, name)          │
│   - DB lookup (Room on IO)               │──► Room thread
│   - fallback to hardcoded moves          │
│   - fromEntity conversion                │
└──────────────────────────────────────────┘

Item Companion Object
┌──────────────────────────────────────────┐
│ searchByDisplayName(context, name)       │
│   - DB lookup (Room on IO)               │──► Room thread
│   - fromEntity conversion                │
│ returnAffect()                           │
│   - effect parsing logic                 │
└──────────────────────────────────────────┘

Battle Logic (background portions)
┌──────────────────────────────────────────┐
│ BattleActivity                           │
│   lifecycleScope.launch {                │
│      - Monster.fetchById() (player)      │──► Firebase thread
│      - Monster.fetchById() (opponent)    │──► Firebase thread
│      - Move.initializeByName()           │──► Room thread
│      - damageCalc()                      │──► Calculation (current thread)
│      - performAttack()                   │
│      - delay(800-1000ms)                 │──► Default dispatcher
│      - checkBattleEnd()                  │
│      - giveVictoryRewards()              │
│      - handlePvPVictory()                │
│      - handlePvPDefeat()                 │
│   }                                      │
│                                          │
│ PvP-specific:                            │
│   - setupBattleStateListener()           │
│      - ValueEventListener                │──► Firebase thread
│      - onDataChange callback             │──► Main thread
│      - updateHpFromBattleState()         │
│      - lastProcessedMoveTimestamp check  │
│   - handlePvPMoveSelection()             │
│      - performAttack()                   │
│      - BattleState.updateHpAndNextTurn() │──► Firebase thread
└──────────────────────────────────────────┘

───────────────────────────────────────────────────────────────────────────────
                              SPAWN SYSTEM THREADING
───────────────────────────────────────────────────────────────────────────────

Monster Spawning
┌──────────────────────────────────────────┐
│ MainActivity.checkAndSpawnMonsters()     │
│   - Firebase addListenerForSingleValue   │──► Firebase thread
│   - onDataChange callback                │──► Main thread
│   - count nearby monsters                │
│   - if < threshold:                      │
│     generateAndUploadMonsters()          │
│       - lifecycleScope.launch(IO)        │──► IO thread
│       - for each monster:                │
│         Monster.initializeByName()       │──► Room + Firebase
│   - displayMonstersOnMap()               │──► Main thread
│     - add markers to GoogleMap           │
└──────────────────────────────────────────┘

Item Spawning
┌──────────────────────────────────────────┐
│ ItemSpawner.generateItemsAround()        │
│   - deterministic grid-based generation  │──► Current thread
│   - uses seeded Random per grid cell     │
│   - returns List              │
│                                          │
│ MainActivity.checkAndSpawnItems()        │──► Main thread
│   - calls ItemSpawner.generateItems()    │
│   - displayItemsOnMap()                  │
│     - add markers to GoogleMap           │
└──────────────────────────────────────────┘

───────────────────────────────────────────────────────────────────────────────
                      FIREBASE THREADS (NETWORK OPERATIONS)
───────────────────────────────────────────────────────────────────────────────

                   ┌─────────────────────────────────────────┐
                   │ Firebase Internal Worker Threadpool     │
                   └─────────────────────────────────────────┘
                            ▲                     ▲
                            │                     │
         Monster operations │                     │ User/Auth operations
         ┌──────────────────────────────┐         ┌───────────────────────────────────────┐
         │ Monster.fetchById()          │         │ User.fetchById()                      │
         │ - suspendCoroutine           │         │   - Firebase .get()                   │
         │ - reference.child(id).get()  │         │   - fromSnapshot parsing              │
         │ - fromSnapshot               │         │ User.createOrUpdate()                 │
         │ - SuccessListener runs here  │         │   - Firebase setValue()               │
         │ - resume on main thread      │         │ User.updateLocation()                 │
         │                              │         │   - Firebase updateChildren().        │
         │ Monster.initializeByName()   │         │ User.updateLastActive()               │
         │ - Firebase push().setValue() │         │   - Firebase updateChildren().        │
         │                              │         │ User.updateAvatarUrl()                │
         │ Monster.takeDamage()         │         │   - Firebase updateChildren().        │
         │ - Firebase updateChildren()  │         │ User.addMonster()                     │
         │   (currentHp sync)           │         │   - Firebase .get() + setValue().     │
         │                              │         │ User.removeMonster()                  │
         │ Monster.setOwner()           │         │   - Firebase .get() + updateChildren()│
         │ - Firebase setValue()        │         │ User.setUserActiveMonster()           │
         │                              │         │   - Firebase updateChildren().        │
         │ Monster.levelUp()            │         │ User.addItem()                        │
         │ - syncHpToFirebase()         │         │   - Firebase .get() + updateBag().    │
         │                              │         │ User.removeByItemName()               │
         │ Monster.healDamage()         │         │   - Firebase .get() + updateBag().    │
         │ - syncHpToFirebase()         │         │ User.updateBag()                      │
         └──────────────────────────────┘         │   - Firebase updateChildren().        │
                                                  └───────────────────────────────────────┘

DuelRequest Firebase Operations
┌──────────────────────────────────────────┐
│ DuelRequest.create()                     │──► Firebase thread
│   - suspendCoroutine                     │
│   - push().setValue()                    │
│ DuelRequest.updateStatus()               │──► Firebase thread
│   - updateChildren()                     │
│ DuelRequest.setBattleReady()             │──► Firebase thread
│   - updateChildren()                     │
│ DuelRequest.delete()                     │──► Firebase thread
│   - removeValue()                        │
│                                          │
│ MainActivity.setupDuelRequestListener()  │
│   - ValueEventListener                   │──► Firebase thread
│   - onDataChange callback                │──► Main thread
│   - showDuelChallengeDialog()            │
└──────────────────────────────────────────┘

BattleState Firebase Operations
┌──────────────────────────────────────────┐
│ BattleState.create()                     │──► Firebase thread
│   - suspendCoroutine                     │
│   - push().setValue()                    │
│ BattleState.updateMove()                 │──► Firebase thread
│   - .get() + updateChildren()            │
│ BattleState.updateHpAndNextTurn()        │──► Firebase thread
│   - updateChildren()                     │
│   - includes: player1Hp, player2Hp,      │
│     currentTurn, lastMove, lastMoveUser  │
│ BattleState.finishBattle()               │──► Firebase thread
│   - updateChildren()                     │
│ BattleState.delete()                     │──► Firebase thread
│   - removeValue()                        │
│                                          │
│ BattleActivity.setupBattleStateListener()│
│   - ValueEventListener                   │──► Firebase thread
│   - onDataChange callback                │──► Main thread
│   - updateHpFromBattleState()            │
│   - timestamp checking                   │
└──────────────────────────────────────────┘

AuthManager Firebase Operations
┌──────────────────────────────────────────┐
│ signInAnonymously()                      │
│   - suspendCoroutine                     │──► Firebase Auth thread
│   - auth.signInAnonymously()             │
│   - resume on callback                   │
│ signOut()                                │──► Firebase Auth thread
│ addAuthStateListener()                   │
│ removeAuthStateListener()                │
└──────────────────────────────────────────┘

Firebase Storage (Avatar Upload)
┌──────────────────────────────────────────┐
│ ChangeAvatarDialogFragment               │
│ - uploadAvatar()                         │
│   - lifecycleScope.launch                │
│   - putFile(uri).await()                 │──► Storage thread
│   - getDownloadUrl().await()             │──► Storage thread
│   - User.updateAvatarUrl()               │──► Firebase thread
└──────────────────────────────────────────┘

───────────────────────────────────────────────────────────────────────────────
                           LOCATION SERVICE THREADING
───────────────────────────────────────────────────────────────────────────────

GPS/Location Provider Thread
┌──────────────────────────────────────────┐
│ TrackingService                          │
│ - onStartCommand()                       │
│ - requestLocationUpdates()               │
│   (callback on NON-UI location thread)   │
│ - onLocationChanged(location)            │
│   (called on location thread)            │
│   → creates Bundle                       │
│   → sends Message to Handler             │──► Main thread via Handler
└──────────────────────────────────────────┘
                    │
                    ▼
     TrackingMessageHandler (MAIN THREAD)
     │ - handleMessage(msg)  
     │ - extracts lat/lng from Bundle
     │ - updates TrackingViewModel._latLng (MutableLiveData)
     ▼
UI THREAD observes latLng.value changes
     │ - MainActivity.updateMap()
     │   - CameraUpdateFactory.newLatLngZoom()
     │   - googleMap.animateCamera()
     │   - update player marker with direction icon
     │   - User.updateLocation()               ──► Firebase thread
     │   - checkAndSpawnMonsters()
     │   - checkAndSpawnItems()
     │   - displayOtherPlayers()
     │   - calculateDirection()

───────────────────────────────────────────────────────────────────────────────
                         MULTIPLAYER THREADING (PvP)
───────────────────────────────────────────────────────────────────────────────

Other Players Display
┌──────────────────────────────────────────┐
│ MainActivity.displayOtherPlayers()       │
│   - Firebase ValueEventListener          │──► Firebase thread
│   - onDataChange callback                │──► Main thread
│   - filter by lastActive (5 min)         │
│   - filter by radius                     │
│   - for each nearby player:              │
│     - Glide.asBitmap().load(avatarUrl)   │──► Glide thread pool
│       - CustomTarget callback            │──► Main thread
│       - cache avatar bitmap              │
│       - updateOtherPlayerMarker()        │
│   - remove markers for departed players  │
└──────────────────────────────────────────┘

Duel Challenge Flow
┌──────────────────────────────────────────┐
│ 1. Marker Click (Other Player)           │──► Main thread
│    - handlePlayerMarkerClick()           │
│      - lifecycleScope.launch             │
│      - User.fetchById (current & target) │──► Firebase thread
│      - Monster.fetchById (check fainted) │──► Firebase thread
│      - show AlertDialog (challenge)      │──► Main thread
│      - sendDuelRequest()                 │
│        - DuelRequest.create()            │──► Firebase thread
│                                          │
│ 2. Receive Challenge                     │
│    - setupDuelRequestListener()          │──► Firebase thread
│      - ValueEventListener.onDataChange   │──► Main thread
│      - showDuelChallengeDialog()         │──► Main thread
│      - on Accept:                        │
│        - lifecycleScope.launch           │
│        - check active monster            │──► Firebase thread
│        - DuelRequest.updateStatus()      │──► Firebase thread
│                                          │
│ 3. Create Battle                         │
│    - challenger sees "accepted" status   │──► Firebase listener
│    - createBattleState()                 │
│      - lifecycleScope.launch             │
│      - User.fetchById (both players)     │──► Firebase thread
│      - Monster.fetchById (both monsters) │──► Firebase thread
│      - BattleState.create()              │──► Firebase thread
│      - DuelRequest.setBattleReady()      │──► Firebase thread
│                                          │
│ 4. Launch Battle                         │
│    - both players see "battle_ready"     │──► Firebase listener
│    - launchBattleFromRequest()           │
│      - lifecycleScope.launch             │
│      - User.fetchById (opponent)         │──► Firebase thread
│      - startActivity(BattleActivity)     │──► Main thread
│      - DuelRequest.delete()              │──► Firebase thread
└──────────────────────────────────────────┘

PvP Battle Synchronization
┌──────────────────────────────────────────┐
│ BattleActivity (PvP Mode)                │
│                                          │
│ setupBattleStateListener()               │
│   - ValueEventListener                   │──► Firebase thread
│   - onDataChange callback                │──► Main thread
│   - check lastMoveTimestamp              │
│   - updateHpFromBattleState()            │
│   - determine isMyTurn                   │
│   - if opponent moved:                   │
│     - show opponent's move               │
│     - update HP displays                 │
│     - check if battle finished           │
│   - if myTurn:                           │
│     - enable move buttons                │
│                                          │
│ handlePvPMoveSelection()                 │
│   - check isMyTurn                       │──► Main thread
│   - lifecycleScope.launch                │
│   - Move.initializeByName()              │──► Room thread
│   - performAttack()                      │
│   - delay(1000)                          │
│   - BattleState.updateHpAndNextTurn()    │──► Firebase thread
│   - checkBattleEnd()                     │
│                                          │
│ checkBattleEnd()                         │
│   - if monster fainted:                  │
│     - BattleState.finishBattle()         │──► Firebase thread
│     - handlePvPVictory() / Defeat()      │
│       - User.removeByItemName()          │──► Firebase thread
│       - User.addItem()                   │──► Firebase thread
│     - delay(2000)                        │
│     - finish()                           │──► Main thread
└──────────────────────────────────────────┘

───────────────────────────────────────────────────────────────────────────────
                          AI CHAT THREADING (GEMINI API)
───────────────────────────────────────────────────────────────────────────────

GeminiAPI
┌──────────────────────────────────────────┐
│ generateContent(prompt)                  │
│   - withContext(Dispatchers.IO)          │──► IO thread
│   - HttpURLConnection setup              │
│   - POST to Gemini API                   │──► Network thread
│   - read response                        │
│   - parseResponse(json)                  │
│   - return text                          │
└──────────────────────────────────────────┘

MonsterAI
┌──────────────────────────────────────────┐
│ initialize(context)                      │──► Main thread
│   - read API key from manifest           │
│   - GeminiAPI.setApiKey()                │
│                                          │
│ getChatResponse()                        │
│   - withContext(Dispatchers.IO)          │──► IO thread
│   - buildChatPrompt()                    │
│   - GeminiAPI.generateContent()          │──► IO thread
│   - return response                      │
└──────────────────────────────────────────┘

MonsterChatDialogFragment
┌──────────────────────────────────────────┐
│ loadMonsterData()                        │
│   - lifecycleScope.launch(IO)            │──► IO thread
│   - Monster.fetchById()                  │──► Firebase thread
│   - withContext(Main)                    │──► Main thread
│   - generateWelcomeMessage()             │
│                                          │
│ sendMessage()                            │──► Main thread
│   - showLoading(true)                    │
│   - lifecycleScope.launch                │
│   - MonsterAI.getChatResponse()          │──► IO thread
│   - addMonsterMessage()                  │──► Main thread
│   - showLoading(false)                   │
└──────────────────────────────────────────┘