Skip to main content

Documentation Index

Fetch the complete documentation index at: https://v5.rpgjs.dev/llms.txt

Use this file to discover all available pages before exploring further.

Projectiles

Use projectiles for arrows, spells, bullets, beams, and other temporary moving effects that need server-side impact validation without synchronizing their position every frame. The server is authoritative. It emits compact projectile spawn, impact, and destroy batches. The client predicts the visual movement locally, including a single local raycast at spawn time to avoid drawing projectiles past obvious client-side hitboxes while it waits for the server impact, and renders each projectile with a registered CanvasEngine component. When a projectile uses a custom server-side canHit filter, the client keeps predicting movement but skips local impact raycast clamping by default because arbitrary server gameplay rules cannot be represented safely on the client. If the filter still matches client-visible physics, opt back in with collision.predictImpact: true.

Server Usage

Emit a projectile from a player:
import { Direction, RpgPlayerHooks } from '@rpgjs/server'

export const player: RpgPlayerHooks = {
  onInput(player, input) {
    if (input.action !== 'attack') return

    player.projectiles.emit({
      type: 'fireball',
      direction: player.getDirection(),
      trajectory: {
        type: 'linear',
        speed: 420,
        range: 600,
        ttl: 1.5
      },
      payload: {
        damage: 20,
        element: 'fire'
      },
      params: {
        sprite: 'fireball',
        radius: 8,
        trail: true
      }
    })
  }
}
payload stays server-side and is useful for damage, states, knockback, or any other gameplay data. params is sent to clients and should contain only visual data used by the projectile component. You can also emit from the map:
map.projectiles.emit({
  type: 'arrow',
  origin: { x: 100, y: 200 },
  direction: Direction.Right,
  trajectory: {
    type: 'linear',
    speed: 500,
    range: 700
  }
})

Shoot Toward The Pointer

Projectiles can also use a direction vector. For pointer-based attacks, send the target position through the normal action input flow, then validate and resolve the shot on the server.
// client map component
const target = client.pointer.world()

if (target) {
  client.processAction('projectile:shoot', {
    source: 'map-click',
    target
  })
}
For keyboard attacks, configure the action key to send the same payload:
import { provideClientGlobalConfig } from '@rpgjs/client'

provideClientGlobalConfig({
  keyboardControls: {
    up: 'up',
    down: 'down',
    left: 'left',
    right: 'right',
    action: {
      bind: 'space',
      action: 'projectile:shoot',
      data: (client) => ({
        source: 'keyboard',
        target: client.pointer.world()
      })
    },
    escape: 'escape'
  }
})
// server player hook
import type { RpgPlayerHooks } from '@rpgjs/server'

function normalize(vector: { x: number, y: number }) {
  const length = Math.hypot(vector.x, vector.y)
  if (!Number.isFinite(length) || length <= 0) return null
  return { x: vector.x / length, y: vector.y / length }
}

export const player: RpgPlayerHooks = {
  onInput(player, input) {
    if (input?.action !== 'projectile:shoot') return

    const target = input.data?.target
    if (!target || !Number.isFinite(target.x) || !Number.isFinite(target.y)) {
      return
    }

    const hitbox = player.hitbox()
    const origin = {
      x: player.x() + hitbox.w / 2,
      y: player.y() + hitbox.h / 2
    }
    const direction = normalize({
      x: target.x - origin.x,
      y: target.y - origin.y
    })
    if (!direction) return

    player.projectiles.emit({
      type: 'fireball',
      origin,
      direction,
      trajectory: {
        type: 'linear',
        speed: 420,
        range: 600
      },
      payload: {
        damage: 20
      }
    })
  }
}
This same input shape can be reused for future map or event interactions, for example map:click with a world position or event:click with an event id. The client sends intent and context; the server still owns validation, collisions, damage, and impacts.

Repeats And Patterns

Use repeat for a compact burst. The server broadcasts one spawn batch with per-projectile delays, so clients can render the whole burst without receiving a position update every frame.
player.projectiles.emit({
  type: 'spark',
  direction: player.getDirection(),
  trajectory: {
    type: 'linear',
    speed: 700,
    range: 500
  },
  repeat: {
    count: 8,
    interval: 50,
    spread: 12,
    seed: true
  }
})
Use pattern for common shapes:
player.projectiles.emit({
  type: 'ice-shard',
  direction: player.getDirection(),
  trajectory: {
    type: 'linear',
    speed: 480,
    range: 500
  },
  pattern: {
    type: 'cone',
    count: 5,
    angle: 45
  }
})
player.projectiles.emit({
  type: 'magic-orb',
  trajectory: {
    type: 'linear',
    speed: 260,
    range: 400
  },
  pattern: {
    type: 'circle',
    count: 12
  }
})

Impact Hooks

Use projectile hooks to apply gameplay effects when the server confirms an impact:
import { RpgServer } from '@rpgjs/server'

export const server: RpgServer = {
  projectiles: {
    onImpact({ projectile, target }) {
      if (!target) return

      const damage = Number(projectile.payload?.damage ?? 0)
      target.hp -= damage
    },

    onDestroy({ projectile, reason }) {
      console.log(projectile.id, reason)
    }
  }
}
You can also filter hits per projectile:
player.projectiles.emit({
  type: 'holy-bolt',
  direction: player.getDirection(),
  trajectory: {
    type: 'linear',
    speed: 450,
    range: 700
  },
  canHit({ owner, target }) {
    return Boolean(target && target.team !== owner?.team)
  }
})
If the canHit filter only narrows server gameplay targets and the normal map physics hitboxes are still safe for visual prediction, enable local clamping:
player.projectiles.emit({
  type: 'bolt',
  direction: player.getDirection(),
  trajectory: {
    type: 'linear',
    speed: 450,
    range: 700
  },
  collision: {
    ignoreOwner: true,
    predictImpact: true
  },
  canHit({ target }) {
    return target?.id === 'target' || !target
  }
})

Client Registration

Register a CanvasEngine component for each projectile type:
import { RpgClient } from '@rpgjs/client'
import FireballProjectile from './components/fireball-projectile.ce'

export const client: RpgClient = {
  projectiles: {
    components: {
      fireball: FireballProjectile
    }
  }
}

Projectile Component

Projectile components receive predicted movement props from the client runtime.
<!-- components/fireball-projectile.ce -->
<Container x={x} y={y} rotation={angle}>
  <Sprite sheet={sheet} anchor={0.5} scale={scale} />
</Container>

<script>
  import { computed } from 'canvasengine'
  import { inject, RpgClientEngine } from '@rpgjs/client'

  const {
    x,
    y,
    angle,
    speed,
    range,
    distance,
    progress,
    impactProgress,
    elapsed,
    direction,
    params,
    impact
  } = defineProps()

  const engine = inject(RpgClientEngine)

  const sheet = computed(() => ({
    definition: engine.getSpriteSheet(params()?.sprite ?? 'fireball'),
    playing: impact() ? 'impact' : 'default'
  }))

  const scale = computed(() => 1 + progress() * 0.2)
</script>
Common props:
  • id, type, ownerId
  • x, y, origin, direction, angle
  • speed, range, ttl, distance, elapsed, progress
  • impactProgress from 0 to 1 while the client keeps an impacted projectile visible for its impact animation
  • index, count for repeated or patterned projectiles
  • params for visual customization
  • impact when the server confirms a collision. While impact is present, x, y, and distance are pinned to the authoritative impact point. Before the server confirmation arrives, the client may also pin x, y, and distance to a locally predicted impact point; impact remains undefined until the server confirms the collision.
Do not apply damage in the component. Components are visual only; gameplay effects belong in server projectile hooks.