Enhance Minigame Framework: Add new Text File Minigame and integrate it into the existing system. Update interaction logic to support text file objects, allowing players to view and interact with file contents. Introduce CSS styles for the text file interface and update relevant HTML files to include new styles. Modify existing minigames to ensure compatibility with the new text file functionality, enhancing overall gameplay experience.

This commit is contained in:
Z. Cliffe Schreuders
2025-10-14 17:12:59 +01:00
parent e3ed198059
commit b04771e0b5
18 changed files with 2247 additions and 465 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

View File

@@ -15,11 +15,105 @@
background: #000;
}
/* Monitor bezel for desktop containers */
.container-monitor-bezel {
background: #666;
border: 8px solid #444;
border-radius: 15px;
padding: 20px;
box-shadow:
inset 0 0 20px rgba(0, 0, 0, 0.5),
0 0 30px rgba(0, 0, 0, 0.8);
position: relative;
margin: 20px;
flex: 1;
display: flex;
flex-direction: column;
}
.container-monitor-bezel::before {
content: '';
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
background: linear-gradient(45deg, #444, #666, #444);
border-radius: 19px;
z-index: -1;
}
.container-monitor-bezel::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
border-radius: 7px;
z-index: 1;
}
/* Post-it notes for container monitor bezel */
.container-monitor-bezel .postit-note {
position: absolute;
bottom: -15px;
left: 20px;
z-index: 15;
margin: 0;
transform: rotate(-3deg);
background: #ffff88;
border: 1px solid #ddd;
border-radius: 3px;
padding: 15px;
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3);
font-family: 'Press Start 2P', monospace;
font-size: 8px;
color: #333;
max-width: 200px;
word-wrap: break-word;
}
.container-monitor-bezel .postit-note::before {
content: '';
position: absolute;
top: -1px;
right: -1px;
width: 0;
height: 0;
border-left: 15px solid transparent;
border-top: 15px solid #f0f0f0;
}
.container-monitor-bezel .postit-note::after {
content: '';
position: absolute;
top: 5px;
right: 5px;
width: 8px;
height: 8px;
background: #ff6b6b;
border-radius: 50%;
box-shadow: 0 0 0 1px #fff, 0 0 0 2px #ff6b6b;
}
.container-monitor-bezel .postit-note:nth-child(2) {
left: 120px;
transform: rotate(2deg);
}
.container-monitor-bezel .postit-note:nth-child(3) {
left: 220px;
transform: rotate(-1deg);
}
.desktop-background {
flex: 1;
position: relative;
background: #000;
overflow: hidden;
min-height: 400px;
}
.desktop-wallpaper {
@@ -28,6 +122,8 @@
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background-image: url('../assets/mini-games/desktop-wallpaper.png');
background-size: cover;
background-position: center;
@@ -35,18 +131,9 @@
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
opacity: 0.8;
opacity: 1.0;
}
.desktop-wallpaper::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.2);
}
.desktop-icons {
position: relative;

View File

@@ -38,7 +38,6 @@
border-radius: 5px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5) inset;
position: relative;
overflow: hidden;
}
.minigame-message-container {
@@ -191,384 +190,4 @@
border-radius: 5px;
}
/* Password Minigame Specific Styles */
.password-minigame-area {
display: flex;
flex-direction: column;
height: 100%;
padding: 20px;
gap: 15px;
background: #1a1a1a;
position: relative;
}
.password-input-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.monitor-bezel {
background: #2a2a2a;
border: 8px solid #1a1a1a;
border-radius: 15px;
padding: 20px;
box-shadow:
inset 0 0 20px rgba(0, 0, 0, 0.5),
0 0 30px rgba(0, 0, 0, 0.8);
position: relative;
background-image: url('../assets/mini-games/desktop-wallpaper.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
}
.monitor-bezel::before {
content: '';
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
background: linear-gradient(45deg, #444, #666, #444);
border-radius: 19px;
z-index: -1;
}
.monitor-bezel::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
border-radius: 7px;
z-index: 1;
}
.monitor-screen {
border: 2px solid #333;
border-radius: 8px;
padding: 15px;
min-height: 120px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
z-index: 2;
}
.monitor-screen::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(0, 255, 0, 0.1), rgba(0, 255, 255, 0.1));
border-radius: 6px;
z-index: 1;
}
.monitor-screen > * {
position: relative;
z-index: 2;
}
.password-input-container label {
font-size: 12px;
color: #00ff00;
margin-bottom: 5px;
}
.password-field-wrapper {
position: relative;
display: flex;
align-items: center;
}
.password-field {
width: 100%;
padding: 12px 45px 12px 12px;
background: #1a1a1a;
border: 2px solid #00ff00;
border-radius: 5px;
color: white;
font-family: 'Press Start 2P', monospace;
font-size: 10px;
outline: none;
transition: border-color 0.3s ease;
}
.password-field:focus {
border-color: #00ffff;
box-shadow: 0 0 10px rgba(0, 255, 255, 0.3);
}
.password-field::placeholder {
color: #666;
}
.toggle-password-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #00ff00;
cursor: pointer;
font-size: 16px;
padding: 5px;
border-radius: 3px;
transition: background-color 0.3s ease;
}
.toggle-password-btn:hover {
background: rgba(0, 255, 0, 0.1);
}
.password-hint-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.hint-btn {
background: #f39c12;
color: white;
border: none;
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
font-family: 'Press Start 2P', monospace;
font-size: 8px;
transition: background 0.3s ease;
align-self: flex-start;
}
.hint-btn:hover {
background: #e67e22;
}
.password-hint {
background: rgba(243, 156, 18, 0.1);
border: 1px solid #f39c12;
border-radius: 5px;
padding: 10px;
font-size: 10px;
color: #f39c12;
}
.postit-note {
background: #ffff88;
border: 1px solid #ddd;
border-radius: 3px;
padding: 15px;
margin: 10px 0;
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3);
position: relative;
transform: rotate(-2deg);
font-family: 'Press Start 2P', monospace;
font-size: 8px;
color: #333;
max-width: 200px;
word-wrap: break-word;
}
.postit-note::before {
content: '';
position: absolute;
top: -1px;
right: -1px;
width: 0;
height: 0;
border-left: 15px solid transparent;
border-top: 15px solid #f0f0f0;
}
.postit-note::after {
content: '';
position: absolute;
top: 5px;
right: 5px;
width: 8px;
height: 8px;
background: #ff6b6b;
border-radius: 50%;
box-shadow: 0 0 0 1px #fff, 0 0 0 2px #ff6b6b;
}
.onscreen-keyboard {
display: flex;
flex-direction: column;
gap: 5px;
background: #2a2a2a;
border: 2px solid #444;
border-radius: 8px;
padding: 10px;
margin: 10px 0;
}
.keyboard-row {
display: flex;
justify-content: center;
gap: 3px;
flex-wrap: wrap;
}
.key {
background: #444;
color: white;
border: 1px solid #666;
border-radius: 4px;
padding: 8px 12px;
cursor: pointer;
font-family: 'Press Start 2P', monospace;
font-size: 8px;
min-width: 35px;
text-align: center;
transition: all 0.2s ease;
user-select: none;
}
.key:hover {
background: #555;
border-color: #00ff00;
}
.key:active {
background: #00ff00;
color: black;
transform: scale(0.95);
}
.key-backspace {
background: #e74c3c;
min-width: 60px;
}
.key-backspace:hover {
background: #c0392b;
}
.key-space {
background: #3498db;
min-width: 100px;
}
.key-space:hover {
background: #2980b9;
}
.key-special {
background: #9b59b6;
min-width: 80px;
}
.key-special:hover {
background: #8e44ad;
}
.password-actions {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 10px;
}
.submit-btn {
background: #2ecc71;
color: white;
border: none;
padding: 12px 24px;
border-radius: 5px;
cursor: pointer;
font-family: 'Press Start 2P', monospace;
font-size: 10px;
transition: background 0.3s ease;
}
.submit-btn:hover {
background: #27ae60;
}
.submit-btn:active {
background: #229954;
}
.cancel-btn {
background: #e74c3c;
color: white;
border: none;
padding: 12px 24px;
border-radius: 5px;
cursor: pointer;
font-family: 'Press Start 2P', monospace;
font-size: 10px;
transition: background 0.3s ease;
}
.cancel-btn:hover {
background: #c0392b;
}
.cancel-btn:active {
background: #a93226;
}
.attempts-counter {
text-align: center;
font-size: 10px;
color: #f39c12;
background: rgba(243, 156, 18, 0.1);
border: 1px solid #f39c12;
border-radius: 5px;
padding: 8px;
margin-top: 10px;
}
.attempts-counter span {
color: #e74c3c;
font-weight: bold;
}
/* Responsive design for smaller screens */
@media (max-width: 768px) {
.onscreen-keyboard {
padding: 5px;
}
.key {
padding: 6px 8px;
font-size: 7px;
min-width: 30px;
}
.key-backspace {
min-width: 50px;
}
.key-space {
min-width: 80px;
}
.key-special {
min-width: 60px;
}
.password-field {
font-size: 9px;
padding: 10px 40px 10px 10px;
}
.submit-btn, .cancel-btn {
padding: 10px 20px;
font-size: 9px;
}
}

448
css/password-minigame.css Normal file
View File

@@ -0,0 +1,448 @@
/* Password Minigame Specific Styles */
.password-minigame-area {
display: flex;
flex-direction: column;
height: 100%;
padding: 20px;
gap: 15px;
background: #1a1a1a;
position: relative;
}
.password-input-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.monitor-bezel {
background: #666;
border: 8px solid #444;
border-radius: 15px;
padding: 20px;
box-shadow:
inset 0 0 20px rgba(0, 0, 0, 0.5),
0 0 30px rgba(0, 0, 0, 0.8);
}
.monitor-bezel::before {
content: '';
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
background: linear-gradient(45deg, #444, #666, #444);
border-radius: 19px;
z-index: -1;
}
.monitor-bezel::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
border-radius: 7px;
z-index: 1;
}
.monitor-screen {
border: 2px solid #333;
border-radius: 8px;
padding: 15px;
min-height: 180px;
position: relative;
background-image: url('../assets/mini-games/desktop-wallpaper.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
z-index: 2;
}
.monitor-screen::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(0, 255, 0, 0.1), rgba(0, 255, 255, 0.1));
border-radius: 6px;
z-index: 1;
}
.monitor-screen > * {
position: relative;
z-index: 2;
}
.password-input-container label {
font-size: 12px;
color: #00ff00;
margin-bottom: 5px;
}
.password-field-wrapper {
position: relative;
display: flex;
align-items: center;
}
.password-field {
width: 100%;
padding: 12px 45px 12px 12px;
background: #1a1a1a;
border: 2px solid #00ff00;
border-radius: 5px;
color: white;
font-family: 'Press Start 2P', monospace;
font-size: 10px;
outline: none;
transition: border-color 0.3s ease;
}
.password-field:focus {
border-color: #00ffff;
box-shadow: 0 0 10px rgba(0, 255, 255, 0.3);
}
.password-field::placeholder {
color: #666;
}
.toggle-password-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #00ff00;
cursor: pointer;
font-size: 16px;
padding: 5px;
border-radius: 3px;
transition: background-color 0.3s ease;
}
.toggle-password-btn:hover {
background: rgba(0, 255, 0, 0.1);
}
.password-hint-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.hint-btn {
background: #f39c12;
color: white;
border: none;
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
font-family: 'Press Start 2P', monospace;
font-size: 8px;
transition: background 0.3s ease;
align-self: flex-start;
}
.hint-btn:hover {
background: #e67e22;
}
.password-hint {
background: rgba(243, 156, 18, 0.1);
border: 1px solid #f39c12;
border-radius: 5px;
padding: 10px;
font-size: 10px;
color: #f39c12;
}
.postit-note {
background: #ffff88;
border: 1px solid #ddd;
border-radius: 3px;
padding: 15px;
margin: 10px 0;
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3);
position: relative;
transform: rotate(-2deg);
font-family: 'Press Start 2P', monospace;
font-size: 8px;
color: #333;
max-width: 200px;
word-wrap: break-word;
top: -40px;
z-index: 15;
}
/* Post-it notes stuck to monitor bezel */
.monitor-bezel .postit-note {
bottom: -15px;
left: 20px;
z-index: 15;
margin: 0;
transform: rotate(-3deg);
}
/* Post-it notes between monitor-bezel and keyboard */
.password-minigame-area .postit-note {
position: relative;
margin: 15px 20px;
z-index: 15;
transform: rotate(-3deg);
align-self: flex-start;
}
.password-minigame-area .postit-note:nth-child(2) {
margin-left: 140px;
transform: rotate(2deg);
}
.password-minigame-area .postit-note:nth-child(3) {
margin-left: 260px;
transform: rotate(-1deg);
}
.monitor-bezel .postit-note:nth-child(2) {
left: 120px;
transform: rotate(2deg);
}
.monitor-bezel .postit-note:nth-child(3) {
left: 220px;
transform: rotate(-1deg);
}
.postit-note::before {
content: '';
position: absolute;
top: -1px;
right: -1px;
width: 0;
height: 0;
border-left: 15px solid transparent;
border-top: 15px solid #f0f0f0;
}
.postit-note::after {
content: '';
position: absolute;
top: 5px;
right: 5px;
width: 8px;
height: 8px;
background: #ff6b6b;
border-radius: 50%;
box-shadow: 0 0 0 1px #fff, 0 0 0 2px #ff6b6b;
}
.onscreen-keyboard {
display: flex;
flex-direction: column;
gap: 5px;
background: #2a2a2a;
border: 2px solid #444;
border-radius: 8px;
padding: 10px;
margin: 10px 0;
position: relative;
z-index: 10;
}
.keyboard-row {
display: flex;
justify-content: center;
gap: 3px;
flex-wrap: wrap;
}
.key {
background: #444;
color: white;
border: 1px solid #666;
border-radius: 4px;
padding: 8px 12px;
cursor: pointer;
font-family: 'Press Start 2P', monospace;
font-size: 8px;
min-width: 35px;
text-align: center;
transition: all 0.2s ease;
user-select: none;
position: relative;
z-index: 11;
}
.key:hover {
background: #555;
border-color: #00ff00;
}
.key:active {
background: #00ff00;
color: black;
transform: scale(0.95);
}
.key-backspace {
background: #e74c3c;
min-width: 60px;
}
.key-backspace:hover {
background: #c0392b;
}
.key-space {
background: #3498db;
min-width: 100px;
}
.key-space:hover {
background: #2980b9;
}
.key-special {
background: #9b59b6;
min-width: 80px;
}
.key-special:hover {
background: #8e44ad;
}
.key-shift {
background: #e67e22;
min-width: 60px;
}
.key-shift:hover {
background: #d35400;
}
.key-shift.active {
background: #f39c12;
color: #000;
}
.password-actions {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 10px;
position: relative;
z-index: 10;
}
.submit-btn {
background: #2ecc71;
color: white;
border: none;
padding: 12px 24px;
border-radius: 5px;
cursor: pointer;
font-family: 'Press Start 2P', monospace;
font-size: 10px;
transition: background 0.3s ease;
position: relative;
z-index: 11;
}
.submit-btn:hover {
background: #27ae60;
}
.submit-btn:active {
background: #229954;
}
.cancel-btn {
background: #e74c3c;
color: white;
border: none;
padding: 12px 24px;
border-radius: 5px;
cursor: pointer;
font-family: 'Press Start 2P', monospace;
font-size: 10px;
transition: background 0.3s ease;
position: relative;
z-index: 11;
}
.cancel-btn:hover {
background: #c0392b;
}
.cancel-btn:active {
background: #a93226;
}
.attempts-counter {
text-align: center;
font-size: 10px;
color: #f39c12;
background: rgba(243, 156, 18, 0.1);
border: 1px solid #f39c12;
border-radius: 5px;
padding: 8px;
margin-top: 10px;
position: relative;
z-index: 10;
}
.attempts-counter span {
color: #e74c3c;
font-weight: bold;
}
/* Responsive design for smaller screens */
@media (max-width: 768px) {
.onscreen-keyboard {
padding: 5px;
}
.key {
padding: 6px 8px;
font-size: 7px;
min-width: 30px;
}
.key-backspace {
min-width: 50px;
}
.key-space {
min-width: 80px;
}
.key-special {
min-width: 60px;
}
.password-field {
font-size: 9px;
padding: 10px 40px 10px 10px;
}
.submit-btn, .cancel-btn {
padding: 10px 20px;
font-size: 9px;
}
}

433
css/text-file-minigame.css Normal file
View File

@@ -0,0 +1,433 @@
/* Text File Minigame Styles */
/* Import VT font */
@import url('https://fonts.googleapis.com/css2?family=VT323:wght@400&display=swap');
/* Text File Minigame Container */
.text-file-container {
display: flex;
flex-direction: column;
height: 100%;
background: #ffffff;
border-radius: 12px;
border: 1px solid #d1d5db;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
overflow: hidden;
font-family: 'VT323', monospace;
}
/* Mac-style Window Title Bar */
.text-file-window-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: linear-gradient(to bottom, #f6f6f6 0%, #e8e8e8 100%);
border-bottom: 1px solid #d1d5db;
border-radius: 12px 12px 0 0;
min-height: 28px;
}
.window-controls {
display: flex;
gap: 6px;
align-items: center;
}
.window-control {
width: 12px;
height: 12px;
border-radius: 50%;
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
.window-control.close {
background: #ff5f57;
}
.window-control.minimize {
background: #ffbd2e;
}
.window-control.maximize {
background: #28ca42;
}
.window-control:hover {
transform: scale(1.1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.window-title {
font-size: 13px;
font-weight: 500;
color: #333333;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
flex: 1;
text-align: center;
margin: 0 20px;
}
/* File Header Section */
.file-header {
display: flex;
align-items: center;
padding: 16px 20px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.file-icon {
font-size: 24px;
margin-right: 12px;
color: #495057;
}
.file-info {
flex: 1;
}
.file-name {
font-size: 18px;
font-weight: bold;
color: #212529;
margin-bottom: 4px;
font-family: 'VT323', monospace;
}
.file-meta {
display: flex;
gap: 12px;
font-size: 14px;
color: #6c757d;
font-family: 'VT323', monospace;
}
.file-type {
background: #e9ecef;
padding: 2px 8px;
border-radius: 4px;
border: 1px solid #dee2e6;
color: #495057;
}
.file-size {
color: #6c757d;
}
/* File Content Area */
.file-content-area {
flex: 1;
display: flex;
flex-direction: column;
background: #ffffff;
overflow: hidden;
}
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
min-height: 36px;
}
.content-label {
font-size: 14px;
color: #495057;
font-weight: 500;
font-family: 'VT323', monospace;
}
.content-actions {
display: flex;
gap: 8px;
}
.action-btn {
background: #ffffff;
border: 1px solid #d1d5db;
color: #374151;
padding: 4px 12px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
font-family: 'VT323', monospace;
transition: all 0.2s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.action-btn:hover {
background: #f3f4f6;
border-color: #9ca3af;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.action-btn:active {
background: #e5e7eb;
transform: translateY(1px);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* File Content Display */
.file-content {
flex: 1;
padding: 16px 20px;
overflow: auto;
background: #ffffff;
border: none;
margin: 0;
}
.file-text {
color: #000000;
font-family: 'VT323', monospace;
font-size: 16px;
line-height: 1.5;
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
user-select: text;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
}
/* File Observations Section */
.file-observations {
margin: 0;
padding: 16px 20px;
background: #fff3cd;
border-top: 1px solid #ffeaa7;
}
.file-observations h4 {
color: #856404;
font-size: 14px;
margin: 0 0 8px 0;
font-family: 'VT323', monospace;
font-weight: bold;
}
.file-observations p {
color: #6c5700;
font-size: 14px;
line-height: 1.4;
margin: 0;
font-family: 'VT323', monospace;
}
/* Text Selection Styling */
.file-content ::selection {
background: #3b82f6;
color: #ffffff;
}
.file-content ::-moz-selection {
background: #3b82f6;
color: #ffffff;
}
/* Custom Scrollbar Styling */
.file-content::-webkit-scrollbar {
width: 12px;
}
.file-content::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 6px;
}
.file-content::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 6px;
border: 2px solid #f1f5f9;
}
.file-content::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
.file-content::-webkit-scrollbar-corner {
background: #f1f5f9;
}
/* Responsive Design */
@media (max-width: 768px) {
.text-file-container {
border-radius: 8px;
}
.text-file-window-header {
border-radius: 8px 8px 0 0;
padding: 6px 10px;
min-height: 24px;
}
.window-control {
width: 10px;
height: 10px;
}
.window-title {
font-size: 12px;
margin: 0 15px;
}
.file-header {
padding: 12px 16px;
}
.file-name {
font-size: 16px;
}
.file-meta {
font-size: 12px;
gap: 8px;
}
.content-header {
padding: 6px 12px;
min-height: 32px;
}
.content-label {
font-size: 12px;
}
.action-btn {
padding: 3px 8px;
font-size: 11px;
}
.file-content {
padding: 12px 16px;
}
.file-text {
font-size: 14px;
}
.file-observations {
padding: 12px 16px;
}
.file-observations h4 {
font-size: 12px;
}
.file-observations p {
font-size: 12px;
}
}
/* Dark Mode Support (Optional) */
@media (prefers-color-scheme: dark) {
.text-file-container {
background: #1f2937;
border-color: #374151;
}
.text-file-window-header {
background: linear-gradient(to bottom, #374151 0%, #1f2937 100%);
border-bottom-color: #374151;
}
.window-title {
color: #f9fafb;
}
.file-header {
background: #374151;
border-bottom-color: #4b5563;
}
.file-icon {
color: #d1d5db;
}
.file-name {
color: #f9fafb;
}
.file-meta {
color: #9ca3af;
}
.file-type {
background: #4b5563;
border-color: #6b7280;
color: #d1d5db;
}
.file-size {
color: #9ca3af;
}
.content-header {
background: #374151;
border-bottom-color: #4b5563;
}
.content-label {
color: #d1d5db;
}
.action-btn {
background: #4b5563;
border-color: #6b7280;
color: #f9fafb;
}
.action-btn:hover {
background: #6b7280;
border-color: #9ca3af;
}
.action-btn:active {
background: #374151;
}
.file-content {
background: #1f2937;
}
.file-text {
color: #f9fafb;
}
.file-observations {
background: #451a03;
border-top-color: #92400e;
}
.file-observations h4 {
color: #fbbf24;
}
.file-observations p {
color: #fcd34d;
}
.file-content::-webkit-scrollbar-track {
background: #374151;
}
.file-content::-webkit-scrollbar-thumb {
background: #6b7280;
border-color: #374151;
}
.file-content::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
.file-content::-webkit-scrollbar-corner {
background: #374151;
}
}

View File

@@ -42,6 +42,8 @@
<link rel="stylesheet" href="css/phone.css">
<link rel="stylesheet" href="css/pin.css">
<link rel="stylesheet" href="css/minigames.css">
<link rel="stylesheet" href="css/password-minigame.css">
<link rel="stylesheet" href="css/text-file-minigame.css">
<!-- External JavaScript libraries -->
<script src="https://cdn.jsdelivr.net/npm/phaser@3.60.0/dist/phaser.min.js"></script>

View File

@@ -92,12 +92,18 @@ export class ContainerMinigame extends MinigameScene {
createDesktopUI() {
this.gameContainer.innerHTML = `
<div class="container-minigame desktop-mode">
<div class="desktop-background">
<div class="desktop-wallpaper"></div>
<div class="desktop-icons" id="desktop-icons">
<!-- Desktop icons will be populated here -->
<div class="container-monitor-bezel">
<div class="desktop-background">
<div class="desktop-wallpaper"></div>
<div class="desktop-icons" id="desktop-icons">
<!-- Desktop icons will be populated here -->
</div>
</div>
</div>
${this.containerItem.scenarioData.postitNote && this.containerItem.scenarioData.showPostit ? `
<div class="postit-note">${this.containerItem.scenarioData.postitNote}</div>
` : ''}
<div class="desktop-taskbar">
<div class="desktop-info">
@@ -145,16 +151,15 @@ export class ContainerMinigame extends MinigameScene {
itemImg.name = item.type;
itemImg.objectId = `container_${index}`;
// Add click handler for taking items
if (item.takeable) {
itemImg.style.cursor = 'pointer';
// Special handling for notes - trigger notes minigame instead of taking
if (item.type === 'notes' && item.readable && item.text) {
itemImg.addEventListener('click', () => this.handleNotesItem(item, itemImg));
} else {
itemImg.addEventListener('click', () => this.takeItem(item, itemImg));
}
// Add click handler for all items (both takeable and interactive)
itemImg.style.cursor = 'pointer';
// Check if this is an interactive item that should trigger a minigame
if (this.isInteractiveItem(item)) {
itemImg.addEventListener('click', () => this.handleInteractiveItem(item, itemImg));
} else if (item.takeable) {
// Regular takeable items
itemImg.addEventListener('click', () => this.takeItem(item, itemImg));
}
// Create tooltip
@@ -195,16 +200,15 @@ export class ContainerMinigame extends MinigameScene {
iconImg.name = item.type;
iconImg.objectId = `desktop_${index}`;
// Add click handler for taking items
if (item.takeable) {
icon.style.cursor = 'pointer';
// Special handling for notes - trigger notes minigame instead of taking
if (item.type === 'notes' && item.readable && item.text) {
icon.addEventListener('click', () => this.handleNotesItem(item, iconImg));
} else {
icon.addEventListener('click', () => this.takeItem(item, iconImg));
}
// Add click handler for all items (both takeable and interactive)
icon.style.cursor = 'pointer';
// Check if this is an interactive item that should trigger a minigame
if (this.isInteractiveItem(item)) {
icon.addEventListener('click', () => this.handleInteractiveItem(item, iconImg));
} else if (item.takeable) {
// Regular takeable items
icon.addEventListener('click', () => this.takeItem(item, iconImg));
}
// Position icon randomly on desktop
@@ -233,30 +237,37 @@ export class ContainerMinigame extends MinigameScene {
}
}
handleNotesItem(item, itemElement) {
console.log('Handling notes item from container:', item);
isInteractiveItem(item) {
// Check if this item should trigger a minigame instead of being taken
// Remove the note from container display
itemElement.parentElement.remove();
// Remove from contents array
const itemIndex = this.contents.findIndex(content => content === item);
if (itemIndex !== -1) {
this.contents.splice(itemIndex, 1);
// Notes with readable text
if (item.type === 'notes' && item.readable && item.text) {
return true;
}
// Show success message
this.showMessage(`Read ${item.name}`, 'success');
// If container is now empty, update display
if (this.contents.length === 0) {
const contentsGrid = document.getElementById('container-contents-grid');
if (contentsGrid) {
contentsGrid.innerHTML = '<p class="empty-contents">This container is empty.</p>';
}
// Text files
if (item.type === 'text_file' && item.text) {
return true;
}
// Store container state for return after notes minigame
// Phone with messages
if (item.type === 'phone' && (item.text || item.voice)) {
return true;
}
// Workstation (crypto workstation)
if (item.type === 'workstation') {
return true;
}
// Add more interactive item types as needed
return false;
}
handleInteractiveItem(item, itemElement) {
console.log('Handling interactive item from container:', item);
// Store container state for return after minigame
const containerState = {
containerItem: this.containerItem,
contents: this.contents,
@@ -269,6 +280,24 @@ export class ContainerMinigame extends MinigameScene {
// Close the container minigame first
this.complete(false);
// Route to appropriate minigame based on item type
if (item.type === 'notes' && item.readable && item.text) {
this.handleNotesItem(item);
} else if (item.type === 'text_file' && item.text) {
this.handleTextFileItem(item);
} else if (item.type === 'phone' && (item.text || item.voice)) {
this.handlePhoneItem(item);
} else if (item.type === 'workstation') {
this.handleWorkstationItem(item);
} else {
console.warn('Unknown interactive item type:', item.type);
this.showMessage(`Unknown item type: ${item.type}`, 'error');
}
}
handleNotesItem(item) {
console.log('Handling notes item from container:', item);
// Start the notes minigame
if (window.startNotesMinigame) {
// Create a temporary sprite-like object for the notes minigame
@@ -286,6 +315,88 @@ export class ContainerMinigame extends MinigameScene {
}
}
handleTextFileItem(item) {
console.log('Handling text file item from container:', item);
// Start the text file minigame
if (window.MinigameFramework) {
const minigameParams = {
title: `Text File - ${item.name || 'Unknown File'}`,
fileName: item.name || 'Unknown File',
fileContent: item.text,
fileType: item.fileType || 'text',
observations: item.observations,
source: item.source || 'Container',
onComplete: (success, result) => {
console.log('Text file minigame completed:', success, result);
}
};
window.MinigameFramework.startMinigame('text-file', null, minigameParams);
} else {
console.error('MinigameFramework not available');
window.gameAlert('Text file minigame not available', 'error', 'Error', 3000);
}
}
handlePhoneItem(item) {
console.log('Handling phone item from container:', item);
// Start the phone messages minigame
if (window.MinigameFramework) {
const messages = [];
// Add text message if available
if (item.text) {
messages.push({
type: 'text',
sender: item.sender || 'Unknown',
text: item.text,
timestamp: item.timestamp || 'Unknown time',
read: false
});
}
// Add voice message if available
if (item.voice) {
messages.push({
type: 'voice',
sender: item.sender || 'Unknown',
text: item.text || null,
voice: item.voice,
timestamp: item.timestamp || 'Unknown time',
read: false
});
}
const minigameParams = {
title: item.name || 'Phone Messages',
messages: messages,
observations: item.observations,
onComplete: (success, result) => {
console.log('Phone messages minigame completed:', success, result);
}
};
window.MinigameFramework.startMinigame('phone-messages', null, minigameParams);
} else {
console.error('MinigameFramework not available');
window.gameAlert('Phone minigame not available', 'error', 'Error', 3000);
}
}
handleWorkstationItem(item) {
console.log('Handling workstation item from container:', item);
// Open the crypto workstation
if (window.openCryptoWorkstation) {
window.openCryptoWorkstation();
} else {
console.error('Crypto workstation not available');
window.gameAlert('Crypto workstation not available', 'error', 'Error', 3000);
}
}
takeItem(item, itemElement) {
console.log('Taking item from container:', item);

View File

@@ -13,6 +13,7 @@ export { ContainerMinigame, startContainerMinigame, returnToContainerAfterNotes
export { PhoneMessagesMinigame, returnToPhoneAfterNotes } from './phone/phone-messages-minigame.js';
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';
// Initialize the global minigame framework for backward compatibility
import { MinigameFramework } from './framework/minigame-manager.js';
@@ -65,6 +66,9 @@ import { PinMinigame, startPinMinigame } from './pin/pin-minigame.js';
// Import the password minigame
import { PasswordMinigame } from './password/password-minigame.js';
// Import the text file minigame
import { TextFileMinigame, returnToTextFileAfterNotes } from './text-file/text-file-minigame.js';
// Register minigames
MinigameFramework.registerScene('lockpicking', LockpickingMinigamePhaser); // Use Phaser version as default
MinigameFramework.registerScene('lockpicking-phaser', LockpickingMinigamePhaser); // Keep explicit phaser name
@@ -77,6 +81,7 @@ MinigameFramework.registerScene('container', ContainerMinigame);
MinigameFramework.registerScene('phone-messages', PhoneMessagesMinigame);
MinigameFramework.registerScene('pin', PinMinigame);
MinigameFramework.registerScene('password', PasswordMinigame);
MinigameFramework.registerScene('text-file', TextFileMinigame);
// Make minigame functions available globally
window.startNotesMinigame = startNotesMinigame;
@@ -87,4 +92,5 @@ window.startLockpickSetMinigame = startLockpickSetMinigame;
window.startContainerMinigame = startContainerMinigame;
window.returnToContainerAfterNotes = returnToContainerAfterNotes;
window.returnToPhoneAfterNotes = returnToPhoneAfterNotes;
window.returnToTextFileAfterNotes = returnToTextFileAfterNotes;
window.startPinMinigame = startPinMinigame;

View File

@@ -748,6 +748,15 @@ export function startNotesMinigame(item, noteContent, observationText, navigateT
window.returnToPhoneAfterNotes();
}, 100);
}
// Check if we need to return to text file after notes minigame
if (window.pendingTextFileReturn && window.returnToTextFileAfterNotes) {
console.log('Returning to text file after notes minigame');
// Small delay to ensure notes minigame cleanup completes
setTimeout(() => {
window.returnToTextFileAfterNotes();
}, 100);
}
}
};

View File

@@ -14,7 +14,8 @@ export class PasswordMinigame extends MinigameScene {
attempts: 0,
showPassword: false,
postitNote: params.postitNote || '',
showPostit: params.showPostit || false
showPostit: params.showPostit || false,
capsLock: false
};
// Store the correct password for validation
@@ -42,12 +43,6 @@ export class PasswordMinigame extends MinigameScene {
// Create the password entry interface
this.gameContainer.innerHTML = `
<div class="password-minigame-area">
${this.gameData.showPostit && this.gameData.postitNote ? `
<div class="postit-note">
${this.gameData.postitNote}
</div>
` : ''}
<div class="monitor-bezel">
<div class="monitor-screen">
<div class="password-input-container">
@@ -75,6 +70,12 @@ export class PasswordMinigame extends MinigameScene {
</div>
</div>
${this.gameData.showPostit && this.gameData.postitNote ? `
<div class="postit-note">
${this.gameData.postitNote}
</div>
` : ''}
${this.gameData.showKeyboard ? `
<div class="onscreen-keyboard" id="onscreen-keyboard">
<div class="keyboard-row">
@@ -114,6 +115,7 @@ export class PasswordMinigame extends MinigameScene {
<button class="key" data-key="l">L</button>
</div>
<div class="keyboard-row">
<button class="key key-shift" data-key="Shift" id="shift-key">Shift</button>
<button class="key" data-key="z">Z</button>
<button class="key" data-key="x">X</button>
<button class="key" data-key="c">C</button>
@@ -268,6 +270,8 @@ export class PasswordMinigame extends MinigameScene {
this.submitPassword();
} else if (keyValue === 'Escape') {
this.cancelPassword();
} else if (keyValue === 'Shift') {
this.toggleCapsLock();
} else if (keyValue === 'Backspace') {
this.passwordField.value = this.passwordField.value.slice(0, -1);
this.gameData.password = this.passwordField.value;
@@ -275,7 +279,8 @@ export class PasswordMinigame extends MinigameScene {
this.passwordField.value += ' ';
this.gameData.password = this.passwordField.value;
} else if (keyValue && keyValue.length === 1) {
this.passwordField.value += keyValue;
const char = this.gameData.capsLock ? keyValue.toUpperCase() : keyValue.toLowerCase();
this.passwordField.value += char;
this.gameData.password = this.passwordField.value;
}
@@ -283,6 +288,18 @@ export class PasswordMinigame extends MinigameScene {
this.passwordField.focus();
}
toggleCapsLock() {
this.gameData.capsLock = !this.gameData.capsLock;
const shiftKey = document.getElementById('shift-key');
if (shiftKey) {
if (this.gameData.capsLock) {
shiftKey.classList.add('active');
} else {
shiftKey.classList.remove('active');
}
}
}
submitPassword() {
if (!this.gameState.isActive) return;

View File

@@ -0,0 +1,377 @@
import { MinigameScene } from '../framework/base-minigame.js';
export class TextFileMinigame extends MinigameScene {
constructor(container, params) {
super(container, params);
// Ensure params is an object with default values
const safeParams = params || {};
// Initialize text file specific state
this.textFileData = {
fileName: safeParams.fileName || 'Unknown File',
fileContent: safeParams.fileContent || '',
fileType: safeParams.fileType || 'text',
observations: safeParams.observations || '',
source: safeParams.source || 'Unknown Source'
};
}
init() {
// Call parent init to set up basic UI structure
super.init();
// Customize the header
this.headerElement.innerHTML = `
<h3>📄 ${this.textFileData.fileName}</h3>
<p>Viewing text file contents</p>
`;
// Add notebook button to minigame controls (before cancel button)
if (this.controlsElement) {
const notebookBtn = document.createElement('button');
notebookBtn.className = 'minigame-button';
notebookBtn.id = 'minigame-notebook';
notebookBtn.innerHTML = '📝 Add to Notebook';
this.controlsElement.appendChild(notebookBtn);
// Change cancel button text to "Close"
const cancelBtn = document.getElementById('minigame-cancel');
if (cancelBtn) {
cancelBtn.innerHTML = 'Close';
}
}
// Set up the text file interface
this.setupTextFileInterface();
// Set up event listeners
this.setupEventListeners();
}
setupTextFileInterface() {
// Create the text file interface with Mac-style window
this.gameContainer.innerHTML = `
<div class="text-file-container">
<div class="text-file-window-header">
<div class="window-controls">
<button class="window-control close" title="Close"></button>
<button class="window-control minimize" title="Minimize"></button>
<button class="window-control maximize" title="Maximize"></button>
</div>
<div class="window-title">${this.textFileData.fileName}</div>
<div></div>
</div>
<div class="file-header">
<div class="file-icon">📄</div>
<div class="file-info">
<div class="file-name">${this.textFileData.fileName}</div>
<div class="file-meta">
<span class="file-type">${this.textFileData.fileType.toUpperCase()}</span>
<span class="file-size">${this.getFileSize()}</span>
</div>
</div>
</div>
<div class="file-content-area">
<div class="content-header">
<span class="content-label">File Contents:</span>
<div class="content-actions">
<button class="action-btn" id="copy-btn" title="Copy to clipboard">📋 Copy</button>
<button class="action-btn" id="select-all-btn" title="Select all text">Select All</button>
</div>
</div>
<div class="file-content" id="file-content">
${this.formatFileContent()}
</div>
</div>
${this.textFileData.observations ? `
<div class="file-observations">
<h4>📋 Observations:</h4>
<p>${this.textFileData.observations}</p>
</div>
` : ''}
</div>
`;
// Get references to important elements
this.fileContent = document.getElementById('file-content');
this.copyBtn = document.getElementById('copy-btn');
this.selectAllBtn = document.getElementById('select-all-btn');
// Get window control references
this.closeBtn = this.gameContainer.querySelector('.window-control.close');
this.minimizeBtn = this.gameContainer.querySelector('.window-control.minimize');
this.maximizeBtn = this.gameContainer.querySelector('.window-control.maximize');
}
formatFileContent() {
// Format the file content for display
let content = this.textFileData.fileContent;
// Escape HTML characters
content = content.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
// Convert line breaks to <br> tags
content = content.replace(/\n/g, '<br>');
// Wrap in a pre element to preserve formatting
return `<pre class="file-text">${content}</pre>`;
}
getFileSize() {
// Calculate approximate file size
const bytes = new Blob([this.textFileData.fileContent]).size;
if (bytes < 1024) {
return `${bytes} B`;
} else if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
} else {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
}
setupEventListeners() {
// Window controls
this.addEventListener(this.closeBtn, 'click', () => {
this.complete(false);
});
this.addEventListener(this.minimizeBtn, 'click', () => {
// For now, just show a message (could implement minimize functionality later)
this.showSuccess("Minimize functionality not implemented", false, 2000);
});
this.addEventListener(this.maximizeBtn, 'click', () => {
// For now, just show a message (could implement maximize functionality later)
this.showSuccess("Maximize functionality not implemented", false, 2000);
});
// Copy button
this.addEventListener(this.copyBtn, 'click', () => {
this.copyToClipboard();
});
// Select all button
this.addEventListener(this.selectAllBtn, 'click', () => {
this.selectAllText();
});
// Notebook button (in minigame controls)
const notebookBtn = document.getElementById('minigame-notebook');
if (notebookBtn) {
this.addEventListener(notebookBtn, 'click', () => {
this.addToNotebook();
});
}
// Keyboard controls
this.addEventListener(document, 'keydown', (event) => {
this.handleKeyPress(event);
});
// Double-click to select all
this.addEventListener(this.fileContent, 'dblclick', () => {
this.selectAllText();
});
}
handleKeyPress(event) {
if (!this.gameState.isActive) return;
// Handle Ctrl+A for select all
if (event.ctrlKey && event.key === 'a') {
event.preventDefault();
this.selectAllText();
}
// Handle Ctrl+C for copy (when text is selected)
if (event.ctrlKey && event.key === 'c') {
// Let the default behavior handle copying selected text
return;
}
// Handle Escape to close
if (event.key === 'Escape') {
event.preventDefault();
this.complete(false);
}
}
copyToClipboard() {
try {
// Use the modern clipboard API if available
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(this.textFileData.fileContent).then(() => {
this.showSuccess("File content copied to clipboard!", false, 2000);
}).catch(err => {
console.error('Failed to copy to clipboard:', err);
this.fallbackCopyToClipboard();
});
} else {
// Fallback for older browsers or non-secure contexts
this.fallbackCopyToClipboard();
}
} catch (error) {
console.error('Copy failed:', error);
this.fallbackCopyToClipboard();
}
}
fallbackCopyToClipboard() {
// Create a temporary textarea element
const textArea = document.createElement('textarea');
textArea.value = this.textFileData.fileContent;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
try {
textArea.focus();
textArea.select();
const successful = document.execCommand('copy');
if (successful) {
this.showSuccess("File content copied to clipboard!", false, 2000);
} else {
this.showFailure("Failed to copy to clipboard", false, 2000);
}
} catch (err) {
console.error('Fallback copy failed:', err);
this.showFailure("Copy not supported on this browser", false, 2000);
} finally {
document.body.removeChild(textArea);
}
}
selectAllText() {
// Select all text in the file content
const range = document.createRange();
range.selectNodeContents(this.fileContent);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
this.showSuccess("All text selected", false, 1000);
}
addToNotebook() {
// Check if there's content to add
if (!this.textFileData.fileContent || this.textFileData.fileContent.trim() === '') {
this.showFailure("No content to add to notebook", false, 2000);
return;
}
// Create comprehensive notebook content
const notebookContent = this.formatContentForNotebook();
const notebookTitle = `Text File - ${this.textFileData.fileName}`;
const notebookObservations = this.textFileData.observations ||
`Text file "${this.textFileData.fileName}" from ${this.textFileData.source}`;
// Check if notes minigame is available
if (window.startNotesMinigame) {
// Store the text file state globally so we can return to it
const textFileState = {
fileName: this.textFileData.fileName,
fileContent: this.textFileData.fileContent,
fileType: this.textFileData.fileType,
observations: this.textFileData.observations,
source: this.textFileData.source,
params: this.params
};
window.pendingTextFileReturn = textFileState;
// Create a text file item for the notes minigame
const textFileItem = {
scenarioData: {
type: 'text_file',
name: notebookTitle,
text: notebookContent,
observations: notebookObservations,
important: true // Mark as important since it's from a file
}
};
// Start notes minigame - it will handle returning to text file via returnToTextFileAfterNotes
window.startNotesMinigame(
textFileItem,
notebookContent,
notebookObservations,
null, // Let notes minigame auto-navigate to the newly added note
false, // Don't auto-add to inventory
false // Don't auto-close
);
this.showSuccess("Added file content to notebook", false, 2000);
} else {
this.showFailure("Notebook not available", false, 2000);
}
}
formatContentForNotebook() {
let content = `Text File: ${this.textFileData.fileName}\n`;
content += `Source: ${this.textFileData.source}\n`;
content += `Type: ${this.textFileData.fileType.toUpperCase()}\n`;
content += `Date: ${new Date().toLocaleString()}\n\n`;
content += `${'='.repeat(50)}\n\n`;
content += `FILE CONTENTS:\n`;
content += `${'-'.repeat(20)}\n\n`;
content += this.textFileData.fileContent;
content += `\n\n${'='.repeat(50)}\n`;
content += `End of File: ${this.textFileData.fileName}`;
return content;
}
start() {
// Call parent start
super.start();
console.log("Text file minigame started");
console.log("File:", this.textFileData.fileName);
console.log("Content length:", this.textFileData.fileContent.length);
}
cleanup() {
// Call parent cleanup (handles event listeners)
super.cleanup();
}
}
// Function to return to text file after notes minigame (similar to container pattern)
export function returnToTextFileAfterNotes() {
console.log('Returning to text file after notes minigame');
// Check if there's a pending text file return
if (window.pendingTextFileReturn) {
const textFileState = window.pendingTextFileReturn;
// Clear the pending return state
window.pendingTextFileReturn = null;
// Start the text file minigame with the stored state
if (window.MinigameFramework) {
window.MinigameFramework.startMinigame('text-file', null, {
title: `Text File - ${textFileState.fileName}`,
fileName: textFileState.fileName,
fileContent: textFileState.fileContent,
fileType: textFileState.fileType,
observations: textFileState.observations,
source: textFileState.source,
onComplete: (success, result) => {
console.log('Text file minigame completed:', success, result);
}
});
}
} else {
console.warn('No pending text file return state found');
}
}

View File

@@ -302,6 +302,33 @@ export function handleObjectInteraction(sprite) {
}
}
// For text_file type objects, use the text file minigame
if (data.type === 'text_file' && data.text) {
console.log('Text file object detected:', { type: data.type, name: data.name, text: data.text });
// Start the text file minigame
if (window.MinigameFramework) {
// Initialize the framework if not already done
if (!window.MinigameFramework.mainGameScene && window.game) {
window.MinigameFramework.init(window.game);
}
const minigameParams = {
title: `Text File - ${data.name || 'Unknown File'}`,
fileName: data.name || 'Unknown File',
fileContent: data.text,
fileType: data.fileType || 'text',
observations: data.observations,
source: data.source || 'Unknown Source',
onComplete: (success, result) => {
console.log('Text file minigame completed:', success, result);
}
};
window.MinigameFramework.startMinigame('text-file', null, minigameParams);
return; // Exit early since minigame handles the interaction
}
}
if (data.readable && data.text) {
message += `Text: ${data.text}\n`;

View File

@@ -85,8 +85,10 @@
"lockType": "password",
"requires": "desktop123",
"showHint": false,
"showKeyboard": false,
"showKeyboard": true,
"maxAttempts": 3,
"postitNote": "Password: desktop123",
"showPostit": true,
"locked": true,
"contents": [
{
@@ -112,7 +114,7 @@
"observations": "A digital encryption key file."
}
],
"observations": "An office computer with desktop access. Desktop mode will be automatically enabled."
"observations": "An office computer with desktop access. Desktop mode will be automatically enabled. Password is on the sticky note!"
},
{
"id": "tablet_device",
@@ -127,6 +129,8 @@
"showHint": false,
"showKeyboard": true,
"maxAttempts": 3,
"postitNote": "Tablet password: tablet2024",
"showPostit": true,
"locked": true,
"contents": [
{
@@ -145,7 +149,7 @@
"observations": "Sensitive financial information."
}
],
"observations": "An executive tablet device. Desktop mode will be automatically enabled."
"observations": "An executive tablet device. Desktop mode will be automatically enabled. Password is written on the sticky note."
}
],
"victoryConditions": [

View File

@@ -36,23 +36,17 @@
"showKeyboard": true,
"maxAttempts": 3,
"locked": true,
"requires": "password",
"requires": "secret123",
"observations": "The reception's computer, currently locked",
"postitNote": "Password: secret123",
"showPostit": true,
"contents": [
{
"type": "notes",
"name": "Private Note",
"takeable": true,
"type": "text_file",
"name": "Private",
"takeable": false,
"readable": true,
"text": "Closet keypad code: 7391 - Must move evidence to safe before audit",
"observations": "A hastily written note on expensive paper"
},
{
"type": "key",
"name": "Safe Key",
"takeable": true,
"key_id": "safe_key:52,29,44,37",
"observations": "A heavy-duty safe key hidden behind server equipment"
"text": "Closet keypad code: 7391 - Must move evidence to safe before audit"
}
]
},
@@ -144,8 +138,14 @@
"type": "pc",
"name": "Office Computer",
"takeable": false,
"requires": "password",
"observations": "A standard office computer"
"lockType": "password",
"requires": "office2024",
"showKeyboard": true,
"maxAttempts": 3,
"locked": true,
"postitNote": "Password: office2024",
"showPostit": true,
"observations": "A standard office computer with a sticky note on the monitor"
},
{
"type": "notes",
@@ -212,7 +212,14 @@
"type": "pc",
"name": "CEO Computer",
"takeable": false,
"observations": "The CEO's laptop, still warm - recently used"
"lockType": "password",
"requires": "ceo2024",
"showKeyboard": true,
"maxAttempts": 3,
"locked": true,
"postitNote": "Password: ceo2024",
"showPostit": true,
"observations": "The CEO's laptop, still warm - recently used. A sticky note is attached to the screen."
},
{
"type": "suitcase",

View File

@@ -0,0 +1,269 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Container Interactive Items Test</title>
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/minigames.css">
<link rel="stylesheet" href="css/text-file-minigame.css">
<style>
body {
background: #000;
color: #00ff00;
font-family: 'Courier New', monospace;
margin: 0;
padding: 20px;
}
.test-container {
max-width: 800px;
margin: 0 auto;
}
.test-button {
background: rgba(0, 255, 0, 0.1);
border: 2px solid #00ff00;
color: #00ff00;
padding: 15px 30px;
margin: 10px;
border-radius: 5px;
cursor: pointer;
font-family: 'Courier New', monospace;
font-size: 14px;
transition: all 0.2s ease;
}
.test-button:hover {
background: rgba(0, 255, 0, 0.2);
box-shadow: 0 0 15px rgba(0, 255, 0, 0.3);
}
.test-info {
background: rgba(0, 255, 0, 0.05);
border: 1px solid #00ff00;
padding: 15px;
margin: 20px 0;
border-radius: 5px;
}
.test-info h3 {
margin-top: 0;
color: #00ff00;
}
.test-info p {
margin: 5px 0;
color: #ccc;
}
</style>
</head>
<body>
<div class="test-container">
<h1>📦 Container Interactive Items Test</h1>
<div class="test-info">
<h3>Test Instructions:</h3>
<p>1. Click the test buttons below to launch different container scenarios</p>
<p>2. Test clicking on interactive items within containers</p>
<p>3. Verify that interactive items trigger their respective minigames</p>
<p>4. Test that takeable items still go to inventory</p>
</div>
<button class="test-button" onclick="testCEOExfilContainer()">
🎯 Test CEO Exfil Container (PC with text_file)
</button>
<button class="test-button" onclick="testMixedContainer()">
📦 Test Mixed Container (notes, text_file, key)
</button>
<button class="test-button" onclick="testPhoneContainer()">
📱 Test Phone Container (phone with messages)
</button>
<button class="test-button" onclick="testWorkstationContainer()">
💻 Test Workstation Container (crypto workstation)
</button>
<div class="test-info">
<h3>Expected Behavior:</h3>
<p>• Interactive items (notes, text_file, phone, workstation) should trigger their minigames</p>
<p>• Takeable items (keys, etc.) should go to inventory</p>
<p>• After minigame completion, should return to container</p>
<p>• Container should show remaining items after interaction</p>
</div>
</div>
<!-- Minigame container -->
<div id="minigame-container" style="display: none;"></div>
<script type="module">
// Import the minigame framework and container minigame
import { MinigameFramework } from './js/minigames/framework/minigame-manager.js';
import { ContainerMinigame, startContainerMinigame } from './js/minigames/container/container-minigame.js';
import { TextFileMinigame } from './js/minigames/text-file/text-file-minigame.js';
import { PhoneMessagesMinigame } from './js/minigames/phone/phone-messages-minigame.js';
import { NotesMinigame, startNotesMinigame } from './js/minigames/notes/notes-minigame.js';
// Initialize the framework
window.MinigameFramework = MinigameFramework;
MinigameFramework.registerScene('container', ContainerMinigame);
MinigameFramework.registerScene('text-file', TextFileMinigame);
MinigameFramework.registerScene('phone-messages', PhoneMessagesMinigame);
MinigameFramework.registerScene('notes', NotesMinigame);
// Make functions available globally
window.startNotesMinigame = startNotesMinigame;
window.startContainerMinigame = startContainerMinigame;
// Mock crypto workstation function
window.openCryptoWorkstation = function() {
alert('Crypto Workstation opened! (This would normally open the crypto workstation interface)');
};
// Test data
const testContainers = {
ceoExfil: {
containerItem: {
scenarioData: {
name: 'Reception Computer',
type: 'pc',
observations: 'The reception computer, currently unlocked'
},
name: 'pc'
},
contents: [
{
type: 'text_file',
name: 'Private',
takeable: false,
readable: true,
text: 'Closet keypad code: 7391 - Must move evidence to safe before audit',
observations: 'Private file found on reception computer',
source: 'Reception Computer'
}
],
isTakeable: false
},
mixed: {
containerItem: {
scenarioData: {
name: 'Desk Drawer',
type: 'drawer',
observations: 'A desk drawer containing various items'
},
name: 'drawer'
},
contents: [
{
type: 'notes',
name: 'Meeting Notes',
takeable: false,
readable: true,
text: 'Important meeting notes about the security audit. Need to review access logs and update passwords.',
observations: 'Handwritten meeting notes'
},
{
type: 'text_file',
name: 'Security Report',
takeable: false,
readable: true,
text: 'SECURITY AUDIT REPORT\n\nDate: Today\nStatus: In Progress\n\nFindings:\n- Weak passwords detected\n- Unauthorized access attempts\n- Missing security patches\n\nRecommendations:\n- Implement 2FA\n- Update all passwords\n- Install security updates',
observations: 'Digital security audit report',
source: 'IT Department'
},
{
type: 'key',
name: 'Office Key',
takeable: true,
observations: 'A key that might open something important'
}
],
isTakeable: false
},
phone: {
containerItem: {
scenarioData: {
name: 'Smartphone',
type: 'phone',
observations: 'A smartphone found on the desk'
},
name: 'phone'
},
contents: [
{
type: 'phone',
name: 'Phone Messages',
takeable: false,
text: 'Hey, did you get the files? The boss is asking about the audit.',
voice: 'This is a test voice message. The security audit is scheduled for tomorrow morning.',
sender: 'Colleague',
timestamp: '2:30 PM',
observations: 'Phone with text and voice messages'
}
],
isTakeable: true
},
workstation: {
containerItem: {
scenarioData: {
name: 'Crypto Workstation',
type: 'workstation',
observations: 'A specialized cryptographic workstation'
},
name: 'workstation'
},
contents: [
{
type: 'workstation',
name: 'Crypto Workstation',
takeable: false,
observations: 'A specialized cryptographic workstation for data analysis'
}
],
isTakeable: false
}
};
// Test functions
window.testCEOExfilContainer = function() {
startContainerMinigame(
testContainers.ceoExfil.containerItem,
testContainers.ceoExfil.contents,
testContainers.ceoExfil.isTakeable
);
};
window.testMixedContainer = function() {
startContainerMinigame(
testContainers.mixed.containerItem,
testContainers.mixed.contents,
testContainers.mixed.isTakeable
);
};
window.testPhoneContainer = function() {
startContainerMinigame(
testContainers.phone.containerItem,
testContainers.phone.contents,
testContainers.phone.isTakeable
);
};
window.testWorkstationContainer = function() {
startContainerMinigame(
testContainers.workstation.containerItem,
testContainers.workstation.contents,
testContainers.workstation.isTakeable
);
};
console.log('Container Interactive Items Test Page Loaded');
console.log('Available test functions:', Object.keys(window).filter(key => key.startsWith('test')));
</script>
</body>
</html>

View File

@@ -8,6 +8,7 @@
<!-- Include the game's CSS -->
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/minigames.css">
<link rel="stylesheet" href="css/password-minigame.css">
<link rel="stylesheet" href="css/notifications.css">
<style>
@@ -128,9 +129,9 @@
<!-- Password with Onscreen Keyboard -->
<div class="test-section">
<h2>Password with Onscreen Keyboard</h2>
<h2>Password with Onscreen Keyboard (with Shift)</h2>
<div class="test-description">
Test password entry with onscreen QWERTY keyboard. Password: "keyboard"
Test password entry with onscreen QWERTY keyboard including shift key for caps. Password: "Keyboard"
</div>
<button class="test-button" onclick="testPasswordWithKeyboard()">Test Password with Keyboard</button>
</div>
@@ -218,10 +219,10 @@
};
window.testPasswordWithKeyboard = function() {
updateTestResults("Starting password with keyboard test...");
updateTestResults("Starting password with keyboard test (try using shift key for caps)...");
MinigameFramework.startMinigame('password', null, {
title: 'Password with Keyboard Test',
password: 'keyboard',
password: 'Keyboard',
showHint: false,
showKeyboard: true,
maxAttempts: 3,

View File

@@ -0,0 +1,364 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Text File Minigame Test</title>
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/minigames.css">
<link rel="stylesheet" href="css/text-file-minigame.css">
<style>
body {
background: #000;
color: #00ff00;
font-family: 'Courier New', monospace;
margin: 0;
padding: 20px;
}
.test-container {
max-width: 800px;
margin: 0 auto;
}
.test-button {
background: rgba(0, 255, 0, 0.1);
border: 2px solid #00ff00;
color: #00ff00;
padding: 15px 30px;
margin: 10px;
border-radius: 5px;
cursor: pointer;
font-family: 'Courier New', monospace;
font-size: 14px;
transition: all 0.2s ease;
}
.test-button:hover {
background: rgba(0, 255, 0, 0.2);
box-shadow: 0 0 15px rgba(0, 255, 0, 0.3);
}
.test-info {
background: rgba(0, 255, 0, 0.05);
border: 1px solid #00ff00;
padding: 15px;
margin: 20px 0;
border-radius: 5px;
}
.test-info h3 {
margin-top: 0;
color: #00ff00;
}
.test-info p {
margin: 5px 0;
color: #ccc;
}
</style>
</head>
<body>
<div class="test-container">
<h1>📄 Text File Minigame Test</h1>
<div class="test-info">
<h3>Test Instructions:</h3>
<p>1. Click the test buttons below to launch different text file scenarios</p>
<p>2. Test the "Add to Notebook" functionality</p>
<p>3. Test copy and select all features</p>
<p>4. Test keyboard shortcuts (Ctrl+A, Ctrl+C, Escape)</p>
</div>
<button class="test-button" onclick="testBasicTextFile()">
📄 Test Basic Text File
</button>
<button class="test-button" onclick="testLongTextFile()">
📄 Test Long Text File
</button>
<button class="test-button" onclick="testCodeFile()">
💻 Test Code File
</button>
<button class="test-button" onclick="testEmptyFile()">
📄 Test Empty File
</button>
<button class="test-button" onclick="testCEOExfilFile()">
🎯 Test CEO Exfil File (from scenario)
</button>
<div class="test-info">
<h3>Expected Behavior:</h3>
<p>• Text file minigame should open with Mac-style window decorations</p>
<p>• VT323 font should be used for all text content</p>
<p>• Black text on white background (clean, readable interface)</p>
<p>• Window controls (close, minimize, maximize) should be functional</p>
<p>• "Add to Notebook" button should work (if notes minigame is available)</p>
<p>• Copy and Select All buttons should function</p>
<p>• Escape key should close the minigame</p>
</div>
</div>
<!-- Minigame container -->
<div id="minigame-container" style="display: none;"></div>
<script type="module">
// Import the minigame framework and text file minigame
import { MinigameFramework } from './js/minigames/framework/minigame-manager.js';
import { TextFileMinigame } from './js/minigames/text-file/text-file-minigame.js';
// Initialize the framework
window.MinigameFramework = MinigameFramework;
MinigameFramework.registerScene('text-file', TextFileMinigame);
// Test data
const testFiles = {
basic: {
fileName: 'readme.txt',
fileContent: `Welcome to BreakEscape!
This is a test text file to demonstrate the text file minigame functionality.
Features:
- File content display
- Copy to clipboard
- Select all text
- Add to notebook
- Keyboard shortcuts
The minigame should handle this content properly and provide a good user experience.`,
fileType: 'text',
observations: 'A simple text file for testing purposes',
source: 'Test Environment'
},
long: {
fileName: 'security_report.txt',
fileContent: `SECURITY INCIDENT REPORT
================================
Date: ${new Date().toLocaleDateString()}
Time: ${new Date().toLocaleTimeString()}
Reporter: Security Team
Classification: CONFIDENTIAL
INCIDENT SUMMARY:
A potential security breach was detected in the main server room. Unauthorized access was attempted through the ventilation system. The intruder appears to have been familiar with the building layout and security protocols.
DETAILS:
- Time of incident: 02:30 AM
- Location: Server Room A, Floor 3
- Method: Physical intrusion via ventilation shaft
- Duration: Approximately 15 minutes
- Damage: None detected
- Evidence: Fingerprints on server rack, disturbed dust patterns
SUSPECT PROFILE:
- Height: Approximately 5'8" to 6'0"
- Build: Average to athletic
- Clothing: Dark clothing, possibly tactical gear
- Equipment: Professional lockpicking tools, small flashlight
- Knowledge: Familiar with building layout and security systems
EVIDENCE COLLECTED:
1. Fingerprint samples from server rack
2. Dust disturbance patterns
3. Security camera footage (partially obscured)
4. Tool marks on ventilation cover
RECOMMENDATIONS:
1. Increase security patrols in server room area
2. Install additional cameras in ventilation access points
3. Review and update access control procedures
4. Conduct security awareness training for all staff
5. Consider upgrading physical security measures
FOLLOW-UP ACTIONS:
- [ ] Review security camera footage
- [ ] Interview security personnel on duty
- [ ] Check access logs for any anomalies
- [ ] Coordinate with law enforcement if necessary
- [ ] Update incident response procedures
This incident highlights the need for enhanced physical security measures and regular security assessments. All personnel should remain vigilant and report any suspicious activity immediately.
END OF REPORT`,
fileType: 'text',
observations: 'Confidential security incident report',
source: 'Security Department'
},
code: {
fileName: 'access_control.py',
fileContent: `#!/usr/bin/env python3
"""
Access Control System
Handles authentication and authorization for the BreakEscape system
"""
import hashlib
import time
from datetime import datetime, timedelta
class AccessControl:
def __init__(self):
self.users = {}
self.sessions = {}
self.failed_attempts = {}
self.max_attempts = 3
self.lockout_duration = 300 # 5 minutes
def hash_password(self, password):
"""Hash password using SHA-256"""
return hashlib.sha256(password.encode()).hexdigest()
def authenticate(self, username, password):
"""Authenticate user with username and password"""
if username in self.failed_attempts:
if self.failed_attempts[username]['count'] >= self.max_attempts:
if time.time() - self.failed_attempts[username]['last_attempt'] < self.lockout_duration:
return False, "Account locked due to too many failed attempts"
if username in self.users:
hashed_password = self.hash_password(password)
if self.users[username]['password'] == hashed_password:
# Reset failed attempts on successful login
if username in self.failed_attempts:
del self.failed_attempts[username]
# Create session
session_id = self.create_session(username)
return True, session_id
else:
self.record_failed_attempt(username)
return False, "Invalid password"
else:
return False, "User not found"
def create_session(self, username):
"""Create a new session for the user"""
session_id = hashlib.md5(f"{username}{time.time()}".encode()).hexdigest()
self.sessions[session_id] = {
'username': username,
'created': time.time(),
'last_activity': time.time()
}
return session_id
def validate_session(self, session_id):
"""Validate if session is still active"""
if session_id in self.sessions:
session = self.sessions[session_id]
if time.time() - session['last_activity'] < 3600: # 1 hour timeout
session['last_activity'] = time.time()
return True, session['username']
else:
del self.sessions[session_id]
return False, "Session expired"
return False, "Invalid session"
def record_failed_attempt(self, username):
"""Record a failed login attempt"""
if username not in self.failed_attempts:
self.failed_attempts[username] = {'count': 0, 'last_attempt': 0}
self.failed_attempts[username]['count'] += 1
self.failed_attempts[username]['last_attempt'] = time.time()
def add_user(self, username, password, role='user'):
"""Add a new user to the system"""
if username in self.users:
return False, "User already exists"
self.users[username] = {
'password': self.hash_password(password),
'role': role,
'created': time.time()
}
return True, "User created successfully"
# Example usage
if __name__ == "__main__":
ac = AccessControl()
# Add some test users
ac.add_user("admin", "admin123", "admin")
ac.add_user("user1", "password123", "user")
# Test authentication
success, result = ac.authenticate("admin", "admin123")
if success:
print(f"Login successful! Session ID: {result}")
else:
print(f"Login failed: {result}")`,
fileType: 'python',
observations: 'Python code for access control system',
source: 'Development Team'
},
empty: {
fileName: 'empty.txt',
fileContent: '',
fileType: 'text',
observations: 'An empty text file',
source: 'Test Environment'
},
ceoExfil: {
fileName: 'Private',
fileContent: 'Closet keypad code: 7391 - Must move evidence to safe before audit',
fileType: 'text',
observations: 'Private file found on reception computer',
source: 'Reception Computer'
}
};
// Test functions
window.testBasicTextFile = function() {
startTextFileMinigame(testFiles.basic);
};
window.testLongTextFile = function() {
startTextFileMinigame(testFiles.long);
};
window.testCodeFile = function() {
startTextFileMinigame(testFiles.code);
};
window.testEmptyFile = function() {
startTextFileMinigame(testFiles.empty);
};
window.testCEOExfilFile = function() {
startTextFileMinigame(testFiles.ceoExfil);
};
function startTextFileMinigame(fileData) {
const container = document.getElementById('minigame-container');
container.style.display = 'block';
const minigameParams = {
title: `Text File - ${fileData.fileName}`,
fileName: fileData.fileName,
fileContent: fileData.fileContent,
fileType: fileData.fileType,
observations: fileData.observations,
source: fileData.source,
onComplete: (success, result) => {
console.log('Text file minigame completed:', success, result);
container.style.display = 'none';
}
};
MinigameFramework.startMinigame('text-file', container, minigameParams);
}
console.log('Text File Minigame Test Page Loaded');
console.log('Available test functions:', Object.keys(window).filter(key => key.startsWith('test')));
</script>
</body>
</html>

View File

@@ -8,6 +8,7 @@
<!-- Include the game's CSS -->
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/minigames.css">
<link rel="stylesheet" href="css/password-minigame.css">
<link rel="stylesheet" href="css/notifications.css">
<style>