add flag and waypoint markers

This commit is contained in:
Brian Beck 2025-11-25 23:15:00 -08:00
parent b50ce94636
commit 608b538366
6 changed files with 114 additions and 17 deletions

View file

@ -51,6 +51,8 @@ main {
color: #fff;
font-size: 11px;
white-space: nowrap;
padding: 1px 3px;
border-radius: 1px;
}
.StatsPanel {

View file

@ -10,37 +10,49 @@ export const FloatingLabel = memo(function FloatingLabel({
children,
color = "white",
position = DEFAULT_POSITION,
opacity = "fadeWithDistance",
}: {
children: ReactNode;
color?: string;
position?: [x: number, y: number, z: number];
opacity?: number | "fadeWithDistance";
}) {
const fadeWithDistance = opacity === "fadeWithDistance";
const groupRef = useRef<Object3D>(null);
const distanceRef = useDistanceFromCamera(groupRef);
const [isVisible, setIsVisible] = useState(false);
const [isVisible, setIsVisible] = useState(opacity !== 0);
const labelRef = useRef<HTMLDivElement>(null);
// Initialize opacity when label ref is attached
useEffect(() => {
if (labelRef.current && distanceRef.current != null) {
const opacity = Math.max(0, Math.min(1, 1 - distanceRef.current / 200));
labelRef.current.style.opacity = opacity.toString();
if (fadeWithDistance) {
if (labelRef.current && distanceRef.current != null) {
const opacity = Math.max(0, Math.min(1, 1 - distanceRef.current / 200));
labelRef.current.style.opacity = opacity.toString();
}
}
}, [isVisible]);
}, [isVisible, fadeWithDistance]);
useFrame(() => {
const distance = distanceRef.current;
const shouldBeVisible = distance != null && distance < 200;
if (fadeWithDistance) {
const distance = distanceRef.current;
const shouldBeVisible = distance != null && distance < 200;
// Update visibility state only when crossing threshold
if (isVisible !== shouldBeVisible) {
setIsVisible(shouldBeVisible);
}
// Update visibility state only when crossing threshold
if (isVisible !== shouldBeVisible) {
setIsVisible(shouldBeVisible);
}
// Update opacity directly on DOM element (no re-render)
if (labelRef.current && shouldBeVisible) {
const opacity = Math.max(0, Math.min(1, 1 - distance / 200));
labelRef.current.style.opacity = opacity.toString();
// Update opacity directly on DOM element (no re-render)
if (labelRef.current && shouldBeVisible) {
const opacity = Math.max(0, Math.min(1, 1 - distance / 200));
labelRef.current.style.opacity = opacity.toString();
}
} else {
setIsVisible(opacity !== 0);
if (labelRef.current) {
labelRef.current.style.opacity = opacity.toString();
}
}
});

View file

@ -9,6 +9,8 @@ import {
} from "../mission";
import { ShapeModel, ShapePlaceholder } from "./GenericShape";
import { ShapeInfoProvider } from "./ShapeInfoProvider";
import { useSimGroup } from "./SimGroup";
import { FloatingLabel } from "./FloatingLabel";
const dataBlockToShapeName = {
AmmoPack: "pack_upgrade_ammo.dts",
@ -54,7 +56,13 @@ function getDataBlockShape(dataBlock: string) {
return _caseInsensitiveLookup[dataBlock.toLowerCase()];
}
const TEAM_NAMES = {
1: "Storm",
2: "Inferno",
};
export function Item({ object }: { object: ConsoleObject }) {
const simGroup = useSimGroup();
const dataBlock = getProperty(object, "dataBlock").value;
const position = useMemo(() => getPosition(object), [object]);
@ -67,6 +75,11 @@ export function Item({ object }: { object: ConsoleObject }) {
console.error(`<Item> missing shape for dataBlock: ${dataBlock}`);
}
const isFlag = dataBlock?.toLowerCase() === "flag";
const team = simGroup?.team ?? null;
const teamName = team > 0 ? TEAM_NAMES[team] : null;
const label = isFlag && teamName ? `${teamName} Flag` : null;
return (
<ShapeInfoProvider shapeName={shapeName} type="Item">
<group position={position} quaternion={q} scale={scale}>
@ -74,6 +87,9 @@ export function Item({ object }: { object: ConsoleObject }) {
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
<Suspense fallback={<ShapePlaceholder color="pink" />}>
<ShapeModel />
{label ? (
<FloatingLabel opacity={0.6}>{label}</FloatingLabel>
) : null}
</Suspense>
</ErrorBoundary>
) : (

View file

@ -1,6 +1,55 @@
import { createContext, useContext, useMemo } from "react";
import { ConsoleObject } from "../mission";
import { renderObject } from "./renderObject";
export function SimGroup({ object }: { object: ConsoleObject }) {
return object.children.map((child, i) => renderObject(child, i));
export type SimGroupContextType = {
object: ConsoleObject;
parent: SimGroupContextType;
hasTeams: boolean;
team: null | number;
};
const SimGroupContext = createContext<SimGroupContextType | null>(null);
export function useSimGroup() {
return useContext(SimGroupContext);
}
export function SimGroup({ object }: { object: ConsoleObject }) {
const parent = useSimGroup();
const simGroup: SimGroupContextType = useMemo(() => {
let team: number | null = null;
let hasTeams = false;
if (parent && parent.hasTeams) {
hasTeams = true;
if (parent.team != null) {
team = parent.team;
} else if (object.instanceName) {
const match = object.instanceName.match(/^team(\d+)$/i);
team = parseInt(match[1], 10);
}
} else if (object.instanceName) {
hasTeams = object.instanceName.toLowerCase() === "teams";
}
return {
// the current SimGroup's data
object,
// the closest ancestor of this SimGroup
parent,
// whether this is, or is the descendant of, the "Teams" SimGroup
hasTeams,
// what team this is for, when this is either a "Team<N>" SimGroup itself,
// or a descendant of one
team,
};
}, [object, parent]);
return (
<SimGroupContext.Provider value={simGroup}>
{object.children.map((child, i) => renderObject(child, i))}
</SimGroupContext.Provider>
);
}

View file

@ -0,0 +1,16 @@
import { useMemo } from "react";
import { ConsoleObject, getPosition, getProperty } from "../mission";
import { FloatingLabel } from "./FloatingLabel";
import { useSimGroup } from "./SimGroup";
export function WayPoint({ object }: { object: ConsoleObject }) {
const simGroup = useSimGroup();
const position = useMemo(() => getPosition(object), [object]);
const label = getProperty(object, "name").value;
return label ? (
<FloatingLabel position={position} opacity={0.6}>
{label}
</FloatingLabel>
) : null;
}

View file

@ -10,6 +10,7 @@ import { StaticShape } from "./StaticShape";
import { Item } from "./Item";
import { Turret } from "./Turret";
import { AudioEmitter } from "./AudioEmitter";
import { WayPoint } from "./WayPoint";
const componentMap = {
AudioEmitter,
@ -23,6 +24,7 @@ const componentMap = {
TSStatic,
Turret,
WaterBlock,
WayPoint,
};
export function renderObject(object: ConsoleObject, key: string | number) {