6.4 KiB
Skill Tree Milestone Popups
Context
When users reach major skill tree milestones, they should see celebratory popups explaining what they've achieved and what's next. Four milestone types:
- Lowercase complete — all 26 lowercase keys mastered, other branches become available
- Branch complete — a non-lowercase branch fully mastered
- All keys unlocked — every key on the keyboard is available for practice
- All keys mastered — every key at full confidence, ultimate achievement
These popups appear after key unlock/mastery popups and before the drill summary screen, using the existing milestone_queue system. The existing post-drill input lock (800ms) applies to these popups when they're the first popup shown after a drill.
Implementation
1. Extend SkillTreeUpdate (src/engine/skill_tree.rs)
Add fields to SkillTreeUpdate:
pub branches_newly_available: Vec<BranchId>, // Locked → Available transitions
pub branches_newly_completed: Vec<BranchId>, // → Complete transitions
pub all_keys_unlocked: bool, // every key now in practice pool
pub all_keys_mastered: bool, // every key at confidence >= 1.0
In update():
- Snapshot non-lowercase branch statuses before the auto-unlock loop. After it, collect
Locked→Availabletransitions intobranches_newly_available. - Snapshot all branch statuses before updates. After
update_lowercase()and allupdate_branch_level()calls, collect branches that becameCompleteintobranches_newly_completed. all_keys_unlocked: comparetotal_unlocked_count()againstcompute_total_unique_keys(). Set totrueonly if they're equal now AND they weren't equal before (using a before-snapshot of unlocked count).all_keys_mastered:trueif every branch inALL_BRANCHEShasBranchStatus::Completeafter updates AND at least one wasn'tCompletebefore.
BranchId is already used across all layers. Display names come from get_branch_definition(id).name.
2. Add milestone variants to MilestoneKind (src/app.rs)
pub enum MilestoneKind {
Unlock,
Mastery,
BranchesAvailable, // lowercase complete → other branches available
BranchComplete, // a non-lowercase branch fully completed
AllKeysUnlocked, // every key on the keyboard is unlocked
AllKeysMastered, // every key at full confidence
}
In finish_drill(), after mastery popup queueing, check each flag and push popups in order:
branches_newly_availablenon-empty → pushBranchesAvailablebranches_newly_completednon-empty (excludingBranchId::LowercasesinceBranchesAvailablecovers it) → pushBranchCompleteall_keys_unlocked→ pushAllKeysUnlockedall_keys_mastered→ pushAllKeysMastered
For all four: keys and finger_info are empty, message is unused. The renderer owns all copy.
Input lock: These popups are pushed to milestone_queue, so the existing check !self.milestone_queue.is_empty() at finish_drill() already triggers arm_post_drill_input_lock(). No changes needed — the lock applies to whatever the first popup is.
3. Render popup variants in render_milestone_overlay() (src/main.rs)
Each variant gets its own rendering branch. No keyboard diagram for any of these. All use the standard footer (input lock remaining / "Press any key to continue").
BranchesAvailable:
- Title:
"New Skill Branches Available!" - Body:
(Branch names rendered dynamically from
Congratulations! You've mastered all 26 lowercase keys! New skill branches are now available: • Capitals A-Z • Numbers 0-9 • Prose Punctuation • Whitespace • Code Symbols Visit the Skill Tree to unlock a new branch and start training! Press [t] from the menu to open the Skill Treeget_branch_definition(id).namefor each ID inbranches_newly_available.)
BranchComplete:
- Title:
"Branch Complete!" - Body:
(If multiple branches completed simultaneously, list them all: "You've fully mastered the {name1} and {name2} branches!")
You've fully mastered the {branch_name} branch! Other branches are waiting to be unlocked in the Skill Tree. Keep going! Press [t] from the menu to open the Skill Tree
AllKeysUnlocked:
- Title:
"Every Key Unlocked!" - Body:
You've unlocked every key on the keyboard! All keys are now part of your practice drills. Keep training to build full confidence with each key!
AllKeysMastered:
- Title:
"Full Keyboard Mastery!" - Body:
Incredible! You've reached full confidence with every single key on the keyboard! You've completed everything keydr has to teach. Keep practicing to maintain your skills!
4. Sequencing
Queue order in finish_drill():
- Key unlock popups (existing)
- Key mastery popups (existing)
BranchesAvailable(if applicable)BranchComplete(if applicable, excluding lowercase)AllKeysUnlocked(if applicable)AllKeysMastered(if applicable)
The input lock is armed once when milestone_queue is non-empty (existing logic). User dismisses each popup with any keypress.
5. Tests
In src/engine/skill_tree.rs tests:
branches_newly_availablenon-empty on firstupdate()after lowercase completion, empty on second callbranches_newly_completedcontains the branch ID when a non-lowercase branch completesall_keys_unlockedfires when the last key becomes available, not on subsequent callsall_keys_masteredfires when all branches reach Complete, not on subsequent callsbranches_newly_availableonly contains the five non-lowercase branch IDs
In src/app.rs tests:
- Queue order test: last lowercase key mastered → queue contains unlock → mastery → BranchesAvailable (no BranchComplete for lowercase)
- Branch complete test: non-lowercase branch completes → BranchComplete queued
- Helper:
seed_near_complete_lowercase(app)— 25 keys at confidence 1.0, last key at 0.95
Files to Modify
src/engine/skill_tree.rs— ExtendSkillTreeUpdate, detect transitions inupdate()src/app.rs— Add variants toMilestoneKind, queue popups infinish_drill()src/main.rs— Render the four new popup variants inrender_milestone_overlay()
Verification
cargo build— compiles cleanlycargo test— all existing + new tests pass- Manual testing with test profiles for each milestone scenario