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 = ` +
+

Welcome to BreakEscape!

+

Would you like to go through a quick tutorial to learn the basic controls?

+
+ + +
+
+ `; + + document.body.appendChild(overlay); + + document.getElementById('tutorial-yes').addEventListener('click', () => { + document.body.removeChild(overlay); + resolve(true); + }); + + document.getElementById('tutorial-no').addEventListener('click', () => { + this.markDeclined(); + document.body.removeChild(overlay); + resolve(false); + }); + }); + } + + /** + * Start the tutorial + */ + async start(onComplete) { + this.active = true; + this.onComplete = onComplete; + this.currentStep = 0; + + // Define tutorial steps based on device type + if (this.isMobile) { + this.steps = [ + { + title: 'Movement', + instruction: 'Click or tap on the ground where you want to move. Your character will walk to that position.', + objective: 'Try moving around by clicking different locations', + checkComplete: () => this.playerMoved + }, + { + title: 'Interaction', + instruction: 'Click or tap on objects, items, or characters to interact with them.', + objective: 'Look for highlighted objects you can interact with', + checkComplete: () => this.playerInteracted + }, + { + title: 'Objectives', + instruction: 'Check the objectives panel in the top-right corner to see your current tasks.', + objective: 'Complete objectives to progress through the game', + checkComplete: () => true, // Auto-complete this step + autoAdvanceDelay: 3000 + } + ]; + } else { + this.steps = [ + { + title: 'Movement', + instruction: 'Use W, A, S, D keys to move your character around.', + objective: 'Try moving in different directions', + checkComplete: () => this.playerMoved + }, + { + title: 'Running', + instruction: 'Hold Shift while moving to run faster.', + objective: 'Hold Shift and move with WASD', + checkComplete: () => this.playerRan + }, + { + title: 'Interaction', + instruction: 'Press E to interact with nearby objects, pick up items, or talk to characters.', + objective: 'Look for highlighted objects and press E to interact', + checkComplete: () => this.playerInteracted + }, + { + title: 'Alternative Movement', + instruction: 'You can also click on the ground to move to that location.', + objective: 'Try clicking where you want to go', + checkComplete: () => true, // Auto-complete + autoAdvanceDelay: 3000 + }, + { + title: 'Objectives', + instruction: 'Check the objectives panel in the top-right corner to see your current tasks.', + objective: 'Complete objectives to progress through the game', + checkComplete: () => true, // Auto-complete + autoAdvanceDelay: 3000 + } + ]; + } + + this.createTutorialOverlay(); + this.showStep(0); + } + + /** + * Create the tutorial overlay UI + */ + createTutorialOverlay() { + this.tutorialOverlay = document.createElement('div'); + this.tutorialOverlay.className = 'tutorial-overlay'; + this.tutorialOverlay.innerHTML = ` +
+
+ + +
+

+

+
+ Objective: + +
+
+ +
+
+ `; + + document.body.appendChild(this.tutorialOverlay); + + // Skip button + this.tutorialOverlay.querySelector('.tutorial-skip').addEventListener('click', () => { + this.skip(); + }); + + // Next button + this.tutorialOverlay.querySelector('.tutorial-next').addEventListener('click', () => { + this.nextStep(); + }); + } + + /** + * Show a specific tutorial step + */ + showStep(stepIndex) { + if (stepIndex >= this.steps.length) { + this.complete(); + return; + } + + this.currentStep = stepIndex; + const step = this.steps[stepIndex]; + + // Update UI + const overlay = this.tutorialOverlay; + overlay.querySelector('.tutorial-progress').textContent = `Step ${stepIndex + 1} of ${this.steps.length}`; + overlay.querySelector('.tutorial-title').textContent = step.title; + overlay.querySelector('.tutorial-instruction').textContent = step.instruction; + overlay.querySelector('.tutorial-objective-text').textContent = step.objective; + + // Hide next button initially + const nextButton = overlay.querySelector('.tutorial-next'); + nextButton.style.display = 'none'; + + // Check if step has auto-advance + if (step.autoAdvanceDelay) { + setTimeout(() => { + if (this.active && this.currentStep === stepIndex) { + this.nextStep(); + } + }, step.autoAdvanceDelay); + } else { + // Start checking for completion + this.checkStepCompletion(step, nextButton); + } + } + + /** + * Check if current step is completed + */ + checkStepCompletion(step, nextButton) { + const interval = setInterval(() => { + if (!this.active || this.currentStep !== this.steps.indexOf(step)) { + clearInterval(interval); + return; + } + + if (step.checkComplete()) { + // Step completed! + nextButton.style.display = 'inline-block'; + nextButton.textContent = 'Continue →'; + clearInterval(interval); + + // Auto-advance after showing success + setTimeout(() => { + if (this.active && nextButton.style.display === 'inline-block') { + this.nextStep(); + } + }, 1500); + } + }, 100); + } + + /** + * Advance to next step + */ + nextStep() { + this.showStep(this.currentStep + 1); + } + + /** + * Complete the tutorial + */ + complete() { + this.active = false; + this.markCompleted(); + + if (this.tutorialOverlay) { + document.body.removeChild(this.tutorialOverlay); + this.tutorialOverlay = null; + } + + // Show completion message + if (window.showNotification) { + window.showNotification( + 'You can now explore the facility. Check your objectives in the top-right corner!', + 'success', + 'Tutorial Complete!', + 5000 + ); + } + + if (this.onComplete) { + this.onComplete(); + } + } + + /** + * Skip the tutorial + */ + skip() { + if (confirm('Are you sure you want to skip the tutorial?')) { + this.active = false; + this.markCompleted(); + + if (this.tutorialOverlay) { + document.body.removeChild(this.tutorialOverlay); + this.tutorialOverlay = null; + } + + if (this.onComplete) { + this.onComplete(); + } + } + } + + /** + * Notify tutorial of player movement + */ + notifyPlayerMoved() { + if (this.active) { + this.playerMoved = true; + } + } + + /** + * Notify tutorial of player interaction + */ + notifyPlayerInteracted() { + if (this.active) { + this.playerInteracted = true; + } + } + + /** + * Notify tutorial of player running + */ + notifyPlayerRan() { + if (this.active) { + this.playerRan = true; + } + } + + /** + * Reset tutorial progress (for testing) + */ + static resetTutorial() { + localStorage.removeItem(TUTORIAL_STORAGE_KEY); + localStorage.removeItem(TUTORIAL_DECLINED_KEY); + } +} + +// Create singleton instance +let tutorialManagerInstance = null; + +export function getTutorialManager() { + if (!tutorialManagerInstance) { + tutorialManagerInstance = new TutorialManager(); + } + return tutorialManagerInstance; +} + +// Expose to window for easy access +if (typeof window !== 'undefined') { + window.getTutorialManager = getTutorialManager; + window.resetTutorial = TutorialManager.resetTutorial; +}