diff --git a/css/rfid-minigame.css b/css/rfid-minigame.css new file mode 100644 index 0000000..c901b74 --- /dev/null +++ b/css/rfid-minigame.css @@ -0,0 +1,376 @@ +/** + * RFID Minigame CSS + * Flipper Zero-inspired RFID reader/cloner interface + */ + +/* Container */ +.rfid-minigame-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background: rgba(0, 0, 0, 0.9); + z-index: 1000; +} + +.rfid-minigame-game-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +/* Flipper Zero Device */ +.flipper-zero-frame { + width: 400px; + height: 550px; + background: #FF8200; + border-radius: 20px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + padding: 20px; + display: flex; + flex-direction: column; + font-family: 'Courier New', monospace; +} + +/* Header */ +.flipper-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 2px solid rgba(255, 255, 255, 0.3); +} + +.flipper-logo { + font-size: 14px; + font-weight: bold; + color: white; + letter-spacing: 1px; +} + +.flipper-battery { + font-size: 12px; + color: white; +} + +/* Screen */ +.flipper-screen { + flex: 1; + background: #333; + border-radius: 10px; + padding: 15px; + color: white; + font-size: 14px; + overflow-y: auto; + box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; +} + +/* Breadcrumb */ +.flipper-breadcrumb { + font-size: 12px; + color: #FFA500; + margin-bottom: 15px; + font-weight: bold; +} + +/* Menu */ +.flipper-menu { + display: flex; + flex-direction: column; + gap: 8px; +} + +.flipper-menu-item { + padding: 8px 10px; + background: rgba(255, 255, 255, 0.05); + border-radius: 5px; + cursor: pointer; + transition: background 0.2s; + user-select: none; +} + +.flipper-menu-item:hover { + background: rgba(255, 255, 255, 0.15); +} + +/* Info Text */ +.flipper-info { + color: white; + margin: 10px 0; + text-align: center; +} + +.flipper-info-dim { + color: #888; + margin: 10px 0; + text-align: center; + font-size: 12px; +} + +/* Card List */ +.flipper-card-list { + display: flex; + flex-direction: column; + gap: 6px; + margin-top: 15px; + max-height: 300px; + overflow-y: auto; +} + +/* Card Name */ +.flipper-card-name { + font-size: 16px; + font-weight: bold; + color: #FFA500; + margin: 10px 0; + text-align: center; +} + +/* Card Data */ +.flipper-card-data { + background: rgba(0, 0, 0, 0.3); + padding: 15px; + border-radius: 8px; + margin: 15px 0; + font-size: 13px; + line-height: 1.8; +} + +.flipper-card-data div { + margin: 5px 0; +} + +/* Buttons */ +.flipper-buttons { + display: flex; + gap: 10px; + margin-top: auto; + padding-top: 15px; +} + +.flipper-button { + flex: 1; + padding: 12px; + background: #FF8200; + color: white; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s; + font-family: 'Courier New', monospace; +} + +.flipper-button:hover { + background: #FFA500; + transform: translateY(-2px); +} + +.flipper-button-secondary { + background: #555; +} + +.flipper-button-secondary:hover { + background: #777; +} + +.flipper-button-back { + margin-top: auto; + padding: 10px; + color: #FFA500; + cursor: pointer; + text-align: center; + user-select: none; +} + +.flipper-button-back:hover { + color: white; +} + +/* NFC Waves */ +.rfid-nfc-waves-container { + display: flex; + justify-content: center; + align-items: center; + margin: 30px 0; +} + +.rfid-nfc-icon { + font-size: 48px; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.1); + opacity: 0.8; + } +} + +.rfid-nfc-waves { + position: relative; + width: 100px; + height: 100px; +} + +.rfid-nfc-wave { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 20px; + height: 20px; + border: 2px solid #FF8200; + border-radius: 50%; + animation: wave 1.5s infinite; +} + +@keyframes wave { + 0% { + width: 20px; + height: 20px; + opacity: 1; + } + 100% { + width: 100px; + height: 100px; + opacity: 0; + } +} + +/* Progress Bar */ +.rfid-progress-container { + width: 100%; + height: 20px; + background: rgba(0, 0, 0, 0.3); + border-radius: 10px; + overflow: hidden; + margin: 20px 0; +} + +.rfid-progress-bar { + height: 100%; + background: #FF8200; + transition: width 0.1s linear, background-color 0.3s; + border-radius: 10px; +} + +/* Emulation */ +.rfid-emulate-icon { + font-size: 64px; + text-align: center; + margin: 20px 0; + animation: pulse 1.5s infinite; +} + +.flipper-emulating { + color: #00FF00; + text-align: center; + margin: 15px 0; + font-weight: bold; + animation: blink 1s infinite; +} + +@keyframes blink { + 0%, 50%, 100% { + opacity: 1; + } + 25%, 75% { + opacity: 0.5; + } +} + +/* Success/Error Messages */ +.flipper-success, +.flipper-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; +} + +.flipper-success-icon, +.flipper-error-icon { + font-size: 72px; + margin-bottom: 20px; +} + +.flipper-success-icon { + color: #00FF00; +} + +.flipper-error-icon { + color: #FF0000; +} + +.flipper-success-message, +.flipper-error-message { + font-size: 18px; + font-weight: bold; +} + +.flipper-success-message { + color: #00FF00; +} + +.flipper-error-message { + color: #FF0000; +} + +/* Scrollbar */ +.flipper-screen::-webkit-scrollbar { + width: 8px; +} + +.flipper-screen::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.2); + border-radius: 4px; +} + +.flipper-screen::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.3); + border-radius: 4px; +} + +.flipper-screen::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.5); +} + +.flipper-card-list::-webkit-scrollbar { + width: 6px; +} + +.flipper-card-list::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.2); + border-radius: 3px; +} + +.flipper-card-list::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.3); + border-radius: 3px; +} + +/* Responsive */ +@media (max-width: 500px) { + .flipper-zero-frame { + width: 90%; + height: 80vh; + min-height: 500px; + } +} diff --git a/js/minigames/index.js b/js/minigames/index.js index ada6010..9bbc8af 100644 --- a/js/minigames/index.js +++ b/js/minigames/index.js @@ -15,6 +15,7 @@ export { PinMinigame, startPinMinigame } from './pin/pin-minigame.js'; export { PasswordMinigame } from './password/password-minigame.js'; export { TextFileMinigame, returnToTextFileAfterNotes } from './text-file/text-file-minigame.js'; export { TitleScreenMinigame, startTitleScreenMinigame } from './title-screen/title-screen-minigame.js'; +export { RFIDMinigame, startRFIDMinigame, returnToConversationAfterRFID } from './rfid/rfid-minigame.js'; // Initialize the global minigame framework for backward compatibility import { MinigameFramework } from './framework/minigame-manager.js'; @@ -70,8 +71,13 @@ import { PasswordMinigame } from './password/password-minigame.js'; // Import the text file minigame import { TextFileMinigame, returnToTextFileAfterNotes } from './text-file/text-file-minigame.js'; +<<<<<<< HEAD // Import the title screen minigame import { TitleScreenMinigame, startTitleScreenMinigame } from './title-screen/title-screen-minigame.js'; +======= +// Import the RFID minigame +import { RFIDMinigame, startRFIDMinigame, returnToConversationAfterRFID } from './rfid/rfid-minigame.js'; +>>>>>>> a4e2561 (feat(rfid): Implement core RFID minigame system) // Register minigames MinigameFramework.registerScene('lockpicking', LockpickingMinigamePhaser); // Use Phaser version as default @@ -86,7 +92,11 @@ MinigameFramework.registerScene('person-chat', PersonChatMinigame); MinigameFramework.registerScene('pin', PinMinigame); MinigameFramework.registerScene('password', PasswordMinigame); MinigameFramework.registerScene('text-file', TextFileMinigame); +<<<<<<< HEAD MinigameFramework.registerScene('title-screen', TitleScreenMinigame); +======= +MinigameFramework.registerScene('rfid', RFIDMinigame); +>>>>>>> a4e2561 (feat(rfid): Implement core RFID minigame system) // Make minigame functions available globally window.startNotesMinigame = startNotesMinigame; @@ -99,4 +109,9 @@ window.returnToConversationAfterNPCInventory = returnToConversationAfterNPCInven window.returnToPhoneAfterNotes = returnToPhoneAfterNotes; window.returnToTextFileAfterNotes = returnToTextFileAfterNotes; window.startPinMinigame = startPinMinigame; -window.startTitleScreenMinigame = startTitleScreenMinigame; \ No newline at end of file +<<<<<<< HEAD +window.startTitleScreenMinigame = startTitleScreenMinigame; +======= +window.startRFIDMinigame = startRFIDMinigame; +window.returnToConversationAfterRFID = returnToConversationAfterRFID; +>>>>>>> a4e2561 (feat(rfid): Implement core RFID minigame system) diff --git a/js/minigames/rfid/rfid-animations.js b/js/minigames/rfid/rfid-animations.js new file mode 100644 index 0000000..9924aee --- /dev/null +++ b/js/minigames/rfid/rfid-animations.js @@ -0,0 +1,103 @@ +/** + * RFID Animations + * + * Handles animation effects for RFID minigame: + * - Card reading progress animation + * - Tap success/failure animations + * - NFC wave animations + * - Emulation success/failure animations + * + * @module rfid-animations + */ + +export class RFIDAnimations { + constructor(minigame) { + this.minigame = minigame; + this.activeIntervals = []; + console.log('✨ RFIDAnimations initialized'); + } + + /** + * Animate card reading progress + * @param {Function} progressCallback - Called with progress (0-100) + * @returns {Promise} Resolves when reading complete + */ + animateReading(progressCallback) { + return new Promise((resolve) => { + let progress = 0; + const interval = setInterval(() => { + progress += 2; + progressCallback(progress); + + if (progress >= 100) { + clearInterval(interval); + this.activeIntervals = this.activeIntervals.filter(i => i !== interval); + resolve(); + } + }, 50); // 2% every 50ms = 2.5 seconds total + + this.activeIntervals.push(interval); + }); + } + + /** + * Show tap success animation + */ + showTapSuccess() { + console.log('✅ Tap success'); + // Visual feedback handled by UI layer + } + + /** + * Show tap failure animation + */ + showTapFailure() { + console.log('❌ Tap failure'); + // Visual feedback handled by UI layer + } + + /** + * Show emulation success animation + */ + showEmulationSuccess() { + console.log('✅ Emulation success'); + // Visual feedback handled by UI layer + } + + /** + * Show emulation failure animation + */ + showEmulationFailure() { + console.log('❌ Emulation failure'); + // Visual feedback handled by UI layer + } + + /** + * Animate NFC waves + * @param {HTMLElement} container - Container element + */ + animateNFCWaves(container) { + // Create wave elements + const waves = document.createElement('div'); + waves.className = 'rfid-nfc-waves'; + + for (let i = 0; i < 3; i++) { + const wave = document.createElement('div'); + wave.className = 'rfid-nfc-wave'; + wave.style.animationDelay = `${i * 0.3}s`; + waves.appendChild(wave); + } + + container.appendChild(waves); + return waves; + } + + /** + * Clean up all active animations + */ + cleanup() { + this.activeIntervals.forEach(interval => clearInterval(interval)); + this.activeIntervals = []; + console.log('🧹 RFIDAnimations cleanup complete'); + } +} diff --git a/js/minigames/rfid/rfid-data.js b/js/minigames/rfid/rfid-data.js new file mode 100644 index 0000000..b375683 --- /dev/null +++ b/js/minigames/rfid/rfid-data.js @@ -0,0 +1,222 @@ +/** + * RFID Data Manager + * + * Handles RFID card data management: + * - Card generation with EM4100 protocol + * - Hex ID validation + * - Card save/load to cloner device + * - Format conversions (hex, DEZ8, facility codes) + * + * @module rfid-data + */ + +// Maximum number of cards that can be saved to cloner +const MAX_SAVED_CARDS = 50; + +// Template names for generated cards +const CARD_NAME_TEMPLATES = [ + 'Security Badge', + 'Employee ID', + 'Access Card', + 'Visitor Pass', + 'Executive Key', + 'Maintenance Card', + 'Lab Access', + 'Server Room' +]; + +export class RFIDDataManager { + constructor() { + console.log('🔐 RFIDDataManager initialized'); + } + + /** + * Generate a random RFID card with EM4100 format + * @returns {Object} Card data with hex, facility code, card number + */ + generateRandomCard() { + // Generate 10-character hex ID (5 bytes) + const hex = Array.from({ length: 10 }, () => + Math.floor(Math.random() * 16).toString(16).toUpperCase() + ).join(''); + + // Calculate facility code from first byte + const facility = parseInt(hex.substring(0, 2), 16); + + // Calculate card number from next 2 bytes + const cardNumber = parseInt(hex.substring(2, 6), 16); + + // Generate card name + const nameTemplate = CARD_NAME_TEMPLATES[Math.floor(Math.random() * CARD_NAME_TEMPLATES.length)]; + const name = `${nameTemplate} #${Math.floor(Math.random() * 9000) + 1000}`; + + return { + name: name, + rfid_hex: hex, + rfid_facility: facility, + rfid_card_number: cardNumber, + rfid_protocol: 'EM4100', + type: 'keycard', + key_id: `card_${hex.toLowerCase()}` + }; + } + + /** + * Validate hex ID format + * @param {string} hex - Hex ID to validate + * @returns {Object} {valid: boolean, error?: string} + */ + validateHex(hex) { + if (!hex || typeof hex !== 'string') { + return { valid: false, error: 'Hex ID must be a string' }; + } + + if (hex.length !== 10) { + return { valid: false, error: 'Hex ID must be exactly 10 characters' }; + } + + if (!/^[0-9A-Fa-f]{10}$/.test(hex)) { + return { valid: false, error: 'Hex ID must contain only hex characters (0-9, A-F)' }; + } + + return { valid: true }; + } + + /** + * Save card to RFID cloner device + * @param {Object} cardData - Card data to save + * @returns {Object} {success: boolean, message: string} + */ + saveCardToCloner(cardData) { + // Find rfid_cloner in inventory + const cloner = window.inventory?.items?.find(item => + item?.scenarioData?.type === 'rfid_cloner' + ); + + if (!cloner) { + return { success: false, message: 'RFID cloner not found in inventory' }; + } + + // Validate hex ID + const validation = this.validateHex(cardData.rfid_hex); + if (!validation.valid) { + return { success: false, message: validation.error }; + } + + // Initialize saved_cards array if missing + if (!cloner.scenarioData.saved_cards) { + cloner.scenarioData.saved_cards = []; + } + + // Check if at max capacity + if (cloner.scenarioData.saved_cards.length >= MAX_SAVED_CARDS) { + return { success: false, message: `Cloner full (max ${MAX_SAVED_CARDS} cards)` }; + } + + // Check for duplicate hex ID + const existingIndex = cloner.scenarioData.saved_cards.findIndex(card => + card.rfid_hex === cardData.rfid_hex + ); + + if (existingIndex !== -1) { + // Overwrite existing card with updated timestamp + cloner.scenarioData.saved_cards[existingIndex] = { + ...cardData, + timestamp: Date.now() + }; + console.log(`📡 Overwritten duplicate card: ${cardData.name}`); + return { success: true, message: `Updated: ${cardData.name}` }; + } else { + // Add new card + cloner.scenarioData.saved_cards.push({ + ...cardData, + timestamp: Date.now() + }); + console.log(`📡 Saved new card: ${cardData.name}`); + return { success: true, message: `Saved: ${cardData.name}` }; + } + } + + /** + * Get all saved cards from cloner + * @returns {Array} Array of saved cards + */ + getSavedCards() { + const cloner = window.inventory?.items?.find(item => + item?.scenarioData?.type === 'rfid_cloner' + ); + + if (!cloner || !cloner.scenarioData.saved_cards) { + return []; + } + + return cloner.scenarioData.saved_cards; + } + + /** + * Convert hex ID to facility code and card number + * EM4100 format: First byte = facility, next 2 bytes = card number + * @param {string} hex - 10-character hex ID + * @returns {Object} {facility: number, cardNumber: number} + */ + hexToFacilityCard(hex) { + const facility = parseInt(hex.substring(0, 2), 16); + const cardNumber = parseInt(hex.substring(2, 6), 16); + return { facility, cardNumber }; + } + + /** + * Convert facility code and card number to hex ID + * @param {number} facility - Facility code (0-255) + * @param {number} cardNumber - Card number (0-65535) + * @returns {string} 10-character hex ID + */ + facilityCardToHex(facility, cardNumber) { + // Convert to hex and pad + const facilityHex = facility.toString(16).toUpperCase().padStart(2, '0'); + const cardHex = cardNumber.toString(16).toUpperCase().padStart(4, '0'); + + // Generate 4 random chars for remaining data + const randomHex = Array.from({ length: 4 }, () => + Math.floor(Math.random() * 16).toString(16).toUpperCase() + ).join(''); + + return facilityHex + cardHex + randomHex; + } + + /** + * Convert hex ID to DEZ 8 format + * EM4100 DEZ 8: Last 3 bytes (6 hex chars) converted to decimal + * @param {string} hex - 10-character hex ID + * @returns {string} 8-digit decimal string with leading zeros + */ + toDEZ8(hex) { + const lastThreeBytes = hex.slice(-6); + const decimal = parseInt(lastThreeBytes, 16); + return decimal.toString().padStart(8, '0'); + } + + /** + * Calculate EM4100 checksum + * XOR of all bytes + * @param {string} hex - 10-character hex ID + * @returns {number} Checksum byte (0x00-0xFF) + */ + calculateChecksum(hex) { + const bytes = hex.match(/.{1,2}/g).map(b => parseInt(b, 16)); + let checksum = 0; + bytes.forEach(byte => { + checksum ^= byte; + }); + return checksum & 0xFF; + } + + /** + * Format hex for display (add spaces every 2 chars) + * @param {string} hex - Hex string + * @returns {string} Formatted hex string + */ + formatHex(hex) { + return hex.match(/.{1,2}/g).join(' ').toUpperCase(); + } +} diff --git a/js/minigames/rfid/rfid-minigame.js b/js/minigames/rfid/rfid-minigame.js new file mode 100644 index 0000000..170e240 --- /dev/null +++ b/js/minigames/rfid/rfid-minigame.js @@ -0,0 +1,299 @@ +/** + * RFID Minigame Controller + * + * Flipper Zero-inspired RFID reader/cloner minigame: + * - Unlock mode: Tap keycard or emulate saved card to unlock doors + * - Clone mode: Read and save keycard data for later emulation + * + * Modes: + * - unlock: Player needs to unlock an RFID-locked door + * - clone: Player is cloning a keycard (from conversation or inventory click) + * + * @module rfid-minigame + */ + +import { MinigameScene } from '../framework/base-minigame.js'; +import { RFIDUIRenderer } from './rfid-ui.js'; +import { RFIDDataManager } from './rfid-data.js'; +import { RFIDAnimations } from './rfid-animations.js'; + +export class RFIDMinigame extends MinigameScene { + constructor(container, params) { + // Set title based on mode + const title = params.mode === 'clone' ? 'Cloning Card...' : 'RFID Reader'; + + super(container, { + ...params, + title: title, + showCancel: true, + cancelText: 'Close', + requiresKeyboardInput: false + }); + + // Parameters + this.params = params; + this.mode = params.mode || 'unlock'; // 'unlock' or 'clone' + this.requiredCardId = params.requiredCardId; // For unlock mode + this.availableCards = params.availableCards || []; // For unlock mode + this.hasCloner = params.hasCloner || false; // For unlock mode + this.cardToClone = params.cardToClone; // For clone mode + + // Components + this.ui = null; + this.dataManager = null; + this.animations = null; + + // State + this.gameResult = null; + + console.log(`🔐 RFIDMinigame created in ${this.mode} mode`); + } + + init() { + // Call parent init + super.init(); + + // Add CSS class to container + this.container.classList.add('rfid-minigame-container'); + this.gameContainer.classList.add('rfid-minigame-game-container'); + + // Initialize components + this.dataManager = new RFIDDataManager(); + this.animations = new RFIDAnimations(this); + this.ui = new RFIDUIRenderer(this); + + // Create appropriate interface + if (this.mode === 'unlock') { + this.ui.createUnlockInterface(); + } else if (this.mode === 'clone') { + this.ui.createCloneInterface(); + } + + console.log('🔐 RFIDMinigame initialized'); + } + + start() { + super.start(); + console.log('🔐 RFIDMinigame started'); + + // Emit event + if (window.eventDispatcher) { + window.eventDispatcher.emit('rfid_lock_accessed', { + mode: this.mode, + timestamp: Date.now() + }); + } + } + + /** + * Handle card tap (unlock mode) + * @param {Object} card - Card that was tapped + */ + handleCardTap(card) { + console.log('📡 Card tapped:', card.scenarioData?.name); + + const cardId = card.scenarioData?.key_id || card.key_id; + const isCorrect = cardId === this.requiredCardId; + + if (isCorrect) { + this.animations.showTapSuccess(); + this.ui.showSuccess('Access Granted'); + + setTimeout(() => { + this.complete(true); + }, 1500); + } else { + this.animations.showTapFailure(); + this.ui.showError('Access Denied'); + + setTimeout(() => { + this.ui.showTapInterface(); + }, 1500); + } + } + + /** + * Handle card emulation (unlock mode) + * @param {Object} savedCard - Saved card from cloner + */ + handleEmulate(savedCard) { + console.log('📡 Emulating card:', savedCard.name); + + const cardId = savedCard.key_id; + const isCorrect = cardId === this.requiredCardId; + + if (isCorrect) { + this.animations.showEmulationSuccess(); + this.ui.showSuccess('Access Granted'); + + // Emit event + if (window.eventDispatcher) { + window.eventDispatcher.emit('card_emulated', { + cardName: savedCard.name, + cardHex: savedCard.rfid_hex, + success: true, + timestamp: Date.now() + }); + } + + setTimeout(() => { + this.complete(true); + }, 2000); + } else { + this.animations.showEmulationFailure(); + this.ui.showError('Access Denied'); + + // Emit event + if (window.eventDispatcher) { + window.eventDispatcher.emit('card_emulated', { + cardName: savedCard.name, + cardHex: savedCard.rfid_hex, + success: false, + timestamp: Date.now() + }); + } + + setTimeout(() => { + this.ui.showSavedCards(); + }, 1500); + } + } + + /** + * Start card reading (clone mode) + */ + startCardReading() { + console.log('📡 Starting card read...'); + + // Animate reading progress + this.animations.animateReading((progress) => { + this.ui.updateReadingProgress(progress); + }).then(() => { + // Reading complete - show card data + console.log('📡 Card read complete'); + this.ui.showCardDataScreen(this.cardToClone); + }); + } + + /** + * Handle save card (clone mode) + * @param {Object} cardData - Card data to save + */ + handleSaveCard(cardData) { + console.log('💾 Saving card:', cardData.name); + + const result = this.dataManager.saveCardToCloner(cardData); + + if (result.success) { + this.ui.showSuccess(result.message); + + // Emit event + if (window.eventDispatcher) { + window.eventDispatcher.emit('card_cloned', { + cardName: cardData.name, + cardHex: cardData.rfid_hex, + timestamp: Date.now() + }); + } + + this.gameResult = { + success: true, + cardSaved: true, + cardData: cardData + }; + + setTimeout(() => { + this.complete(true); + }, 1500); + } else { + this.ui.showError(result.message); + + setTimeout(() => { + this.ui.showCardDataScreen(cardData); + }, 1500); + } + } + + complete(success) { + // Check if we need to return to conversation + if (window.pendingConversationReturn && window.returnToConversationAfterRFID) { + console.log('Returning to conversation after RFID minigame'); + setTimeout(() => { + window.returnToConversationAfterRFID(); + }, 100); + } + + // Call parent complete + super.complete(success, this.gameResult); + } + + cleanup() { + // Cleanup animations + if (this.animations) { + this.animations.cleanup(); + } + + // Call parent cleanup + super.cleanup(); + console.log('🧹 RFIDMinigame cleanup complete'); + } +} + +/** + * Start RFID minigame + * @param {Object} lockable - The locked object (for unlock mode) + * @param {string} type - 'door' or 'item' (for unlock mode) + * @param {Object} params - Minigame parameters + */ +export function startRFIDMinigame(lockable, type, params) { + console.log('🔐 Starting RFID minigame', { mode: params.mode, params }); + + // Initialize framework if needed + if (!window.MinigameFramework.mainGameScene && window.game) { + window.MinigameFramework.init(window.game.scene.scenes[0]); + } + + // Start minigame + window.MinigameFramework.startMinigame('rfid', lockable, params); +} + +/** + * Return to conversation after RFID minigame + * Follows exact pattern from container minigame + * @see /js/minigames/container/container-minigame.js:720-754 + */ +export function returnToConversationAfterRFID() { + console.log('Returning to conversation after RFID minigame'); + + // Check if there's a pending conversation return + if (window.pendingConversationReturn) { + const conversationState = window.pendingConversationReturn; + + // Clear the pending return state + window.pendingConversationReturn = null; + + console.log('Restoring conversation:', conversationState); + + // Restart the appropriate conversation minigame + if (window.MinigameFramework) { + // Small delay to ensure RFID minigame is fully closed + setTimeout(() => { + if (conversationState.type === 'person-chat') { + // Restart person-chat minigame + window.MinigameFramework.startMinigame('person-chat', null, { + npcId: conversationState.npcId, + fromTag: true // Flag to indicate resuming from tag action + }); + } else if (conversationState.type === 'phone-chat') { + // Restart phone-chat minigame + window.MinigameFramework.startMinigame('phone-chat', null, { + npcId: conversationState.npcId, + fromTag: true + }); + } + }, 50); + } + } else { + console.log('No pending conversation return found'); + } +} diff --git a/js/minigames/rfid/rfid-ui.js b/js/minigames/rfid/rfid-ui.js new file mode 100644 index 0000000..70202a7 --- /dev/null +++ b/js/minigames/rfid/rfid-ui.js @@ -0,0 +1,462 @@ +/** + * RFID UI Renderer + * + * Renders Flipper Zero-style RFID interface: + * - Main menu (Read / Saved) + * - Tap interface (unlock mode) + * - Saved cards list + * - Emulation screen + * - Card reading screen (clone mode) + * - Card data display + * + * @module rfid-ui + */ + +export class RFIDUIRenderer { + constructor(minigame) { + this.minigame = minigame; + this.container = minigame.gameContainer; + this.dataManager = minigame.dataManager; + console.log('🎨 RFIDUIRenderer initialized'); + } + + /** + * Create unlock mode interface + */ + createUnlockInterface() { + this.clear(); + + // Create Flipper Zero frame + const flipper = this.createFlipperFrame(); + + // Show main menu + this.showMainMenu('unlock'); + + this.container.appendChild(flipper); + } + + /** + * Create clone mode interface + */ + createCloneInterface() { + this.clear(); + + // Create Flipper Zero frame + const flipper = this.createFlipperFrame(); + + // Auto-start reading if card provided + if (this.minigame.params.cardToClone) { + this.showReadingScreen(); + } else { + this.showMainMenu('clone'); + } + + this.container.appendChild(flipper); + } + + /** + * Create Flipper Zero device frame + * @returns {HTMLElement} Flipper frame element + */ + createFlipperFrame() { + const frame = document.createElement('div'); + frame.className = 'flipper-zero-frame'; + + // Header with logo and battery + const header = document.createElement('div'); + header.className = 'flipper-header'; + + const logo = document.createElement('div'); + logo.className = 'flipper-logo'; + logo.textContent = 'FLIPPER ZERO'; + + const battery = document.createElement('div'); + battery.className = 'flipper-battery'; + battery.textContent = '⚡ 100%'; + + header.appendChild(logo); + header.appendChild(battery); + + // Screen container + const screen = document.createElement('div'); + screen.className = 'flipper-screen'; + screen.id = 'rfid-screen'; + + frame.appendChild(header); + frame.appendChild(screen); + + return frame; + } + + /** + * Get screen element + * @returns {HTMLElement} Screen element + */ + getScreen() { + return document.getElementById('rfid-screen'); + } + + /** + * Show main menu + * @param {string} mode - 'unlock' or 'clone' + */ + showMainMenu(mode) { + const screen = this.getScreen(); + screen.innerHTML = ''; + + // Breadcrumb + const breadcrumb = document.createElement('div'); + breadcrumb.className = 'flipper-breadcrumb'; + breadcrumb.textContent = 'RFID'; + screen.appendChild(breadcrumb); + + // Menu items + const menu = document.createElement('div'); + menu.className = 'flipper-menu'; + + if (mode === 'unlock') { + // Read option (tap cards) + const readOption = document.createElement('div'); + readOption.className = 'flipper-menu-item'; + readOption.textContent = '> Read'; + readOption.addEventListener('click', () => this.showTapInterface()); + menu.appendChild(readOption); + + // Saved option (emulate) + const savedOption = document.createElement('div'); + savedOption.className = 'flipper-menu-item'; + savedOption.textContent = ' Saved'; + savedOption.addEventListener('click', () => this.showSavedCards()); + menu.appendChild(savedOption); + } else { + // Clone mode - just show "Reading..." message + const info = document.createElement('div'); + info.className = 'flipper-info'; + info.textContent = 'Place card...'; + menu.appendChild(info); + } + + screen.appendChild(menu); + } + + /** + * Show tap interface for unlock mode + */ + showTapInterface() { + const screen = this.getScreen(); + screen.innerHTML = ''; + + // Breadcrumb + const breadcrumb = document.createElement('div'); + breadcrumb.className = 'flipper-breadcrumb'; + breadcrumb.textContent = 'RFID > Read'; + screen.appendChild(breadcrumb); + + // NFC waves animation + const waves = document.createElement('div'); + waves.className = 'rfid-nfc-waves-container'; + waves.innerHTML = '
📡
'; + screen.appendChild(waves); + + // Instruction + const instruction = document.createElement('div'); + instruction.className = 'flipper-info'; + instruction.textContent = 'Place card near reader...'; + screen.appendChild(instruction); + + // List available keycards + const cardList = document.createElement('div'); + cardList.className = 'flipper-card-list'; + + const availableCards = this.minigame.params.availableCards || []; + + if (availableCards.length === 0) { + const noCards = document.createElement('div'); + noCards.className = 'flipper-info-dim'; + noCards.textContent = 'No keycards in inventory'; + cardList.appendChild(noCards); + } else { + availableCards.forEach(card => { + const cardItem = document.createElement('div'); + cardItem.className = 'flipper-menu-item'; + cardItem.textContent = `> ${card.scenarioData?.name || 'Keycard'}`; + cardItem.addEventListener('click', () => { + this.minigame.handleCardTap(card); + }); + cardList.appendChild(cardItem); + }); + } + + screen.appendChild(cardList); + + // Back button + const back = document.createElement('div'); + back.className = 'flipper-button-back'; + back.textContent = '← Back'; + back.addEventListener('click', () => this.showMainMenu('unlock')); + screen.appendChild(back); + } + + /** + * Show saved cards list + */ + showSavedCards() { + const screen = this.getScreen(); + screen.innerHTML = ''; + + // Breadcrumb + const breadcrumb = document.createElement('div'); + breadcrumb.className = 'flipper-breadcrumb'; + breadcrumb.textContent = 'RFID > Saved'; + screen.appendChild(breadcrumb); + + // Get saved cards + const savedCards = this.dataManager.getSavedCards(); + + if (savedCards.length === 0) { + const noCards = document.createElement('div'); + noCards.className = 'flipper-info'; + noCards.textContent = 'No saved cards'; + screen.appendChild(noCards); + } else { + // Card list + const cardList = document.createElement('div'); + cardList.className = 'flipper-card-list'; + + savedCards.forEach(card => { + const cardItem = document.createElement('div'); + cardItem.className = 'flipper-menu-item'; + cardItem.textContent = `> ${card.name}`; + cardItem.addEventListener('click', () => this.showEmulationScreen(card)); + cardList.appendChild(cardItem); + }); + + screen.appendChild(cardList); + } + + // Back button + const back = document.createElement('div'); + back.className = 'flipper-button-back'; + back.textContent = '← Back'; + back.addEventListener('click', () => this.showMainMenu('unlock')); + screen.appendChild(back); + } + + /** + * Show emulation screen + * @param {Object} card - Card to emulate + */ + showEmulationScreen(card) { + const screen = this.getScreen(); + screen.innerHTML = ''; + + // Breadcrumb + const breadcrumb = document.createElement('div'); + breadcrumb.className = 'flipper-breadcrumb'; + breadcrumb.textContent = 'RFID > Saved > Emulate'; + screen.appendChild(breadcrumb); + + // Emulation icon + const icon = document.createElement('div'); + icon.className = 'rfid-emulate-icon'; + icon.textContent = '📡'; + screen.appendChild(icon); + + // Protocol + const protocol = document.createElement('div'); + protocol.className = 'flipper-info'; + protocol.textContent = 'EM-Micro EM4100'; + screen.appendChild(protocol); + + // Card name + const name = document.createElement('div'); + name.className = 'flipper-card-name'; + name.textContent = card.name; + screen.appendChild(name); + + // Card data + const { facility, cardNumber } = this.dataManager.hexToFacilityCard(card.rfid_hex); + + const data = document.createElement('div'); + data.className = 'flipper-card-data'; + data.innerHTML = ` +
HEX: ${this.dataManager.formatHex(card.rfid_hex)}
+
Facility: ${facility}
+
Card: ${cardNumber}
+ `; + screen.appendChild(data); + + // Emulating message + const emulating = document.createElement('div'); + emulating.className = 'flipper-emulating'; + emulating.textContent = 'Emulating...'; + screen.appendChild(emulating); + + // Trigger emulation after showing screen + setTimeout(() => { + this.minigame.handleEmulate(card); + }, 500); + } + + /** + * Show card reading screen (clone mode) + */ + showReadingScreen() { + const screen = this.getScreen(); + screen.innerHTML = ''; + + // Breadcrumb + const breadcrumb = document.createElement('div'); + breadcrumb.className = 'flipper-breadcrumb'; + breadcrumb.textContent = 'RFID > Read'; + screen.appendChild(breadcrumb); + + // Status + const status = document.createElement('div'); + status.className = 'flipper-info'; + status.textContent = 'Reading 1/2'; + screen.appendChild(status); + + // Modulation + const modulation = document.createElement('div'); + modulation.className = 'flipper-info-dim'; + modulation.textContent = '> ASK PSK'; + screen.appendChild(modulation); + + // Instruction + const instruction = document.createElement('div'); + instruction.className = 'flipper-info'; + instruction.textContent = "Don't move card..."; + screen.appendChild(instruction); + + // Progress bar + const progressContainer = document.createElement('div'); + progressContainer.className = 'rfid-progress-container'; + + const progressBar = document.createElement('div'); + progressBar.className = 'rfid-progress-bar'; + progressBar.id = 'rfid-progress-bar'; + + progressContainer.appendChild(progressBar); + screen.appendChild(progressContainer); + + // Start reading animation + this.minigame.startCardReading(); + } + + /** + * Update reading progress + * @param {number} progress - Progress percentage (0-100) + */ + updateReadingProgress(progress) { + const progressBar = document.getElementById('rfid-progress-bar'); + if (progressBar) { + progressBar.style.width = `${progress}%`; + + // Change color based on progress + if (progress < 50) { + progressBar.style.backgroundColor = '#FF8200'; + } else if (progress < 100) { + progressBar.style.backgroundColor = '#FFA500'; + } else { + progressBar.style.backgroundColor = '#00FF00'; + } + } + } + + /** + * Show card data screen after reading + * @param {Object} cardData - Read card data + */ + showCardDataScreen(cardData) { + const screen = this.getScreen(); + screen.innerHTML = ''; + + // Breadcrumb + const breadcrumb = document.createElement('div'); + breadcrumb.className = 'flipper-breadcrumb'; + breadcrumb.textContent = 'RFID > Read'; + screen.appendChild(breadcrumb); + + // Protocol + const protocol = document.createElement('div'); + protocol.className = 'flipper-info'; + protocol.textContent = 'EM-Micro EM4100'; + screen.appendChild(protocol); + + // Card data + const { facility, cardNumber } = this.dataManager.hexToFacilityCard(cardData.rfid_hex); + const checksum = this.dataManager.calculateChecksum(cardData.rfid_hex); + const dez8 = this.dataManager.toDEZ8(cardData.rfid_hex); + + const data = document.createElement('div'); + data.className = 'flipper-card-data'; + data.innerHTML = ` +
HEX: ${this.dataManager.formatHex(cardData.rfid_hex)}
+
Facility: ${facility}
+
Card: ${cardNumber}
+
Checksum: 0x${checksum.toString(16).toUpperCase().padStart(2, '0')}
+
DEZ 8: ${dez8}
+ `; + screen.appendChild(data); + + // Buttons + const buttons = document.createElement('div'); + buttons.className = 'flipper-buttons'; + + const saveBtn = document.createElement('button'); + saveBtn.className = 'flipper-button'; + saveBtn.textContent = 'Save'; + saveBtn.addEventListener('click', () => this.minigame.handleSaveCard(cardData)); + + const cancelBtn = document.createElement('button'); + cancelBtn.className = 'flipper-button flipper-button-secondary'; + cancelBtn.textContent = 'Cancel'; + cancelBtn.addEventListener('click', () => this.minigame.complete(false)); + + buttons.appendChild(saveBtn); + buttons.appendChild(cancelBtn); + screen.appendChild(buttons); + } + + /** + * Show success message + * @param {string} message - Success message + */ + showSuccess(message) { + const screen = this.getScreen(); + screen.innerHTML = ''; + + const success = document.createElement('div'); + success.className = 'flipper-success'; + success.innerHTML = ` +
+
${message}
+ `; + screen.appendChild(success); + } + + /** + * Show error message + * @param {string} message - Error message + */ + showError(message) { + const screen = this.getScreen(); + screen.innerHTML = ''; + + const error = document.createElement('div'); + error.className = 'flipper-error'; + error.innerHTML = ` +
+
${message}
+ `; + screen.appendChild(error); + } + + /** + * Clear screen + */ + clear() { + this.container.innerHTML = ''; + } +} diff --git a/js/systems/unlock-system.js b/js/systems/unlock-system.js index 9983b3d..3632534 100644 --- a/js/systems/unlock-system.js +++ b/js/systems/unlock-system.js @@ -301,7 +301,59 @@ export function handleUnlock(lockable, type) { 'error', 'Device Not Found', 5000); } break; - + + case 'rfid': + console.log('RFID LOCK UNLOCK ATTEMPT'); + const requiredCardId = lockRequirements.requires; + console.log('RFID CARD REQUIRED', requiredCardId); + + // Check for keycards in inventory + const keycards = window.inventory.items.filter(item => + item && item.scenarioData && + item.scenarioData.type === 'keycard' + ); + + // Check for RFID cloner with saved cards + const cloner = window.inventory.items.find(item => + item && item.scenarioData && + item.scenarioData.type === 'rfid_cloner' + ); + + const hasCloner = !!cloner; + const savedCards = cloner?.scenarioData?.saved_cards || []; + + // Combine available cards + const availableCards = [...keycards]; + + console.log('RFID CHECK', { + requiredCardId, + hasCloner, + keycardsCount: keycards.length, + savedCardsCount: savedCards.length + }); + + if (keycards.length > 0 || savedCards.length > 0) { + // Start RFID minigame in unlock mode + window.startRFIDMinigame(lockable, type, { + mode: 'unlock', + requiredCardId: requiredCardId, + availableCards: availableCards, + hasCloner: hasCloner, + onComplete: (success) => { + if (success) { + setTimeout(() => { + unlockTarget(lockable, type, lockable.layer); + window.gameAlert('RFID lock unlocked!', 'success', 'Access Granted', 3000); + }, 100); + } + } + }); + } else { + console.log('NO RFID CARDS OR CLONER AVAILABLE'); + window.gameAlert('Requires RFID keycard', 'error', 'Access Denied', 4000); + } + break; + default: window.gameAlert(`This ${type} requires ${lockRequirements.lockType} to unlock.`, 'info', 'Locked', 4000); break;