diff --git a/public/images/LCE Team.png b/public/images/LCE Team.png new file mode 100644 index 0000000..bcbd463 Binary files /dev/null and b/public/images/LCE Team.png differ diff --git a/public/images/esrb_warning.png b/public/images/esrb_warning.png new file mode 100644 index 0000000..b372a51 Binary files /dev/null and b/public/images/esrb_warning.png differ diff --git a/public/images/minecraft_title_neoLegacy.png b/public/images/minecraft_title_neoLegacy.png index 618c261..3d2cc52 100644 Binary files a/public/images/minecraft_title_neoLegacy.png and b/public/images/minecraft_title_neoLegacy.png differ diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index fb16c7c..b5295d6 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -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), diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index af0247e..c7592bb 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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 = std::env::args().collect(); if args.len() > 1 && !args[1].starts_with('-') { let instance_id = args[1].clone(); diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index 44db3b7..ba3ea98 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -40,6 +40,7 @@ pub struct AppConfig { pub animations_enabled: Option, pub vfx_enabled: Option, pub rpc_enabled: Option, + pub start_fullscreen: Option, pub music_vol: Option, pub sfx_vol: Option, pub legacy_mode: Option, diff --git a/src/components/common/CinematicIntro.tsx b/src/components/common/CinematicIntro.tsx new file mode 100644 index 0000000..f434250 --- /dev/null +++ b/src/components/common/CinematicIntro.tsx @@ -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("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 ( +
+ + {phase === "black" && ( + + )} + + {(phase === "white-lceteam" || phase === "white-esrb" || phase === "out") && ( + + + {phase === "white-lceteam" && ( + + )} + + {(phase === "white-esrb" || phase === "out") && ( + + )} + + + )} + +
+ ); +} diff --git a/src/components/views/SettingsView.tsx b/src/components/views/SettingsView.tsx index b8f0837..018d125 100644 --- a/src/components/views/SettingsView.tsx +++ b/src/components/views/SettingsView.tsx @@ -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"}`, diff --git a/src/context/LauncherContext.tsx b/src/context/LauncherContext.tsx index 31560d9..6438304 100644 --- a/src/context/LauncherContext.tsx +++ b/src/context/LauncherContext.tsx @@ -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, }); diff --git a/src/hooks/useAppConfig.ts b/src/hooks/useAppConfig.ts index 2e1519d..a78f1b0 100644 --- a/src/hooks/useAppConfig.ts +++ b/src/hooks/useAppConfig.ts @@ -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, diff --git a/src/hooks/useAudioController.ts b/src/hooks/useAudioController.ts index bf6533b..946aa21 100644 --- a/src/hooks/useAudioController.ts +++ b/src/hooks/useAudioController.ts @@ -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, }; } diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 8f94ca9..7e33dfb 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -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
; + } + + if (showSetup) { + return ( +
+ { + setShowSetup(false); + setShowIntro(true); + }} + /> +
+ ); + } + + if (showIntro) { + return ( +
+ { + setShowIntro(false); + }} + startMusic={audio.startMusic} + /> +
+ ); + } + return (
+ {config.vfxEnabled && } @@ -197,39 +224,13 @@ export default function App() { /> - {showSetup ? ( - { - setShowSetup(false); - setShowIntro(true); - }} - /> - ) : showIntro ? ( - - - - ) : ( - - - {logoAnimDone && ( + + <> {!config.legacyMode && ( )} - )}
@@ -338,7 +338,6 @@ export default function App() { style={{ imageRendering: "pixelated" }} /> - {logoAnimDone && ( <> )} - )}
{activeView === "main" && ( @@ -442,7 +440,6 @@ export default function App() {
- {logoAnimDone && ( - )} - )}