feat: add new lce team cinematic intro

This commit is contained in:
KayJann 2026-06-02 01:41:48 +02:00
parent b76e0c305d
commit bd632e7e0b
13 changed files with 190 additions and 85 deletions

BIN
public/images/LCE Team.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

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

View file

@ -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),

View file

@ -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();

View file

@ -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>,

View 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>
);
}

View file

@ -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"}`,

View file

@ -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,
});

View file

@ -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,

View file

@ -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,
};
}

View file

@ -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

View file

@ -36,6 +36,7 @@ export interface AppConfig {
animationsEnabled?: boolean;
vfxEnabled?: boolean;
rpcEnabled?: boolean;
startFullscreen?: boolean;
musicVol?: number;
sfxVol?: number;
legacyMode?: boolean;