UNPKG

caravan-x

Version:

A terminal-based utility for managing Caravan multisig wallets in regtest mode. This tool simplifies development and testing with Caravan by providing an easy-to-use interface

1,273 lines 144 kB
"use strict"; /** * CaravanXBlockchain - A pixel art visualization of the blockchain as a trading caravan journey * * This class creates an interactive visualization where: * - Blocks are represented as caravans traveling along a chain * - Transactions are traders waiting to join caravans * - Mining is visualized as prospectors creating new caravans * - The environment changes with time (day/night cycle, weather) */ class CaravanXBlockchain { constructor() { // Path data this.pathPoints = []; // World elements this.sky = null; this.sun = null; this.moon = null; this.stars = null; this.clouds = null; this.trail = null; this.ground = null; this.mountains = null; // Weather effects this.rain = null; this.rainInterval = null; // Socket connection this.socket = null; // Sounds this.sounds = { mining: { play: () => { }, pause: () => { } }, transaction: { play: () => { }, pause: () => { } }, click: { play: () => { }, pause: () => { } }, caravan: { play: () => { }, pause: () => { } }, ambient: { play: () => { }, pause: () => { } }, rain: { play: () => { }, pause: () => { } }, }; // Animation tracking this.activeAnimations = new Set(); // Debug counters this.frameCount = 0; // World initialization flag this.worldInitialized = false; // Weather types for fallback this.weathers = ["clear", "cloudy", "rainy"]; // Initialize game state this.state = { blocks: [], mempool: [], caravans: [], traders: [], miners: [], outposts: [], focusedEntity: null, timeOfDay: 8, // 0-24 for day-night cycle gameTime: 0, weather: "clear", // clear, cloudy, rainy weatherTimer: 0, weatherDuration: 300, // 5 minutes in game time before weather changes isTutorialShown: false, soundEnabled: true, camera: { x: 0, y: 0, scale: 1, targetScale: 1, dragging: false, lastPosition: null, }, animationIds: [], // Store animation IDs for cleanup }; // Initialize PIXI Application this.app = new PIXI.Application({ width: window.innerWidth, height: window.innerHeight, backgroundColor: 0x0a0a1a, resolution: window.devicePixelRatio || 1, antialias: false, }); const gameContainer = document.getElementById("game-container"); if (gameContainer) { gameContainer.appendChild(this.app.view); } // Create all containers this.worldContainer = new PIXI.Container(); this.backgroundLayer = new PIXI.Container(); this.trailLayer = new PIXI.Container(); this.caravanLayer = new PIXI.Container(); this.traderLayer = new PIXI.Container(); this.minerLayer = new PIXI.Container(); this.outpostLayer = new PIXI.Container(); this.effectsLayer = new PIXI.Container(); this.uiLayer = new PIXI.Container(); // Add all layers to world container this.app.stage.addChild(this.worldContainer); this.worldContainer.addChild(this.backgroundLayer); this.worldContainer.addChild(this.trailLayer); this.worldContainer.addChild(this.caravanLayer); this.worldContainer.addChild(this.traderLayer); this.worldContainer.addChild(this.minerLayer); this.worldContainer.addChild(this.outpostLayer); this.worldContainer.addChild(this.effectsLayer); this.app.stage.addChild(this.uiLayer); // Initialize sounds this.initializeSounds(); // Socket connection this.initializeSocket(); // Set up event listeners this.setupEventListeners(); // Start game loop this.app.ticker.add(this.gameLoop.bind(this)); // Initialize game this.initialize(); } // Initialize sounds initializeSounds() { try { if (typeof Howl !== "undefined") { this.sounds = { mining: new Howl({ src: ["sounds/mining.mp3"], volume: 0.5, loop: false, }), transaction: new Howl({ src: ["sounds/transaction.mp3"], volume: 0.5, loop: false, }), click: new Howl({ src: ["sounds/click.mp3"], volume: 0.5, loop: false, }), caravan: new Howl({ src: ["sounds/caravan.mp3"], volume: 0.5, loop: false, }), ambient: new Howl({ src: ["sounds/blockchain_ambient.mp3"], volume: 0.3, loop: true, }), rain: new Howl({ src: ["sounds/rain.mp3"], volume: 0.2, loop: true, }), }; } else { console.warn("Howler.js not available, sound disabled"); this.createDummySounds(); } } catch (error) { console.error("Error initializing sounds:", error); this.createDummySounds(); } } // Create dummy sound objects createDummySounds() { this.sounds = { mining: { play: () => { }, pause: () => { }, fade: () => { } }, transaction: { play: () => { }, pause: () => { }, fade: () => { } }, click: { play: () => { }, pause: () => { }, fade: () => { } }, caravan: { play: () => { }, pause: () => { }, fade: () => { } }, ambient: { play: () => { }, pause: () => { } }, rain: { play: () => { }, pause: () => { } }, }; } // Initialize socket connection initializeSocket() { try { if (typeof io !== "undefined") { this.socket = io(); console.log("Socket.io initialized"); } } catch (error) { console.warn("Could not connect to socket.io:", error); this.socket = null; } } // Initialize the game async initialize() { try { console.log("Starting initialization..."); // Load assets await this.loadAssets(); console.log("Assets loaded!"); // Create the world this.createWorld(); console.log("World created!"); // Fetch initial blockchain data await this.fetchBlockchainData(); console.log("Blockchain data fetched!"); // Set default camera position to show the blockchain path this.state.camera = { x: -100, // Start slightly to the right so we can see the beginning of the path y: 0, scale: 0.8, targetScale: 0.8, dragging: false, lastPosition: null, }; // Hide loading screen const loadingScreen = document.getElementById("loading-screen"); if (loadingScreen) { loadingScreen.style.display = "none"; } // Add intro animation this.playIntroAnimation(); // Start ambient sound if (this.state.soundEnabled && this.sounds) { this.sounds.ambient.play(); } // Show tutorial if first visit const hasTutorialBeenShown = localStorage.getItem("caravanXTutorialShown"); if (!hasTutorialBeenShown) { this.showTutorial(); localStorage.setItem("caravanXTutorialShown", "true"); } console.log("Game initialized successfully"); } catch (error) { console.error("Error initializing game:", error); const loadingText = document.getElementById("loading-text"); if (loadingText) { loadingText.textContent = "Error loading caravan: " + error.message; } } } // Load game assets async loadAssets() { return new Promise((resolve) => { // Simulate loading process let progress = 0; const loadingInterval = setInterval(() => { progress += 5; const loadingBar = document.getElementById("loading-bar"); if (loadingBar) { loadingBar.style.width = `${progress}%`; } const loadingText = document.getElementById("loading-text"); if (loadingText) { loadingText.textContent = `Loading the caravan... ${progress}%`; } if (progress >= 100) { clearInterval(loadingInterval); resolve(); } }, 100); }); } // Create the world environment createWorld() { try { // Create sky background with gradient this.createSky(); console.log("Sky created"); // Create distant mountains this.createMountains(); console.log("Mountains created"); // Create the blockchain trail (the path that caravans follow) this.createBlockchainTrail(); console.log("Trail created"); // Create ground this.createGround(); console.log("Ground created"); // Add environmental elements this.addEnvironmentalElements(); console.log("Environmental elements added"); // Mark world as initialized this.worldInitialized = true; console.log("World marked as initialized!"); } catch (error) { console.error("Error creating world:", error); throw error; } } // Create the sky with gradient and day/night cycle capability createSky() { // Create sky gradient const sky = new PIXI.Graphics(); sky.beginFill(0x4477aa); // Day sky color sky.drawRect(-window.innerWidth, -window.innerHeight, window.innerWidth * 3, window.innerHeight * 2); sky.endFill(); this.backgroundLayer.addChild(sky); this.sky = sky; // Add sun const sun = new PIXI.Graphics(); sun.beginFill(0xffdd88); sun.drawCircle(0, 0, 40); sun.endFill(); sun.x = window.innerWidth * 0.7; sun.y = 100; this.backgroundLayer.addChild(sun); this.sun = sun; // Add moon const moon = new PIXI.Graphics(); moon.beginFill(0xddddff); moon.drawCircle(0, 0, 30); moon.endFill(); moon.x = window.innerWidth * 0.3; moon.y = 100; moon.alpha = 0; this.backgroundLayer.addChild(moon); this.moon = moon; // Add stars container this.stars = new PIXI.Container(); this.backgroundLayer.addChild(this.stars); // Generate stars for (let i = 0; i < 100; i++) { const star = new PIXI.Graphics(); star.beginFill(0xffffff); const size = Math.random() * 2 + 1; star.drawRect(0, 0, size, size); star.endFill(); star.x = Math.random() * window.innerWidth * 3 - window.innerWidth; star.y = (Math.random() * window.innerHeight) / 2; star.alpha = 0; // Stars start invisible during day this.stars.addChild(star); } // Add clouds container this.clouds = new PIXI.Container(); this.backgroundLayer.addChild(this.clouds); // Generate clouds for (let i = 0; i < 10; i++) { this.createCloud(Math.random() * window.innerWidth * 3 - window.innerWidth, Math.random() * 200, 0.1 + Math.random() * 0.2); } } // Create a single cloud createCloud(x, y, speed) { if (!this.clouds) return null; const cloud = new PIXI.Graphics(); cloud.beginFill(0xffffff, 0.8); // Create fluffy cloud shape const cloudWidth = 30 + Math.random() * 70; const cloudHeight = 20 + Math.random() * 30; // Draw multiple overlapping circles for a cloud shape for (let i = 0; i < 5; i++) { const circleSize = cloudHeight / 2 + (Math.random() * cloudHeight) / 2; const offsetX = (i / 5) * cloudWidth - cloudWidth / 2; const offsetY = Math.random() * 10 - 5; cloud.drawCircle(offsetX, offsetY, circleSize); } cloud.endFill(); cloud.x = x; cloud.y = y; // Add custom properties cloud.speed = speed; cloud.cloudWidth = cloudWidth; this.clouds.addChild(cloud); return cloud; } // Create distant mountains for background createMountains() { const mountains = new PIXI.Graphics(); // Draw several mountain ranges with different colors for (let range = 0; range < 3; range++) { // Use progressively lighter colors for distant ranges const color = range === 0 ? 0x303656 : range === 1 ? 0x424c78 : 0x5a6291; const baseY = 300 - range * 40; // Each range is higher on screen (more distant) const peakHeight = 100 - range * 20; // More distant mountains are shorter mountains.beginFill(color); // Start at the left edge below the terrain line mountains.moveTo(-window.innerWidth, baseY + 50); // Create a series of peaks const numPeaks = 20; const peakWidth = (window.innerWidth * 3) / numPeaks; for (let i = 0; i <= numPeaks; i++) { const x = -window.innerWidth + i * peakWidth; // Randomize peak heights for a natural look const heightVariation = Math.random() * peakHeight; const y = baseY - heightVariation; // For smoother mountains, use quadraticCurveTo instead of lineTo if (i === 0) { mountains.lineTo(x, y); } else { const cpX = x - peakWidth / 2; const cpY = baseY - (Math.random() * peakHeight) / 2; mountains.quadraticCurveTo(cpX, cpY, x, y); } } // Close the shape by extending to the right edge and down mountains.lineTo(window.innerWidth * 2, baseY + 50); mountains.lineTo(-window.innerWidth, baseY + 50); mountains.endFill(); } this.backgroundLayer.addChild(mountains); this.mountains = mountains; } // Create the blockchain trail (path for caravans) createBlockchainTrail() { const trail = new PIXI.Graphics(); // Main dirt path trail.beginFill(0x8b4513, 0.7); // Create a winding path using bezier curves this.pathPoints = []; let x = -window.innerWidth; const baseY = 300; // Generate winding path points for (let i = 0; i < 50; i++) { const segment = { x: x, y: baseY + Math.sin(i * 0.2) * 30, }; this.pathPoints.push(segment); x += 200; } // Draw the path as a series of quadratic curves trail.moveTo(this.pathPoints[0].x, this.pathPoints[0].y); for (let i = 1; i < this.pathPoints.length; i++) { const prev = this.pathPoints[i - 1]; const current = this.pathPoints[i]; // Control point halfway between points const cpX = (prev.x + current.x) / 2; const cpY = prev.y + (Math.random() * 20 - 10); // Slight random variation trail.quadraticCurveTo(cpX, cpY, current.x, current.y); } // Make the path wider by drawing another line slightly offset const pathWidth = 30; for (let i = this.pathPoints.length - 1; i >= 0; i--) { const point = this.pathPoints[i]; trail.lineTo(point.x, point.y + pathWidth); } trail.endFill(); // Add some small stones along the path edges trail.beginFill(0x999999, 0.5); for (let i = 0; i < 200; i++) { const pathIndex = Math.floor(Math.random() * this.pathPoints.length); const point = this.pathPoints[pathIndex]; const offsetY = (Math.random() > 0.5 ? 1 : -1) * (pathWidth / 2 + Math.random() * 5); const stoneSize = 2 + Math.random() * 3; trail.drawCircle(point.x + Math.random() * 100 - 50, point.y + offsetY, stoneSize); } trail.endFill(); this.trailLayer.addChild(trail); this.trail = trail; } // Create ground with grass and texture createGround() { const ground = new PIXI.Graphics(); // Main ground ground.beginFill(0x225522); ground.drawRect(-window.innerWidth, 300, window.innerWidth * 3, window.innerHeight); ground.endFill(); // Add texture to the ground with small grass tufts ground.beginFill(0x338833); for (let x = -window.innerWidth; x < window.innerWidth * 2; x += 20) { for (let y = 320; y < window.innerHeight + 300; y += 20) { if (Math.random() > 0.8) { ground.drawRect(x, y, 3 + Math.random() * 3, 3 + Math.random() * 3); } } } ground.endFill(); this.backgroundLayer.addChild(ground); this.ground = ground; } // Add environmental elements to the scene addEnvironmentalElements() { // Add outposts (representing blockchain nodes) this.createOutposts(); // Add miners' camps this.createMinerCamps(); // Add decorative elements like trees, rocks, etc. this.addDecorations(); } // Create outposts (blockchain nodes) createOutposts() { if (!this.pathPoints || this.pathPoints.length === 0) { console.warn("Path points not initialized, cannot create outposts"); return; } // Create 3 outposts along the blockchain path for (let i = 0; i < 3; i++) { const pathIndex = Math.floor(this.pathPoints.length / 3) * i + 5; if (pathIndex >= this.pathPoints.length) continue; const point = this.pathPoints[pathIndex]; // Create outpost building const outpost = new PIXI.Graphics(); // Main building outpost.beginFill(0x8b4513); outpost.drawRect(-15, -30, 30, 30); outpost.endFill(); // Roof outpost.beginFill(0x555555); outpost.moveTo(-18, -30); outpost.lineTo(0, -45); outpost.lineTo(18, -30); outpost.lineTo(-18, -30); outpost.endFill(); // Window outpost.beginFill(0xffdd88); outpost.drawRect(-5, -25, 10, 10); outpost.endFill(); // Antenna (for node connectivity) outpost.lineStyle(2, 0xaaaaaa); outpost.moveTo(0, -45); outpost.lineTo(0, -60); outpost.lineStyle(0); // Small dish on top outpost.beginFill(0xcccccc); outpost.drawCircle(0, -60, 5); outpost.endFill(); // Position outpost outpost.x = point.x; outpost.y = point.y - 10; // Slightly above the path // Add a sign const sign = new PIXI.Graphics(); sign.beginFill(0x8b4513); sign.drawRect(-10, -25, 20, 15); sign.endFill(); sign.beginFill(0xffdd88); sign.drawRect(-8, -23, 16, 11); sign.endFill(); sign.x = point.x + 25; sign.y = point.y; // Store outpost data outpost.outpostData = { id: `outpost_${i}`, name: `Node ${i + 1}`, type: i === 0 ? "Full Node" : i === 1 ? "Mining Node" : "Light Client", connections: Math.floor(Math.random() * 8) + 3, }; // Make interactive outpost.interactive = true; outpost.buttonMode = true; outpost.on("pointerover", (event) => { this.showTooltip(`${outpost.outpostData.name} (${outpost.outpostData.type})`, event.data.global.x, event.data.global.y); }); outpost.on("pointerout", () => { this.hideTooltip(); }); outpost.on("pointerdown", () => { this.showOutpostDetails(outpost.outpostData); if (this.state.soundEnabled) this.sounds.click.play(); }); this.outpostLayer.addChild(outpost); this.outpostLayer.addChild(sign); this.state.outposts.push(outpost); // Add connectivity animation this.addConnectivityEffect(outpost); } } // Add pulsing connectivity animation to outposts addConnectivityEffect(outpost) { if (!this.effectsLayer) return; // Create effect container to keep animations properly tracked const effectContainer = new PIXI.Container(); effectContainer.position.set(outpost.x, outpost.y - 60); this.effectsLayer.addChild(effectContainer); const connectivityPulse = new PIXI.Graphics(); connectivityPulse.beginFill(0x44aaff, 0.5); connectivityPulse.drawCircle(0, 0, 5); connectivityPulse.endFill(); effectContainer.addChild(connectivityPulse); // Store a reference to the active animations for possible cleanup this.activeAnimations.add(effectContainer); // Recursive animation function const animate = () => { // Skip if container was removed if (!effectContainer.parent) return; // Reset pulse connectivityPulse.scale.set(1); connectivityPulse.alpha = 1; // Animation timeline with cleanup const tl = gsap.timeline({ onComplete: () => { // Schedule next pulse after a delay if (effectContainer.parent) { gsap.delayedCall(1 + Math.random(), animate); } else { // Container was removed, stop animation gsap.killTweensOf(connectivityPulse); this.activeAnimations.delete(effectContainer); } }, }); tl.to(connectivityPulse.scale, { x: 3, y: 3, duration: 1.5, ease: "sine.out", }, 0); tl.to(connectivityPulse, { alpha: 0, duration: 1.5, ease: "sine.out", }, 0); }; // Start animation animate(); } // Create miner camps createMinerCamps() { if (!this.pathPoints || this.pathPoints.length === 0) { console.warn("Path points not initialized, cannot create miner camps"); return; } // Create 4 mining camps around the trail for (let i = 0; i < 4; i++) { const pathIndex = Math.floor(this.pathPoints.length / 5) * (i + 1); if (pathIndex >= this.pathPoints.length) continue; const point = this.pathPoints[pathIndex]; // Determine position (alternate sides of the trail) const side = i % 2 === 0 ? -1 : 1; const x = point.x + side * (40 + Math.random() * 30); const y = point.y + (Math.random() * 30 - 15); // Create miner camp const minerCamp = new PIXI.Graphics(); // Main tent minerCamp.beginFill(0x8b4513); minerCamp.drawRect(-10, -5, 20, 15); minerCamp.endFill(); // Tent top minerCamp.beginFill(0xaa5533); minerCamp.moveTo(-15, -5); minerCamp.lineTo(0, -20); minerCamp.lineTo(15, -5); minerCamp.lineTo(-15, -5); minerCamp.endFill(); // Mining equipment minerCamp.beginFill(0x555555); minerCamp.drawRect(-15, 5, 10, 5); minerCamp.endFill(); // Pickaxe minerCamp.lineStyle(2, 0x888888); minerCamp.moveTo(10, 0); minerCamp.lineTo(20, -15); minerCamp.lineStyle(0); minerCamp.beginFill(0x444444); minerCamp.drawRect(17, -19, 6, 4); minerCamp.endFill(); // Position miner camp minerCamp.x = x; minerCamp.y = y; // Campfire const campfire = new PIXI.Graphics(); campfire.beginFill(0x555555); campfire.drawCircle(0, 0, 5); campfire.endFill(); // Logs campfire.beginFill(0x8b4513); campfire.drawRect(-6, -1, 12, 2); campfire.drawRect(-1, -6, 2, 12); campfire.endFill(); campfire.x = x - 20; campfire.y = y + 10; // Miner character const miner = this.createMinerCharacter(x + 15, y + 10); if (!miner) continue; // Store miner data miner.minerData = { id: `miner_${i}`, name: `Miner ${i + 1}`, hashPower: Math.floor(Math.random() * 100) + 10, blocksFound: Math.floor(Math.random() * 5), active: Math.random() > 0.3, // 70% chance of being active }; // Make interactive miner.interactive = true; miner.buttonMode = true; miner.on("pointerover", (event) => { this.showTooltip(`${miner.minerData.name} (Hashpower: ${miner.minerData.hashPower})`, event.data.global.x, event.data.global.y); }); miner.on("pointerout", () => { this.hideTooltip(); }); miner.on("pointerdown", () => { this.showMinerDetails(miner.minerData); if (this.state.soundEnabled) this.sounds.click.play(); }); if (this.minerLayer) { this.minerLayer.addChild(minerCamp); this.minerLayer.addChild(campfire); this.state.miners.push(miner); } // Add fire animation this.addFireAnimation(campfire); // Add mining animation if active if (miner.minerData.active) { this.addMiningAnimation(miner); } } } // Create miner character createMinerCharacter(x, y) { if (!this.minerLayer) return null; const miner = new PIXI.Graphics(); // Body miner.beginFill(0x8b4513); miner.drawRect(-5, -15, 10, 15); miner.endFill(); // Head miner.beginFill(0xffcc99); miner.drawCircle(0, -20, 5); miner.endFill(); // Mining hat miner.beginFill(0x444444); miner.drawRect(-6, -25, 12, 3); miner.drawCircle(0, -25, 5); miner.endFill(); // Position miner miner.x = x; miner.y = y; if (this.minerLayer) { this.minerLayer.addChild(miner); } return miner; } // Add fire animation to campfire addFireAnimation(campfire) { if (!this.effectsLayer) return; // Create a container for the fire effect (for easier cleanup) const fireContainer = new PIXI.Container(); fireContainer.position.set(campfire.x, campfire.y); this.effectsLayer.addChild(fireContainer); // Create the fire graphics inside the container const fireGraphics = new PIXI.Graphics(); fireContainer.addChild(fireGraphics); // Track for cleanup this.activeAnimations.add(fireContainer); // Animation function using requestAnimationFrame const animate = () => { // Skip if container was removed if (!fireContainer.parent) return; // Redraw fire fireGraphics.clear(); // Orange outer flame fireGraphics.beginFill(0xff6600, 0.8); fireGraphics.moveTo(-2, 0); fireGraphics.quadraticCurveTo(-3 + Math.random() * 6, -5 - Math.random() * 5, 2, -8 - Math.random() * 4); fireGraphics.quadraticCurveTo(3 + Math.random() * 2, -5 - Math.random() * 5, 2, 0); fireGraphics.lineTo(-2, 0); fireGraphics.endFill(); // Yellow inner flame fireGraphics.beginFill(0xffcc00, 0.9); fireGraphics.moveTo(-1, 0); fireGraphics.quadraticCurveTo(-1 + Math.random() * 2, -3 - Math.random() * 3, 1, -5 - Math.random() * 2); fireGraphics.quadraticCurveTo(1 + Math.random() * 1, -3 - Math.random() * 3, 1, 0); fireGraphics.lineTo(-1, 0); fireGraphics.endFill(); // Schedule next frame if container still exists if (fireContainer.parent) { requestAnimationFrame(animate); } else { // Container removed, clean up this.activeAnimations.delete(fireContainer); } }; // Start animation animate(); } // Add mining animation to miner addMiningAnimation(miner) { if (!this.minerLayer) return; // Create a container for better cleanup and control const miningContainer = new PIXI.Container(); miningContainer.position.set(miner.x, miner.y - 5); this.minerLayer.addChild(miningContainer); // Create pickaxe const pickaxe = new PIXI.Graphics(); pickaxe.lineStyle(2, 0x888888); pickaxe.moveTo(0, 0); pickaxe.lineTo(15, -10); pickaxe.lineStyle(0); pickaxe.beginFill(0x444444); pickaxe.drawRect(12, -14, 6, 4); pickaxe.endFill(); pickaxe.pivot.set(0, 0); miningContainer.addChild(pickaxe); // Track for cleanup this.activeAnimations.add(miningContainer); // Mining animation with safety and cleanup const miningAnimation = gsap.to(pickaxe, { rotation: -0.5, duration: 0.5, repeat: -1, yoyo: true, ease: "power1.inOut", onComplete: () => { if (this.minerLayer && miningContainer.parent) { this.minerLayer.removeChild(miningContainer); this.activeAnimations.delete(miningContainer); } }, }); // Occasionally play mining sound const playMiningSound = () => { if (!miningContainer.parent) return; if (this.state.soundEnabled && Math.random() > 0.7) { this.sounds.mining.play(); } // Schedule next sound only if container still exists if (miningContainer.parent) { gsap.delayedCall(5 + Math.random() * 10, playMiningSound); } }; // Start sound timing gsap.delayedCall(Math.random() * 5, playMiningSound); } // Add decorative elements to the world addDecorations() { try { // Add trees, rocks, cacti, etc. this.addTrees(); this.addRocks(); // Add occasional landmarks this.addBlockchainMonument(); this.addTradingOasis(); } catch (error) { console.error("Error adding decorations:", error); } } // Add trees to the scene addTrees() { if (!this.backgroundLayer || !this.pathPoints) return; // Create different types of trees scattered around for (let i = 0; i < 40; i++) { // Position away from the path const x = Math.random() * window.innerWidth * 2 - window.innerWidth; const y = 300 + Math.random() * 200; // Skip if too close to the path let tooClose = false; for (const point of this.pathPoints) { const distance = Math.sqrt(Math.pow(point.x - x, 2) + Math.pow(point.y - y, 2)); if (distance < 60) { tooClose = true; break; } } if (tooClose) continue; // Randomly choose tree type const treeType = Math.floor(Math.random() * 3); const tree = new PIXI.Graphics(); switch (treeType) { case 0: // Pine tree tree.beginFill(0x8b4513); tree.drawRect(-3, 0, 6, 15); tree.endFill(); tree.beginFill(0x006400); for (let j = 0; j < 3; j++) { const size = 15 - j * 4; tree.drawPolygon([ -size, -j * 10 - 5, 0, -j * 10 - 15, size, -j * 10 - 5, ]); } tree.endFill(); break; case 1: // Oak tree tree.beginFill(0x8b4513); tree.drawRect(-3, 0, 6, 10); tree.endFill(); tree.beginFill(0x228b22); tree.drawCircle(0, -10, 10); tree.endFill(); break; case 2: // Dead tree tree.beginFill(0x8b4513); tree.drawRect(-2, 0, 4, 15); tree.endFill(); tree.lineStyle(2, 0x8b4513); tree.moveTo(0, -15); tree.lineTo(8, -25); tree.moveTo(0, -10); tree.lineTo(-6, -20); tree.moveTo(0, -5); tree.lineTo(5, -15); tree.lineStyle(0); break; } tree.x = x; tree.y = y; this.backgroundLayer.addChild(tree); } } // Add rocks to the scene addRocks() { if (!this.backgroundLayer || !this.pathPoints) return; // Add scattered rocks for (let i = 0; i < 30; i++) { const x = Math.random() * window.innerWidth * 2 - window.innerWidth; const y = 300 + Math.random() * 200; // Skip if too close to the path let tooClose = false; for (const point of this.pathPoints) { const distance = Math.sqrt(Math.pow(point.x - x, 2) + Math.pow(point.y - y, 2)); if (distance < 40) { tooClose = true; break; } } if (tooClose) continue; const rock = new PIXI.Graphics(); // Determine rock size const size = 3 + Math.random() * 8; // Draw rock with slight variations rock.beginFill(0x888888); if (Math.random() > 0.5) { // Rounded rock rock.drawCircle(0, 0, size); } else { // Angular rock const points = []; const numPoints = 5 + Math.floor(Math.random() * 3); for (let j = 0; j < numPoints; j++) { const angle = (j / numPoints) * Math.PI * 2; const distance = size * (0.8 + Math.random() * 0.4); points.push(Math.cos(angle) * distance, Math.sin(angle) * distance); } rock.drawPolygon(points); } rock.endFill(); // Add some texture/highlights rock.beginFill(0xaaaaaa, 0.5); rock.drawCircle(size * 0.3, -size * 0.3, size * 0.3); rock.endFill(); rock.x = x; rock.y = y; this.backgroundLayer.addChild(rock); } } // Add a blockchain monument (decorative landmark) addBlockchainMonument() { if (!this.backgroundLayer || !this.effectsLayer || !this.pathPoints) return; // Position at a specific point on the path const pathIndex = Math.floor(this.pathPoints.length * 0.7); if (pathIndex >= this.pathPoints.length) return; const point = this.pathPoints[pathIndex]; const x = point.x + 80; const y = point.y - 20; const monument = new PIXI.Graphics(); // Stone base monument.beginFill(0x888888); monument.drawRect(-20, 0, 40, 10); monument.endFill(); // Monument pillar monument.beginFill(0x777777); monument.drawRect(-10, -40, 20, 40); monument.endFill(); // Decorative elements monument.beginFill(0xffcc00); // Bitcoin-inspired symbol monument.drawRect(-5, -35, 10, 2); monument.drawRect(-5, -30, 10, 2); monument.drawRect(-5, -25, 10, 2); monument.drawRect(-5, -20, 10, 2); // Angle marks monument.drawRect(-8, -35, 3, 2); monument.drawRect(5, -25, 3, 2); monument.endFill(); monument.x = x; monument.y = y; // Create a container for the glow effect const glowContainer = new PIXI.Container(); glowContainer.position.set(x, y - 20); this.effectsLayer.addChild(glowContainer); // Add ambient glow effect const glow = new PIXI.Graphics(); glow.beginFill(0xffcc00, 0.2); glow.drawCircle(0, 0, 30); glow.endFill(); glowContainer.addChild(glow); // Animate glow with safe cleanup gsap.to(glow, { alpha: 0.4, duration: 2, repeat: -1, yoyo: true, ease: "sine.inOut", onComplete: () => { if (this.effectsLayer && glowContainer.parent) { this.effectsLayer.removeChild(glowContainer); this.activeAnimations.delete(glowContainer); } }, }); // Track for cleanup this.activeAnimations.add(glowContainer); this.backgroundLayer.addChild(monument); // Make interactive monument.interactive = true; monument.buttonMode = true; monument.on("pointerover", (event) => { this.showTooltip("Ancient Blockchain Monument", event.data.global.x, event.data.global.y); }); monument.on("pointerout", () => { this.hideTooltip(); }); monument.on("pointerdown", () => { this.showNotification("The monument contains inscriptions from the genesis block."); if (this.state.soundEnabled) this.sounds.click.play(); }); } // Add a trading oasis (a gathering place for traders) addTradingOasis() { if (!this.backgroundLayer || !this.pathPoints) return; // Position at a specific point on the path const pathIndex = Math.floor(this.pathPoints.length * 0.3); if (pathIndex >= this.pathPoints.length) return; const point = this.pathPoints[pathIndex]; const x = point.x - 60; const y = point.y + 20; const oasis = new PIXI.Graphics(); // Water pool oasis.beginFill(0x4477aa); oasis.drawCircle(0, 0, 15); oasis.endFill(); // Sand around the water oasis.beginFill(0xddcc88); oasis.drawCircle(0, 0, 20); oasis.endFill(); // Add palm trees for (let i = 0; i < 3; i++) { const angle = (i / 3) * Math.PI * 2; const treeX = Math.cos(angle) * 18; const treeY = Math.sin(angle) * 18; // Tree trunk oasis.beginFill(0x8b4513); oasis.drawRect(treeX - 2, treeY - 15, 4, 15); oasis.endFill(); // Slight curve to trunk oasis.beginFill(0x8b4513); oasis.drawRect(treeX - 1, treeY - 22, 3, 7); oasis.endFill(); // Palm leaves oasis.beginFill(0x00aa00); for (let j = 0; j < 4; j++) { const leafAngle = (j / 4) * Math.PI * 2; oasis.moveTo(treeX, treeY - 22); // Create curved palm leaf const controlX = treeX + Math.cos(leafAngle) * 10; const controlY = treeY - 22 + Math.sin(leafAngle) * 5; const endX = treeX + Math.cos(leafAngle) * 15; const endY = treeY - 22 + Math.sin(leafAngle) * 10; oasis.quadraticCurveTo(controlX, controlY, endX, endY); oasis.quadraticCurveTo(controlX + Math.cos(leafAngle + 0.2) * 3, controlY + Math.sin(leafAngle + 0.2) * 3, treeX, treeY - 22); } oasis.endFill(); } // Add a small trading stand oasis.beginFill(0x8b4513); oasis.drawRect(10, -5, 15, 10); oasis.endFill(); // Stand roof oasis.beginFill(0xaa5533); oasis.drawRect(8, -10, 19, 5); oasis.endFill(); oasis.x = x; oasis.y = y; this.backgroundLayer.addChild(oasis); // Add a trader character this.createTrader(x + 20, y + 5, { id: "oasis_trader", name: "Oasis Merchant", speciality: "Rare Tokens", inventory: Math.floor(Math.random() * 10) + 5, }); // Add ambient effects this.addWaterAnimation(x, y); } // Add water ripple animation for the oasis addWaterAnimation(x, y) { if (!this.effectsLayer) return; // Create a container for the water effect const waterContainer = new PIXI.Container(); waterContainer.position.set(x, y); this.effectsLayer.addChild(waterContainer); // Main water circle with animation const waterCircle = new PIXI.Graphics(); waterContainer.addChild(waterCircle); // Track for cleanup this.activeAnimations.add(waterContainer); // Animation function const animate = () => { // Skip if container was removed if (!waterContainer.parent) return; // Redraw water waterCircle.clear(); waterCircle.beginFill(0x4477aa, 0.5); waterCircle.drawCircle(0, 0, 5 + Math.sin(Date.now() / 500) * 2); waterCircle.endFill(); // Randomly create ripples if (Math.random() > 0.98) { this.createWaterRipple(waterContainer); } // Continue animation only if container still exists if (waterContainer.parent) { requestAnimationFrame(animate); } else { // Cleanup this.activeAnimations.delete(waterContainer); } }; // Start animation animate(); } // Create water ripple effect createWaterRipple(parentContainer) { // Create a ripple graphic const ripple = new PIXI.Graphics(); ripple.position.set(Math.random() * 10 - 5, Math.random() * 10 - 5); ripple.lineStyle(1, 0x4477aa, 0.8); ripple.drawCircle(0, 0, 2); ripple.lineStyle(0); parentContainer.addChild(ripple); // Animate and remove with GSAP const tl = gsap.timeline({ onComplete: () => { if (ripple.parent) { ripple.parent.removeChild(ripple); } }, }); tl.to(ripple.scale, { x: 4, y: 4, duration: 1.5, ease: "sine.out" }, 0); tl.to(ripple, { alpha: 0, duration: 1.5, ease: "sine.out" }, 0); } // Create a trader character createTrader(x, y, data) { if (!this.traderLayer) return null; const trader = new PIXI.Graphics(); // Body trader.beginFill(0xaa5500); trader.drawRect(-5, -15, 10, 15); trader.endFill(); // Head trader.beginFill(0xffcc99); trader.drawCircle(0, -20, 5); trader.endFill(); // Hat trader.beginFill(0xaa0000); trader.drawCircle(0, -23, 6); trader.endFill(); // Position trader trader.x = x; trader.y = y; // Store trader data trader.traderData = data; // Make interactive trader.interactive = true; trader.buttonMode = true; trader.on("pointerover", (event) => { this.showTooltip(`${trader.traderData.name} (${trader.traderData.speciality})`, event.data.global.x, event.data.global.y); }); trader.on("pointerout", () => { this.hideTooltip(); }); trader.on("pointerdown", () => { this.showTraderDetails(trader.traderData); if (this.state.soundEnabled) this.sounds.click.play(); }); this.traderLayer.addChild(trader); return trader; } // Fetch blockchain data from API async fetchBlockchainData() { try { console.log("Fetching blockchain data..."); // Add timeout for fetch const fetchPromise = fetch("/api/blockchain"); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Fetch timeout")), 10000)); const response = await Promise.race([fetchPromise, timeoutPromise]); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); console.log("Blockchain data received:", data); // Validate the data structure if (!data || typeof data !== "object") { throw new Error("Invalid data format received"); } // Use default values if properties are missing const safeData = { stats: data.stats || { blockCount: 0, totalTxCount: 0, mempoolSize: 0 }, blocks: Array.isArray(data.blocks) ? data.blocks : [], mempool: data.mempool || { txids: [] }, }; // Update stats display this.updateStats(safeData.stats); // Only process blocks if the world is fully initialized if (this.worldInitialized) { this.processBlocks(safeData.blocks); } else { console.log("Deferring block processing until world is initialized"); } // Process mempool this.processMempool(safeData.mempool); return safeData; } catch (error) { console.error("Error fetching blockchain data:", error); // Create fallback data const fallbackData = { stats: { blockCount: 3, totalTxCount: 10, mempoolSize: 5 }, blocks: [ { height: 2, hash: "000000000000000000000000000000000000000000000000000000000000002", time: Date.now() / 1000, txCount: 3, size: 1500, }, { height: 1, hash: "000000000000000000000000000000000000000000000000000000000000001", time: Date.now() / 1000 - 600, txCount: 2, size: 1200, }, { height: 0, hash: "000000000000000000000000000000000000000000000000000000000000000", time: Date.now() / 1000 - 1200, txCount: 1, size: 1000, }, ], mempool: { txids: [ "tx1111111111111111111111111111111111111111111111111111111111111111", "tx2222222222222222222222222222222222222222222222222222222222222222", "tx3333333333333333333333333333333333333333333333333333333333333333", "tx4444444444444444444444444444444444444444444444444444444444444444", "tx5555555555555555555555555555555555555555555555555555555555555555", ], }, }; // Show notification this.show