mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
Add interactive tutorial system for new players
Implements a comprehensive tutorial system that: - Prompts first-time players with option to take tutorial - Detects device type (mobile vs keyboard) for appropriate instructions - Shows interactive steps with objectives for basic controls: * Keyboard: WASD movement, Shift to run, E to interact * Mobile: Click/tap to move and interact - Tracks player actions to progress through tutorial steps - Saves completion status in localStorage - Includes polished UI with animations and responsive design Files added: - tutorial-manager.js: Core tutorial logic and state management - tutorial.css: Styled UI components with animations Files modified: - game.js: Integrated tutorial check on first load - player.js: Added tutorial notifications for movement/running - interactions.js: Added tutorial notification for interactions - main.js: Imported tutorial manager system - index.html: Added tutorial CSS stylesheet
This commit is contained in:
@@ -48,7 +48,8 @@
|
||||
<link rel="stylesheet" href="css/password-minigame.css">
|
||||
<link rel="stylesheet" href="css/text-file-minigame.css">
|
||||
<link rel="stylesheet" href="css/npc-barks.css">
|
||||
|
||||
<link rel="stylesheet" href="css/tutorial.css">
|
||||
|
||||
<!-- External JavaScript libraries -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/phaser@3.60.0/dist/phaser.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/easystarjs@0.4.4/bin/easystar-0.4.4.js"></script>
|
||||
|
||||
305
public/break_escape/css/tutorial.css
Normal file
305
public/break_escape/css/tutorial.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
368
public/break_escape/js/systems/tutorial-manager.js
Normal file
368
public/break_escape/js/systems/tutorial-manager.js
Normal file
@@ -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 = `
|
||||
<div class="tutorial-prompt-modal">
|
||||
<h2>Welcome to BreakEscape!</h2>
|
||||
<p>Would you like to go through a quick tutorial to learn the basic controls?</p>
|
||||
<div class="tutorial-prompt-buttons">
|
||||
<button id="tutorial-yes" class="tutorial-btn tutorial-btn-primary">Yes, show me</button>
|
||||
<button id="tutorial-no" class="tutorial-btn tutorial-btn-secondary">No, I'll figure it out</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div class="tutorial-panel">
|
||||
<div class="tutorial-header">
|
||||
<span class="tutorial-progress"></span>
|
||||
<button class="tutorial-skip" title="Skip Tutorial">Skip Tutorial</button>
|
||||
</div>
|
||||
<h3 class="tutorial-title"></h3>
|
||||
<p class="tutorial-instruction"></p>
|
||||
<div class="tutorial-objective">
|
||||
<strong>Objective:</strong>
|
||||
<span class="tutorial-objective-text"></span>
|
||||
</div>
|
||||
<div class="tutorial-actions">
|
||||
<button class="tutorial-next" style="display: none;">Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user