mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
feat: Implement phone badge for unread messages; add timed messages for NPCs and update inventory UI
This commit is contained in:
@@ -118,4 +118,29 @@
|
||||
/* Hide count badge for single keys */
|
||||
.inventory-item[data-type="key_ring"][data-key-count="1"]::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Phone unread message badge */
|
||||
.inventory-slot {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inventory-slot .phone-badge {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
background: #5fcf69; /* Green to match phone LCD screen */
|
||||
color: #000;
|
||||
border: 2px solid #000;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 4px;
|
||||
line-height: 16px; /* Center text vertically (20px - 2px border * 2 = 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; /* Maintain pixel-art aesthetic */
|
||||
}
|
||||
|
||||
@@ -1,110 +1,63 @@
|
||||
/* NPC Bark Notifications */
|
||||
|
||||
@keyframes bark-slide-in {
|
||||
/* Bark container positioned above inventory */
|
||||
#npc-bark-container {
|
||||
position: fixed;
|
||||
bottom: 80px; /* Above inventory bar */
|
||||
left: 20px;
|
||||
z-index: 9999 !important;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
flex-direction: column-reverse; /* Stack upward */
|
||||
gap: 8px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
/* Individual bark notification styled like phone message */
|
||||
.npc-bark {
|
||||
background: #5fcf69; /* Phone screen green */
|
||||
color: #000;
|
||||
padding: 12px 15px;
|
||||
border: 2px solid #000;
|
||||
font-family: 'VT323', monospace;
|
||||
font-size: 18px;
|
||||
line-height: 1.4;
|
||||
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3);
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s, box-shadow 0.1s;
|
||||
word-wrap: break-word;
|
||||
animation: bark-slide-up 0.3s ease-out;
|
||||
}
|
||||
|
||||
.npc-bark:hover {
|
||||
transform: translate(-2px, -2px);
|
||||
box-shadow: 5px 5px 0 rgba(0, 0, 0, 0.3);
|
||||
background: #6fe079;
|
||||
}
|
||||
|
||||
@keyframes bark-slide-up {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bark-slide-out {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(400px);
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.npc-bark-notification {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
width: 320px;
|
||||
background: #fff;
|
||||
border: 2px solid #000;
|
||||
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.3);
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
animation: bark-slide-in 0.3s ease-out;
|
||||
z-index: 9999;
|
||||
font-family: 'VT323', monospace;
|
||||
transition: transform 0.1s, box-shadow 0.1s;
|
||||
}
|
||||
|
||||
.npc-bark-notification:hover {
|
||||
background: #f0f0f0;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 4px 6px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.npc-bark-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: crisp-edges;
|
||||
border: 2px solid #000;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.npc-bark-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.npc-bark-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #000;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.npc-bark-message {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.npc-bark-close {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #ff0000;
|
||||
color: #fff;
|
||||
border: 2px solid #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
transition: background-color 0.1s;
|
||||
}
|
||||
|
||||
.npc-bark-close:hover {
|
||||
background: #cc0000;
|
||||
}
|
||||
|
||||
/* Ensure barks appear above inventory */
|
||||
#npc-bark-container {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
/* Phone access button (will be added later) */
|
||||
.phone-access-button {
|
||||
position: fixed;
|
||||
|
||||
@@ -274,6 +274,11 @@ export class PhoneChatMinigame extends MinigameScene {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update phone badge after preloading messages
|
||||
if (window.updatePhoneBadge && this.phoneId) {
|
||||
window.updatePhoneBadge(this.phoneId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -665,6 +670,11 @@ export class PhoneChatMinigame extends MinigameScene {
|
||||
// Clean up conversation
|
||||
this.isConversationActive = false;
|
||||
|
||||
// Update phone badge in inventory
|
||||
if (window.updatePhoneBadge && this.phoneId) {
|
||||
window.updatePhoneBadge(this.phoneId);
|
||||
}
|
||||
|
||||
// Call parent complete
|
||||
super.complete(success);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Handles inventory management and display
|
||||
|
||||
import { rooms } from '../core/rooms.js';
|
||||
import InkEngine from './ink/ink-engine.js?v=1';
|
||||
|
||||
// Helper function to create a unique identifier for an item
|
||||
export function createItemIdentifier(scenarioData) {
|
||||
@@ -37,6 +38,71 @@ export function initializeInventory() {
|
||||
console.log('INVENTORY INITIALIZED', window.inventory);
|
||||
}
|
||||
|
||||
// Helper function to preload intro messages for a phone
|
||||
async function preloadPhoneIntroMessages(phoneId) {
|
||||
console.log(`📱 preloadPhoneIntroMessages called for ${phoneId}`);
|
||||
|
||||
if (!window.npcManager) {
|
||||
console.warn('❌ npcManager not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Import PhoneChatConversation class (default export)
|
||||
const PhoneChatConversation = (await import('../minigames/phone-chat/phone-chat-conversation.js')).default;
|
||||
|
||||
// Create a temporary ink engine for preloading
|
||||
const tempEngine = new InkEngine();
|
||||
|
||||
const npcs = window.npcManager.getNPCsByPhone(phoneId);
|
||||
console.log(`📱 Found ${npcs.length} NPCs on phone ${phoneId}:`, npcs.map(n => n.id));
|
||||
|
||||
for (const npc of npcs) {
|
||||
const history = window.npcManager.getConversationHistory(npc.id);
|
||||
console.log(`📱 NPC ${npc.id}: history length = ${history.length}, has story = ${!!(npc.storyPath || npc.storyJSON)}`);
|
||||
|
||||
// Only preload if no history exists and NPC has a story
|
||||
if (history.length === 0 && (npc.storyPath || npc.storyJSON)) {
|
||||
try {
|
||||
console.log(`📱 Preloading intro for ${npc.id}...`);
|
||||
const tempConversation = new PhoneChatConversation(npc.id, window.npcManager, tempEngine);
|
||||
const storySource = npc.storyJSON || npc.storyPath;
|
||||
const loaded = await tempConversation.loadStory(storySource);
|
||||
|
||||
if (loaded) {
|
||||
const startKnot = npc.currentKnot || 'start';
|
||||
tempConversation.goToKnot(startKnot);
|
||||
const result = tempConversation.continue();
|
||||
|
||||
if (result.text && result.text.trim()) {
|
||||
const messages = result.text.trim().split('\n').filter(line => line.trim());
|
||||
console.log(`📱 Adding ${messages.length} preloaded messages for ${npc.id}`);
|
||||
messages.forEach(message => {
|
||||
if (message.trim()) {
|
||||
window.npcManager.addMessage(npc.id, 'npc', message.trim(), {
|
||||
preloaded: true,
|
||||
timestamp: Date.now() - 3600000 // 1 hour ago
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
npc.storyState = tempConversation.saveState();
|
||||
console.log(`✅ Preloaded intro for ${npc.id}`);
|
||||
} else {
|
||||
console.log(`⚠️ No intro text for ${npc.id}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`⚠️ Failed to load story for ${npc.id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Error preloading intro for ${npc.id}:`, error);
|
||||
}
|
||||
} else {
|
||||
console.log(`⏭️ Skipping ${npc.id} - history=${history.length}, story=${!!(npc.storyPath || npc.storyJSON)}`);
|
||||
}
|
||||
}
|
||||
console.log(`📱 Finished preloading for phone ${phoneId}`);
|
||||
}
|
||||
|
||||
// Process initial inventory items
|
||||
export function processInitialInventoryItems() {
|
||||
console.log('Processing initial inventory items');
|
||||
@@ -190,6 +256,37 @@ export function addToInventory(sprite) {
|
||||
itemImg.requires = sprite.scenarioData?.requires;
|
||||
itemImg.difficulty = sprite.scenarioData?.difficulty;
|
||||
|
||||
// Add data-type attribute for CSS styling
|
||||
itemImg.setAttribute('data-type', sprite.scenarioData?.type);
|
||||
|
||||
// For phones, add unread message count and badge
|
||||
if (sprite.scenarioData?.type === 'phone' && sprite.scenarioData?.phoneId) {
|
||||
const phoneId = sprite.scenarioData.phoneId;
|
||||
itemImg.setAttribute('data-phone-id', phoneId);
|
||||
|
||||
if (window.npcManager) {
|
||||
// Preload intro messages for all NPCs on this phone
|
||||
preloadPhoneIntroMessages(phoneId).then(() => {
|
||||
const unreadCount = window.npcManager.getTotalUnreadCount(phoneId);
|
||||
console.log(`📱 Phone ${phoneId} added to inventory, unread count: ${unreadCount}`);
|
||||
itemImg.setAttribute('data-unread-count', unreadCount);
|
||||
|
||||
// Create badge element if there are unread messages
|
||||
if (unreadCount > 0) {
|
||||
console.log(`✅ Creating badge for phone ${phoneId}`);
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'phone-badge';
|
||||
badge.textContent = unreadCount;
|
||||
itemImg.parentElement.appendChild(badge);
|
||||
} else {
|
||||
console.log(`❌ Not creating badge, count is ${unreadCount}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('❌ npcManager not available when adding phone');
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as non-takeable once in inventory (so it won't try to be picked up again)
|
||||
itemImg.scenarioData.takeable = false;
|
||||
|
||||
@@ -482,6 +579,41 @@ export function removeFromInventory(item) {
|
||||
}
|
||||
}
|
||||
|
||||
// Update phone badge with unread count
|
||||
export function updatePhoneBadge(phoneId) {
|
||||
if (!window.npcManager) return;
|
||||
|
||||
// Find phone items in inventory
|
||||
const phoneItems = window.inventory.items.filter(item =>
|
||||
item.scenarioData?.type === 'phone' &&
|
||||
item.getAttribute('data-phone-id') === phoneId
|
||||
);
|
||||
|
||||
// Update badge for each phone with this ID
|
||||
phoneItems.forEach(phoneItem => {
|
||||
const unreadCount = window.npcManager.getTotalUnreadCount(phoneId);
|
||||
phoneItem.setAttribute('data-unread-count', unreadCount);
|
||||
|
||||
// Get the inventory slot (parent element)
|
||||
const inventorySlot = phoneItem.parentElement;
|
||||
if (!inventorySlot) return;
|
||||
|
||||
// Remove existing badge if present
|
||||
const existingBadge = inventorySlot.querySelector('.phone-badge');
|
||||
if (existingBadge) {
|
||||
existingBadge.remove();
|
||||
}
|
||||
|
||||
// Create new badge if there are unread messages
|
||||
if (unreadCount > 0) {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'phone-badge';
|
||||
badge.textContent = unreadCount;
|
||||
inventorySlot.appendChild(badge);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Export for global access
|
||||
window.initializeInventory = initializeInventory;
|
||||
window.processInitialInventoryItems = processInitialInventoryItems;
|
||||
@@ -489,4 +621,5 @@ window.addToInventory = addToInventory;
|
||||
window.removeFromInventory = removeFromInventory;
|
||||
window.addNotepadToInventory = addNotepadToInventory;
|
||||
window.createItemIdentifier = createItemIdentifier;
|
||||
window.handleKeyRingInteraction = handleKeyRingInteraction;
|
||||
window.handleKeyRingInteraction = handleKeyRingInteraction;
|
||||
window.updatePhoneBadge = updatePhoneBadge;
|
||||
@@ -13,65 +13,48 @@ export default class NPCBarkSystem {
|
||||
if (!this.container) {
|
||||
this.container = document.createElement('div');
|
||||
this.container.id = 'npc-bark-container';
|
||||
const style = this.container.style;
|
||||
style.position = 'fixed';
|
||||
style.right = '12px';
|
||||
style.top = '12px';
|
||||
style.zIndex = 9999;
|
||||
style.pointerEvents = 'auto';
|
||||
document.body.appendChild(this.container);
|
||||
}
|
||||
}
|
||||
|
||||
// payload: { npcId, text|message, duration, onClick, openPhone }
|
||||
// payload: { npcId, npcName, text|message, duration, onClick, openPhone }
|
||||
showBark(payload = {}) {
|
||||
if (!this.container) this.init();
|
||||
const { npcId, npcName } = payload;
|
||||
const text = payload.text || payload.message || '';
|
||||
const duration = ('duration' in payload) ? payload.duration : 4000;
|
||||
const duration = ('duration' in payload) ? payload.duration : 5000;
|
||||
|
||||
// Create bark element
|
||||
const el = document.createElement('div');
|
||||
el.className = 'npc-bark';
|
||||
el.textContent = (npcId ? npcId + ': ' : '') + (text || '...');
|
||||
// basic styling
|
||||
el.style.background = 'rgba(0,0,0,0.8)';
|
||||
el.style.color = 'white';
|
||||
el.style.padding = '8px 12px';
|
||||
el.style.marginTop = '8px';
|
||||
el.style.borderRadius = '4px';
|
||||
el.style.fontFamily = 'sans-serif';
|
||||
el.style.fontSize = '13px';
|
||||
el.style.maxWidth = '320px';
|
||||
el.style.boxShadow = '0 2px 6px rgba(0,0,0,0.5)';
|
||||
el.style.transition = 'all 0.2s';
|
||||
|
||||
// Format: "Name: message"
|
||||
const displayName = npcName || npcId || 'NPC';
|
||||
el.textContent = `${displayName}: ${text}`;
|
||||
|
||||
this.container.appendChild(el);
|
||||
|
||||
// Handle clicks - either custom handler or auto-open phone
|
||||
if (typeof payload.onClick === 'function') {
|
||||
el.style.cursor = 'pointer';
|
||||
el.addEventListener('click', () => payload.onClick(el));
|
||||
} else if (payload.openPhone !== false && npcId) {
|
||||
// Default: clicking bark opens phone chat with this NPC
|
||||
el.style.cursor = 'pointer';
|
||||
el.addEventListener('click', () => {
|
||||
this.openPhoneChat(payload);
|
||||
// Remove bark when clicked
|
||||
if (el.parentNode) el.parentNode.removeChild(el);
|
||||
});
|
||||
|
||||
// Add visual hint that it's clickable
|
||||
el.addEventListener('mouseenter', () => {
|
||||
el.style.background = 'rgba(74, 158, 255, 0.9)';
|
||||
el.style.transform = 'scale(1.05)';
|
||||
});
|
||||
el.addEventListener('mouseleave', () => {
|
||||
el.style.background = 'rgba(0,0,0,0.8)';
|
||||
el.style.transform = 'scale(1)';
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-remove after duration
|
||||
setTimeout(() => {
|
||||
if (el && el.parentNode) el.parentNode.removeChild(el);
|
||||
if (el && el.parentNode) {
|
||||
// Fade out animation
|
||||
el.style.animation = 'bark-slide-out 0.3s ease-out';
|
||||
setTimeout(() => {
|
||||
if (el.parentNode) el.parentNode.removeChild(el);
|
||||
}, 300);
|
||||
}
|
||||
}, duration);
|
||||
return el;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,19 @@ export default class NPCManager {
|
||||
this._setupEventMappings(realId, entry.eventMappings);
|
||||
}
|
||||
|
||||
// Schedule timed messages if any are 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
|
||||
});
|
||||
});
|
||||
console.log(`[NPCManager] Scheduled ${entry.timedMessages.length} timed messages for ${realId}`);
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -95,6 +108,20 @@ export default class NPCManager {
|
||||
return Array.from(this.npcs.values()).filter(npc => npc.phoneId === phoneId);
|
||||
}
|
||||
|
||||
// Get total unread message count for a phone
|
||||
getTotalUnreadCount(phoneId) {
|
||||
const npcs = this.getNPCsByPhone(phoneId);
|
||||
let totalUnread = 0;
|
||||
|
||||
for (const npc of npcs) {
|
||||
const history = this.getConversationHistory(npc.id);
|
||||
const unreadCount = history.filter(msg => !msg.read && msg.type === 'npc').length;
|
||||
totalUnread += unreadCount;
|
||||
}
|
||||
|
||||
return totalUnread;
|
||||
}
|
||||
|
||||
// Set up event listeners for an NPC's event mappings
|
||||
_setupEventMappings(npcId, eventMappings) {
|
||||
if (!this.eventDispatcher) return;
|
||||
@@ -221,24 +248,27 @@ export default class NPCManager {
|
||||
}
|
||||
|
||||
// Schedule a timed message to be delivered after a delay
|
||||
// opts: { npcId, text, triggerTime (ms from game start), phoneId }
|
||||
// opts: { npcId, text, triggerTime (ms from game start) OR delay (ms from now), phoneId }
|
||||
scheduleTimedMessage(opts) {
|
||||
const { npcId, text, triggerTime = 0, phoneId } = opts;
|
||||
const { npcId, text, triggerTime, delay, phoneId } = opts;
|
||||
|
||||
if (!npcId || !text) {
|
||||
console.error('[NPCManager] scheduleTimedMessage requires npcId and text');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use triggerTime if provided, otherwise use delay (defaults to 0)
|
||||
const actualTriggerTime = triggerTime !== undefined ? triggerTime : (delay || 0);
|
||||
|
||||
this.timedMessages.push({
|
||||
npcId,
|
||||
text,
|
||||
triggerTime, // milliseconds from game start
|
||||
triggerTime: actualTriggerTime, // milliseconds from game start
|
||||
phoneId: phoneId || 'player_phone',
|
||||
delivered: false
|
||||
});
|
||||
|
||||
console.log(`[NPCManager] Scheduled timed message from ${npcId} at ${triggerTime}ms:`, text);
|
||||
console.log(`[NPCManager] Scheduled timed message from ${npcId} at ${actualTriggerTime}ms:`, text);
|
||||
}
|
||||
|
||||
// Start checking for timed messages (call this when game starts)
|
||||
@@ -292,6 +322,11 @@ export default class NPCManager {
|
||||
phoneId: message.phoneId
|
||||
});
|
||||
|
||||
// Update phone badge if updatePhoneBadge function exists
|
||||
if (window.updatePhoneBadge && message.phoneId) {
|
||||
window.updatePhoneBadge(message.phoneId);
|
||||
}
|
||||
|
||||
// Show bark notification
|
||||
if (this.barkSystem) {
|
||||
this.barkSystem.showBark({
|
||||
|
||||
@@ -18,7 +18,14 @@
|
||||
"avatar": null,
|
||||
"phoneId": "player_phone",
|
||||
"currentKnot": "start",
|
||||
"npcType": "phone"
|
||||
"npcType": "phone",
|
||||
"timedMessages": [
|
||||
{
|
||||
"delay": 5000,
|
||||
"message": "Hey! 👋 Got any juicy gossip for me today?",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"startItemsInInventory": [
|
||||
|
||||
Reference in New Issue
Block a user