1
0

feat: add private profile media and realtime contracts

This commit is contained in:
2026-08-11 19:21:49 -07:00
parent a2a52bfdae
commit 1c37ef8f3e
22 changed files with 2321 additions and 8 deletions
+83 -8
View File
@@ -69,6 +69,14 @@ import {
type JourneyEvent,
type JourneyState,
} from "./journey/index.ts";
import {
actorKindForPresence,
createDefaultLocalProfile,
loadLocalProfile,
resolveHumanoidAppearance,
saveLocalProfile,
type LocalProfile,
} from "./profile/index.ts";
/**
* Three type-only imports and not one value among them, which is what keeps the
* office and the instruments out of the entry chunk.
@@ -118,6 +126,12 @@ let journey: JourneyState = restoredJourney.status === "loaded"
profile: { displayName: "Guest" },
},
});
const LOCAL_PROFILE_KEY = "tera:profile:v2";
let localProfile: LocalProfile | null = null;
function humanoidAppearance() {
return localProfile ? resolveHumanoidAppearance(localProfile.appearance) : null;
}
function dispatchJourney(event: JourneyEvent): boolean {
const next = journeyReducer(journey, event);
@@ -801,7 +815,7 @@ async function mountCity(id: string) {
const handle = await createScene(stage, {
city: entry.city,
actor: {
kind: access.subject === null ? "crow" : "humanoid",
kind: actorKindForPresence(access.subject !== null, "outdoors"),
identity: {
id: access.subject ?? "anonymous",
displayName: access.subject ?? "Guest",
@@ -809,11 +823,22 @@ async function mountCity(id: string) {
profile: {
appearance: access.subject === null
? { primaryColor: "#11151a", accentColor: "#f2b134" }
: { primaryColor: "#151a20", accentColor: "#f2b134" },
: (() => {
const appearance = humanoidAppearance();
return appearance
? {
skinTone: appearance.skinTone,
primaryColor: appearance.outfitColor,
accentColor: appearance.accentColor,
hairColor: appearance.hairColor,
bodyShape: appearance.bodyShape,
}
: { primaryColor: "#151a20", accentColor: "#f2b134" };
})(),
},
},
mode: access.subject === null ? "flight" : "ground",
position: access.subject === null ? { y: 500 } : { y: 0 },
position: access.subject === null ? { y: 1_200 } : { y: 0 },
minFlightAltitude: 20,
maxFlightAltitude: 1_500,
...(access.subject === null
@@ -1010,6 +1035,7 @@ async function mountCity(id: string) {
*/
requestAnimationFrame(function pumpMinimap() {
requestAnimationFrame(pumpMinimap);
syncJourneyVehicle(performance.now());
// Only the mounted one. The other is still constructed and still holds a live
// camera, but its canvas is out of the document, so `clientWidth` is 0, every
// `resize()` puts it back to `ready = false`, and ticking it would be a
@@ -1034,6 +1060,8 @@ requestAnimationFrame(function pumpMinimap() {
* subtraction on the frames it skips.
*/
let livenessCheckedAt = 0;
let journeySyncedAt = 0;
let previousVehicleProgress: number | null = null;
function pollLiveness() {
const now = performance.now();
if (now - livenessCheckedAt < 1000) return;
@@ -1041,6 +1069,32 @@ function pollLiveness() {
renderSource();
}
/** Persist route progress cheaply and turn the hero's wrap into a real scale door. */
function syncJourneyVehicle(now: number): void {
if (!routeDriveIsActive() || !city || journey.route === null || journey.vehicle === null) {
previousVehicleProgress = null;
return;
}
const vehicle = city.vehicleState();
if (!vehicle) return;
const progress = vehicle.progress;
const arrived = journey.route.direction === 1
? previousVehicleProgress !== null && previousVehicleProgress > 0.94 && progress < 0.06
: previousVehicleProgress !== null && previousVehicleProgress < 0.06 && progress > 0.94;
previousVehicleProgress = progress;
if (arrived) {
const endpoint = journey.route.direction === 1 ? "san-francisco" : "los-angeles";
dispatchJourney({ type: "reach-route-endpoint", endpoint });
dispatchJourney({ type: "exit-vehicle" });
switchCity(endpoint === "san-francisco" ? "sf" : "socal");
return;
}
if (now - journeySyncedAt < 500) return;
journeySyncedAt = now;
dispatchJourney({ type: "update-route-progress", progress });
}
// ---- Office ---------------------------------------------------------------
/**
@@ -1108,9 +1162,9 @@ async function enterOffice() {
z: -Math.cos(pack.viewpoints[0].focus.rotation),
}
: { x: 0, z: -1 },
actor: access.subject === null
actor: actorKindForPresence(access.subject !== null, "office") === "dog"
? { kind: "anonymous-dog", coatColor: 0x17191c, collarColor: 0xf2b134 }
: { kind: "humanoid", outfitColor: 0x151a20, accentColor: 0xf2b134 },
: { kind: "humanoid", ...(humanoidAppearance() ?? { outfitColor: 0x151a20, accentColor: 0xf2b134 }) },
},
// Only when there is no sky to put behind it. A sited office computes a
// gradient and a horizon; painting the old flat colour over that is the
@@ -1593,6 +1647,9 @@ function renderLegend() {
renderOfficeBadge();
if (driveControls) driveControls.hidden = !routeDriveIsActive();
if (walkControls) walkControls.hidden = !(walking || exploring);
for (const control of walkControls?.querySelectorAll<HTMLElement>(".flight-only") ?? []) {
control.hidden = inside;
}
if (driveHint) driveHint.hidden = inside || cityId !== "california";
if (walkHint) walkHint.hidden = !(inside || city.actorState());
}
@@ -2091,7 +2148,8 @@ function publishCityActorActions(): boolean {
right: (heldDriveKeys.has("d") ? 1 : 0) - (heldDriveKeys.has("a") ? 1 : 0),
turn: (heldDriveKeys.has("d") ? 1 : 0) - (heldDriveKeys.has("a") ? 1 : 0),
sprint: heldDriveKeys.has(" "),
climb: heldDriveKeys.has(" ") ? 1 : 0,
climb: (heldDriveKeys.has("e") || heldDriveKeys.has(" ") ? 1 : 0) -
(heldDriveKeys.has("q") ? 1 : 0),
});
return true;
}
@@ -2232,7 +2290,10 @@ window.addEventListener("keydown", (event) => {
return;
}
const lower = event.key.toLowerCase();
if (lower === "w" || lower === "a" || lower === "s" || lower === "d" || event.key === " ") {
if (
lower === "w" || lower === "a" || lower === "s" || lower === "d" ||
lower === "q" || lower === "e" || event.key === " "
) {
heldDriveKeys.add(event.key === " " ? " " : lower);
if (publishVehicleActions()) {
event.preventDefault();
@@ -2698,6 +2759,17 @@ async function boot() {
if (bootStep) bootStep.textContent = "Asking the deployment who you are…";
access = await resolveAccess();
if (access.subject !== null) {
const loadedProfile = loadLocalProfile(sessionStorage, LOCAL_PROFILE_KEY);
localProfile = loadedProfile.status === "loaded"
? loadedProfile.profile
: createDefaultLocalProfile(access.subject, access.subject);
if (loadedProfile.status !== "loaded" || loadedProfile.migrated) {
saveLocalProfile(sessionStorage, LOCAL_PROFILE_KEY, localProfile);
}
} else {
localProfile = null;
}
dispatchJourney({
type: "sign-in-actor-swap",
actor: access.subject === null
@@ -2711,7 +2783,10 @@ async function boot() {
id: access.subject,
kind: "humanoid",
signedIn: true,
profile: { displayName: access.subject },
profile: {
displayName: localProfile?.displayName ?? access.subject,
color: humanoidAppearance()?.accentColor,
},
},
});
applyTimeControl();