diff --git a/index.html b/index.html index 0e97b91..3ae8d52 100644 --- a/index.html +++ b/index.html @@ -48,7 +48,8 @@ - + + diff --git a/public/break_escape/css/tutorial.css b/public/break_escape/css/tutorial.css new file mode 100644 index 0000000..745ab7a --- /dev/null +++ b/public/break_escape/css/tutorial.css @@ -0,0 +1,305 @@ +/** + * Tutorial System Styles + */ + +/* Tutorial Prompt Modal */ +.tutorial-prompt-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.85); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + animation: fadeIn 0.3s ease-in; +} + +.tutorial-prompt-modal { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 2px solid #00ff88; + border-radius: 12px; + padding: 30px; + max-width: 500px; + width: 90%; + box-shadow: 0 10px 40px rgba(0, 255, 136, 0.3), + inset 0 0 20px rgba(0, 255, 136, 0.1); + animation: slideDown 0.4s ease-out; +} + +.tutorial-prompt-modal h2 { + color: #00ff88; + font-family: 'Press Start 2P', monospace; + font-size: 20px; + margin: 0 0 20px 0; + text-align: center; + text-shadow: 0 0 10px rgba(0, 255, 136, 0.5); +} + +.tutorial-prompt-modal p { + color: #e0e0e0; + font-family: 'VT323', monospace; + font-size: 20px; + line-height: 1.6; + margin: 0 0 25px 0; + text-align: center; +} + +.tutorial-prompt-buttons { + display: flex; + gap: 15px; + justify-content: center; +} + +.tutorial-btn { + font-family: 'Press Start 2P', monospace; + font-size: 12px; + padding: 12px 20px; + border: 2px solid; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + text-transform: uppercase; +} + +.tutorial-btn-primary { + background: #00ff88; + color: #1a1a2e; + border-color: #00ff88; +} + +.tutorial-btn-primary:hover { + background: #00cc6a; + border-color: #00cc6a; + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 255, 136, 0.4); +} + +.tutorial-btn-secondary { + background: transparent; + color: #888; + border-color: #444; +} + +.tutorial-btn-secondary:hover { + color: #aaa; + border-color: #666; + transform: translateY(-2px); +} + +/* Tutorial Overlay */ +.tutorial-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.4); + z-index: 9999; + pointer-events: none; + animation: fadeIn 0.3s ease-in; +} + +.tutorial-panel { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 2px solid #00ff88; + border-radius: 12px; + padding: 20px 25px; + max-width: 600px; + width: 90%; + box-shadow: 0 10px 40px rgba(0, 255, 136, 0.3), + inset 0 0 20px rgba(0, 255, 136, 0.1); + pointer-events: all; + animation: slideUp 0.4s ease-out; +} + +.tutorial-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.tutorial-progress { + color: #00ff88; + font-family: 'VT323', monospace; + font-size: 18px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.tutorial-skip { + background: transparent; + color: #888; + border: 1px solid #444; + border-radius: 4px; + padding: 6px 12px; + font-family: 'VT323', monospace; + font-size: 16px; + cursor: pointer; + transition: all 0.2s ease; +} + +.tutorial-skip:hover { + color: #ff6b6b; + border-color: #ff6b6b; +} + +.tutorial-title { + color: #00ff88; + font-family: 'Press Start 2P', monospace; + font-size: 16px; + margin: 0 0 12px 0; + text-shadow: 0 0 10px rgba(0, 255, 136, 0.5); +} + +.tutorial-instruction { + color: #e0e0e0; + font-family: 'VT323', monospace; + font-size: 20px; + line-height: 1.5; + margin: 0 0 15px 0; +} + +.tutorial-objective { + background: rgba(0, 255, 136, 0.1); + border-left: 3px solid #00ff88; + padding: 10px 15px; + margin: 15px 0; + border-radius: 4px; +} + +.tutorial-objective strong { + color: #00ff88; + font-family: 'Press Start 2P', monospace; + font-size: 12px; + display: block; + margin-bottom: 5px; +} + +.tutorial-objective-text { + color: #fff; + font-family: 'VT323', monospace; + font-size: 18px; +} + +.tutorial-actions { + display: flex; + justify-content: flex-end; + margin-top: 15px; +} + +.tutorial-next { + background: #00ff88; + color: #1a1a2e; + border: 2px solid #00ff88; + border-radius: 6px; + padding: 10px 20px; + font-family: 'Press Start 2P', monospace; + font-size: 12px; + cursor: pointer; + transition: all 0.2s ease; +} + +.tutorial-next:hover { + background: #00cc6a; + border-color: #00cc6a; + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 255, 136, 0.4); +} + +/* Animations */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideDown { + from { + transform: translateY(-50px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes slideUp { + from { + transform: translateX(-50%) translateY(50px); + opacity: 0; + } + to { + transform: translateX(-50%) translateY(0); + opacity: 1; + } +} + +/* Mobile Responsive */ +@media (max-width: 768px) { + .tutorial-prompt-modal { + padding: 20px; + } + + .tutorial-prompt-modal h2 { + font-size: 16px; + } + + .tutorial-prompt-modal p { + font-size: 18px; + } + + .tutorial-btn { + font-size: 10px; + padding: 10px 15px; + } + + .tutorial-panel { + bottom: 10px; + padding: 15px 20px; + } + + .tutorial-title { + font-size: 14px; + } + + .tutorial-instruction { + font-size: 18px; + } + + .tutorial-objective strong { + font-size: 11px; + } + + .tutorial-objective-text { + font-size: 16px; + } + + .tutorial-next { + font-size: 11px; + padding: 8px 15px; + } +} + +/* High contrast mode for accessibility */ +@media (prefers-contrast: high) { + .tutorial-prompt-modal, + .tutorial-panel { + border-width: 3px; + } + + .tutorial-btn-primary { + font-weight: bold; + } +} diff --git a/public/break_escape/js/core/game.js b/public/break_escape/js/core/game.js index af1fd13..d6b183e 100644 --- a/public/break_escape/js/core/game.js +++ b/public/break_escape/js/core/game.js @@ -20,6 +20,7 @@ import { GameOverScreen } from '../ui/game-over-screen.js'; import { PlayerCombat } from '../systems/player-combat.js'; import { NPCCombat } from '../systems/npc-combat.js'; import { ApiClient } from '../api-client.js'; // Import to ensure window.ApiClient is set +import { getTutorialManager } from '../systems/tutorial-manager.js'; // Global variables that will be set by main.js let gameScenario; @@ -863,7 +864,10 @@ export async function create() { // Show introduction introduceScenario(); - + + // Check if tutorial should be shown + checkAndShowTutorial(); + // Initialize physics debug display (visual debug off by default) if (window.initializePhysicsDebugDisplay) { window.initializePhysicsDebugDisplay(); @@ -873,6 +877,31 @@ export async function create() { window.game = this; } +/** + * Check if tutorial should be shown and display it if needed + */ +async function checkAndShowTutorial() { + const tutorialManager = getTutorialManager(); + + // Don't show tutorial if already completed or declined + if (tutorialManager.hasCompletedTutorial() || tutorialManager.hasDeclinedTutorial()) { + return; + } + + // Wait a bit for the game to settle (after title screen, etc.) + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Ask if player wants tutorial + const wantsTutorial = await tutorialManager.showTutorialPrompt(); + + if (wantsTutorial) { + // Start the tutorial + tutorialManager.start(() => { + console.log('Tutorial completed'); + }); + } +} + // Update function - main game loop export function update() { // Safety check: ensure player exists before running updates diff --git a/public/break_escape/js/core/player.js b/public/break_escape/js/core/player.js index fdd9d50..af7da96 100644 --- a/public/break_escape/js/core/player.js +++ b/public/break_escape/js/core/player.js @@ -356,16 +356,22 @@ function createPlayerAnimations() { export function movePlayerToPoint(x, y) { const worldBounds = gameRef.physics.world.bounds; - + // Ensure coordinates are within bounds x = Phaser.Math.Clamp(x, worldBounds.x, worldBounds.x + worldBounds.width); y = Phaser.Math.Clamp(y, worldBounds.y, worldBounds.y + worldBounds.height); - + // Create click indicator createClickIndicator(x, y); - + targetPoint = { x, y }; isMoving = true; + + // Notify tutorial of movement + if (window.getTutorialManager) { + const tutorialManager = window.getTutorialManager(); + tutorialManager.notifyPlayerMoved(); + } } function updatePlayerDepth(x, y) { @@ -467,6 +473,15 @@ function updatePlayerKeyboardMovement() { const speed = keyboardInput.shift ? MOVEMENT_SPEED * RUN_SPEED_MULTIPLIER : MOVEMENT_SPEED; velocityX = (dirX / magnitude) * speed; velocityY = (dirY / magnitude) * speed; + + // Notify tutorial of movement and running + if (window.getTutorialManager) { + const tutorialManager = window.getTutorialManager(); + tutorialManager.notifyPlayerMoved(); + if (keyboardInput.shift) { + tutorialManager.notifyPlayerRan(); + } + } } // Update animation speed every frame while moving diff --git a/public/break_escape/js/main.js b/public/break_escape/js/main.js index a440c8f..6214ec8 100644 --- a/public/break_escape/js/main.js +++ b/public/break_escape/js/main.js @@ -25,6 +25,9 @@ import './systems/npc-game-bridge.js'; // Bridge for NPCs to influence game stat // Import Objectives System import { getObjectivesManager } from './systems/objectives-manager.js?v=1'; +// Import Tutorial System +import { getTutorialManager } from './systems/tutorial-manager.js'; + // Global game variables window.game = null; window.gameScenario = null; diff --git a/public/break_escape/js/systems/interactions.js b/public/break_escape/js/systems/interactions.js index 9633178..7df6e80 100644 --- a/public/break_escape/js/systems/interactions.js +++ b/public/break_escape/js/systems/interactions.js @@ -1062,6 +1062,12 @@ export function tryInteractWithNearest() { // Interact with the nearest object if one was found if (nearestObject) { + // Notify tutorial of interaction + if (window.getTutorialManager) { + const tutorialManager = window.getTutorialManager(); + tutorialManager.notifyPlayerInteracted(); + } + // Check if this is a door (doors have doorProperties instead of scenarioData) if (nearestObject.doorProperties) { // Handle door interaction - triggers unlock/open sequence based on lock state diff --git a/public/break_escape/js/systems/tutorial-manager.js b/public/break_escape/js/systems/tutorial-manager.js new file mode 100644 index 0000000..5cef0d1 --- /dev/null +++ b/public/break_escape/js/systems/tutorial-manager.js @@ -0,0 +1,368 @@ +/** + * Tutorial Manager + * Handles the basic actions tutorial for new players + */ + +const TUTORIAL_STORAGE_KEY = 'tutorial_completed'; +const TUTORIAL_DECLINED_KEY = 'tutorial_declined'; + +export class TutorialManager { + constructor() { + this.active = false; + this.currentStep = 0; + this.steps = []; + this.isMobile = this.detectMobile(); + this.tutorialOverlay = null; + this.onComplete = null; + + // Track player actions for tutorial progression + this.playerMoved = false; + this.playerInteracted = false; + this.playerRan = false; + } + + /** + * Detect if the user is on a mobile device + */ + detectMobile() { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) + || window.innerWidth < 768; + } + + /** + * Check if tutorial has been completed before + */ + hasCompletedTutorial() { + return localStorage.getItem(TUTORIAL_STORAGE_KEY) === 'true'; + } + + /** + * Check if tutorial was declined + */ + hasDeclinedTutorial() { + return localStorage.getItem(TUTORIAL_DECLINED_KEY) === 'true'; + } + + /** + * Mark tutorial as completed + */ + markCompleted() { + localStorage.setItem(TUTORIAL_STORAGE_KEY, 'true'); + } + + /** + * Mark tutorial as declined + */ + markDeclined() { + localStorage.setItem(TUTORIAL_DECLINED_KEY, 'true'); + } + + /** + * Show prompt asking if player wants to do tutorial + */ + showTutorialPrompt() { + return new Promise((resolve) => { + // Create modal overlay + const overlay = document.createElement('div'); + overlay.className = 'tutorial-prompt-overlay'; + overlay.innerHTML = ` +
Would you like to go through a quick tutorial to learn the basic controls?
+ +