JSPM

spoint

0.1.248
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 126
  • Score
    100M100P100Q96094F
  • License MIT

Physics and netcode SDK for multiplayer game servers

Package Exports

    This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (spoint) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

    Readme

    Spawnpoint

    Multiplayer game server SDK with authoritative physics, hot reload, and app-based world logic.

    Quick Start

    npm install
    npm start
    # open http://localhost:3001

    server.js runs scaffold() first, then boots the server.

    Common Commands

    node ./bin/create-app.js my-app
    node run-bots.js 50 30000 60
    node run-profiling.js
    • create-app: generate apps/<name>/index.js from a template
    • bots: start bot harness (count durationMs inputHz)
    • profile: run included 50-bot profiling harness

    Architecture

    server.js                    Entry point, calls boot()
    src/sdk/server.js            Creates all subsystems, wires them together
    src/sdk/TickHandler.js       Per-tick: movement -> physics -> collisions -> app tick -> snapshot
    src/sdk/ReloadManager.js     File watchers for SDK hot reload
    src/apps/AppRuntime.js       Entity system, app lifecycle, timers, collision events
    src/apps/AppLoader.js        Loads apps from apps/ directory, validates, watches for changes
    src/physics/World.js          Jolt Physics wrapper (bodies, characters, raycasts)
    src/netcode/PhysicsIntegration.js  CharacterVirtual per player, gravity application
    src/connection/ConnectionManager.js  WebSocket client management, heartbeat, msgpack encode/decode
    src/protocol/msgpack.js       Hand-rolled msgpack encoder/decoder
    client/app.js                Three.js renderer, VRM loading, entity rendering, input loop

    World Config

    apps/world/index.js exports the world definition:

    export default {
      port: 3001,
      tickRate: 64,
      gravity: [0, -9.81, 0],
      movement: { maxSpeed: 4.0, groundAccel: 10.0, airAccel: 1.0, friction: 6.0, stopSpeed: 2.0, jumpImpulse: 4.0 },
      player: { health: 100, capsuleRadius: 0.4, capsuleHalfHeight: 0.9, mass: 120, modelScale: 1.323, feetOffset: 0.212 },
      scene: { skyColor: 0x87ceeb, sunColor: 0xffffff, sunIntensity: 1.5, sunPosition: [21, 50, 20] },
      camera: { fov: 70, shoulderOffset: 0.35, zoomStages: [0, 1.5, 3, 5, 8], defaultZoomIndex: 2 },
      animation: { mixerTimeScale: 1.3, walkTimeScale: 2.0, sprintTimeScale: 0.56, fadeTime: 0.15 },
      entities: [
        { id: 'environment', model: './apps/tps-game/schwust.glb', position: [0, 0, 0], app: 'environment' },
        { id: 'game', position: [0, 0, 0], app: 'tps-game' }
      ],
      playerModel: './apps/tps-game/Cleetus.vrm',
      spawnPoint: [-35, 3, -65]
    }

    Creating Apps

    Create an app file:

    node ./bin/create-app.js my-app

    Or manually create apps/<name>/index.js:

    export default {
      server: {
        setup(ctx) {
          // Called once on spawn and on hot reload
          // ctx.state persists across hot reloads
          ctx.state.counter = ctx.state.counter || 0
        },
        update(ctx, dt) {
          // Called every tick (128/sec)
          ctx.state.counter += dt
        },
        teardown(ctx) {
          // Called on destroy or before hot reload
        },
        onMessage(ctx, msg) {
          // Receives player_join, player_leave, fire, and custom APP_EVENT messages
          if (msg.type === 'player_join') { /* ... */ }
        },
        onInteract(ctx, player) {
          // Called when client sends APP_EVENT with this entity's ID
        },
        onCollision(ctx, other) {
          // Called when this entity's collider overlaps another entity's collider
        }
      },
      client: {
        setup(engine) {
          // engine.scene, engine.camera, engine.renderer, engine.THREE, engine.client, engine.cam
        },
        render(ctx) {
          // Return visual state. ctx.entity, ctx.state, ctx.h (createElement), ctx.engine, ctx.players
          return {
            position: ctx.entity.position,
            custom: { mesh: 'box', color: 0xff0000, sx: 1, sy: 1, sz: 1 },
            ui: ctx.h('div', {}, 'Hello')
          }
        },
        onInput(input, engine) { },
        onFrame(dt, engine) { },
        onEvent(payload, engine) { },
        onMouseDown(e, engine) { },
        onMouseUp(e, engine) { }
      }
    }

    Server-Side ctx API

    Property Description
    ctx.state Persistent state object (survives hot reload)
    ctx.entity Entity proxy: .id, .position, .rotation, .scale, .velocity, .custom, .destroy()
    ctx.physics .addBoxCollider(size), .addSphereCollider(r), .addCapsuleCollider(r, h), .addTrimeshCollider(), .setDynamic(bool), .addForce(vec3), .setVelocity(vec3)
    ctx.world .spawn(id, cfg), .destroy(id), .attach(eid, app), .getEntity(id), .query(filter), .nearby(pos, radius)
    ctx.players .getAll(), .getNearest(pos, r), .send(pid, msg), .broadcast(msg), .setPosition(pid, pos)
    ctx.time .tick, .deltaTime, .elapsed, .after(sec, fn), .every(sec, fn)
    ctx.bus .on(channel, fn), .emit(channel, data), .once(channel, fn), .handover(targetEntityId, state)
    ctx.network .broadcast(msg), .sendTo(id, msg)
    ctx.storage .get(key), .set(key, val), .delete(key), .list(prefix), .has(key)
    ctx.config Entity config passed from world definition
    ctx.raycast(origin, dir, maxDist) Physics raycast against world geometry

    Client Engine API

    Property Description
    engine.scene THREE.Scene
    engine.camera THREE.PerspectiveCamera
    engine.renderer THREE.WebGLRenderer
    engine.THREE Three.js module
    engine.client PhysicsNetworkClient instance
    engine.playerId Local player ID
    engine.cam Camera controller: .yaw, .pitch, .getAimDirection(pos), .setMode(m), .applyConfig(cfg)
    engine.players .getMesh(id), .getState(id), .getAnimator(id), .setExpression(id, name, val)
    engine.createElement webjsx createElement for UI

    Entity Custom Mesh Properties

    When an entity has no model, the client builds geometry from entity.custom:

    Field Description
    mesh 'box' (default), 'cylinder', 'sphere'
    color Hex color (default 0xff8800)
    sx, sy, sz Box dimensions
    r Radius for sphere/cylinder
    h Height for cylinder
    spin Y-axis rotation speed (radians/sec)
    hover Vertical bob amplitude
    light PointLight color
    lightIntensity, lightRange PointLight params
    emissive, emissiveIntensity Material emissive

    EventBus Channels

    Apps communicate via ctx.bus:

    // Publisher
    ctx.bus.emit('combat.fire', { shooterId, origin, direction })
    
    // Subscriber (supports wildcard)
    ctx.bus.on('combat.*', (event) => {
      // event.channel, event.data, event.meta.sourceEntity, event.meta.timestamp
    })

    Scoped subscriptions auto-cleanup on entity destroy/hot reload.

    App Editor Guide

    For app-authoring workflow, lifecycle rules, and a complete checklist, see:

    Dependencies

    • jolt-physics - WASM physics engine
    • ws - WebSocket server
    • webjsx - JSX-like DOM diffing for client UI
    • d3-octree - Spatial indexing

    License

    MIT