Compare commits
3 Commits
e212e4bbd5
...
51569af7b8
| Author | SHA1 | Date | |
|---|---|---|---|
| 51569af7b8 | |||
| d26c676edf | |||
| d3b4a55e71 |
@@ -84,11 +84,11 @@ This matters for `lafabrik`: adding `public/models/lafabrik-LOD/` is not enough
|
|||||||
To add LOD support for a model:
|
To add LOD support for a model:
|
||||||
|
|
||||||
1. Add the light model in `public/models/<name>-LOD/model.gltf`.
|
1. Add the light model in `public/models/<name>-LOD/model.gltf`.
|
||||||
2. Keep the regular model in `public/models/<name>/model.gltf`.
|
2. Keep the regular model in `public/models/<name>/model.glb` or `public/models/<name>/model.gltf`.
|
||||||
3. Add the mapping in `src/data/world/mapLodConfig.ts`.
|
3. Add the mapping in `src/data/world/mapLodConfig.ts`.
|
||||||
4. If the model uses a dedicated component, call `useMapLodModelPath()` in that component.
|
4. If the model uses a dedicated component, call `useMapLodModelPath()` in that component.
|
||||||
5. Preload both paths when the component is dedicated and uses `useGLTF.preload()`.
|
5. Preload both paths when the component is dedicated and uses `useGLTF.preload()`.
|
||||||
6. Verify the GLTF references: buffers, textures, opacity maps, and relative paths.
|
6. Verify the GLTF/GLB references: buffers, textures, opacity maps, and relative paths.
|
||||||
|
|
||||||
## Current LOD Models
|
## Current LOD Models
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ This document tracks the current map-rendering performance pass.
|
|||||||
|
|
||||||
The first performance bottleneck was draw calls. Some assets were exported as many small GLTF primitives even when they used only a few materials.
|
The first performance bottleneck was draw calls. Some assets were exported as many small GLTF primitives even when they used only a few materials.
|
||||||
|
|
||||||
| Model | Instances | Meshes / primitives | Notes |
|
| Model | Instances | Meshes / primitives | Notes |
|
||||||
| ---------------- | --------: | ------------------: | ---------------------------------------------------------------- |
|
| ---------------- | --------: | ------------------: | ------------------------------------------------------------------------------------ |
|
||||||
| `generateur` | 3 | 3152 | Worst draw-call offender. Needs asset-side mesh merging. |
|
| `generateur` | 3 | 3152 | Worst draw-call offender. Needs asset-side mesh merging. |
|
||||||
| `lafabrik` | 4 | 56 | Moderate draw calls, heavy 2048 texture set. |
|
| `lafabrik` | 4 | 474 | High primitive count; current HD GLB has embedded geometry and no external textures. |
|
||||||
| `ecole` | 1 | 107 | One material but many primitives; should be merged. |
|
| `ecole` | 1 | 107 | One material but many primitives; should be merged. |
|
||||||
| `fermeverticale` | 3 | 1 | Geometry is fine; textures are large for the visible complexity. |
|
| `fermeverticale` | 3 | 1 | Geometry is fine; textures are large for the visible complexity. |
|
||||||
|
|
||||||
`generateur` was especially expensive because three visible instances could multiply thousands of primitives into thousands of draw calls. Instancing reduces repeated instance cost, but the source asset still needs a cleaner export.
|
`generateur` was especially expensive because three visible instances could multiply thousands of primitives into thousands of draw calls. Instancing reduces repeated instance cost, but the source asset still needs a cleaner export.
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ Estimated source primitive count versus runtime merged groups:
|
|||||||
| `generateur` | 3152 | 8 |
|
| `generateur` | 3152 | 8 |
|
||||||
| `ecole` | 107 | 2 |
|
| `ecole` | 107 | 2 |
|
||||||
| `eolienne` | 118 | 8 |
|
| `eolienne` | 118 | 8 |
|
||||||
| `lafabrik` | 56 | 14 |
|
| `lafabrik` | 474 | ~77 |
|
||||||
|
|
||||||
This is a code-side safety net, not a replacement for clean asset exports. Clean GLB exports with merged meshes and fewer textures remain the preferred long-term path.
|
This is a code-side safety net, not a replacement for clean asset exports. Clean GLB exports with merged meshes and fewer textures remain the preferred long-term path.
|
||||||
|
|
||||||
@@ -255,7 +255,7 @@ Design/export should prioritize:
|
|||||||
1. Produce lower-poly `buisson`, `arbre`, `sapin`, and crop assets.
|
1. Produce lower-poly `buisson`, `arbre`, `sapin`, and crop assets.
|
||||||
2. Add LOD or billboard variants for far vegetation.
|
2. Add LOD or billboard variants for far vegetation.
|
||||||
3. Merge `generateur` meshes from 3152 primitives to a small number of material groups.
|
3. Merge `generateur` meshes from 3152 primitives to a small number of material groups.
|
||||||
4. Reduce `lafabrik` texture count and downscale flat/low-detail maps.
|
4. Keep `lafabrik` exports texture-light, and merge repeated material primitives where possible.
|
||||||
5. Merge `ecole` primitives because it uses a single material.
|
5. Merge `ecole` primitives because it uses a single material.
|
||||||
6. Prefer runtime `.glb` or compressed runtime textures when the pipeline supports it.
|
6. Prefer runtime `.glb` or compressed runtime textures when the pipeline supports it.
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ The loading progress in `HomePage` is monotonic:
|
|||||||
|
|
||||||
This prevents the overlay from jumping backward when nested loaders finish in a slightly different order.
|
This prevents the overlay from jumping backward when nested loaders finish in a slightly different order.
|
||||||
|
|
||||||
|
After the initial map boot is complete, late loading signals no longer reopen the full-screen loading overlay. Instead, `HomePage` shows the compact `AppLoadingIndicator` while the game remains visible. This is reserved for explicit runtime reload signals such as graphics preset changes, repair-state transitions, or late world loading events; chunk streaming intentionally does not drive this indicator.
|
||||||
|
|
||||||
## World Composition
|
## World Composition
|
||||||
|
|
||||||
`src/world/World.tsx` is the main scene composer.
|
`src/world/World.tsx` is the main scene composer.
|
||||||
|
|||||||
Binary file not shown.
@@ -6,7 +6,7 @@ import {
|
|||||||
import { getMapLodModelPath } from "@/data/world/mapLodConfig";
|
import { getMapLodModelPath } from "@/data/world/mapLodConfig";
|
||||||
import { useMapLodModelPath } from "@/hooks/world/useMapLodModelPath";
|
import { useMapLodModelPath } from "@/hooks/world/useMapLodModelPath";
|
||||||
|
|
||||||
const LA_FABRIK_MODEL_PATH = "/models/lafabrik/model.gltf";
|
const LA_FABRIK_MODEL_PATH = "/models/lafabrik/model.glb";
|
||||||
const LA_FABRIK_LOD_MODEL_PATH = getMapLodModelPath("lafabrik");
|
const LA_FABRIK_LOD_MODEL_PATH = getMapLodModelPath("lafabrik");
|
||||||
|
|
||||||
type LaFabrikMapModelProps = Omit<MergedStaticMapModelProps, "modelPath">;
|
type LaFabrikMapModelProps = Omit<MergedStaticMapModelProps, "modelPath">;
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
interface AppLoadingIndicatorProps {
|
||||||
|
className?: string | undefined;
|
||||||
|
floating?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppLoadingIndicator({
|
||||||
|
className,
|
||||||
|
floating = false,
|
||||||
|
}: AppLoadingIndicatorProps): React.JSX.Element {
|
||||||
|
const classes = [
|
||||||
|
"app-loading-indicator",
|
||||||
|
floating ? "app-loading-indicator--floating" : null,
|
||||||
|
className,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes} role="status" aria-live="polite">
|
||||||
|
<span>Loading...</span>
|
||||||
|
<svg
|
||||||
|
className="app-loading-indicator__spinner"
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M16 3a13 13 0 1 1-9.2 3.8"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeWidth="3.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M6.8 6.8V2.8H2.8"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="3.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -285,35 +285,36 @@ export function GameSettingsMenu(): React.JSX.Element | null {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
<div className="game-settings-menu__subsection">
|
||||||
className="game-settings-menu__section game-settings-menu__section--right"
|
<div className="game-settings-menu__section-title">
|
||||||
aria-labelledby="hand-tracking-settings-heading"
|
<Hand size={16} aria-hidden="true" />
|
||||||
>
|
<h3 id="hand-tracking-settings-heading">Détection des mains</h3>
|
||||||
<div className="game-settings-menu__section-title">
|
</div>
|
||||||
<Hand size={16} aria-hidden="true" />
|
<div
|
||||||
<h3 id="hand-tracking-settings-heading">Détection des mains</h3>
|
className="game-settings-menu__choice-group game-settings-menu__choice-group--hand-tracking"
|
||||||
</div>
|
aria-labelledby="hand-tracking-settings-heading"
|
||||||
<div
|
>
|
||||||
className="game-settings-menu__choice-group game-settings-menu__choice-group--hand-tracking"
|
{HAND_TRACKING_OPTIONS.map((option) => (
|
||||||
aria-label="Mode de détection des mains"
|
<button
|
||||||
>
|
key={option.source}
|
||||||
{HAND_TRACKING_OPTIONS.map((option) => (
|
type="button"
|
||||||
<button
|
className={
|
||||||
key={option.source}
|
handTrackingSource === option.source
|
||||||
type="button"
|
? "active"
|
||||||
className={
|
: undefined
|
||||||
handTrackingSource === option.source ? "active" : undefined
|
}
|
||||||
}
|
onClick={() =>
|
||||||
onClick={() => handleHandTrackingSourceChange(option.source)}
|
handleHandTrackingSourceChange(option.source)
|
||||||
aria-pressed={handTrackingSource === option.source}
|
}
|
||||||
>
|
aria-pressed={handTrackingSource === option.source}
|
||||||
{option.icon}
|
>
|
||||||
<span>{option.label}</span>
|
{option.icon}
|
||||||
<small>{option.description}</small>
|
<span>{option.label}</span>
|
||||||
</button>
|
<small>{option.description}</small>
|
||||||
))}
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AppLoadingIndicator } from "@/components/ui/AppLoadingIndicator";
|
||||||
import type { SceneLoadingState } from "@/types/world/sceneLoading";
|
import type { SceneLoadingState } from "@/types/world/sceneLoading";
|
||||||
|
|
||||||
const LOADING_BACKGROUND_PATH = "/assets/bg-site.png";
|
const LOADING_BACKGROUND_PATH = "/assets/bg-site.png";
|
||||||
@@ -36,30 +37,7 @@ export function SceneLoadingOverlay({
|
|||||||
/>
|
/>
|
||||||
<div className="scene-loading-overlay__footer">
|
<div className="scene-loading-overlay__footer">
|
||||||
<div className="scene-loading-overlay__meta">
|
<div className="scene-loading-overlay__meta">
|
||||||
<div className="scene-loading-overlay__label">
|
<AppLoadingIndicator className="scene-loading-overlay__label" />
|
||||||
<span>Loading...</span>
|
|
||||||
<svg
|
|
||||||
className="scene-loading-overlay__spinner"
|
|
||||||
viewBox="0 0 32 32"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M16 3a13 13 0 1 1-9.2 3.8"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeWidth="3.5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M6.8 6.8V2.8H2.8"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="3.5"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<strong>{progress}%</strong>
|
<strong>{progress}%</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="scene-loading-overlay__track">
|
<div className="scene-loading-overlay__track">
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export const galleryModels: GalleryModel[] = [
|
|||||||
path: "/models/habitant2-animated/model.gltf",
|
path: "/models/habitant2-animated/model.gltf",
|
||||||
},
|
},
|
||||||
{ id: "immeuble1", name: "Immeuble", path: "/models/immeuble1/model.gltf" },
|
{ id: "immeuble1", name: "Immeuble", path: "/models/immeuble1/model.gltf" },
|
||||||
{ id: "lafabrik", name: "La Fabrik", path: "/models/lafabrik/model.gltf" },
|
{ id: "lafabrik", name: "La Fabrik", path: "/models/lafabrik/model.glb" },
|
||||||
{ id: "maison1", name: "Maison", path: "/models/maison1/model.gltf" },
|
{ id: "maison1", name: "Maison", path: "/models/maison1/model.gltf" },
|
||||||
{
|
{
|
||||||
id: "packderelance",
|
id: "packderelance",
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
const DEFAULT_LOADING_DURATION_MS = 900;
|
||||||
|
|
||||||
|
export function useTransientLoadingIndicator(): {
|
||||||
|
showLoading: (durationMs?: number) => void;
|
||||||
|
visible: boolean;
|
||||||
|
} {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const timeoutRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
const showLoading = useCallback(
|
||||||
|
(durationMs = DEFAULT_LOADING_DURATION_MS) => {
|
||||||
|
if (timeoutRef.current !== null) {
|
||||||
|
window.clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
setVisible(true);
|
||||||
|
timeoutRef.current = window.setTimeout(() => {
|
||||||
|
setVisible(false);
|
||||||
|
timeoutRef.current = null;
|
||||||
|
}, durationMs);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (timeoutRef.current !== null) {
|
||||||
|
window.clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { showLoading, visible };
|
||||||
|
}
|
||||||
+49
-25
@@ -869,6 +869,40 @@ canvas {
|
|||||||
box-shadow: 0 0 14px rgba(56, 189, 248, 0.86);
|
box-shadow: 0 0 14px rgba(56, 189, 248, 0.86);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-loading-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: clamp(8px, 1.2vw, 14px);
|
||||||
|
min-width: 0;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
line-height: 1;
|
||||||
|
text-transform: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-loading-indicator--floating {
|
||||||
|
position: fixed;
|
||||||
|
bottom: clamp(22px, 5vh, 48px);
|
||||||
|
left: clamp(18px, 4vw, 56px);
|
||||||
|
z-index: 45;
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: "Nersans One", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
font-size: clamp(16px, 2.3vw, 30px);
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
pointer-events: none;
|
||||||
|
text-shadow: 0 2px 14px rgba(0, 0, 0, 0.45);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-loading-indicator__spinner {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: clamp(18px, 2.2vw, 30px);
|
||||||
|
height: clamp(18px, 2.2vw, 30px);
|
||||||
|
color: currentColor;
|
||||||
|
animation: app-loading-spin 900ms linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.scene-loading-overlay {
|
.scene-loading-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -947,14 +981,6 @@ canvas {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-loading-overlay__spinner {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: clamp(18px, 2.2vw, 30px);
|
|
||||||
height: clamp(18px, 2.2vw, 30px);
|
|
||||||
color: #ffffff;
|
|
||||||
animation: scene-loading-spin 900ms linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scene-loading-overlay__meta strong {
|
.scene-loading-overlay__meta strong {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
@@ -976,7 +1002,7 @@ canvas {
|
|||||||
transition: width 180ms ease;
|
transition: width 180ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes scene-loading-spin {
|
@keyframes app-loading-spin {
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
@@ -1219,12 +1245,10 @@ canvas {
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: clamp(18px, 4vw, 42px);
|
padding: clamp(18px, 4vw, 42px);
|
||||||
background:
|
background: rgba(2, 8, 14, 0.28);
|
||||||
linear-gradient(180deg, rgba(4, 10, 18, 0.72), rgba(2, 6, 12, 0.9)),
|
|
||||||
rgba(0, 0, 0, 0.82);
|
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(8px) saturate(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-settings-menu__panel {
|
.game-settings-menu__panel {
|
||||||
@@ -1236,12 +1260,13 @@ canvas {
|
|||||||
border: 1px solid rgba(125, 211, 252, 0.38);
|
border: 1px solid rgba(125, 211, 252, 0.38);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(8, 20, 34, 0.98), rgba(3, 8, 14, 0.99)),
|
linear-gradient(180deg, rgba(8, 20, 34, 0.84), rgba(3, 8, 14, 0.78)),
|
||||||
rgba(4, 8, 12, 0.99);
|
rgba(4, 8, 12, 0.74);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 28px 90px rgba(0, 0, 0, 0.58),
|
0 28px 90px rgba(0, 0, 0, 0.58),
|
||||||
inset 0 0 0 1px rgba(255, 255, 255, 0.05),
|
inset 0 0 0 1px rgba(255, 255, 255, 0.05),
|
||||||
0 0 34px rgba(56, 189, 248, 0.16);
|
0 0 34px rgba(56, 189, 248, 0.16);
|
||||||
|
backdrop-filter: blur(18px) saturate(1.12);
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: rgba(125, 211, 252, 0.55) transparent;
|
scrollbar-color: rgba(125, 211, 252, 0.55) transparent;
|
||||||
}
|
}
|
||||||
@@ -1327,17 +1352,13 @@ canvas {
|
|||||||
padding: 14px;
|
padding: 14px;
|
||||||
border: 1px solid rgba(125, 211, 252, 0.2);
|
border: 1px solid rgba(125, 211, 252, 0.2);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: rgba(5, 14, 24, 0.78);
|
background: rgba(5, 14, 24, 0.58);
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-settings-menu__section--wide {
|
.game-settings-menu__section--wide {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-settings-menu__section--right {
|
|
||||||
grid-column: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-settings-menu__section-title {
|
.game-settings-menu__section-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1345,6 +1366,13 @@ canvas {
|
|||||||
color: #7dd3fc;
|
color: #7dd3fc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__subsection {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid rgba(125, 211, 252, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
.game-settings-menu__section h3 {
|
.game-settings-menu__section h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #dff7ff;
|
color: #dff7ff;
|
||||||
@@ -1422,7 +1450,7 @@ canvas {
|
|||||||
padding: 11px 12px;
|
padding: 11px 12px;
|
||||||
border: 1px solid rgba(125, 211, 252, 0.2);
|
border: 1px solid rgba(125, 211, 252, 0.2);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: rgba(6, 18, 30, 0.84);
|
background: rgba(6, 18, 30, 0.7);
|
||||||
color: #edfaff;
|
color: #edfaff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
@@ -1517,10 +1545,6 @@ canvas {
|
|||||||
.game-settings-menu__choice-group--presets {
|
.game-settings-menu__choice-group--presets {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-settings-menu__section--right {
|
|
||||||
grid-column: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Debug overlay panels */
|
/* Debug overlay panels */
|
||||||
|
|||||||
+44
-4
@@ -1,9 +1,10 @@
|
|||||||
import { Suspense, useCallback, useEffect, useState } from "react";
|
import { Suspense, useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { Canvas } from "@react-three/fiber";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { DebugPerf } from "@/components/debug/DebugPerf";
|
import { DebugPerf } from "@/components/debug/DebugPerf";
|
||||||
import { EbikeIntroSequence } from "@/components/game/EbikeIntroSequence";
|
import { EbikeIntroSequence } from "@/components/game/EbikeIntroSequence";
|
||||||
|
import { AppLoadingIndicator } from "@/components/ui/AppLoadingIndicator";
|
||||||
import { DialogMessage } from "@/components/ui/DialogMessage";
|
import { DialogMessage } from "@/components/ui/DialogMessage";
|
||||||
import { GameUI } from "@/components/ui/GameUI";
|
import { GameUI } from "@/components/ui/GameUI";
|
||||||
import {
|
import {
|
||||||
@@ -14,8 +15,10 @@ import {
|
|||||||
} from "@/components/ui/intro";
|
} from "@/components/ui/intro";
|
||||||
import { SceneLoadingOverlay } from "@/components/ui/SceneLoadingOverlay";
|
import { SceneLoadingOverlay } from "@/components/ui/SceneLoadingOverlay";
|
||||||
import { INITIAL_SCENE_LOADING_STATE } from "@/data/world/sceneLoadingConfig";
|
import { INITIAL_SCENE_LOADING_STATE } from "@/data/world/sceneLoadingConfig";
|
||||||
|
import { useTransientLoadingIndicator } from "@/hooks/ui/useTransientLoadingIndicator";
|
||||||
import { AudioManager } from "@/managers/AudioManager";
|
import { AudioManager } from "@/managers/AudioManager";
|
||||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||||
|
import { useWorldSettingsStore } from "@/managers/stores/useWorldSettingsStore";
|
||||||
import { HandTrackingProvider } from "@/providers/gameplay/HandTrackingProvider";
|
import { HandTrackingProvider } from "@/providers/gameplay/HandTrackingProvider";
|
||||||
import type { SceneLoadingState } from "@/types/world/sceneLoading";
|
import type { SceneLoadingState } from "@/types/world/sceneLoading";
|
||||||
import { hasSiteBeenVisitedToday } from "@/utils/cookies/siteVisitCookie";
|
import { hasSiteBeenVisitedToday } from "@/utils/cookies/siteVisitCookie";
|
||||||
@@ -26,15 +29,31 @@ const LOADING_TO_VIDEO_FADE_MS = 500;
|
|||||||
|
|
||||||
export function HomePage(): React.JSX.Element | null {
|
export function HomePage(): React.JSX.Element | null {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const mainState = useGameStore((state) => state.mainState);
|
||||||
const introStep = useGameStore((state) => state.intro.currentStep);
|
const introStep = useGameStore((state) => state.intro.currentStep);
|
||||||
|
const ebikeStep = useGameStore((state) => state.ebike.currentStep);
|
||||||
|
const pylonStep = useGameStore((state) => state.pylon.currentStep);
|
||||||
|
const farmStep = useGameStore((state) => state.farm.currentStep);
|
||||||
const setIntroStep = useGameStore((state) => state.setIntroStep);
|
const setIntroStep = useGameStore((state) => state.setIntroStep);
|
||||||
|
const graphicsPreset = useWorldSettingsStore(
|
||||||
|
(state) => state.graphics.preset,
|
||||||
|
);
|
||||||
const dialogMessage = useGameStore(
|
const dialogMessage = useGameStore(
|
||||||
(state) => state.missionFlow.dialogMessage,
|
(state) => state.missionFlow.dialogMessage,
|
||||||
);
|
);
|
||||||
const hideDialog = useGameStore((state) => state.hideDialog);
|
const hideDialog = useGameStore((state) => state.hideDialog);
|
||||||
|
const { showLoading, visible: showTransientLoading } =
|
||||||
|
useTransientLoadingIndicator();
|
||||||
const [sceneLoadingState, setSceneLoadingState] = useState<SceneLoadingState>(
|
const [sceneLoadingState, setSceneLoadingState] = useState<SceneLoadingState>(
|
||||||
INITIAL_SCENE_LOADING_STATE,
|
INITIAL_SCENE_LOADING_STATE,
|
||||||
);
|
);
|
||||||
|
const sceneReadyRef = useRef(false);
|
||||||
|
const runtimeLoadingSignal = `${graphicsPreset}:${mainState}:${ebikeStep}:${pylonStep}:${farmStep}`;
|
||||||
|
const previousRuntimeLoadingSignalRef = useRef(runtimeLoadingSignal);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sceneReadyRef.current = sceneLoadingState.status === "ready";
|
||||||
|
}, [sceneLoadingState.status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasSiteBeenVisitedToday()) {
|
if (!hasSiteBeenVisitedToday()) {
|
||||||
@@ -56,6 +75,11 @@ export function HomePage(): React.JSX.Element | null {
|
|||||||
|
|
||||||
const handleSceneLoadingStateChange = useCallback(
|
const handleSceneLoadingStateChange = useCallback(
|
||||||
(nextState: SceneLoadingState) => {
|
(nextState: SceneLoadingState) => {
|
||||||
|
if (sceneReadyRef.current && nextState.status === "loading") {
|
||||||
|
showLoading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setSceneLoadingState((currentState) => {
|
setSceneLoadingState((currentState) => {
|
||||||
if (currentState.status === "ready" && nextState.status === "loading") {
|
if (currentState.status === "ready" && nextState.status === "loading") {
|
||||||
return currentState;
|
return currentState;
|
||||||
@@ -67,9 +91,20 @@ export function HomePage(): React.JSX.Element | null {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[],
|
[showLoading],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (previousRuntimeLoadingSignalRef.current === runtimeLoadingSignal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousRuntimeLoadingSignalRef.current = runtimeLoadingSignal;
|
||||||
|
if (sceneLoadingState.status !== "ready") return;
|
||||||
|
|
||||||
|
showLoading();
|
||||||
|
}, [runtimeLoadingSignal, sceneLoadingState.status, showLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (introStep === "loading-map" && sceneLoadingState.status === "ready") {
|
if (introStep === "loading-map" && sceneLoadingState.status === "ready") {
|
||||||
AudioManager.getInstance().stopMusic();
|
AudioManager.getInstance().stopMusic();
|
||||||
@@ -132,6 +167,8 @@ export function HomePage(): React.JSX.Element | null {
|
|||||||
const showFadeToVideoOverlay =
|
const showFadeToVideoOverlay =
|
||||||
introStep === "fade-to-video" ||
|
introStep === "fade-to-video" ||
|
||||||
(introStep === "loading-map" && sceneLoadingState.status === "ready");
|
(introStep === "loading-map" && sceneLoadingState.status === "ready");
|
||||||
|
const showSceneLoadingOverlay =
|
||||||
|
introStep === "loading-map" || introStep === "fade-to-video";
|
||||||
|
|
||||||
const renderIntroOverlay = () => {
|
const renderIntroOverlay = () => {
|
||||||
if (showFadeToVideoOverlay) return <FadeToVideoOverlay />;
|
if (showFadeToVideoOverlay) return <FadeToVideoOverlay />;
|
||||||
@@ -173,9 +210,12 @@ export function HomePage(): React.JSX.Element | null {
|
|||||||
onClose={hideDialog}
|
onClose={hideDialog}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{(introStep === "loading-map" || introStep === "fade-to-video") && (
|
{showSceneLoadingOverlay ? (
|
||||||
<SceneLoadingOverlay state={sceneLoadingState} />
|
<SceneLoadingOverlay state={sceneLoadingState} />
|
||||||
)}
|
) : null}
|
||||||
|
{showTransientLoading && !showSceneLoadingOverlay ? (
|
||||||
|
<AppLoadingIndicator floating />
|
||||||
|
) : null}
|
||||||
{renderIntroOverlay()}
|
{renderIntroOverlay()}
|
||||||
<EbikeIntroSequence />
|
<EbikeIntroSequence />
|
||||||
</HandTrackingProvider>
|
</HandTrackingProvider>
|
||||||
|
|||||||
Reference in New Issue
Block a user