feat: unify life-sim play controls
This commit is contained in:
@@ -106,6 +106,15 @@ export interface OfficeMinimapOptions {
|
||||
maxPixelRatio?: number;
|
||||
}
|
||||
|
||||
export interface OfficeMinimapPlayer {
|
||||
levelId: string;
|
||||
x: number;
|
||||
z: number;
|
||||
/** Radians in office X/Z space; zero faces local north (-Z). */
|
||||
headingRad: number;
|
||||
kind: "humanoid" | "anonymous-dog";
|
||||
}
|
||||
|
||||
export interface OfficeMinimap {
|
||||
/** The widget. The caller inserts it into its own container and sizes it in CSS. */
|
||||
canvas: HTMLCanvasElement;
|
||||
@@ -145,6 +154,7 @@ export interface OfficeMinimap {
|
||||
* heading until they have taken a step.
|
||||
*/
|
||||
setRobots(robots: readonly PlanRobot[]): void;
|
||||
setPlayer(player: OfficeMinimapPlayer | null): void;
|
||||
/** Call from the stage tick. Cheap by construction — see the file header. */
|
||||
tick(): void;
|
||||
/** Re-do the backing store at the current size and re-rasterise the plan. */
|
||||
@@ -271,6 +281,7 @@ export function createOfficeMinimap(options: OfficeMinimapOptions): OfficeMinima
|
||||
*/
|
||||
let level: LevelPlan | null = plan.levels[0] ?? null;
|
||||
let activeViewId: string | null = null;
|
||||
let player: OfficeMinimapPlayer | null = null;
|
||||
/** Occupied seats on this storey: x, y device pixels per person, laid out once. */
|
||||
let occupiedPx = new Float64Array(0);
|
||||
/** Seat id -> label, for the hover readout. Every seat in the building, not just this storey. */
|
||||
@@ -927,6 +938,29 @@ export function createOfficeMinimap(options: OfficeMinimapOptions): OfficeMinima
|
||||
}
|
||||
}
|
||||
|
||||
function drawPlayer(ctx: Ctx) {
|
||||
if (!player || !level || player.levelId !== level.id) return;
|
||||
const x = toPxX(player.x);
|
||||
const y = toPxY(player.z);
|
||||
const nx = -Math.sin(player.headingRad);
|
||||
const ny = -Math.cos(player.headingRad);
|
||||
const sx = -ny;
|
||||
const sy = nx;
|
||||
const r = (player.kind === "anonymous-dog" ? 3.6 : 4.2) * dpr;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, r + 2.5 * dpr, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = theme.viewpointActive;
|
||||
ctx.lineWidth = 1.2 * dpr;
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + nx * r * 1.35, y + ny * r * 1.35);
|
||||
ctx.lineTo(x - nx * r * 0.65 + sx * r * 0.65, y - ny * r * 0.65 + sy * r * 0.65);
|
||||
ctx.lineTo(x - nx * r * 0.65 - sx * r * 0.65, y - ny * r * 0.65 - sy * r * 0.65);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = theme.viewpointActive;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function drawPing(ctx: Ctx, now: number) {
|
||||
if (pinging === 0) return;
|
||||
const t = (now - pinging) / PING_MS;
|
||||
@@ -960,6 +994,7 @@ export function createOfficeMinimap(options: OfficeMinimapOptions): OfficeMinima
|
||||
// want to see; the camera is the thing you want to see over everything, and
|
||||
// that has been the order here since the widget was one function.
|
||||
drawRobots(ctx);
|
||||
drawPlayer(ctx);
|
||||
crosshair(ctx, toPxX(controls.target.x), toPxY(controls.target.z), theme.target, 5 * dpr);
|
||||
drawCamera(ctx);
|
||||
if (pendingX >= 0) crosshair(ctx, pendingX, pendingY, theme.pending, 7 * dpr);
|
||||
@@ -1356,6 +1391,16 @@ export function createOfficeMinimap(options: OfficeMinimapOptions): OfficeMinima
|
||||
dirty = true;
|
||||
},
|
||||
|
||||
setPlayer(next) {
|
||||
if (
|
||||
player?.levelId === next?.levelId && player?.x === next?.x &&
|
||||
player?.z === next?.z && player?.headingRad === next?.headingRad &&
|
||||
player?.kind === next?.kind
|
||||
) return;
|
||||
player = next ? { ...next } : null;
|
||||
dirty = true;
|
||||
},
|
||||
|
||||
tick() {
|
||||
if (!ready || !viewCtx) return;
|
||||
const now = performance.now();
|
||||
|
||||
Reference in New Issue
Block a user