Skip to main content

Pass

Pass

TypeNameInterface Description
VariablesblendState: BlendState

Function: The blend state of the pass, which defines how colors are blended during rendering.

Returns The blend state.

VariablescolorMask: ColorMask

Function: The color mask for the first attachment in the render state's color blend settings.

Returns The color mask.

VariablescullMode: CullMode

Function: The culling mode for the rendering pass, which determines how triangles are culled based on their orientation.

Returns The cull mode.

VariablesdepthFunction: DepthFunction

Function: The depth function used for depth comparison in the rendering pass.

Returns The depth function.

VariablesdepthTest: boolean

Function: Indicates whether depth testing is enabled for the pass.

Returns True if depth testing is enabled, false otherwise.

VariablesdepthWrite: boolean

Function: Indicates whether depth writing is enabled for the pass.

Returns True if depth writing is enabled, false otherwise.

VariablesstencilState: StencilState

Function: The stencil state of the pass.

Returns The stencil state.

Functionsconstructor()

Examples

constructor()

let obj = new APJS.Pass();

Use Case

Example 1 — Hoop / basketball-ring trigger scoring — detects a projectile passing through a horizontal hoop using two stacked thin BoxCollider triggers (TopTrigger above, B…

@component()
export class HoopScorer extends APJS.BasicScriptComponent {
@serializeProperty topTrigger!: APJS.SceneObject;
@serializeProperty bottomTrigger!: APJS.SceneObject;
@serializeProperty ball!: APJS.SceneObject;
@serializeProperty scoreText!: APJS.SceneObject;
@serializeProperty mode: number = 0;
@serializeProperty dualWindowSeconds: number = 0.6;
@serializeProperty gravityY: number = -9.81;

// Scene authoring (DSL):
// - TopTrigger: Empty Object @ (0, 1.7, -4) + BoxCollider size (1.5, 0.2, 1.5), isTangible=false.
// - BottomTrigger: Empty Object @ (0, 1.3, -4) + BoxCollider size (1.5, 0.2, 1.5), isTangible=false.
// - RimMarker: Cube scaled (0.2, 0.02, 0.2) at (0, 1.5, -4) -- visual only, no collider.
// - Ball: Sphere with RigidBody + SphereCollider; either drop-tested or fired by ChargeMeterThrow.
// - ScoreText: 2D Text in safe zone, sibling under 2D Camera.
//
// CollisionEvent footgun: pass the COLLIDER component, NOT the SceneObject, into
// APJS.EventManager.getObjectEmitter(...). Passing the SceneObject silently fails.
// Also: collider.emitCollisionEvent must be set to true on init -- default is false.

private inited = false;
private topCol!: APJS.BoxCollider;
private botCol!: APJS.BoxCollider;
private ballName = "";
private scoreTextComp!: APJS.Text;
private score = 0;
private topArmed = false;
private topArmTime = 0;
private now = 0;
private topHandler!: (e: APJS.IEvent) => void;
private bottomHandler!: (e: APJS.IEvent) => void;
private recordStartCb!: () => void;

private initOnce(): void {
this.topCol = this.topTrigger.getComponent("BoxCollider") as APJS.BoxCollider;
this.botCol = this.bottomTrigger.getComponent("BoxCollider") as APJS.BoxCollider;
if (!this.topCol || !this.botCol) return;
this.scoreTextComp = this.scoreText.getComponent("Text") as APJS.Text;
if (this.ball) this.ballName = this.ball.name;

this.topCol.emitCollisionEvent = true;
this.botCol.emitCollisionEvent = true;

this.topHandler = (e: APJS.IEvent) => this.onTopEnter(e);
this.bottomHandler = (e: APJS.IEvent) => this.onBottomEnter(e);
APJS.EventManager.getObjectEmitter(this.topCol).on(APJS.CollisionEvent.Enter, this.topHandler, this);
APJS.EventManager.getObjectEmitter(this.botCol).on(APJS.CollisionEvent.Enter, this.bottomHandler, this);

if (typeof this.gravityY === "number") {
const g = APJS.Physics3D.gravity;
APJS.Physics3D.gravity = new APJS.Vector3f(g.x, this.gravityY, g.z);
console.log("[HoopScorer] gravity set to y=" + this.gravityY);
}

this.recordStartCb = () => this.resetScore();
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.RecordStart, this.recordStartCb, this);

this.refreshScoreText();
console.log("[HoopScorer] init mode=" + this.mode + " ball=" + this.ballName);
this.inited = true;
}

private isBall(other: APJS.SceneObject | null): boolean {
if (!other) return false;
if (this.ballName && other.name === this.ballName) return true;
return !this.ballName;
}

private onTopEnter(event: APJS.IEvent): void {
const infos = event.args[0] as APJS.CollisionInfo[];
for (const info of infos) {
if (!this.isBall(info.otherObject)) continue;
console.log("[HoopScorer] TOP enter @t=" + this.now.toFixed(3));
if (this.mode === 0) {
this.score++;
this.refreshScoreText();
} else {
this.topArmed = true;
this.topArmTime = this.now;
}
return;
}
}

private onBottomEnter(event: APJS.IEvent): void {
const infos = event.args[0] as APJS.CollisionInfo[];
for (const info of infos) {
if (!this.isBall(info.otherObject)) continue;
console.log("[HoopScorer] BOTTOM enter @t=" + this.now.toFixed(3) + " armed=" + this.topArmed);
if (this.mode === 1 && this.topArmed) {
const dt = this.now - this.topArmTime;
if (dt <= this.dualWindowSeconds) {
this.score++;
this.refreshScoreText();
console.log("[HoopScorer] DUAL score! window=" + dt.toFixed(3) + "s");
} else {
console.log("[HoopScorer] DUAL miss \u2014 window expired (" + dt.toFixed(3) + "s)");
}
this.topArmed = false;
}
return;
}
}

private refreshScoreText(): void {
if (this.scoreTextComp) this.scoreTextComp.text = "Hoops: " + this.score;
}

private resetScore(): void {
this.score = 0;
this.topArmed = false;
this.topArmTime = 0;
this.refreshScoreText();
}

onUpdate(dt: number): void {
if (!this.inited) {
if (this.topTrigger && this.bottomTrigger && this.scoreText) {
this.initOnce();
}
return;
}
this.now += dt;
if (this.topArmed && this.now - this.topArmTime > this.dualWindowSeconds) {
this.topArmed = false;
}
}

onDestroy(): void {
if (this.topHandler && this.topCol) {
APJS.EventManager.getObjectEmitter(this.topCol).off(APJS.CollisionEvent.Enter, this.topHandler, this);
}
if (this.bottomHandler && this.botCol) {
APJS.EventManager.getObjectEmitter(this.botCol).off(APJS.CollisionEvent.Enter, this.bottomHandler, this);
}
if (this.recordStartCb) {
APJS.EventManager.getGlobalEmitter().off(APJS.EventType.RecordStart, this.recordStartCb, this);
}
}
}

Example 2 — 3D bowling-lane timing game — the ball auto-ping-pongs laterally in the ready state, touch Began locks the current lane line, hold duration maps linearly to lau…

@component()
export class BowlingGameManager extends APJS.BasicScriptComponent {
@serializeProperty ball: APJS.SceneObject;
@serializeProperty pins: APJS.SceneObject[] = [];
@serializeProperty scoreTextObject: APJS.SceneObject;
@serializeProperty resetButtonObject: APJS.SceneObject;

@serializeProperty minLaunchSpeed: number = 30;
@serializeProperty maxLaunchSpeed: number = 60;
@serializeProperty maxChargeSeconds: number = 1.4;
@serializeProperty settleVelocityThreshold: number = 0.12;
@serializeProperty settleAngularThreshold: number = 0.28;
@serializeProperty pinSettleFrames: number = 14;
@serializeProperty ballSettleFrames: number = 10;
@serializeProperty roundTimeoutSeconds: number = 5.8;
@serializeProperty fallenDotThreshold: number = 0.6;
@serializeProperty ballStartXClamp: number = 3.1;
@serializeProperty pinActivationZ: number = -1.5;
@serializeProperty aimPingPongSpeed: number = 2.2;

private static readonly STATE_READY = 0;
private static readonly STATE_CHARGING = 1;
private static readonly STATE_ROLLING = 2;
private static readonly STATE_SETTLING = 3;
private static readonly STATE_RESULT = 4;
private static readonly BALL_SPIN_FACTOR = 1.35;
private static readonly PIN_GRAVITY_DELAY_FRAMES = 3;
private static readonly PIN_FAST_SLEEP_FRAMES = 8;
private static readonly PIN_SLEEP_CLEARANCE_Z = 0.28;

private initialized = false;
private currentState = BowlingGameManager.STATE_READY;
private chargeSeconds = 0;
private roundElapsed = 0;
private ballStillFrames = 0;
private touchActive = false;
private score = 0;
private pinsActive = false;
private pinResetFramesRemaining = 0;
private pinGravityDelayFramesRemaining = 0;
private aimDirection = 1;
private lockedAimX = 0;
private debugApproachLogged = false;
private debugPastLogged = false;
private debugResultLogged = false;

private ballRigidBody!: APJS.RigidBody;
private ballTransform!: APJS.Transform;
private scoreText!: APJS.Text;
private resetButtonImage!: APJS.Image;
private ballStartPosition!: APJS.Vector3f;
private ballStartRotation!: APJS.Quaternionf;

private pinRigidBodies: APJS.RigidBody[] = [];
private pinTransforms: APJS.Transform[] = [];
private pinStartPositions: APJS.Vector3f[] = [];
private pinStartRotations: APJS.Quaternionf[] = [];
private pinLowMotionFrames: number[] = [];
private pinCounted: boolean[] = [];
private pinAssistApplied: boolean[] = [];

private touchCallback = (event: APJS.IEvent) => {
this.onTouch(event);
};

private onRecordStart = (_event: APJS.IEvent) => {
this.resetRound();
};

private logBallAndHeadPin(label: string): void {
if (this.pinRigidBodies.length === 0) {
console.log(label + " pins=0");
return;
}

const ballPos = this.ballTransform.getWorldPosition();
const headPos = this.pinTransforms[0].getWorldPosition();
const headRb = this.pinRigidBodies[0];
const headVel = headRb.velocity;
console.log(
label +
" pins=" + this.pinRigidBodies.length +
" ball=(" + ballPos.x.toFixed(3) + "," + ballPos.y.toFixed(3) + "," + ballPos.z.toFixed(3) + ")" +
" head=(" + headPos.x.toFixed(3) + "," + headPos.y.toFixed(3) + "," + headPos.z.toFixed(3) + ")" +
" headVel=(" + headVel.x.toFixed(3) + "," + headVel.y.toFixed(3) + "," + headVel.z.toFixed(3) + ")" +
" headStatic=" + headRb.static +
" headGravity=" + headRb.useGravity +
" headCounted=" + this.pinCounted[0]
);
}

private initOnce(): void {
if (!this.ball || this.pins.length === 0 || !this.scoreTextObject || !this.resetButtonObject) return;

this.ballRigidBody = this.ball.getComponent("RigidBody") as APJS.RigidBody;
this.ballTransform = this.ball.getTransform();
this.scoreText = this.scoreTextObject.getComponent("Text") as APJS.Text;
this.resetButtonImage = this.resetButtonObject.getComponent("Image") as APJS.Image;
if (!this.ballRigidBody || !this.ballTransform || !this.scoreText || !this.resetButtonImage) return;

const ballWorld = this.ballTransform.getWorldPosition();
this.ballStartPosition = new APJS.Vector3f(ballWorld.x, ballWorld.y, ballWorld.z);
this.ballStartRotation = this.ballTransform.getWorldRotation().clone();
this.lockedAimX = this.ballStartPosition.x;

this.pinRigidBodies = [];
this.pinTransforms = [];
this.pinStartPositions = [];
this.pinStartRotations = [];
this.pinLowMotionFrames = [];
this.pinCounted = [];
this.pinAssistApplied = [];

for (let i = 0; i < this.pins.length; i++) {
const pin = this.pins[i];
if (!pin) continue;
const rb = pin.getComponent("RigidBody") as APJS.RigidBody;
const tf = pin.getTransform();
if (!rb || !tf) continue;
this.pinRigidBodies.push(rb);
this.pinTransforms.push(tf);
const pos = tf.getWorldPosition();
this.pinStartPositions.push(new APJS.Vector3f(pos.x, pos.y, pos.z));
this.pinStartRotations.push(tf.getWorldRotation().clone());
this.pinLowMotionFrames.push(0);
this.pinCounted.push(false);
this.pinAssistApplied.push(false);
}

console.log("init pins=" + this.pinRigidBodies.length);
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.Touch, this.touchCallback, this);
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.RecordStart, this.onRecordStart, this);
this.initialized = true;
this.resetRound();
}

private resetRigidBody(rb: APJS.RigidBody, position: APJS.Vector3f, rotation: APJS.Quaternionf, useGravity: boolean): void {
rb.static = false;
rb.velocity = new APJS.Vector3f(0, 0, 0);
rb.angularVelocity = new APJS.Vector3f(0, 0, 0);
rb.position = new APJS.Vector3f(position.x, position.y, position.z);
rb.rotation = rotation.clone();
rb.useGravity = useGravity;
}

private freezePin(index: number): void {
const rb = this.pinRigidBodies[index];
rb.velocity = new APJS.Vector3f(0, 0, 0);
rb.angularVelocity = new APJS.Vector3f(0, 0, 0);
rb.useGravity = false;
rb.static = true;
}

private forcePinsToStartPose(): void {
for (let i = 0; i < this.pinRigidBodies.length; i++) {
const rb = this.pinRigidBodies[i];
const tf = this.pinTransforms[i];
const startPosition = this.pinStartPositions[i];
const startRotation = this.pinStartRotations[i];
rb.static = false;
rb.velocity = new APJS.Vector3f(0, 0, 0);
rb.angularVelocity = new APJS.Vector3f(0, 0, 0);
rb.useGravity = false;
rb.position = new APJS.Vector3f(startPosition.x, startPosition.y, startPosition.z);
rb.rotation = startRotation.clone();
tf.setWorldPosition(new APJS.Vector3f(startPosition.x, startPosition.y, startPosition.z));
tf.setWorldRotation(startRotation.clone());
rb.static = true;
}
}

private setPinsActive(active: boolean): void {
this.pinsActive = active;
if (active) {
this.pinResetFramesRemaining = 0;
this.pinGravityDelayFramesRemaining = BowlingGameManager.PIN_GRAVITY_DELAY_FRAMES;
for (let i = 0; i < this.pinRigidBodies.length; i++) {
const rb = this.pinRigidBodies[i];
rb.static = false;
rb.velocity = new APJS.Vector3f(0, 0, 0);
rb.angularVelocity = new APJS.Vector3f(0, 0, 0);
rb.useGravity = false;
}
return;
}

this.pinGravityDelayFramesRemaining = 0;
this.pinResetFramesRemaining = 3;
this.forcePinsToStartPose();
}

private updatePinGravityDelay(): void {
if (!this.pinsActive || this.pinGravityDelayFramesRemaining <= 0) return;
this.pinGravityDelayFramesRemaining -= 1;
if (this.pinGravityDelayFramesRemaining > 0) return;

for (let i = 0; i < this.pinRigidBodies.length; i++) {
const rb = this.pinRigidBodies[i];
if (!rb.static) {
rb.useGravity = true;
}
}
}

private resetRound(): void {
if (!this.initialized) return;
this.currentState = BowlingGameManager.STATE_READY;
this.chargeSeconds = 0;
this.roundElapsed = 0;
this.ballStillFrames = 0;
this.touchActive = false;
this.score = 0;
this.aimDirection = 1;
this.lockedAimX = this.ballStartPosition.x;
this.debugApproachLogged = false;
this.debugPastLogged = false;
this.debugResultLogged = false;

this.resetRigidBody(this.ballRigidBody, this.ballStartPosition, this.ballStartRotation, false);
this.setPinsActive(false);

for (let i = 0; i < this.pinRigidBodies.length; i++) {
this.pinLowMotionFrames[i] = 0;
this.pinCounted[i] = false;
this.pinAssistApplied[i] = false;
}

this.updateHud();
}

private updateHud(): void {
if (!this.scoreText) return;
if (this.currentState === BowlingGameManager.STATE_READY) {
this.scoreText.text = "Tap and hold";
return;
}
if (this.currentState === BowlingGameManager.STATE_CHARGING) {
const power = Math.min(100, Math.round((this.chargeSeconds / this.maxChargeSeconds) * 100));
this.scoreText.text = "Power: " + power + "%";
return;
}
if (this.currentState === BowlingGameManager.STATE_RESULT) {
this.scoreText.text = "Final: " + this.score + "/10";
return;
}
if (this.currentState === BowlingGameManager.STATE_ROLLING || this.currentState === BowlingGameManager.STATE_SETTLING) {
this.scoreText.text = "Score: " + this.score + "/10";
return;
}
}

private isBallIdle(): boolean {
const v = this.ballRigidBody.velocity;
const a = this.ballRigidBody.angularVelocity;
const v2 = v.x * v.x + v.y * v.y + v.z * v.z;
const a2 = a.x * a.x + a.y * a.y + a.z * a.z;
return v2 < this.settleVelocityThreshold * this.settleVelocityThreshold && a2 < this.settleAngularThreshold * this.settleAngularThreshold;
}

private startCharging(): void {
this.touchActive = true;
this.chargeSeconds = 0;
this.currentState = BowlingGameManager.STATE_CHARGING;
this.updateHud();
}

private onTouch(event: APJS.IEvent): void {
if (!this.initialized) return;
const touch = event.args[0] as APJS.TouchData;

if (touch.phase === APJS.TouchPhase.Began && APJS.TouchUtils.isScreenPointOnImage(touch.position, this.resetButtonImage)) {
this.resetRound();
return;
}

if (this.currentState === BowlingGameManager.STATE_ROLLING || this.currentState === BowlingGameManager.STATE_SETTLING) {
return;
}

if (touch.phase === APJS.TouchPhase.Began) {
if (this.currentState === BowlingGameManager.STATE_RESULT) {
return;
}
if (this.currentState === BowlingGameManager.STATE_READY) {
this.lockedAimX = this.ballRigidBody.position.x;
this.startCharging();
return;
}
}

if ((touch.phase === APJS.TouchPhase.Ended || touch.phase === APJS.TouchPhase.Canceled) && this.touchActive && this.currentState === BowlingGameManager.STATE_CHARGING) {
this.touchActive = false;
this.launchBall();
}
}

private launchBall(): void {
const ratio = Math.max(0, Math.min(1, this.chargeSeconds / this.maxChargeSeconds));
const speed = this.minLaunchSpeed + (this.maxLaunchSpeed - this.minLaunchSpeed) * ratio;
const spinSpeed = speed * BowlingGameManager.BALL_SPIN_FACTOR;

if (!this.pinsActive) {
this.setPinsActive(true);
}

this.ballRigidBody.velocity = new APJS.Vector3f(0, 0, -speed);
this.ballRigidBody.angularVelocity = new APJS.Vector3f(spinSpeed, 0, 0);
this.ballRigidBody.useGravity = true;

this.logBallAndHeadPin("launch speed=" + speed.toFixed(3) + " aimX=" + this.lockedAimX.toFixed(3));
this.currentState = BowlingGameManager.STATE_ROLLING;
this.roundElapsed = 0;
this.ballStillFrames = 0;
this.updateHud();
}

private isPinTilted(index: number): boolean {
const up = this.pinTransforms[index].getWorldRotation().multiplyVector(new APJS.Vector3f(0, 0, -1));
return up.y < this.fallenDotThreshold;
}

private maybeAssistPins(): void {
for (let i = 0; i < this.pinRigidBodies.length; i++) {
if (this.pinAssistApplied[i] || this.pinCounted[i] || this.isPinTilted(i)) continue;

const rb = this.pinRigidBodies[i];
if (rb.static) continue;

const velocity = rb.velocity;
const horizontalSpeed2 = velocity.x * velocity.x + velocity.z * velocity.z;
if (horizontalSpeed2 < 0.08) continue;

const horizontalSpeed = Math.sqrt(horizontalSpeed2);
const invLen = 1 / horizontalSpeed;
const dirX = velocity.x * invLen;
const dirZ = velocity.z * invLen;
const liftBias = 0.18;
const impulse = Math.max(3.6, Math.min(7.2, horizontalSpeed * 4.8));
const pinPos = this.pinTransforms[i].getWorldPosition();
const pushForce = new APJS.Vector3f(dirX * impulse * 0.35, impulse * liftBias, dirZ * impulse * 0.55);
const pushPoint = new APJS.Vector3f(pinPos.x, pinPos.y + 0.18, pinPos.z);
rb.addForceAt(pushForce, pushPoint, false, APJS.ForceMode3D.Impulse);
rb.addTorque(new APJS.Vector3f(dirZ * impulse * 0.22, 0, -dirX * impulse * 0.22), APJS.ForceMode3D.Impulse);
this.pinAssistApplied[i] = true;
}
}

private updatePinScoreState(): void {
let counted = 0;
for (let i = 0; i < this.pinRigidBodies.length; i++) {
const rb = this.pinRigidBodies[i];
const isTilted = this.isPinTilted(i);
const v = rb.velocity;
const a = rb.angularVelocity;
const v2 = v.x * v.x + v.y * v.y + v.z * v.z;
const a2 = a.x * a.x + a.y * a.y + a.z * a.z;
const isLowMotion = v2 < this.settleVelocityThreshold * this.settleVelocityThreshold && a2 < this.settleAngularThreshold * this.settleAngularThreshold;

if (isLowMotion) {
this.pinLowMotionFrames[i] += 1;
} else {
this.pinLowMotionFrames[i] = 0;
}

if (!this.pinCounted[i] && isTilted) {
this.pinCounted[i] = true;
}
if (this.pinCounted[i]) counted++;
}

if (counted !== this.score) {
this.score = counted;
this.updateHud();
}
}

private freezeQuietPinsAfterBallPass(ballZ: number): void {
for (let i = 0; i < this.pinRigidBodies.length; i++) {
const rb = this.pinRigidBodies[i];
if (rb.static) continue;
if (this.pinLowMotionFrames[i] < BowlingGameManager.PIN_FAST_SLEEP_FRAMES) continue;

if (this.pinCounted[i]) {
this.freezePin(i);
continue;
}

const pinZ = this.pinTransforms[i].getWorldPosition().z;
const ballPassedPin = ballZ <= pinZ - BowlingGameManager.PIN_SLEEP_CLEARANCE_Z;
if (ballPassedPin && !this.isPinTilted(i)) {
this.freezePin(i);
}
}
}

private freezeQuietPinsNow(): void {
for (let i = 0; i < this.pinRigidBodies.length; i++) {
const rb = this.pinRigidBodies[i];
if (rb.static) continue;
if (this.pinLowMotionFrames[i] >= BowlingGameManager.PIN_FAST_SLEEP_FRAMES) {
this.freezePin(i);
}
}
}

private updateAimPingPong(dt: number): void {
const minX = this.ballStartPosition.x - this.ballStartXClamp;
const maxX = this.ballStartPosition.x + this.ballStartXClamp;
let targetX = this.ballRigidBody.position.x + this.aimPingPongSpeed * this.aimDirection * dt;

if (targetX >= maxX) {
targetX = maxX;
this.aimDirection = -1;
} else if (targetX <= minX) {
targetX = minX;
this.aimDirection = 1;
}

this.resetRigidBody(this.ballRigidBody, new APJS.Vector3f(targetX, this.ballStartPosition.y, this.ballStartPosition.z), this.ballStartRotation, false);
}

private updateRollingState(dt: number): void {
this.roundElapsed += dt;

const ballPos = this.ballTransform.getWorldPosition();
if (!this.debugApproachLogged && ballPos.z <= -1.1) {
this.debugApproachLogged = true;
this.logBallAndHeadPin("approach");
}
if (!this.debugPastLogged && ballPos.z <= -1.95) {
this.debugPastLogged = true;
this.logBallAndHeadPin("past");
}

if (!this.pinsActive && ballPos.z <= this.pinActivationZ) {
this.setPinsActive(true);
}

this.updatePinGravityDelay();
//this.maybeAssistPins();

if (this.pinsActive) {
this.updatePinScoreState();
this.freezeQuietPinsAfterBallPass(ballPos.z);
}

if (this.isBallIdle()) {
this.ballStillFrames += 1;
} else {
this.ballStillFrames = 0;
}

if (this.ballStillFrames >= this.ballSettleFrames || ballPos.z < -2.8 || this.roundElapsed >= this.roundTimeoutSeconds) {
if (!this.pinsActive) {
this.setPinsActive(true);
}
this.currentState = BowlingGameManager.STATE_SETTLING;
this.ballStillFrames = 0;
this.updateHud();
}
}

private updateSettlingState(dt: number): void {
this.roundElapsed += dt;
this.updatePinGravityDelay();
//this.maybeAssistPins();
this.updatePinScoreState();
this.freezeQuietPinsNow();

let allPinsQuiet = true;
for (let i = 0; i < this.pinRigidBodies.length; i++) {
if (this.pinLowMotionFrames[i] < this.pinSettleFrames) {
allPinsQuiet = false;
break;
}
}

if (!this.debugResultLogged && (allPinsQuiet || this.roundElapsed >= this.roundTimeoutSeconds + 1.5)) {
this.debugResultLogged = true;
this.logBallAndHeadPin("result");
}

if (allPinsQuiet || this.roundElapsed >= this.roundTimeoutSeconds + 1.5) {
this.currentState = BowlingGameManager.STATE_RESULT;
this.updatePinScoreState();
this.updateHud();
}
}

private updateResultState(): void {
this.updatePinScoreState();
this.freezeQuietPinsNow();
}

onUpdate(dt: number): void {
if (!this.initialized) {
this.initOnce();
return;
}

if (this.pinResetFramesRemaining > 0) {
this.forcePinsToStartPose();
this.pinResetFramesRemaining -= 1;
}

if (this.currentState === BowlingGameManager.STATE_READY) {
this.updateAimPingPong(dt);
if (this.pinsActive) {
this.setPinsActive(false);
}
return;
}

if (this.currentState === BowlingGameManager.STATE_CHARGING) {
this.chargeSeconds += dt;
if (this.chargeSeconds > this.maxChargeSeconds) {
this.chargeSeconds = this.maxChargeSeconds;
}
this.resetRigidBody(this.ballRigidBody, new APJS.Vector3f(this.lockedAimX, this.ballStartPosition.y, this.ballStartPosition.z), this.ballStartRotation, false);
if (this.pinsActive) {
this.setPinsActive(false);
}
this.updateHud();
return;
}

if (this.currentState === BowlingGameManager.STATE_ROLLING) {
this.updateRollingState(dt);
return;
}

if (this.currentState === BowlingGameManager.STATE_SETTLING) {
this.updateSettlingState(dt);
return;
}

if (this.currentState === BowlingGameManager.STATE_RESULT) {
this.updateResultState();
}
}

onDestroy(): void {
APJS.EventManager.getGlobalEmitter().off(APJS.EventType.Touch, this.touchCallback, this);
APJS.EventManager.getGlobalEmitter().off(APJS.EventType.RecordStart, this.onRecordStart, this);
}
}
Copyright © 2026 TikTok. All rights reserved.
About TikTokHelp CenterCareersContactLegalTerms of ServicePrivacy PolicyCookies