mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-07-11 14:34:51 +00:00
entity tracking and physics improvements, new HUD options
This commit is contained in:
parent
b6975da8ed
commit
2d054a7884
46 changed files with 160 additions and 85 deletions
|
|
@ -25,8 +25,12 @@ import styles from "./DemoPlaybackControls.module.css";
|
|||
// ];
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
const m = Math.floor(seconds / 60);
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
if (h > 0) {
|
||||
return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
|
||||
}
|
||||
return `${m}:${s.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,10 @@ export const InspectorControls = memo(function InspectorControls({
|
|||
setFpsLimit,
|
||||
showInputOverlay,
|
||||
setShowInputOverlay,
|
||||
showChat,
|
||||
setShowChat,
|
||||
showReticle,
|
||||
setShowReticle,
|
||||
} = useSettings();
|
||||
const {
|
||||
speedMultiplier,
|
||||
|
|
@ -331,6 +335,39 @@ export const InspectorControls = memo(function InspectorControls({
|
|||
Show input overlay
|
||||
</label>
|
||||
</div>
|
||||
{hasStreamData && (
|
||||
<>
|
||||
<div className={styles.CheckboxField}>
|
||||
<input
|
||||
id="showChatInput"
|
||||
type="checkbox"
|
||||
checked={showChat}
|
||||
onChange={(event) => {
|
||||
setShowChat(event.target.checked);
|
||||
}}
|
||||
/>
|
||||
<label className={styles.Label} htmlFor="showChatInput">
|
||||
Show chat HUD
|
||||
</label>
|
||||
</div>
|
||||
<div className={styles.CheckboxField}>
|
||||
<input
|
||||
id="showReticleInput"
|
||||
type="checkbox"
|
||||
checked={showReticle}
|
||||
onChange={(event) => {
|
||||
setShowReticle(event.target.checked);
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
className={styles.Label}
|
||||
htmlFor="showReticleInput"
|
||||
>
|
||||
Show reticles
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Accordion>
|
||||
<Accordion value="audio" label="Audio">
|
||||
<div className={styles.CheckboxField}>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { textureToUrl } from "../loaders";
|
|||
import type { StreamEntity, TeamScore, WeaponsHudSlot } from "../stream/types";
|
||||
import styles from "./PlayerHUD.module.css";
|
||||
import { ChatWindow } from "./ChatWindow";
|
||||
import { useSettings } from "./SettingsProvider";
|
||||
|
||||
const COMPASS_URL = textureToUrl("gui/hud_new_compass");
|
||||
const NSEW_URL = textureToUrl("gui/hud_new_NSEW");
|
||||
|
|
@ -416,10 +417,11 @@ export function PlayerHUD() {
|
|||
// In free-fly mode the camera is disconnected from the player, so
|
||||
// player-specific HUD elements (health, energy, weapons, etc.) are hidden.
|
||||
const showPlayerElements = hasControlPlayer && cameraMode !== "freeFly";
|
||||
const { showChat, showReticle } = useSettings();
|
||||
|
||||
return (
|
||||
<div className={styles.PlayerHUD}>
|
||||
<ChatWindow />
|
||||
{showChat && <ChatWindow />}
|
||||
{showPlayerElements && (
|
||||
<div className={styles.Bars}>
|
||||
<HealthBar />
|
||||
|
|
@ -431,7 +433,7 @@ export function PlayerHUD() {
|
|||
<>
|
||||
<WeaponHUD />
|
||||
<PackAndInventoryHUD />
|
||||
<Reticle />
|
||||
{showReticle && <Reticle />}
|
||||
</>
|
||||
)}
|
||||
<TeamScores />
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@ type SettingsContextType = {
|
|||
setFpsLimit: StateSetter<number | null>;
|
||||
showInputOverlay: boolean;
|
||||
setShowInputOverlay: StateSetter<boolean>;
|
||||
showChat: boolean;
|
||||
setShowChat: StateSetter<boolean>;
|
||||
showReticle: boolean;
|
||||
setShowReticle: StateSetter<boolean>;
|
||||
};
|
||||
|
||||
type DebugContextType = {
|
||||
|
|
@ -96,6 +100,8 @@ type PersistedSettings = {
|
|||
sidebarOpen?: boolean;
|
||||
fpsLimit?: number | null;
|
||||
showInputOverlay?: boolean;
|
||||
showChat?: boolean;
|
||||
showReticle?: boolean;
|
||||
};
|
||||
|
||||
export function useSettings() {
|
||||
|
|
@ -149,6 +155,8 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
|||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [fpsLimit, setFpsLimit] = useState<number | null>(null);
|
||||
const [showInputOverlay, setShowInputOverlay] = useState(true);
|
||||
const [showChat, setShowChat] = useState(true);
|
||||
const [showReticle, setShowReticle] = useState(true);
|
||||
const [renderOnDemand, setRenderOnDemand] = useState(false);
|
||||
|
||||
const [fogEnabledOverride, setFogEnabledOverride] = useFogQueryState();
|
||||
|
|
@ -189,6 +197,10 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
|||
setFpsLimit,
|
||||
showInputOverlay,
|
||||
setShowInputOverlay,
|
||||
showChat,
|
||||
setShowChat,
|
||||
showReticle,
|
||||
setShowReticle,
|
||||
}),
|
||||
[
|
||||
fogEnabled,
|
||||
|
|
@ -205,6 +217,8 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
|||
sidebarOpen,
|
||||
fpsLimit,
|
||||
showInputOverlay,
|
||||
showChat,
|
||||
showReticle,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
@ -323,6 +337,12 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
|||
if (savedSettings.showInputOverlay != null) {
|
||||
setShowInputOverlay(savedSettings.showInputOverlay);
|
||||
}
|
||||
if (savedSettings.showChat != null) {
|
||||
setShowChat(savedSettings.showChat);
|
||||
}
|
||||
if (savedSettings.showReticle != null) {
|
||||
setShowReticle(savedSettings.showReticle);
|
||||
}
|
||||
if (savedSettings.sidebarOpen != null) {
|
||||
// Don't restore on touch devices!
|
||||
if (!isTouch) {
|
||||
|
|
@ -366,6 +386,8 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
|||
sidebarOpen,
|
||||
fpsLimit,
|
||||
showInputOverlay,
|
||||
showChat,
|
||||
showReticle,
|
||||
};
|
||||
try {
|
||||
localStorage.setItem("settings", JSON.stringify(settingsToSave));
|
||||
|
|
@ -398,6 +420,8 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
|||
sidebarOpen,
|
||||
fpsLimit,
|
||||
showInputOverlay,
|
||||
showChat,
|
||||
showReticle,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import { ghostToSceneObject } from "../scene";
|
||||
import { getTerrainHeightAt } from "../terrainHeight";
|
||||
import type { SceneObject } from "../scene/types";
|
||||
import { getTerrainHeightAt } from "../terrainHeight";
|
||||
import {
|
||||
linearProjectileClassNames,
|
||||
ballisticProjectileClassNames,
|
||||
seekerProjectileClassNames,
|
||||
toEntityType,
|
||||
allocateEntityId,
|
||||
resetEntityIdCounter,
|
||||
GhostMessage,
|
||||
IFF_GREEN,
|
||||
IFF_RED,
|
||||
|
|
@ -116,10 +115,11 @@ export interface MutableEntity {
|
|||
/** ShapeBase sound slots (4 max). Raw ghost SoundMask data — components
|
||||
* manage PositionalAudio objects directly, matching Tribes 2's approach. */
|
||||
soundSlots?: Array<{ index: number; playing: boolean; profileId?: number }>;
|
||||
/** Item velocity interpolation state (dropped weapons/items).
|
||||
* The real Tribes 2 client does NOT simulate physics (gravity/collision)
|
||||
* for items — it just interpolates position using server-sent velocity
|
||||
* until the next server update arrives. */
|
||||
/** Item mStatic flag (from InitialUpdateMask). Static items (flags at
|
||||
* flagstand) skip all physics in Item::processTick. */
|
||||
isStaticItem?: boolean;
|
||||
/** Item velocity interpolation state. The client simulates full physics
|
||||
* (gravity, collision, bounce) for non-static, non-at-rest items. */
|
||||
itemPhysics?: {
|
||||
velocity: [number, number, number];
|
||||
atRest: boolean;
|
||||
|
|
@ -353,13 +353,14 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
|
||||
// ── Shared reset logic ──
|
||||
|
||||
/** Clear all entity state (entities, ghost→ID map, ID counter, generation).
|
||||
* Called on full reset and on GhostingMessageEvent (mission change). */
|
||||
/** Clear all entity state (entities, ghost→ID map, generation).
|
||||
* Called on full reset and on GhostingMessageEvent (mission change).
|
||||
* Does NOT reset the ID counter — IDs must never be reused to avoid
|
||||
* stale entity collisions in the render store after seeks. */
|
||||
protected clearAllEntities(): void {
|
||||
this.entities.clear();
|
||||
this.entityIdByGhostIndex.clear();
|
||||
this.entityGeneration++;
|
||||
resetEntityIdCounter();
|
||||
}
|
||||
|
||||
protected resetSharedState(): void {
|
||||
|
|
@ -945,6 +946,7 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
entity.explosionDataBlockId = undefined;
|
||||
entity.maintainEmitterId = undefined;
|
||||
entity.soundSlots = undefined;
|
||||
entity.isStaticItem = undefined;
|
||||
}
|
||||
|
||||
// ── Apply ghost data ──
|
||||
|
|
@ -1257,10 +1259,19 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
if (typeof data.moveFlag0 === "boolean") entity.falling = data.moveFlag0;
|
||||
if (typeof data.moveFlag1 === "boolean") entity.jetting = data.moveFlag1;
|
||||
|
||||
// Item velocity interpolation.
|
||||
// Item physics state.
|
||||
if (entity.type === "Item") {
|
||||
// mStatic: sent via InitialUpdateMask. Static items (flags at home)
|
||||
// skip all physics in Item::processTick.
|
||||
if (typeof data.isStatic === "boolean") {
|
||||
entity.isStaticItem = data.isStatic;
|
||||
// When server sets mStatic=true, force atRest (matching Item::onAdd).
|
||||
if (data.isStatic) {
|
||||
entity.itemPhysics = undefined;
|
||||
}
|
||||
}
|
||||
const atRest = data.atRest as boolean | undefined;
|
||||
if (atRest === false && isVec3Like(data.velocity)) {
|
||||
if (atRest === false && !entity.isStaticItem && isVec3Like(data.velocity)) {
|
||||
const vel = data.velocity as Vec3;
|
||||
entity.itemPhysics = {
|
||||
velocity: [vel.x, vel.y, vel.z],
|
||||
|
|
@ -1587,7 +1598,10 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
const dt = TICK_DURATION_MS / 1000;
|
||||
for (const entity of this.entities.values()) {
|
||||
const phys = entity.itemPhysics;
|
||||
if (!phys || phys.atRest || !entity.position) continue;
|
||||
// Binary-verified: Item::processTick skips physics when
|
||||
// mStatic || mAtRest || isHidden. We check static and atRest.
|
||||
if (!phys || phys.atRest || entity.isStaticItem || !entity.position)
|
||||
continue;
|
||||
const v = phys.velocity;
|
||||
const p = entity.position;
|
||||
|
||||
|
|
@ -1604,7 +1618,6 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
const groundZ = getTerrainHeightAt(p[0], p[1]);
|
||||
if (groundZ != null && p[2] < groundZ) {
|
||||
p[2] = groundZ;
|
||||
// Flat-normal collision response (normal = [0,0,1]).
|
||||
const bd = Math.abs(v[2]);
|
||||
// Friction: reduce horizontal velocity.
|
||||
const friction = bd * 0.6;
|
||||
|
|
|
|||
|
|
@ -77,11 +77,6 @@ const FIRST_DYNAMIC_ID = 1027;
|
|||
|
||||
let _nextEntityId = FIRST_DYNAMIC_ID;
|
||||
|
||||
/** Reset the entity ID counter (e.g. on mission/recording change). */
|
||||
export function resetEntityIdCounter(): void {
|
||||
_nextEntityId = FIRST_DYNAMIC_ID;
|
||||
}
|
||||
|
||||
/** Allocate the next sequential entity ID, mimicking Torque's registerObject. */
|
||||
export function allocateEntityId(): string {
|
||||
return String(_nextEntityId++);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue