Files
BreakEscape/public/break_escape/js/systems/tutorial-manager.js
Z. Cliffe Schreuders 5c28743144 Update CSS file paths and enhance tutorial system
- Changed CSS file paths in index.html and show.html.erb to reflect new directory structure under public/break_escape.
- Added a new tutorial.css file with styles tailored for the tutorial system, ensuring a cohesive pixel-art aesthetic.
- Enhanced the tutorial manager to track player interactions, including clicks to move and inventory item usage, improving the tutorial experience for new players.
- Updated tutorial steps to dynamically include objectives based on the current scenario.

Files modified:
- index.html: Updated CSS links.
- show.html.erb: Updated CSS links.
- tutorial.css: New styles for the tutorial system.
- player.js: Added notifications for player actions.
- tutorial-manager.js: Enhanced logic for tracking tutorial progress and objectives.
- interactions.js: Added notifications for inventory interactions.
2026-01-19 09:54:15 +00:00

414 lines
13 KiB
JavaScript

/**
* 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;
this.playerClickedToMove = false;
this.playerClickedInventoryItem = 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 current scenario has objectives
*/
hasObjectives() {
// Check if objectives exist in the scenario
const hasScenarioObjectives = window.gameScenario?.objectives?.length > 0;
const hasManagerObjectives = window.objectivesManager?.aims?.length > 0;
return hasScenarioObjectives || hasManagerObjectives;
}
/**
* 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: 'Inventory',
instruction: 'Your inventory is at the bottom of the screen. Click on items like the Notepad to use them.',
objective: 'Click on the Notepad in your inventory to open it',
checkComplete: () => this.playerClickedInventoryItem
}
];
// Only add objectives step if scenario has objectives
if (this.hasObjectives()) {
this.steps.push({
title: 'Objectives',
instruction: 'Check the objectives panel in the top-left corner to see your current tasks.',
objective: 'Take a look at your objectives, then click Continue',
checkComplete: () => true // Always shows Continue button immediately
});
}
} 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: () => this.playerClickedToMove
},
{
title: 'Inventory',
instruction: 'Your inventory is at the bottom of the screen. Click on items like the Notepad to use them.',
objective: 'Click on the Notepad in your inventory to open it',
checkComplete: () => this.playerClickedInventoryItem
}
];
// Only add objectives step if scenario has objectives
if (this.hasObjectives()) {
this.steps.push({
title: 'Objectives',
instruction: 'Check the objectives panel in the top-left corner to see your current tasks.',
objective: 'Take a look at your objectives, then click Continue',
checkComplete: () => true // Always shows Continue button immediately
});
}
}
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;
// Remove completed class from objective
const objectiveElement = overlay.querySelector('.tutorial-objective');
if (objectiveElement) {
objectiveElement.classList.remove('completed');
}
// Hide next button initially
const nextButton = overlay.querySelector('.tutorial-next');
nextButton.style.display = 'none';
// 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!
const objectiveElement = this.tutorialOverlay.querySelector('.tutorial-objective');
if (objectiveElement) {
objectiveElement.classList.add('completed');
}
nextButton.style.display = 'inline-block';
nextButton.textContent = 'Continue →';
clearInterval(interval);
// Player must click Continue button to proceed
// (No auto-advance - gives player control)
}
}, 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 click-to-move
*/
notifyPlayerClickedToMove() {
if (this.active) {
this.playerClickedToMove = true;
}
}
/**
* Notify tutorial of inventory item click
*/
notifyPlayerClickedInventoryItem() {
if (this.active) {
this.playerClickedInventoryItem = 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;
}