Skip to content

Integration Guide

This guide explains how to integrate with Loot Survivor, whether you're building a frontend client, creating tools, or extending the game with additional features.

Quick Start

Prerequisites

  • Node.js 18+ or your preferred runtime
  • Starknet wallet or programmatic account
  • Basic understanding of Starknet and Cairo

Installation

JavaScript/TypeScript

# Using npm
npm install starknet @dojoengine/core
 
# Using pnpm
pnpm add starknet @dojoengine/core
 
# Using yarn
yarn add starknet @dojoengine/core

Python

pip install starknet-py

Setting Up Connection

Initialize Provider

import { Provider, constants } from 'starknet';
 
// Mainnet
const provider = new Provider({
    sequencer: {
        network: constants.NetworkName.SN_MAIN,
    },
});
 
// Testnet (Sepolia)
const provider = new Provider({
    sequencer: {
        network: constants.NetworkName.SN_SEPOLIA,
    },
});

Contract Instances

import { Contract } from 'starknet';
import GameABI from './abis/Game.json';
import AdventurerABI from './abis/Adventurer.json';
 
const GAME_ADDRESS = "0xbcb2386436161d8d3afea0a805a8610ab90af5cf5582d866b83e9cb779bef3";
const ADVENTURER_ADDRESS = "0x3befa9c969bf82bbfa0a96374da9f7aab172101298c0ff2611ec8c2fd02692";
 
const gameContract = new Contract(GameABI, GAME_ADDRESS, provider);
const adventurerContract = new Contract(AdventurerABI, ADVENTURER_ADDRESS, provider);

Wallet Integration

Using StarknetKit

import { connect, disconnect } from '@starknet-io/get-starknet';
 
const connectWallet = async () => {
    const starknet = await connect({
        modalMode: "alwaysAsk",
        modalTheme: "dark",
    });
    
    if (!starknet?.isConnected) {
        throw new Error("Failed to connect wallet");
    }
    
    return starknet;
};

Using Cartridge Controller

import Controller from '@cartridge/controller';
 
const controller = new Controller({
    policies: [
        {
            target: GAME_ADDRESS,
            method: 'attack',
        },
        {
            target: GAME_ADDRESS,
            method: 'explore',
        },
    ],
    rpc: "https://api.cartridge.gg/x/starknet/mainnet",
});
 
await controller.connect();

Reading Game State

Get Adventurer Data

interface Adventurer {
    id: bigint;
    owner: string;
    name: string;
    level: number;
    health: number;
    gold: number;
    experience: number;
    stats: Stats;
    equipment: Equipment;
}
 
const getAdventurer = async (adventurerId: bigint): Promise<Adventurer> => {
    const result = await adventurerContract.get_adventurer(adventurerId);
    
    return {
        id: result.id,
        owner: result.owner,
        name: result.name,
        level: Number(result.level),
        health: Number(result.health),
        gold: Number(result.gold),
        experience: Number(result.experience),
        stats: parseStats(result.stats),
        equipment: parseEquipment(result.equipment),
    };
};

Get Current Beast

const getCurrentBeast = async (adventurerId: bigint) => {
    const beast = await gameContract.get_current_beast(adventurerId);
    
    if (!beast) return null;
    
    return {
        id: beast.id,
        type: beast.beast_type,
        level: Number(beast.level),
        health: Number(beast.health),
        maxHealth: Number(beast.starting_health),
    };
};

Get Market Items

const getMarketItems = async (adventurerId: bigint) => {
    const market = await gameContract.get_market(adventurerId);
    
    return market.items.map(item => ({
        id: item.id,
        name: item.name,
        type: item.item_type,
        tier: item.tier,
        price: Number(item.price),
        slot: item.slot,
    }));
};

Writing Transactions

Starting a Game

const startAdventure = async (
    account: Account,
    weaponType: number,
    name: string
) => {
    const tx = await account.execute({
        contractAddress: GAME_ADDRESS,
        entrypoint: 'start_game',
        calldata: [
            account.address,  // reward address
            weaponType,       // 0: Blade, 1: Bludgeon, 2: Magic
            name,            // adventurer name
            0,               // golden token (0 if none)
            0,               // interface camel case (0 for snake)
        ],
    });
    
    const receipt = await provider.waitForTransaction(tx.transaction_hash);
    
    // Extract adventurer ID from events
    const adventurerId = extractAdventurerIdFromEvents(receipt.events);
    return adventurerId;
};

Combat Actions

// Attack beast
const attack = async (
    account: Account,
    adventurerId: bigint,
    tillDeath: boolean = false
) => {
    const tx = await account.execute({
        contractAddress: GAME_ADDRESS,
        entrypoint: 'attack',
        calldata: [
            adventurerId.toString(),
            tillDeath ? 1 : 0,
        ],
    });
    
    return await provider.waitForTransaction(tx.transaction_hash);
};
 
// Flee from combat
const flee = async (
    account: Account,
    adventurerId: bigint
) => {
    const tx = await account.execute({
        contractAddress: GAME_ADDRESS,
        entrypoint: 'flee',
        calldata: [
            adventurerId.toString(),
            0, // to_the_death
        ],
    });
    
    return await provider.waitForTransaction(tx.transaction_hash);
};

Exploration

const explore = async (
    account: Account,
    adventurerId: bigint,
    tillBeast: boolean = false
) => {
    const tx = await account.execute({
        contractAddress: GAME_ADDRESS,
        entrypoint: 'explore',
        calldata: [
            adventurerId.toString(),
            tillBeast ? 1 : 0,
        ],
    });
    
    return await provider.waitForTransaction(tx.transaction_hash);
};

Upgrading

interface StatUpgrade {
    strength: number;
    dexterity: number;
    vitality: number;
    intelligence: number;
    wisdom: number;
    charisma: number;
    luck: number;
}
 
interface ItemPurchase {
    item_id: number;
    equip: boolean;
}
 
const upgrade = async (
    account: Account,
    adventurerId: bigint,
    stats: StatUpgrade,
    items: ItemPurchase[]
) => {
    const calldata = [
        adventurerId.toString(),
        // Stats (must sum to available points)
        stats.strength,
        stats.dexterity,
        stats.vitality,
        stats.intelligence,
        stats.wisdom,
        stats.charisma,
        stats.luck,
        // Items array
        items.length,
        ...items.flatMap(i => [i.item_id, i.equip ? 1 : 0]),
    ];
    
    const tx = await account.execute({
        contractAddress: GAME_ADDRESS,
        entrypoint: 'upgrade',
        calldata,
    });
    
    return await provider.waitForTransaction(tx.transaction_hash);
};

Event Listening

Setting Up Event Listeners

import { events } from 'starknet';
 
const subscribeToEvents = (adventurerId: bigint) => {
    // Listen for combat events
    gameContract.on('AttackEvent', (event) => {
        if (event.adventurer_id === adventurerId) {
            console.log('Attack:', {
                damage_dealt: event.damage_dealt,
                damage_taken: event.damage_taken,
                is_critical: event.is_critical,
            });
        }
    });
    
    // Listen for level up
    gameContract.on('LevelUpEvent', (event) => {
        if (event.adventurer_id === adventurerId) {
            console.log('Level Up! New level:', event.new_level);
        }
    });
    
    // Listen for death
    gameContract.on('AdventurerDiedEvent', (event) => {
        if (event.adventurer_id === adventurerId) {
            console.log('Game Over! Final level:', event.level);
        }
    });
};

Processing Historical Events

const getAdventurerHistory = async (adventurerId: bigint) => {
    const events = await provider.getEvents({
        address: GAME_ADDRESS,
        from_block: { block_number: 0 },
        to_block: 'latest',
        keys: [[adventurerId.toString()]],
        chunk_size: 100,
    });
    
    return events.events.map(parseEvent);
};

Using Torii Indexer

GraphQL Queries

const TORII_URL = "https://api.cartridge.gg/x/lootsurvivor/torii/graphql";
 
const query = `
    query GetAdventurer($id: String!) {
        adventurer(id: $id) {
            id
            owner
            level
            health
            gold
            experience
            stats {
                strength
                dexterity
                vitality
                intelligence
                wisdom
                charisma
                luck
            }
        }
    }
`;
 
const fetchAdventurerData = async (adventurerId: string) => {
    const response = await fetch(TORII_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            query,
            variables: { id: adventurerId },
        }),
    });
    
    const data = await response.json();
    return data.data.adventurer;
};

WebSocket Subscriptions

import { createClient } from 'graphql-ws';
 
const client = createClient({
    url: 'wss://api.cartridge.gg/x/lootsurvivor/torii/ws',
});
 
const subscription = `
    subscription OnAdventurerUpdate($id: String!) {
        adventurerUpdated(id: $id) {
            id
            level
            health
            gold
        }
    }
`;
 
client.subscribe(
    {
        query: subscription,
        variables: { id: adventurerId },
    },
    {
        next: (data) => console.log('Update:', data),
        error: (err) => console.error('Error:', err),
        complete: () => console.log('Subscription complete'),
    }
);

Building Custom Frontends

React Hook Example

import { useState, useEffect } from 'react';
 
const useAdventurer = (adventurerId: bigint) => {
    const [adventurer, setAdventurer] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    
    useEffect(() => {
        const fetchData = async () => {
            try {
                setLoading(true);
                const data = await getAdventurer(adventurerId);
                setAdventurer(data);
            } catch (err) {
                setError(err);
            } finally {
                setLoading(false);
            }
        };
        
        fetchData();
        
        // Set up event listeners
        const unsubscribe = subscribeToAdventurerUpdates(
            adventurerId,
            setAdventurer
        );
        
        return unsubscribe;
    }, [adventurerId]);
    
    return { adventurer, loading, error };
};

State Management

import { create } from 'zustand';
 
interface GameStore {
    adventurer: Adventurer | null;
    beast: Beast | null;
    market: MarketItem[];
    
    setAdventurer: (adventurer: Adventurer) => void;
    setBeast: (beast: Beast) => void;
    setMarket: (items: MarketItem[]) => void;
    
    attack: () => Promise<void>;
    flee: () => Promise<void>;
    explore: () => Promise<void>;
}
 
const useGameStore = create<GameStore>((set, get) => ({
    adventurer: null,
    beast: null,
    market: [],
    
    setAdventurer: (adventurer) => set({ adventurer }),
    setBeast: (beast) => set({ beast }),
    setMarket: (market) => set({ market }),
    
    attack: async () => {
        const { adventurer } = get();
        if (!adventurer) return;
        
        const result = await attackBeast(adventurer.id);
        // Update state based on result
    },
    
    // ... other actions
}));

Building Game Tools

Leaderboard API

const getLeaderboard = async (limit = 100) => {
    const query = `
        query GetLeaderboard($limit: Int!) {
            adventurers(
                orderBy: "level",
                orderDirection: "desc",
                limit: $limit
            ) {
                id
                owner
                name
                level
                gold
                timestamp
            }
        }
    `;
    
    const response = await fetch(TORII_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            query,
            variables: { limit },
        }),
    });
    
    const data = await response.json();
    return data.data.adventurers;
};

Statistics Tracker

const getPlayerStats = async (playerAddress: string) => {
    const query = `
        query GetPlayerStats($owner: String!) {
            adventurers(where: { owner: $owner }) {
                id
                level
                gold
                experience
                timestamp
                died_at
            }
        }
    `;
    
    const response = await fetch(TORII_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            query,
            variables: { owner: playerAddress },
        }),
    });
    
    const data = await response.json();
    
    // Calculate statistics
    const adventures = data.data.adventurers;
    return {
        totalAdventures: adventures.length,
        highestLevel: Math.max(...adventures.map(a => a.level)),
        totalGold: adventures.reduce((sum, a) => sum + a.gold, 0),
        averageLevel: adventures.reduce((sum, a) => sum + a.level, 0) / adventures.length,
    };
};

Error Handling

Common Errors

const handleGameAction = async (action: () => Promise<any>) => {
    try {
        const result = await action();
        return { success: true, data: result };
    } catch (error) {
        // Handle specific errors
        if (error.message.includes('Adventurer is dead')) {
            return { success: false, error: 'GAME_OVER' };
        }
        if (error.message.includes('Insufficient gold')) {
            return { success: false, error: 'INSUFFICIENT_FUNDS' };
        }
        if (error.message.includes('Invalid stat allocation')) {
            return { success: false, error: 'INVALID_STATS' };
        }
        
        // Generic error
        return { success: false, error: error.message };
    }
};

Retry Logic

const retryWithBackoff = async (
    fn: () => Promise<any>,
    maxRetries = 3,
    delay = 1000
) => {
    for (let i = 0; i < maxRetries; i++) {
        try {
            return await fn();
        } catch (error) {
            if (i === maxRetries - 1) throw error;
            await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
        }
    }
};

Testing Integration

Unit Tests

import { describe, it, expect } from 'vitest';
 
describe('Game Integration', () => {
    it('should start a new game', async () => {
        const adventurerId = await startAdventure(
            mockAccount,
            WeaponType.Blade,
            'Test Hero'
        );
        
        expect(adventurerId).toBeGreaterThan(0n);
    });
    
    it('should handle combat', async () => {
        const result = await attack(mockAccount, adventurerId);
        expect(result.status).toBe('ACCEPTED');
    });
});

Integration Tests

describe('Full Game Flow', () => {
    it('should complete a full game cycle', async () => {
        // Start game
        const id = await startAdventure(account, 0, 'Hero');
        
        // Explore
        await explore(account, id, false);
        
        // Combat
        const beast = await getCurrentBeast(id);
        if (beast) {
            await attack(account, id, false);
        }
        
        // Check state
        const adventurer = await getAdventurer(id);
        expect(adventurer.experience).toBeGreaterThan(0);
    });
});

Performance Optimization

Caching Strategy

class GameCache {
    private cache = new Map();
    private ttl = 5000; // 5 seconds
    
    async get(key: string, fetcher: () => Promise<any>) {
        const cached = this.cache.get(key);
        
        if (cached && Date.now() - cached.timestamp < this.ttl) {
            return cached.data;
        }
        
        const data = await fetcher();
        this.cache.set(key, { data, timestamp: Date.now() });
        return data;
    }
}
 
const cache = new GameCache();
 
const getCachedAdventurer = (id: bigint) =>
    cache.get(`adventurer:${id}`, () => getAdventurer(id));

Batch Requests

const batchGetAdventurers = async (ids: bigint[]) => {
    const promises = ids.map(id => getAdventurer(id));
    return await Promise.all(promises);
};

Security Best Practices

Input Validation

const validateAdventurerId = (id: any): bigint => {
    if (typeof id !== 'bigint' && typeof id !== 'string') {
        throw new Error('Invalid adventurer ID type');
    }
    
    const bigIntId = BigInt(id);
    
    if (bigIntId <= 0n) {
        throw new Error('Adventurer ID must be positive');
    }
    
    return bigIntId;
};

Rate Limiting

class RateLimiter {
    private requests = new Map();
    private limit = 10; // requests per second
    
    canMakeRequest(key: string): boolean {
        const now = Date.now();
        const requests = this.requests.get(key) || [];
        
        // Remove old requests
        const recent = requests.filter(t => now - t < 1000);
        
        if (recent.length >= this.limit) {
            return false;
        }
        
        recent.push(now);
        this.requests.set(key, recent);
        return true;
    }
}

Conclusion

This integration guide provides the foundation for building applications that interact with Loot Survivor. Whether you're creating a custom frontend, building tools, or extending the game, these patterns and examples will help you get started quickly and build robust integrations.