Add player onboarding and tactical viewer updates
This commit is contained in:
@@ -1,20 +1,18 @@
|
||||
import * as THREE from "three";
|
||||
import {
|
||||
completeMarqueeSelection,
|
||||
hideMarqueeBox,
|
||||
pickSelectableHitAtClientPosition,
|
||||
pickSelectableAtClientPosition,
|
||||
updateHoverLabel,
|
||||
updateMarqueeBox,
|
||||
} from "./viewerInteraction";
|
||||
import {
|
||||
applyKeyboardControl,
|
||||
cycleStatsOverlayMode,
|
||||
toggleCameraMode,
|
||||
navigateFromWheel,
|
||||
} from "./viewerControls";
|
||||
import { NAV_DISTANCE, NAV_DISTANCE_PLANET_ORBIT, NAV_DISTANCE_SHIP_HULL } from "./viewerConstants";
|
||||
import { MAX_CAMERA_DISTANCE, MIN_CAMERA_DISTANCE, NAV_DISTANCE_PLANET_ORBIT } from "./viewerConstants";
|
||||
import { ViewerHistoryWindowController } from "./viewerHistoryWindowController";
|
||||
import type { ViewerHudState } from "./viewerHudState";
|
||||
import type { StatsOverlayMode, ViewerHudState } from "./viewerHudState";
|
||||
import type {
|
||||
CameraMode,
|
||||
DragMode,
|
||||
@@ -61,88 +59,128 @@ export interface ViewerInteractionContext {
|
||||
getFollowCameraPosition: () => THREE.Vector3;
|
||||
getFollowCameraFocus: () => THREE.Vector3;
|
||||
screenPointFromClient: (clientX: number, clientY: number) => THREE.Vector2;
|
||||
applyOrbitDelta: (delta: THREE.Vector2) => void;
|
||||
applyPanDelta: (delta: THREE.Vector2) => void;
|
||||
syncFollowStateFromSelection: () => void;
|
||||
updatePanels: () => void;
|
||||
focusOnSelection: (selection: Selectable) => void;
|
||||
updateGamePanel: (mode: string) => void;
|
||||
openOrderContextMenu: (x: number, y: number, target: ViewerOrderContextMenuTarget) => void;
|
||||
closeOrderContextMenu: () => void;
|
||||
getStatsOverlayMode: () => StatsOverlayMode;
|
||||
setStatsOverlayMode: (mode: StatsOverlayMode) => void;
|
||||
refreshStatsOverlay: () => void;
|
||||
historyController: ViewerHistoryWindowController;
|
||||
}
|
||||
|
||||
export class ViewerInteractionController {
|
||||
private readonly activePointers = new Map<number, THREE.Vector2>();
|
||||
private pinchStartDistance?: number;
|
||||
private pinchStartZoom?: number;
|
||||
private pinchLastCenter?: THREE.Vector2;
|
||||
|
||||
constructor(private readonly context: ViewerInteractionContext) {}
|
||||
|
||||
readonly onPointerDown = (event: PointerEvent) => {
|
||||
if (event.button === 1) {
|
||||
this.context.setDragMode("orbit");
|
||||
this.context.setDragPointerId(event.pointerId);
|
||||
this.context.dragLast.copy(this.context.screenPointFromClient(event.clientX, event.clientY));
|
||||
this.context.renderer.domElement.setPointerCapture(event.pointerId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.context.setDragMode("marquee");
|
||||
this.context.setDragPointerId(event.pointerId);
|
||||
this.context.dragStart.copy(this.context.screenPointFromClient(event.clientX, event.clientY));
|
||||
this.context.dragLast.copy(this.context.dragStart);
|
||||
this.context.setMarqueeActive(false);
|
||||
const point = this.context.screenPointFromClient(event.clientX, event.clientY);
|
||||
this.activePointers.set(event.pointerId, point);
|
||||
this.context.renderer.domElement.setPointerCapture(event.pointerId);
|
||||
|
||||
if (this.activePointers.size >= 2) {
|
||||
const gesture = this.getPinchGesture();
|
||||
if (!gesture) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.context.setSuppressClickSelection(true);
|
||||
this.context.setDragMode("pinch");
|
||||
this.context.setDragPointerId(event.pointerId);
|
||||
this.pinchStartDistance = gesture.distance;
|
||||
this.pinchStartZoom = this.context.getDesiredDistance();
|
||||
this.pinchLastCenter = gesture.center;
|
||||
return;
|
||||
}
|
||||
|
||||
this.context.setDragMode("pan");
|
||||
this.context.setDragPointerId(event.pointerId);
|
||||
this.context.dragStart.copy(point);
|
||||
this.context.dragLast.copy(point);
|
||||
};
|
||||
|
||||
readonly onPointerMove = (event: PointerEvent) => {
|
||||
this.updateHoverLabel(event);
|
||||
|
||||
const point = this.context.screenPointFromClient(event.clientX, event.clientY);
|
||||
if (this.activePointers.has(event.pointerId)) {
|
||||
this.activePointers.set(event.pointerId, point);
|
||||
}
|
||||
|
||||
if (this.context.getDragPointerId() !== event.pointerId || !this.context.getDragMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const point = this.context.screenPointFromClient(event.clientX, event.clientY);
|
||||
if (this.context.getDragMode() === "orbit") {
|
||||
const delta = point.clone().sub(this.context.dragLast);
|
||||
this.context.dragLast.copy(point);
|
||||
this.context.applyOrbitDelta(delta);
|
||||
if (this.context.getDragMode() === "pinch") {
|
||||
const gesture = this.getPinchGesture();
|
||||
if (!gesture || !this.pinchStartDistance || !this.pinchStartZoom || !this.pinchLastCenter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zoomRatio = THREE.MathUtils.clamp(gesture.distance / this.pinchStartDistance, 0.25, 4);
|
||||
this.context.setDesiredDistance(THREE.MathUtils.clamp(
|
||||
this.pinchStartZoom / zoomRatio,
|
||||
MIN_CAMERA_DISTANCE,
|
||||
MAX_CAMERA_DISTANCE,
|
||||
));
|
||||
const centerDelta = gesture.center.clone().sub(this.pinchLastCenter);
|
||||
this.pinchLastCenter = gesture.center;
|
||||
this.context.applyPanDelta(centerDelta);
|
||||
return;
|
||||
}
|
||||
|
||||
const delta = point.clone().sub(this.context.dragLast);
|
||||
const dragDistance = point.distanceTo(this.context.dragStart);
|
||||
if (!this.context.getMarqueeActive() && dragDistance > 8) {
|
||||
this.context.setMarqueeActive(true);
|
||||
if (dragDistance > 6) {
|
||||
this.context.setSuppressClickSelection(true);
|
||||
this.context.hudState.marquee.visible = true;
|
||||
this.context.marqueeEl.style.display = "block";
|
||||
}
|
||||
|
||||
if (!this.context.getMarqueeActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.context.dragLast.copy(point);
|
||||
updateMarqueeBox(this.context.hudState.marquee, this.context.marqueeEl, this.context.dragStart, this.context.dragLast);
|
||||
this.context.applyPanDelta(delta);
|
||||
};
|
||||
|
||||
readonly onPointerUp = (event: PointerEvent) => {
|
||||
if (this.context.getDragPointerId() !== event.pointerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.context.renderer.domElement.hasPointerCapture(event.pointerId)) {
|
||||
this.context.renderer.domElement.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
this.activePointers.delete(event.pointerId);
|
||||
|
||||
if (this.context.getDragMode() === "marquee" && this.context.getMarqueeActive()) {
|
||||
this.completeMarqueeSelection();
|
||||
hideMarqueeBox(this.context.hudState.marquee, this.context.marqueeEl);
|
||||
if (this.activePointers.size >= 2) {
|
||||
const gesture = this.getPinchGesture();
|
||||
if (gesture) {
|
||||
this.context.setDragMode("pinch");
|
||||
this.pinchStartDistance = gesture.distance;
|
||||
this.pinchStartZoom = this.context.getDesiredDistance();
|
||||
this.pinchLastCenter = gesture.center;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.context.setDragMode(undefined);
|
||||
this.context.setDragPointerId(undefined);
|
||||
this.context.setMarqueeActive(false);
|
||||
const remainingPointer = this.activePointers.entries().next();
|
||||
if (!remainingPointer.done) {
|
||||
const [pointerId, point] = remainingPointer.value;
|
||||
this.context.setDragMode("pan");
|
||||
this.context.setDragPointerId(pointerId);
|
||||
this.context.dragStart.copy(point);
|
||||
this.context.dragLast.copy(point);
|
||||
} else {
|
||||
this.context.setDragMode(undefined);
|
||||
this.context.setDragPointerId(undefined);
|
||||
}
|
||||
|
||||
this.pinchStartDistance = undefined;
|
||||
this.pinchStartZoom = undefined;
|
||||
this.pinchLastCenter = undefined;
|
||||
};
|
||||
|
||||
readonly onClick = (event: MouseEvent) => {
|
||||
@@ -225,8 +263,7 @@ export class ViewerInteractionController {
|
||||
this.context.setSelectedItems([{ kind: "ship", id: shipId }]);
|
||||
this.context.syncFollowStateFromSelection();
|
||||
this.context.focusOnSelection({ kind: "ship", id: shipId });
|
||||
this.toggleCameraMode("follow");
|
||||
this.context.setDesiredDistance(NAV_DISTANCE_SHIP_HULL);
|
||||
this.toggleCameraMode("tactical");
|
||||
this.context.updatePanels();
|
||||
this.context.updateGamePanel("Live");
|
||||
return;
|
||||
@@ -268,8 +305,7 @@ export class ViewerInteractionController {
|
||||
}
|
||||
|
||||
if (selection.kind === "ship") {
|
||||
this.toggleCameraMode("follow");
|
||||
this.context.setDesiredDistance(NAV_DISTANCE_SHIP_HULL);
|
||||
this.toggleCameraMode("tactical");
|
||||
this.context.updatePanels();
|
||||
this.context.updateGamePanel("Live");
|
||||
return;
|
||||
@@ -288,6 +324,13 @@ export class ViewerInteractionController {
|
||||
}
|
||||
|
||||
const key = event.key.toLowerCase();
|
||||
if (key === "f10") {
|
||||
event.preventDefault();
|
||||
this.context.setStatsOverlayMode(cycleStatsOverlayMode(this.context.getStatsOverlayMode()));
|
||||
this.context.refreshStatsOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
const controlState = applyKeyboardControl({
|
||||
keyState: this.context.keyState,
|
||||
cameraMode: this.context.getCameraMode(),
|
||||
@@ -371,17 +414,17 @@ export class ViewerInteractionController {
|
||||
);
|
||||
}
|
||||
|
||||
private completeMarqueeSelection() {
|
||||
const selection = completeMarqueeSelection({
|
||||
renderer: this.context.renderer,
|
||||
systemCamera: this.context.systemCamera,
|
||||
dragStart: this.context.dragStart,
|
||||
dragLast: this.context.dragLast,
|
||||
systemSelectableTargets: this.context.systemSelectableTargets,
|
||||
});
|
||||
this.context.setSelectedItems(selection);
|
||||
this.context.syncFollowStateFromSelection();
|
||||
this.context.updatePanels();
|
||||
private getPinchGesture() {
|
||||
const points = [...this.activePointers.values()];
|
||||
if (points.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [first, second] = points;
|
||||
return {
|
||||
center: first.clone().add(second).multiplyScalar(0.5),
|
||||
distance: first.distanceTo(second),
|
||||
};
|
||||
}
|
||||
|
||||
private shouldFocusSelectionOnClick(selection: Selectable) {
|
||||
|
||||
Reference in New Issue
Block a user