mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-21 11:18:08 +00:00
feat(rfid): Implement core RFID minigame system
Implemented complete RFID keycard lock system with Flipper Zero-inspired interface. Core Components: - RFIDDataManager: Card generation, validation, save/load, EM4100 formulas - RFIDUIRenderer: Flipper Zero UI with unlock/clone modes - RFIDAnimations: Progress animations and visual feedback - RFIDMinigame: Main controller with unlock/clone modes - CSS: Complete Flipper Zero styling (orange device, monochrome screen) Features: - Unlock mode: Tap physical keycards or emulate saved cards - Clone mode: Read and save cards to RFID cloner - EM4100 protocol with DEZ8, facility codes, checksums - Automatic conversation return after cloning (proven pattern) - 50-card storage limit with duplicate overwrite - Validated 10-char hex IDs Integration: - Registered in MinigameFramework as 'rfid' - Added to unlock-system.js switch statement - Exported window.startRFIDMinigame and returnToConversationAfterRFID Files Created: - js/minigames/rfid/rfid-minigame.js (main controller) - js/minigames/rfid/rfid-data.js (data management) - js/minigames/rfid/rfid-ui.js (UI rendering) - js/minigames/rfid/rfid-animations.js (animations) - css/rfid-minigame.css (Flipper Zero styles) Next: Add chat-helpers tag, interactions handler, HTML/Phaser integration
This commit is contained in:
376
css/rfid-minigame.css
Normal file
376
css/rfid-minigame.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
<<<<<<< HEAD
|
||||
window.startTitleScreenMinigame = startTitleScreenMinigame;
|
||||
=======
|
||||
window.startRFIDMinigame = startRFIDMinigame;
|
||||
window.returnToConversationAfterRFID = returnToConversationAfterRFID;
|
||||
>>>>>>> a4e2561 (feat(rfid): Implement core RFID minigame system)
|
||||
|
||||
103
js/minigames/rfid/rfid-animations.js
Normal file
103
js/minigames/rfid/rfid-animations.js
Normal file
@@ -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');
|
||||
}
|
||||
}
|
||||
222
js/minigames/rfid/rfid-data.js
Normal file
222
js/minigames/rfid/rfid-data.js
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
299
js/minigames/rfid/rfid-minigame.js
Normal file
299
js/minigames/rfid/rfid-minigame.js
Normal file
@@ -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');
|
||||
}
|
||||
}
|
||||
462
js/minigames/rfid/rfid-ui.js
Normal file
462
js/minigames/rfid/rfid-ui.js
Normal file
@@ -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 = '<div class="rfid-nfc-icon">📡</div>';
|
||||
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 = `
|
||||
<div>HEX: ${this.dataManager.formatHex(card.rfid_hex)}</div>
|
||||
<div>Facility: ${facility}</div>
|
||||
<div>Card: ${cardNumber}</div>
|
||||
`;
|
||||
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 = `
|
||||
<div>HEX: ${this.dataManager.formatHex(cardData.rfid_hex)}</div>
|
||||
<div>Facility: ${facility}</div>
|
||||
<div>Card: ${cardNumber}</div>
|
||||
<div>Checksum: 0x${checksum.toString(16).toUpperCase().padStart(2, '0')}</div>
|
||||
<div>DEZ 8: ${dez8}</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="flipper-success-icon">✓</div>
|
||||
<div class="flipper-success-message">${message}</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="flipper-error-icon">✗</div>
|
||||
<div class="flipper-error-message">${message}</div>
|
||||
`;
|
||||
screen.appendChild(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear screen
|
||||
*/
|
||||
clear() {
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user