mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-01-19 20:25:01 +00:00
add flag and waypoint markers
This commit is contained in:
parent
b50ce94636
commit
608b538366
|
|
@ -51,6 +51,8 @@ main {
|
|||
color: #fff;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
padding: 1px 3px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.StatsPanel {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
16
src/components/WayPoint.tsx
Normal file
16
src/components/WayPoint.tsx
Normal 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;
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue