mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-04-21 20:35:39 +00:00
allow selecting different game types
This commit is contained in:
parent
7f75ed84da
commit
049566cdbb
56 changed files with 436 additions and 207 deletions
|
|
@ -70,7 +70,6 @@ function createMaterialFromFlags(
|
|||
const isTranslucent = flagNames.has("Translucent");
|
||||
const isAdditive = flagNames.has("Additive");
|
||||
const isSelfIlluminating = flagNames.has("SelfIlluminating");
|
||||
const neverEnvMap = flagNames.has("NeverEnvMap");
|
||||
|
||||
// SelfIlluminating materials are unlit (use MeshBasicMaterial)
|
||||
if (isSelfIlluminating) {
|
||||
|
|
@ -334,21 +333,25 @@ export function DebugPlaceholder({
|
|||
* pattern used across shape-rendering components.
|
||||
*/
|
||||
export function ShapeRenderer({
|
||||
shapeName,
|
||||
loadingColor = "yellow",
|
||||
children,
|
||||
}: {
|
||||
shapeName: string | undefined;
|
||||
loadingColor?: string;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const { object, shapeName } = useShapeInfo();
|
||||
|
||||
if (!shapeName) {
|
||||
return <DebugPlaceholder color="orange" />;
|
||||
return (
|
||||
<DebugPlaceholder color="orange" label={`${object._id}: <missing>`} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={<DebugPlaceholder color="red" label={shapeName} />}
|
||||
fallback={
|
||||
<DebugPlaceholder color="red" label={`${object._id}: ${shapeName}`} />
|
||||
}
|
||||
>
|
||||
<Suspense fallback={<ShapePlaceholder color={loadingColor} />}>
|
||||
<ShapeModel />
|
||||
|
|
@ -359,7 +362,7 @@ export function ShapeRenderer({
|
|||
}
|
||||
|
||||
export const ShapeModel = memo(function ShapeModel() {
|
||||
const { shapeName, isOrganic } = useShapeInfo();
|
||||
const { object, shapeName, isOrganic } = useShapeInfo();
|
||||
const { debugMode } = useDebug();
|
||||
const { nodes } = useStaticShape(shapeName);
|
||||
|
||||
|
|
@ -501,7 +504,11 @@ export const ShapeModel = memo(function ShapeModel() {
|
|||
) : null}
|
||||
</Suspense>
|
||||
))}
|
||||
{debugMode ? <FloatingLabel>{shapeName}</FloatingLabel> : null}
|
||||
{debugMode ? (
|
||||
<FloatingLabel>
|
||||
{object._id}: {shapeName}
|
||||
</FloatingLabel>
|
||||
) : null}
|
||||
</group>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,10 +3,18 @@ import { MissionSelect } from "./MissionSelect";
|
|||
|
||||
export function InspectorControls({
|
||||
missionName,
|
||||
missionType,
|
||||
onChangeMission,
|
||||
}: {
|
||||
missionName: string;
|
||||
onChangeMission: (name: string) => void;
|
||||
missionType: string;
|
||||
onChangeMission: ({
|
||||
missionName,
|
||||
missionType,
|
||||
}: {
|
||||
missionName: string;
|
||||
missionType: string;
|
||||
}) => void;
|
||||
}) {
|
||||
const {
|
||||
fogEnabled,
|
||||
|
|
@ -28,7 +36,11 @@ export function InspectorControls({
|
|||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<MissionSelect value={missionName} onChange={onChangeMission} />
|
||||
<MissionSelect
|
||||
value={missionName}
|
||||
missionType={missionType}
|
||||
onChange={onChangeMission}
|
||||
/>
|
||||
<div className="CheckboxField">
|
||||
<input
|
||||
id="fogInput"
|
||||
|
|
|
|||
|
|
@ -186,7 +186,13 @@ function InteriorMesh({ node }: { node: Mesh }) {
|
|||
}
|
||||
|
||||
export const InteriorModel = memo(
|
||||
({ interiorFile }: { interiorFile: string }) => {
|
||||
({
|
||||
object,
|
||||
interiorFile,
|
||||
}: {
|
||||
object: TorqueObject;
|
||||
interiorFile: string;
|
||||
}) => {
|
||||
const { nodes } = useInterior(interiorFile);
|
||||
const debugContext = useDebug();
|
||||
const debugMode = debugContext?.debugMode ?? false;
|
||||
|
|
@ -198,7 +204,11 @@ export const InteriorModel = memo(
|
|||
.map(([name, node]: [string, any]) => (
|
||||
<InteriorMesh key={name} node={node} />
|
||||
))}
|
||||
{debugMode ? <FloatingLabel>{interiorFile}</FloatingLabel> : null}
|
||||
{debugMode ? (
|
||||
<FloatingLabel>
|
||||
{object._id}: {interiorFile}
|
||||
</FloatingLabel>
|
||||
) : null}
|
||||
</group>
|
||||
);
|
||||
},
|
||||
|
|
@ -239,10 +249,12 @@ export const InteriorInstance = memo(function InteriorInstance({
|
|||
return (
|
||||
<group position={position} quaternion={q} scale={scale}>
|
||||
<ErrorBoundary
|
||||
fallback={<DebugInteriorPlaceholder label={interiorFile} />}
|
||||
fallback={
|
||||
<DebugInteriorPlaceholder label={`${object._id}: ${interiorFile}`} />
|
||||
}
|
||||
>
|
||||
<Suspense fallback={<InteriorPlaceholder color="orange" />}>
|
||||
<InteriorModel interiorFile={interiorFile} />
|
||||
<InteriorModel object={object} interiorFile={interiorFile} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</group>
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ export function Item({ object }: { object: TorqueObject }) {
|
|||
const label = isFlag && teamName ? `${teamName} Flag` : null;
|
||||
|
||||
return (
|
||||
<ShapeInfoProvider shapeName={shapeName} type="Item">
|
||||
<ShapeInfoProvider type="Item" object={object} shapeName={shapeName}>
|
||||
<group position={position} quaternion={q} scale={scale}>
|
||||
<ShapeRenderer shapeName={shapeName} loadingColor="pink">
|
||||
<ShapeRenderer loadingColor="pink">
|
||||
{label ? <FloatingLabel opacity={0.6}>{label}</FloatingLabel> : null}
|
||||
</ShapeRenderer>
|
||||
</group>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import picomatch from "picomatch";
|
|||
import { loadMission } from "../loaders";
|
||||
import { type ParsedMission } from "../mission";
|
||||
import { createScriptLoader } from "../torqueScript/scriptLoader.browser";
|
||||
import { renderObject } from "./renderObject";
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { SimObject } from "./SimObject";
|
||||
import { memo, useEffect, useMemo, useState } from "react";
|
||||
import { RuntimeProvider } from "./RuntimeProvider";
|
||||
import {
|
||||
createProgressTracker,
|
||||
|
|
@ -20,6 +20,7 @@ import {
|
|||
getResourceMap,
|
||||
getSourceAndPath,
|
||||
} from "../manifest";
|
||||
import { MissionProvider } from "./MissionContext";
|
||||
|
||||
const loadScript = createScriptLoader();
|
||||
// Shared cache for parsed scripts - survives runtime restarts
|
||||
|
|
@ -30,7 +31,7 @@ const fileSystem: FileSystemHandler = {
|
|||
return getResourceList()
|
||||
.filter((path) => isMatch(path))
|
||||
.map((resourceKey) => {
|
||||
const [sourcePath, actualPath] = getSourceAndPath(resourceKey);
|
||||
const [, actualPath] = getSourceAndPath(resourceKey);
|
||||
return actualPath;
|
||||
});
|
||||
},
|
||||
|
|
@ -56,6 +57,7 @@ interface ExecutedMissionState {
|
|||
|
||||
function useExecutedMission(
|
||||
missionName: string,
|
||||
missionType: string,
|
||||
parsedMission: ParsedMission | undefined,
|
||||
): ExecutedMissionState {
|
||||
const [state, setState] = useState<ExecutedMissionState>({
|
||||
|
|
@ -70,8 +72,6 @@ function useExecutedMission(
|
|||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
// FIXME: Always just runs as the first game type for now...
|
||||
const missionType = parsedMission.missionTypes[0];
|
||||
|
||||
// Create progress tracker and update state on changes
|
||||
const progressTracker = createProgressTracker();
|
||||
|
|
@ -138,19 +138,33 @@ function useExecutedMission(
|
|||
|
||||
interface MissionProps {
|
||||
name: string;
|
||||
missionType: string;
|
||||
setMissionType: (type: string) => void;
|
||||
onLoadingChange?: (isLoading: boolean, progress?: number) => void;
|
||||
}
|
||||
|
||||
export const Mission = memo(function Mission({
|
||||
name,
|
||||
missionType,
|
||||
onLoadingChange,
|
||||
}: MissionProps) {
|
||||
const { data: parsedMission } = useParsedMission(name);
|
||||
|
||||
const { missionGroup, runtime, progress } = useExecutedMission(
|
||||
name,
|
||||
missionType,
|
||||
parsedMission,
|
||||
);
|
||||
const isLoading = !missionGroup || !runtime;
|
||||
const isLoading = !parsedMission || !missionGroup || !runtime;
|
||||
|
||||
const missionContext = useMemo(
|
||||
() => ({
|
||||
metadata: parsedMission,
|
||||
missionType,
|
||||
missionGroup,
|
||||
}),
|
||||
[parsedMission, missionType, missionGroup],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onLoadingChange?.(isLoading, progress);
|
||||
|
|
@ -161,8 +175,10 @@ export const Mission = memo(function Mission({
|
|||
}
|
||||
|
||||
return (
|
||||
<RuntimeProvider runtime={runtime}>
|
||||
{renderObject(missionGroup)}
|
||||
</RuntimeProvider>
|
||||
<MissionProvider value={missionContext}>
|
||||
<RuntimeProvider runtime={runtime}>
|
||||
<SimObject object={missionGroup} />
|
||||
</RuntimeProvider>
|
||||
</MissionProvider>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
17
src/components/MissionContext.tsx
Normal file
17
src/components/MissionContext.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { createContext, useContext } from "react";
|
||||
import { ParsedMission } from "../mission";
|
||||
import { TorqueObject } from "../torqueScript";
|
||||
|
||||
export type MissionContextType = {
|
||||
metadata: ParsedMission;
|
||||
missionType: string;
|
||||
missionGroup: TorqueObject;
|
||||
};
|
||||
|
||||
const MissionContext = createContext<MissionContextType | null>(null);
|
||||
|
||||
export const MissionProvider = MissionContext.Provider;
|
||||
|
||||
export function useMission() {
|
||||
return useContext(MissionContext);
|
||||
}
|
||||
|
|
@ -132,7 +132,11 @@ function MissionItemContent({ mission }: { mission: MissionItem }) {
|
|||
{mission.missionTypes.length > 0 && (
|
||||
<span className="MissionSelect-itemTypes">
|
||||
{mission.missionTypes.map((type) => (
|
||||
<span key={type} className="MissionSelect-itemType">
|
||||
<span
|
||||
key={type}
|
||||
className="MissionSelect-itemType"
|
||||
data-mission-type={type}
|
||||
>
|
||||
{type}
|
||||
</span>
|
||||
))}
|
||||
|
|
@ -148,19 +152,41 @@ function MissionItemContent({ mission }: { mission: MissionItem }) {
|
|||
|
||||
export function MissionSelect({
|
||||
value,
|
||||
missionType,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (missionName: string) => void;
|
||||
missionType: string;
|
||||
onChange: ({
|
||||
missionName,
|
||||
missionType,
|
||||
}: {
|
||||
missionName: string;
|
||||
missionType: string | undefined;
|
||||
}) => void;
|
||||
}) {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const missionTypeRef = useRef<string | null>(missionType);
|
||||
|
||||
const combobox = useComboboxStore({
|
||||
resetValueOnHide: true,
|
||||
selectedValue: value,
|
||||
setSelectedValue: (newValue) => {
|
||||
if (newValue) onChange(newValue);
|
||||
if (newValue) {
|
||||
let newMissionType = missionTypeRef.current;
|
||||
const availableMissionTypes = getMissionInfo(newValue).missionTypes;
|
||||
if (
|
||||
!newMissionType ||
|
||||
!availableMissionTypes.includes(newMissionType)
|
||||
) {
|
||||
newMissionType = availableMissionTypes[0];
|
||||
}
|
||||
onChange({
|
||||
missionName: newValue,
|
||||
missionType: newMissionType,
|
||||
});
|
||||
}
|
||||
},
|
||||
setValue: (value) => {
|
||||
startTransition(() => setSearchValue(value));
|
||||
|
|
@ -200,6 +226,40 @@ export function MissionSelect({
|
|||
? filteredResults.missions.length === 0
|
||||
: filteredResults.groups.length === 0;
|
||||
|
||||
const renderItem = (mission) => {
|
||||
return (
|
||||
<ComboboxItem
|
||||
key={mission.missionName}
|
||||
value={mission.missionName}
|
||||
className="MissionSelect-item"
|
||||
focusOnHover
|
||||
onClick={(event) => {
|
||||
if (event.target && event.target instanceof HTMLElement) {
|
||||
const missionType = event.target.dataset.missionType;
|
||||
if (missionType) {
|
||||
missionTypeRef.current = missionType;
|
||||
const isOnlyMissionTypeChange = mission.missionName === value;
|
||||
if (isOnlyMissionTypeChange) {
|
||||
// Need to trigger change ourselves, because Combobox sees this
|
||||
// as no change.
|
||||
onChange({
|
||||
missionName: mission.missionName,
|
||||
missionType,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
missionTypeRef.current = null;
|
||||
}
|
||||
} else {
|
||||
missionTypeRef.current = null;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MissionItemContent mission={mission} />
|
||||
</ComboboxItem>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ComboboxProvider store={combobox}>
|
||||
<div className="MissionSelect-inputWrapper">
|
||||
|
|
@ -213,21 +273,23 @@ export function MissionSelect({
|
|||
combobox.show();
|
||||
}}
|
||||
/>
|
||||
<div className="MissionSelect-selectedValue">
|
||||
<span className="MissionSelect-selectedName">{displayValue}</span>
|
||||
{missionType && (
|
||||
<span
|
||||
className="MissionSelect-itemType"
|
||||
data-mission-type={missionType}
|
||||
>
|
||||
{missionType}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<kbd className="MissionSelect-shortcut">{isMac ? "⌘K" : "^K"}</kbd>
|
||||
</div>
|
||||
<ComboboxPopover gutter={4} fitViewport className="MissionSelect-popover">
|
||||
<ComboboxList className="MissionSelect-list">
|
||||
{filteredResults.type === "flat"
|
||||
? filteredResults.missions.map((mission) => (
|
||||
<ComboboxItem
|
||||
key={mission.missionName}
|
||||
value={mission.missionName}
|
||||
className="MissionSelect-item"
|
||||
focusOnHover
|
||||
>
|
||||
<MissionItemContent mission={mission} />
|
||||
</ComboboxItem>
|
||||
))
|
||||
? filteredResults.missions.map(renderItem)
|
||||
: filteredResults.groups.map(([groupName, missions]) =>
|
||||
groupName ? (
|
||||
<ComboboxGroup
|
||||
|
|
@ -237,29 +299,11 @@ export function MissionSelect({
|
|||
<ComboboxGroupLabel className="MissionSelect-groupLabel">
|
||||
{groupName}
|
||||
</ComboboxGroupLabel>
|
||||
{missions.map((mission) => (
|
||||
<ComboboxItem
|
||||
key={mission.missionName}
|
||||
value={mission.missionName}
|
||||
className="MissionSelect-item"
|
||||
focusOnHover
|
||||
>
|
||||
<MissionItemContent mission={mission} />
|
||||
</ComboboxItem>
|
||||
))}
|
||||
{missions.map(renderItem)}
|
||||
</ComboboxGroup>
|
||||
) : (
|
||||
<Fragment key="ungrouped">
|
||||
{missions.map((mission) => (
|
||||
<ComboboxItem
|
||||
key={mission.missionName}
|
||||
value={mission.missionName}
|
||||
className="MissionSelect-item"
|
||||
focusOnHover
|
||||
>
|
||||
<MissionItemContent mission={mission} />
|
||||
</ComboboxItem>
|
||||
))}
|
||||
{missions.map(renderItem)}
|
||||
</Fragment>
|
||||
),
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { createContext, ReactNode, useContext, useMemo } from "react";
|
||||
import { TorqueObject } from "../torqueScript";
|
||||
|
||||
export type StaticShapeType = "TSStatic" | "StaticShape" | "Item" | "Turret";
|
||||
|
||||
|
|
@ -17,25 +18,44 @@ export function isOrganicShape(shapeName: string): boolean {
|
|||
return ORGANIC_PATTERN.test(shapeName);
|
||||
}
|
||||
|
||||
const ShapeInfoContext = createContext(null);
|
||||
interface ShapeInfoContextValue {
|
||||
object: TorqueObject;
|
||||
shapeName: string;
|
||||
type: StaticShapeType;
|
||||
isOrganic: boolean;
|
||||
}
|
||||
|
||||
export function useShapeInfo() {
|
||||
return useContext(ShapeInfoContext);
|
||||
const ShapeInfoContext = createContext<ShapeInfoContextValue | null>(null);
|
||||
|
||||
export function useShapeInfo(): ShapeInfoContextValue {
|
||||
const context = useContext(ShapeInfoContext);
|
||||
if (!context) {
|
||||
throw new Error("useShapeInfo must be used within ShapeInfoProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function ShapeInfoProvider({
|
||||
children,
|
||||
object,
|
||||
shapeName,
|
||||
type,
|
||||
}: {
|
||||
object: TorqueObject;
|
||||
children: ReactNode;
|
||||
shapeName: string;
|
||||
type: StaticShapeType;
|
||||
}) {
|
||||
const isOrganic = useMemo(() => isOrganicShape(shapeName), [shapeName]);
|
||||
|
||||
const context = useMemo(
|
||||
() => ({ shapeName, type, isOrganic }),
|
||||
[shapeName, type, isOrganic],
|
||||
() => ({
|
||||
object,
|
||||
shapeName,
|
||||
type,
|
||||
isOrganic,
|
||||
}),
|
||||
[object, shapeName, type, isOrganic],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createContext, useContext, useMemo } from "react";
|
||||
import type { TorqueObject } from "../torqueScript";
|
||||
import { renderObject } from "./renderObject";
|
||||
import { SimObject } from "./SimObject";
|
||||
|
||||
export type SimGroupContextType = {
|
||||
object: TorqueObject;
|
||||
|
|
@ -51,7 +51,9 @@ export function SimGroup({ object }: { object: TorqueObject }) {
|
|||
|
||||
return (
|
||||
<SimGroupContext.Provider value={simGroup}>
|
||||
{(object._children ?? []).map((child, i) => renderObject(child, i))}
|
||||
{(object._children ?? []).map((child, i) => (
|
||||
<SimObject object={child} key={child._id} />
|
||||
))}
|
||||
</SimGroupContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { lazy, Suspense } from "react";
|
||||
import { lazy, Suspense, useMemo } from "react";
|
||||
import type { TorqueObject } from "../torqueScript";
|
||||
import { TerrainBlock } from "./TerrainBlock";
|
||||
import { SimGroup } from "./SimGroup";
|
||||
|
|
@ -12,6 +12,8 @@ import { Turret } from "./Turret";
|
|||
import { WayPoint } from "./WayPoint";
|
||||
import { Camera } from "./Camera";
|
||||
import { useSettings } from "./SettingsProvider";
|
||||
import { useMission } from "./MissionContext";
|
||||
import { getProperty } from "../mission";
|
||||
|
||||
const AudioEmitter = lazy(() =>
|
||||
import("./AudioEmitter").then((mod) => ({ default: mod.AudioEmitter })),
|
||||
|
|
@ -27,7 +29,7 @@ const ForceFieldBare = lazy(() =>
|
|||
import("./ForceFieldBare").then((mod) => ({ default: mod.ForceFieldBare })),
|
||||
);
|
||||
|
||||
// Not every map will have force fields.
|
||||
// Not every map will have water.
|
||||
const WaterBlock = lazy(() =>
|
||||
import("./WaterBlock").then((mod) => ({ default: mod.WaterBlock })),
|
||||
);
|
||||
|
|
@ -49,10 +51,26 @@ const componentMap = {
|
|||
WayPoint,
|
||||
};
|
||||
|
||||
export function renderObject(object: TorqueObject, key?: string | number) {
|
||||
export function SimObject({ object }: { object: TorqueObject }) {
|
||||
const { missionType } = useMission();
|
||||
// FIXME: In theory we could make sure TorqueScript is calling `hide()`
|
||||
// based on the mission type already, which is built-in behavior, then just
|
||||
// make sure we respect the hidden/visible state here. For now do it this way.
|
||||
const shouldShowObject = useMemo(() => {
|
||||
const missionTypesList = new Set(
|
||||
(getProperty(object, "missionTypesList") ?? "")
|
||||
.toLowerCase()
|
||||
.split(/s+/)
|
||||
.filter(Boolean),
|
||||
);
|
||||
return (
|
||||
!missionTypesList.size || missionTypesList.has(missionType.toLowerCase())
|
||||
);
|
||||
}, [object, missionType]);
|
||||
|
||||
const Component = componentMap[object._className];
|
||||
return Component ? (
|
||||
<Suspense key={key}>
|
||||
return shouldShowObject && Component ? (
|
||||
<Suspense>
|
||||
<Component object={object} />
|
||||
</Suspense>
|
||||
) : null;
|
||||
|
|
@ -6,9 +6,9 @@ import { Color, Fog } from "three";
|
|||
import type { TorqueObject } from "../torqueScript";
|
||||
import { getInt, getProperty } from "../mission";
|
||||
import { useSettings } from "./SettingsProvider";
|
||||
import { BASE_URL, loadDetailMapList, textureToUrl } from "../loaders";
|
||||
import { loadDetailMapList, textureToUrl } from "../loaders";
|
||||
import { CloudLayers } from "./CloudLayers";
|
||||
import { parseFogState, type FogState, type FogVolume } from "./FogProvider";
|
||||
import { parseFogState, type FogState } from "./FogProvider";
|
||||
import { installCustomFogShader } from "../fogShader";
|
||||
import {
|
||||
globalFogUniforms,
|
||||
|
|
@ -17,8 +17,6 @@ import {
|
|||
resetGlobalFogUniforms,
|
||||
} from "../globalFogUniforms";
|
||||
|
||||
const FALLBACK_TEXTURE_URL = `${BASE_URL}/black.png`;
|
||||
|
||||
// Track if fog shader has been installed (idempotent installation)
|
||||
let fogShaderInstalled = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ export function StaticShape({ object }: { object: TorqueObject }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<ShapeInfoProvider shapeName={shapeName} type="StaticShape">
|
||||
<ShapeInfoProvider type="StaticShape" object={object} shapeName={shapeName}>
|
||||
<group position={position} quaternion={q} scale={scale}>
|
||||
<ShapeRenderer shapeName={shapeName} />
|
||||
<ShapeRenderer />
|
||||
</group>
|
||||
</ShapeInfoProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ export function TSStatic({ object }: { object: TorqueObject }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<ShapeInfoProvider shapeName={shapeName} type="TSStatic">
|
||||
<ShapeInfoProvider type="TSStatic" object={object} shapeName={shapeName}>
|
||||
<group position={position} quaternion={q} scale={scale}>
|
||||
<ShapeRenderer shapeName={shapeName} />
|
||||
<ShapeRenderer />
|
||||
</group>
|
||||
</ShapeInfoProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -30,13 +30,17 @@ export function Turret({ object }: { object: TorqueObject }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<ShapeInfoProvider shapeName={shapeName} type="Turret">
|
||||
<ShapeInfoProvider type="Turret" object={object} shapeName={shapeName}>
|
||||
<group position={position} quaternion={q} scale={scale}>
|
||||
<ShapeRenderer shapeName={shapeName} />
|
||||
<ShapeRenderer />
|
||||
{barrelShapeName ? (
|
||||
<ShapeInfoProvider shapeName={barrelShapeName} type="Turret">
|
||||
<ShapeInfoProvider
|
||||
type="Turret"
|
||||
object={object}
|
||||
shapeName={barrelShapeName}
|
||||
>
|
||||
<group position={[0, 1.5, 0]}>
|
||||
<ShapeRenderer shapeName={barrelShapeName} />
|
||||
<ShapeRenderer />
|
||||
</group>
|
||||
</ShapeInfoProvider>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ export const WaterBlock = memo(function WaterBlock({
|
|||
// TODO: Use this for terrain intersection masking (reject water blocks where
|
||||
// terrain height > surfaceZ + waveMagnitude/2). Requires TerrainProvider.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const surfaceZ = position[1] + scaleY;
|
||||
// const surfaceZ = position[1] + scaleY;
|
||||
|
||||
// Wave magnitude affects terrain masking (Torque adds half to surface height)
|
||||
const waveMagnitude = getFloat(object, "waveMagnitude") ?? 1.0;
|
||||
|
|
@ -148,7 +148,10 @@ export const WaterBlock = memo(function WaterBlock({
|
|||
// Matches fluidQuadTree.cc RunQuadTree():
|
||||
// I = (s32)(m_Eye.X / 2048.0f);
|
||||
// if( m_Eye.X < 0.0f ) I--;
|
||||
const calculateReps = (camX: number, camZ: number): Array<[number, number]> => {
|
||||
const calculateReps = (
|
||||
camX: number,
|
||||
camZ: number,
|
||||
): Array<[number, number]> => {
|
||||
// Convert camera to terrain space
|
||||
const terrainCamX = camX + TERRAIN_OFFSET;
|
||||
const terrainCamZ = camZ + TERRAIN_OFFSET;
|
||||
|
|
|
|||
|
|
@ -2,10 +2,8 @@ import { useMemo } from "react";
|
|||
import type { TorqueObject } from "../torqueScript";
|
||||
import { getPosition, getProperty } from "../mission";
|
||||
import { FloatingLabel } from "./FloatingLabel";
|
||||
import { useSimGroup } from "./SimGroup";
|
||||
|
||||
export function WayPoint({ object }: { object: TorqueObject }) {
|
||||
const simGroup = useSimGroup();
|
||||
const position = useMemo(() => getPosition(object), [object]);
|
||||
const label = getProperty(object, "name");
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,11 @@ function createAtlas(textures: Texture[]): IflAtlas {
|
|||
textures.forEach((tex, i) => {
|
||||
const col = i % columns;
|
||||
const row = Math.floor(i / columns);
|
||||
ctx.drawImage(tex.image as CanvasImageSource, col * frameWidth, row * frameHeight);
|
||||
ctx.drawImage(
|
||||
tex.image as CanvasImageSource,
|
||||
col * frameWidth,
|
||||
row * frameHeight,
|
||||
);
|
||||
});
|
||||
|
||||
const texture = new CanvasTexture(canvas);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue