AIArchives / archives /rblx /app /public /js /engine /physics.js
CompactAI's picture
Upload 126 files
f6b8770 verified
Raw
History Blame Contribute Delete
6.54 kB
// AABB character controller. Player is an axis-aligned box; world parts are
// axis-aligned boxes. Movement resolved per-axis with auto-step.
import * as THREE from 'three';
import { SOLID_TYPES } from './world.js';
export const PLAYER = { hw: 0.45, hh: 1.5 }; // half-width, half-height
const STEP_HEIGHT = 0.6;
export class CharacterController {
constructor(world, config) {
this.world = world;
this.config = config;
this.pos = new THREE.Vector3(0, 6, 0);
this.vel = new THREE.Vector3();
this.grounded = false;
this.groundPart = null;
this.groundPrev = new THREE.Vector3();
this.inWater = false;
this.onSpeedPad = false;
this.touching = []; // special parts overlapping this frame
}
teleport(v) {
this.pos.copy(v);
this.vel.set(0, 0, 0);
this.groundPart = null;
}
// collision candidates near the player
_solids() {
const out = [];
const c = new THREE.Vector3();
for (const part of this.world.parts.values()) {
if (!SOLID_TYPES.has(part.type)) continue;
this.world.partCenter(part, c);
if (Math.abs(c.x - this.pos.x) > part.sx / 2 + 8) continue;
if (Math.abs(c.y - this.pos.y) > part.sy / 2 + 8) continue;
if (Math.abs(c.z - this.pos.z) > part.sz / 2 + 8) continue;
out.push({
part,
minX: c.x - part.sx / 2, maxX: c.x + part.sx / 2,
minY: c.y - part.sy / 2, maxY: c.y + part.sy / 2,
minZ: c.z - part.sz / 2, maxZ: c.z + part.sz / 2,
});
}
return out;
}
_overlaps(b, px, py, pz, pad = 0) {
return px + PLAYER.hw + pad > b.minX && px - PLAYER.hw - pad < b.maxX &&
py + PLAYER.hh + pad > b.minY && py - PLAYER.hh - pad < b.maxY &&
pz + PLAYER.hw + pad > b.minZ && pz - PLAYER.hw - pad < b.maxZ;
}
/**
* @param dt seconds
* @param input { moveX, moveZ (camera-space -1..1), jump, yaw (camera yaw) }
*/
step(dt, input) {
dt = Math.min(dt, 0.05);
const cfg = this.config;
const speedMul = this.onSpeedPad ? 1.8 : 1;
const walk = cfg.walkSpeed * speedMul;
// camera-relative move direction
const sin = Math.sin(input.yaw), cos = Math.cos(input.yaw);
let dx = input.moveX * cos - input.moveZ * sin;
let dz = -input.moveX * sin - input.moveZ * cos;
const len = Math.hypot(dx, dz);
if (len > 1) { dx /= len; dz /= len; }
// mover carry: ride platforms
if (this.groundPart && this.groundPart.type === 'mover') {
const c = this.world.partCenter(this.groundPart);
const delta = c.clone().sub(this.groundPrev);
this.pos.add(delta);
this.groundPrev.copy(c);
}
if (this.inWater) {
this.vel.y += -cfg.gravity * 0.12 * dt;
this.vel.y *= 0.92;
if (input.jump) this.vel.y = Math.min(this.vel.y + cfg.gravity * 0.5 * dt + 1.2, 8);
this.vel.x = dx * walk * 0.65;
this.vel.z = dz * walk * 0.65;
} else {
this.vel.y -= cfg.gravity * dt;
this.vel.x = dx * walk;
this.vel.z = dz * walk;
if (input.jump && this.grounded) {
this.vel.y = cfg.jumpPower;
this.grounded = false;
if (this.onJump) this.onJump();
}
}
if (this.vel.y < -80) this.vel.y = -80;
const solids = this._solids();
this.grounded = false;
const prevGround = this.groundPart;
this.groundPart = null;
// --- Y axis ---
this.pos.y += this.vel.y * dt;
for (const b of solids) {
if (!this._overlaps(b, this.pos.x, this.pos.y, this.pos.z)) continue;
if (this.vel.y <= 0 && this.pos.y > (b.minY + b.maxY) / 2) {
this.pos.y = b.maxY + PLAYER.hh;
this.vel.y = 0;
this.grounded = true;
this.groundPart = b.part;
} else if (this.vel.y > 0) {
this.pos.y = b.minY - PLAYER.hh;
this.vel.y = 0;
}
}
// --- X then Z with auto-step ---
this._moveAxis(solids, 'x', this.vel.x * dt);
this._moveAxis(solids, 'z', this.vel.z * dt);
if (this.groundPart && this.groundPart !== prevGround && this.groundPart.type === 'mover') {
this.world.partCenter(this.groundPart, this.groundPrev);
} else if (this.groundPart && this.groundPart.type === 'mover' && this.groundPart === prevGround) {
// groundPrev already tracked
}
// --- special-part touch detection ---
this.touching.length = 0;
this.inWater = false;
this.onSpeedPad = false;
const c = new THREE.Vector3();
for (const part of this.world.parts.values()) {
if (SOLID_TYPES.has(part.type) && part.type !== 'kill' && part.type !== 'bounce' &&
part.type !== 'speed' && part.type !== 'tele' && part.type !== 'checkpoint' && part.type !== 'goal') continue;
this.world.partCenter(part, c);
const pad = part.type === 'coin' ? 0.6 : 0.15;
const b = {
minX: c.x - part.sx / 2, maxX: c.x + part.sx / 2,
minY: c.y - part.sy / 2, maxY: c.y + part.sy / 2,
minZ: c.z - part.sz / 2, maxZ: c.z + part.sz / 2,
};
if (!this._overlaps(b, this.pos.x, this.pos.y, this.pos.z, pad)) continue;
if (part.type === 'water') { this.inWater = true; continue; }
if (part.type === 'speed') { this.onSpeedPad = true; continue; }
if (part.type === 'deco' || part.type === 'block' || part.type === 'spawn' || part.type === 'mover') continue;
this.touching.push(part);
}
// standing directly on pads also counts
if (this.groundPart && ['bounce', 'speed', 'tele', 'checkpoint', 'goal', 'kill'].includes(this.groundPart.type)) {
if (this.groundPart.type === 'speed') this.onSpeedPad = true;
else if (!this.touching.includes(this.groundPart)) this.touching.push(this.groundPart);
}
}
_moveAxis(solids, axis, delta) {
if (delta === 0) return;
this.pos[axis] += delta;
for (const b of solids) {
if (!this._overlaps(b, this.pos.x, this.pos.y, this.pos.z)) continue;
// try auto-step: small ledges walk up
const stepY = b.maxY + PLAYER.hh;
if (this.grounded && stepY - this.pos.y <= STEP_HEIGHT && stepY - this.pos.y > 0) {
let clear = true;
for (const o of solids) {
if (o === b) continue;
if (this._overlaps(o, this.pos.x, stepY, this.pos.z)) { clear = false; break; }
}
if (clear) { this.pos.y = stepY + 0.001; continue; }
}
const hw = PLAYER.hw;
if (axis === 'x') {
this.pos.x = delta > 0 ? b.minX - hw : b.maxX + hw;
} else {
this.pos.z = delta > 0 ? b.minZ - hw : b.maxZ + hw;
}
}
}
}