mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-07-11 06:24:47 +00:00
WayPoint fix, FloatingLabel improvements
This commit is contained in:
parent
5675b952b5
commit
9a95c04c68
40 changed files with 632 additions and 628 deletions
|
|
@ -6,4 +6,5 @@
|
|||
padding: 1px 3px;
|
||||
border-radius: 1px;
|
||||
text-align: center;
|
||||
opacity: 0; /* updated in useFrame */
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,21 +21,22 @@ function isBehindCamera(
|
|||
);
|
||||
}
|
||||
|
||||
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";
|
||||
/** Default fade distance for fadeWithDistance labels. */
|
||||
const DEFAULT_FADE_DISTANCE = 200;
|
||||
|
||||
/**
|
||||
* Hook that manages visibility and opacity fading for a floating label group.
|
||||
* Attach `groupRef` to a `<group>` so world-position lookups work. Apply
|
||||
* `opacityRef.current` to DOM elements each frame for smooth fading.
|
||||
*/
|
||||
export function useFloatingLabelFade({
|
||||
opacity: opacityProp = "fadeWithDistance" as number | "fadeWithDistance",
|
||||
fadeDistance = DEFAULT_FADE_DISTANCE,
|
||||
} = {}) {
|
||||
const fadeWithDistance = opacityProp === "fadeWithDistance";
|
||||
const groupRef = useRef<Object3D>(null);
|
||||
const [isVisible, setIsVisible] = useState(opacity !== 0);
|
||||
const labelRef = useRef<HTMLDivElement>(null);
|
||||
const [isVisible, setIsVisible] = useState(opacityProp !== 0);
|
||||
const opacityRef = useRef("0");
|
||||
|
||||
useFrame(({ camera }) => {
|
||||
const group = groupRef.current;
|
||||
|
|
@ -53,25 +54,49 @@ export const FloatingLabel = memo(function FloatingLabel({
|
|||
const distance = behind
|
||||
? Infinity
|
||||
: camera.position.distanceTo(_worldPos);
|
||||
const shouldBeVisible = distance < 200;
|
||||
const shouldBeVisible = distance < fadeDistance;
|
||||
|
||||
if (isVisible !== shouldBeVisible) {
|
||||
setIsVisible(shouldBeVisible);
|
||||
}
|
||||
|
||||
// Update opacity directly on DOM element (no re-render).
|
||||
if (labelRef.current && shouldBeVisible) {
|
||||
const fadeOpacity = Math.max(0, Math.min(1, 1 - distance / 200));
|
||||
labelRef.current.style.opacity = fadeOpacity.toString();
|
||||
}
|
||||
opacityRef.current = shouldBeVisible
|
||||
? Math.max(0, Math.min(1, 1 - distance / fadeDistance)).toString()
|
||||
: "0";
|
||||
} else {
|
||||
const shouldBeVisible = !behind && opacity !== 0;
|
||||
const shouldBeVisible = !behind && opacityProp !== 0;
|
||||
if (isVisible !== shouldBeVisible) {
|
||||
setIsVisible(shouldBeVisible);
|
||||
}
|
||||
if (labelRef.current) {
|
||||
labelRef.current.style.opacity = (opacity as number).toString();
|
||||
}
|
||||
opacityRef.current = (opacityProp as number).toString();
|
||||
}
|
||||
});
|
||||
|
||||
return { groupRef, isVisible, opacityRef };
|
||||
}
|
||||
|
||||
export const FloatingLabel = memo(function FloatingLabel({
|
||||
children,
|
||||
color = "white",
|
||||
position = DEFAULT_POSITION,
|
||||
opacity = "fadeWithDistance",
|
||||
fadeDistance,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
color?: string;
|
||||
position?: [x: number, y: number, z: number];
|
||||
opacity?: number | "fadeWithDistance";
|
||||
fadeDistance?: number;
|
||||
}) {
|
||||
const { groupRef, isVisible, opacityRef } = useFloatingLabelFade({
|
||||
opacity,
|
||||
fadeDistance,
|
||||
});
|
||||
const labelRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useFrame(() => {
|
||||
if (labelRef.current) {
|
||||
labelRef.current.style.opacity = opacityRef.current;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -79,11 +104,7 @@ export const FloatingLabel = memo(function FloatingLabel({
|
|||
<group ref={groupRef}>
|
||||
{isVisible ? (
|
||||
<Html position={position} center>
|
||||
<div
|
||||
ref={labelRef}
|
||||
className={styles.Label}
|
||||
style={{ color, opacity: fadeWithDistance ? 0 : undefined }}
|
||||
>
|
||||
<div ref={labelRef} className={styles.Label} style={{ color }}>
|
||||
{children}
|
||||
</div>
|
||||
</Html>
|
||||
|
|
|
|||
|
|
@ -159,8 +159,8 @@
|
|||
}
|
||||
|
||||
.Description {
|
||||
font-size: 13px;
|
||||
line-height: calc(20 / 13);
|
||||
font-size: 14px;
|
||||
line-height: calc(20 / 14);
|
||||
}
|
||||
|
||||
.Field label,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
opacity: 0; /* updated in useFrame */
|
||||
}
|
||||
|
||||
.Top {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { useMemo, useRef, useState } from "react";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useMemo, useRef } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { Html } from "@react-three/drei";
|
||||
import { Box3, Object3D, Vector3 } from "three";
|
||||
import { Box3 } from "three";
|
||||
import { getKeyframeAtTime } from "../stream/playbackUtils";
|
||||
import { textureToUrl } from "../loaders";
|
||||
import { useStaticShape } from "./GenericShape";
|
||||
import { useFloatingLabelFade } from "./FloatingLabel";
|
||||
import { streamPlaybackStore } from "../state/streamPlaybackStore";
|
||||
import type { PlayerEntity } from "../state/gameEntityTypes";
|
||||
import styles from "./PlayerNameplate.module.css";
|
||||
|
|
@ -21,8 +22,6 @@ const NAME_HEIGHT = -0.2;
|
|||
const IFF_FRIENDLY_URL = textureToUrl("gui/hud_alliedtriangle");
|
||||
const IFF_ENEMY_URL = textureToUrl("gui/hud_enemytriangle");
|
||||
|
||||
const _tmpVec = new Vector3();
|
||||
|
||||
const EMPTY_KEYFRAMES: never[] = [];
|
||||
|
||||
/**
|
||||
|
|
@ -31,13 +30,13 @@ const EMPTY_KEYFRAMES: never[] = [];
|
|||
*/
|
||||
export function PlayerNameplate({ entity }: { entity: PlayerEntity }) {
|
||||
const gltf = useStaticShape((entity.shapeName ?? entity.dataBlock)!);
|
||||
const camera = useThree((state) => state.camera);
|
||||
const groupRef = useRef<Object3D>(null);
|
||||
const { groupRef, isVisible, opacityRef } = useFloatingLabelFade({
|
||||
fadeDistance: NAMEPLATE_FADE_DISTANCE,
|
||||
});
|
||||
const iffContainerRef = useRef<HTMLDivElement>(null);
|
||||
const nameContainerRef = useRef<HTMLDivElement>(null);
|
||||
const fillRef = useRef<HTMLDivElement>(null);
|
||||
const iffImgRef = useRef<HTMLImageElement>(null);
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
const nameRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Derive IFF height from the shape's bounding box.
|
||||
|
|
@ -54,27 +53,7 @@ export function PlayerNameplate({ entity }: { entity: PlayerEntity }) {
|
|||
);
|
||||
|
||||
useFrame(() => {
|
||||
const group = groupRef.current;
|
||||
if (!group) return;
|
||||
|
||||
// Compute world-space distance to camera.
|
||||
group.getWorldPosition(_tmpVec);
|
||||
const distance = camera.position.distanceTo(_tmpVec);
|
||||
|
||||
// Check if behind camera using dot product with camera forward (-Z column).
|
||||
const e = camera.matrixWorld.elements;
|
||||
const behind =
|
||||
(_tmpVec.x - e[12]) * -e[8] +
|
||||
(_tmpVec.y - e[13]) * -e[9] +
|
||||
(_tmpVec.z - e[14]) * -e[10] <
|
||||
0;
|
||||
const shouldBeVisible = !behind && distance < NAMEPLATE_FADE_DISTANCE;
|
||||
|
||||
if (isVisible !== shouldBeVisible) {
|
||||
setIsVisible(shouldBeVisible);
|
||||
}
|
||||
|
||||
if (!shouldBeVisible) return;
|
||||
if (!isVisible) return;
|
||||
|
||||
// Hide nameplate when player is dead.
|
||||
const kf = getKeyframeAtTime(
|
||||
|
|
@ -89,17 +68,13 @@ export function PlayerNameplate({ entity }: { entity: PlayerEntity }) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Update opacity on both label containers.
|
||||
const opacity = Math.max(
|
||||
0,
|
||||
Math.min(1, 1 - distance / NAMEPLATE_FADE_DISTANCE),
|
||||
);
|
||||
const opacityStr = opacity.toString();
|
||||
// Apply shared fade opacity to both containers.
|
||||
const opacity = opacityRef.current;
|
||||
if (iffContainerRef.current) {
|
||||
iffContainerRef.current.style.opacity = opacityStr;
|
||||
iffContainerRef.current.style.opacity = opacity;
|
||||
}
|
||||
if (nameContainerRef.current) {
|
||||
nameContainerRef.current.style.opacity = opacityStr;
|
||||
nameContainerRef.current.style.opacity = opacity;
|
||||
}
|
||||
|
||||
// Update player name imperatively — entity.playerName is mutated in-place
|
||||
|
|
|
|||
|
|
@ -78,17 +78,23 @@ export function TouchHandler() {
|
|||
// Map joystick to movement axes, pre-scaled by speedMultiplier.
|
||||
x = Math.max(
|
||||
-1,
|
||||
Math.min(1, joyX * normalizedMoveForce * speedMultiplier),
|
||||
Math.min(
|
||||
1,
|
||||
joyX * normalizedMoveForce * (0.8 * speedMultiplier + 0.05),
|
||||
),
|
||||
);
|
||||
y = Math.max(
|
||||
-1,
|
||||
Math.min(1, joyY * normalizedMoveForce * speedMultiplier),
|
||||
Math.min(
|
||||
1,
|
||||
joyY * normalizedMoveForce * (0.8 * speedMultiplier + 0.05),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (touchMode === "moveLookStick") {
|
||||
if (moveForce > 0) {
|
||||
// Move forward at half speed.
|
||||
y = Math.max(-1, Math.min(1, 0.5 * speedMultiplier));
|
||||
y = Math.min(1, 0.5 * speedMultiplier + 0.05);
|
||||
|
||||
if (moveForce >= SINGLE_STICK_DEADZONE) {
|
||||
// Outer zone: also control camera look (yaw + pitch).
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { FloatingLabel } from "./FloatingLabel";
|
|||
|
||||
export function WayPoint({ entity }: { entity: WayPointEntity }) {
|
||||
return entity.label ? (
|
||||
<FloatingLabel position={entity.position} opacity={0.6}>
|
||||
<FloatingLabel opacity={0.6}>
|
||||
{entity.label}
|
||||
</FloatingLabel>
|
||||
) : null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue