Update Room and Scenario Files: Rename an object in the reception room JSON files to "phone" for clarity. Modify the scenario brief in the CEO exfiltration scenario for improved narrative engagement. Enhance CSS styles across various minigames, including adjustments to layout, font sizes, and new sections for item displays in lockpicking and password minigames, improving overall user experience.

This commit is contained in:
Z. Cliffe Schreuders
2025-10-21 14:43:34 +01:00
parent 8fe71efa89
commit 2230885f12
15 changed files with 386 additions and 45 deletions

View File

@@ -145,7 +145,7 @@
"gid":230,
"height":17,
"id":47,
"name":"",
"name":"phone",
"rotation":0,
"type":"",
"visible":true,

View File

@@ -153,7 +153,7 @@
"gid":230,
"height":17,
"id":47,
"name":"",
"name":"phone",
"rotation":0,
"type":"",
"visible":true,

View File

@@ -1,5 +1,5 @@
{
"scenario_brief": "Hi, You are a cyber investigator tasked with uncovering evidence of corporate espionage. Anonymous tips suggest the CEO has been selling company secrets, but you need proof.",
"scenario_brief": "old version. You are a cyber investigator tasked with uncovering evidence of corporate espionage. Anonymous tips suggest the CEO has been selling company secrets, but you need proof.",
"startRoom": "reception",
"rooms": {
"reception": {

View File

@@ -225,7 +225,6 @@
.desktop-taskbar {
background: rgba(0, 0, 0, 0.8);
border-top: 2px solid #333;
padding: 10px 20px;
display: flex;
justify-content: space-between;
@@ -271,9 +270,6 @@
align-items: center;
gap: 20px;
padding: 20px;
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.container-image {
@@ -374,10 +370,11 @@
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
transition: transform 0.2s ease;
transform: scale(2);
}
.container-content-item:hover {
transform: scale(1.1);
transform: scale(2.2);
}
.container-content-tooltip {

View File

@@ -80,7 +80,7 @@
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
font-size: 18px;
white-space: nowrap;
pointer-events: none;
opacity: 0;

View File

@@ -27,3 +27,73 @@
.lockpick-feedback:empty {
display: none;
}
/* Lockpicking item section styling */
.lockpicking-item-section {
display: flex;
align-items: center;
gap: 20px;
padding: 20px;
background: rgba(0, 0, 0, 0.3);
margin-bottom: 20px;
}
.lockpicking-item-image {
min-width: 80px;
min-height: 80px;
object-fit: contain;
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
border: 2px solid rgba(255, 255, 255, 0.3);
background: rgba(0, 0, 0, 0.3);
flex-shrink: 0;
}
.lockpicking-item-info h4 {
font-family: 'Press Start 2P', monospace;
font-size: 20px;
margin: 0 0 10px 0;
color: #3498db;
}
.lockpicking-item-info p {
font-size: 16px;
margin: 0;
color: #ecf0f1;
line-height: 1.4;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.lockpicking-item-section {
flex-direction: column;
text-align: center;
gap: 15px;
}
.lockpicking-item-image {
width: 70px;
height: 70px;
}
.lockpicking-item-info h4 {
font-size: 16px;
}
.lockpicking-item-info p {
font-size: 14px;
}
}
/* Phaser game container styling - prevent margin/padding shifts */
#phaser-game-container {
margin: 0 !important;
padding: 0 !important;
}
#phaser-game-container canvas {
margin: 0 !important;
padding: 0 !important;
display: block;
}

View File

@@ -10,6 +10,38 @@
position: relative;
}
.password-image-section {
display: flex;
align-items: center;
gap: 20px;
padding: 20px;
}
.password-image {
width: 80px;
height: 80px;
object-fit: contain;
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
border: 2px solid rgba(255, 255, 255, 0.3);
background: rgba(0, 0, 0, 0.3);
}
.password-info h4 {
font-family: 'Press Start 2P', monospace;
font-size: 20px;
margin: 0 0 10px 0;
color: #3498db;
}
.password-info p {
font-size: 20px;
margin: 0;
color: #ecf0f1;
line-height: 1.4;
}
.password-input-container {
display: flex;
flex-direction: column;
@@ -529,4 +561,16 @@
padding: 10px 20px;
font-size: 18px;
}
.password-image-section {
flex-direction: column;
align-items: center;
gap: 10px;
padding: 10px;
}
.password-image {
width: 60px;
height: 60px;
}
}

View File

@@ -1,4 +1,38 @@
/* Phone Messages Minigame Styles */
.phone-image-section {
display: flex;
align-items: center;
gap: 20px;
padding: 20px;
margin-bottom: 20px;
}
.phone-image {
width: 80px;
height: 80px;
object-fit: contain;
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
border: 2px solid rgba(255, 255, 255, 0.3);
background: rgba(0, 0, 0, 0.3);
}
.phone-info h4 {
font-family: 'Press Start 2P', monospace;
font-size: 20px;
margin: 0 0 10px 0;
color: #3498db;
}
.phone-info p {
font-size: 20px;
margin: 0;
color: #ecf0f1;
line-height: 1.4;
}
.phone-messages-container {
display: flex;
flex-direction: column;
@@ -7,7 +41,6 @@
max-width: 400px;
margin: 0 auto;
background: #a0a0ad;
border: 4px solid #333;
clip-path: polygon(
0px calc(100% - 10px),
2px calc(100% - 10px),
@@ -469,3 +502,4 @@
padding: 5px;
font-weight: bold;
}

View File

@@ -92,7 +92,6 @@ export class ContainerMinigame extends MinigameScene {
<div class="container-actions">
${this.isTakeable ? '<button class="minigame-button" id="take-container-btn">Take Container</button>' : ''}
<button class="minigame-button" id="close-container-btn">Close</button>
</div>
</div>
`;
@@ -100,6 +99,15 @@ export class ContainerMinigame extends MinigameScene {
createDesktopUI() {
this.gameContainer.innerHTML = `
<div class="container-image-section">
<img src="assets/objects/${this.containerItem.name}.png"
alt="${this.containerItem.scenarioData.name}"
class="container-image">
<div class="container-info">
<h4>${this.containerItem.scenarioData.name}</h4>
<p>${this.containerItem.scenarioData.observations || ''}</p>
</div>
</div>
<div class="container-minigame desktop-mode">
<div class="container-monitor-bezel">
<div class="desktop-background">
@@ -115,13 +123,8 @@ export class ContainerMinigame extends MinigameScene {
` : ''}
<div class="desktop-taskbar">
<div class="desktop-info">
<span class="desktop-title">${this.containerItem.scenarioData.name}</span>
<span class="desktop-subtitle">${this.containerItem.scenarioData.observations || ''}</span>
</div>
<div class="desktop-actions">
${this.isTakeable ? '<button class="minigame-button" id="take-container-btn">Take Container</button>' : ''}
<button class="minigame-button" id="close-container-btn">Close</button>
</div>
</div>
</div>

View File

@@ -251,9 +251,37 @@ export class LockpickingMinigamePhaser extends MinigameScene {
<p>Apply tension and hold click on pins to lift them to the shear line</p>
`;
// Create the lockable item display section if item info is provided
this.createLockableItemDisplay();
this.setupPhaserGame();
}
createLockableItemDisplay() {
// Create display for the locked item (door, chest, etc.)
const itemName = this.params?.itemName || this.lockable || 'Locked Item';
const itemImage = this.params?.itemImage || null;
const itemObservations = this.params?.itemObservations || '';
if (!itemImage) return; // Only create if image is provided
// Create container for the item display
const itemDisplayDiv = document.createElement('div');
itemDisplayDiv.className = 'lockpicking-item-section';
itemDisplayDiv.innerHTML = `
<img src="${itemImage}"
alt="${itemName}"
class="lockpicking-item-image">
<div class="lockpicking-item-info">
<h4>${itemName}</h4>
<p>${itemObservations}</p>
</div>
`;
// Insert before the game container
this.gameContainer.parentElement.insertBefore(itemDisplayDiv, this.gameContainer);
}
setupPhaserGame() {
// Create a container for the Phaser game
this.gameContainer.innerHTML = `
@@ -3108,6 +3136,7 @@ export class LockpickingMinigamePhaser extends MinigameScene {
if (!this.lockState.tensionApplied) {
this.updateFeedback("Apply tension first before picking pins");
this.flashWrenchRed();
}
});
@@ -3231,6 +3260,7 @@ export class LockpickingMinigamePhaser extends MinigameScene {
if (!this.lockState.tensionApplied) {
this.updateFeedback("Apply tension first before picking pins");
this.flashWrenchRed();
}
}
}
@@ -4418,4 +4448,37 @@ export class LockpickingMinigamePhaser extends MinigameScene {
}
return array;
}
flashWrenchRed() {
// Flash the tension wrench red to indicate tension is needed
if (!this.wrenchGraphics) return;
const originalFillStyle = this.lockState.tensionApplied ? 0x00ff00 : 0x888888;
// Store original state
const originalClear = this.wrenchGraphics.clear.bind(this.wrenchGraphics);
// Flash red 3 times
for (let i = 0; i < 3; i++) {
this.scene.time.delayedCall(i * 150, () => {
this.wrenchGraphics.clear();
this.wrenchGraphics.fillStyle(0xff0000); // Red
// Long vertical arm
this.wrenchGraphics.fillRect(0, -120, 10, 170);
// Short horizontal arm
this.wrenchGraphics.fillRect(0, 40, 37.5, 10);
});
this.scene.time.delayedCall(i * 150 + 75, () => {
this.wrenchGraphics.clear();
this.wrenchGraphics.fillStyle(originalFillStyle); // Back to original color
// Long vertical arm
this.wrenchGraphics.fillRect(0, -120, 10, 170);
// Short horizontal arm
this.wrenchGraphics.fillRect(0, 40, 37.5, 10);
});
}
}
}

View File

@@ -51,8 +51,43 @@ export class PasswordMinigame extends MinigameScene {
setupPasswordInterface() {
// Create the password entry interface
// Check if we can get the device image from sprite or params
const getImageData = () => {
// Try to get sprite data from params (from lockable object passed through minigame framework)
const sprite = this.params.sprite || this.params.lockable;
if (sprite && sprite.name && sprite.scenarioData) {
return {
imageFile: sprite.name,
deviceName: sprite.scenarioData.name || sprite.name,
observations: sprite.scenarioData.observations || ''
};
}
// Fallback to explicit params if provided
if (this.params.deviceImage) {
return {
imageFile: this.params.deviceImage,
deviceName: this.params.deviceName || this.params.title || 'Device',
observations: this.params.observations || ''
};
}
return null;
};
const imageData = getImageData();
this.gameContainer.innerHTML = `
<div class="password-minigame-area">
${imageData ? `
<div class="password-image-section">
<img src="assets/objects/${imageData.imageFile}.png"
alt="${imageData.deviceName}"
class="password-image">
<div class="password-info">
<h4>${imageData.deviceName}</h4>
<p>${imageData.observations}</p>
</div>
</div>
` : ''}
<div class="monitor-bezel">
<div class="monitor-screen">
<div class="password-input-container">

View File

@@ -328,7 +328,8 @@ export class PhoneMessagesMinigame extends MinigameScene {
const notebookBtn = document.createElement('button');
notebookBtn.className = 'minigame-button';
notebookBtn.id = 'minigame-notebook';
notebookBtn.innerHTML = '<img src="assets/icons/notebook.png" alt="Notebook" class="icon"> Add to Notebook';
notebookBtn.innerHTML = '<img src="assets/icons/notes-sm.png" alt="Notebook" class="icon-small"> Add to Notebook';
this.controlsElement.appendChild(notebookBtn);
// Change cancel button text to "Close"
@@ -347,7 +348,42 @@ export class PhoneMessagesMinigame extends MinigameScene {
setupPhoneInterface() {
// Create the phone interface
// Check if we can get the device image from sprite or params
const getImageData = () => {
// Try to get sprite data from params (from lockable object passed through minigame framework)
const sprite = this.params.sprite || this.params.lockable;
if (sprite && sprite.name && sprite.scenarioData) {
return {
imageFile: sprite.name,
deviceName: sprite.scenarioData.name || sprite.name,
observations: sprite.scenarioData.observations || ''
};
}
// Fallback to explicit params if provided
if (this.params.deviceImage) {
return {
imageFile: this.params.deviceImage,
deviceName: this.params.deviceName || this.params.title || 'Device',
observations: this.params.observations || ''
};
}
return null;
};
const imageData = getImageData();
this.gameContainer.innerHTML = `
${imageData ? `
<div class="phone-image-section">
<img src="assets/objects/${imageData.imageFile}.png"
alt="${imageData.deviceName}"
class="phone-image">
<div class="phone-info">
<h4>${imageData.deviceName}</h4>
<p>${imageData.observations}</p>
</div>
</div>
` : ''}
<div class="phone-messages-container">
<div class="phone-screen">
<div class="phone-header">
@@ -393,10 +429,7 @@ export class PhoneMessagesMinigame extends MinigameScene {
</div>
</div>
<div class="phone-observations" id="phone-observations">
<!-- Observations will be populated here -->
</div>
`;
// Get references to important elements
@@ -406,7 +439,6 @@ export class PhoneMessagesMinigame extends MinigameScene {
this.messageTime = document.getElementById('message-time');
this.messageContent = document.getElementById('message-content');
this.messageActions = document.getElementById('message-actions');
this.phoneObservations = document.getElementById('phone-observations');
// Control buttons
this.prevBtn = document.getElementById('prev-btn');
@@ -422,9 +454,7 @@ export class PhoneMessagesMinigame extends MinigameScene {
// Populate messages
this.populateMessages();
// Populate observations
this.populateObservations();
}
populateMessages() {
@@ -444,13 +474,11 @@ export class PhoneMessagesMinigame extends MinigameScene {
messageElement.className = `message-item ${message.type || 'text'}`;
messageElement.dataset.index = index;
const icon = message.type === 'voice' ? '🎤' : '💬';
const preview = message.type === 'voice'
? (message.text ? message.text.substring(0, 50) + '...' : 'Voice message')
: (message.text || 'No text content');
messageElement.innerHTML = `
<div class="message-icon">${icon}</div>
<div class="message-preview">
<div class="message-sender">${message.sender || 'Unknown'}</div>
<div class="message-text">${preview}</div>
@@ -463,22 +491,7 @@ export class PhoneMessagesMinigame extends MinigameScene {
});
}
populateObservations() {
// Get observations from the original object data
const observations = this.params?.observations || this.phoneData?.observations;
if (observations) {
this.phoneObservations.innerHTML = `
<div class="observations-content">
<h4>Observations:</h4>
<p>${observations}</p>
</div>
`;
} else {
this.phoneObservations.innerHTML = '';
}
}
setupEventListeners() {
// Message list clicks
this.addEventListener(this.messagesList, 'click', (event) => {
@@ -788,7 +801,7 @@ export class PhoneMessagesMinigame extends MinigameScene {
if (message.type === 'voice') {
// For voice messages, show audio icon and transcript
content += `🎵 [Audio Message]\n`;
content += `[Audio Message]\n`;
content += `Transcript: ${message.voice || message.text || 'No transcript available'}\n\n`;
} else {
// For text messages, show the content

View File

@@ -292,6 +292,7 @@ export function handleObjectInteraction(sprite) {
title: data.name || 'Phone Messages',
messages: messages,
observations: data.observations,
lockable: sprite,
onComplete: (success, result) => {
console.log('Phone messages minigame completed:', success, result);
}
@@ -318,6 +319,7 @@ export function handleObjectInteraction(sprite) {
fileContent: data.text,
fileType: data.fileType || 'text',
observations: data.observations,
lockable: sprite,
source: data.source || 'Unknown Source',
onComplete: (success, result) => {
console.log('Text file minigame completed:', success, result);

View File

@@ -35,10 +35,49 @@ export function startLockpickingMinigame(lockable, scene, difficulty = 'medium',
window.MinigameFramework.init(scene);
}
// Extract item information from lockable object (handles both items and doors)
let itemName, itemImage, itemObservations;
// Check if this is a door (has doorProperties) or an item
if (lockable?.doorProperties) {
// This is a door - get the connected room name
const connectedRoomId = lockable.doorProperties.connectedRoom;
const currentRoomId = lockable.doorProperties.roomId;
const gameScenario = window.gameScenario;
const connectedRoom = gameScenario?.rooms?.[connectedRoomId];
const currentRoom = gameScenario?.rooms?.[currentRoomId];
const isLocked = lockable.doorProperties.locked;
// Use door_sign if available (player-visible sign on the door)
const doorSignOrName = connectedRoom?.door_sign || connectedRoom?.name;
// Format item name with locked status
if (doorSignOrName) {
// Has door_sign or room name - show it
itemName = isLocked ? `Locked ${doorSignOrName}` : doorSignOrName;
itemObservations = `Door to ${doorSignOrName}`;
} else {
// No door_sign and undiscovered room - use generic names
itemName = 'Locked door';
const currentRoomName = currentRoom?.name || currentRoomId;
itemObservations = `A door leading out of ${currentRoomName}`;
}
itemImage = 'assets/tiles/door.png'; // Use default door image
} else {
// This is a regular item - use scenarioData
itemName = lockable?.scenarioData?.name || lockable?.name || 'Locked Item';
itemImage = lockable?.name ? `assets/objects/${lockable.name}.png` : null;
itemObservations = lockable?.scenarioData?.observations || '';
}
// Start the lockpicking minigame (Phaser version)
window.MinigameFramework.startMinigame('lockpicking', null, {
lockable: lockable,
difficulty: difficulty,
itemName: itemName,
itemImage: itemImage,
itemObservations: itemObservations,
cancelText: 'Close',
onComplete: (success, result) => {
if (success) {
@@ -158,6 +197,42 @@ export function startKeySelectionMinigame(lockable, type, playerKeys, requiredKe
console.log(`Using default lock configuration for ${lockId}:`, lockConfig);
}
// Extract item information from lockable object (handles both items and doors)
let itemName, itemImage, itemObservations;
// Check if this is a door (has doorProperties) or an item
if (lockable?.doorProperties) {
// This is a door - get the connected room name
const connectedRoomId = lockable.doorProperties.connectedRoom;
const currentRoomId = lockable.doorProperties.roomId;
const gameScenario = window.gameScenario;
const connectedRoom = gameScenario?.rooms?.[connectedRoomId];
const currentRoom = gameScenario?.rooms?.[currentRoomId];
const isLocked = lockable.doorProperties.locked;
// Use door_sign if available (player-visible sign on the door)
const doorSignOrName = connectedRoom?.door_sign || connectedRoom?.name;
// Format item name with locked status
if (doorSignOrName) {
// Has door_sign or room name - show it
itemName = isLocked ? `${doorSignOrName}` : doorSignOrName;
itemObservations = `Door to ${doorSignOrName}`;
} else {
// No door_sign and undiscovered room - use generic names
itemName = 'Locked door';
const currentRoomName = currentRoom?.name || currentRoomId;
itemObservations = `A door leading out of ${currentRoomName}`;
}
itemImage = 'assets/tiles/door.png'; // Use default door image
} else {
// This is a regular item - use scenarioData
itemName = lockable?.scenarioData?.name || lockable?.name || 'Locked Item';
itemImage = lockable?.name ? `assets/objects/${lockable.name}.png` : null;
itemObservations = lockable?.scenarioData?.observations || '';
}
// Start the key selection minigame
window.MinigameFramework.startMinigame('lockpicking', null, {
keyMode: true,
@@ -167,6 +242,9 @@ export function startKeySelectionMinigame(lockable, type, playerKeys, requiredKe
pinCount: lockConfig.pinCount,
predefinedPinHeights: lockConfig.pinHeights, // Pass the predefined pin heights
difficulty: lockConfig.difficulty,
itemName: itemName,
itemImage: itemImage,
itemObservations: itemObservations,
cancelText: 'Close',
onComplete: (success, result) => {
if (success) {
@@ -299,6 +377,7 @@ export function startPasswordMinigame(lockable, type, correctPassword, callback,
maxAttempts: options.maxAttempts || 3,
postitNote: options.postitNote || '',
showPostit: options.showPostit || false,
lockable: lockable,
onComplete: (success, result) => {
if (success) {
console.log('PASSWORD MINIGAME SUCCESS');

View File

@@ -1,5 +1,5 @@
{
"scenario_brief": "You are a cyber investigator tasked with uncovering evidence of corporate espionage. Anonymous tips suggest the CEO has been selling company secrets, but you need proof.",
"scenario_brief": "Hi! You are a cyber investigator tasked with uncovering evidence of corporate espionage. Anonymous tips suggest the CEO has been selling company secrets, but you need proof.",
"startRoom": "reception",
"rooms": {
"reception": {
@@ -95,6 +95,7 @@
"lockType": "key",
"requires": "office1_key:40,35,38,32,10",
"difficulty": "easy",
"door_sign": "4A Hot Desks",
"connections": {
"north": ["office2", "office3"],