Health and Damage - Basics

Learn the fundamentals of health tracking and damage systems in multiplayer games.

What You’ll Learn

  • Basic health tracking
  • Damage application
  • Health bars with visual feedback
  • Invincibility frames

Basic Health System

Use Case: Any game with player health

Simple health tracking with damage application.

Step 1: Define State

Start with basic health properties in your game state:

import { defineGame } from '@martini-kit/core';

export const game = defineGame({
  setup: ({ playerIds }) => ({
    players: Object.fromEntries(
      playerIds.map((id) => [
        id,
        {
          x: 400,
          y: 300,
          health: 100,
          maxHealth: 100,
        },
      ])
    ),
  }),
});

Key Points:

  • health: Current health value
  • maxHealth: Maximum health for calculating percentages

Step 2: Add Damage Action

Create an action to apply damage:

actions: {
  takeDamage: {
    apply: (state, context, input: { amount: number }) => {
      const player = state.players[context.targetId]; // IMPORTANT: Use targetId!
      if (!player) return;

      // Apply damage
      player.health -= input.amount;

      // Clamp to 0
      if (player.health < 0) {
        player.health = 0;
      }

      console.log(`Player ${context.targetId} took ${input.amount} damage. Health: ${player.health}`);
    },
  },
}

Why targetId?

  • context.targetId: The player receiving damage (correct)
  • context.playerId: The player who initiated the action (wrong for damage recipient)

Step 3: Add Healing Action

Balance damage with healing:

heal: {
  apply: (state, context, input: { amount: number }) => {
    const player = state.players[context.targetId];
    if (!player) return;

    // Apply healing
    player.health += input.amount;

    // Clamp to max
    if (player.health > player.maxHealth) {
      player.health = player.maxHealth;
    }
  },
},

Step 4: Render Health Bar

Create a visual health bar that updates automatically.

Using HealthBarManager - Automatic health bars above sprites:

import Phaser from 'phaser';
import { PhaserAdapter, SpriteManager, HealthBarManager } from '@martini-kit/phaser';

export class GameScene extends Phaser.Scene {
  private adapter!: PhaserAdapter;
  private spriteManager!: SpriteManager;
  private healthBars!: HealthBarManager;

  create() {
    this.adapter = new PhaserAdapter(runtime, this);

    // Create sprite manager for players
    this.spriteManager = new SpriteManager(this.adapter, this, {
      collection: 'players',
      createSprite: (player) => {
        return this.add.circle(player.x, player.y, 20, 0x00aaff);
      },
      updateSprite: (sprite, player) => {
        sprite.x = player.x;
        sprite.y = player.y;
      },
    });

    // Auto-attach health bars to all player sprites
    this.healthBars = new HealthBarManager(this.adapter, {
      spriteManager: this.spriteManager,
      healthKey: 'health',
      maxHealth: 100,
      offset: { x: 0, y: -30 },
      width: 50,
      height: 5,
      colorThresholds: {
        high: { value: 50, color: 0x48bb78 },   // Green > 50%
        medium: { value: 25, color: 0xeab308 }, // Yellow > 25%
        low: { value: 0, color: 0xef4444 },     // Red <= 25%
      },
    });
  }

  update() {
    // Auto-updates all health bars
    this.healthBars.update();
  }
}

Benefits:

  • ✅ Auto-creates health bars for all sprites
  • ✅ Auto-positions above sprites
  • ✅ Auto-scales based on health
  • ✅ Auto-colors based on thresholds
  • ✅ Just 2 lines of code!

What You’ve Built:

  • ✅ Health tracking system
  • ✅ Damage and healing actions
  • ✅ Visual health bar with color coding
  • ✅ Automatic updates on state changes

Damage with Invincibility Frames

Use Case: Prevent instant death from multiple hits

Temporary invincibility after taking damage prevents unfair rapid elimination.

Step 1: Add Invincibility State

Extend player state with invincibility tracking:

const INVINCIBILITY_DURATION = 1000; // ms

export const game = defineGame({
  setup: ({ playerIds }) => ({
    players: Object.fromEntries(
      playerIds.map((id) => [
        id,
        {
          x: 400,
          y: 300,
          health: 100,
          isInvulnerable: false,
          invulnerabilityTimer: 0, // ms remaining
        },
      ])
    ),
  }),
});

Step 2: Check Invincibility Before Damage

Update damage action to respect invincibility:

actions: {
  takeDamage: {
    apply: (state, context, input: { amount: number; attackerId?: string }) => {
      const player = state.players[context.targetId];
      if (!player) return;

      // Check invincibility
      if (player.isInvulnerable) {
        console.log('Player is invulnerable!');
        return; // No damage applied
      }

      // Apply damage
      player.health -= input.amount;

      // Grant invincibility
      player.isInvulnerable = true;
      player.invulnerabilityTimer = INVINCIBILITY_DURATION;

      console.log(`Damage dealt by ${input.attackerId || 'unknown'}: ${input.amount}`);
    },
  },
}

Step 3: Countdown Invincibility Timer

Use a tick action to decrement the timer:

tick: {
  apply: (state, context, input: { delta: number }) => {
    // Update invincibility timers for all players
    for (const player of Object.values(state.players)) {
      if (player.isInvulnerable) {
        player.invulnerabilityTimer -= input.delta;

        if (player.invulnerabilityTimer <= 0) {
          player.isInvulnerable = false;
          player.invulnerabilityTimer = 0;
        }
      }
    }
  },
},

Important: Call this action from your Phaser update() loop:

update(time: number, delta: number) {
  this.runtime.dispatchAction('tick', { delta }, { broadcast: true });
}

Step 4: Visual Feedback

Show invincibility with sprite flashing.

Using SpriteManager - Automatic sprite flashing with updateSprite callback:

import { PhaserAdapter, SpriteManager } from '@martini-kit/phaser';

create() {
  this.adapter = new PhaserAdapter(runtime, this);

  // Auto-manages player sprites with invincibility effects
  this.spriteManager = new SpriteManager(this.adapter, this, {
    collection: 'players',
    createSprite: (player) => {
      return this.add.circle(player.x, player.y, 20, 0x00aaff);
    },
    updateSprite: (sprite, player) => {
      sprite.x = player.x;
      sprite.y = player.y;

      // Flash sprite when invulnerable
      if (player.isInvulnerable) {
        const flashPhase = Math.floor(Date.now() / 100) % 2;
        sprite.setAlpha(flashPhase === 0 ? 0.3 : 1.0);
      } else {
        sprite.setAlpha(1.0);
      }
    },
  });
}

Benefits:

  • ✅ Automatic sprite lifecycle management
  • ✅ Built-in update callback for effects
  • ✅ Minimal boilerplate

What You’ve Built:

  • ✅ Invincibility frames after damage
  • ✅ Prevents spam damage
  • ✅ Visual flashing feedback
  • ✅ Timed duration with automatic removal

See Also