npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@darkcore/game-library

v1.9.29

Published

## Table of Contents - [Overview](#overview) - [Installation](#installation) - [Integration](#integration-guide) - [Asset Loading](#asset-loading-system) - [AutoPlay](#autoplay-system) - [Responsive Grid](#responsive-grid-system) - [Spine Animation](#spin

Readme

DracoFusion Game Library

Table of Contents

Overview

DracoFusion is a game development library built on top of PixiJS, providing components and utilities for creating web-based games with @pixi/react.

Installation

Version Requirements

This library requires specific versions of its peer dependencies:

"peerDependencies": {
  "@pixi/react": "7.1.1",
  "howler": "2.2.4",
  "pixi-spine": "4.0.4",
  "pixi.js": "7.4.0",
  "react": "^18.3.1",
  "react-dom": "^18.3.1"
}

React Version Resolution

If you encounter React version conflicts, update your Vite configuration:

import tailwindcss from '@tailwindcss/vite'
import path from "path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
      react: path.resolve(__dirname, "node_modules/react"),
      "react-dom": path.resolve(__dirname, "node_modules/react-dom")
    },
  },
})

Installation Steps

# 1. Cleanup old installs
rm -rf node_modules pnpm-lock.yaml
pnpm store prune

# 2. Install peer dependencies with exact versions
pnpm add @pixi/[email protected] [email protected] [email protected] [email protected]

# 3. Install everything
pnpm install

# 4. Verify only one @pixi/react is present
pnpm why @pixi/react

Integration Guide

Application Structure

DracoFusion uses a layered approach:

  1. DarkCoreProvider: Top-level provider for theming
  2. Board & BoardContent: Layout components from @darkcore/ui
  3. GameCanvas: The Stage capsule for @pixi/react components
  4. GameBoard: Your custom @pixi/react components

Basic implementation:

import { DarkCoreProvider, Board, BoardContent } from "@darkcore/ui";
import GameCanvas from "./components/GameCanvas";
import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(
  <DarkCoreProvider theme={{ /* theme options */ }}>
    <Board>
      <BoardContent>
        <GameCanvas />
      </BoardContent>
    </Board>
  </DarkCoreProvider>
);

GameCanvas Component

The GameCanvas component manages the WebGL rendering context:

import { useEffect } from "react";
import { GameCanvasProvider } from "@darkcore/game-library";
import { useBoardSizes } from "@darkcore/ui";
import { useGameStore } from "@/store/game";
import { STAGE_OPTIONS } from "@/lib/constants";
import { Container } from "@pixi/react";

export default function GameCanvas() {
  // Get layout bounds from the BoardContent component
  const { width, height } = useBoardSizes();
  
  // Canvas dimensions in global state
  const { canvasWidth, canvasHeight } = useGameStore((s) => s);
  
  // Update canvas dimensions in global state
  useEffect(() => {
    setTimeout(() => {
      useGameStore.setState({ canvasWidth: width, canvasHeight: height });
    }, 25);
  }, [width, height]);

  return (
    <GameCanvasProvider
      settings={{
        width: canvasWidth,
        height: canvasHeight,
        options: STAGE_OPTIONS
      }}
    >
      <Container>
        {/* Your @pixi/react components */}
      </Container>
    </GameCanvasProvider>
  );
}

GameCanvasProvider Configuration

// Types
export type SettingsProps = {
  /** canvas pixel width */
  width: number;
  /** canvas pixel height */
  height: number;
  /** Partial PIXI.ApplicationOptions */
  options: Partial<IApplicationOptions>;
  /** optional FPS cap; defaults to 120 */
  maxFPS?: number;
};

// Component type
export const GameCanvasProvider: React.FC<{
  settings: SettingsProps;
  children: React.ReactNode;
}>;

Asset Loading System

The asset loading system manages images, sounds, fonts, and spine animations.

Asset Types

// Type definitions
export type ImagesResult = Record<string, string>;
export type SpinesResult = Record<string, Spine>;
export type SoundsResult = Record<string, Howl>;
export type FontsResult = Record<string, { url: string; weight: string }>;
export type AssetsRequest = Record<string, string>;
export type FontsRequest = Record<string, { url: string; weight: string }>;

Asset Configuration

Define your assets in a file:

// lib/assets.ts
export const ASSETS: UseAssetsLoaderConfig = {
  images: {
    background: "/assets/images/background.webp",
  },
  fonts: {
    Inter: { url: "/assets/fonts/Inter.ttf", weight: "600" }
  },
  spines: {
    dice: "/assets/spines/Dice.json"
  },
  sounds: {
    SFX_click: "/assets/sounds/SFX_click.m4a",
  }
}

export interface UseAssetsLoaderConfig {
    images?: AssetsRequest;
    fonts?: FontsRequest;
    spines?: AssetsRequest;
    sounds?: AssetsRequest;
}

Asset Loader Hook

export interface UseAssetsLoaderResult {
  images: ImagesResult; 
  fonts: FontsResult; 
  spines: SpinesResult; 
  sounds: SoundsResult; 
  isAllAssetsLoaded: boolean; 
  error?: Error | null;  
}

// The exported hook
export const useAssetsLoader: (config: UseAssetsLoaderConfig) => UseAssetsLoaderResult;

Asset Access Hooks

// Specialized asset access hooks
export const useGetImages: () => ImagesResult;
export const useGetSounds: () => SoundsResult;
export const useGetSpines: () => SpinesResult;
export const useGetFonts: () => FontsResult;
export const useGetAllAssets: () => useGetAssetsResult;

export interface useGetAssetsResult {
    images: ImagesResult;
    spines: SpinesResult;
    sounds: SoundsResult;
    fonts: FontsResult;
    error: Error | null;
}

Game State Store

// store/game.ts
import { create } from "zustand";

type GameState = {
  isAllAssetsLoaded: boolean;
  canvasWidth: number;
  canvasHeight: number;
};

export const useGameStore = create<GameState>(() => ({
  isAllAssetsLoaded: false,
  canvasWidth: 0,
  canvasHeight: 0,
}));

Game Initialization Hook

// hooks/useInitializeGame.ts
import { useEffect } from "react";
import { useAssetsLoader } from "@darkcore/game-library";
import { ASSETS } from "@/lib/assets";
import { useGameStore } from "@/store/game";

export const useInitializeGame = () => {
  const { isAllAssetsLoaded } = useAssetsLoader(ASSETS);

  useEffect(() => {
    if (isAllAssetsLoaded) {
      useGameStore.setState({
        isAllAssetsLoaded: true
      });
    }
  }, [isAllAssetsLoaded]);
};

Application Initialization

// components/IFrameGuard.tsx
import { useInitializeGame } from "@/hooks/useInitializeGame";
import Layout from "../_Layout/_Layout";

export default function IFrameGuard() {
  // Initialize game assets
  useInitializeGame();

  return <Layout />;
}

Note: Place initialization code and one-time setup logic in the IFrameGuard component to prevent unnecessary re-renders.

// components/_Layout/_Layout.tsx
import GameCanvas from "../GameCanvas/GameCanvas";
import { Loader, Board, BoardContent } from "@darkcore/ui";
import { useState, useEffect } from "react";
import { useGameStore } from "@/store/game.ts";

export default function _Layout() {
  const [loading, setLoading] = useState(true);
  const isAllAssetsLoaded = useGameStore((s) => s.isAllAssetsLoaded);

  useEffect(() => {
    if (isAllAssetsLoaded) {
      // Add delay for appearing
      setTimeout(() => {
        setLoading(false);
      }, 500);
    }
  }, [isAllAssetsLoaded]);

  return (
    <>
      {loading && <Loader
        isVisible={loading}
        size={40}
        placeholder="Just a moment"
      />}

      <Board>
        <BoardContent>
          {/* GameCanvas renders only when assets are loaded */}
          {isAllAssetsLoaded && <GameCanvas />}
        </BoardContent>
      </Board>
    </>
  );
}

Using Assets in Components

import { 
  useGetImages, 
  useGetSounds, 
  useGetSpines,
  useGetFonts,
  useGetAllAssets
} from "@darkcore/game-library";
import { Container, Sprite, Text } from "@pixi/react";
import { TextStyle } from "pixi.js";

export default function GameBoard() {
  // Get only the assets you need
  const images = useGetImages();
  const sounds = useGetSounds();
  const spines = useGetSpines();
  const fonts = useGetFonts();
  // Or get all assets at once
  const allAssets = useGetAllAssets();
  
  const handleOnPointerDown = () => {
    sounds.SFX_click.play();
  };

  return (
    <Container>
      <Sprite 
        image={images.background} 
        interactive={true} 
        onpointerdown={handleOnPointerDown} 
      />
      
      {/* Fonts can be used directly as strings */}
      <Text 
        text="Hello World" 
        style={{ fill: '#ffffff', fontFamily: 'Inter' } as TextStyle} 
      />
    </Container>
  );
}

AutoPlay System

The AutoPlay system provides an automated gameplay mechanism for games requiring repetitive actions.

AutoPlay Class

import { AutoPlay } from "@darkcore/game-library";

// Create an AutoPlay instance
const autoPlay = new AutoPlay({
  totalIterations: 10,        // Number of rounds (0 = infinite)
  delayMs: 2500,              // Delay between rounds in normal mode (ms)
  instantDelayMs: 1500,       // Delay between rounds in instant mode (ms)
  getInstantPlay: () => true, // Function to check if instant mode is enabled 
  onPlay: async () => {       // Function to execute each round
    // Play one round of the game
    await playRound();
  },
  onStop: () => {             // Called when autoplay stops
    console.log('Autoplay stopped');
  },
  onPause: () => {            // Called when autoplay is paused
    console.log('Autoplay paused');
  },
  onResume: () => {           // Called when autoplay is resumed
    console.log('Autoplay resumed');
  },
  onProgress: (completed, remaining) => {
    console.log(`Completed: ${completed}, Remaining: ${remaining}`);
  }
});

// Start autoplay
autoPlay.start();

// Pause autoplay (preserves remaining delay time)
autoPlay.pause();

// Resume autoplay (continues with remaining delay)
autoPlay.resume();

// Check if autoplay is currently paused
const isPaused = autoPlay.isPaused();

// Stop autoplay
autoPlay.stop();

AutoPlay Type Definitions

export interface AutoPlayConfig {
  /** Total number of iterations to run. 0 = infinite */
  totalIterations: number;
  /** Normal delay between rounds (ms) */
  delayMs: number;
  /** Delay when instant play is enabled (ms) */
  instantDelayMs: number;
  /** Function to get current instant play mode state */
  getInstantPlay: () => boolean;
  /** Function to execute for each round */
  onPlay: () => Promise<void>;
  /** Called when autoplay starts */
  onStart?: () => void;
  /** Called when autoplay stops */
  onStop?: () => void;
  /** Called when autoplay is paused */
  onPause?: () => void;
  /** Called when autoplay is resumed */
  onResume?: () => void;
  /** Called when an error occurs */
  onError?: (error: any) => void;
  /** Feedback after each round completion: completed and remaining counts */
  onProgress?: (completedIterations: number, remainingIterations: number) => void;
}

AutoPlay Methods

export class AutoPlay {
  /** Start autoplay from the beginning */
  start(): void;
  
  /** Stop autoplay completely */
  stop(): void;
  
  /** Pause autoplay (preserves remaining delay time) */
  pause(): void;
  
  /** Resume autoplay (continues with remaining delay) */
  resume(): void;
  
  /** Check if autoplay is currently paused */
  isPaused(): boolean;
  
  /** Get remaining iterations (Infinity for unlimited) */
  getRemainingIterations(): number;
}

Implementation Example

AutoPlay Store

// store/autoplay.ts
import { create } from 'zustand'

interface AutoPlayStore {
  autoPlayStarted: boolean
  stopRequested: boolean
  totalProfit: number

  startAutoBet: () => void
  stopAutoBet: () => void
}

export const useAutoPlayStore = create<AutoPlayStore>()((set, get) => ({
  autoPlayStarted: false,
  stopRequested: false,  // Special flag to show "Stopping..." until last round completes
  totalProfit: 0,

  startAutoBet: async () => {
    set({
      autoPlayStarted: true,
      totalProfit: 0
    });
  },

  stopAutoBet: async () => {
    set({
      autoPlayStarted: false,
      totalProfit: 0
    });
  }
}));

AutoPlay Hook

// src/hooks/useAutoPlay.ts
import { useEffect, useRef } from "react";
import { AutoPlay } from "@darkcore/game-library";
import { useGameStore } from "@/store/game";
import { useAutoPlayStore } from "@/store/autoplay";

export function useAutoPlay() {
  const { autoPlayStarted, totalProfit } = useAutoPlayStore();
  const { numberOfBets, onPlay, isOffline, onProfitStop, onLossStop, activeTab } = useGameStore();

  const autoRef = useRef<AutoPlay | null>(null);

  const start = () => {
    if (autoRef.current) autoRef.current.stop();

    autoRef.current = new AutoPlay({
      totalIterations: numberOfBets,
      delayMs: 2500,
      instantDelayMs: 1500,
      getInstantPlay: () => useGameStore.getState().instantPlay,
      onPlay: onPlay,
      onStop: () => {
        useAutoPlayStore.setState({
          stopRequested: false,
        });
      },
      onProgress: (_completed, progress) => {
        if(progress !== Infinity) {
          useGameStore.setState({
            numberOfBets: progress || 0,
          });
        }

        if(progress === 0) {
          useAutoPlayStore.setState({
            autoPlayStarted: false,
            stopRequested: true,
          });
        }
      }
    });

    autoRef.current.start();
  };

  const stop = () => {
    useAutoPlayStore.setState({
      stopRequested: true,
    });
    autoRef.current?.stop();
  };

  useEffect(() => {
    if(activeTab === "AUTO") {
      autoPlayStarted ? start() : stop();
    }
  }, [autoPlayStarted]);

  useEffect(() => {
    if (isOffline) stop();
  }, [isOffline]);

  // Stop conditions: profit or loss limits reached
  useEffect(() => {
    if ((onProfitStop !== 0 && totalProfit >= onProfitStop) ||
      (onLossStop !== 0 && totalProfit <= -onLossStop)) {
      stop();
    }
  }, [totalProfit]);
}

Usage in Components

import { useAutoPlay } from "@/hooks/useAutoPlay";
import { useAutoPlayStore } from "@/store/autoplay";

export default function GameControls() {
  // Initialize the autoplay hook
  useAutoPlay();
  
  const { autoPlayStarted, stopRequested } = useAutoPlayStore();
  
  return (
    <div>
      <button 
        onClick={() => {
          if (autoPlayStarted) {
            useAutoPlayStore.getState().stopAutoBet();
          } else {
            useAutoPlayStore.getState().startAutoBet();
          }
        }}
      >
        {autoPlayStarted 
          ? (stopRequested ? "Stopping..." : "Stop Auto") 
          : "Start Auto"}
      </button>
    </div>
  );
}

Responsive Grid System

A flexible, responsive grid utility for positioning game elements that adapts to any canvas size, including support for configurable margins and cover ratios.

Usage Example

import { useResponsiveGrid } from "@darkcore/game-library";
import { Container, Sprite } from "@pixi/react";
import * as PIXI from "pixi.js";

const images = useGetImages();
const cellTexture = PIXI.Texture.from(images.cell);

const rows = 3;
const columns = 5;

const {
  scale,
  containerPosition,
  cellCenterPositions,
  gridRect,
  gridEdges
} = useResponsiveGrid({
  canvasWidth,
  canvasHeight,
  cellWidth: cellTexture.width,
  cellHeight: cellTexture.height,
  columns,
  rows,
  coverRatio: 0.9,          // grid + margins will occupy 90% of the canvas
  horizontalSpacing: 20,
  verticalSpacing: 20,
  marginX: 40,              // 40px margin on left and right
  marginY: 40               // 40px margin on top and bottom
});

return (
  <>
    {/* Main grid container or position={[gridRect.x, gridRect.y]} */}
    <Container position={containerPosition} scale={scale}>
      {cellCenterPositions.map((pt, i) => (
        <Sprite key={i} texture={cellTexture} x={pt.x} y={pt.y} anchor={0.5} />
      ))}
    </Container>

    {/* Example UI panel at right edge */}
    <Container
      position={[gridEdges.right.x + 20, gridEdges.right.y]}
      scale={scale}
    >
      <Sprite texture={uiTexture} anchor={0.5} />
    </Container>
  </>
);

API

interface ResponsiveGridConfig {
  canvasWidth: number;
  canvasHeight: number;
  cellWidth: number;
  cellHeight: number;
  columns: number;
  rows: number;
  coverRatio: number;       // 0–1, total grid+margin coverage of canvas
  horizontalSpacing?: number;
  verticalSpacing?: number;
  marginX?: number;         // px margin on left/right 
  marginY?: number;         // px margin on top/bottom
}

interface ResponsiveGridResult {
  /** Scale factor to apply to the grid container */
  scale: number;

  /** Top-left position (x,y) of the grid container */
  containerPosition: Point;

   /** Center positions for each cell, ensuring they are evenly centered within the grid based on row and column counts */
  cellCenterPositions: Point[];

  /**
   * The actual on-screen rectangle of the grid after scaling
   * { x, y, width, height }
   */
  gridRect: { x: number; y: number; width: number; height: number };

  /**
   * Midpoints of each side of the grid rect:
   * top, bottom, left, right
   */
  gridEdges: {
    top: Point;
    bottom: Point;
    left: Point;
    right: Point;
  };
}

Spine Animation System

The library includes components and hooks for working with Spine animations (using pixi-spine).

GameSpine Component

GameSpine is a reusable component that manages Spine animations within a Pixi Container:

import { GameSpine } from "@darkcore/game-library";
import { useGetSpines } from "@darkcore/game-library";

export default function SpineExample() {
  const spines = useGetSpines();
  
  return (
    <GameSpine
      spine={spines.dice}
      animation="idle"
      loop={true}
      scale={0.5}
      position={{ x: 0, y: 0 }}
      zIndex={5}
      opacity={1}
      timeScale={1}
      autoUpdate={true}
      name="character"
      isAnimating={true}
    />
  );
}

GameSpine Props

export interface GameSpineProps {
  /** Spine asset (with .spineData) */
  spine: Spine;
  /** Animation key to play */
  animation: string;
  /** Loop the animation */
  loop: boolean;
  /** Scale factors */
  scale: number | { x: number; y: number };
  /** Position inside parent container */
  position: { x: number; y: number };
  /** Z-index for sorting */
  zIndex?: number;
  /** Opacity (0–1) */
  opacity?: number;
  /** Playback speed multiplier */
  timeScale?: number;
  /** Controls whether animation plays (true) or stops (false) */
  autoUpdate?: boolean;
  /** Unique name for the instance */
  name: string;
  /** Counter to force recreation of spine instance */
  restartCounter?: number;
  /** External ref to access spine instance */
  outerSpineRef?: React.RefObject<Spine | null>;
}

Spine Animation Info Hook

The library provides a hook to extract animation information from a loaded Spine asset:

import { useSpineInfo } from "@darkcore/game-library";
import { useGetSpines } from "@darkcore/game-library";

// Get all animations from the Spine asset
const spines = useGetSpines();
const animations = useSpineInfo(spines.dice);

console.log(animations);
// Returns:
// [
//   { name: "Dice_Left", duration: 0.3332999944686889 },
//   { name: "Dice_Right", duration: 0.3332999944686889 }
// ]

The useSpineInfo hook returns an array of animation information objects:

export interface SpineAnimationInfo {
  /** Animation name as defined in the Spine data */
  name: string;
  /** Duration of the animation in seconds */
  duration: number;
}

Spine Animation Controller

The library provides a controller hook for managing Spine animations with a convenient API:

import { useSpineController } from "@darkcore/game-library";
import { useGetSpines, useSpineInfo } from "@darkcore/game-library";
import { GameSpine } from "@darkcore/game-library";

function SpineExample() {
  const diceSpine = useGetSpines().dice;
  const animations = useSpineInfo(diceSpine);
  
  const controller = useSpineController({
    initialAnimation: animations[0].name,
    initialLoop: true,
    initialTimeScale: 1,
    initialScale: { x: 1, y: 1 },
    initialPosition: { x: 0, y: 0 },
  });
  
  // Start animation
  controller.start();
  
  // Change animation
  controller.setAnimation(animations[1].name);
  
  // Stop animation
  controller.stop();
  
  // Reset animation
  controller.reset();
  
  // Other available methods:
  // controller.setLoop(true/false)
  // controller.setTimeScale(1.5)
  // controller.setScale({ x: 2, y: 2 })
  // controller.setPosition({ x: 100, y: 100 })
  
  return (
    <GameSpine
      spine={diceSpine}
      animation={controller.props.animation}
      loop={controller.props.loop}
      scale={controller.props.scale}
      position={controller.props.position}
      timeScale={controller.props.timeScale}
      name={controller.props.name}
      autoUpdate={controller.props.autoUpdate}
      isAnimating={controller.isAnimating}
    />
  );
}

The useSpineController hook provides a simple interface for controlling Spine animations:

// Configuration options
interface SpineControllerOptions {
  initialAnimation: string;
  initialLoop?: boolean;
  initialTimeScale?: number;
  initialScale?: { x: number; y: number };
  initialPosition?: { x: number; y: number };
}

// Returned controller object
interface SpineController {
  // Direct props for GameSpine component
  props: {
    animation: string;
    loop: boolean;
    autoUpdate: boolean;
    timeScale: number;
    scale: { x: number; y: number };
    position: { x: number; y: number };
    name: string;
    restartCounter: number;
  };
  
  // Control methods
  /** Start playing the animation from beginning */
  start: () => void;
  /** Stop the animation by setting timeScale to 0 */
  stop: () => void;
  /** Resume the animation by restoring timeScale */
  resume: () => void;
  /** Reset the animation */
  reset: () => void;
  /** Change the current animation */
  setAnimation: (animationName: string) => void;
  /** Toggle looping (It affects animation stopping and starting) */
  setLoop: (loop: boolean) => void;
  /** Change animation playback speed */
  setTimeScale: (timeScale: number) => void;
  /** Adjust the size of the animation */
  setScale: (scale: { x: number; y: number }) => void;
  /** Change the position of the animation */
  setPosition: (position: { x: number; y: number }) => void;
  
  // Direct spine instance access
  /** Reference to the spine instance for advanced control */
  spineRef: React.RefObject<Spine | null>;
}

Timers System

The library provides a timeout management system with pause/resume capabilities and ID-based timeout tracking.

Timeout Manager

The createTimeoutManager function creates a timeout manager instance with ID-based timeout control:

import { createTimeoutManager } from "@darkcore/game-library";

// Create a timeout manager instance
const timeoutManager = createTimeoutManager();

// Create timeouts with unique IDs
timeoutManager.createTimeout("timer1", () => {
  console.log("Timer 1 executed");
}, 3000);

timeoutManager.createTimeout("timer2", () => {
  console.log("Timer 2 executed");
}, 5000);

// Clear a specific timeout by ID
timeoutManager.clearTimeout("timer1");

// Check if a timeout exists
const exists = timeoutManager.hasTimeout("timer2");

// Get number of active timeouts
const count = timeoutManager.size();

// Pause all timeouts
timeoutManager.pauseAll();

// Resume all paused timeouts
timeoutManager.resumeAll();

// Clear all timeouts
timeoutManager.clearAll();

Key Features

  • ID-based Management: Each timeout has a unique string identifier
  • Pause/Resume: Pause and resume all timeouts while preserving remaining time
  • Automatic Cleanup: Completed timeouts are automatically removed
  • Status Checking: Check existence and count of active timeouts

Global Timeout Manager

The library provides a global timeout manager instance that can be used across components:

import { timeoutManager } from "@darkcore/game-library";

function GameComponent() {
  useEffect(() => {
    // Set multiple timers with IDs
    timeoutManager.createTimeout("action1", () => {
      console.log("Action 1 completed");
    }, 1000);

    timeoutManager.createTimeout("action2", () => {
      console.log("Action 2 completed");
    }, 2000);
    
    // Clean up on component unmount
    return () => {
      timeoutManager.clearTimeout("action1");
      timeoutManager.clearTimeout("action2");
    };
  }, []);
  
  return (
    // Your component JSX
  );
}

Custom Timeout Manager

You can also create your own timeout manager instance:

import { createTimeoutManager } from "@darkcore/game-library";
import { useEffect } from "react";

function GameComponent() {
  useEffect(() => {
    const timeoutManager = createTimeoutManager();
    
    // Set multiple timers with IDs
    timeoutManager.createTimeout("action1", () => {
      console.log("Action 1 completed");
    }, 1000);

    timeoutManager.createTimeout("action2", () => {
      console.log("Action 2 completed");
    }, 2000);
    
    // Clean up on component unmount
    return () => {
      timeoutManager.clearAll();
    };
  }, []);
  
  return (
    // Your component JSX
  );
}

API Reference

export function createTimeoutManager() {
  return {
    /**
     * Create or restart a timeout with a unique ID
     * @param id Unique identifier for the timeout
     * @param callback Function to execute when timer completes
     * @param delay Delay in milliseconds
     */
    createTimeout(id: string, callback: () => void, delay: number): void;
    
    /**
     * Clear a timeout by its ID
     * @param id Timeout identifier
     */
    clearTimeout(id: string): void;
    
    /**
     * Pause all active timeouts
     */
    pauseAll(): void;
    
    /**
     * Resume all paused timeouts
     */
    resumeAll(): void;
    
    /**
     * Clear all timeouts
     */
    clearAll(): void;
    
    /**
     * Check if a timeout exists
     * @param id Timeout identifier
     * @returns True if timeout exists
     */
    hasTimeout(id: string): boolean;
    
    /**
     * Get number of active timeouts
     * @returns Number of managed timeouts
     */
    size(): number;
  };
}

Tick System

The library provides a shared tick system that optimizes performance by sharing a single ticker across multiple components, automatically handling browser tab visibility changes.

Shared Tick Manager

The useSharedTickManager hook creates a shared ticker instance that can be used by multiple components. This is more efficient than creating individual tickers for each component.

import { useSharedTickManager } from "@darkcore/game-library";

function App() {
  // Start shared tick manager once at the top level
  useSharedTickManager();
  
  return (
    <Container>
      {/* All child components can now use useSharedTick */}
      <AnimatedComponent1 />
      <AnimatedComponent2 />
      <AnimatedComponent3 />
    </Container>
  );
}

useSharedTick Hook

Components can use the shared ticker via the useSharedTick hook:

import { useSharedTick } from "@darkcore/game-library";
import { useState } from "react";

function AnimatedComponent() {
  const [rotation, setRotation] = useState(0);
  
  useSharedTick((deltaMS) => {
    // deltaMS accounts for pause time
    setRotation(prev => prev + (deltaMS * 0.001));
  });
  
  return <Sprite image="character.png" rotation={rotation} />;
}

Key Features

  • Shared Performance: Single ticker shared across all components
  • Auto Pause/Resume: Pauses when tab is hidden, resumes when visible
  • Accurate Timing: Accounts for paused time to maintain consistent speed
  • Easy Integration: Simple drop-in replacement for useTick

API

// Initialize shared tick manager (call once at app level)
export function useSharedTickManager(): void;

// Use shared tick in components
export function useSharedTick(onTick: (deltaMS: number) => void): void;

This ensures animations continue correctly regardless of tab visibility changes while maintaining optimal performance.

Custom UI Components

CustomButton

The CustomButton component provides an advanced, interactive button with smooth press animations and multiple visual layers.

Features

  • Smooth Press Animation: Y-axis animation when button is pressed/released
  • Hover Effect: Visual feedback with tint change when mouse hovers over button
  • External Hover Control: Support for external hover state management
  • Hover Event Callbacks: Optional hover event handlers for custom interactions
  • Multi-layer Design: Support for plate, mask, image, and text layers
  • Disabled State: Built-in disabled state handling with separate disabled image
  • Interactive: Pointer events with cursor changes
  • Customizable: All visual elements can be customized

Usage

import { CustomButton } from "@darkcore/game-library";
import { useGetImages } from "@darkcore/game-library";
import * as PIXI from "pixi.js";

export default function ButtonExample() {
  const images = useGetImages();
  const [isDisabled, setIsDisabled] = useState(false);

  return (
    <CustomButton
      position={[100, 100]}
      scale={[1, 1]}
      anchor={[0.5, 0.5]}
      disabled={isDisabled}
      onClick={() => {
        console.log("Button clicked!");
        setIsDisabled(!isDisabled);
      }}
      onHover={(hovered) => {
        console.log("Button hover:", hovered);
      }}
      externalHovered={false}
      buttonPlateProps={{
        x: 0,
        y: 0,
        anchor: 0.5,
        image: images.buttonPlate,
      }}
      buttonMaskProps={{
        x: 0,
        y: 0,
        anchor: 0.5,
        image: images.buttonMask,
        buttonImagePressedY: 20, // Y offset when pressed
      }}
      buttonImageProps={{
        x: 0,
        y: 0,
        anchor: 0.5,
        image: images.greenButton,
        disabledImage: images.grayButton,
      }}
      textProps={{
        x: 0,
        y: -10,
        anchor: 0.5,
        text: "PLAY",
        style: {
          fontFamily: "Inter",
          fontSize: 24,
          fill: "#ffffff",
        } as PIXI.TextStyle
      }}
    />
  );
}

Props

interface CustomButtonType {
  /** Button position [x, y] */
  position?: [number, number];
  /** Button scale [x, y] */
  scale?: [number, number];
  /** Button anchor [x, y] */
  anchor?: [number, number];
  /** Disable button interactions */
  disabled: boolean;
  /** Click handler function */
  onClick: () => void;
  /** Hover event handler (called when pointer enters/leaves) */
  onHover?: (hovered: boolean) => void;
  /** External hover state control */
  externalHovered?: boolean;
  /** Props for the button plate (background layer) */
  buttonPlateProps?: React.ComponentProps<typeof Sprite>;
  /** Props for the button mask with press animation */
  buttonMaskProps?: React.ComponentProps<typeof Sprite> & {
    /** Y offset when button is pressed */
    buttonImagePressedY: number;
  };
  /** Props for the main button image */
  buttonImageProps?: React.ComponentProps<typeof Sprite> & {
    /** Image to show when button is disabled */
    disabledImage?: string;
  };
  /** Props for the button text */
  textProps?: React.ComponentProps<typeof Text>;
  /** Child components to render inside the button */
  children?: React.ReactNode;
  /** Enable mask test mode to visualize the button mask (debug feature) */
  maskTestMode?: boolean;
}

Animation System

The CustomButton uses a smooth animation system:

  1. Press Animation: When pressed, the button image moves to buttonImagePressedY position
  2. Release Animation: When released, returns to original position
  3. Smooth Interpolation: Uses useTick for frame-based animation with 20% lerp factor
  4. Automatic Reset: On pointer leave, immediately resets to original position

Visual Layers

The button consists of multiple layers (from bottom to top):

  1. Button Plate: Background/base layer
  2. Button Mask: Defines the clipping area for the button image
  3. Button Image: Main visual element (gets masked and animated, automatically switches to disabledImage when disabled)
  4. Button Text: Text overlay

States

  • Normal: Default interactive state
  • Hover: Pointer over button with visual tint effect (maintains press capability)
  • Pressed: During pointer down (image moves down)
  • Disabled: Non-interactive state (cursor changes to default)

Debug Features

Mask Test Mode

The button includes a debug feature to visualize the mask area. When maskTestMode is enabled, a semi-transparent red overlay shows the exact mask boundaries:

<CustomButton
  maskTestMode={true}  // Enable debug mode
  buttonMaskProps={{
    x: 0,
    y: 0,
    anchor: 0.5,
    image: images.buttonMask,
    buttonImagePressedY: 20,
  }}
  // ... other props
/>

Button Hooks

useAnimatedPosition

The useAnimatedPosition hook provides smooth position animation for any UI element, with customizable easing functions and duration.

Features:

  • Smooth Position Transition: Animated movement between target positions
  • Customizable Duration: Configurable animation duration in milliseconds
  • Easing Support: Built-in smooth step easing with option for custom easing functions
  • Frame-based Animation: Uses @pixi/react's useTick for consistent 60fps animations
  • Automatic Completion: Automatically stops when animation reaches target
  • Interruption Handling: Smoothly handles new targets during ongoing animations

Usage:

import { useAnimatedPosition } from "@darkcore/game-library";
import { Container, Sprite } from "@pixi/react";

export default function AnimatedElement() {
  const [targetPos, setTargetPos] = useState<[number, number]>([100, 100]);
  const images = useGetImages();
  
  const { position, isAnimating } = useAnimatedPosition({
    targetPosition: targetPos,
    duration: 500,              // 500ms animation
    easing: (t) => t * t * (3 - 2 * t)  // smooth step (default)
  });

  const handleClick = () => {
    const newX = Math.random() * 800;
    const newY = Math.random() * 600;
    setTargetPos([newX, newY]);
  };

  return (
    <Container>
      <Sprite 
        image={images.button}
        position={position}
        anchor={0.5}
        interactive={true}
        onpointerdown={handleClick}
        alpha={isAnimating ? 0.8 : 1.0}
      />
    </Container>
  );
}

API Reference:

export type Vec2 = [number, number];

interface UseAnimatedPositionConfig {
  /** Target position [x, y] to animate towards */
  targetPosition: Vec2;
  /** Animation duration in milliseconds (default: 200) */
  duration?: number;
  /** Easing function (t: 0-1) => (0-1), default: smooth step */
  easing?: (t: number) => number;
}

interface UseAnimatedPositionResult {
  /** Current animated position [x, y] */
  position: Vec2;
  /** Whether animation is currently running */
  isAnimating: boolean;
}

export function useAnimatedPosition(config: UseAnimatedPositionConfig): UseAnimatedPositionResult;

Default Easing:

  • The default easing function is smooth step: t * t * (3 - 2 * t)
  • Provides natural acceleration and deceleration
  • Custom easing functions can be provided for different animation styles

Animation Behavior:

  • When targetPosition changes, automatically starts new animation from current position
  • If animation is already running when target changes, current position becomes new start point
  • Animation completes when time reaches specified duration
  • Position updates every frame using Pixi's render loop
useModalAnimation

The useModalAnimation hook provides smooth modal opening/closing animations with scale and alpha transitions, supporting custom easing functions and configurable delays.

Features:

  • Scale & Alpha Animation: Smooth transitions for both scale and opacity
  • Custom Easing: Built-in smooth step easing with option for custom easing functions
  • Configurable Delays: Optional delay before animation starts
  • State Tracking: Provides animation state and completion status
  • Frame-based Animation: Uses @pixi/react's useTick for consistent 60fps animations
  • Flexible Configuration: Customizable open/closed values for scale and alpha

Usage:

  const { scale, alpha, isAnimating, isFullyOpened, isFullyClosed } = useModalAnimation({
    isOpened: isOpen,
    duration: 300,
    openScale: 1,
    closedScale: 0.8,
    openAlpha: 1,
    closedAlpha: 0,
    delay: 100,
    easing: (t) => t * t * (3 - 2 * t)  // smooth step (default)
  });

API Reference:

export interface ModalAnimationConfig {
  /** Whether modal should be in opened state */
  isOpened: boolean;
  /** Animation duration in milliseconds (default: 300) */
  duration?: number;
  /** Easing function (t: 0-1) => (0-1), default: smooth step */
  easing?: (t: number) => number;
  /** Scale value when modal is open (default: 1) */
  openScale?: number;
  /** Scale value when modal is closed (default: 0.8) */
  closedScale?: number;
  /** Alpha value when modal is open (default: 1) */
  openAlpha?: number;
  /** Alpha value when modal is closed (default: 0) */
  closedAlpha?: number;
  /** Delay before animation starts in milliseconds (default: 0) */
  delay?: number;
}

export interface ModalAnimationResult {
  /** Current animated scale value */
  scale: number;
  /** Current animated alpha value */
  alpha: number;
  /** Whether animation is currently running */
  isAnimating: boolean;
  /** Whether modal is fully opened (not animating and at open values) */
  isFullyOpened: boolean;
  /** Whether modal is fully closed (not animating and at closed values) */
  isFullyClosed: boolean;
}

export function useModalAnimation(config: ModalAnimationConfig): ModalAnimationResult;

Default Values:

  • Duration: 300ms
  • Open Scale: 1
  • Closed Scale: 0.8
  • Open Alpha: 1
  • Closed Alpha: 0
  • Delay: 0ms
  • Easing: smooth step function t * t * (3 - 2 * t)

Animation Behavior:

  • When isOpened changes, automatically starts animation with optional delay
  • Animates from current values to target values smoothly
  • Provides state flags for managing modal visibility and interactions
  • Animation updates every frame using Pixi's render loop

CustomPanel

The CustomPanel component provides a flexible panel container with background image and optional header text support.

Features

  • Flexible Layout: Customizable position, scale, and anchor points
  • Background Support: Sprite-based background with full customization
  • Header Text: Optional header text with independent styling
  • Child Components: Support for nested components within the panel
  • Interactive: Click and hover event support with pointer interactions
  • State Management: Built-in press state handling for visual feedback
  • Responsive: Handles different anchor points and scaling options

Usage

import { CustomPanel } from "@darkcore/game-library";
import { useGetImages } from "@darkcore/game-library";
import * as PIXI from "pixi.js";

export default function PanelExample() {
  const images = useGetImages();
  const [isDisabled, setIsDisabled] = useState(false);

  return (
    <CustomPanel
      position={[400, 300]}
      scale={1.5}
      anchor={0.5}
      disabled={isDisabled}
      onClick={() => {
        console.log("Panel clicked!");
        setIsDisabled(!isDisabled);
      }}
      onHover={(hovered) => {
        console.log("Panel hover:", hovered);
      }}
      bgSpriteProps={{
        x: 0,
        y: 0,
        anchor: 0.5,
        image: images.panelBg,
      }}
      headerTextProps={{
        x: 0,
        y: -100,
        anchor: 0.5,
        text: "GAME SETTINGS",
        style: {
          fontFamily: "Inter",
          fontSize: 32,
          fill: "#ffffff",
        } as PIXI.TextStyle
      }}
    >
      {/* Child components go here */}
      <Text 
        x={0} 
        y={0} 
        anchor={0.5} 
        text="Panel Content" 
        style={{ fontSize: 24, fill: "#ffffff" } as PIXI.TextStyle} 
      />
    </CustomPanel>
  );
}

Props

interface CustomPanelType {
  /** Panel position [x, y] */
  position?: [number, number];
  /** Panel scale (number for uniform, [x, y] for separate axes) */
  scale?: number | [number, number];
  /** Panel anchor (number for uniform, [x, y] for separate axes) */
  anchor?: number | [number, number];
  /** Panel alpha/opacity (0-1) */
  alpha?: number;
  /** Disable panel interactions */
  disabled?: boolean;
  /** Props for the optional header text */
  headerTextProps?: React.ComponentProps<typeof Text>;
  /** Props for the background sprite (required) */
  bgSpriteProps: React.ComponentProps<typeof Sprite>;
  /** Child components to render inside the panel */
  children?: React.ReactNode;
  /** Click handler function */
  onClick?: () => void;
  /** Hover event handler (called when pointer enters/leaves) */
  onHover?: (hovered: boolean) => void;
}

Structure

The CustomPanel creates a layered structure:

  1. Container: Main container with position, scale, and anchor
  2. Background Sprite: Named "PanelBg" for easy identification
  3. Header Text: Optional text element positioned above content
  4. Children: Custom content rendered last (on top)

CustomCounter

The CustomCounter component provides an interactive value selector with increment/decrement buttons from a predefined list of values.

Features

  • Predefined Values: Works with an array of string values (e.g., bet amounts, mine counts)
  • Interactive Buttons: Plus and minus buttons with customizable styling
  • Number Formatting: Optional currency and locale formatting support
  • State Management: Internal state management with external change callbacks
  • Button States: Automatic button disabling when limits are reached
  • Flexible Layout: Customizable positioning and scaling

Type Definitions

export interface CustomCounterType {
  position?: [number, number];
  scale?: number | [number, number];
  anchor?: number | [number, number];
  values: string[];
  startValue: string;
  onChange: ({value, index}: {value: string, index: number}) => void;
  textProps: React.ComponentProps<typeof Text>;
  plusButtonProps: React.ComponentProps<typeof CustomButton>;
  minusButtonProps: React.ComponentProps<typeof CustomButton>;
}

Usage

import { CustomCounter, numberFormatter } from "@darkcore/game-library";
import { useGetImages } from "@darkcore/game-library";
import * as PIXI from "pixi.js";

export default function CounterExample() {
  const images = useGetImages();
  const [currentValue, setCurrentValue] = useState("1.00");

  const buttonProps = {
    position: [0, 0] as [number, number],
    scale: 0.8,
    disabled: false,
    onClick: () => {},
    buttonPlateProps: {
      x: 0,
      y: 0,
      anchor: 0.5,
      image: images.buttonPlate,
    },
    buttonMaskProps: {
      x: 0,
      y: 0,
      anchor: 0.5,
      image: images.buttonMask,
      buttonImagePressedY: 15,
    },
    buttonImageProps: {
      x: 0,
      y: 0,
      anchor: 0.5,
      image: images.greenButton,
      disabledImage: images.grayButton,
    },
    textProps: {
      x: 0,
      y: -5,
      anchor: 0.5,
      text: "+",
      style: {
        fontFamily: "Inter",
        fontSize: 20,
        fill: "#ffffff",
      } as PIXI.TextStyle
    }
  };

  return (
    <CustomCounter
      position={[400, 300]}
      scale={[1, 1]}
      anchor={0.5}
      values={["0.50", "1.00", "2.50", "5.00", "10.00"]}
      startValue="1.00"
      onChange={({ value, index }) => {
        setCurrentValue(newValue);
        console.log("Counter value changed:", value);
        console.log("Counter index changed:", index);
      }}
      textProps={{
        x: 0,
        y: -50,
        anchor: 0.5,
        text: "", // Will be overridden with formatted value
        style: {
          fontFamily: "Inter",
          fontSize: 24,
          fill: "#ffffff",
        } as PIXI.TextStyle
      }}
      plusButtonProps={{
        ...buttonProps,
        position: [80, 20],
      }}
      minusButtonProps={{
        ...buttonProps,
        position: [-80, 20],
        textProps: {
          ...buttonProps.textProps,
          text: "-"
        }
      }}
    />
  );
}

Props

interface CustomCounterType {
  /** Counter position [x, y] */
  position?: [number, number];
  /** Counter scale (number for uniform, [x, y] for separate axes) */
  scale?: number | [number, number];
  /** Counter anchor (number for uniform, [x, y] for separate axes) */
  anchor?: number | [number, number];
  /** Array of selectable values as strings */
  values: string[];
  /** Initial/starting value (must exist in values array) */
  startValue: string;
  /** Currency code for number formatting (e.g., "USD", "EUR") */
  currency?: string;
  /** Callback when counter value changes */
  onChange: (value: string) => void;
  /** Props for the counter value text */
  textProps: React.ComponentProps<typeof Text>;
  /** Props for the plus/increment button */
  plusButtonProps: React.ComponentProps<typeof CustomButton>;
  /** Props for the minus/decrement button */
  minusButtonProps: React.ComponentProps<typeof CustomButton>;
}

Number Formatting

The numberFormatter function provides locale-aware number formatting with currency support:

import { numberFormatter } from "@darkcore/game-library";

// Example formatter usage
const formattedValue = numberFormatter({
  number: 1234.56,
  locale: "en-US",
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 2
});
// Result: "$1,234.56"

Behavior

  • Value Management: Maintains internal state with array index tracking
  • Boundary Checking: Disables buttons when at first/last values in array
  • Change Callbacks: Calls onChange with new string value when buttons are clicked
  • Button Customization: Both plus and minus buttons support full CustomButton props

Structure

The CustomCounter creates the following structure:

  1. Container: Main container with position, scale, and anchor
  2. Value Text: Displays current counter value (formatted if formatter provided)
  3. Plus Button: Increment button (moves to next value in array)
  4. Minus Button: Decrement button (moves to previous value in array)

Example Usage with Panel

// Complete example combining CustomPanel and CustomCounter (Mine Counter)
export const MineCounter = ({ cellCenterPositions }: MineCounterProps) => {
  const images = useGetImages();
  const [mineCount, setMineCount] = useState("0");

  const buttonProps = {
    position: [0, 0] as [number, number],
    scale: 0.5,
    disabled: false,
    onClick: () => {},
    buttonPlateProps: {
      x: 0,
      y: 0,
      anchor: 0.5,
      image: images.buttonPlate,
    },
    buttonMaskProps: {
      x: 0,
      y: 0,
      anchor: 0.5,
      image: images.buttonMask,
      buttonImagePressedY: 20,
    },
    buttonImageProps: {
      x: 0,
      y: 0,
      anchor: 0.5,
      image: images.greenButton,
      disabledImage: images.grayButton,
    },
    textProps: {
      x: 0,
      y: -100,
      anchor: 0.5,
      scale: 2,
      text: "",
      style: {
        fontFamily: "Inter",
        fontSize: 200,
        fill: "#ffffff",
      } as PIXI.TextStyle
    }
  };

  return (
    <CustomPanel
      bgSpriteProps={{
        x: cellCenterPositions[0].x,
        y: cellCenterPositions[0].y,
        anchor: 0.5,
        scale: 1.7,
        image: images.panelBg,
      }}
      headerTextProps={{
        x: cellCenterPositions[0].x,
        y: cellCenterPositions[0].y - 250,
        anchor: 0.5,
        scale: 1,
        text: "MINES",
        style: {
          fontFamily: "Inter",
          fontSize: 120,
          fill: "#ffffff",
        } as PIXI.TextStyle
      }}
    >
      <CustomCounter
        position={[cellCenterPositions[0].x, cellCenterPositions[0].y + 200]}
        scale={[0.8, 0.8]}
        values={["0", "1", "2", "3", "4"]}
        startValue="1"
        onChange={(value) => {
          setMineCount(value);
          console.log("Mine count changed:", value);
        }}
        textProps={{
          x: 0,
          y: -270,
          anchor: 0.5,
          scale: 2,
          style: {
            fontFamily: "Inter",
            fontSize: 110,
            fill: "#ffffff",
          } as PIXI.TextStyle
        }}
        plusButtonProps={{
          ...buttonProps,
          position: [185, 50],
        }}
        minusButtonProps={{
          ...buttonProps,
          position: [-185, 50],
        }}
      />
    </CustomPanel>
  );
};

CustomProfit

The CustomProfit component provides an advanced profit display with win/loss animations, value increment animations, and win text effects using the shared tick system for optimal performance.

Features

  • Win/Loss Toggle: Alternates between win text and profit value display
  • Value Increment Animation: Smooth animation when profit value increases
  • Instant Value Decrease: Immediate update when profit value decreases
  • Win Text Scale Animation: Optional scale animation for win text with customizable easing
  • Multiple Win Cycles: Support for multiple win text animations with configurable count
  • Number Formatting: Configurable number formatter function for currency display with locale support
  • Configurable Timings: Customizable animation durations and pause intervals
  • Shared Tick Performance: Uses shared tick system for optimal performance
  • Smooth Transitions: Frame-based animations with proper handling of rapid value changes

Usage

import { CustomProfit } from "@darkcore/game-library";
import { useGetImages } from "@darkcore/game-library";
import * as PIXI from "pixi.js";
import { numberFormatter } from "./utils"

export default function ProfitExample() {
  const images = useGetImages();
  const [profitValue, setProfitValue] = useState(0);
  const [isWin, setIsWin] = useState(false);

  return (
    <CustomProfit
      customPanelProps={{
        position: [400, 100],
        scale: 1.2,
        anchor: 0.5,
        onClick: () => {
          setIsWin(!isWin);
          setProfitValue(profitValue + 1.24);
        },
        bgSpriteProps: {
          x: 0,
          y: 0,
          anchor: 0.5,
          image: images.profitBg,
        }
      }}
      valueTextProps={{
        x: 0,
        y: 0,
        anchor: 0.5,
        style: {
          fontFamily: "Inter",
          fontSize: 24,
          fill: "#ffffff",
        } as PIXI.TextStyle
      }}
      winTextProps={{
        x: 0,
        y: 0,
        anchor: 0.5,
        style: {
          fontFamily: "Inter",
          fontSize: 28,
          fill: "#00ff00",
        } as PIXI.TextStyle
      }}
      goodLuckTextProps={{
        x: 0,
        y: 0,
        anchor: 0.5,
        style: {
          fontFamily: "Inter",
          fontSize: 24,
          fill: "#ffff00",
        } as PIXI.TextStyle
      }}
      numberFormatter={numberFormatter}  // Currency formatting function
      currency={"USD"}
      profitValue={profitValue}
      winText="WIN!"
      goodLuckText="GOOD LUCK!"
      isGoodLuck={false}
      isWin={isWin}
      winCount={2}                      // Number of win text cycles
      pauseDuration={600}               // Pause between animations
      counterAnimationDuration={250}    // Duration for value increment
      enableTextScaleAnimation={true}   // Enable win text scale animation
      scaleAmplitude={0.4}              // Scale amplitude for win text animation
      decimalPlaces={2}                 // Number of decimal places for counter
    />
  );
}

Props

interface CustomProfitType {
  /** Props for the CustomPanel container */
  customPanelProps: React.ComponentProps<typeof CustomPanel>;
  /** Props for the profit value text */
  valueTextProps: React.ComponentProps<typeof Text>;
  /** Props for the win text */
  winTextProps: React.ComponentProps<typeof Text>;
  /** Props for the good luck text */
  goodLuckTextProps: React.ComponentProps<typeof Text>;
  /** Number formatter function for currency display */
  numberFormatter: ({ number, locale, style, currency, maximumFractionDigits }: NumberFormatterType) => string;
  /** Currency code for number formatting (e.g., "USD", "EUR") */
  currency: string;
  /** Current profit value to display */
  profitValue: number;
  /** Text to display when showing win state */
  winText: string;
  /** Text to display when showing good luck state */
  goodLuckText: string;
  /** Whether to show good luck text */
  isGoodLuck: boolean;
  /** Whether to show win animation */
  isWin: boolean;
  /** Number of win text animation cycles (default: 1) */
  winCount?: number;
  /** Duration between win text and value text phases (ms) */
  pauseDuration: number;
  /** Duration for value increment animation (ms) */
  counterAnimationDuration: number;
  /** Enable/disable win text scale animation */
  enableTextScaleAnimation?: boolean;
  /** Scale amplitude for win text animation (default: 0.4) */
  scaleAmplitude?: number;
  /** Number of decimal places for counter animation (default: 2) */
  decimalPlaces?: number;
}

Animation System

The CustomProfit component features multiple animation systems with sophisticated phase management:

Text State Display

The component displays different text based on state priority:

  1. Win State: When isWin is true, alternates between win text and profit value
  2. Good Luck State: When isGoodLuck is true and isWin is false, shows good luck text
  3. Normal State: When neither isWin nor isGoodLuck is true, shows the profit value
Animation Phase Flow

The component uses a four-phase animation system:

  1. Count Phase: Animates profit value changes over counterAnimationDuration
  2. Pre-pause Phase: Shows final value for pauseDuration before win text
  3. Scale Phase: Displays win text with optional scale animation
  4. Pause Phase: Shows profit value between win text cycles
Value Increment Animation
  • When profitValue increases: Smooth animation over counterAnimationDuration
  • When profitValue decreases: Instant update (typically when resetting to 0)
  • Handles rapid value changes by completing current animation to target, then starting new animation
Win Text Scale Animation
  • Optional scale animation when enableTextScaleAnimation is true
  • Uses ease-in-out curve for natural motion: p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2
  • Scale range: base scale to base scale + scaleAmplitude (default: 0.4)
  • Configurable amplitude via scaleAmplitude prop
  • Runs for each win text cycle defined by winCount
Multiple Win Cycles
  • When winCount is greater than 1, win text animation repeats multiple times
  • Each cycle consists of scale animation followed by pause showing profit value
  • Cycles through: Scale → Pause → Scale → Pause → ... until winCount is reached

CustomHistory

The CustomHistory component provides an animated history display system that shows a list of values with smooth animations when new entries are added or removed. The component is optimized with React.memo for performance.

Features

  • Smooth Animations: Frame-based animations with easing for position and scale
  • Vertical/Horizontal Layout: Configurable orientation for different layouts
  • Maximum Count Limit: Automatically removes old entries when limit is reached
  • Duplicate Support: Handles duplicate values with unique identification
  • Configurable Spacing: Adjustable spacing between history items
  • Background Support: Optional background sprites for each history item
  • Animation Tuning: Customizable easing factors and animation thresholds
  • Performance Optimized: Uses React.memo for efficient re-renders
  • Memoized Items: Individual history items are memoized for optimal performance

Usage

import { CustomHistory } from "@darkcore/game-library";
import { useGetImages } from "@darkcore/game-library";
import * as PIXI from "pixi.js";

export default function HistoryExample() {
  const images = useGetImages();
  const [historyValues, setHistoryValues] = useState<string[]>([]);

  const addHistoryValue = () => {
    const newValue = `${Math.floor(Math.random() * 10) + 1}x`;
    setHistoryValues([...historyValues, newValue]);
  };

  return (
    <CustomHistory
      position={[400, 300]}
      historyValues={historyValues}
      maxCount={6}
      isVertical={true}
      betweenSpacing={80}
      historyValueBgSpriteProps={{
        x: 0,
        y: 0,
        anchor: 0.5,
        scale: 0.8,
        image: images.historyTextBg,
      }}
      historyValueTextProps={{
        x: 0,
        y: 0,
        anchor: 0.5,
        style: {
          fontFamily: "Inter",
          fontSize: 20,
          fill: "#ffffff",
        } as PIXI.TextStyle
      }}
      animationConfig={{
        easingFactor: 0.12,
        scaleThreshold: 0.01,
        positionThreshold: 0.1,
      }}
    />
  );
}

Props

interface CustomHistoryType {
  /** History container position [x, y] */
  position?: [number, number];
  /** History scale (number for uniform, [x, y] for separate axes) */
  scale?: number | [number, number];
  /** History anchor (number for uniform, [x, y] for separate axes) */
  anchor?: number | [number, number];
  /** Array of history values to display */
  historyValues: any[];
  /** Optional props for history item background sprites */
  historyValueBgSpriteProps?: React.ComponentProps<typeof Sprite>;
  /** Props for history item text */
  historyValueTextProps: React.ComponentProps<typeof Text>;
  /** Whether to arrange items vertically or horizontally */
  isVertical?: boolean;
  /** Spacing between history items in pixels */
  betweenSpacing?: number;
  /** Maximum number of history items to display */
  maxCount?: number;
  /** Animation configuration options */
  animationConfig?: {
    /** Easing speed factor (0-1, lower = slower) */
    easingFactor?: number;
    /** Scale animation threshold for completion */
    scaleThreshold?: number;
    /** Position animation threshold for completion */
    positionThreshold?: number;
  };
}

Animation System

The CustomHistory uses the useHistoryAnimation hook that provides sophisticated animation handling:

New Entry Animation
  • Scale Animation: New entries start with scale 0 and animate to scale 1
  • Position Animation: New entries start off-screen and slide into position
  • Smooth Easing: Uses configurable easing factor for natural motion
Position Updates
  • Existing Items: When new entries are added, existing items smoothly move to new positions
  • Removal Animation: When items exceed maxCount, oldest items are removed smoothly
  • Duplicate Handling: Duplicate values are treated as separate entries with unique IDs
Animation Configuration
const DEFAULT_CONFIG = {
  easingFactor: 0.12,      // Animation speed (0.05 = slow, 0.2 = fast)
  scaleThreshold: 0.01,    // When to complete scale animation
  positionThreshold: 0.1,  // When to complete position animation
};

Advanced Usage with Panel

export const HistoryBoard = ({ cellCenterPositions }: HistoryBoardProps) => {
  const images = useGetImages();
  const [historyValues, setHistoryValues] = useState<number[]>([]);

  return (
    <CustomPanel
      bgSpriteProps={{
        x: cellCenterPositions[0].x,
        y: cellCenterPositions[0].y,
        anchor: 0.5,
        image: images.historyBg,
        interactive: true,
        onpointerdown: () => {
          const newValue = (historyValues[historyValues.length - 1] || 0) + 1;
          setHistoryValues([...historyValues, newValue]);
        }
      }}
      headerTextProps={{
        text: "History",
        x: cellCenterPositions[0].x,
        y: cellCenterPositions[0].y - 300,
        anchor: 0.5,
        style: {
          fontSize: 65,
          fill: "#ffffff",
          fontFamily: "Inter"
        } as PIXI.TextStyle,
      }}
    >
      <CustomHistory
        historyValues={historyValues.map(value => value.toString() + "x")}
        position={[cellCenterPositions[0].x / 2, cellCenterPositions[0].y / 2 - 92]}
        maxCount={6}
        isVertical={true}
        betweenSpacing={95}
        historyValueBgSpriteProps={{
          x: 0,
          y: 0,
          anchor: 0.5,
          scale: 0.8,
          image: images.historyTextBg,
        }}
        historyValueTextProps={{
          x: 0,
          y: 0,
          anchor: 0.5,
          style: {
            fontSize: 60,
            fill: "#ffffff",
            fontFamily: "Inter"
          } as PIXI.TextStyle,
        }}
      />
    </CustomPanel>
  );
};

Animation Hook Details

The component internally uses useHistoryAnimation which provides:

  • Duplicate Detection: Uses original array indices to uniquely identify entries
  • Smooth Transitions: Frame-based animations with useTick from @pixi/react
  • Performance Optimization: Efficient diffing algorithm to minimize unnecessary updates
  • State Management: Maintains animation state across renders

CustomWinModal

The CustomWinModal component provides a win popup modal with dual spine animations for different types of wins. The component uses the shared tick system for optimal performance and timeout management for precise timing control.

Features

  • Multiple Win Types: Supports bigWin, megaWin, hugeWin, and epicWin animations
  • Dual Spine Animations: Uses two separate spines for main popup and coin effects
  • Animation Sequencing: Plays start animation then loops end animation
  • Amount Text Display: Shows formatted win amount with configurable delay and styling
  • Text Animation: Smooth pulsing animation for amount text using shared tick system
  • Timeout Management: Uses timeout manager for precise timing and cleanup
  • State Management: React state for active, clickable, and text visibility states
  • Configurable Timing: Customizable animation durations and instant play mode
  • Click to Close: Interactive modal with cooldown protection
  • Auto-complete: Automatically completes after animation sequence
  • Performance Optimized: Uses shared tick system and optimized state management

Usage

import { CustomWinModal } from "@darkcore/game-library";
import { useGetSpines } from "@darkcore/game-library";
import { useSpineController } from "@darkcore/game-library";
import { useDynamicFontSize } from "@darkcore/game-library";
import { TextStyle } from "pixi.js";

export default function WinModalExample() {
  const spines = useGetSpines();
  const [showWin, setShowWin] = useState(false);
  const [winType, setWinType] = useState<"bigWin" | "megaWin" | "hugeWin" | "epicWin">("bigWin");
  const [winAmount, setWinAmount] = useState(1000);

  const winAnimations = {
    bigWin: {
      startAnimationName: "big_win_start",
      loopAnimationName: "big_win_loop",
      coinStartAnimationName: "coin_start",
      coinLoopAnimationName: "coin_loop"
    },
    megaWin: {
      startAnimationName: "mega_win_start",
      loopAnimationName: "mega_win_loop",
      coinStartAnimationName: "coin_start",
      coinLoopAnimationName: "coin_loop"
    },
    hugeWin: {
      startAnimationName: "huge_win_start",
      loopAnimationName: "huge_win_loop",
      coinStartAnimationName: "coin_start",
      coinLoopAnimationName: "coin_loop"
    },
    epicWin: {
      startAnimationName: "epic_win_start",
      loopAnimationName: "epic_win_loop",
      coinStartAnimationName: "coin_start",
      coinLoopAnimationName: "coin_loop"
    }
  };

  const winPopupConfig = {
    animation: "",
    loop: false,
    scale: 1,
    position: [0, 0],
    timeScale