### Setup Project Slippi JS development environment Source: https://github.com/project-slippi/slippi-js/blob/master/README.md Commands to clone the slippi-js repository, navigate to the project directory, and install dependencies using npm. This sets up the local environment for development. ```bash git clone https://github.com/project-slippi/slippi-js cd slippi-js npm install ``` -------------------------------- ### Install slippi-js using NPM or Yarn Source: https://github.com/project-slippi/slippi-js/blob/master/README.md Instructions for installing the slippi-js package using either NPM or Yarn package managers. This is a prerequisite for using the SDK in your project. ```bash npm install @slippi/slippi-js ``` ```bash yarn add @slippi/slippi-js ``` -------------------------------- ### Read live SLP files with JavaScript for real-time updates Source: https://github.com/project-slippi/slippi-js/blob/master/README.md Example of how to configure the SlippiGame class to process SLP files on-the-fly for real-time data extraction during live gameplay. Enable `processOnTheFly: true` when initializing the SlippiGame object. ```javascript const game = new SlippiGame("path/to/your/slp/file", { processOnTheFly: true }); ``` -------------------------------- ### Stage Data Lookup with Slippi-js Source: https://context7.com/project-slippi/slippi-js/llms.txt Access stage names and information for all Melee stages using the `stages` utility. This includes functions to get stage info by ID and get stage names directly. Examples demonstrate common stage IDs and practical use in analyzing stage frequency from multiple replays. ```javascript const { stages } = require("@slippi/slippi-js"); // Get stage information by ID const stageInfo = stages.getStageInfo(0x02); // Fountain of Dreams console.log(`Stage ID 0x02: ${stageInfo.name}`); // Get stage name directly const stageName = stages.getStageName(0x08); // Yoshi's Story console.log(`Stage: ${stageName}`); // Common stage IDs const commonStages = [ { id: 0x02, name: "Fountain of Dreams" }, { id: 0x03, name: "Pokemon Stadium" }, { id: 0x08, name: "Yoshi's Story" }, { id: 0x1C, name: "Dream Land N64" }, { id: 0x1F, name: "Battlefield" }, { id: 0x20, name: "Final Destination" }, ]; commonStages.forEach(stage => { const retrievedName = stages.getStageName(stage.id); console.log(`0x${stage.id.toString(16)}: ${retrievedName}`); }); // Practical example: Stage statistics from multiple replays const fs = require("fs"); const { SlippiGame } = require("@slippi/slippi-js"); const replayFiles = fs.readdirSync("./replays").filter(f => f.endsWith(".slp")); const stageCount = {}; replayFiles.forEach(filename => { const game = new SlippiGame(`./replays/${filename}`); const settings = game.getSettings(); if (settings) { const stageName = stages.getStageName(settings.stageId); stageCount[stageName] = (stageCount[stageName] || 0) + 1; } }); console.log("\nStage frequency:"); Object.entries(stageCount) .sort((a, b) => b[1] - a[1]) .forEach(([stage, count]) => { console.log(` ${stage}: ${count} games`); }); ``` -------------------------------- ### Access Melee Move Data with Slippi JS Source: https://context7.com/project-slippi/slippi-js/llms.txt Retrieve move names, short names, and IDs using the `@slippi/slippi-js` library. This is useful for parsing game data, displaying move information, and analyzing player actions. It requires the `@slippi/slippi-js` package to be installed. ```javascript const { moves } = require("@slippi/slippi-js"); // Get move information by ID const moveInfo = moves.getMoveInfo(0x02); // Jab 1 console.log(`Move ID 0x02:\n Name: ${moveInfo.name}\n Short name: ${moveInfo.shortName}\n ID: ${moveInfo.id}`); // Get move name directly const moveName = moves.getMoveName(0x0A); // "Forward Tilt" console.log(`Move 0x0A: ${moveName}`); // Get short move name const shortName = moves.getMoveShortName(0x0D); // "Dash Attack" console.log(`Short name: ${shortName}`); ``` -------------------------------- ### Character Data Lookup with Slippi-js Source: https://context7.com/project-slippi/slippi-js/llms.txt Access character names, IDs, and color information for all 26 Melee characters using the `characters` utility. This includes functions to get character info by ID, get names, short names, color names, and retrieve all character data. It also shows how to integrate this with replay analysis. ```javascript const { characters } = require("@slippi/slippi-js"); // Get full character information const foxInfo = characters.getCharacterInfo(0x00); // Fox's external ID console.log(`Name: ${foxInfo.name}`); // "Fox" console.log(`Short name: ${foxInfo.shortName}`); // "Fox" console.log(`ID: ${foxInfo.id}`); // 0 console.log(`Available colors:`, foxInfo.colors); // ["Default", "Red", "Blue", "Green"] // Get character name by ID const charName = characters.getCharacterName(0x02); // "Samus" console.log(`Character 0x02: ${charName}`); // Get short name (useful for display) const shortName = characters.getCharacterShortName(0x13); // "Puff" console.log(`Short name: ${shortName}`); // Get color name for character costume const colorName = characters.getCharacterColorName(0x00, 1); // Fox, Red costume console.log(`Fox color 1: ${colorName}`); // "Red" // Get all characters const allCharacters = characters.getAllCharacters(); console.log(`Total characters: ${allCharacters.length}`); // 26 // Example: Building a character select display allCharacters.forEach(char => { console.log(`${char.id}: ${char.name} (${char.shortName})`); console.log(` Colors: ${char.colors.join(", ")}`); }); // Practical example: Analyze character usage from a replay const { SlippiGame } = require("@slippi/slippi-js"); const game = new SlippiGame("replay.slp"); const settings = game.getSettings(); settings.players.forEach(player => { const charInfo = characters.getCharacterInfo(player.characterId); const colorName = characters.getCharacterColorName( player.characterId, player.characterColor ); console.log( `Port ${player.port}: ${charInfo.name} (${colorName}) - ${player.nametag}` ); }); ``` -------------------------------- ### Process Live Game Data with Slippi-js in JavaScript Source: https://context7.com/project-slippi/slippi-js/llms.txt This snippet shows how to use slippi-js to watch a directory for new replay files and process them in real-time. It leverages `chokidar` for file watching and `SlippiGame` with `processOnTheFly: true` to get live game states, player data, and combo information. The script logs player statistics and combo details as the game progresses and displays final statistics upon game end. ```javascript const { SlippiGame } = require("@slippi/slippi-js"); const chokidar = require("chokidar"); const replayFolder = "/path/to/slippi/replays"; const gamesByPath = {}; // Watch for file changes const watcher = chokidar.watch(replayFolder, { depth: 0, persistent: true, usePolling: true, ignoreInitial: true, }); watcher.on("change", (filePath) => { try { // Get or create game instance with live processing enabled let game = gamesByPath[filePath]; if (!game) { console.log(`New game detected: ${filePath}`); // IMPORTANT: Enable processOnTheFly for live stats game = new SlippiGame(filePath, { processOnTheFly: true }); gamesByPath[filePath] = game; } // Get current game state const settings = game.getSettings(); const latestFrame = game.getLatestFrame(); const stats = game.getStats(); if (!settings || !latestFrame) { return; // Game not started yet } // Display live player information console.log(`\nFrame ${latestFrame.frame}:`); settings.players.forEach(player => { const frameData = latestFrame.players[player.playerIndex]; if (frameData && frameData.post) { const post = frameData.post; console.log( ` Port ${player.port}: ${post.percent.toFixed(1)}% | ` + `${post.stocksRemaining} stocks | ` + `State: ${post.actionStateId}` ); } }); // Display live combo detection if (stats && stats.combos) { const activeCombos = stats.combos.filter(combo => !combo.endFrame); if (activeCombos.length > 0) { console.log(` Active combos: ${activeCombos.length}`); activeCombos.forEach(combo => { const duration = latestFrame.frame - combo.startFrame; console.log( ` Player ${combo.playerIndex}: ${combo.moves.length} moves, ` + `${combo.currentComboPercent.toFixed(1)}% damage, ${duration} frames` ); }); } } // Check if game ended const gameEnd = game.getGameEnd(); if (gameEnd) { const endTypes = { 1: "TIME!", 2: "GAME!", 7: "No Contest", }; console.log(`\n[GAME END] ${endTypes[gameEnd.gameEndMethod] || "Unknown"}`); // Final statistics const finalStats = game.getStats(); if (finalStats) { console.log("\nFinal Statistics:"); finalStats.overall.forEach((playerStats, idx) => { console.log(` Player ${idx}:`); console.log(` Inputs per minute: ${playerStats.inputsPerMinute.ratio.toFixed(2)}`); console.log(` Total damage: ${playerStats.totalDamage.toFixed(1)}%`); console.log(` Kills: ${playerStats.killCount}`); }); } // Clean up delete gamesByPath[filePath]; } } catch (err) { console.error(`Error processing ${filePath}:`, err.message); } }); console.log(`Watching for live games in: ${replayFolder}`); ``` -------------------------------- ### Parse and Analyze Melee Replay Files with slippi-js Source: https://context7.com/project-slippi/slippi-js/llms.txt This JavaScript code demonstrates how to use the SlippiGame class from the @slippi/slippi-js library to load replay files. It shows how to extract game settings, metadata, comprehensive statistics, frame-by-frame data, determine winners, and get game end information. This is useful for building replay analysis tools. ```javascript const { SlippiGame } = require("@slippi/slippi-js"); // Load a replay file const game = new SlippiGame("replays/tournament_final.slp"); // Get game settings (stage, characters, players) const settings = game.getSettings(); console.log(`Stage: ${settings.stageId}`); console.log(`Players: ${settings.players.length}`); settings.players.forEach(player => { console.log(` Port ${player.port}: Character ${player.characterId}, Color ${player.characterColor}`); console.log(` Tag: ${player.nametag}`); }); // Get metadata (timestamp, console info) const metadata = game.getMetadata(); console.log(`Played on: ${metadata.startAt}`); console.log(`Console: ${metadata.consoleNick}`); console.log(`Duration: ${metadata.lastFrame} frames`); // Get comprehensive statistics const stats = game.getStats(); console.log(`Game completed: ${stats.gameComplete}`); console.log(`Total frames: ${stats.lastFrame}`); // Access per-player statistics stats.overall.forEach((playerStats, idx) => { console.log(`\nPlayer ${idx}:`); console.log(` Inputs per minute: ${playerStats.inputsPerMinute.ratio.toFixed(2)}`); console.log(` Total damage dealt: ${playerStats.totalDamage.toFixed(1)}%`); console.log(` Kill count: ${playerStats.killCount}`); console.log(` Conversions: ${playerStats.conversionCount}`); console.log(` Successful conversion rate: ${(playerStats.successfulConversions.ratio * 100).toFixed(1)}%`); console.log(` Neutral win rate: ${(playerStats.neutralWinRatio.ratio * 100).toFixed(1)}%`); console.log(` Damage per opening: ${playerStats.damagePerOpening.ratio.toFixed(2)}%`); }); // Get frame-by-frame data const frames = game.getFrames(); const frameCount = Object.keys(frames).length; console.log(`\nTotal frames captured: ${frameCount}`); // Access specific frame data const firstFrame = frames[-123]; // Frame -123 is character select if (firstFrame) { firstFrame.players.forEach((playerFrame, idx) => { const post = playerFrame.post; console.log(`Player ${idx} at frame -123:`); console.log(` Position: (${post.positionX}, ${post.positionY})`); console.log(` Damage: ${post.percent}%`); console.log(` Stocks: ${post.stocksRemaining}`); }); } // Get latest frame (useful for live replays) const latestFrame = game.getLatestFrame(); if (latestFrame) { console.log(`\nLatest frame number: ${latestFrame.frame}`); } // Determine winners const winners = game.getWinners(); winners.forEach(placement => { console.log(`Place ${placement.position}: Player ${placement.playerIndex}`); }); // Get game end information const gameEnd = game.getGameEnd(); if (gameEnd) { console.log(`\nGame end method: ${gameEnd.gameEndMethod}`); // 1=TIME, 2=GAME, 7=NO_CONTEST console.log(`LRAS initiator: ${gameEnd.lrasInitiatorIndex}`); } ``` -------------------------------- ### Establish Real-Time Console Connection with Slippi.js Source: https://context7.com/project-slippi/slippi-js/llms.txt Connects to a Wii console or Slippi relay server to receive live game data over TCP. It utilizes the ConsoleConnection class from the @slippi/slippi-js library. Handles connection status changes, incoming game data (raw SLP format), handshake details, and errors. Includes an example of using SlippiGame for in-memory data processing and programmatic status checks. ```javascript const { ConsoleConnection, ConnectionEvent, ConnectionStatus } = require("@slippi/slippi-js"); const { SlippiGame } = require("@slippi/slippi-js"); // Create connection instance const connection = new ConsoleConnection(); // IP and port of your Wii console or relay const CONSOLE_IP = "192.168.1.100"; const CONSOLE_PORT = 51441; // Default Slippi port // Track connection status connection.on(ConnectionEvent.STATUS_CHANGE, (status) => { const statusNames = { [ConnectionStatus.DISCONNECTED]: "Disconnected", [ConnectionStatus.CONNECTING]: "Connecting...", [ConnectionStatus.CONNECTED]: "Connected", [ConnectionStatus.RECONNECT_WAIT]: "Reconnecting...", }; console.log(`Connection status: ${statusNames[status]}`); if (status === ConnectionStatus.CONNECTED) { const details = connection.getDetails(); console.log(`Connected to: ${details.consoleNick}`); console.log(`Version: ${details.version}`); } }); // Handle connection established connection.on(ConnectionEvent.CONNECT, () => { console.log("Successfully connected to console!"); const settings = connection.getSettings(); console.log(`Connected to: ${settings.ipAddress}:${settings.port}`); }); // Handle incoming game data let currentGame = null; connection.on(ConnectionEvent.DATA, (data) => { // Data is raw SLP format - you can write to file or process in memory console.log(`Received ${data.length} bytes of game data`); // Example: Write to a file for processing // fs.appendFileSync("live_game.slp", data); // Or process in memory with SlippiGame if (!currentGame) { currentGame = new SlippiGame(data, { processOnTheFly: true }); } }); // Handle handshake (initial connection setup) connection.on(ConnectionEvent.HANDSHAKE, (details) => { console.log("Handshake completed:"); console.log(` Console nickname: ${details.consoleNick}`); console.log(` Version: ${details.version}`); console.log(` Client token: ${details.clientToken}`); }); // Handle errors connection.on(ConnectionEvent.ERROR, (error) => { console.error("Connection error:", error.message); }); // Handle general messages connection.on(ConnectionEvent.MESSAGE, (message) => { console.log("Received message:", message); }); // Start the connection console.log(`Connecting to ${CONSOLE_IP}:${CONSOLE_PORT}...`); connection.connect(CONSOLE_IP, CONSOLE_PORT); // Check connection status programmatically setInterval(() => { const status = connection.getStatus(); if (status === ConnectionStatus.CONNECTED) { const details = connection.getDetails(); console.log(`Still connected to: ${details.consoleNick}`); } }, 30000); // Check every 30 seconds // Graceful shutdown process.on("SIGINT", () => { console.log("\nDisconnecting..."); connection.disconnect(); process.exit(0); }); // Example with auto-reconnect disabled const manualConnection = new ConsoleConnection({ autoReconnect: false }); // Connect and manually handle disconnections manualConnection.connect(CONSOLE_IP, CONSOLE_PORT); ``` -------------------------------- ### Run tests for slippi-js Source: https://github.com/project-slippi/slippi-js/blob/master/README.md Command to execute the test suite for the slippi-js project. This ensures the library is functioning correctly. ```bash npm run test ``` -------------------------------- ### Build slippi-js project Source: https://github.com/project-slippi/slippi-js/blob/master/README.md Commands to build the slippi-js project. `npm run build` performs a standard build, while `npm run watch` continuously rebuilds the project as changes are detected. ```bash npm run build ``` ```bash npm run watch ``` -------------------------------- ### Analyze Combos and Conversions from Replay Files Source: https://context7.com/project-slippi/slippi-js/llms.txt Utilize `SlippiGame` to load replay files and extract detailed combo and conversion statistics. This includes combo start/end frames, damage dealt, move sequences, and kill status. It requires a replay file path and the `@slippi/slippi-js` package. ```javascript const { SlippiGame } = require("@slippi/slippi-js"); const game = new SlippiGame("replay.slp"); const stats = game.getStats(); if (stats && stats.combos) { console.log(`Total combos: ${stats.combos.length}`); stats.combos.forEach((combo, idx) => { if (combo.moves.length >= 4) { // Show only 4+ hit combos console.log(`\nCombo #${idx + 1} by Player ${combo.playerIndex}:\n Frames ${combo.startFrame}-${combo.endFrame}\n Damage: ${combo.endPercent - combo.startPercent}%\n Moves used:`); combo.moves.forEach(move => { const moveName = moves.getMoveName(move.moveId); console.log(` ${moveName} (${move.hitCount} hits)`); }); } }); } // Example: Count move usage across all conversions const moveUsage = {}; stats.conversions.forEach(conversion => { conversion.moves.forEach(move => { const moveName = moves.getMoveName(move.moveId); moveUsage[moveName] = (moveUsage[moveName] || 0) + 1; }); }); console.log("\nMost used moves in conversions:"); Object.entries(moveUsage) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .forEach(([move, count]) => { console.log(` ${move}: ${count} times`); }); ``` -------------------------------- ### Analyze Slippi Game Conversions and Openings with JavaScript Source: https://context7.com/project-slippi/slippi-js/llms.txt This JavaScript code snippet uses the Slippi-JS library to load and analyze a game replay file ('replay.slp'). It extracts conversion statistics, including details about each conversion's attacker, victim, duration, opening type, damage dealt, and move sequence. It also aggregates statistics by opening type and calculates per-player conversion success rates and average damage. Requires the '@slippi/slippi-js' package. ```javascript const { SlippiGame } = require("@slippi/slippi-js"); const game = new SlippiGame("replay.slp"); const stats = game.getStats(); if (stats && stats.conversions) { console.log(`Total conversions: ${stats.conversions.length}\n`); // Analyze each conversion stats.conversions.forEach((conversion, idx) => { console.log(`Conversion #${idx + 1}: `); console.log(` Attacker: Player ${conversion.playerIndex}`); console.log(` Victim: Player ${conversion.opponentIndex}`); console.log(` Frames: ${conversion.startFrame} to ${conversion.endFrame || "ongoing"}`); // Opening type classification console.log(` Opening type: ${conversion.openingType}`); // "neutral-win", "counter-attack", "punish", or "trade" // Damage dealt const damage = conversion.endPercent - conversion.startPercent; console.log(` Damage: ${damage.toFixed(1)}%`); console.log(` Start %: ${conversion.startPercent.toFixed(1)}%`); console.log(` End %: ${conversion.endPercent.toFixed(1)}%`); // Move sequence console.log(` Moves used: ${conversion.moves.length}`); conversion.moves.forEach((move, moveIdx) => { console.log(` ${moveIdx + 1}. Move ${move.moveId} at frame ${move.frame} (${move.hitCount} hits)`); }); // Result if (conversion.didKill) { console.log(` Result: KILL (stock lost)`); } console.log(); }); // Aggregate conversion statistics by opening type const openingTypeStats = { "neutral-win": { count: 0, totalDamage: 0, kills: 0 }, "counter-attack": { count: 0, totalDamage: 0, kills: 0 }, "punish": { count: 0, totalDamage: 0, kills: 0 }, "trade": { count: 0, totalDamage: 0, kills: 0 }, }; stats.conversions.forEach(conversion => { const type = conversion.openingType; if (openingTypeStats[type]) { openingTypeStats[type].count++; openingTypeStats[type].totalDamage += conversion.endPercent - conversion.startPercent; if (conversion.didKill) { openingTypeStats[type].kills++; } } }); console.log("Opening Type Analysis:"); Object.entries(openingTypeStats).forEach(([type, data]) => { if (data.count > 0) { console.log(`\n${type}: `); console.log(` Count: ${data.count}`); console.log(` Avg damage: ${(data.totalDamage / data.count).toFixed(2)}%`); console.log(` Kills: ${data.kills}`); console.log(` Conversion rate: ${((data.kills / data.count) * 100).toFixed(1)}%`); } }); // Per-player conversion success const playerConversions = {}; stats.conversions.forEach(conversion => { const pid = conversion.playerIndex; if (!playerConversions[pid]) { playerConversions[pid] = { total: 0, successful: 0, totalDamage: 0, }; } playerConversions[pid].total++; playerConversions[pid].totalDamage += conversion.endPercent - conversion.startPercent; if (conversion.didKill) { playerConversions[pid].successful++; } }); console.log("\n\nPer-Player Conversion Statistics:"); Object.entries(playerConversions).forEach(([playerIndex, data]) => { console.log(`\nPlayer ${playerIndex}: `); console.log(` Total conversions: ${data.total}`); console.log(` Successful (killed): ${data.successful}`); console.log(` Success rate: ${((data.successful / data.total) * 100).toFixed(1)}%`); console.log(` Avg damage per conversion: ${(data.totalDamage / data.total).toFixed(2)}%`); }); } ``` -------------------------------- ### Detailed Combo Analysis from Replay Files Source: https://context7.com/project-slippi/slippi-js/llms.txt Extract comprehensive combo data from Slippi replay files, including individual combo details like attacker, victim, frames, damage, move sequence, and kill status. Also aggregates per-player statistics such as combo count, total damage, and kill rates. Requires a replay file path and the `@slippi/slippi-js` package. ```javascript const { SlippiGame } = require("@slippi/slippi-js"); const game = new SlippiGame("replay.slp"); const stats = game.getStats(); if (stats && stats.combos) { console.log(`Total combos detected: ${stats.combos.length}\n`); // Analyze each combo stats.combos.forEach((combo, idx) => { console.log(`Combo #${idx + 1}:\n Attacker: Player ${combo.playerIndex}\n Victim: Player ${combo.opponentIndex}\n Frames: ${combo.startFrame} to ${combo.endFrame || "ongoing"}\n Duration: ${(combo.endFrame || combo.lastFrame) - combo.startFrame} frames`); // Damage information const damage = combo.endPercent - combo.startPercent; console.log(` Damage: ${combo.startPercent.toFixed(1)}% -> ${combo.endPercent.toFixed(1)}% (+${damage.toFixed(1)}%)`); // Move sequence console.log(` Moves: ${combo.moves.length} different moves`); combo.moves.forEach(move => { console.log(` Move ID ${move.moveId}: ${move.hitCount} hits`); }); // Combo ending if (combo.didKill) { console.log(` Result: KILL`); } else if (combo.endFrame) { console.log(` Result: Combo ended`); } else { console.log(` Result: Still active`); } console.log(); }); // Aggregate combo statistics const playerCombos = {}; let totalDamage = 0; let kills = 0; stats.combos.forEach(combo => { const pid = combo.playerIndex; if (!playerCombos[pid]) { playerCombos[pid] = { count: 0, totalDamage: 0, kills: 0, maxDamage: 0, }; } const damage = combo.endPercent - combo.startPercent; playerCombos[pid].count++; playerCombos[pid].totalDamage += damage; playerCombos[pid].maxDamage = Math.max(playerCombos[pid].maxDamage, damage); if (combo.didKill) { playerCombos[pid].kills++; kills++; } totalDamage += damage; }); console.log("Per-Player Combo Statistics:"); Object.entries(playerCombos).forEach(([playerIndex, data]) => { console.log(`\nPlayer ${playerIndex}:\n Total combos: ${data.count}\n Average damage per combo: ${(data.totalDamage / data.count).toFixed(2)}%\n Max combo damage: ${data.maxDamage.toFixed(1)}%\n Combos leading to kills: ${data.kills}\n Kill rate: ${((data.kills / data.count) * 100).toFixed(1)}%`); }); } ``` -------------------------------- ### Access Frame-Level Game Data with Slippi.js Source: https://context7.com/project-slippi/slippi-js/llms.txt This JavaScript snippet shows how to load a Slippi replay file and access its per-frame data. It demonstrates retrieving all frames and game settings, then iterating through a specific frame to access player inputs (pre-frame) and game state (post-frame) such as position, velocity, damage, and stocks. Dependencies include the '@slippi/slippi-js' package. ```javascript const { SlippiGame } = require("@slippi/slippi-js"); const game = new SlippiGame("replay.slp"); const frames = game.getFrames(); const settings = game.getSettings(); console.log(`Total frames: ${Object.keys(frames).length}\n`); // Access a specific frame const frame = frames[100]; // Frame 100 if (frame) { console.log(`Frame ${frame.frame}:`); // Iterate through players frame.players.forEach((playerFrame, idx) => { console.log(`\n Player ${idx}:`); // Pre-frame data (inputs before physics) const pre = playerFrame.pre; console.log(` Pre-frame (inputs):`); console.log(` Random seed: ${pre.randomSeed}`); console.log(` Action state: ${pre.actionStateId}`); console.log(` Position: (${pre.positionX}, ${pre.positionY})`); console.log(` Facing direction: ${pre.facingDirection}`); console.log(` Joystick: (${pre.joystickX}, ${pre.joystickY})`); console.log(` C-stick: (${pre.cStickX}, ${pre.cStickY})`); console.log(` Trigger: L=${pre.trigger.logical}, R=${pre.trigger.physical}`); console.log(` Buttons: ${pre.buttons.logical} (logical), ${pre.buttons.physical} (physical)`); // Post-frame data (state after physics) const post = playerFrame.post; console.log(` Post-frame (state):`); console.log(` Position: (${post.positionX?.toFixed(2)}, ${post.positionY?.toFixed(2)})`); console.log(` Velocity: (${post.velocityX?.toFixed(2)}, ${post.velocityY?.toFixed(2)})`); console.log(` Damage: ${post.percent?.toFixed(1)}%`); console.log(` Shield health: ${post.shieldHealth?.toFixed(1)}`); console.log(` Stocks: ${post.stocksRemaining}`); console.log(` Action state: ${post.actionStateId}`); console.log(` Animation state: ${post.stateFrame}`); console.log(` Flags: ${post.flags}`); console.log(` Miscellaneous AS: ${post.miscActionState}`); console.log(` Airborne: ${post.isAirborne ? "Yes" : "No"}`); console.log(` Last attack landed: ${post.lastAttackLanded}`); console.log(` Combo count: ${post.comboCount}`); console.log(` Last hit by: ${post.lastHitBy}`); console.log(` Jumps remaining: ${post.jumpsRemaining}`); }); } ``` -------------------------------- ### Batch Process Replays and Aggregate Statistics (JavaScript) Source: https://context7.com/project-slippi/slippi-js/llms.txt This JavaScript code uses the 'slippi-js' library to read and process multiple replay files (.slp) from a directory. It aggregates statistics like total games, character and stage usage, player matchups, combo counts, and total damage dealt. The script handles potential errors during file processing and outputs the aggregated data to a JSON file named 'aggregate_stats.json'. Dependencies include 'fs' and 'path' from Node.js, and '@slippi/slippi-js'. ```javascript const { SlippiGame, characters, stages } = require("@slippi/slippi-js"); const fs = require("fs"); const path = require("path"); const replayDirectory = "./replays"; const replayFiles = fs.readdirSync(replayDirectory) .filter(file => file.endsWith(".slp")) .map(file => path.join(replayDirectory, file)); console.log(`Found ${replayFiles.length} replay files\n`); // Aggregate statistics const aggregateStats = { totalGames: 0, characterUsage: {}, stageUsage: {}, playerMatchups: {}, totalCombos: 0, totalDamage: 0, averageGameLength: 0, totalFrames: 0, }; replayFiles.forEach((filePath, idx) => { try { console.log(`Processing ${idx + 1}/${replayFiles.length}: ${path.basename(filePath)}`); const game = new SlippiGame(filePath); const settings = game.getSettings(); const stats = game.getStats(); const metadata = game.getMetadata(); if (!settings || !stats) { console.log(" Skipped: Invalid or incomplete replay\n"); return; } aggregateStats.totalGames++; // Track stage usage const stageName = stages.getStageName(settings.stageId); aggregateStats.stageUsage[stageName] = (aggregateStats.stageUsage[stageName] || 0) + 1; // Track character usage settings.players.forEach(player => { const charName = characters.getCharacterName(player.characterId); aggregateStats.characterUsage[charName] = (aggregateStats.characterUsage[charName] || 0) + 1; }); // Track matchups (for 1v1) if (settings.players.length === 2) { const char1 = characters.getCharacterName(settings.players[0].characterId); const char2 = characters.getCharacterName(settings.players[1].characterId); const matchup = [char1, char2].sort().join(" vs "); aggregateStats.playerMatchups[matchup] = (aggregateStats.playerMatchups[matchup] || 0) + 1; } // Combo statistics if (stats.combos) { aggregateStats.totalCombos += stats.combos.length; } // Damage statistics if (stats.overall) { stats.overall.forEach(playerStats => { aggregateStats.totalDamage += playerStats.totalDamage; }); } // Game length if (metadata && metadata.lastFrame) { aggregateStats.totalFrames += metadata.lastFrame; } console.log(` Processed: ${stats.combos?.length || 0} combos, ${metadata?.lastFrame || 0} frames\n`); } catch (err) { console.error(` Error: ${err.message}\n`); } }); // Calculate averages if (aggregateStats.totalGames > 0) { aggregateStats.averageGameLength = aggregateStats.totalFrames / aggregateStats.totalGames; } // Display results console.log("\n========================================"); console.log("AGGREGATE STATISTICS"); console.log("========================================\n"); console.log(`Total games processed: ${aggregateStats.totalGames}`); console.log(`Average game length: ${aggregateStats.averageGameLength.toFixed(0)} frames (${(aggregateStats.averageGameLength / 60).toFixed(1)} seconds)`); console.log(`Total combos: ${aggregateStats.totalCombos}`); console.log(`Average combos per game: ${(aggregateStats.totalCombos / aggregateStats.totalGames).toFixed(2)}`); console.log(`Total damage dealt: ${aggregateStats.totalDamage.toFixed(1)}%`); console.log("\nMost Popular Characters:"); Object.entries(aggregateStats.characterUsage) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .forEach(([character, count], idx) => { const percentage = ((count / (aggregateStats.totalGames * 2)) * 100).toFixed(1); console.log(` ${idx + 1}. ${character}: ${count} games (${percentage}%)`); }); console.log("\nMost Popular Stages:"); Object.entries(aggregateStats.stageUsage) .sort((a, b) => b[1] - a[1]) .forEach(([stage, count], idx) => { const percentage = ((count / aggregateStats.totalGames) * 100).toFixed(1); console.log(` ${idx + 1}. ${stage}: ${count} games (${percentage}%)`); }); console.log("\nMost Common Matchups:"); Object.entries(aggregateStats.playerMatchups) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .forEach(([matchup, count], idx) => { const percentage = ((count / aggregateStats.totalGames) * 100).toFixed(1); console.log(` ${idx + 1}. ${matchup}: ${count} games (${percentage}%)`); }); // Export results to JSON const outputPath = "./aggregate_stats.json"; fs.writeFileSync(outputPath, JSON.stringify(aggregateStats, null, 2)); console.log(`\nResults exported to: ${outputPath}`); ``` -------------------------------- ### Parse SLP file and extract game data with JavaScript Source: https://github.com/project-slippi/slippi-js/blob/master/README.md A basic Javascript script to demonstrate parsing a '.slp' file using the SlippiGame class. It shows how to retrieve game settings, metadata, computed stats, and frame data. Ensure a 'test.slp' file is present in the same directory. ```javascript const { SlippiGame } = require("@slippi/slippi-js"); const game = new SlippiGame("test.slp"); // Get game settings – stage, characters, etc const settings = game.getSettings(); console.log(settings); // Get metadata - start time, platform played on, etc const metadata = game.getMetadata(); console.log(metadata); // Get computed stats - openings / kill, conversions, etc const stats = game.getStats(); console.log(stats); // Get frames – animation state, inputs, etc // This is used to compute your own stats or get more frame-specific info (advanced) const frames = game.getFrames(); console.log(frames[0].players); // Print frame when timer starts counting down ``` -------------------------------- ### Track Damage Events with Slippi.js Source: https://context7.com/project-slippi/slippi-js/llms.txt This JavaScript code snippet tracks and reports damage taken by each player throughout a Slippi replay. It iterates through all frames, identifies instances where a player's percentage increases, and logs the damage amount, frame number, and cumulative total. This helps in analyzing offensive and defensive interactions. Requires the '@slippi/slippi-js' package. ```javascript // Track damage taken per frame console.log("\n\nDamage Tracking:"); settings.players.forEach(player => { const pid = player.playerIndex; let previousPercent = 0; let damageEvents = []; Object.values(frames).forEach(frame => { if (frame.players[pid] && frame.players[pid].post.percent !== null) { const currentPercent = frame.players[pid].post.percent; if (currentPercent > previousPercent) { damageEvents.push({ frame: frame.frame, damage: currentPercent - previousPercent, newTotal: currentPercent, }); } previousPercent = currentPercent; } }); console.log(`\nPlayer ${pid} damage events: ${damageEvents.length}`); if (damageEvents.length > 0) { console.log(` First hit: ${damageEvents[0].damage.toFixed(1)}% at frame ${damageEvents[0].frame}`); console.log(` Last hit: ${damageEvents[damageEvents.length - 1].damage.toFixed(1)}% at frame ${damageEvents[damageEvents.length - 1].frame}`); const totalDamage = damageEvents.reduce((sum, evt) => sum + evt.damage, 0); console.log(` Total damage: ${totalDamage.toFixed(1)}%`); } }); ``` -------------------------------- ### Track Stock Losses and Damage with Slippi-js Source: https://context7.com/project-slippi/slippi-js/llms.txt This code snippet demonstrates how to use the slippi-js library to extract and analyze stock loss data from a replay file. It calculates total stock losses, details for each loss (player, opponent, frames, damage), and aggregates per-player statistics such as stocks lost, total damage taken, and average damage per stock. It also includes logic to correlate stock losses with killing conversions for death attribution. ```javascript const { SlippiGame } = require("@slippi/slippi-js"); const game = new SlippiGame("replay.slp"); const stats = game.getStats(); if (stats && stats.stocks) { console.log(`Total stock losses: ${stats.stocks.length}\n`); // Analyze each stock loss stats.stocks.forEach((stock, idx) => { console.log(`Stock Loss #${idx + 1}: `); console.log(` Player: ${stock.playerIndex}`); console.log(` Opponent: ${stock.opponentIndex}`); console.log(` Start frame: ${stock.startFrame}`); console.log(` End frame: ${stock.endFrame}`); console.log(` Duration: ${stock.endFrame - stock.startFrame} frames`); console.log(` Start percent: ${stock.startPercent.toFixed(1)}%`); console.log(` End percent: ${stock.endPercent.toFixed(1)}%`); console.log(` Total damage taken: ${(stock.endPercent - stock.startPercent).toFixed(1)}%`); console.log(` Death animation: ${stock.deathAnimation || "N/A"}`); console.log(); }); // Per-player stock analysis const playerStocks = {}; stats.stocks.forEach(stock => { const pid = stock.playerIndex; if (!playerStocks[pid]) { playerStocks[pid] = { stocksLost: 0, totalDamageTaken: 0, averageDamagePerStock: 0, quickestDeath: Infinity, longestStock: 0, }; } const damage = stock.endPercent - stock.startPercent; const duration = stock.endFrame - stock.startFrame; playerStocks[pid].stocksLost++; playerStocks[pid].totalDamageTaken += damage; playerStocks[pid].quickestDeath = Math.min(playerStocks[pid].quickestDeath, duration); playerStocks[pid].longestStock = Math.max(playerStocks[pid].longestStock, duration); }); console.log("Per-Player Stock Statistics:"); Object.entries(playerStocks).forEach(([playerIndex, data]) => { data.averageDamagePerStock = data.totalDamageTaken / data.stocksLost; console.log(` Player ${playerIndex}: `); console.log(` Stocks lost: ${data.stocksLost}`); console.log(` Total damage taken: ${data.totalDamageTaken.toFixed(1)}%`); console.log(` Avg damage per stock: ${data.averageDamagePerStock.toFixed(1)}%`); console.log(` Quickest death: ${data.quickestDeath} frames`); console.log(` Longest stock: ${data.longestStock} frames`); }); // Correlate stocks with conversions for kill attribution if (stats.conversions) { console.log("\n\nKill Attribution (Stock losses with final conversion):"); stats.stocks.forEach((stock, idx) => { // Find conversion that ended at this stock loss const killingConversion = stats.conversions.find( conv => conv.didKill && conv.endFrame === stock.endFrame && conv.opponentIndex === stock.playerIndex ); if (killingConversion) { console.log(` Stock #${idx + 1} (Player ${stock.playerIndex} died): `); console.log(` Killed by: Player ${killingConversion.playerIndex}`); console.log(` Opening type: ${killingConversion.openingType}`); console.log(` Damage in final conversion: ${(killingConversion.endPercent - killingConversion.startPercent).toFixed(1)}%`); console.log(` Moves in killing sequence: ${killingConversion.moves.length}`); } }); } } ```