Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion js/Controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export class Controls {
this.x = 0;
this.z = 0;

this.prevSpace = false;
this.prevGamepadFire = false;

// Touch state
this.touchActive = false;
this.touchDirX = 0;
Expand Down Expand Up @@ -112,6 +115,10 @@ export class Controls {
if ( this.keys[ 'KeyW' ] || this.keys[ 'ArrowUp' ] ) z += 1;
if ( this.keys[ 'KeyS' ] || this.keys[ 'ArrowDown' ] ) z -= 1;

const spaceDown = !! this.keys[ 'Space' ];
let fire = spaceDown && ! this.prevSpace;
this.prevSpace = spaceDown;

// Gamepad

const gamepads = navigator.getGamepads();
Expand All @@ -128,6 +135,10 @@ export class Controls {

if ( rt > 0.1 || lt > 0.1 ) z = rt - lt;

const gpFire = gp.buttons[ 0 ] ? !! gp.buttons[ 0 ].pressed : false;
if ( gpFire && ! this.prevGamepadFire ) fire = true;
this.prevGamepadFire = gpFire;

break;

}
Expand All @@ -152,7 +163,7 @@ export class Controls {
this.x = x;
this.z = z;

return { x, z, touchActive: this.touchActive };
return { x, z, touchActive: this.touchActive, fire };

}

Expand Down
126 changes: 126 additions & 0 deletions js/HitFX.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import * as THREE from 'three';

const RING_COUNT = 8;
const STAR_COUNT = 48;
const STARS_PER_BURST = 8;

const RING_LIFETIME = 0.45;
const RING_MAX_SCALE = 2.8;
const STAR_LIFETIME = 0.7;
const STAR_SPEED_MIN = 3;
const STAR_SPEED_RANGE = 3;
const STAR_UP_MIN = 2.5;
const STAR_UP_RANGE = 2;
const STAR_GRAVITY = 11;

const _ringGeom = new THREE.TorusGeometry( 0.4, 0.07, 8, 28 );
const _starGeom = new THREE.IcosahedronGeometry( 0.15, 0 );

export class HitFX {

constructor( scene ) {

this.scene = scene;

this.rings = [];
for ( let i = 0; i < RING_COUNT; i ++ ) {

const mat = new THREE.MeshBasicMaterial( { color: 0xffee55, transparent: true, opacity: 0, depthWrite: false } );
const mesh = new THREE.Mesh( _ringGeom, mat );
mesh.rotation.x = - Math.PI / 2;
mesh.visible = false;
scene.add( mesh );
this.rings.push( { mesh, material: mat, life: 0 } );

}
this.ringIndex = 0;

this.stars = [];
for ( let i = 0; i < STAR_COUNT; i ++ ) {

const mat = new THREE.MeshStandardMaterial( {
color: 0xfff29a, emissive: 0xffcc33, emissiveIntensity: 1.2,
roughness: 0.3, transparent: true, opacity: 0,
} );
const mesh = new THREE.Mesh( _starGeom, mat );
mesh.visible = false;
scene.add( mesh );
this.stars.push( {
mesh, material: mat, life: 0,
vx: 0, vy: 0, vz: 0,
rotX: 0, rotY: 0,
} );

}
this.starIndex = 0;

}

burst( x, y, z ) {

const ring = this.rings[ this.ringIndex ];
this.ringIndex = ( this.ringIndex + 1 ) % RING_COUNT;
ring.mesh.visible = true;
ring.mesh.position.set( x, y + 0.1, z );
ring.mesh.scale.setScalar( 0.2 );
ring.material.opacity = 1;
ring.life = RING_LIFETIME;

for ( let i = 0; i < STARS_PER_BURST; i ++ ) {

const star = this.stars[ this.starIndex ];
this.starIndex = ( this.starIndex + 1 ) % STAR_COUNT;

const angle = ( i / STARS_PER_BURST ) * Math.PI * 2 + Math.random() * 0.4;
const speed = STAR_SPEED_MIN + Math.random() * STAR_SPEED_RANGE;

star.mesh.visible = true;
star.mesh.position.set( x, y + 0.3, z );
star.mesh.scale.setScalar( 0.8 + Math.random() * 0.6 );
star.material.opacity = 1;
star.vx = Math.cos( angle ) * speed;
star.vz = Math.sin( angle ) * speed;
star.vy = STAR_UP_MIN + Math.random() * STAR_UP_RANGE;
star.rotX = ( Math.random() - 0.5 ) * 20;
star.rotY = ( Math.random() - 0.5 ) * 20;
star.life = STAR_LIFETIME;

}

}

update( dt ) {

for ( const r of this.rings ) {

if ( r.life <= 0 ) continue;
r.life -= dt;
const prog = 1 - Math.max( 0, r.life / RING_LIFETIME );
r.mesh.scale.setScalar( 0.2 + prog * RING_MAX_SCALE );
r.material.opacity = Math.max( 0, 1 - prog );
if ( r.life <= 0 ) r.mesh.visible = false;

}

for ( const s of this.stars ) {

if ( s.life <= 0 ) continue;
s.life -= dt;

s.mesh.position.x += s.vx * dt;
s.mesh.position.y += s.vy * dt;
s.mesh.position.z += s.vz * dt;
s.vy -= STAR_GRAVITY * dt;

s.mesh.rotation.x += s.rotX * dt;
s.mesh.rotation.y += s.rotY * dt;

s.material.opacity = Math.max( 0, s.life / STAR_LIFETIME );

if ( s.life <= 0 ) s.mesh.visible = false;

}

}

}
194 changes: 194 additions & 0 deletions js/NPC.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import * as THREE from 'three';
import { rigidBody, box, MotionType } from 'crashcat';

const NPC_HALF_EXTENTS = [ 0.5, 0.4, 1.0 ];
const HIT_DURATION = 2.2;
const HIT_SPIN_VEL = 22;
const SPIN_DECAY = 0.45;
const HIT_HOP_VEL = 5.5;
const HIT_GRAVITY = 18;
const HIT_TILT = 0.35;
const ARRIVAL_DIST_SQ = 4.0;
const DEFAULT_SPEED = 5.5;
const ROT_LERP = 5;

// Outer loop waypoints around the figure-8 track, in driving order
export const WAYPOINTS = [
[ 3.75, 11.25 ],
[ 3.75, 18.75 ],
[ -3.75, 18.75 ],
[ -11.25, 18.75 ],
[ -11.25, 11.25 ],
[ -11.25, -3.75 ],
[ -18.75, -11.25 ],
[ -18.75, -18.75 ],
[ -11.25, -18.75 ],
[ -3.75, -18.75 ],
[ 3.75, -18.75 ],
[ 3.75, -11.25 ],
[ 3.75, -3.75 ],
[ 3.75, 3.75 ],
];

function lerpAngle( a, b, t ) {

let diff = b - a;
while ( diff > Math.PI ) diff -= Math.PI * 2;
while ( diff < - Math.PI ) diff += Math.PI * 2;
return a + diff * t;

}

function nearestWaypointIndex( x, z ) {

let best = 0;
let bestDist = Infinity;
for ( let i = 0; i < WAYPOINTS.length; i ++ ) {

const [ wx, wz ] = WAYPOINTS[ i ];
const d = ( wx - x ) * ( wx - x ) + ( wz - z ) * ( wz - z );
if ( d < bestDist ) {

bestDist = d;
best = i;

}

}

return best;

}

export class NPC {

constructor( world, scene, modelSource, x, y, z, rotDeg, speed = DEFAULT_SPEED ) {

this.world = world;

this.mesh = modelSource.clone();
this.mesh.position.set( x, y, z );
this.mesh.rotation.y = THREE.MathUtils.degToRad( rotDeg + 180 );
this.mesh.traverse( ( c ) => {

if ( c.isMesh ) {

c.castShadow = true;
c.receiveShadow = true;

}

} );
scene.add( this.mesh );

this.bodyY = y + NPC_HALF_EXTENTS[ 1 ];
this.body = rigidBody.create( world, {
shape: box.create( { halfExtents: NPC_HALF_EXTENTS } ),
motionType: MotionType.KINEMATIC,
objectLayer: world._OL_MOVING,
position: [ x, this.bodyY, z ],
quaternion: [ 0, Math.sin( this.mesh.rotation.y / 2 ), 0, Math.cos( this.mesh.rotation.y / 2 ) ],
} );

this.speed = speed;
this.waypointIndex = ( nearestWaypointIndex( x, z ) + 1 ) % WAYPOINTS.length;

this.hitTimer = 0;
this.spinVel = 0;
this.hopVel = 0;
this.baseY = y;

}

hit() {

this.hitTimer = HIT_DURATION;
this.spinVel = HIT_SPIN_VEL * ( Math.random() < 0.5 ? - 1 : 1 );
this.hopVel = HIT_HOP_VEL;

}

update( dt ) {

if ( this.hitTimer > 0 ) {

this.hitTimer = Math.max( 0, this.hitTimer - dt );
this.mesh.rotation.y += this.spinVel * dt;
this.spinVel *= Math.max( 0, 1 - SPIN_DECAY * dt );

this.hopVel -= HIT_GRAVITY * dt;
this.mesh.position.y = Math.max( this.baseY, this.mesh.position.y + this.hopVel * dt );
if ( this.mesh.position.y <= this.baseY && this.hopVel < 0 ) {

this.hopVel *= - 0.35;
this.mesh.position.y = this.baseY;

}

const tiltPhase = this.hitTimer * 9;
this.mesh.rotation.z = Math.sin( tiltPhase ) * HIT_TILT * ( this.hitTimer / HIT_DURATION );
this.mesh.rotation.x = Math.cos( tiltPhase * 0.7 ) * HIT_TILT * 0.5 * ( this.hitTimer / HIT_DURATION );
return;

}

if ( this.mesh.rotation.z !== 0 || this.mesh.rotation.x !== 0 ) {

this.mesh.rotation.z *= Math.max( 0, 1 - dt * 6 );
this.mesh.rotation.x *= Math.max( 0, 1 - dt * 6 );

}

const [ tx, tz ] = WAYPOINTS[ this.waypointIndex ];
const dx = tx - this.mesh.position.x;
const dz = tz - this.mesh.position.z;
const distSq = dx * dx + dz * dz;

if ( distSq < ARRIVAL_DIST_SQ ) {

this.waypointIndex = ( this.waypointIndex + 1 ) % WAYPOINTS.length;
return;

}

const dist = Math.sqrt( distSq );
const dirX = dx / dist;
const dirZ = dz / dist;
const step = this.speed * dt;

const newX = this.mesh.position.x + dirX * step;
const newZ = this.mesh.position.z + dirZ * step;
this.mesh.position.x = newX;
this.mesh.position.z = newZ;

const targetRot = Math.atan2( dirX, dirZ );
this.mesh.rotation.y = lerpAngle( this.mesh.rotation.y, targetRot, Math.min( 1, dt * ROT_LERP ) );

const halfAngle = this.mesh.rotation.y * 0.5;
rigidBody.setPosition( this.world, this.body, [ newX, this.bodyY, newZ ], false );
rigidBody.setQuaternion( this.world, this.body, [ 0, Math.sin( halfAngle ), 0, Math.cos( halfAngle ) ], false );
rigidBody.setLinearVelocity( this.world, this.body, [ dirX * this.speed, 0, dirZ * this.speed ] );

}

}

export function createNPCs( world, scene, models, npcDefs ) {

const npcs = [];
const bodyToNPC = new Map();

for ( const [ key, x, y, z, rotDeg ] of npcDefs ) {

const src = models[ key ];
if ( ! src ) continue;

const npc = new NPC( world, scene, src, x, y, z, rotDeg );
npcs.push( npc );
bodyToNPC.set( npc.body, npc );

}

return { npcs, bodyToNPC };

}
Loading