iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
Build on Google AI

《30天打造喵語日誌:Gemini API × Vibe Coding 實戰》系列 第 21

Day 21:【成就觸發邏輯】自動判定解鎖條件與即時彈窗:打造驚喜的遊戲化體驗!

  • 分享至 

  • xImage
  •  

從單純記錄到遊戲化回饋

昨天完成 Firestore 資料讀取與情緒圖表即時同步後,《喵語日誌》已經具備完整的情緒記錄與視覺化回顧能力。

不過,單純記錄情緒,長期下來仍然可能讓使用者失去新鮮感。

因此,今天要為 App 加入遊戲化成就系統,讓每一次日記記錄都不只是保存資料,也能轉化為一個可追蹤、可累積的目標。

這次的重點是建立事件驅動的成就判定流程:

送出日記
    ↓
Gemini 分析文字與圖片
    ↓
Firestore 寫入資料
    ↓
onSnapshot 取得最新日記
    ↓
重新計算成就進度
    ↓
偵測新解鎖成就
    ↓
顯示解鎖彈窗

當使用者達成條件時,畫面會立即跳出解鎖通知,提供即時的成就感與正向回饋。

成就系統資料結構

成就系統的核心不只是顯示徽章,更重要的是讓成就規則保持清楚、獨立,並且方便未來擴充。

目前在 src/data/achievements.ts 中定義了 Achievement 型別:

export type Achievement = {
  id: string;
  title: string;
  description: string;
  icon: string;
  isUnlocked: boolean;
  progress: number;
  total: number;
  unlockedDate?: string;
};

各欄位用途如下:

  • id:成就的唯一識別名稱

  • title:成就標題

  • description:解鎖條件說明

  • icon:徽章使用的 Emoji 圖示

  • isUnlocked:是否已經解鎖

  • progress:目前完成進度

  • total:解鎖所需的總進度

  • unlockedDate:成就解鎖日期

目前設計的成就條件涵蓋不同類型。

「初來乍到 🐾」屬於累積篇數型成就,完成第一筆日記即可解鎖;「持之以恆 📅」則是連續紀錄型成就,需要連續記錄心情達到指定天數。

另外,也加入與生活內容相關的成就。例如「美食家 🍣」會根據圖片與 Gemini 分析出的生活場景判斷;「情緒大師 🌈」則會統計使用者是否曾經記錄過所有指定的心情類型。

這樣的設計可以讓成就條件不只依賴單一數字,也能涵蓋使用頻率、內容類型、圖片紀錄與情緒多樣性。

動態成就判定引擎

接著,我建立純函式 calculateAchievements(diaries),負責根據目前所有日記資料,計算每個成就的解鎖狀態與完成進度。

這個函式不直接修改 React State,也不負責處理 UI,而是單純接收日記資料並回傳計算結果。

const achievements = calculateAchievements(diaries);

這種將規則與 UI 元件完全抽離的純函式設計,帶來許多架構上的優勢:不僅讓畫面單純專注於呈現,未來擴充規則時完全不必改動 UI,也能針對各個成就獨立編寫測試,甚至跨頁面重複調用同套邏輯。

每當 Firestore 傳回最新的日記清單時,引擎便會全面比對日記總筆數、連續天數、圖片數量、心情分佈以及 Gemini 辨識出的生活場景,即時產出最新的成就進度與狀態清單。

如何偵測「剛剛解鎖」?

完成計算後,面臨的一個關鍵問題是:如果每次資料更新都重新計算,系統要如何精確捕捉「當下這一刻才剛解鎖」的成就,避免每次重新載入時重複觸發彈窗?

為此,我在 App.tsx 中透過 useRef 保存先前已解鎖的成就 ID 集合:

const currentAchievements =
  calculateAchievements(data);

const currentUnlocked = currentAchievements.filter(
  (achievement) => achievement.isUnlocked
);

const currentUnlockedIds = new Set(
  currentUnlocked.map((achievement) => achievement.id)
);

if (isFirstLoadRef.current) {
  prevUnlockedIdsRef.current = currentUnlockedIds;
  isFirstLoadRef.current = false;
  return;
}

const newAchievement = currentUnlocked.find(
  (achievement) =>
    !prevUnlockedIdsRef.current.has(achievement.id)
);

if (newAchievement) {
  setNewlyUnlocked(newAchievement);
}

prevUnlockedIdsRef.current = currentUnlockedIds;

這段邏輯可以分成三個階段。

第一次載入

第一次取得 Firestore 資料時,先記錄目前已經解鎖的成就,但不顯示彈窗。

這樣可以避免使用者重新開啟 App 時,所有過去已完成的徽章再次跳出通知。

後續資料更新

當使用者新增一筆日記,onSnapshot 推送最新資料後,系統會重新計算目前的成就狀態。

找出新成就

將這次已解鎖的成就與上一次的 ID 集合進行比較,只要某個成就不在先前的清單中,就代表它是剛剛解鎖的成就。

接著將該成就存入 newlyUnlocked 狀態,交由解鎖彈窗元件顯示。

解鎖彈窗設計

確認判定機制運作順暢後,我建立了 AchievementModal.tsx。為了賦予解鎖時滿滿的儀式感,彈窗採用了 Zoom-in 放大進場動畫,並疊加毛玻璃遮罩與金色光暈效果,中央以大尺寸 Emoji 搭配醒目的標題與說明文字,下方則配置「收下徽章 🐾」按鈕。這讓解鎖不僅是資料狀態的改變,更是使用者每一次認真記錄生活時,與 App 之間明確而愉悅的正向互動。

成果驗收:美食日記觸發成就

為了測試圖片辨識與成就觸發流程,我在「喵喵對話」中上傳了一張義大利麵照片,選取「😻 Great」心情,並輸入:

今天跟好朋友一起去吃義大利麵!

送出後,Gemini 成功辨識出照片中的起司粉與金黃色麵條,並產生具有情境感的貓咪回覆。

https://ithelp.ithome.com.tw/upload/images/20260821/20178708NEcNLNLQCg.png
▲ 使用者上傳義大利麵照片,Gemini 辨識菜色並給予溫暖的陪伴回應。

接著,資料成功寫入 Firestore,onSnapshot 收到最新日記後,成就判定引擎立即重新計算。

當系統確認使用者達成條件後,畫面隨即顯示解鎖彈窗。

https://ithelp.ithome.com.tw/upload/images/20260821/20178708t8lujMBx8k.png
▲ 畫面立即跳出「持之以恆 📅」解鎖彈窗,搭配彈跳動畫與「收下徽章 🐾」按鈕。

點擊「收下徽章」後,再切換到「成就徽章」頁籤,可以看到收藏牆已經即時更新。

https://ithelp.ithome.com.tw/upload/images/20260821/20178708BHSh5oxJ3e.png
▲ 成就收藏牆即時更新,總完成度提升至 50%,目前共有 3 / 6 個成就解鎖。

從上傳照片、Gemini 影像與情緒分析、Firestore 寫入,到觸發監聽重新計分並跳出通知,整個資料流完全符合預期的事件驅動設計。

這次實作的心得

今天的實作讓《喵語日誌》從單純的情緒記錄工具,進一步變成具有回饋機制的互動應用。

當使用者完成一筆日記後,系統不只保存內容,也會根據使用者的行為進行判斷,並在適當的時機給予回饋。

這次特別重要的是,成就判定並不是寫死在某一個按鈕事件中,而是建立在資料更新流程上。

只要 Firestore 的日記資料發生變化,成就引擎就會重新計算。這種事件驅動的架構,讓成就系統與聊天室、資料庫及圖表之間保持清楚的分工,也方便未來新增更多條件。

未來如果想加入「連續使用 7 天」、「夜貓族」、「一週記錄 5 次」或「完成所有生活場景」等成就,只需要擴充判定規則即可,不必重新設計整個畫面。

結語

今天順利完成了成就自動判定與即時解鎖彈窗。從現在開始,每一次心情書寫都能累積為一枚實體化的徽章,在收藏牆上留下清晰的成長足跡。

目前《喵語日誌》的核心拼圖已大致就緒,從 Gemini 多模態分析、Firebase 匿名登入與雲端保存,到情緒圖表同步與成就系統皆已到位。明天將迎來第三週的小結,我們將進行前後端全鏈路整合測試,實際驗證從「拍照上傳」一路到「成就解鎖」的完整生命週期,並盤點整體資料流架構,見證《喵語日誌》真正活起來的完整樣貌。

明天見,喵~ 🐾


上一篇
Day 20:【數據動態同步】從 Firestore 讀取紀錄並渲染至情緒日曆與折線圖:狀態提升與即時監聽實戰!
系列文
《30天打造喵語日誌:Gemini API × Vibe Coding 實戰》21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言