9.3 KiB
Phone Badge System - Implementation Summary
Completed: 2025-10-30
Status: ✅ Fully Functional
Overview
The phone badge system provides a visual indicator of unread NPC messages on phone items in the inventory. The badge appears as a green number in the top-right corner of the phone icon, matching the phone's LCD screen aesthetic.
Features
1. Unread Message Indicator
- Visual Design: Green badge (#5fcf69) with black text and 2px border
- Position: Top-right corner of inventory slot (top: -5px, right: -5px)
- Content: Total count of unread NPC messages across all NPCs on that phone
- Styling: VT323 font, 20x20px, pixel-art aesthetic (no border-radius)
2. Dynamic Updates
Badge updates automatically when:
- Phone is added to inventory (with intro messages preloaded)
- Phone-chat minigame is closed (after reading messages)
- Timed messages are delivered to the phone
- Any NPC message is marked as read/unread
3. Intro Message Preloading
When a phone is added to inventory:
- Creates temporary InkEngine instance
- Loads Ink stories for all NPCs on the phone
- Navigates to start knot and gets intro message
- Adds intro messages to conversation history (marked as preloaded)
- Saves NPC story state to prevent replay
- Creates badge with correct initial count
Implementation Details
Files Modified
js/systems/inventory.js
// Import InkEngine for preloading
import InkEngine from './ink/ink-engine.js?v=1';
// Helper function to preload intro messages
async function preloadPhoneIntroMessages(phoneId) {
// Creates temp engine, loads stories, preloads intros
}
// Update badge with unread count
export function updatePhoneBadge(phoneId) {
// Finds phone items by phoneId
// Gets total unread count from NPCManager
// Creates/removes badge DOM element as needed
}
// When adding phone to inventory
if (sprite.scenarioData?.type === 'phone' && sprite.scenarioData?.phoneId) {
// Preload intro messages, then create badge
preloadPhoneIntroMessages(phoneId).then(() => {
// Create badge element if unread count > 0
});
}
Lines Added: ~90 lines (preload function + badge logic)
css/inventory.css
/* Phone badge styling */
.inventory-slot {
position: relative;
}
.inventory-slot .phone-badge {
display: block;
position: absolute;
top: -5px;
right: -5px;
background: #5fcf69; /* Phone LCD green */
color: #000;
border: 2px solid #000;
min-width: 20px;
height: 20px;
padding: 0 4px;
line-height: 16px;
text-align: center;
font-size: 12px;
font-weight: bold;
box-shadow: 0 2px 4px rgba(0,0,0,0.8);
z-index: 10;
border-radius: 0; /* Pixel-art aesthetic */
}
Lines Added: ~25 lines
js/systems/npc-manager.js
// Get total unread count for a specific phone
getTotalUnreadCount(phoneId) {
const npcs = this.getNPCsByPhone(phoneId);
let total = 0;
npcs.forEach(npc => {
const history = this.conversationHistory.get(npc.id) || [];
const unreadCount = history.filter(msg =>
msg.type === 'npc' && !msg.read
).length;
total += unreadCount;
});
return total;
}
// Register NPC with timed messages
registerNPC(id, opts = {}) {
// ... existing code ...
// Schedule timed messages if defined
if (entry.timedMessages && Array.isArray(entry.timedMessages)) {
entry.timedMessages.forEach(msg => {
this.scheduleTimedMessage({
npcId: realId,
text: msg.message,
delay: msg.delay,
phoneId: entry.phoneId
});
});
}
}
// Deliver timed message and update badge
_deliverTimedMessage(message) {
// Add message to history
this.addMessage(message.npcId, 'npc', message.text, { timed: true });
// Update phone badge
if (window.updatePhoneBadge && message.phoneId) {
window.updatePhoneBadge(message.phoneId);
}
// Show bark notification
// ...
}
Lines Modified: ~50 lines
js/minigames/phone-chat/phone-chat-minigame.js
complete() {
// Update phone badge when closing
if (window.updatePhoneBadge && this.phoneId) {
window.updatePhoneBadge(this.phoneId);
}
// ... rest of complete logic
}
async preloadIntroMessages() {
// ... preload logic ...
// Update phone badge after preloading
if (window.updatePhoneBadge && this.phoneId) {
window.updatePhoneBadge(this.phoneId);
}
}
Lines Modified: ~10 lines
Global Exports
// inventory.js
window.updatePhoneBadge = updatePhoneBadge;
// Usage anywhere:
window.updatePhoneBadge('player_phone');
Technical Decisions
Why Real DOM Elements Instead of CSS Pseudo-elements?
Initially attempted using ::after with content: attr(data-unread-count), but:
- Browser compatibility issues with CSS
attr()function attr()showing as strike-through in dev tools- Pseudo-elements harder to debug and manipulate
Solution: Create real <span class="phone-badge"> elements via JavaScript
- More reliable across browsers
- Easier to debug (visible in DOM inspector)
- Can be dynamically created/removed without CSS tricks
Why Preload Intro Messages?
Without preloading:
- Badge would show 0 on game load
- Badge would only update after opening phone once
- Poor UX - player wouldn't know there are messages
Solution: Preload intro messages when phone added to inventory
- Badge shows correct count immediately
- Messages already in history when phone opened
- Better UX - player sees indicator right away
Why Separate InkEngine Instances?
Each conversation needs its own story state:
- Variables are per-story
- Knots visited are tracked per-instance
- Multiple NPCs can't share the same engine
Solution: Create temporary engine for preloading
- Isolated from active conversations
- Clean state for each preload
- Matches phone-chat minigame pattern
Usage Examples
Scenario JSON with Timed Messages
{
"npcs": [
{
"id": "gossip_girl",
"displayName": "Gossip Girl",
"storyPath": "scenarios/ink/gossip-girl.json",
"phoneId": "player_phone",
"timedMessages": [
{
"delay": 5000,
"message": "Hey! 👋 Got any juicy gossip for me today?",
"type": "text"
}
]
}
],
"startItemsInInventory": [
{
"type": "phone",
"name": "Your Phone",
"phoneId": "player_phone",
"npcIds": ["neye_eve", "gossip_girl"]
}
]
}
Manual Badge Update
// After manually adding a message
window.npcManager.addMessage('npc_id', 'npc', 'New message text');
window.updatePhoneBadge('player_phone');
Testing
Test Scenario
- Load game with
ceo_exfil.jsonscenario - Badge should show "2" on phone (Neye Eve + Gossip Girl intros)
- After 5 seconds, badge updates to "3" (timed message from Gossip Girl)
- Bark notification appears above inventory
- Click phone in inventory
- Read all messages
- Close phone
- Badge disappears (count = 0)
Expected Behavior
- ✅ Badge appears immediately on load with correct count
- ✅ Badge updates when timed messages arrive
- ✅ Badge updates when phone closes after reading
- ✅ Badge disappears when all messages read
- ✅ Bark notifications trigger badge updates
Integration Points
For Game Systems
// When adding NPC message programmatically
window.npcManager.addMessage(npcId, 'npc', messageText);
window.updatePhoneBadge(phoneId);
// When marking messages as read
messages.forEach(msg => msg.read = true);
window.updatePhoneBadge(phoneId);
For Scenario Designers
// Define timed messages in NPC config
{
"timedMessages": [
{ "delay": 10000, "message": "First timed message" },
{ "delay": 30000, "message": "Second timed message" }
]
}
Known Limitations
-
Badge position: Fixed at top-right of slot
- Works for all inventory items
- May overlap if item has very wide sprite
-
Count display: Shows total number
- No breakdown by NPC
- No indication of message priority
-
Phone detection: Uses
data-phone-idattribute- Must be set when phone added to inventory
- No fallback if attribute missing
Future Enhancements
Potential Improvements
- Different badge colors for urgent messages
- Animated badge pulse when new message arrives
- Breakdown tooltip (e.g., "2 from Alice, 1 from Bob")
- Badge on phone button in bottom-right corner
- Sound effect when badge count increases
Alternative Designs Considered
- Multiple badges: One per NPC (rejected - too cluttered)
- Badge animation: Pulse/glow effect (deferred - keep simple)
- Badge on phone button: Global phone access (planned for Phase 3)
Documentation Updates
- ✅
01_IMPLEMENTATION_LOG.md- Added badge system section - ✅
PHONE_BADGE_FEATURE.md- This document - ✅ Code comments in inventory.js, npc-manager.js
- ✅ Updated bug fixes list
Summary
The phone badge system successfully provides visual feedback for unread NPC messages. The implementation uses real DOM elements for reliability, preloads intro messages for immediate feedback, and integrates seamlessly with the existing NPC/phone-chat systems.
Total Implementation Time: ~4 hours
Total Lines Added/Modified: ~175 lines across 5 files
Bug Fixes Required: 4 (CSS attr(), InkEngine import, preload timing, timed message delays)
Status: ✅ Complete and tested in ceo_exfil.json scenario