mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-07-18 00:07:09 +00:00
feat: add new lce team cinematic intro
This commit is contained in:
parent
b76e0c305d
commit
bd632e7e0b
BIN
public/images/LCE Team.png
Normal file
BIN
public/images/LCE Team.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
BIN
public/images/esrb_warning.png
Normal file
BIN
public/images/esrb_warning.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 273 KiB After Width: | Height: | Size: 143 KiB |
|
|
@ -25,6 +25,7 @@ pub fn load_config_raw(app: AppHandle) -> AppConfig {
|
|||
animations_enabled: Some(true),
|
||||
vfx_enabled: Some(true),
|
||||
rpc_enabled: Some(true),
|
||||
start_fullscreen: Some(true),
|
||||
music_vol: Some(50),
|
||||
sfx_vol: Some(100),
|
||||
legacy_mode: Some(false),
|
||||
|
|
|
|||
|
|
@ -97,6 +97,14 @@ pub fn run() {
|
|||
relay::join_game,
|
||||
])
|
||||
.setup(|app| {
|
||||
let app_handle = app.handle().clone();
|
||||
let config = config::load_config_raw(app_handle.clone());
|
||||
if config.start_fullscreen.unwrap_or(true) {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.set_fullscreen(true);
|
||||
}
|
||||
}
|
||||
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() > 1 && !args[1].starts_with('-') {
|
||||
let instance_id = args[1].clone();
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ pub struct AppConfig {
|
|||
pub animations_enabled: Option<bool>,
|
||||
pub vfx_enabled: Option<bool>,
|
||||
pub rpc_enabled: Option<bool>,
|
||||
pub start_fullscreen: Option<bool>,
|
||||
pub music_vol: Option<u32>,
|
||||
pub sfx_vol: Option<u32>,
|
||||
pub legacy_mode: Option<bool>,
|
||||
|
|
|
|||
93
src/components/common/CinematicIntro.tsx
Normal file
93
src/components/common/CinematicIntro.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
interface CinematicIntroProps {
|
||||
onComplete: () => void;
|
||||
startMusic: () => void;
|
||||
}
|
||||
|
||||
type Phase = "black" | "white-lceteam" | "white-esrb" | "out";
|
||||
|
||||
const TIMINGS = {
|
||||
black: 800,
|
||||
"white-lceteam": 3000,
|
||||
"white-esrb": 3000,
|
||||
out: 800,
|
||||
};
|
||||
|
||||
export function CinematicIntro({ onComplete, startMusic }: CinematicIntroProps) {
|
||||
const [phase, setPhase] = useState<Phase>("black");
|
||||
|
||||
useEffect(() => {
|
||||
startMusic();
|
||||
|
||||
const run = async () => {
|
||||
await new Promise((r) => setTimeout(r, TIMINGS.black));
|
||||
setPhase("white-lceteam");
|
||||
|
||||
await new Promise((r) => setTimeout(r, TIMINGS["white-lceteam"]));
|
||||
setPhase("white-esrb");
|
||||
|
||||
await new Promise((r) => setTimeout(r, TIMINGS["white-esrb"]));
|
||||
setPhase("out");
|
||||
|
||||
await new Promise((r) => setTimeout(r, TIMINGS.out));
|
||||
onComplete();
|
||||
};
|
||||
|
||||
run();
|
||||
}, [onComplete, startMusic]);
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 z-50 bg-black">
|
||||
<AnimatePresence initial={false}>
|
||||
{phase === "black" && (
|
||||
<motion.div
|
||||
key="black"
|
||||
className="absolute inset-0 bg-black"
|
||||
initial={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(phase === "white-lceteam" || phase === "white-esrb" || phase === "out") && (
|
||||
<motion.div
|
||||
key="white-bg"
|
||||
className="absolute inset-0 bg-white flex items-center justify-center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: phase === "out" ? 0 : 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: phase === "out" ? 0.8 : 0.6 }}
|
||||
>
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
{phase === "white-lceteam" && (
|
||||
<motion.img
|
||||
key="lceteam"
|
||||
src="/images/LCE Team.png"
|
||||
className="max-w-xl object-contain"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(phase === "white-esrb" || phase === "out") && (
|
||||
<motion.img
|
||||
key="esrb"
|
||||
src="/images/esrb_warning.png"
|
||||
className="max-w-xl object-contain"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -28,6 +28,8 @@ const SettingsView = memo(function SettingsView() {
|
|||
setPerfBoost,
|
||||
rpcEnabled,
|
||||
setRpcEnabled,
|
||||
startFullscreen,
|
||||
setStartFullscreen,
|
||||
legacyMode,
|
||||
setLegacyMode,
|
||||
mangohudEnabled,
|
||||
|
|
@ -100,6 +102,11 @@ const SettingsView = memo(function SettingsView() {
|
|||
setRpcEnabled(!rpcEnabled);
|
||||
};
|
||||
|
||||
const handleFullscreenToggle = () => {
|
||||
playPressSound();
|
||||
setStartFullscreen(!startFullscreen);
|
||||
};
|
||||
|
||||
const handleLegacyToggle = () => {
|
||||
playPressSound();
|
||||
setLegacyMode(!legacyMode);
|
||||
|
|
@ -396,6 +403,12 @@ const SettingsView = memo(function SettingsView() {
|
|||
},
|
||||
});
|
||||
} else if (currentSubMenu === "launcher") {
|
||||
items.push({
|
||||
id: "fullscreen",
|
||||
label: `Start in Fullscreen: ${startFullscreen ? "ON" : "OFF"}`,
|
||||
type: "button",
|
||||
onClick: handleFullscreenToggle,
|
||||
});
|
||||
items.push({
|
||||
id: "rpc",
|
||||
label: `Discord RPC: ${rpcEnabled ? "ON" : "OFF"}`,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
|||
const audioRaw = useAudioController({
|
||||
musicVol: configRaw.musicVol,
|
||||
sfxVol: configRaw.sfxVol,
|
||||
showIntro,
|
||||
isGameRunning: gameRaw.isGameRunning,
|
||||
isWindowVisible,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export function useAppConfig() {
|
|||
const [vfxEnabled, setVfxEnabled] = useLocalStorage("lce-vfx", true);
|
||||
const [animationsEnabled, setAnimationsEnabled] = useLocalStorage("lce-animations", true);
|
||||
const [rpcEnabled, setRpcEnabled] = useLocalStorage("discord-rpc", true);
|
||||
const [startFullscreen, setStartFullscreen] = useLocalStorage("lce-fullscreen", true);
|
||||
const [musicVol, setMusicVol] = useLocalStorage("lce-music", 50);
|
||||
const [sfxVol, setSfxVol] = useLocalStorage("lce-sfx", 100);
|
||||
const [isDayTime, setIsDayTime] = useLocalStorage("lce-daytime", true);
|
||||
|
|
@ -36,6 +37,7 @@ export function useAppConfig() {
|
|||
if (config.vfxEnabled !== undefined) setVfxEnabled(config.vfxEnabled);
|
||||
if (config.animationsEnabled !== undefined) setAnimationsEnabled(config.animationsEnabled);
|
||||
if (config.rpcEnabled !== undefined) setRpcEnabled(config.rpcEnabled);
|
||||
if (config.startFullscreen !== undefined) setStartFullscreen(config.startFullscreen);
|
||||
if (config.musicVol !== undefined && config.musicVol !== null) setMusicVol(config.musicVol);
|
||||
if (config.sfxVol !== undefined && config.sfxVol !== null) setSfxVol(config.sfxVol);
|
||||
if (config.legacyMode !== undefined) setLegacyMode(config.legacyMode);
|
||||
|
|
@ -60,6 +62,7 @@ export function useAppConfig() {
|
|||
animationsEnabled,
|
||||
vfxEnabled,
|
||||
rpcEnabled,
|
||||
startFullscreen,
|
||||
musicVol,
|
||||
sfxVol,
|
||||
legacyMode,
|
||||
|
|
@ -69,7 +72,7 @@ export function useAppConfig() {
|
|||
launchEnvVars,
|
||||
}).catch(console.error);
|
||||
}
|
||||
}, [username, theme, linuxRunner, perfBoost, profile, customEditions, customizations, animationsEnabled, vfxEnabled, rpcEnabled, musicVol, sfxVol, legacyMode, mangohudEnabled, extraLaunchArgs, launchPrefix, launchEnvVars, isLoaded]);
|
||||
}, [username, theme, linuxRunner, perfBoost, profile, customEditions, customizations, animationsEnabled, vfxEnabled, rpcEnabled, startFullscreen, musicVol, sfxVol, legacyMode, mangohudEnabled, extraLaunchArgs, launchPrefix, launchEnvVars, isLoaded]);
|
||||
|
||||
return {
|
||||
username,
|
||||
|
|
@ -84,6 +87,8 @@ export function useAppConfig() {
|
|||
setAnimationsEnabled,
|
||||
rpcEnabled,
|
||||
setRpcEnabled,
|
||||
startFullscreen,
|
||||
setStartFullscreen,
|
||||
musicVol: musicVol ?? 50,
|
||||
setMusicVol,
|
||||
sfxVol: sfxVol ?? 100,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
|
||||
const TRACKS = [
|
||||
"music/06. Moog City 2.opus",
|
||||
"music/Blind Spots.ogg",
|
||||
"music/Key.ogg",
|
||||
"music/Living Mice.ogg",
|
||||
|
|
@ -173,7 +174,6 @@ const SPLASHES = [
|
|||
interface AudioControllerProps {
|
||||
musicVol: number;
|
||||
sfxVol: number;
|
||||
showIntro: boolean;
|
||||
isGameRunning: boolean;
|
||||
isWindowVisible: boolean;
|
||||
}
|
||||
|
|
@ -181,7 +181,6 @@ interface AudioControllerProps {
|
|||
export function useAudioController({
|
||||
musicVol,
|
||||
sfxVol,
|
||||
showIntro,
|
||||
isGameRunning,
|
||||
isWindowVisible,
|
||||
}: AudioControllerProps) {
|
||||
|
|
@ -404,10 +403,18 @@ export function useAudioController({
|
|||
setSplashIndex(newIndex);
|
||||
}, [playSplashSound, splashIndex]);
|
||||
|
||||
const [isMusicStarted, setIsMusicStarted] = useState(false);
|
||||
|
||||
const startMusic = useCallback(() => {
|
||||
setIsMusicStarted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (showIntro) return;
|
||||
if (!isMusicStarted) return;
|
||||
|
||||
const loadAndPlay = async () => {
|
||||
await ensureAudioContextReady();
|
||||
|
||||
let buffer = trackBuffersRef.current.get(currentTrack);
|
||||
if (!buffer) {
|
||||
buffer = await loadAudioBuffer(resolveAudioUrl(TRACKS[currentTrack]));
|
||||
|
|
@ -416,35 +423,16 @@ export function useAudioController({
|
|||
}
|
||||
}
|
||||
if (buffer) {
|
||||
await playMusicBuffer(buffer);
|
||||
}
|
||||
};
|
||||
|
||||
loadAndPlay();
|
||||
|
||||
return () => {
|
||||
stopMusic();
|
||||
};
|
||||
}, [showIntro, currentTrack, loadAudioBuffer, playMusicBuffer, stopMusic]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!audioContextRef.current || showIntro) return;
|
||||
|
||||
const loadAndPlay = async () => {
|
||||
let buffer = trackBuffersRef.current.get(currentTrack);
|
||||
if (!buffer) {
|
||||
buffer = await loadAudioBuffer(resolveAudioUrl(TRACKS[currentTrack]));
|
||||
if (buffer) {
|
||||
trackBuffersRef.current.set(currentTrack, buffer);
|
||||
if (currentTrack === 0) {
|
||||
await playMusicBuffer(buffer);
|
||||
} else {
|
||||
await fadeInMusic(buffer, targetVolumeRef.current, 500);
|
||||
}
|
||||
}
|
||||
if (buffer) {
|
||||
await fadeInMusic(buffer, musicVol / 100, 500);
|
||||
}
|
||||
};
|
||||
|
||||
loadAndPlay();
|
||||
}, [currentTrack, showIntro, musicVol, loadAudioBuffer, fadeInMusic]);
|
||||
}, [isMusicStarted, currentTrack, loadAudioBuffer, playMusicBuffer, fadeInMusic, ensureAudioContextReady]);
|
||||
|
||||
useEffect(() => {
|
||||
const shouldPause = isGameRunning || !isWindowVisible;
|
||||
|
|
@ -517,5 +505,6 @@ export function useAudioController({
|
|||
playSfx,
|
||||
tracks: TRACKS,
|
||||
splashes: SPLASHES,
|
||||
startMusic,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import SkinViewer from "../components/common/SkinViewer";
|
|||
import TeamModal from "../components/modals/TeamModal";
|
||||
import PanoramaBackground from "../components/common/PanoramaBackground";
|
||||
import { ClickParticles } from "../components/common/ClickParticles";
|
||||
import { CinematicIntro } from "../components/common/CinematicIntro";
|
||||
import { DownloadOverlay } from "../components/layout/DownloadOverlay";
|
||||
import { AchievementToast } from "../components/common/AchievementToast";
|
||||
import {
|
||||
|
|
@ -37,8 +38,6 @@ export default function App() {
|
|||
const {
|
||||
showIntro,
|
||||
setShowIntro,
|
||||
logoAnimDone,
|
||||
setLogoAnimDone,
|
||||
activeView,
|
||||
setActiveView,
|
||||
isUiHidden,
|
||||
|
|
@ -61,12 +60,22 @@ export default function App() {
|
|||
clearFriendRequestMessage,
|
||||
clearGameInviteMessage,
|
||||
} = useLceLiveNotifications();
|
||||
const [showSetup, setShowSetup] = useState(true);
|
||||
const [showSetup, setShowSetup] = useState(false);
|
||||
const [displayIsDay, setDisplayIsDay] = useState(config.isDayTime);
|
||||
const [isSetupChecked, setIsSetupChecked] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayIsDay(config.isDayTime);
|
||||
}, [config.isDayTime]);
|
||||
|
||||
useEffect(() => {
|
||||
if (config.isLoaded) {
|
||||
const setupCompleted = localStorage.getItem("lce-setup-completed") === "true";
|
||||
setShowSetup(!setupCompleted);
|
||||
setIsSetupChecked(true);
|
||||
}
|
||||
}, [config.isLoaded]);
|
||||
|
||||
const selectedEdition = game.editions.find(
|
||||
(e: Edition) => e.instanceId === config.profile,
|
||||
);
|
||||
|
|
@ -76,19 +85,6 @@ export default function App() {
|
|||
? selectedEdition?.titleImage || "/images/MenuTitle.png"
|
||||
: "/images/MenuTitle.png";
|
||||
|
||||
useEffect(() => {
|
||||
if (config.isLoaded) {
|
||||
const setupCompleted =
|
||||
localStorage.getItem("lce-setup-completed") === "true";
|
||||
setShowSetup(!setupCompleted);
|
||||
}
|
||||
}, [config.isLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => setShowIntro(false), 2400);
|
||||
setTimeout(() => setLogoAnimDone(true), 3400);
|
||||
}, [showSetup]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleContextMenu = (e: MouseEvent) => e.preventDefault();
|
||||
document.addEventListener("contextmenu", handleContextMenu);
|
||||
|
|
@ -109,6 +105,36 @@ export default function App() {
|
|||
transition: { duration: config.animationsEnabled ? 0.8 : 0 },
|
||||
};
|
||||
|
||||
if (!config.isLoaded || !isSetupChecked) {
|
||||
return <div className="w-screen h-screen bg-black" />;
|
||||
}
|
||||
|
||||
if (showSetup) {
|
||||
return (
|
||||
<div className={`w-screen h-screen overflow-hidden select-none flex flex-col relative bg-black text-white font-['Mojangles'] outline-none focus:outline-none ${!config.animationsEnabled ? "no-animations" : ""}`}>
|
||||
<SetupView
|
||||
onComplete={() => {
|
||||
setShowSetup(false);
|
||||
setShowIntro(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (showIntro) {
|
||||
return (
|
||||
<div className={`w-screen h-screen overflow-hidden select-none flex flex-col relative bg-black text-white font-['Mojangles'] outline-none focus:outline-none ${!config.animationsEnabled ? "no-animations" : ""}`}>
|
||||
<CinematicIntro
|
||||
onComplete={() => {
|
||||
setShowIntro(false);
|
||||
}}
|
||||
startMusic={audio.startMusic}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MotionConfig transition={config.animationsEnabled ? {} : { duration: 0 }}>
|
||||
<div
|
||||
|
|
@ -139,6 +165,7 @@ export default function App() {
|
|||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{config.vfxEnabled && <ClickParticles />}
|
||||
|
||||
<AnimatePresence>
|
||||
|
|
@ -197,39 +224,13 @@ export default function App() {
|
|||
/>
|
||||
|
||||
<AnimatePresence>
|
||||
{showSetup ? (
|
||||
<SetupView
|
||||
key="setup"
|
||||
onComplete={() => {
|
||||
setShowSetup(false);
|
||||
setShowIntro(true);
|
||||
}}
|
||||
/>
|
||||
) : showIntro ? (
|
||||
<motion.div
|
||||
key="intro"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="flex flex-1 items-center justify-center z-10 pointer-events-none"
|
||||
>
|
||||
<motion.img
|
||||
layoutId="mainLogo"
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
src={titleImage}
|
||||
className="w-3/4 max-w-3xl"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="dashboard"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="flex flex-col h-full z-10 w-full relative"
|
||||
>
|
||||
<AnimatePresence>
|
||||
{logoAnimDone && (
|
||||
<motion.div
|
||||
key="dashboard"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="flex flex-col h-full z-10 w-full relative"
|
||||
>
|
||||
<AnimatePresence>
|
||||
<>
|
||||
{!config.legacyMode && (
|
||||
<motion.div
|
||||
|
|
@ -321,7 +322,6 @@ export default function App() {
|
|||
</motion.div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="shrink-0 flex justify-center py-4 relative w-full pt-4">
|
||||
|
|
@ -338,7 +338,6 @@ export default function App() {
|
|||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{logoAnimDone && (
|
||||
<>
|
||||
<motion.div
|
||||
key="splash"
|
||||
|
|
@ -367,14 +366,13 @@ export default function App() {
|
|||
</motion.div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="flex-1 w-full relative">
|
||||
<div
|
||||
className={`w-full h-full flex flex-col items-center justify-center ${!logoAnimDone || isUiHidden ? "opacity-0 pointer-events-none" : "opacity-100"}`}
|
||||
className={`w-full h-full flex flex-col items-center justify-center ${isUiHidden ? "opacity-0 pointer-events-none" : "opacity-100"}`}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
{activeView === "main" && (
|
||||
|
|
@ -442,7 +440,6 @@ export default function App() {
|
|||
</main>
|
||||
|
||||
<AnimatePresence>
|
||||
{logoAnimDone && (
|
||||
<motion.footer
|
||||
key="footer"
|
||||
{...uiFade}
|
||||
|
|
@ -460,10 +457,8 @@ export default function App() {
|
|||
{useUI().connected && "CONTROLLER CONNECTED"}
|
||||
</div>
|
||||
</motion.footer>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AchievementToast
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export interface AppConfig {
|
|||
animationsEnabled?: boolean;
|
||||
vfxEnabled?: boolean;
|
||||
rpcEnabled?: boolean;
|
||||
startFullscreen?: boolean;
|
||||
musicVol?: number;
|
||||
sfxVol?: number;
|
||||
legacyMode?: boolean;
|
||||
|
|
|
|||
Loading…
Reference in a new issue