Appearance
Client Sprite Hooks
Sprite hooks allow you to customize the behavior of sprites (players and events) on the client side. These hooks are defined in the sprite property of your client module.
Usage
ts
import { RpgSprite, RpgSpriteHooks, defineModule } from '@rpgjs/client'
const sprite: RpgSpriteHooks = {
onInit(sprite: RpgSprite) {
console.log(`Sprite ${sprite.id} initialized`)
},
onChanges(sprite: RpgSprite, data: any, old: any) {
console.log(`Sprite ${sprite.id} data changed`)
}
}
export default defineModule({
sprite
})Available Hooks
onInit
Description: Called when a sprite is initialized on the client
Parameters:
sprite: RpgSprite- The sprite instance
Example:
ts
const sprite: RpgSpriteHooks = {
onInit(sprite: RpgSprite) {
console.log(`🎭 Sprite ${sprite.id} initialized`)
// Set default properties
sprite.opacity = 1.0
sprite.scale = { x: 1, y: 1 }
// Initialize based on sprite type
if (sprite.type === 'player') {
// Add player-specific visual effects
sprite.addGlow('#00ff00', 0.3)
sprite.showNameplate(sprite.name)
// Add health bar
sprite.healthBar = sprite.addChild('health-bar', {
width: 32,
height: 4,
offsetY: -40
})
}
if (sprite.type === 'npc') {
// Add interaction indicator
sprite.interactionIcon = sprite.addChild('interaction-icon', {
graphic: 'exclamation',
visible: false,
offsetY: -50
})
}
if (sprite.type === 'enemy') {
// Add enemy visual effects
sprite.addGlow('#ff0000', 0.2)
sprite.showHealthBar()
}
// Play spawn animation
sprite.playAnimation('spawn')
}
}onDestroy
Description: Called when a sprite is removed from the scene
Parameters:
sprite: RpgSprite- The sprite instance
Example:
ts
const sprite: RpgSpriteHooks = {
onDestroy(sprite: RpgSprite) {
console.log(`🗑️ Sprite ${sprite.id} destroyed`)
// Clean up visual effects
if (sprite.glow) {
sprite.removeGlow()
}
if (sprite.nameplate) {
sprite.nameplate.destroy()
}
if (sprite.healthBar) {
sprite.healthBar.destroy()
}
// Play destruction animation
if (sprite.type === 'enemy') {
sprite.playAnimation('death', () => {
// Animation complete callback
sprite.showParticleEffect('explosion')
})
}
// Clean up event listeners
sprite.removeAllListeners()
// Clear timers
if (sprite.timers) {
sprite.timers.forEach(timer => clearTimeout(timer))
}
}
}onChanges
Description: Called when sprite data changes (from server updates)
Parameters:
sprite: RpgSprite- The sprite instancedata: any- New dataold: any- Previous data
Example:
ts
const sprite: RpgSpriteHooks = {
onChanges(sprite: RpgSprite, data: any, old: any) {
console.log(`📊 Sprite ${sprite.id} data updated`)
// Handle health changes
if (data.hp !== old.hp) {
const healthPercent = data.hp / data.maxHp
// Update health bar
if (sprite.healthBar) {
sprite.healthBar.setPercent(healthPercent)
}
// Show damage/healing numbers
const difference = data.hp - old.hp
if (difference < 0) {
// Damage taken
sprite.showFloatingText(`-${Math.abs(difference)}`, '#ff0000')
sprite.playAnimation('hit')
sprite.shake(200, 3)
} else if (difference > 0) {
// Healing received
sprite.showFloatingText(`+${difference}`, '#00ff00')
sprite.playAnimation('heal')
}
// Low health warning
if (healthPercent < 0.25) {
sprite.addGlow('#ff0000', 0.5)
sprite.startBlinking()
} else {
sprite.removeGlow()
sprite.stopBlinking()
}
}
// Handle level changes
if (data.level !== old.level) {
sprite.showFloatingText('LEVEL UP!', '#ffff00', {
size: 24,
duration: 3000
})
sprite.playAnimation('level-up')
sprite.showParticleEffect('level-up-sparkles')
}
// Handle equipment changes
if (data.equipment !== old.equipment) {
sprite.updateEquipmentVisuals(data.equipment)
}
// Handle state changes
if (data.states !== old.states) {
sprite.updateStateVisuals(data.states, old.states)
}
// Handle name changes
if (data.name !== old.name) {
if (sprite.nameplate) {
sprite.nameplate.setText(data.name)
}
}
}
}onUpdate
Description: Called at each frame for sprite updates
Parameters:
sprite: RpgSprite- The sprite instanceobj: any- Update data
Example:
ts
const sprite: RpgSpriteHooks = {
onUpdate(sprite: RpgSprite, obj: any) {
// Update custom animations
if (sprite.customAnimations) {
sprite.customAnimations.forEach(animation => {
animation.update(obj.deltaTime)
})
}
// Update floating elements
if (sprite.nameplate) {
sprite.nameplate.followSprite(sprite)
}
if (sprite.healthBar) {
sprite.healthBar.followSprite(sprite)
}
// Update particle effects
if (sprite.particleEffects) {
sprite.particleEffects.forEach(effect => {
effect.update(obj.deltaTime)
})
}
// Handle breathing animation for idle sprites
if (sprite.isIdle && !sprite.isMoving) {
const breathingScale = 1 + Math.sin(obj.time * 0.002) * 0.02
sprite.scale.y = breathingScale
}
// Update glow effects
if (sprite.glowEffect) {
const glowIntensity = 0.3 + Math.sin(obj.time * 0.005) * 0.2
sprite.glowEffect.intensity = glowIntensity
}
// Update interaction indicators
if (sprite.interactionIcon && sprite.type === 'npc') {
const player = sprite.scene.getPlayerSprite()
if (player && sprite.distanceTo(player) < 64) {
sprite.interactionIcon.visible = true
sprite.interactionIcon.y = -50 + Math.sin(obj.time * 0.01) * 5
} else {
sprite.interactionIcon.visible = false
}
}
}
}onMove
Description: Called when the sprite's position changes
Parameters:
sprite: RpgSprite- The sprite instance
Example:
ts
const sprite: RpgSpriteHooks = {
onMove(sprite: RpgSprite) {
console.log(`🚶 Sprite ${sprite.id} moved to (${sprite.x}, ${sprite.y})`)
// Create footstep particles
if (sprite.type === 'player' && sprite.isOnGround) {
sprite.createFootstepParticles()
}
// Update shadow position
if (sprite.shadow) {
sprite.shadow.x = sprite.x
sprite.shadow.y = sprite.y + sprite.height
}
// Play movement sound
if (sprite.isMoving && !sprite.movementSound?.isPlaying) {
const terrain = sprite.scene.getTerrainAt(sprite.x, sprite.y)
sprite.playMovementSound(terrain)
}
// Update camera if this is the main player
if (sprite.isMainPlayer) {
sprite.scene.camera.followSprite(sprite)
}
// Check for area triggers
const areas = sprite.scene.getAreasAt(sprite.x, sprite.y)
areas.forEach(area => {
if (!sprite.currentAreas.includes(area.id)) {
sprite.onEnterArea(area)
}
})
// Update lighting if sprite has a light source
if (sprite.lightSource) {
sprite.lightSource.x = sprite.x
sprite.lightSource.y = sprite.y
}
}
}Complete Example
ts
import { RpgSprite, RpgSpriteHooks, defineModule } from '@rpgjs/client'
const sprite: RpgSpriteHooks = {
onInit(sprite: RpgSprite) {
console.log(`🎭 Initializing sprite: ${sprite.id} (${sprite.type})`)
// Common initialization
sprite.opacity = 1.0
sprite.customData = {}
sprite.visualEffects = []
sprite.timers = []
// Type-specific initialization
switch (sprite.type) {
case 'player':
this.initializePlayer(sprite)
break
case 'npc':
this.initializeNPC(sprite)
break
case 'enemy':
this.initializeEnemy(sprite)
break
case 'item':
this.initializeItem(sprite)
break
}
// Play spawn animation
sprite.playAnimation('spawn', () => {
sprite.isReady = true
})
},
onDestroy(sprite: RpgSprite) {
console.log(`🗑️ Destroying sprite: ${sprite.id}`)
// Clean up visual effects
sprite.visualEffects.forEach(effect => effect.destroy())
// Clear timers
sprite.timers.forEach(timer => clearTimeout(timer))
// Remove UI elements
if (sprite.nameplate) sprite.nameplate.destroy()
if (sprite.healthBar) sprite.healthBar.destroy()
if (sprite.interactionIcon) sprite.interactionIcon.destroy()
// Play destruction effect
if (sprite.type === 'enemy') {
sprite.scene.addParticleEffect('explosion', sprite.x, sprite.y)
}
},
onChanges(sprite: RpgSprite, data: any, old: any) {
// Health changes
if (data.hp !== old.hp) {
this.handleHealthChange(sprite, data.hp, old.hp, data.maxHp)
}
// Level changes
if (data.level !== old.level) {
this.handleLevelUp(sprite, data.level, old.level)
}
// Equipment changes
if (JSON.stringify(data.equipment) !== JSON.stringify(old.equipment)) {
this.updateEquipmentVisuals(sprite, data.equipment)
}
// State changes
if (JSON.stringify(data.states) !== JSON.stringify(old.states)) {
this.updateStateEffects(sprite, data.states, old.states)
}
},
onUpdate(sprite: RpgSprite, { deltaTime, time }) {
// Update custom animations
sprite.visualEffects.forEach(effect => effect.update(deltaTime))
// Update UI elements
if (sprite.nameplate) {
sprite.nameplate.x = sprite.x
sprite.nameplate.y = sprite.y - sprite.height - 10
}
// Breathing animation for idle sprites
if (!sprite.isMoving && sprite.isReady) {
const breathScale = 1 + Math.sin(time * 0.003) * 0.01
sprite.scale.y = breathScale
}
// Update interaction indicators
if (sprite.interactionIcon) {
const player = sprite.scene.getMainPlayer()
const distance = sprite.distanceTo(player)
sprite.interactionIcon.visible = distance < 64
if (sprite.interactionIcon.visible) {
sprite.interactionIcon.y = sprite.y - sprite.height - 20 + Math.sin(time * 0.01) * 3
}
}
},
onMove(sprite: RpgSprite) {
// Create movement particles
if (sprite.type === 'player' && sprite.isMoving) {
const terrain = sprite.scene.getTerrainAt(sprite.x, sprite.y)
this.createMovementParticles(sprite, terrain)
}
// Update camera for main player
if (sprite.isMainPlayer) {
sprite.scene.camera.centerOn(sprite.x, sprite.y)
}
// Update lighting
if (sprite.lightSource) {
sprite.lightSource.position.set(sprite.x, sprite.y)
}
},
// Helper methods
initializePlayer(sprite: RpgSprite) {
sprite.addGlow('#00ff00', 0.2)
sprite.nameplate = sprite.scene.addUI('nameplate', {
text: sprite.name,
x: sprite.x,
y: sprite.y - sprite.height - 10
})
sprite.healthBar = sprite.scene.addUI('health-bar', {
maxValue: sprite.maxHp,
currentValue: sprite.hp,
x: sprite.x - 16,
y: sprite.y - sprite.height - 25
})
},
initializeNPC(sprite: RpgSprite) {
sprite.interactionIcon = sprite.scene.addUI('interaction-icon', {
graphic: 'chat-bubble',
x: sprite.x,
y: sprite.y - sprite.height - 20,
visible: false
})
},
initializeEnemy(sprite: RpgSprite) {
sprite.addGlow('#ff0000', 0.3)
sprite.healthBar = sprite.scene.addUI('enemy-health-bar', {
maxValue: sprite.maxHp,
currentValue: sprite.hp,
x: sprite.x - 16,
y: sprite.y - sprite.height - 30
})
},
initializeItem(sprite: RpgSprite) {
sprite.addGlow('#ffff00', 0.4)
sprite.startFloating()
},
handleHealthChange(sprite: RpgSprite, newHp: number, oldHp: number, maxHp: number) {
const difference = newHp - oldHp
if (difference < 0) {
// Damage
sprite.showFloatingText(`-${Math.abs(difference)}`, '#ff0000')
sprite.playAnimation('hit')
sprite.shake(150, 2)
} else if (difference > 0) {
// Healing
sprite.showFloatingText(`+${difference}`, '#00ff00')
sprite.playAnimation('heal')
}
// Update health bar
if (sprite.healthBar) {
sprite.healthBar.setValue(newHp, maxHp)
}
// Low health effects
const healthPercent = newHp / maxHp
if (healthPercent < 0.25) {
sprite.addStatusEffect('low-health')
} else {
sprite.removeStatusEffect('low-health')
}
},
handleLevelUp(sprite: RpgSprite, newLevel: number, oldLevel: number) {
sprite.showFloatingText('LEVEL UP!', '#ffff00', { size: 24, duration: 3000 })
sprite.playAnimation('level-up')
sprite.scene.addParticleEffect('level-up-sparkles', sprite.x, sprite.y)
sprite.scene.playSound('level-up')
}
}
export default defineModule({
sprite
})