diff --git a/app/style.css b/app/style.css index 1c9c3424..0ca39d1c 100644 --- a/app/style.css +++ b/app/style.css @@ -51,6 +51,8 @@ main { color: #fff; font-size: 11px; white-space: nowrap; + padding: 1px 3px; + border-radius: 1px; } .StatsPanel { diff --git a/src/components/FloatingLabel.tsx b/src/components/FloatingLabel.tsx index c6578f0c..f5509b01 100644 --- a/src/components/FloatingLabel.tsx +++ b/src/components/FloatingLabel.tsx @@ -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(null); const distanceRef = useDistanceFromCamera(groupRef); - const [isVisible, setIsVisible] = useState(false); + const [isVisible, setIsVisible] = useState(opacity !== 0); const labelRef = useRef(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(); + } } }); diff --git a/src/components/Item.tsx b/src/components/Item.tsx index ce8c691f..baf29d36 100644 --- a/src/components/Item.tsx +++ b/src/components/Item.tsx @@ -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(` 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 ( @@ -74,6 +87,9 @@ export function Item({ object }: { object: ConsoleObject }) { }> }> + {label ? ( + {label} + ) : null} ) : ( diff --git a/src/components/SimGroup.tsx b/src/components/SimGroup.tsx index 5ae55e68..a5b1e137 100644 --- a/src/components/SimGroup.tsx +++ b/src/components/SimGroup.tsx @@ -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(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" SimGroup itself, + // or a descendant of one + team, + }; + }, [object, parent]); + + return ( + + {object.children.map((child, i) => renderObject(child, i))} + + ); } diff --git a/src/components/WayPoint.tsx b/src/components/WayPoint.tsx new file mode 100644 index 00000000..776118c3 --- /dev/null +++ b/src/components/WayPoint.tsx @@ -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 ? ( + + {label} + + ) : null; +} diff --git a/src/components/renderObject.tsx b/src/components/renderObject.tsx index 3beaebb3..b6071512 100644 --- a/src/components/renderObject.tsx +++ b/src/components/renderObject.tsx @@ -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) {