/** * Portals Guardian avatar SDK — TypeScript declarations. * * Stamped into every published bundle as a pair. Include the script exactly * like `_portals/sdk.js`: * * * * * * const { PortalsAvatars } = await PortalsGuardians.ready(); * * or, from a module, through the bare specifier the managed import map * provides: * * import { PortalsAvatars } from '@portals/avatars'; * * Both reach the same ES module at `_portals/guardians-sdk.module.js`. * `three` is a peer: it resolves through the same import map to the managed * runtime at `_portals/vendor/three/three.module.js`, so the page has exactly * one Three.js instance. * * Hand-authored, like portals.d.ts. `guardiansSdkSource.test.ts` fails if the * built bundle exports a name this file does not declare. */ /** Global installed by the `_portals/guardians-sdk.js` classic script. */ declare const PortalsGuardians: { /** * Resolves with the SDK's module namespace. Safe to call repeatedly — the * module is fetched once and the promise is shared. */ ready(): Promise; /** True once `ready()` has resolved and the exports are on this object. */ loaded: boolean; /** Absolute URL of the ES module this loader pulls in. */ moduleUrl: string; version: string; } & Partial; declare module '@portals/avatars' { import type * as THREE from 'three'; /** * The managed Three.js, re-exported. A game loaded through the classic * script has no other way to reach it, and taking it from here guarantees * there is exactly one instance on the page. */ export * as THREE from 'three'; export const GUARDIANS_SDK_VERSION: string; // ------------------------------------------------------------------------- // Core types // ------------------------------------------------------------------------- export type BodyType = 'male' | 'female'; export type WearableSlot = | 'hat' | 'glasses' | 'neck' | 'top' | 'sleeves' | 'gloves' | 'left_hand' | 'right_hand' | 'back' | 'bottom' | 'shoes' | 'full_body' | 'not_interact'; /** Behaviour class for `right_hand` items; drives the carry pose and use action. */ export type HandItemType = 'carry' | 'melee' | 'throwable' | 'gun' | 'gun2h'; export type CameraMode = 'third' | 'first'; /** See `ControllerOptions.strafe`. */ export type StrafeMode = 'off' | 'always' | 'aim'; /** Which take a diagonal collapses onto — see `StrafeTuning.collapse`. */ export type StrafeCollapse = 'off' | 'hip' | 'aim' | 'auto'; /** * Shooter-grade strafe selection. Every field is off by default, so a game * that does not set `strafeTuning` animates exactly as it always has. */ export interface StrafeTuning { /** * Radians the body turns to face a FORWARD diagonal instead of crabbing * along it. π/4 makes a diagonal run read as an angled run. Read the live * value off `CharacterController.diagonalYaw`. Needs `collapse` on `'hip'` * or `'auto'` to have an effect. */ diagonalTurn?: number; /** * `'aim'` folds every diagonal onto the pure sidestep; `'hip'` sends the * forward diagonals to the plain forward clip and the back diagonals to the * sidestep; `'auto'` is `'aim'` while aiming and `'hip'` otherwise. */ collapse?: StrafeCollapse; /** * Keep the strafe lock while sprinting and cap the sprint at run pace in * every direction but straight ahead and the forward diagonals. */ sprintCap?: boolean; /** Dead band (2.0 / 2.6 m/s) on the walk/run clip boundary. */ hysteresis?: boolean; } /** Local transform applied when a rigid wearable is attached to a bone. */ export interface WearableOffset { position?: [number, number, number]; rotation?: [number, number, number]; scale?: number; } /** Ragdoll feel — see `RAGDOLL_DEFAULTS` for what each field does. */ export interface RagdollTuning { gravity?: number; knockback?: number; headshotKnockback?: number; lift?: number; spread?: number; woundRadius?: number; momentum?: number; drag?: number; groundFriction?: number; limpTime?: number; limpFloor?: number; bounce?: number; iterations?: number; maxSubsteps?: number; maxSpeed?: number; settleDrift?: number; settleTime?: number; maxTime?: number; } export interface RagdollOptions { tuning?: RagdollTuning; /** `SceneCollider.getGroundHeight` — what a body lands on. */ getGroundHeight?: (x: number, z: number, y?: number, radius?: number) => number; /** `SceneCollider.raycast` — what keeps a thrown body out of walls. */ raycast?: (origin: THREE.Vector3, direction: THREE.Vector3, maxDistance: number) => RaycastHit | null; /** Half-width of the box bodies are kept inside; omit for a map with a void. */ bounds?: number | null; } export interface ShotKickParams { from: THREE.Vector3; point: THREE.Vector3; head?: boolean; tuning?: RagdollTuning; } /** Spread straight into `Ragdoll.start()`. */ export interface ShotKick { impulse: THREE.Vector3; point: THREE.Vector3; } /** The three gun stances `loadGunPoses` registers. */ export type GunPoseName = 'adsPose' | 'hipPose' | 'heavyPose'; /** * Per-stance gun placement in the hand, for `aimWeapon`'s `nudge` and `tilt`. * Keyed by the stance being held; `down` is the carry with no stance overlay * (`pose` null). Rows are `[x, y, z]` in the WEAPON's own frame: metres for a * nudge (x right, y up, z back along the barrel), Euler XYZ radians for a * tilt. A missing row means no adjustment for that stance. */ export type GunPoseTable = Partial>; export interface LoadGunPosesOptions { /** Log a warning when the clip set is unavailable. Default true. */ warn?: boolean; } /** How much of the camera's pitch the body takes on — see `AIM_BEND_DEFAULTS`. */ export interface AimBendTuning { follow?: number; limit?: number; hipSpineShare?: number; } /** The two-handed hip carry — see `HEAVY_CARRY` for what each field does. */ export interface CarryTuning { twist?: number; tuck?: number; reach?: number; hug?: number; elbowLift?: number; elbowBack?: number; kickSpine?: number; kickShoulder?: number; kickElbow?: number; kickBarrel?: number; } /** * Where the same two-handed weapon goes when it is SHOULDERED — see * `SCOPE_DEFAULTS`. A superset of `CarryTuning`: the raise walks each carry * number toward this table's copy of it, so spread a carry into a custom one * (`{ ...RAIL_CARRY, ...yours }`) rather than writing it from scratch. */ export interface ScopeTuning extends CarryTuning { /** Exponential rate, per second, the weapon comes up and goes down at. */ blend?: number; /** Where the trigger hand welds, in metres off the Head bone, camera frame. */ hand?: { forward: number; right: number; up: number }; /** Support-hand target in the WEAPON's frame, shouldered. */ foregrip?: [number, number, number]; /** Radians the clavicle lifts into the stock — the pocket the butt sits in. */ shoulderLift?: number; /** Radians the chin drops onto the cheek plate. */ headTilt?: number; /** Share of the aim the torso does NOT take that the neck makes up. */ headFollow?: number; } /** What a shot does to the gun and the arm — see `RECOIL_DEFAULTS`. */ export interface RecoilTuning { gunKickBack?: number; gunKickArm?: number; gunKickForearm?: number; } export interface AimWeaponParams { /** The stance being held, from `wantedPose`. Null leaves the body alone. */ pose: GunPoseName | null; /** The CAMERA's heading, radians — not the body's. */ lookYaw: number; /** Camera elevation, radians, positive looking up. */ pitch: number; /** `CharacterController.diagonalYaw`. */ diagYaw?: number; /** 0..1 decay of the current shot's jolt, the game's own to drive. */ kick?: number; carry?: Required; bend?: Required; recoil?: Required; /** Name of the empty marking the barrel tip. Default `'muzzle'`. */ muzzleNode?: string; /** Support-hand target in the WEAPON's frame. Default `HEAVY_FOREGRIP`. */ foregrip?: [number, number, number]; /** * Where each stance wants the gun IN THE HAND, over the equip-time grip — * see `GunPoseTable`. Applied to the wearable holder every frame (at kick 0 * too, so the recoil slide returns to the nudged rest). Pass the same table * at both ends of the wire, like every other posing input here. */ nudge?: GunPoseTable; /** * ...and how each stance wants the gun TURNED in the grip, over the * equip-time rotation. The nudge's frame and the recoil slide both read the * tilted orientation. On a RAISED weapon the barrel is re-aimed through the * wrist afterwards, so pitch/yaw read as the gun sitting differently in the * grip while the barrel stays on the crosshair; only the `down` carry (and * roll everywhere) shows raw. */ tilt?: GunPoseTable; /** * Bring the waist carry UP to the eye — the shouldered scope pose. Default * false. Only `heavyPose` reads it; drive it from the same flag at both ends * of the wire, or a peer sniping you still holds the weapon at their hip. */ scoped?: boolean; /** Where the shouldered weapon goes. Default `SCOPE_DEFAULTS`. */ scope?: Required; /** * Which caller is posing this body, when more than one does in a frame * (a killcam rewind alongside the live loop). Default `'live'`. The raise is * eased per avatar AND per driver, or one advances it and the other does not. */ driver?: string; /** Seconds since this driver's last call. Omit and the raise reads its own clock. */ dt?: number; } /** Shared by `muzzleWorld` and `shotOrigin`. */ export interface MuzzleWorldOptions { /** Name of the empty marking the barrel tip. Default `'muzzle'`. */ muzzleNode?: string; /** How far from the feet a muzzle may be before it is not this body's gun, m. Default 3. */ maxReach?: number; /** Fallback height above the feet when there is no barrel to read. Default 1.16. */ chestHeight?: number; } /** Everything `shotOrigin` needs. */ export interface ShotOriginParams extends MuzzleWorldOptions { /** The world's ray test — pass `SceneCollider.raycast`. */ raycast: ( origin: THREE.Vector3, direction: THREE.Vector3, maxDistance: number ) => RaycastHit | null; /** `CharacterController.crouching`. Lowers the fallback under a low ledge. */ crouching?: boolean; /** Fallback height above the feet while crouched. Default 0.82. */ crouchChestHeight?: number; } export type AnimationName = | 'idle' | 'walk' | 'run' | 'sprint' | 'jump' | 'fall' | 'land' | (string & {}); /** Declarative avatar appearance. Every field is optional. */ export interface GuardianConfig { bodyType?: BodyType; skinColor?: string | number; hairStyle?: string | null; hairColor?: string | number; eyeColor?: string | number; facialHair?: string | null; hiddenParts?: string[]; wearables?: string[]; } export interface AvatarLoadOptions extends GuardianConfig { modelUrl?: string; /** Preserve an override model's authored materials, visibility, eye treatment, and geometry. */ preserveModelAppearance?: boolean; /** Override whether this avatar receives the protected basic outfit. */ defaultOutfit?: boolean; position?: THREE.Vector3 | { x: number; y: number; z: number }; rotationY?: number; shadows?: boolean; /** Clamp model height to [1, 1.6] m, matching the Unity client. Default true. */ normalizeHeight?: boolean; eyeTextures?: { diffuse: string; mask: string }; } /** One public wearable in the saved `/avatar` look. */ export interface PortalsPlayerAvatarWearable { itemId: string; name: string; slots: WearableSlot[]; glb: string; removesHair: boolean; } /** A selected full-avatar replacement in the saved `/avatar` look. */ export interface PortalsPlayerFullAvatar { itemId: string; name: string; glb: string; } export interface PortalsPlayerAvatar { config: { bodyType: BodyType; skinColor: string; hairColor: string; hairStyle: string; }; customAvatar: PortalsPlayerFullAvatar | null; wearables: PortalsPlayerAvatarWearable[]; } /** Current public profile returned by `Portals.player.get()`. */ export interface PortalsPlayerProfile { playerId: string | null; displayName: string | null; avatarUrl: string | null; username: string | null; avatar: PortalsPlayerAvatar | null; } export type PlayerAvatarLoadOptions = Omit< AvatarLoadOptions, keyof GuardianConfig | 'modelUrl' | 'preserveModelAppearance' | 'defaultOutfit' >; /** * A wearable. `id` is yours to choose; set `portalsItemId` only when the * item genuinely came from a player's Portals inventory, which is what lets * an asset on a non-Portals host reach a published game via the item proxy. */ export interface WearableDef { id: string; name: string; slot: WearableSlot; /** All slots occupied, for multi-slot items. Defaults to `[slot]`. */ slots?: WearableSlot[]; /** Portals inventory item id, when this came from a player's inventory. */ portalsItemId?: string; url?: string; /** GLB used on a female Guardian (Portals `SourceUrlFemale`). */ urlFemale?: string; object?: THREE.Object3D; hides?: string[]; hideHair?: boolean; attachBone?: string; offset?: WearableOffset; bodyType?: BodyType; thumbnail?: string; handItemType?: HandItemType; } export interface EquippedWearable { def: WearableDef; slots: WearableSlot[]; root: THREE.Object3D; hiddenMeshes: THREE.Object3D[]; } export interface AnimationSource { name: AnimationName; url: string; /** Clip name inside the GLB; defaults to the first clip. */ clip?: string; loop?: boolean; timeScale?: number; /** Register as a masked overlay layer instead of a full-body action. */ overlay?: 'rightArm' | 'leftArm' | 'upperBody' | RegExp; /** * Overlays only. Take the phase from the base locomotion clip instead of a * free-running clock — the port of Unity's synced animator layer * (`m_SyncedLayerIndex` on the Unity client's arms layer). Needed by any overlay * that moves: an arm swing on its own clock drifts against the stride. */ sync?: boolean; /** * Corrective rotations (Euler XYZ radians) baked into named bones' * keyframes at registration — fixes a clip rather than everything it * holds. Useful when a source take does not suit the rig; a per-item * offset cannot fix a bad grip without * breaking the same item's (correct) idle grip. */ boneAdjust?: Record; } export interface HairStyleInfo { id: string; name: string; node: THREE.Object3D; } export interface RaycastHit { point: { x: number; y: number; z: number }; normal: { x: number; y: number; z: number }; /** * The mesh that was struck, when the caster knows it — `SceneCollider` * reports it; hand-rolled `raycast` hooks may omit it (features that * read it, like `CameraRig.allowedOccluders`, then simply stay off). */ object?: THREE.Object3D; } export interface ControllerOptions { domElement: HTMLElement; camera: THREE.PerspectiveCamera; /** * On-screen touch controls — virtual joystick, drag-to-look with * pinch-to-zoom, semi-transparent action buttons — drawn as a DOM overlay * over the renderer element. * * - `'auto'` (default) — shown on coarse-pointer (touch) devices only. * - `true` / `false` — force on / off. * - a `TouchControlsOptions` object — force on, configured. * * The overlay only drives the controller's public input API * (`setMoveVector`, `setJumpHeld`, `setSprint`, `beginPrimaryAction`, …) — * a game with its own mobile UI passes `false` and calls those itself. * Restyle via the `.pa-touch*` CSS classes; the instance is * `controller.touch`. */ touch?: boolean | 'auto' | TouchControlsOptions; cameraMode?: CameraMode; walkSpeed?: number; runSpeed?: number; sprintSpeed?: number; /** Jump apex height in metres, clamped to `MOVEMENT_LIMITS.jumpHeight`. */ jumpHeight?: number; jumpSpeed?: number; jumpTimer?: number; /** Downward acceleration, m/s². Default 12. The default 1.6 m jump apex is derived from it. */ gravity?: number; /** Terminal fall speed, m/s. Default 12. */ maxFallSpeed?: number; /** * Biggest drop walked down without leaving the ground, metres. Default 0.4 * — keeps a run down a staircase out of the falling clip. */ stepDown?: number; airSpeed?: number; crouchSpeed?: number; /** * Max speed while swimming, m/s (default 1.9 — the swim clip's authored * pace). Swim mode itself is toggled with `controller.setSwimming()`. */ swimSpeed?: number; invertX?: boolean; /** * Strafing — hold the camera's heading and slide, instead of turning to * face the direction of travel. * * - `'off'` (default) — the body turns toward the move direction, so A/D * walk it left/right rather than sidestepping. * - `'always'` — the body keeps the camera's heading and the 8-way * directional clips play according to the direction of travel. * - `'aim'` — free movement until aiming (`setAiming(true)`, or holding a * `gun` / `gun2h` right-hand item). * * First person always strafes regardless of this setting. */ strafe?: StrafeMode; /** * Shooter-grade refinements to how a strafing body is animated and faced. * Every field is off by default — see `StrafeTuning`. Change them at * runtime with `setStrafeTuning()`. */ strafeTuning?: StrafeTuning; rotationSpeed?: number; cameraDistance?: number; cameraHeight?: number; shoulderOffset?: number; /** * Keep the third-person camera out of the level: the boom shortens when * something comes between the character and the camera, and drops to first * person when there is no room left. **On by default** — it reuses the * `raycast` hook below, so a game with no raycaster pays nothing. * * Other avatars never block the view (`SceneCollider` skips skinned * meshes); `userData.noCollide` hides a mesh from the character and the * camera alike, and `cameraCollision.probe` gives the camera its own * geometry. */ cameraCollision?: boolean | CameraCollisionOptions; /** * Restrict how far the camera can be turned — a yaw window, tighter pitch * stops, or an axis locked away from the player. Unrestricted by default * apart from the standing pitch stops. * * ```js * // A side-on stage: the view looks along −Z ±34°, height fixed. * cameraLimits: { minYaw: -0.6, maxYaw: 0.6, lockPitch: true } * ``` */ cameraLimits?: CameraLimitOptions; /** * `y` is the character's current height, so a sampler that cares about * overhangs can report the surface under the character rather than the * topmost one in the column; `radius` the capsule radius, for samplers * that probe like a capsule bottom rather than a point (`SceneCollider` * does). Ignoring either is fine for open levels. */ getGroundHeight?: (x: number, z: number, y?: number, radius?: number) => number; /** * Collide with the other avatars in the world (default true): the * character is pushed back out of any avatar capsule it overlaps, so * players cannot run through NPCs or each other. Wired automatically for * controllers and NPCs created through `PortalsAvatars` (hidden avatars * are skipped). Set false for a ghost — a spectator, a cutscene actor. */ collideWithAvatars?: boolean; autoJump?: boolean; autoJumpDelay?: number; resolveMovement?: ( current: THREE.Vector3, proposed: THREE.Vector3, radius: number ) => THREE.Vector3; /** * Capsule depenetration: return the position (feet) pushed back out of any * geometry the body's capsule overlaps. Runs after the vertical step each * frame — the safety net that keeps a falling character from sinking into * walls, which `resolveMovement`'s swept probes cannot catch. * `SceneCollider.controllerOptions()` supplies it (see its `capsule` * option, default on). */ depenetrate?: ( position: THREE.Vector3, radius: number, height: number ) => THREE.Vector3; /** * The same recovery for a capsule the caller positions: given the two * sphere CENTRES in world space and the radius, return the offset that * lifts the capsule clear. Swim mode uses it for the prone body — flat, * reaching ~0.9 m ahead of and behind the root — which no upright capsule * describes. `SceneCollider.controllerOptions()` supplies it; without it a * swimmer falls back to the upright `depenetrate` capsule. */ depenetrateSegment?: ( start: THREE.Vector3, end: THREE.Vector3, radius: number ) => THREE.Vector3; /** * Enables ledge grabbing. Two rows of rays, one per shoulder, are cast down * from above the head across the character's reach; a hit within 30° of * horizontal is a ledge, and the line between the rows' hits is the edge * the character squares up to. Both rows must hit, and the probe only runs * while airborne, so a climb is always the result of a jump or a fall. * Nothing taller than the grabbed surface may stand between it and the * character, so a wall in front is never climbed through to reach a roof * behind it. Without this hook the character cannot climb at all. */ raycast?: ( origin: THREE.Vector3, direction: THREE.Vector3, maxDistance: number ) => RaycastHit | null; /** * Standing-room check at a prospective ledge target. Make it a VOLUME test: * rays started inside a single-sided mesh see nothing, so rays alone let a * ledge whose top lies inside the neighbouring prop pass — and the mantle * then teleports the body, head included, into it. `SceneCollider` runs a * capsule overlap plus `isUnderSurface` (0.42.0). */ checkClearance?: ( point: { x: number; y: number; z: number }, radius: number, height: number ) => boolean; /** * Key that, HELD for `respawnHoldMs`, calls `controller.requestRespawn()` * — the player's own way out of a spot the physics lost them in. Default * `'KeyR'`; `null` disables the key (0.42.0). */ respawnKey?: string | null; /** How long `respawnKey` must be held, ms (default 800). */ respawnHoldMs?: number; } // ------------------------------------------------------------------------- // Asset resolution // ------------------------------------------------------------------------- export interface ManagedAssetOrigin { prefix: string; hosts: readonly string[]; } /** * Managed same-origin path prefixes and the upstream hosts they front. * Published games run under `connect-src 'self'` and cannot reach the CDNs * directly. */ export const PORTALS_MANAGED_ORIGINS: readonly ManagedAssetOrigin[]; /** Prefix of the item proxy, used for assets on non-Portals hosts. */ export const PORTALS_ITEM_PREFIX: string; export type AssetRewriteMode = 'auto' | 'always' | 'never'; /** * Map an asset URL onto the managed same-origin path a published game can * actually fetch. Relative, `blob:` and `data:` URLs pass through untouched. * Use it for `` thumbnails too — `img-src` is `'self'` as well. */ export function resolveAssetUrl( url: string, options?: { itemId?: string; variant?: 'female' } ): string; /** Override sandbox detection. `'auto'` rewrites only in an Arcade build. */ export function setAssetRewriting(next: AssetRewriteMode): void; /** Release one parsed GLTF template from the shared cache without disposing live clones. */ export function evictGLTF( url: string, options?: { itemId?: string; variant?: 'female' } ): boolean; // ------------------------------------------------------------------------- // Catalog // ------------------------------------------------------------------------- export const PORTALS_CDN: string; export const GUARDIAN_MODELS: Record; export const GUARDIAN_SKIN_COLORS: readonly string[]; export const GUARDIAN_HAIR_COLORS: readonly string[]; export const GUARDIAN_EYE_COLORS: readonly string[]; export const DEFAULT_SKIN_COLOR: string; export const DEFAULT_HAIR_COLOR: string; export const DEFAULT_EYE_COLOR: string; export const DEFAULT_HAIR: Record; export const SKIN_PRESETS: Record; export const HAIR_COLOR_PRESETS: Record; export const EYE_COLOR_PRESETS: Record; /** * Body-clip GLBs on the Portals CDN, one per behaviour. Clip loading is lazy * per URL, so a game only downloads the behaviours it actually uses. */ export const GUARDIAN_LOCOMOTION_URL: string; export const GUARDIAN_LOCOMOTION_EXTRA_URL: string; export const GUARDIAN_STRAFE_URL: string; export const GUARDIAN_CROUCH_URL: string; export const GUARDIAN_CRAWL_URL: string; export const GUARDIAN_CLIMB_URL: string; export const GUARDIAN_LEDGE_URL: string; export const GUARDIAN_ACROBATICS_URL: string; export const GUARDIAN_SWIM_URL: string; export const GUARDIAN_UNARMED_URL: string; export const GUARDIAN_HIT_URL: string; export const GUARDIAN_SWORD_URL: string; export const GUARDIAN_PISTOL_URL: string; export const GUARDIAN_SPELL_URL: string; export const GUARDIAN_SIT_URL: string; export const GUARDIAN_INTERACT_URL: string; export const GUARDIAN_COUNTER_URL: string; export const GUARDIAN_EMOTE_URL: string; /** * Second clip library: a walk-speed 8-way directional set, purpose-built wall * mantles (`ledge_2m` is what the ledge grab plays), parkour, weapon movesets, * gathering, a zombie NPC set, and a real overhand throw. */ export const GUARDIAN_WALK_URL: string; export const GUARDIAN_LEDGE_2M_URL: string; export const GUARDIAN_MANTLE_URL: string; export const GUARDIAN_PARKOUR_URL: string; export const GUARDIAN_NINJA_JUMP_URL: string; export const GUARDIAN_TURN_URL: string; export const GUARDIAN_SWORD_LIGHT_URL: string; export const GUARDIAN_SWORD_HEAVY_URL: string; export const GUARDIAN_SWORD_REGULAR_URL: string; export const GUARDIAN_SWORD_SPECIAL_URL: string; export const GUARDIAN_MELEE_COMBO_URL: string; export const GUARDIAN_BOW_URL: string; export const GUARDIAN_SHIELD_URL: string; export const GUARDIAN_LAUNCHED_URL: string; export const GUARDIAN_FARM_URL: string; export const GUARDIAN_FISH_URL: string; export const GUARDIAN_GATHER_URL: string; export const GUARDIAN_IDLE_EXTRA_URL: string; export const GUARDIAN_LAY_URL: string; export const GUARDIAN_THROW_ACTION_URL: string; export const GUARDIAN_ZOMBIE_URL: string; export const GUARDIAN_MONSTER_URL: string; /** * Behaviour groups beyond what the character controller drives itself: * `locomotionExtra`, `lean`, `crouchTransitions`, `crawl`, `climb`, * `acrobatics`, `swim`, `unarmed`, `hit`, `sword`, `pistol`, `spell`, `sit`, * `interact`, `counter`, `emote`. * * ```js * await avatar.animations.load(ANIMATION_SETS.emote); * avatar.animations.play('dance'); * ``` */ export const ANIMATION_SETS: Record; /** * Which clip set backs the run/jog band: `'run4'` (default — a natural * forward run at ≈3.6 m/s world, with the UAL strafe/backward band behind * the other seven headings) or `'jog2'` (its jog-pace sibling: a relaxed * ≈2.0 m/s forward jog over the same strafe band). Both stay available at * runtime; switch with `PortalsAvatars.setRunStyle()`. */ export type RunStyle = 'run4' | 'jog2'; /** The run/jog clip sets, keyed by `RunStyle`. */ export const RUN_STYLES: Record; /** The jog takes GLB (`runStyle: 'jog2'` uses its `Jog_Fwd`). */ export const JOG_SET_URL: string; /** The run takes GLB (`runStyle: 'run4'` uses its `Running_B`). */ export const RUN_SET_URL: string; /** * Eye textures for the iris-mask shader, applied to every avatar by default. * Without them `setEyeColor` tints the whole eyeball rather than the iris. */ export const DEFAULT_EYE_TEXTURES: { diffuse: string; mask: string }; /** Older Guardian_v1 clip pack — still the source of the `throw` overlay. */ export const GUARDIAN_HAND_ITEMS_URL: string; /** * Every clip the character controller plays by itself: `idle`, `walk`, `run`, * `sprint`, `jump`, `fall`, `land`, `roll`, the 8-way directional set * (`runBack`, `runLeft`, `runRight`, `runFwdLeft`, `runFwdRight`, * `runBackLeft`, `runBackRight`), the matching WALK-speed set (`walkBack`, * `walkLeft`, `walkRight`, `walkFwdLeft`, `walkFwdRight`, `walkBackLeft`, * `walkBackRight`), the crouch set (`crouchIdle`, `crouchWalk`, `crouchBack`, * `crouchLeft`, `crouchRight`, `crouchFwdLeft`, `crouchFwdRight`, * `crouchBackLeft`, `crouchBackRight`), plus `climbTop` and `pickup`. */ export const DEFAULT_ANIMATIONS: AnimationSource[]; /** * Masked overlays for right-hand items: `carry` (held steady through the * run's arm swing, the Unity client's RightArm layer) plus `meleeA`/`meleeB`, * `throw`, `shoot`/`shoot2h`. Names match `CharacterController.itemAnimations`. */ export const DEFAULT_HAND_ANIMATIONS: AnimationSource[]; export const DEFAULT_FACE_ANIMATIONS: AnimationSource[]; /** The three default outfit items the Portals client equips. */ export const GUARDIAN_BASIC_SET: WearableDef[]; // ------------------------------------------------------------------------- // Entry point // ------------------------------------------------------------------------- export interface PortalsAvatarsOptions { scene: THREE.Scene; animations?: AnimationSource[]; /** * Hand-item overlays; defaults to `DEFAULT_HAND_ANIMATIONS`, `[]` to skip. * Fetched only once the avatar actually holds something. */ handAnimations?: AnimationSource[]; wearables?: WearableDef[]; faceAnimations?: AnimationSource[]; basicSet?: WearableDef[]; /** * Run/jog clip style for every avatar: `'run4'` (default) or `'jog2'`. * Only a starting default — `setRunStyle()` switches live. Ignored when a * custom `animations` array is supplied. */ runStyle?: RunStyle; /** Equip the basic set on every new avatar. Default true. */ defaultOutfit?: boolean; eyeTextures?: { diffuse: string; mask: string }; defaultConfig?: GuardianConfig; controllerDefaults?: Partial>; /** NPC defaults applied to every `createNPC()` / `createNPCFromUrl()` call. */ npcDefaults?: NPCOptions; } export class PortalsAvatars { readonly scene: THREE.Scene; constructor(options: PortalsAvatarsOptions); createAvatar(options?: AvatarLoadOptions): Promise; /** Load the active `/avatar` look returned by `Portals.player.get()`. */ createAvatarFromPlayer( player: PortalsPlayerProfile, extra?: PlayerAvatarLoadOptions ): Promise; /** Build an avatar from a Portals avatar URL (`sc`/`hc`/`ec`/`hair` params). */ createAvatarFromUrl( url: string, extra?: Omit ): Promise; createController( avatar: GuardianAvatar, options: ControllerOptions ): CharacterController; /** * Load a Guardian as an NPC — the same avatar `createAvatar` builds, but * driven by code instead of player input: `moveTo` / `follow` / `lookAt` / * `playAnimation` / `show` / `hide`. Updated by the same `update(dt)`. */ createNPC( avatarOptions?: AvatarLoadOptions, npcOptions?: NPCOptions ): Promise; /** `createAvatarFromUrl`, wrapped in the code-driven `NPCController`. */ createNPCFromUrl( url: string, extra?: Omit, npcOptions?: NPCOptions ): Promise; /** Remove an NPC and its avatar from the scene. */ removeNPC(npc: NPCController): void; /** The run style new avatars are created with. */ getRunStyle(): RunStyle; /** * Switch the run/jog clip set live, in either direction, for every * existing avatar (or just `avatar`) and all future ones. Clips already in * memory are re-loaded from the other set's GLB; if one is playing right * now it restarts in the new style with a short cross-fade. */ setRunStyle(style: RunStyle, avatar?: GuardianAvatar): Promise; removeAvatar(avatar: GuardianAvatar): void; /** * Force every declared clip into memory (all avatars by default). Clips * are lazy: `createAvatar` resolves without them and each GLB downloads on * first use. Call this behind a loading screen if a visible pop from rest * pose to idle would be worse than a longer load. */ /** * Mount the default customisation panel — body type, skin/hair/eye colour, * hair, facial hair and wearables — with a small edge button the player * uses to show and hide it. Open by default. */ createSetupPanel( avatar: GuardianAvatar, options?: Omit ): SetupPanel; preloadAnimations(avatar?: GuardianAvatar): Promise; /** Advance every avatar and controller. Call once per frame. */ update(dt: number): void; /** * Run a callback every frame right after the animation mixers have posed * the skeletons — the slot for anything that writes bones on top of the * clips (procedural aiming, ragdoll blending, boneAdjust previews, the * pose tuner dev tool). Callbacks run in registration order, after * controller post-updates and before the game code that follows its own * `update(dt)` call. Returns an unsubscribe function. (0.42.0) */ onAfterUpdate(callback: (dt: number) => void): () => void; dispose(): void; } // ------------------------------------------------------------------------- // Avatar // ------------------------------------------------------------------------- export class GuardianAvatar { readonly root: THREE.Group; readonly bodyType: BodyType; readonly wearables: WearableManager; readonly animations: AnimationController; readonly face: FaceAnimationController; readonly expressions: ExpressionManager; readonly bones: Map; readonly skeleton: THREE.Skeleton; static load(options?: AvatarLoadOptions): Promise; configure(config: GuardianConfig): this; getConfig(): GuardianConfig; setSkinColor(color: string | number): this; getHairStyles(): { id: string; name: string }[]; setHairStyle(styleId: string | null): this; setHairColor(color: string | number): this; setEyeColor(color: string | number): this; /** Swap in the Portals iris-mask eye shader so `eyeColor` tints only the iris. */ applyEyeShader(urls: EyeTextureUrls): Promise; getFacialHairStyles(): { id: string; name: string }[]; setFacialHair(styleId: string | null): this; getParts(): { name: string; visible: boolean }[]; setPartVisible(pattern: string, visible: boolean): number; setHiddenParts(patterns: string[]): this; applyHiddenParts(): void; setPartColor(pattern: string, color: string | number): number; getBone(name: string): THREE.Bone | undefined; attachToBone(boneName: string, object: THREE.Object3D): THREE.Group; findMeshes(nameSubstring: string): THREE.Object3D[]; update(dt: number): void; dispose(): void; } // ------------------------------------------------------------------------- // NPCs // ------------------------------------------------------------------------- /** NPC movement tuning. Numeric defaults match the character controller. */ export interface NPCOptions { /** Speed below which locomotion is a walk (m/s). Default 1.25. */ walkSpeed?: number; /** Speed of `moveTo(..., { run: true })` and fast follows (m/s). Default 4.6. */ runSpeed?: number; /** Yaw smoothing rate toward the travel/look direction. Default 16. */ rotationSpeed?: number; /** How close to a destination counts as arrived (m). Default 0.25. */ stopDistance?: number; /** * Ground height sampler — return the ground Y at (x, z). Without one the * NPC keeps its current height (right for flat levels). */ getGroundHeight?: (x: number, z: number, y?: number, radius?: number) => number; /** * Collide with the other avatars in the world (default true), so the NPC * neither walks through the player nor lets the player stand inside it. * Wired automatically for NPCs created through `PortalsAvatars`. */ collideWithAvatars?: boolean; } export interface NPCMoveOptions { /** Exact speed in m/s; overrides `run`. */ speed?: number; /** Move at `runSpeed` instead of `walkSpeed`. */ run?: boolean; /** Arrival radius for this move (m); defaults to the NPC's `stopDistance`. */ stopDistance?: number; } /** A movement/look target: object, vector, or plain point. */ export type NPCTarget = | THREE.Object3D | THREE.Vector3 | { x: number; y?: number; z: number }; /** A follow target: additionally an accessor returning the current point. */ export type NPCLiveTarget = | NPCTarget | (() => THREE.Vector3 | { x: number; y?: number; z: number }); /** * A code-driven Guardian — the same avatar the player gets (same model, * wearables, animations, face system), with the player input replaced by a * command API. Create via `PortalsAvatars.createNPC()` / * `createNPCFromUrl()`; the shared `PortalsAvatars.update(dt)` drives it. * * Everything an avatar can do stays reachable through `npc.avatar` * (wearables, face emotions, overlays, bones); this class owns movement, * facing and full-body clip selection. */ export class NPCController { readonly avatar: GuardianAvatar; /** Push out of other avatars' capsules — see `NPCOptions.collideWithAvatars`. */ collideWithAvatars: boolean; constructor(avatar: GuardianAvatar, options?: NPCOptions); /** * Wire the set of avatars this NPC collides with — `PortalsAvatars` does * this for every NPC it creates (the NPC's own avatar and hidden roots * are skipped). */ setAvatarObstacles(provider: (() => Iterable) | null): this; /** * Walk (or run) to a point or object, resolving `true` on arrival. * Starting another move, `stop()`, or disposal resolves `false`. An * Object3D target is re-read every frame, so a moving one is chased. */ moveTo(target: NPCTarget, options?: NPCMoveOptions): Promise; /** * Keep within `distance` (default 1.5 m) of a target, indefinitely — * walks when the gap opens, idles when close. `stop()` ends it. */ follow(target: NPCLiveTarget, options?: NPCMoveOptions & { distance?: number }): void; /** Cancel any move/follow and settle into idle. */ stop(): void; /** True while a `moveTo` or an out-of-range `follow` is driving the NPC. */ isMoving(): boolean; /** Teleport. Clears any in-flight move (resolved `false`). */ setPosition(position: NPCTarget): this; getPosition(out?: THREE.Vector3): THREE.Vector3; /** Instant facing (radians, world yaw). */ setRotationY(yaw: number): this; getRotationY(): number; /** * Turn smoothly to face a point or object (only applies while idle — * movement owns the facing). `{ instant: true }` snaps. */ lookAt(target: NPCTarget, options?: { instant?: boolean }): this; /** Retune movement at runtime. */ configureMovement(opts: { walkSpeed?: number; runSpeed?: number; rotationSpeed?: number; stopDistance?: number; }): this; /** * Play a full-body clip by name, fetching it first if needed; resolves * `true` once it is actually playing. While the NPC moves, the clip takes * the body over from locomotion until it finishes (one-shots) or * `stopAnimation()` (loops). For masked overlays (wave while walking) use * `avatar.animations.playOverlay()` directly. */ playAnimation( name: AnimationName, opts?: { fade?: number; restart?: boolean; timeScale?: number } ): Promise; /** Hand the body back to the NPC's own idle/locomotion selection. */ stopAnimation(fade?: number): void; show(): void; hide(): void; setVisible(visible: boolean): void; readonly visible: boolean; /** Advance movement and facing — `PortalsAvatars.update()` calls this. */ update(dt: number): void; dispose(): void; } // ------------------------------------------------------------------------- // Wearables // ------------------------------------------------------------------------- /** * Warm the GLB cache for a set of wearables, in parallel, without an avatar * — downloading needs only the definitions, so it can overlap the body * model's own load. Failures are swallowed; `equip()` surfaces them later. */ export function prefetchWearables(defs: WearableDef[], bodyType: BodyType): Promise; export class WearableManager { constructor(avatar: GuardianAvatar); registerCatalog(defs: WearableDef[]): void; setBasicSet(defs: WearableDef[]): void; getBasicSet(): WearableDef[]; isBasic(id: string): boolean; ensureBasicSet(): Promise; getCatalog(): WearableDef[]; getEquipped(): { slot: WearableSlot; slots: WearableSlot[]; id: string; name: string; isBasic: boolean; }[]; getEquippedUrls(): Partial>; getEquippedUrlEntries(): Array<[WearableSlot, string]>; isEquipped(id: string): boolean; getEquippedDef(slot: WearableSlot): WearableDef | undefined; getHandItem(): { def: WearableDef; type: HandItemType } | null; /** * Equip by catalog id, GLB URL, or an inline definition. `options.slot` * names the slot for a URL whose filename does not reveal it; a catalog * entry and an inline definition carry their own. */ equip( item: string | WearableDef, options?: { slot?: WearableSlot } ): Promise; unequip(slotOrId: WearableSlot | string): Promise; unequipAll(): void; dispose(): void; } /** Explicit `def.handItemType` wins, otherwise inferred from the name/id. */ export function classifyHandItem(def: WearableDef): HandItemType; // ------------------------------------------------------------------------- // Animation // ------------------------------------------------------------------------- export const OVERLAY_MASKS: { rightArm: RegExp; leftArm: RegExp; upperBody: RegExp; }; export type OverlayMask = keyof typeof OVERLAY_MASKS | RegExp; export class AnimationController { readonly mixer: THREE.AnimationMixer; constructor(avatarRoot: THREE.Object3D, targetMesh?: THREE.SkinnedMesh); /** Load clip GLBs now. Foreign Mixamo-rigged clips are retargeted automatically. */ load(sources: AnimationSource[]): Promise; /** * Register clips without fetching them. The GLB downloads the first time * something asks to `play()` one of its clips; clips sharing a GLB load * together. `has()` reports declared clips as available so state selection * is correct immediately, and `play()` returns false until the bytes land. */ declare(sources: AnimationSource[]): void; /** Load a declared clip if it is not in memory yet. */ ensure(name: AnimationName): Promise; /** Force declared clips into memory (all of them by default). */ preload(names?: AnimationName[]): Promise; /** Decoded and playable now, as opposed to merely declared. */ isLoaded(name: AnimationName): boolean; register( name: AnimationName, clip: THREE.AnimationClip, opts?: { loop?: boolean; timeScale?: number } ): void; registerOverlay( name: AnimationName, clip: THREE.AnimationClip, opts?: { mask: OverlayMask; loop?: boolean; timeScale?: number } ): void; hasOverlay(name: AnimationName): boolean; getOverlayDuration(name: AnimationName): number | null; isOverlayActive(name: AnimationName): boolean; playOverlay( name: AnimationName, opts?: { fade?: number; restart?: boolean; hold?: number } ): boolean; stopOverlay(name: AnimationName, fade?: number): void; /** Available to this avatar — loaded OR declared. See `isLoaded`. */ has(name: AnimationName): boolean; getClipDuration(name: AnimationName): number | null; getClipRootRise(name: AnimationName): number; sampleClipRootRise(name: AnimationName, time: number): number; getNames(): string[]; getCurrent(): string | null; play( name: AnimationName, opts?: { fade?: number; restart?: boolean; timeScale?: number } ): boolean; stopAll(fade?: number): void; setCurrentTimeScale(scale: number): void; update(dt: number): void; dispose(): void; } export type FaceEmotion = 'neutral' | 'smile' | 'serious' | 'frown'; export class FaceAnimationController { readonly mixer: THREE.AnimationMixer; constructor(avatarRoot: THREE.Object3D); load(sources: AnimationSource[]): Promise; /** * Register face clips without fetching them — each emotion is its own * small GLB, so `setEmotion` / `setTalking` / `blink` pull in only what * they need. A declared `blink` is fetched immediately when auto-blink is * on, since nothing else would ever ask for it. */ declare(sources: AnimationSource[]): void; ensure(name: string): Promise; preload(names?: string[]): Promise; isLoaded(name: string): boolean; register(name: string, clip: THREE.AnimationClip, opts?: { loop?: boolean }): void; getEmotions(): FaceEmotion[]; getEmotion(): FaceEmotion | null; setEmotion(emotion: FaceEmotion | null, fade?: number): boolean; isTalking(): boolean; setTalking(on: boolean, fade?: number): void; blink(): void; setAutoBlink(on: boolean): void; update(dt: number): void; dispose(): void; } /** Morph-target driver. Guardian v1 GLBs ship no morph targets; custom models may. */ export class ExpressionManager { constructor(root: THREE.Object3D); getExpressions(): string[]; has(name: string): boolean; set(name: string, weight?: number, fade?: number): boolean; get(name: string): number; reset(fade?: number): void; update(dt: number): void; } // ------------------------------------------------------------------------- // Controller + camera // ------------------------------------------------------------------------- export const MOVEMENT_LIMITS: { walkSpeed: [number, number]; runSpeed: [number, number]; sprintSpeed: [number, number]; jumpHeight: [number, number]; gravity: [number, number]; maxFallSpeed: [number, number]; swimSpeed: [number, number]; }; /** * Capsule radius assumed for OTHER avatars in avatar-vs-avatar collision * (0.35 — Guardians are all normalized to the same build); the character's * own side of a pair uses its configurable `radius`. */ export const AVATAR_BODY_RADIUS: number; export function jumpSpeedForHeight( height: number, gravity: number, holdTime: number ): number; export interface ItemUseEvent { type: HandItemType; action: 'melee' | 'throw' | 'shoot'; phase: 'start' | 'release'; hand: THREE.Vector3; direction: THREE.Vector3; } /** An item leaving the avatar via `throwItem()` / `dropItem()`. */ export interface ItemReleaseEvent { /** The item that left; pass it back to `pickUpItem()` to take it again. */ def: WearableDef; /** * The item itself, detached with its world transform kept and parented to * nothing — add it to your scene and simulate it from here (the host owns * the physics). */ object: THREE.Object3D; /** Launch velocity in m/s (zero for a drop). */ velocity: THREE.Vector3; action: 'throw' | 'drop'; } export class CharacterController { readonly avatar: GuardianAvatar; readonly cameraRig: CameraRig; /** * The on-screen touch controls, or null when off — see * `ControllerOptions.touch`. Disposed with the controller. */ readonly touch: TouchControls | null; readonly velocity: THREE.Vector3; walkSpeed: number; runSpeed: number; sprintSpeed: number; jumpSpeed: number; jumpTimer: number; gravity: number; /** Terminal fall speed, m/s (the Unity client's `limitFallVelocity`). */ maxFallSpeed: number; /** Biggest drop walked down without going airborne, metres. */ stepDown: number; /** Seconds of unjumped ground loss tolerated before the falling clip plays. */ fallAnimDelay: number; airSpeed: number; /** Max speed while swimming, m/s (default 1.9 — the clip's authored pace). */ swimSpeed: number; /** Vertical swim speed (Space rises, C dives), m/s. Default 1.5. */ swimVerticalSpeed: number; /** World Y the root may not swim above, or null for none — see `setSwimming`. */ swimSurfaceY: number | null; /** * Radius of the horizontal capsule the swimmer collides with, m (default * 0.35) — the prone body's cross-section, used in place of `radius` while * `swimming` is on. */ swimRadius: number; /** * How far that capsule reaches ahead of and behind the root, less * `swimRadius` (default 0.55) — so a swimmer stops 0.9 m short of a wall * head-on and 0.35 m from one it drifts into sideways. */ swimReach: number; rotationSpeed: number; /** Strafe behaviour — see `ControllerOptions.strafe`. */ strafe: StrafeMode; /** Shooter-grade strafe refinements, all off by default — see `StrafeTuning`. */ strafeTuning: Required; /** * How far the body is currently turned off the camera's heading to face a * forward diagonal, radians. Always 0 unless `strafeTuning.diagonalTurn` is * set. Hand it to `aimWeapon` as `diagYaw`, and replicate it so a peer can * reconstruct the aim line. */ readonly diagonalYaw: number; /** True while the game (or a held gun) has the character aiming. */ aiming: boolean; /** Push out of other avatars' capsules — see `ControllerOptions.collideWithAvatars`. */ collideWithAvatars: boolean; getGroundHeight: (x: number, z: number, y?: number, radius?: number) => number; resolveMovement: | ((current: THREE.Vector3, proposed: THREE.Vector3, radius: number) => THREE.Vector3) | null; /** Capsule depenetration — see `ControllerOptions.depenetrate`. */ depenetrate: | ((position: THREE.Vector3, radius: number, height: number) => THREE.Vector3) | null; /** Depenetration for a caller-placed capsule — see `ControllerOptions.depenetrateSegment`. */ depenetrateSegment: | ((start: THREE.Vector3, end: THREE.Vector3, radius: number) => THREE.Vector3) | null; raycast: | ((origin: THREE.Vector3, direction: THREE.Vector3, maxDistance: number) => RaycastHit | null) | null; checkClearance: | ((point: { x: number; y: number; z: number }, radius: number, height: number) => boolean) | null; /** Return true to suppress the built-in animation state pick for this frame. */ onBeforeAnimationUpdate: ((controller: CharacterController, dt: number) => boolean) | null; onItemUse: ((event: ItemUseEvent) => void) | null; /** * Called when the player asks for a respawn (holding `respawnKey`, or * `requestRespawn()`). Return `true` once the game has handled it — put * the player where its own rules say — and the controller does nothing * more; otherwise it falls back to `respawn()` at the last position a * `respawn({ position })` call gave it. */ onRespawnRequest: (() => boolean | void) | null; /** * Fired at the release frame of `throwItem()` / `dropItem()` — the item is * off the avatar and comes with a launch velocity; add it to your scene and * simulate it. Without a handler those calls refuse to run (the item would * be lost). */ onItemRelease: ((event: ItemReleaseEvent) => void) | null; /** Fired when `pickUpItem()` has put an item back on the avatar. */ onItemPickup: ((def: WearableDef) => void) | null; constructor(avatar: GuardianAvatar, options: ControllerOptions); /** * Wire the set of avatars this character collides with (see * `ControllerOptions.collideWithAvatars`). `PortalsAvatars` does this for * every controller it creates; hand-built setups can pass their own * provider (the character's own avatar and hidden roots are skipped). */ setAvatarObstacles(provider: (() => Iterable) | null): this; configureMovement(opts: { walkSpeed?: number; runSpeed?: number; sprintSpeed?: number; jumpHeight?: number; gravity?: number; maxFallSpeed?: number; swimSpeed?: number; }): this; setWalkSpeed(v: number): this; setRunSpeed(v: number): this; setSprintSpeed(v: number): this; setJumpHeight(meters: number): this; getJumpHeight(): number; setEnabled(enabled: boolean): void; /** * Give the body to game-driven animation: while locked the controller * stops selecting locomotion clips and ignores movement/jump/item input, * but gravity and the camera keep working. Play your own full-body clips * with `avatar.animations.play(...)`, then unlock to resume idle. Use for * actions the controller has no state for (fishing, mini-games, scripted * moments). */ setBodyLocked(locked: boolean): void; /** True while the game owns the body via `setBodyLocked(true)`. */ isBodyLocked(): boolean; /** * Put the character in (or out of) swim mode — the GAME toggles this at * the water's edge; the SDK has no idea where a level's water is. While * on: gravity is off, movement runs at `swimSpeed`, Space swims up and C * dives (programmatic: `setJumpHeld` / `setSwimVertical`), the * `swimFwd`/`swimIdle` clips drive themselves, and jumping, crouching, * the ledge grab and `pickUpItem()` are suspended. `surfaceY` is an * optional ceiling the feet may not rise above — pick it from your water * level minus how deep the body should ride (null = open water). Turning * it off hands the body straight back to normal physics. */ setSwimming(on: boolean, opts?: { surfaceY?: number | null }): this; /** True while swim mode is on. */ isSwimming(): boolean; /** * Programmatic vertical swim input, −1 (dive) to 1 (rise) — what Space * and C feed while swimming. Sticks until the next call, like * `setMoveVector`. */ setSwimVertical(v: number): this; /** * Vault out of the water onto a shore ahead. The swim step calls this on * every fresh jump press: where ground lies within a waist-depth of * `swimSurfaceY` — or above it, up to a lip the configured jump can clear * — swim mode ends in the grounded jump's own arc, with a send-off along * the facing. Over open water nothing answers and Space stays the rise * key. Needs `swimSurfaceY`; games may also call it directly. Returns * true when the jump has begun and swim mode is over. */ tryJumpOutOfWater(): boolean; /** * The player asks to be put back on their feet — what holding * `respawnKey` does, for a touch button or a menu entry. `onRespawnRequest` * gets first refusal; otherwise `respawn({ position })` at the last respawn * target. Returns false when nothing could be done (no handler and no * respawn position ever given). */ requestRespawn(): boolean; toggleCameraMode(): CameraMode; /** * Restrict (or release) how far the player may turn the camera — a yaw * window, tighter pitch stops, or an axis locked away from input. Only the * keys passed change; a camera already outside a new range moves to the * nearest edge at once. Survives an avatar swap via `adoptFrom`. */ setCameraLimits(limits: CameraLimitOptions): this; /** Change strafe behaviour at runtime. */ setStrafe(mode: StrafeMode): this; /** Turn on (or retune) the shooter-grade strafe refinements. Merges. */ setStrafeTuning(tuning: StrafeTuning): this; /** * Turn the character to face a world heading (radians, 0 = +Z), taking the * camera with it. Use this instead of assigning `avatar.root.rotation.y` — * the controller owns that every frame, so an external assignment either gets * overwritten or fights it, leaving the body facing away from its travel * direction while the forward run clip plays (it looks like running * backwards). `turnCamera: false` rotates only the body. */ setFacing(yaw: number, options?: { turnCamera?: boolean }): this; /** * True world yaw of the root, read from the QUATERNION — immune to the * flipped Euler representation `avatar.root.rotation.y` can carry. This * is the value to PUBLISH when a multiplayer game shares the player's * facing; reading `rotation.y` instead points remote copies anywhere but * where the player looks, most visibly while idle. */ readRootYaw(): number; /** * Write the root's facing — euler and quaternion together, collapsing any * flipped representation back to pure yaw. Use this rather than assigning * `avatar.root.rotation.y` yourself (e.g. when applying a networked yaw * to a controller-less remote avatar, mirror this: `rotation.set(0, yaw, 0)` * then `quaternion.setFromEuler(rotation)`). */ writeRootYaw(yaw: number): void; /** * Back to a known-good state: at `position` (default: unchanged), facing * `facing` with the camera behind it, standing still on the ground with every * in-flight action (climb, pick-up, throw, jump) cancelled. Use it for a * respawn, or as the escape hatch when the character is somewhere it cannot * get out of. */ respawn(options?: { position?: THREE.Vector3 | { x: number; y: number; z: number }; facing?: number; turnCamera?: boolean; }): this; /** * Mesh-facing measurement — runs automatically after the animation mixers * each frame (`PortalsAvatars.update` calls it; don't call it yourself). * Estimates the structural yaw offset between the root and the visible * body (hip line, world space); when a rotation is stuck in a node between * them, the facing code compensates its target so the BODY faces the * travel direction. Writes no transforms itself. */ postUpdate(dt: number): void; /** Only meaningful under `strafe: 'aim'`, where it is what enables it. */ setAiming(aiming: boolean): this; /** * Is the body locked to the camera's heading right now? Always true in * first person, whatever the strafe mode. */ isStrafing(): boolean; /** * Inherit camera orientation and movement tuning from the controller this * one replaces. Call it in `onAvatarReplaced` after a body-type switch — * without it the new camera rig snaps behind the avatar and WASD silently * changes direction under the player. */ adoptFrom(previous: CharacterController): this; jump(): void; /** * Hold or release the jump control (same as holding Space): the press * jumps, keeping it held re-jumps on landing (`autoJump`). */ setJumpHeld(held: boolean): void; /** Sprint on/off programmatically (same as holding Shift). */ setSprint(on: boolean): void; /** * Analog movement input — the programmatic WASD for touch joysticks and * gamepad sticks. Camera-relative: `x` moves right, `y` forward; clamped * to length 1 and it STICKS until the next call, so pass `(0, 0)` on * stick release. Deflection sets the pace: full tilt = run speed (sprint * under `setSprint(true)`), part-way proportionally slower, floored at * walk speed. Movement keys, while pressed, win over the stick. */ setMoveVector(x: number, y: number): void; /** * Press the primary use control (the programmatic F key / left click / * touch action button). A melee item with the combo take in memory * resolves on RELEASE (tap = quick strike, holding past `meleeHoldTime` * = the full combo), so pair every call with `endPrimaryAction()`; * every other item fires on the press itself. */ beginPrimaryAction(): void; /** Release the primary use control — see `beginPrimaryAction`. */ endPrimaryAction(): void; /** Abandon an in-flight primary press without firing (a cancelled gesture). */ cancelPrimaryAction(): void; setCrouch(on: boolean): void; getHandItemType(): HandItemType | null; useItem(): boolean; /** Play the full melee combo take directly (the long-press action). */ useItemCombo(): boolean; /** * Throw the held item away — at the release frame it leaves the avatar and * is handed to `onItemRelease` with a launch velocity. Returns false when * the hand is empty or busy, or no `onItemRelease` handler is set. */ throwItem(opts?: { speed?: number; angle?: number }): boolean; /** Let the held item go where you stand, without the throw animation. */ dropItem(): boolean; /** * Take an item (back) into the right hand, playing the pick-up animation — * aligned to `opts.position` (the item's world position) when given. */ pickUpItem( item: WearableDef | string, opts?: { position?: THREE.Vector3 | { x: number; y: number; z: number } } ): Promise; /** True while the pick-up animation owns the body. */ isPickingUp(): boolean; update(dt: number): void; dispose(): void; } // ------------------------------------------------------------------------- // Combat // ------------------------------------------------------------------------- export interface HealthControllerOptions { /** Blows to a kill. Default 3. */ maxHealth?: number; /** Lock/unlock this controller's body around reactions (optional). */ controller?: CharacterController | null; /** Flinch takes, alternated per blow. Default `['hitChest', 'hitStomach']`. */ hitClips?: string[]; /** Death takes, one picked per death. Default `['death', 'death2']`. */ deathClips?: string[]; /** Longest a flinch may hold the body, seconds. Default 1.1. */ staggerMax?: number; /** Seconds the body lies still after the death take before respawn. Default 1.4. */ corpseLinger?: number; /** Seconds of taking no damage after a respawn. Default 3. */ respawnImmunity?: number; /** * Where/how to respawn — teleport the avatar, restore state, play effects. * Called after the corpse has lingered. Omit (null) to stay dead until * `respawn()` is called by the game. */ respawn?: (() => void) | null; } export type HealthEvent = 'hurt' | 'died' | 'respawned'; export type HitResult = 'immune' | 'dead' | 'hurt' | 'killed'; /** * Health, hit reactions, death and respawn for one Guardian — the receiving * half of melee combat (`MeleeSweep` is the striking half). * * Reactions play on the BASE animation layer, so in a multiplayer game they * travel wherever the game already shares `animations.getCurrent()`; with a * `controller` attached the body is locked for exactly as long as a * reaction plays. Call `warm()` when combat becomes likely — a reaction * first requested at the blow plays nothing while its GLB is on the wire. * * Who calls `takeHit()` is the game's business. The usual multiplayer shape * is victim-authoritative: each client runs a HealthController for its OWN * avatar and applies only the hits that name it. */ export class HealthController { constructor(avatar: GuardianAvatar, options?: HealthControllerOptions); readonly maxHealth: number; health: number; dead: boolean; /** Seconds of post-respawn immunity left (0 when vulnerable). */ immunity: number; /** Pull the reaction takes into memory ahead of the first blow. */ warm(): void; /** A blow lands. Returns what it did, for the caller to narrate. */ takeHit(options?: { damage?: number }): HitResult; /** The killing blow (or scripted death): drop where the body stands. */ kill(): void; /** Back on their feet: full health, brief immunity, the respawn hook. */ respawn(): void; /** Run the stagger/corpse clocks. Call once per frame. */ update(dt: number): void; on(event: HealthEvent, cb: (self: HealthController) => void): () => void; } export interface MeleeSweepOptions { /** * Forgiveness margin, meters: a blow lands when the blade's surface * passes within this of a body capsule's surface. Default 0.18 (measured: * with a ~1 m axe, 0.9 m feet-to-feet always hits, 1.5 m never does). */ margin?: number; /** Targets farther than this from the wielder are not tested. Default 3.5 m. */ cullRadius?: number; /** Blade-path sub-steps between frames. Default 3. */ steps?: number; /** The weapon capsule's radius never reads below this. Default 0.05 m. */ minBladeRadius?: number; } /** Anything sweepable: a record that renders a Guardian. */ export interface MeleeTarget { avatar: GuardianAvatar; } /** * Continuous melee hit detection: the equipped hand item is measured into a * capsule (its real length — a longer weapon reaches further with no * tuning), each target's POSED skeleton into bone capsules, and the blade's * path is swept between frames so a fast slash cannot tunnel through a limb. * * ```ts * const sweep = new MeleeSweep(avatar); * controller.onItemUse = ({ type, phase }) => { * if (type === 'melee' && phase === 'start') sweep.begin(); * }; * // per frame, AFTER the mixers have posed the bones: * const victim = sweep.swinging && sweep.test(targets); * if (victim) { sweep.end(); victim.health.takeHit(); } * ``` */ export class MeleeSweep { constructor(wielder: GuardianAvatar, options?: MeleeSweepOptions); /** A strike is in flight and unlanded — `test()` only runs while true. */ swinging: boolean; /** * Diagnostics: closest surface-to-surface approach of the last swing, in * meters (negative = touching) — tune `margin` from measurements. */ lastApproach: number; /** Open a swing window — call on the strike's `start` (`onItemUse`). */ begin(): void; /** Close the swing window (a hit landed, or the take finished clean). */ end(): void; /** (Re)measure the equipped hand item into the blade capsule. */ measure(): boolean; /** * Per-frame while swinging: the first target the blade passed through * this frame, or null. Leave dead/friendly targets out of `targets`. */ test(targets: Iterable): T | null; } // ------------------------------------------------------------------------- // Level collision // ------------------------------------------------------------------------- export interface SceneColliderOptions { /** Height the character steps onto without jumping. Default 0.32. */ stepHeight?: number; /** Steepest surface still walkable, in degrees. Default 60. */ slopeLimit?: number; /** Ground height where no mesh is hit. Default 0. */ groundY?: number; /** Used to place the knee and chest collision probes. Default 1.6. */ characterHeight?: number; /** How far above the queried Y the ground probe starts. Default 0.8. */ groundSnapUp?: number; /** * Capsule collision (default true): builds a BVH per registered geometry * and adds a `depenetrate` hook that pushes the body back out of walls — * the fix for sinking into geometry while falling. The BVH also speeds up * every raycast here, so per-frame cost goes DOWN; what `false` buys is * load time (the BVH build, ~1 ms per ~1k triangles) and memory on huge * scenes, at the price of wall penetration coming back. */ capsule?: boolean; } /** * Collision read off the scene meshes — supplies every hook the controller * needs, so a game with ordinary geometry writes none of them: * * const collider = new SceneCollider(); * collider.add(ground).add(level); * avatars.createController(avatar, { * camera, domElement, ...collider.controllerOptions(), * }); * * Being triangle-exact, it handles ramps, cylinders and imported levels. * Register level geometry only — never avatars or props. */ export class SceneCollider { constructor(root?: THREE.Object3D, options?: SceneColliderOptions); /** Registered mesh count — what every ray is tested against. */ readonly meshCount: number; /** Register an object and its descendants. Skinned meshes are skipped. */ add(object: THREE.Object3D): this; remove(object: THREE.Object3D): this; clear(): this; /** Re-scan the registered roots after adding or removing level meshes. */ refresh(): this; raycast( origin: THREE.Vector3, direction: THREE.Vector3, maxDistance: number ): RaycastHit | null; /** * Surface height at (x, z); `y` starts the probe just above the character * so overhangs are ignored. Pass the capsule `radius` too (the controller * does) and four rim rays keep the character supported when its centre * slips past an edge, a plank gap or a pillar's rim — the fix for landing * half on a ledge and falling straight through it. */ getGroundHeight(x: number, z: number, y?: number, radius?: number): number; resolveMovement( current: THREE.Vector3, proposed: THREE.Vector3, radius: number ): THREE.Vector3; /** * Standing room for a capsule rising from `point`: upward rays for a * ceiling, then (with `capsule` on) the BVH capsule overlap and * `isUnderSurface`, so a stand point inside a single-sided prop — invisible * to rays started inside it — is refused rather than climbed into (0.42.0). */ checkClearance( point: { x: number; y: number; z: number }, radius: number, height: number ): boolean; /** * Push a character capsule out of any overlapping triangle and return the * corrected position (`position` is the feet). Walls push horizontally, * ceilings push down, floors are left to `getGroundHeight`. Returns the * position unchanged when the `capsule` option is off. */ depenetrate( position: { x: number; y: number; z: number }, radius: number, height: number ): THREE.Vector3; /** * The same, for a capsule the caller places: pass the two sphere centres * in world space and get back the offset that lifts it clear. This is what * the controller collides the flat swim pose with. */ depenetrateSegment( start: { x: number; y: number; z: number }, end: { x: number; y: number; z: number }, radius: number ): THREE.Vector3; /** All the hooks, ready to spread into `createController`. */ controllerOptions(): { getGroundHeight: (x: number, z: number, y?: number, radius?: number) => number; resolveMovement: ( current: THREE.Vector3, proposed: THREE.Vector3, radius: number ) => THREE.Vector3; raycast: ( origin: THREE.Vector3, direction: THREE.Vector3, maxDistance: number ) => RaycastHit | null; checkClearance: ( point: { x: number; y: number; z: number }, radius: number, height: number ) => boolean; depenetrate?: ( position: THREE.Vector3, radius: number, height: number ) => THREE.Vector3; depenetrateSegment?: ( start: THREE.Vector3, end: THREE.Vector3, radius: number ) => THREE.Vector3; }; } /** A ledge the character can pull itself onto. */ export interface LedgeGrab { /** Point on the ledge's top face where the hands land. */ point: THREE.Vector3; /** Horizontal unit normal of the face, pointing out toward the character. */ normal: THREE.Vector3; /** The lip — where the front face meets the top. */ edge: THREE.Vector3; } export interface LedgeProbeParams { position: THREE.Vector3; forward: THREE.Vector3; height: number; radius: number; grounded: boolean; /** The Unity client's `IsJustFalling` — extends the reach for a mid-air grab. */ falling: boolean; raycast: ( origin: THREE.Vector3, direction: THREE.Vector3, maxDistance: number ) => RaycastHit | null; checkClearance?: ( point: { x: number; y: number; z: number }, radius: number, height: number ) => boolean; } /** * The ledge probe the controller runs each frame, exposed for games that * want to show a grab prompt or drive their own climb. Up to ten rays. */ export function probeLedge(params: LedgeProbeParams): LedgeGrab | null; // ------------------------------------------------------------------ ragdoll /** Death feel. Every field is documented on the constant in the SDK source. */ export const RAGDOLL_DEFAULTS: Required; /** * The kick a shot gives a body: a velocity along the shot, lifted enough to * take the body off its feet, and the world point it lands at. The vertical * part of the shot is mostly discarded, so a hit taken from a rooftop throws * the body across the floor rather than hammering it into it. * * Returns null when `from` and `point` coincide. */ export function shotKick(params: ShotKickParams): ShotKick | null; /** * A dead Guardian, falling — a position-based solver run directly on the * skeleton (one particle per joint, distance constraints for the bones, and a * pull back toward the pose the body died in), not a physics engine. * * ```ts * const ragdoll = new Ragdoll(avatar, { ...collider.controllerOptions(), bounds: 40 }); * controller.setBodyLocked(true); // or the controller keeps walking the corpse * ragdoll.start({ ...shotKick({ from, point, head }), velocity: controller.velocity }); * // in the render loop, AFTER avatars.update(dt): * ragdoll.update(dt); * // on respawn: * ragdoll.stop(); * ``` * * ORDERING: `update` must run after `PortalsAvatars.update()`. The mixer * rewrites every bone local each frame, so a ragdoll stepped before it is * simply overwritten and the body plays its death clip instead. */ export class Ragdoll { constructor(avatar: GuardianAvatar, options?: RagdollOptions); readonly avatar: GuardianAvatar; readonly tuning: Required; /** Simulating, or holding the pose it settled into. */ active: boolean; settled: boolean; /** Abandoned mid-fall because the solve went non-finite; the clip has the body. */ broken: boolean; elapsed: number; /** Mean particle speed, m/s. Reported for tuning. */ speed: number; /** `active && !settled`. */ readonly running: boolean; /** * Take the body over. Returns false when the rig is not one the solver can * drive, so the caller can fall back to a death clip. */ start(params?: { /** Velocity added to the whole body, m/s. */ impulse?: THREE.Vector3 | null; /** Where it landed — particles near it take a bigger share. */ point?: THREE.Vector3 | null; /** The motion the body already had. */ velocity?: THREE.Vector3 | null; }): boolean; /** Hand the body back, restoring the captured pose. Call before respawning. */ stop(): void; update(dt: number): void; } // ------------------------------------------------------------------- arm IK /** Rotate a bone about a WORLD axis, expressed into its parent's frame. */ export function rotateBoneWorld( bone: THREE.Object3D | null | undefined, axisWorld: THREE.Vector3, angle: number ): void; /** Premultiply a bone's WORLD orientation by a quaternion. */ export function turnBoneWorld( bone: THREE.Object3D | null | undefined, worldTurn: THREE.Quaternion ): void; /** * Pose a two-bone arm so the wrist lands EXACTLY on `target`, with the elbow * out on the `pole` side — the analytic solve. Use this, not CCD, whenever the * elbow's direction matters: `pole` is an input here, where CCD converges to * whatever plane it likes and erases any bias fed in as a starting pose. * * Runs after the mixers, like everything that writes bone locals. */ export function poseArmTo( armBone: THREE.Object3D, foreBone: THREE.Object3D, handBone: THREE.Object3D, target: THREE.Vector3, pole: THREE.Vector3 ): void; /** * Cyclic coordinate descent down a joint chain (shoulder first, elbow last), * for "get this hand near this point" with no elbow opinion. Degrades * gracefully on a target out of reach — the arm points at it rather than * snapping to it. */ export function solveArmCCD( hand: THREE.Object3D, joints: THREE.Object3D[], target: THREE.Vector3, passes?: number ): void; // ------------------------------------------------------------ weapon posing /** Overlay mask covering everything from the hips up — the aimed stance. */ export const AIM_MASK: RegExp; /** Overlay mask covering the right arm and the upper spine — the hip point. */ export const HIP_MASK: RegExp; /** Corrective rotation that swings the one-handed point out to the shoulder line. */ export const HIP_ARM_ADJUST: Record; /** * Corrective rotation that opens the heavy carry's trigger arm out of the * fishing take it is cut from — registered on the `heavyPose` clip by * `loadGunPoses` (fishing take only; the pistol fallback is another shape). */ export const HEAVY_ARM_ADJUST: Record; /** Name of the empty a game marks its barrel tip with, unless it says otherwise. */ export const DEFAULT_MUZZLE_NODE: string; /** * Where a Guardian's right hand has to hold a weapon whose barrel points down * its own +Z with the grip hanging toward −Y. Solved against the pistol * stance, not eyeballed — spread it into a `WearableDef.offset`. */ export const GUARDIAN_PISTOL_GRIP: WearableOffset; /** * The same grip for a weapon with NO grip column — a tube the hand closes on * directly. Sits lower in the hand so the fist meets the tube's underside. */ export const GUARDIAN_TUBE_GRIP: WearableOffset; /** * Where the support hand goes on a hip-carried two-handed weapon, in the * WEAPON's frame. Re-measure it whenever the right arm or the mesh moves. */ export const HEAVY_FOREGRIP: [number, number, number]; /** How much of the camera's elevation the body takes on. */ export const AIM_BEND_DEFAULTS: Required; /** The two-handed hip carry, wrapping a fat-bodied weapon. */ export const HEAVY_CARRY: Required; /** The same carry for a slim weapon: a straight-arm prop, not a wrap. */ export const RAIL_CARRY: Required; /** * Where that weapon goes when the sights come up: shouldered, the stock into * the cheek. A destination the raise walks to, not a clip — see `aimWeapon`'s * `scoped`. */ export const SCOPE_DEFAULTS: Required; /** * Where a shot comes from when the barrel cannot be used, metres above the * feet — the centre of the torso, so a shot leaves where a shot lands. */ export const DEFAULT_CHEST_HEIGHT: number; export const DEFAULT_CROUCH_CHEST_HEIGHT: number; /** What a shot does to the gun and the arm. Camera kick is the game's own. */ export const RECOIL_DEFAULTS: Required; /** * Register the three gun stances (`adsPose`, `hipPose`, `heavyPose`) and the * one-handed recoil take (`hipShoot`) on an avatar. Returns false when the * clip set is unavailable, in which case every function below is a no-op and * the body simply carries its weapon. */ export function loadGunPoses( avatar: GuardianAvatar, options?: LoadGunPosesOptions ): Promise; /** * Which stance a body should be holding, or null for a weapon that is down. * A two-handed weapon is carried at the waist whether it is aiming or not. */ export function wantedPose(raised: boolean, aiming: boolean, twoHanded?: boolean): GunPoseName | null; /** `RAIL_CARRY` for a slim weapon, `HEAVY_CARRY` for a fat-bodied one. */ export function carryFor(slim: boolean): Required; /** * Swap the stance overlay; returns the mode now held. `snap` brings the new * stance in with no blend, for a stance that comes up because a shot is being * fired — the bullet leaves the barrel where the barrel is. */ export function applyPose( avatar: GuardianAvatar | null, from: GunPoseName | null, to: GunPoseName | null, snap?: boolean ): GunPoseName | null; /** * The recoil take matching the stance, or null for a waist carry — whose * recoil is procedural, inside `aimWeapon`. */ export function recoilClip( avatar: GuardianAvatar | null, aiming: boolean, twoHanded?: boolean ): string | null; /** * Point the weapon where the shot is going: the camera-pitch spine bend, the * procedural recoil, the two-handed hip carry and the barrel itself. * * Runs AFTER `PortalsAvatars.update()` — the mixers rewrite every bone local * each frame, which is also why nothing it writes accumulates. In multiplayer, * drive every remote body through this same function from its replicated aim * yaw, pitch and flags: the body is the only aim indicator a peer gets. */ export function aimWeapon(avatar: GuardianAvatar | null, params: AimWeaponParams): void; /** * The gun's mount, for a tuning tool's translate/rotate gizmo: the holder it * drags and the rest transform a drag is measured against. `aimWeapon` * rewrites the holder as rest + nudge + kick every frame, so a dragged-out * nudge is holder-local `position − rest` at kick 0, and a dragged-out tilt * is `restQuat⁻¹ × quaternion`. Fold those into the rows of the stance the * body is actually HOLDING — a two-handed weapon is always in `heavyPose` * whatever a tuner's own pose selector says. Not used by gameplay. */ export function gunMount( avatar: GuardianAvatar | null, muzzleNode?: string ): { holder: THREE.Object3D; rest: THREE.Vector3; restQuat: THREE.Quaternion } | null; /** * The barrel tip in world space, refreshed off the body's current pose — what * tracers and muzzle flashes are drawn from. Falls back to the chest when * there is no weapon to read, or when the node is further than `maxReach` from * the body (an unparented clone). */ export function muzzleWorld( avatar: GuardianAvatar | null, out?: THREE.Vector3, options?: MuzzleWorldOptions ): THREE.Vector3; /** * Where a hitscan shot should be traced FROM: the barrel, unless the barrel is * inside something, in which case the chest. * * A third-person camera sees over and around what the gun is still behind, so * a shot traced from the lens goes through cover the shooter never left. Trace * the aim from the camera and the shot from here — see the README for the * two-trace pattern. */ export function shotOrigin( avatar: GuardianAvatar | null, params: ShotOriginParams ): THREE.Vector3; /** Tuning for the third-person camera's obstacle avoidance. */ export interface CameraCollisionOptions { /** Default true. */ enabled?: boolean; /** Half-width of the cylinder swept along the boom, m (default 0.2). */ radius?: number; /** Boom length at which third person gives up, m (default 0.7). */ minDistance?: number; /** How fast the boom extends again once clear, m/s (default 4). */ returnSpeed?: number; /** Delay before it starts extending, s (default 0.15). */ returnDelay?: number; /** 5 (default) sweeps a cylinder; 1 casts the axis only. */ rays?: 1 | 5; /** Drop to first person when squeezed under `minDistance` (default true). */ firstPerson?: boolean; /** Geometry to test; defaults to the controller's `raycast` hook. */ probe?: | ((origin: THREE.Vector3, direction: THREE.Vector3, maxDistance: number) => RaycastHit | null) | null; } /** * How far the camera may be turned — `ControllerOptions.cameraLimits`, or * `controller.setCameraLimits()` at runtime. Angles in **radians**. * * Ranges bound the camera however it is moved: mouse, touch drag, and the * SDK's own camera writes (`setFacing`, a climb) all end up inside them. The * locks stop only *input*, leaving the game free to place the camera itself. */ export interface CameraLimitOptions { /** * Yaw range the camera may look within — pass **both** ends, in the * convention of `cameraRig.yaw`: 0 looks along −Z, increasing turns left. * `null` clears the limit, and so does a range a full turn or wider; one * end alone is ignored, having nothing to enforce on a circle. * * Movement is camera-relative and the body faces the camera while strafing * or in first person, so a narrow window also narrows where the character * can walk and face — usually the point, but not a view-only setting. */ minYaw?: number | null; maxYaw?: number | null; /** Lowest the camera may look, radians (default −1.2 ≈ 69° down). */ minPitch?: number; /** Highest the camera may look, radians (default 1.35 ≈ 77° up). */ maxPitch?: number; /** * Ignore horizontal look input. Unlike a zero-width yaw range this pins * nothing — the camera keeps the yaw it has and the game may still turn it. */ lockYaw?: boolean; /** Ignore vertical look input. */ lockPitch?: boolean; } export interface CameraRigOptions { domElement: HTMLElement; mode?: CameraMode; distance?: number; minDistance?: number; maxDistance?: number; height?: number; shoulderOffset?: number; eyeHeight?: number; sensitivity?: number; smoothing?: number; minPitch?: number; maxPitch?: number; minYaw?: number | null; maxYaw?: number | null; lockYaw?: boolean; lockPitch?: boolean; invertX?: boolean; invertY?: boolean; collision?: boolean | CameraCollisionOptions; } export class CameraRig { readonly camera: THREE.PerspectiveCamera; mode: CameraMode; /** * Camera heading, radians: 0 looks along −Z, increasing turns left. The * body faces this + π. Assignments are clamped into `[minYaw, maxYaw]`. */ yaw: number; /** Camera elevation, radians (negative looks down). Clamped to the pitch range. */ pitch: number; distance: number; height: number; shoulderOffset: number; eyeHeight: number; sensitivity: number; smoothing: number; minDistance: number; maxDistance: number; minPitch: number; maxPitch: number; /** Yaw window, `null` for unlimited — see `setLimits`. */ minYaw: number | null; maxYaw: number | null; lockYaw: boolean; lockPitch: boolean; invertX: boolean; invertY: boolean; /** Resolved avoidance settings; mutate `.enabled` to toggle at runtime. */ readonly collision: Required> & { probe: | ((origin: THREE.Vector3, direction: THREE.Vector3, maxDistance: number) => RaycastHit | null) | null; }; /** * Objects allowed to stand between the character and the camera without * the boom pulling in — the player hides behind them. **Empty by * default**: nothing occludes until the game `add()`s a mesh (or a group, * covering its descendants). Even a listed object is avoided while the * camera would sit inside it, so keep listed meshes closed and * consistently wound. Everything not listed pulls the boom in as always. */ readonly allowedOccluders: Set; constructor(camera: THREE.PerspectiveCamera, options: CameraRigOptions); follow(target: THREE.Object3D): void; setMode(mode: CameraMode): void; toggleMode(): CameraMode; onModeChange(cb: (mode: CameraMode) => void): () => void; getFlatForward(out?: THREE.Vector3): THREE.Vector3; /** * Restrict (or release) the look range. Only the keys present are touched, * so `{ lockPitch: true }` leaves a yaw window alone and * `{ minYaw: null, maxYaw: null }` clears one. A camera already outside a * new range is brought to its nearest edge immediately. */ setLimits(limits: CameraLimitOptions): void; /** The limits as `setLimits` would take them — for copying between rigs. */ getLimits(): Required; /** Take the camera away from the player on both axes, or hand it back. */ setLookLocked(locked: boolean): void; /** True while neither axis accepts look input. */ isLookLocked(): boolean; /** * Turn the camera by a look delta in radians — the path every input device * takes. Applies inversion, drops a locked axis, and respects the limits, so * a custom control scheme gets all three for free. */ applyLookDelta(yawDelta: number, pitchDelta: number): void; setCollisionProbe( probe: ((origin: THREE.Vector3, direction: THREE.Vector3, maxDistance: number) => RaycastHit | null) | null ): void; /** True while first person is avoidance, not the player's choice. */ isAutoFirstPerson(): boolean; adoptCollisionState(from: CameraRig): void; update(dt: number): void; dispose(): void; } // ------------------------------------------------------------------------- // Touch controls // ------------------------------------------------------------------------- /** The on-screen buttons `TouchControls` can draw. */ export type TouchButton = 'jump' | 'sprint' | 'use' | 'throw' | 'crouch' | 'camera'; export interface TouchControlsOptions { /** * Element the overlay is appended to. Defaults to the renderer element's * parent, else `document.body`. */ container?: HTMLElement; /** * `'fixed'` (default) anchors the stick bottom-left; `'dynamic'` makes it * appear wherever the thumb lands on the left half and vanish on release. */ joystick?: 'fixed' | 'dynamic'; /** Radians of camera turn per pixel of look-drag (default 0.006). */ lookSensitivity?: number; /** Fraction of the stick radius that counts as centred (default 0.15). */ deadzone?: number; /** * Per-button visibility, e.g. `{ crouch: false }`. Everything is on by * default; `use` and `throw` also show only while a hand item is held. */ buttons?: Partial>; /** Stack order of the overlay (default 10). Put the game's own UI above it. */ zIndex?: number; } /** * On-screen touch controls: a virtual joystick (left), drag-to-look with * pinch-to-zoom (pinching past minimum distance enters first person, like * the mouse wheel), and semi-transparent action buttons (right). Everything * is tracked per pointer, so stick + look + a button work simultaneously. * * Built automatically by `createController` on coarse-pointer devices — see * `ControllerOptions.touch`. A thin skin over the controller's public input * API; restyle it via the `.pa-touch*` CSS classes. */ export class TouchControls { constructor(controller: CharacterController, options?: TouchControlsOptions); /** Overlay root — style it, move it, or `hide()` it. */ readonly element: HTMLDivElement; /** * A tap on the empty look area (down and up without dragging): screen * coordinates, for the game's own tap-to-interact raycasts. Camera drags, * pinches and button presses never fire it. */ onTap: ((x: number, y: number) => void) | null; show(): void; hide(): void; setVisible(visible: boolean): void; dispose(): void; } // ------------------------------------------------------------------------- // Materials + helpers // ------------------------------------------------------------------------- export interface EyeTextureUrls { /** Eye albedo (Unity `_MainTex`). */ diffuse: string; /** Region mask (Unity `_MetallicGlossMap`): G = iris, R = lids. */ mask: string; } export function createGuardianEyeMaterial( textures: { diffuse: THREE.Texture; mask: THREE.Texture }, options?: { irisColor?: THREE.ColorRepresentation; lidsColor?: THREE.ColorRepresentation; tint?: THREE.ColorRepresentation; } ): THREE.MeshStandardMaterial; export function loadEyeTextures( urls: EyeTextureUrls ): Promise<{ diffuse: THREE.Texture; mask: THREE.Texture }>; export function setIrisColor( material: THREE.Material, color: THREE.ColorRepresentation ): boolean; /** Rebuild halves left unbuilt by an unapplied Blender mirror modifier. */ export function completeMirroredMeshes(root: THREE.Object3D): void; // ------------------------------------------------------------------------- // Customisation // ------------------------------------------------------------------------- export interface AvatarSetupOption { id: string; label: string; /** Present for colour options, so a UI can render a swatch. */ color?: string; } export interface AvatarSetupSection { key: 'bodyType' | 'skin' | 'hairColor' | 'eyes' | 'hairStyle' | 'facialHair'; label: string; kind: 'swatches' | 'list'; options: AvatarSetupOption[]; selected: string | null; } export interface AvatarSetupControllerOptions { avatars: PortalsAvatars; /** * A body-type switch reloads the model and disposes the old avatar with * its controller — rebuild anything bound to it here. */ onAvatarReplaced?: (avatar: GuardianAvatar) => void; onChange?: (config: GuardianConfig, avatar: GuardianAvatar) => void; } /** Headless customisation model — build your own UI on it. */ export class AvatarSetupController { constructor(avatar: GuardianAvatar, options: AvatarSetupControllerOptions); readonly avatar: GuardianAvatar; /** True while a body-type reload is in flight; disable inputs meanwhile. */ readonly loading: boolean; subscribe(listener: () => void): () => void; /** Sections filtered to what the loaded model actually supports. */ getSections(): AvatarSetupSection[]; getWearables(): Array<{ def: WearableDef; equipped: boolean; basic: boolean }>; /** `null` clears the option (bald / clean-shaven). */ select(key: AvatarSetupSection['key'], id: string | null): Promise; toggleWearable(id: string): Promise; setBodyType(bodyType: BodyType): Promise; } export interface SetupPanelOptions extends AvatarSetupControllerOptions { /** Defaults to `document.body`. */ container?: HTMLElement; /** Start open. Default false — the edge button opens it. */ open?: boolean; /** Show the small edge toggle. Default true. */ toggleButton?: boolean; /** Default 'left'. */ side?: 'left' | 'right'; title?: string; } export interface SetupPanel { readonly controller: AvatarSetupController; readonly element: HTMLElement; isOpen(): boolean; open(): void; close(): void; toggle(): boolean; destroy(): void; } /** Prefer `PortalsAvatars.createSetupPanel()`, which supplies `avatars`. */ export function createSetupPanel( avatar: GuardianAvatar, options: SetupPanelOptions ): SetupPanel; export interface ParsedPortalsAvatar { bodyType: BodyType; modelUrl: string; config: GuardianConfig; /** Wearable GLB URLs keyed by slot. */ wearables: Partial>; /** Ordered entries preserve repeated non-exclusive accessories. */ wearableEntries: Array<[WearableSlot, string]>; } export function parsePortalsAvatarUrl(url: string): ParsedPortalsAvatar; export function buildPortalsAvatarUrl(avatar: GuardianAvatar): string; }