feat(intro): add ebike onboarding sequence
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { MISSION_NOTIFICATION_IMAGE_PATHS } from "@/data/gameplay/missionNotifications";
|
||||
import type { RepairMissionId } from "@/types/gameplay/repairMission";
|
||||
|
||||
interface MissionNotificationProps {
|
||||
mission: RepairMissionId;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
export function MissionNotification({
|
||||
mission,
|
||||
visible = true,
|
||||
}: MissionNotificationProps): React.JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className={`mission-notification${visible ? "" : " mission-notification--hidden"}`}
|
||||
aria-live="polite"
|
||||
>
|
||||
<div className="mission-notification__glow" />
|
||||
<span className="mission-notification__image-wrap">
|
||||
<img
|
||||
className="mission-notification__image"
|
||||
src={MISSION_NOTIFICATION_IMAGE_PATHS[mission]}
|
||||
alt="Nouvel objectif de mission"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,11 +6,12 @@ const REVEAL_DURATION_MS = 2000;
|
||||
|
||||
/**
|
||||
* Fade-out overlay revealing the game world.
|
||||
* Calls completeIntro() when the fade is done — completeIntro also marks
|
||||
* intro.currentStep as "completed" so no separate setIntroStep call is needed.
|
||||
* Moves to the ebike onboarding step when the fade is done. The intro only
|
||||
* completes after the player rides the ebike and triggers the breakdown.
|
||||
*/
|
||||
export function IntroRevealOverlay(): React.JSX.Element {
|
||||
const completeIntro = useGameStore((state) => state.completeIntro);
|
||||
const setIntroStep = useGameStore((state) => state.setIntroStep);
|
||||
const setCanMove = useGameStore((state) => state.setCanMove);
|
||||
const prefersReducedMotion = usePrefersReducedMotion();
|
||||
const [opacity, setOpacity] = useState(1);
|
||||
|
||||
@@ -20,14 +21,15 @@ export function IntroRevealOverlay(): React.JSX.Element {
|
||||
}, 100);
|
||||
|
||||
const completeTimeout = window.setTimeout(() => {
|
||||
completeIntro();
|
||||
setCanMove(true);
|
||||
setIntroStep("await-ebike-mount");
|
||||
}, REVEAL_DURATION_MS);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(fadeTimeout);
|
||||
window.clearTimeout(completeTimeout);
|
||||
};
|
||||
}, [completeIntro]);
|
||||
}, [setCanMove, setIntroStep]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||
|
||||
const INTRO_VIDEO_PATH = "/cinematics/intro.mp4";
|
||||
const SKIP_KEYS = new Set(["Enter", " "]);
|
||||
const SKIP_HINT_HIDE_DELAY_MS = 1000;
|
||||
|
||||
/**
|
||||
* Full-screen video player for the intro cinematic.
|
||||
@@ -10,6 +11,8 @@ const SKIP_KEYS = new Set(["Enter", " "]);
|
||||
*/
|
||||
export function IntroVideoPlayer(): React.JSX.Element {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const hideHintTimeoutRef = useRef<number | null>(null);
|
||||
const [showSkipHint, setShowSkipHint] = useState(false);
|
||||
const setIntroStep = useGameStore((state) => state.setIntroStep);
|
||||
|
||||
const handleVideoEnd = useCallback(() => {
|
||||
@@ -33,11 +36,33 @@ export function IntroVideoPlayer(): React.JSX.Element {
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [handleSkip]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (hideHintTimeoutRef.current !== null) {
|
||||
window.clearTimeout(hideHintTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleMouseMove = useCallback(() => {
|
||||
setShowSkipHint(true);
|
||||
|
||||
if (hideHintTimeoutRef.current !== null) {
|
||||
window.clearTimeout(hideHintTimeoutRef.current);
|
||||
}
|
||||
|
||||
hideHintTimeoutRef.current = window.setTimeout(() => {
|
||||
setShowSkipHint(false);
|
||||
hideHintTimeoutRef.current = null;
|
||||
}, SKIP_HINT_HIDE_DELAY_MS);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
role="region"
|
||||
aria-label="Vidéo d'introduction. Appuyez sur Entrée pour passer."
|
||||
onClick={handleSkip}
|
||||
onMouseMove={handleMouseMove}
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
@@ -71,6 +96,8 @@ export function IntroVideoPlayer(): React.JSX.Element {
|
||||
color: "rgba(255, 255, 255, 0.6)",
|
||||
fontSize: 14,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
opacity: showSkipHint ? 1 : 0,
|
||||
transition: "opacity 240ms ease",
|
||||
}}
|
||||
>
|
||||
Appuyez pour passer
|
||||
|
||||
Reference in New Issue
Block a user