Enhance RFID system interactions and NPC conversations

- Updated door sprite creation to support lock properties from both doors and connected rooms.
- Added RFID cloner interaction logic to handle minigame initiation from inventory.
- Implemented inventory variable synchronization for NPC conversations, including RFID card details.
- Introduced new scenarios for RFID guards with varying security levels and interactions.
- Revised test scenarios to include comprehensive RFID protocol testing with detailed notes and NPC interactions.
This commit is contained in:
Z. Cliffe Schreuders
2025-11-15 23:48:15 +00:00
parent bff4a6a31a
commit 08ed220618
14 changed files with 476 additions and 239 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 B

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 B

After

Width:  |  Height:  |  Size: 143 B

View File

@@ -237,56 +237,75 @@ export function processGameActionTags(tags, ui) {
window.eventDispatcher.emit('npc_became_hostile', { npcId });
}
case 'clone_keycard':
if (param) {
const [cardName, cardHex] = param.split('|').map(s => s.trim());
// Check if player has RFID cloner
const hasCloner = window.inventory.items.some(item =>
item && item.scenarioData &&
item.scenarioData.type === 'rfid_cloner'
);
if (!hasCloner) {
result.message = '⚠️ You need an RFID cloner to clone cards';
if (ui) ui.showNotification(result.message, 'warning');
break;
}
// Generate card data
const cardData = {
name: cardName,
rfid_hex: cardHex,
rfid_facility: parseInt(cardHex.substring(0, 2), 16),
rfid_card_number: parseInt(cardHex.substring(2, 6), 16),
rfid_protocol: 'EM4100',
type: 'keycard',
key_id: `cloned_${cardName.toLowerCase().replace(/\s+/g, '_')}`
};
// Set pending conversation return (MINIMAL CONTEXT!)
// Conversation state automatically managed by npcConversationStateManager
window.pendingConversationReturn = {
npcId: window.currentConversationNPCId,
type: window.currentConversationMinigameType || 'person-chat'
};
// Start RFID minigame in clone mode
if (window.startRFIDMinigame) {
window.startRFIDMinigame(null, null, {
mode: 'clone',
cardToClone: cardData
});
result.success = true;
result.message = `📡 Starting card clone: ${cardName}`;
console.log('🔐 Started RFID clone minigame for:', cardName);
} else {
result.message = '⚠️ RFID minigame not available';
console.warn('startRFIDMinigame not found');
}
} else {
result.message = '⚠️ clone_keycard tag missing parameters (name|hex)';
// Parameter is the card_id to clone
// Look up card data from NPC's rfidCard property
const cardId = param;
if (!cardId) {
result.message = '⚠️ clone_keycard tag missing card ID parameter';
console.warn(result.message);
break;
}
// Check if player has RFID cloner
const hasCloner = window.inventory.items.some(item =>
item && item.scenarioData &&
item.scenarioData.type === 'rfid_cloner'
);
if (!hasCloner) {
result.message = '⚠️ You need an RFID cloner to clone cards';
if (ui) ui.showNotification(result.message, 'warning');
break;
}
// Get NPC and their card data
const cloneNpcId = window.currentConversationNPCId;
let cardData = null;
if (cloneNpcId && window.npcManager) {
const npc = window.npcManager.getNPC(cloneNpcId);
if (npc?.rfidCard && npc.rfidCard.card_id === cardId) {
// Use NPC's rfidCard data
cardData = {
name: npc.rfidCard.name || cardId,
card_id: npc.rfidCard.card_id,
rfid_protocol: npc.rfidCard.rfid_protocol || 'EM4100',
type: 'keycard'
};
}
}
// Fallback if NPC card not found
if (!cardData) {
cardData = {
name: cardId,
card_id: cardId,
rfid_protocol: 'EM4100',
type: 'keycard'
};
}
// Set pending conversation return (MINIMAL CONTEXT!)
// Conversation state automatically managed by npcConversationStateManager
window.pendingConversationReturn = {
npcId: window.currentConversationNPCId,
type: window.currentConversationMinigameType || 'person-chat'
};
// Start RFID minigame in clone mode
if (window.startRFIDMinigame) {
window.startRFIDMinigame(null, null, {
mode: 'clone',
cardToClone: cardData
});
result.success = true;
result.message = `📡 Starting card clone: ${cardData.name}`;
console.log('🔐 Started RFID clone minigame for:', cardData.name);
} else {
result.message = '⚠️ RFID minigame not available';
console.warn('startRFIDMinigame not found');
}
break;

View File

@@ -337,6 +337,8 @@ export class PersonChatMinigame extends MinigameScene {
// This is important because other NPCs may have changed global variables
if (this.inkEngine && this.inkEngine.story) {
npcConversationStateManager.syncGlobalVariablesToStory(this.inkEngine.story);
// Also sync inventory-based variables on initial load
npcConversationStateManager.syncInventoryVariablesToStory(this.inkEngine.story, this.npc);
}
@@ -344,7 +346,9 @@ export class PersonChatMinigame extends MinigameScene {
// This is critical because Ink evaluates conditionals when continue() is called
if (this.inkEngine && this.inkEngine.story) {
npcConversationStateManager.syncGlobalVariablesToStory(this.inkEngine.story);
console.log('🔄 Re-synced global variables before showing dialogue');
// Also sync inventory-based variables (has_keycard, has_rfid_cloner, card protocols, etc.)
npcConversationStateManager.syncInventoryVariablesToStory(this.inkEngine.story, this.npc);
console.log('🔄 Re-synced global and inventory variables before showing dialogue');
}
this.isConversationActive = true;

View File

@@ -40,6 +40,7 @@ export class RFIDMinigame extends MinigameScene {
this.availableCards = params.availableCards || []; // For unlock mode
this.hasCloner = params.hasCloner || false; // For unlock mode
this.cardToClone = params.cardToClone; // For clone mode
this.isLockingAttempt = this.requiredCardIds.length > 0; // True if trying to unlock a specific lock, false if just browsing
// Components
this.ui = null;
@@ -99,11 +100,11 @@ export class RFIDMinigame extends MinigameScene {
// Support both card_id (new) and key_id (legacy)
const cardId = card.scenarioData?.card_id || card.scenarioData?.key_id || card.key_id;
const isCorrect = this.requiredCardIds.includes(cardId);
const isCorrect = !this.isLockingAttempt || this.requiredCardIds.includes(cardId);
if (isCorrect) {
this.animations.showTapSuccess();
this.ui.showSuccess('Access Granted');
this.ui.showSuccess(this.isLockingAttempt ? 'Access Granted' : 'Card Read');
setTimeout(() => {
this.complete(true);
@@ -128,14 +129,14 @@ export class RFIDMinigame extends MinigameScene {
// Support both card_id (new) and key_id (legacy)
const cardId = savedCard.card_id || savedCard.key_id;
const isCorrect = this.requiredCardIds.includes(cardId);
const isCorrect = !this.isLockingAttempt || this.requiredCardIds.includes(cardId);
// Check if UID-only emulation (MIFARE DESFire without master key)
const protocol = savedCard.rfid_protocol || 'EM4100';
const isUIDOnly = protocol === 'MIFARE_DESFire' && !savedCard.rfid_data?.masterKeyKnown;
// If UID-only and door doesn't accept it, reject
if (isUIDOnly && !this.acceptsUIDOnly) {
// If UID-only and door doesn't accept it, reject (only when attempting to unlock)
if (this.isLockingAttempt && isUIDOnly && !this.acceptsUIDOnly) {
this.animations.showEmulationFailure();
this.ui.showError('Reader requires full authentication');
@@ -160,7 +161,7 @@ export class RFIDMinigame extends MinigameScene {
if (isCorrect) {
this.animations.showEmulationSuccess();
this.ui.showSuccess('Access Granted');
this.ui.showSuccess(this.isLockingAttempt ? 'Access Granted' : 'Card Emulated');
// Emit event
if (window.eventDispatcher) {
@@ -177,7 +178,8 @@ export class RFIDMinigame extends MinigameScene {
setTimeout(() => {
this.complete(true);
}, 2000);
} else {
} else if (this.isLockingAttempt) {
// Only show "Access Denied" when actually trying to unlock a door
this.animations.showEmulationFailure();
this.ui.showError('Access Denied');
@@ -192,6 +194,13 @@ export class RFIDMinigame extends MinigameScene {
});
}
setTimeout(() => {
this.ui.showSavedCards();
}, 1500);
} else {
// When just browsing, show card info instead of error
this.ui.showSuccess(`Card Info: ${savedCard.name} (${protocol})`);
setTimeout(() => {
this.ui.showSavedCards();
}, 1500);
@@ -391,11 +400,11 @@ export function startRFIDMinigame(lockable, type, params) {
// Initialize framework if needed
if (!window.MinigameFramework.mainGameScene && window.game) {
window.MinigameFramework.init(window.game.scene.scenes[0]);
window.MinigameFramework.init(window.game);
}
// Start minigame
window.MinigameFramework.startMinigame('rfid', lockable, params);
window.MinigameFramework.startMinigame('rfid', null, params);
}
/**

View File

@@ -32,10 +32,11 @@ export class RFIDUIRenderer {
// Create Flipper Zero frame
const flipper = this.createFlipperFrame();
// Append to container first so screen element is in the DOM
this.container.appendChild(flipper);
// Show main menu
this.showMainMenu('unlock');
this.container.appendChild(flipper);
}
/**
@@ -47,14 +48,15 @@ export class RFIDUIRenderer {
// Create Flipper Zero frame
const flipper = this.createFlipperFrame();
// Append to container first so screen element is in the DOM
this.container.appendChild(flipper);
// Auto-start reading if card provided
if (this.minigame.params.cardToClone) {
this.showReadingScreen();
} else {
this.showMainMenu('clone');
}
this.container.appendChild(flipper);
}
/**
@@ -230,7 +232,7 @@ export class RFIDUIRenderer {
const cardItem = document.createElement('div');
cardItem.className = 'flipper-menu-item';
cardItem.textContent = `> ${card.name}`;
cardItem.addEventListener('click', () => this.showEmulationScreen(card));
cardItem.addEventListener('click', () => this.showCardDetails(card));
cardList.appendChild(cardItem);
});
@@ -245,6 +247,70 @@ export class RFIDUIRenderer {
screen.appendChild(back);
}
/**
* Show card details with Emulate button
* @param {Object} card - Card to display
*/
showCardDetails(card) {
const screen = this.getScreen();
screen.innerHTML = '';
const displayData = this.dataManager.getCardDisplayData(card);
// Breadcrumb
const breadcrumb = document.createElement('div');
breadcrumb.className = 'flipper-breadcrumb';
breadcrumb.textContent = 'RFID > Saved > Details';
screen.appendChild(breadcrumb);
// Card icon
const icon = document.createElement('div');
icon.className = 'rfid-emulate-icon';
icon.textContent = '🔑';
screen.appendChild(icon);
// Protocol with color indicator
const protocolDiv = document.createElement('div');
protocolDiv.className = 'flipper-info';
protocolDiv.style.borderLeft = `4px solid ${displayData.color}`;
protocolDiv.style.paddingLeft = '8px';
protocolDiv.innerHTML = `${displayData.icon} ${displayData.protocolName}`;
screen.appendChild(protocolDiv);
// Card name
const name = document.createElement('div');
name.className = 'flipper-card-name';
name.textContent = card.name || 'Card';
screen.appendChild(name);
// Card data fields
const data = document.createElement('div');
data.className = 'flipper-card-data';
// Show first 3 fields (most relevant for emulation)
displayData.fields.slice(0, 3).forEach(field => {
const fieldDiv = document.createElement('div');
fieldDiv.innerHTML = `${field.label}: ${field.value}`;
data.appendChild(fieldDiv);
});
screen.appendChild(data);
// Emulate button
const emulateBtn = document.createElement('div');
emulateBtn.className = 'flipper-menu-item';
emulateBtn.textContent = '> Emulate';
emulateBtn.addEventListener('click', () => this.showEmulationScreen(card));
screen.appendChild(emulateBtn);
// Back button
const back = document.createElement('div');
back.className = 'flipper-button-back';
back.textContent = '← Back';
back.addEventListener('click', () => this.showSavedCards());
screen.appendChild(back);
}
/**
* Show emulation screen (supports all protocols)
* @param {Object} card - Card to emulate
@@ -259,7 +325,7 @@ export class RFIDUIRenderer {
// Breadcrumb
const breadcrumb = document.createElement('div');
breadcrumb.className = 'flipper-breadcrumb';
breadcrumb.textContent = 'RFID > Saved > Emulate';
breadcrumb.textContent = 'RFID > Saved > Emulating';
screen.appendChild(breadcrumb);
// Emulation icon

View File

@@ -221,11 +221,19 @@ export function createDoorSpritesForRoom(roomId, position) {
// });
// console.log(`Door depth: ${doorSprite.depth} (roomDepth: ${doorY}, between tiles and sprites)`);
// Get lock properties from the destination room (the room you're trying to enter)
// Get lock properties from either the door object or the destination room
// First check if this door has explicit lock properties in the scenario
const doorDefinition = roomData.doors?.find(d =>
d.connectedRoom === connectedRoom && d.direction === direction
);
// Lock properties can come from the door definition or the connected room
const lockProps = doorDefinition || {};
const connectedRoomData = gameScenario.rooms[connectedRoom];
// Check for both keyPins (camelCase) and key_pins (snake_case) in the room data
const keyPinsArray = connectedRoomData?.keyPins || connectedRoomData?.key_pins;
const keyPinsArray = lockProps.keyPins || lockProps.key_pins ||
connectedRoomData?.keyPins || connectedRoomData?.key_pins;
// DEBUG: Log what we're finding
if (connectedRoomData?.locked) {
@@ -247,11 +255,11 @@ export function createDoorSpritesForRoom(roomId, position) {
worldX: doorX,
worldY: doorY,
open: false,
locked: connectedRoomData?.locked || false,
lockType: connectedRoomData?.lockType || null,
requires: connectedRoomData?.requires || null,
locked: lockProps.locked !== undefined ? lockProps.locked : (connectedRoomData?.locked || false),
lockType: lockProps.lockType || connectedRoomData?.lockType || null,
requires: lockProps.requires || connectedRoomData?.requires || null,
keyPins: keyPinsArray, // Include keyPins from scenario (supports both cases)
difficulty: connectedRoomData?.difficulty // Include difficulty from scenario
difficulty: lockProps.difficulty || connectedRoomData?.difficulty // Include difficulty from scenario
};
// Debug door properties

View File

@@ -609,6 +609,23 @@ export function handleObjectInteraction(sprite) {
}
// If it's not in inventory, let it fall through to the takeable logic below
}
// Handle the RFID Cloner (Flipper Zero) - only open minigame if it's already in inventory
if (sprite.scenarioData.type === "rfid_cloner") {
// Check if this is an inventory item (clicked from inventory)
const isInventoryItem = sprite.objectId && sprite.objectId.startsWith('inventory_');
if (isInventoryItem && window.startRFIDMinigame) {
console.log('Starting RFID minigame from inventory (unlock mode)');
window.startRFIDMinigame(null, null, {
mode: 'unlock',
availableCards: [],
hasCloner: true
});
return;
}
// If it's not in inventory, let it fall through to the takeable logic below
}
// Handle the Lockpick Set - pick it up if takeable, or use it if in inventory
if (sprite.scenarioData.type === "lockpick" || sprite.scenarioData.type === "lockpickset") {

View File

@@ -330,6 +330,78 @@ class NPCConversationStateManager {
}
});
}
/**
* Sync inventory-based variables to story (items player has, tools available, etc.)
* This checks what the player has in inventory and sets corresponding Ink variables.
* Only sets variables that are declared in the story to avoid StoryException errors.
* @param {Object} story - Ink story object
* @param {Object} npc - NPC data (may have rfidCard property)
*/
syncInventoryVariablesToStory(story, npc = null) {
if (!story || !window.inventory?.items) return;
// Helper to safely set a variable only if it exists in the story
const safeSetVariable = (varName, value) => {
try {
if (story.variablesState[varName] !== undefined) {
story.variablesState[varName] = value;
return true;
}
} catch (error) {
// Variable doesn't exist in this story, skip it
}
return false;
};
try {
// Check for RFID cloner in inventory
const hasRFIDCloner = window.inventory.items.some(item =>
item?.scenarioData?.type === 'rfid_cloner'
);
if (safeSetVariable('has_rfid_cloner', hasRFIDCloner)) {
console.log(`📱 Synced has_rfid_cloner = ${hasRFIDCloner}`);
}
// Check for keycards/items in inventory
const hasItems = window.inventory.items.length > 0;
if (safeSetVariable('has_keycard', hasItems)) {
console.log(`🔑 Synced has_keycard = ${hasItems}`);
}
// If NPC has RFID card info, sync card protocol details
if (npc?.rfidCard) {
if (safeSetVariable('card_protocol', npc.rfidCard.rfid_protocol || '')) {
console.log(`📡 Synced card_protocol = ${npc.rfidCard.rfid_protocol}`);
}
if (safeSetVariable('card_name', npc.rfidCard.name || '')) {
console.log(`📡 Synced card_name = ${npc.rfidCard.name}`);
}
if (safeSetVariable('card_card_id', npc.rfidCard.card_id || '')) {
console.log(`📡 Synced card_card_id = ${npc.rfidCard.card_id}`);
}
// Set protocol-specific flags
const isInstantClone = npc.rfidCard.rfid_protocol === 'EM4100' ||
npc.rfidCard.rfid_protocol === 'MIFARE_Classic_Weak_Defaults';
if (safeSetVariable('card_instant_clone', isInstantClone)) {
console.log(`⚡ Synced card_instant_clone = ${isInstantClone}`);
}
const needsAttack = npc.rfidCard.rfid_protocol === 'MIFARE_Classic_Custom_Keys';
if (safeSetVariable('card_needs_attack', needsAttack)) {
console.log(`🔓 Synced card_needs_attack = ${needsAttack}`);
}
const isUIDOnly = npc.rfidCard.rfid_protocol === 'MIFARE_DESFire';
if (safeSetVariable('card_uid_only', isUIDOnly)) {
console.log(`🛡️ Synced card_uid_only = ${isUIDOnly}`);
}
}
} catch (error) {
console.warn(`⚠️ Error syncing inventory variables to story:`, error);
}
}
}
// Create global instance

View File

@@ -0,0 +1 @@
{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["#","^speaker:npc","/#","^Hey. I'm in charge of corporate security.","\n","ev",{"VAR?":"has_keycard"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^This badge uses MIFARE Classic with custom encryption keys.","\n","^Much more secure than those old EM4100 cards.","\n",{"->":"start.9"},null]}],"nop","\n",{"->":"hub"},null],"hub":[["ev",{"VAR?":"has_keycard"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev","str","^Ask about badge security","/str","/ev",{"*":".^.c-0","flg":4},{"->":"hub.0.4"},{"c-0":["\n",{"->":"ask_security"},null]}]}],"nop","\n","ev",{"VAR?":"has_keycard"},{"VAR?":"has_rfid_cloner"},"&&",{"VAR?":"card_needs_attack"},"&&","/ev",[{"->":".^.b","c":true},{"b":["\n","ev","str","^Try to scan their badge","/str","/ev",{"*":".^.c-0","flg":4},{"->":"hub.0.14"},{"c-0":["\n","#","^speaker:player","/#","^You try to scan, but it's encrypted...","\n",{"->":"needs_attack"},null]}]}],"nop","\n","ev","str","^Chat about security","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Leave","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"chat_security"},null],"c-1":["^ ","#","^exit_conversation","/#","\n","#","^speaker:npc","/#","^Stay safe.","\n",{"->":"hub"},null]}],null],"ask_security":[["#","^speaker:npc","/#","^This badge? It's a MIFARE Classic 1K with custom encryption keys.","\n","^Much better than the factory defaults some companies use.","\n","^Can't just clone these with a quick scan. The crypto is... well, it's broken technically, but it takes time to crack.","\n","ev","str","^How long to crack?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Interesting...","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","#","^speaker:npc","/#","^With the right tools? Maybe 30 seconds using a Darkside attack.","\n","^But most people don't have those tools.","\n",{"->":"hub"},null],"c-1":["\n",{"->":"hub"},null]}],null],"chat_security":["#","^speaker:npc","/#","^Corporate security is no joke. We take access control seriously.","\n","^All our badges use custom keys. Random generation, changed quarterly.","\n","^The CEO even has a DESFire card - that's military-grade encryption.","\n",{"->":"hub"},null],"needs_attack":[["#","^speaker:npc","/#","^What are you doing?","\n","#","^speaker:player","/#","^Oh, just... checking my phone!","\n","#","^speaker:npc","/#","^That looked like you were trying to scan my badge.","\n","^You'd need to run a proper attack to get this one. Can't just quick-clone it.","\n","ev","str","^Play it cool","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Tell the truth","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^speaker:player","/#","^Sorry, my device sometimes picks up NFC signals by accident.","\n","#","^speaker:npc","/#","^Uh huh. Sure.","\n",{"->":"hub"},{"#f":5}],"c-1":["\n","#","^speaker:player","/#","^You're right - I was trying to clone it. But it's encrypted.","\n","#","^speaker:npc","/#","^Yeah, that's the point of custom keys.","\n","^You'd need to be close for about 30 seconds to run a Darkside attack.","\n","^Good luck with that while I'm watching!","\n",{"->":"hub"},{"#f":5}]}],null],"global decl":["ev",false,{"VAR=":"has_keycard"},false,{"VAR=":"has_rfid_cloner"},"str","^","/str",{"VAR=":"card_protocol"},"str","^","/str",{"VAR=":"card_name"},"str","^","/str",{"VAR=":"card_card_id"},false,{"VAR=":"card_needs_attack"},"str","^","/str",{"VAR=":"card_uid"},"/ev","end",null]}],"listDefs":{}}

View File

@@ -0,0 +1 @@
{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["#","^speaker:npc","/#","^Hi! I work security here at the building.","\n","ev",{"VAR?":"has_keycard"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^This badge on my belt? Just a basic EM4100 card. Nothing fancy.","\n",{"->":"start.9"},null]}],"nop","\n",{"->":"hub"},null],"hub":[["ev",{"VAR?":"has_keycard"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev","str","^Ask about the badge","/str","/ev",{"*":".^.c-0","flg":4},{"->":"hub.0.4"},{"c-0":["\n",{"->":"ask_badge"},null]}]}],"nop","\n","ev",{"VAR?":"has_keycard"},{"VAR?":"has_rfid_cloner"},"&&",{"VAR?":"card_instant_clone"},"&&","/ev",[{"->":".^.b","c":true},{"b":["\n","ev","str","^Casually scan their badge","/str","/ev",{"*":".^.c-0","flg":4},{"->":"hub.0.14"},{"c-0":["^ ","#","^clone_keycard:","ev",{"VAR?":"card_card_id"},"out","/ev","/#","\n","#","^speaker:player","/#","^You position your Flipper Zero near their badge while chatting...","\n","#","^speaker:npc","/#","^...and that's when I realized I'd left my lunch at home!","\n",{"->":"cloned"},null]}]}],"nop","\n","ev","str","^Chat about the job","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Leave","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"chat_job"},null],"c-1":["^ ","#","^exit_conversation","/#","\n","#","^speaker:npc","/#","^See you around!","\n",{"->":"hub"},null]}],null],"ask_badge":["#","^speaker:npc","/#","^Oh, this old thing? Yeah, it's one of those 125kHz proximity cards.","\n","^Pretty basic technology. I just wave it at the reader and it opens.","\n","^No PIN or anything - just the card itself.","\n",{"->":"hub"},null],"chat_job":["#","^speaker:npc","/#","^The job's not bad. Mostly just sitting at the desk and checking people in.","\n","^I get to read a lot during my shifts. The night shift especially is pretty quiet.","\n",{"->":"hub"},null],"cloned":[["#","^speaker:player","/#","^[You've successfully cloned the ","ev",{"VAR?":"card_name"},"out","/ev","^!]","\n","#","^speaker:npc","/#","^Anyway, I should probably get back to my post.","\n","ev","str","^Thanks for chatting","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ","#","^exit_conversation","/#","\n","#","^speaker:npc","/#","^No problem! Have a good day!","\n",{"->":"hub"},null]}],null],"global decl":["ev",false,{"VAR=":"has_keycard"},false,{"VAR=":"has_rfid_cloner"},"str","^","/str",{"VAR=":"card_protocol"},"str","^","/str",{"VAR=":"card_name"},"str","^","/str",{"VAR=":"card_card_id"},false,{"VAR=":"card_instant_clone"},"/ev","end",null]}],"listDefs":{}}

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,9 @@
{
"name": "RFID Multi-Protocol Test",
"description": "Comprehensive test for all 4 RFID protocols with attacks",
"scenario_brief": "Test all RFID protocols: EM4100, MIFARE Classic (weak/custom), and DESFire",
"scenario_brief": "Test all RFID protocols in sequence: EM4100 (instant), MIFARE weak (instant), MIFARE custom (attack), and DESFire (UID-only)",
"startRoom": "lobby",
"endGoal": "Successfully access all four security levels to test each RFID protocol",
"globalVariables": {},
"player": {
"id": "player",
@@ -23,14 +24,12 @@
"name": "Test Lobby",
"type": "room_reception",
"connections": {
"north": "low_security",
"east": "medium_security",
"south": "high_security"
"north": ["em4100_room", "mifare_weak_room"]
},
"npcs": [
{
"id": "guard_low",
"displayName": "Guard (Low Security)",
"displayName": "Guard (EM4100)",
"npcType": "person",
"position": { "x": 3, "y": 3 },
"spriteSheet": "hacker-red",
@@ -40,77 +39,29 @@
},
"storyPath": "scenarios/ink/rfid-guard-low.json",
"currentKnot": "start",
"itemsHeld": [
{
"type": "keycard",
"card_id": "employee_badge",
"rfid_protocol": "EM4100",
"name": "Employee Badge"
}
]
"rfidCard": {
"card_id": "employee_badge",
"rfid_protocol": "EM4100",
"name": "Employee Badge"
}
},
{
"id": "guard_weak",
"displayName": "Guard (Weak MIFARE)",
"npcType": "person",
"position": { "x": 6, "y": 3 },
"spriteSheet": "hacker-blue",
"spriteConfig": {
"idleFrameStart": 20,
"idleFrameEnd": 23
},
"storyPath": "scenarios/ink/rfid-guard-weak.json",
"currentKnot": "start",
"itemsHeld": [
{
"type": "keycard",
"card_id": "hotel_keycard",
"rfid_protocol": "MIFARE_Classic_Weak_Defaults",
"name": "Hotel Keycard"
}
]
},
{
"id": "guard_custom",
"displayName": "Guard (Custom Keys)",
"displayName": "Guard (MIFARE Weak)",
"npcType": "person",
"position": { "x": 9, "y": 3 },
"spriteSheet": "hacker-green",
"spriteSheet": "hacker-red",
"spriteConfig": {
"idleFrameStart": 20,
"idleFrameEnd": 23
},
"storyPath": "scenarios/ink/rfid-guard-custom.json",
"storyPath": "scenarios/ink/rfid-guard-low.json",
"currentKnot": "start",
"itemsHeld": [
{
"type": "keycard",
"card_id": "corporate_badge",
"rfid_protocol": "MIFARE_Classic_Custom_Keys",
"name": "Corporate Badge"
}
]
},
{
"id": "guard_high",
"displayName": "Guard (DESFire)",
"npcType": "person",
"position": { "x": 12, "y": 3 },
"spriteSheet": "hacker-yellow",
"spriteConfig": {
"idleFrameStart": 20,
"idleFrameEnd": 23
},
"storyPath": "scenarios/ink/rfid-guard-desfire.json",
"currentKnot": "start",
"itemsHeld": [
{
"type": "keycard",
"card_id": "executive_card",
"rfid_protocol": "MIFARE_DESFire",
"name": "Executive Card"
}
]
"rfidCard": {
"card_id": "hotel_keycard",
"rfid_protocol": "MIFARE_Classic_Weak_Defaults",
"name": "Hotel Keycard"
}
}
],
"objects": [
@@ -122,16 +73,25 @@
"takeable": true,
"readable": true,
"note_title": "RFID Protocol Security Levels",
"note_content": "🟥 LOW SECURITY (Instant Clone):\n • EM4100 (125kHz) - Old tech, no encryption\n • MIFARE Classic (Default Keys) - Factory defaults\n\n🟦 MEDIUM SECURITY (Requires Attack):\n • MIFARE Classic (Custom Keys) - 30 sec Darkside attack\n\n🟩 HIGH SECURITY (UID Only):\n • MIFARE DESFire - Strong encryption, can't crack\n • Only UID emulation works on weak readers",
"observations": "Guide to the 4 RFID protocols"
"note_content": "🟥 LEVEL 1 (Instant Clone):\n • EM4100 (125kHz) - Old tech, no encryption\n • Guard at left has this card on their belt\n • Quick and easy to exploit with Flipper Zero\n\n🟦 LEVEL 2 (Weak Defaults):\n • MIFARE Classic (Default Keys) - Uses factory defaults\n • Guard in middle has this card\n • Still instant to crack despite encryption\n\n🟨 LEVEL 3 (Custom Encryption):\n • MIFARE Classic (Custom Keys) - Requires 30 sec attack\n • Located in Medium Security Room (north from Level 2)\n • Requires Darkside attack\n\n🟩 LEVEL 4 (Strong Encryption):\n • MIFARE DESFire - Can't crack, UID only\n • Located in High Security Room (north from Level 3)\n • Best protection available",
"observations": "Guide to the 4 RFID protocol security levels"
},
{
"type": "rfid_cloner",
"name": "Flipper Zero",
"saved_cards": [],
"x": 300,
"y": 200,
"takeable": true,
"observations": "A portable multi-tool for RFID scanning and emulation. Essential for this test."
}
],
"doors": [
{
"roomId": "lobby",
"connectedRoom": "low_security",
"connectedRoom": "em4100_room",
"direction": "north",
"x": 300,
"x": 200,
"y": 100,
"locked": true,
"lockType": "rfid",
@@ -139,32 +99,22 @@
},
{
"roomId": "lobby",
"connectedRoom": "medium_security",
"direction": "east",
"x": 600,
"y": 300,
"connectedRoom": "mifare_weak_room",
"direction": "north",
"x": 400,
"y": 100,
"locked": true,
"lockType": "rfid",
"requires": ["hotel_keycard"]
},
{
"roomId": "lobby",
"connectedRoom": "high_security",
"direction": "south",
"x": 300,
"y": 500,
"locked": true,
"lockType": "rfid",
"requires": ["corporate_badge", "executive_card"],
"acceptsUIDOnly": false
}
]
},
"low_security": {
"name": "Low Security Room (EM4100)",
"em4100_room": {
"name": "Level 1: EM4100 (Instant)",
"type": "room_office",
"connections": {
"south": "lobby"
"south": "lobby",
"north": "mifare_custom_room"
},
"npcs": [],
"objects": [
@@ -175,27 +125,36 @@
"y": 300,
"takeable": true,
"readable": true,
"note_title": "✓ EM4100 Test Passed",
"note_content": "You successfully cloned an EM4100 card!\n\nThis protocol:\n• 125kHz frequency\n• No encryption\n• Instant clone\n• Used in: Old hotels, parking garages",
"note_title": "✓ Level 1 Passed: EM4100",
"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": "low_security",
"roomId": "em4100_room",
"connectedRoom": "lobby",
"direction": "south",
"x": 300,
"x": 200,
"y": 500,
"locked": false
},
{
"roomId": "em4100_room",
"connectedRoom": "mifare_custom_room",
"direction": "north",
"x": 300,
"y": 100,
"locked": false
}
]
},
"medium_security": {
"name": "Medium Security Room (Weak MIFARE)",
"mifare_weak_room": {
"name": "Level 2: MIFARE Weak (Instant)",
"type": "room_office",
"connections": {
"west": "lobby"
"south": "lobby",
"north": "mifare_custom_room"
},
"npcs": [],
"objects": [
@@ -206,40 +165,141 @@
"y": 300,
"takeable": true,
"readable": true,
"note_title": "✓ MIFARE Weak Defaults Test Passed",
"note_content": "You cloned a MIFARE Classic with default keys!\n\nThis protocol:\n• 13.56MHz NFC\n• Encrypted but uses FFFFFFFFFFFF keys\n• Dictionary attack succeeds instantly (~95%)\n• Used in: Cheap hotels, old transit cards",
"note_title": "✓ Level 2 Passed: MIFARE Weak Defaults",
"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": "medium_security",
"roomId": "mifare_weak_room",
"connectedRoom": "lobby",
"direction": "west",
"x": 100,
"y": 300,
"direction": "south",
"x": 400,
"y": 500,
"locked": false
},
{
"roomId": "mifare_weak_room",
"connectedRoom": "mifare_custom_room",
"direction": "north",
"x": 300,
"y": 100,
"locked": false
}
]
},
"high_security": {
"name": "High Security Room (Custom MIFARE / DESFire)",
"type": "room_server",
"mifare_custom_room": {
"name": "Level 3: MIFARE Custom (Attack)",
"type": "room_office",
"connections": {
"north": "lobby"
"south": ["em4100_room", "mifare_weak_room"],
"north": "desfire_room"
},
"npcs": [],
"npcs": [
{
"id": "guard_custom",
"displayName": "Guard (Custom Keys)",
"npcType": "person",
"position": { "x": 6, "y": 4 },
"spriteSheet": "hacker-green",
"spriteConfig": {
"idleFrameStart": 20,
"idleFrameEnd": 23
},
"storyPath": "scenarios/ink/rfid-guard-custom.json",
"currentKnot": "start",
"rfidCard": {
"card_id": "corporate_badge",
"rfid_protocol": "MIFARE_Classic_Custom_Keys",
"name": "Corporate Badge"
}
}
],
"objects": [
{
"type": "notes",
"name": "Success - High Security",
"name": "Success - MIFARE Custom",
"x": 300,
"y": 300,
"takeable": true,
"readable": true,
"note_title": "✓ High Security Protocols Test Passed",
"note_content": "You accessed high security!\n\nMIFARE Classic (Custom Keys):\n• Requires 30-second Darkside attack\n• Used in: Corporate badges, banks\n\nMIFARE DESFire:\n• Can't be cracked - UID only\n• Only works on poorly-configured readers\n• Used in: Government, military",
"observations": "High security test passed"
"note_title": "✓ Level 3 Passed: MIFARE Custom Keys",
"note_content": "You cracked MIFARE Classic with custom keys!\n\nThis protocol:\n• 13.56MHz NFC frequency\n• Uses custom encryption (not factory defaults)\n• Requires Darkside attack (~30 seconds)\n• Used in: Corporate badges, banks\n\nProceed north to test MIFARE DESFire (strongest protection).",
"observations": "MIFARE custom keys attack passed"
},
{
"type": "keycard",
"name": "Test Corporate Badge",
"card_id": "corporate_badge_physical",
"rfid_protocol": "MIFARE_Classic_Custom_Keys",
"x": 350,
"y": 250,
"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": {
"name": "Level 4: MIFARE DESFire (UID-Only)",
"type": "room_servers",
"connections": {
"south": "mifare_custom_room"
},
"npcs": [
{
"id": "guard_high",
"displayName": "Guard (DESFire)",
"npcType": "person",
"position": { "x": 6, "y": 4 },
"spriteSheet": "hacker-yellow",
"spriteConfig": {
"idleFrameStart": 20,
"idleFrameEnd": 23
},
"storyPath": "scenarios/ink/rfid-security-guard-fixed.json",
"currentKnot": "start",
"rfidCard": {
"card_id": "executive_card",
"rfid_protocol": "MIFARE_DESFire",
"name": "Executive Card"
}
}
],
"objects": [
{
"type": "notes",
"name": "Success - All Levels Passed",
"x": 300,
"y": 300,
"takeable": true,
"readable": true,
"note_title": "✓ Level 4 & All Tests Complete!",
"note_content": "You've successfully tested all RFID protocols!\n\nMIFARE DESFire (Strongest):\n• 13.56MHz NFC frequency\n• Military-grade AES encryption\n• Can't be cracked - only UID emulation works\n• Requires weakly-configured reader (poor UID validation)\n• Used in: Government, military, high-security\n\nSUMMARY:\n✓ Level 1: EM4100 - Instant clone\n✓ Level 2: MIFARE Weak - Dictionary attack\n✓ Level 3: MIFARE Custom - Darkside attack (30sec)\n✓ Level 4: DESFire - UID emulation only\n\nAll RFID protocols tested successfully!",
"observations": "All protocol tests passed!",
"isEndGoal": true
},
{
"type": "keycard",
"name": "Executive Card (DESFire)",
"card_id": "executive_card_physical",
"rfid_protocol": "MIFARE_DESFire",
"x": 350,
"y": 250,
"takeable": false,
"observations": "An executive's DESFire card - strongest encryption, UID-only emulation possible"
},
{
"type": "keycard",
@@ -247,18 +307,18 @@
"rfid_protocol": "EM4100",
"name": "Master Override Card",
"x": 350,
"y": 300,
"y": 350,
"takeable": true,
"observations": "A master override card that opens all doors"
"observations": "A master override card - universal access"
}
],
"doors": [
{
"roomId": "high_security",
"connectedRoom": "lobby",
"direction": "north",
"roomId": "desfire_room",
"connectedRoom": "mifare_custom_room",
"direction": "south",
"x": 300,
"y": 100,
"y": 500,
"locked": false
}
]

View File

@@ -1,8 +1,9 @@
{
"name": "RFID System Test",
"description": "Test scenario for RFID keycard lock system",
"scenario_brief": "Test scenario for RFID keycard lock system with Flipper Zero",
"scenario_brief": "Test scenario for RFID keycard lock system with Flipper Zero. Clone the guard's badge to access the secure server room.",
"startRoom": "test_lobby",
"endGoal": "Successfully clone the security guard's master keycard and unlock the secure room",
"globalVariables": {},
"player": {
"id": "player",
@@ -11,13 +12,20 @@
"startX": 200,
"startY": 200
},
"startItemsInInventory": [],
"startItemsInInventory": [
{
"type": "rfid_cloner",
"name": "Flipper Zero",
"saved_cards": [],
"observations": "A portable multi-tool for pentesters. Can read and emulate RFID cards."
}
],
"rooms": {
"test_lobby": {
"name": "Test Lobby",
"type": "room_reception",
"connections": {
"east": "test_secure"
"north": "test_secure"
},
"npcs": [
{
@@ -30,46 +38,16 @@
"idleFrameStart": 20,
"idleFrameEnd": 23
},
"storyPath": "scenarios/ink/rfid-security-guard.json",
"storyPath": "scenarios/ink/rfid-security-guard-fixed.json",
"currentKnot": "start",
"itemsHeld": [
{
"type": "keycard",
"name": "Master Keycard",
"rfid_hex": "FF4A7B9C21",
"rfid_facility": 255,
"rfid_card_number": 18811,
"rfid_protocol": "EM4100",
"key_id": "master_keycard",
"takeable": false,
"observations": "The security guard's master keycard. It has universal access."
}
]
"rfidCard": {
"card_id": "master_keycard",
"rfid_protocol": "EM4100",
"name": "Master Keycard"
}
}
],
"objects": [
{
"type": "keycard",
"name": "Employee Badge",
"rfid_hex": "01AB34CD56",
"rfid_facility": 1,
"rfid_card_number": 43981,
"rfid_protocol": "EM4100",
"key_id": "employee_badge",
"x": 300,
"y": 250,
"takeable": true,
"observations": "A standard employee access badge. Won't open high-security areas."
},
{
"type": "rfid_cloner",
"name": "Flipper Zero",
"saved_cards": [],
"x": 350,
"y": 250,
"takeable": true,
"observations": "A portable multi-tool for pentesters. Can read and emulate RFID cards."
},
{
"type": "notes",
"name": "Security Notice",
@@ -78,7 +56,7 @@
"takeable": true,
"readable": true,
"note_title": "RFID Access Control",
"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\nIf you need to clone badges, use the Flipper Zero in the lab. Tap physical badges or emulate saved ones to unlock doors.",
"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"
}
],
@@ -86,20 +64,20 @@
{
"roomId": "test_lobby",
"connectedRoom": "test_secure",
"direction": "east",
"x": 600,
"y": 300,
"direction": "north",
"x": 300,
"y": 100,
"locked": true,
"lockType": "rfid",
"requires": "master_keycard"
"requires": ["master_keycard"]
}
]
},
"test_secure": {
"name": "Secure Room",
"type": "room_office",
"name": "Secure Server Room",
"type": "room_servers",
"connections": {
"west": "test_lobby"
"south": "test_lobby"
},
"npcs": [],
"objects": [
@@ -112,7 +90,8 @@
"readable": true,
"note_title": "RFID Test Passed!",
"note_content": "Congratulations! You successfully:\n\n1. ✓ Found the Flipper Zero\n2. ✓ Cloned the Security Guard's master keycard\n3. ✓ Emulated the cloned card to unlock the door\n\nThe RFID system is working perfectly!",
"observations": "A congratulations message"
"observations": "A congratulations message",
"isEndGoal": true
},
{
"type": "keycard",
@@ -121,7 +100,7 @@
"rfid_facility": 255,
"rfid_card_number": 43597,
"rfid_protocol": "EM4100",
"key_id": "ceo_keycard",
"card_id": "ceo_keycard",
"x": 350,
"y": 300,
"takeable": true,
@@ -132,9 +111,9 @@
{
"roomId": "test_secure",
"connectedRoom": "test_lobby",
"direction": "west",
"x": 100,
"y": 300,
"direction": "south",
"x": 300,
"y": 500,
"locked": false
}
]