Refactor RFID handling and enhance game logic

- Updated `GamesController` and `Game` model to include RFID lock types in the filtering logic for 'requires' attributes, ensuring proper handling of biometric, bluetooth, and RFID types.
- Improved `RFIDMinigame` to standardize card ID retrieval, supporting both `card_id` and `key_id`.
- Enhanced `unlock-system.js` to prioritize physical keycard checks and streamline the unlocking process with detailed logging.
- Adjusted scenario JSON files to replace `keyId` with `card_id` for consistency and added new lock requirements for various rooms.
This commit is contained in:
Z. Cliffe Schreuders
2025-11-29 23:43:30 +00:00
parent 92330b04dc
commit ef8e2f294a
7 changed files with 60 additions and 122 deletions

View File

@@ -494,10 +494,13 @@ module BreakEscape
def filter_requires_recursive(obj)
case obj
when Hash
# Remove 'requires' for exploitable lock types (key/pin/password/rfid)
# Keep it for biometric/bluetooth since they reference collectible items, not answers
# Remove 'requires' for exploitable lock types (key/pin/password)
# Keep it for biometric/bluetooth/rfid since they reference collectible items, not answers
# - biometric: requires fingerprint owner name (e.g., "Mrs Moo")
# - bluetooth: requires device MAC/name (e.g., "00:11:22:33:44:55")
# - rfid: requires card IDs (e.g., ["master_keycard"])
lock_type = obj['lockType']
if lock_type && !%w[biometric bluetooth].include?(lock_type)
if lock_type && !%w[biometric bluetooth rfid].include?(lock_type)
obj.delete('requires')
end

View File

@@ -519,10 +519,13 @@ module BreakEscape
def filter_requires_and_contents_recursive(obj)
case obj
when Hash
# Remove 'requires' for exploitable lock types (key/pin/password/rfid)
# Keep it for biometric/bluetooth since they reference collectible items, not answers
# Remove 'requires' for exploitable lock types (key/pin/password)
# Keep it for biometric/bluetooth/rfid since they reference collectible items, not answers
# - biometric: requires fingerprint owner name (e.g., "Mrs Moo")
# - bluetooth: requires device MAC/name (e.g., "00:11:22:33:44:55")
# - rfid: requires card IDs (e.g., ["master_keycard"])
lock_type = obj['lockType']
if lock_type && !%w[biometric bluetooth].include?(lock_type)
if lock_type && !%w[biometric bluetooth rfid].include?(lock_type)
obj.delete('requires')
end

View File

@@ -98,8 +98,8 @@ export class RFIDMinigame extends MinigameScene {
handleCardTap(card) {
console.log('📡 Card tapped:', card.scenarioData?.name);
// Support both card_id (new) and key_id (legacy)
const cardId = card.scenarioData?.card_id || card.scenarioData?.key_id || card.key_id;
// Get card ID (standard: card_id, legacy: key_id)
const cardId = card.scenarioData?.card_id || card.scenarioData?.key_id;
const isCorrect = !this.isLockingAttempt || this.requiredCardIds.includes(cardId);
if (isCorrect) {
@@ -127,7 +127,7 @@ export class RFIDMinigame extends MinigameScene {
handleEmulate(savedCard) {
console.log('📡 Emulating card:', savedCard.name);
// Support both card_id (new) and key_id (legacy)
// Get card ID (standard: card_id, legacy: key_id)
const cardId = savedCard.card_id || savedCard.key_id;
const isCorrect = !this.isLockingAttempt || this.requiredCardIds.includes(cardId);

View File

@@ -392,10 +392,14 @@ export function handleUnlock(lockable, type) {
item.scenarioData.type === 'keycard'
);
// Check if any physical card matches
const hasValidCard = keycards.some(card =>
requiredCardIds.includes(card.scenarioData.card_id || card.scenarioData.key_id)
// Helper to get card ID (standard: card_id, legacy: key_id)
const getCardId = (cardData) => cardData.card_id || cardData.key_id;
// Find a matching physical keycard
const matchingKeycard = keycards.find(card =>
requiredCardIds.includes(getCardId(card.scenarioData))
);
const hasValidCard = !!matchingKeycard;
// Check for RFID cloner with saved cards
const cloner = window.inventory.items.find(item =>
@@ -408,7 +412,7 @@ export function handleUnlock(lockable, type) {
// Check if any saved card matches
const hasValidClone = savedCards.some(card =>
requiredCardIds.includes(card.card_id || card.key_id)
requiredCardIds.includes(getCardId(card))
);
console.log('RFID CHECK', {
@@ -418,16 +422,32 @@ export function handleUnlock(lockable, type) {
keycardsCount: keycards.length,
savedCardsCount: savedCards.length,
hasValidCard,
hasValidClone
hasValidClone,
matchingKeycard: matchingKeycard?.scenarioData?.name
});
if (keycards.length > 0 || savedCards.length > 0) {
// PRIORITY 1: If player has a matching physical keycard, open door directly
if (hasValidCard) {
const cardName = matchingKeycard.scenarioData.name || 'Keycard';
console.log(`RFID DIRECT UNLOCK with physical keycard: ${cardName}`);
// Notify server and unlock
notifyServerUnlock(lockable, type, 'rfid').then(serverResponse => {
unlockTarget(lockable, type, lockable.layer, serverResponse);
window.gameAlert(`Door opened with ${cardName}`, 'success', 'Access Granted', 3000);
}).catch(error => {
console.error('RFID unlock failed:', error);
window.gameAlert('Access denied', 'error', 'Error', 3000);
});
}
// PRIORITY 2: If player has RFID cloner, open minigame to emulate or read cards
else if (hasCloner) {
// Start RFID minigame in unlock mode
window.startRFIDMinigame(lockable, type, {
mode: 'unlock',
requiredCardIds: requiredCardIds, // Pass array
requiredCardIds: requiredCardIds,
acceptsUIDOnly: acceptsUIDOnly,
availableCards: keycards,
availableCards: keycards, // Cards they can read in the minigame
hasCloner: hasCloner,
onComplete: async (success) => {
if (success) {
@@ -440,7 +460,9 @@ export function handleUnlock(lockable, type) {
}
}
});
} else {
}
// PRIORITY 3: No valid keycard and no cloner
else {
console.log('NO RFID CARDS OR CLONER AVAILABLE');
window.gameAlert('Requires RFID keycard', 'error', 'Access Denied', 4000);
}

View File

@@ -97,7 +97,7 @@
{
"type": "keycard",
"name": "Server Room Keycard",
"keyId": "server_keycard",
"card_id": "server_keycard",
"takeable": true,
"observations": "A keycard with 'SERVER ROOM' printed on it"
}
@@ -122,7 +122,7 @@
]
},
"server_room": {
"type": "room_server",
"type": "room_servers",
"connections": {
"south": "flag_room"
},

View File

@@ -85,28 +85,6 @@
"takeable": true,
"observations": "A portable multi-tool for RFID scanning and emulation. Essential for this test."
}
],
"doors": [
{
"roomId": "lobby",
"connectedRoom": "em4100_room",
"direction": "north",
"x": 200,
"y": 100,
"locked": true,
"lockType": "rfid",
"requires": ["employee_badge"]
},
{
"roomId": "lobby",
"connectedRoom": "mifare_weak_room",
"direction": "north",
"x": 400,
"y": 100,
"locked": true,
"lockType": "rfid",
"requires": ["hotel_keycard"]
}
]
},
"em4100_room": {
@@ -116,6 +94,9 @@
"south": "lobby",
"north": "mifare_custom_room"
},
"locked": true,
"lockType": "rfid",
"requires": ["employee_badge"],
"npcs": [],
"objects": [
{
@@ -129,24 +110,6 @@
"note_content": "You successfully cloned an EM4100 card!\n\nThis protocol:\n• 125kHz frequency\n• No encryption whatsoever\n• Instant clone - no attack needed\n• Used in: Old hotels, parking garages\n\nProceed north to test MIFARE Classic weak defaults.",
"observations": "EM4100 test passed"
}
],
"doors": [
{
"roomId": "em4100_room",
"connectedRoom": "lobby",
"direction": "south",
"x": 200,
"y": 500,
"locked": false
},
{
"roomId": "em4100_room",
"connectedRoom": "mifare_custom_room",
"direction": "north",
"x": 300,
"y": 100,
"locked": false
}
]
},
"mifare_weak_room": {
@@ -156,6 +119,9 @@
"south": "lobby",
"north": "mifare_custom_room"
},
"locked": true,
"lockType": "rfid",
"requires": ["hotel_keycard"],
"npcs": [],
"objects": [
{
@@ -169,24 +135,6 @@
"note_content": "You cloned a MIFARE Classic with default keys!\n\nThis protocol:\n• 13.56MHz NFC frequency\n• Uses encryption but keeps factory defaults (FFFFFFFFFFFF)\n• Dictionary attack succeeds instantly (~95% success)\n• Used in: Cheap hotels, old transit cards\n\nProceed north to test MIFARE Custom Keys (requires attack).",
"observations": "MIFARE weak defaults test passed"
}
],
"doors": [
{
"roomId": "mifare_weak_room",
"connectedRoom": "lobby",
"direction": "south",
"x": 400,
"y": 500,
"locked": false
},
{
"roomId": "mifare_weak_room",
"connectedRoom": "mifare_custom_room",
"direction": "north",
"x": 300,
"y": 100,
"locked": false
}
]
},
"mifare_custom_room": {
@@ -238,18 +186,6 @@
"takeable": false,
"observations": "A guard's corporate badge - can be scanned with RFID cloner"
}
],
"doors": [
{
"roomId": "mifare_custom_room",
"connectedRoom": "desfire_room",
"direction": "north",
"x": 300,
"y": 100,
"locked": true,
"lockType": "rfid",
"requires": ["corporate_badge"]
}
]
},
"desfire_room": {
@@ -258,6 +194,9 @@
"connections": {
"south": "mifare_custom_room"
},
"locked": true,
"lockType": "rfid",
"requires": ["corporate_badge"],
"npcs": [
{
"id": "guard_high",
@@ -311,16 +250,6 @@
"takeable": true,
"observations": "A master override card - universal access"
}
],
"doors": [
{
"roomId": "desfire_room",
"connectedRoom": "mifare_custom_room",
"direction": "south",
"x": 300,
"y": 500,
"locked": false
}
]
}
}

View File

@@ -59,18 +59,6 @@
"note_content": "All high-security areas require RFID badge access. Master keycards (FF prefix) have universal access. Standard employee badges (01 prefix) have limited access.\n\nThe guard in this room has the master keycard. If you have an RFID cloner, you can scan their badge to clone it.",
"observations": "Instructions about the RFID security system"
}
],
"doors": [
{
"roomId": "test_lobby",
"connectedRoom": "test_secure",
"direction": "north",
"x": 300,
"y": 100,
"locked": true,
"lockType": "rfid",
"requires": ["master_keycard"]
}
]
},
"test_secure": {
@@ -79,6 +67,9 @@
"connections": {
"south": "test_lobby"
},
"locked": true,
"lockType": "rfid",
"requires": ["master_keycard"],
"npcs": [],
"objects": [
{
@@ -106,16 +97,6 @@
"takeable": true,
"observations": "The CEO's personal keycard. Ultimate access level."
}
],
"doors": [
{
"roomId": "test_secure",
"connectedRoom": "test_lobby",
"direction": "south",
"x": 300,
"y": 500,
"locked": false
}
]
}
}