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
784 lines (665 loc) • 22.1 kB
JavaScript
/**
* VoxelEngine - 3D voxel-based rendering engine for blockchain visualization
*/
class VoxelEngine {
constructor(container) {
// Initialize Three.js scene
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x87ceeb); // Sky blue
// Initialize camera
this.camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000,
);
this.camera.position.set(0, 5, 10);
// Initialize renderer
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(this.renderer.domElement);
// Create a camera container for movement
this.cameraContainer = new THREE.Object3D();
this.cameraContainer.position.set(0, 5, 10);
this.scene.add(this.cameraContainer);
this.cameraContainer.add(this.camera);
// Initialize pointer lock controls
// Check if PointerLockControls is available, if not use a fallback
if (typeof THREE.PointerLockControls === "function") {
try {
this.controls = new THREE.PointerLockControls(
this.camera,
document.body,
);
this.controls.pointerSpeed = 0.2;
} catch (error) {
console.error("Could not initialize PointerLockControls:", error);
this.useFallbackControls();
}
} else {
console.warn(
"THREE.PointerLockControls not found, using fallback controls",
);
this.useFallbackControls();
}
// Player physics and movement
this.playerVelocity = new THREE.Vector3();
this.playerDirection = new THREE.Vector3();
this.gravity = 0.2;
this.playerSpeed = 0.1;
this.jumpForce = 0.5;
this.playerHeight = 1.8;
this.playerRadius = 0.4;
this.playerOnGround = false;
// Input state
this.keyStates = {
moveForward: false,
moveBackward: false,
moveLeft: false,
moveRight: false,
jump: false,
};
// World data
this.worldData = {};
this.chunks = {};
this.buildings = [];
this.characters = [];
// Current selected tool
this.currentTool = "pickaxe";
// Setup lights and environment
this.setupLighting();
// Initialize the clock for animations
this.clock = new THREE.Clock();
// Raycaster for block interactions
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
// Start animation loop
this.animate = this.animate.bind(this);
requestAnimationFrame(this.animate);
// Handle window resize
window.addEventListener("resize", this.onWindowResize.bind(this));
// Handle mouse click
document.addEventListener("click", this.onMouseClick.bind(this));
// Add click event to lock controls
document.addEventListener("click", () => {
if (this.controls && this.controls.isLocked === false) {
this.controls.lock();
}
});
// Generate noise for terrain
this.noise = {
simplex2: function (x, y) {
// Simple placeholder noise function (would use a proper library in production)
return Math.sin(x * 0.1) * Math.cos(y * 0.1) * 0.5;
},
};
}
animate() {
const delta = this.clock.getDelta();
// Update the player (movement, physics)
this.updatePlayerPosition(delta);
// Update any mining progress & particles
this.updateMiningProgress(delta);
if (this.miningParticles) {
// you may want to animate your particles here
this.miningParticles.children.forEach((p) => {
p.position.add(p.userData.velocity.clone().multiplyScalar(delta * 60));
p.userData.velocity.y -= this.gravity * delta * 60;
});
}
// Render the scene
this.renderer.render(this.scene, this.camera);
// Schedule next frame
requestAnimationFrame(this.animate);
}
// Create a fallback control mechanism if PointerLockControls doesn't work
useFallbackControls() {
this.controls = {
isLocked: false,
lock: function () {
this.isLocked = true;
document.body.requestPointerLock =
document.body.requestPointerLock ||
document.body.mozRequestPointerLock ||
document.body.webkitRequestPointerLock;
if (document.body.requestPointerLock) {
document.body.requestPointerLock();
}
},
unlock: function () {
this.isLocked = false;
document.exitPointerLock =
document.exitPointerLock ||
document.mozExitPointerLock ||
document.webkitExitPointerLock;
if (document.exitPointerLock) {
document.exitPointerLock();
}
},
getObject: function () {
return this.cameraContainer;
}.bind(this),
};
// Add pointer lock event listeners for the fallback controls
document.addEventListener(
"pointerlockchange",
this.onPointerLockChange.bind(this),
false,
);
document.addEventListener(
"mozpointerlockchange",
this.onPointerLockChange.bind(this),
false,
);
document.addEventListener(
"webkitpointerlockchange",
this.onPointerLockChange.bind(this),
false,
);
// Add mouse movement handler
document.addEventListener("mousemove", this.onMouseMove.bind(this), false);
}
onPointerLockChange() {
if (
document.pointerLockElement === document.body ||
document.mozPointerLockElement === document.body ||
document.webkitPointerLockElement === document.body
) {
this.controls.isLocked = true;
} else {
this.controls.isLocked = false;
}
}
onMouseMove(event) {
if (this.controls.isLocked) {
const movementX =
event.movementX || event.mozMovementX || event.webkitMovementX || 0;
const movementY =
event.movementY || event.mozMovementY || event.webkitMovementY || 0;
// Rotate camera based on mouse movement
this.camera.rotation.y -= movementX * 0.002; // Horizontal rotation
this.camera.rotation.x -= movementY * 0.002; // Vertical rotation
// Limit vertical rotation to avoid flipping
this.camera.rotation.x = Math.max(
-Math.PI / 2,
Math.min(Math.PI / 2, this.camera.rotation.x),
);
}
}
setupLighting() {
// Add ambient light
const ambientLight = new THREE.AmbientLight(0x666666);
this.scene.add(ambientLight);
// Add directional light (sun)
this.sunLight = new THREE.DirectionalLight(0xffffff, 1);
this.sunLight.position.set(100, 100, 50);
this.sunLight.castShadow = true;
// Set shadow properties
this.sunLight.shadow.mapSize.width = 1024;
this.sunLight.shadow.mapSize.height = 1024;
this.sunLight.shadow.camera.near = 0.5;
this.sunLight.shadow.camera.far = 500;
this.sunLight.shadow.camera.left = -100;
this.sunLight.shadow.camera.right = 100;
this.sunLight.shadow.camera.top = 100;
this.sunLight.shadow.camera.bottom = -100;
this.scene.add(this.sunLight);
// Add hemisphere light
const hemiLight = new THREE.HemisphereLight(0x0088ff, 0x00ff88, 0.5);
this.scene.add(hemiLight);
}
onWindowResize() {
// Update camera aspect ratio
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
// Update renderer size
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
onMouseClick(event) {
// Only handle click if controls are locked
if (!this.controls.isLocked) return;
// Set raycaster from camera
this.raycaster.setFromCamera(new THREE.Vector2(0, 0), this.camera);
// Check for intersections with blocks
const intersects = this.raycaster.intersectObjects(
this.scene.children,
true,
);
if (intersects.length > 0) {
const intersection = intersects[0];
const object = intersection.object;
// Check if it's a block
if (object.blockData) {
this.handleBlockClick(object, intersection);
}
// Check if it's a character
else if (object.characterType) {
this.handleCharacterClick(object);
}
}
}
handleBlockClick(block, intersection) {
console.log("Clicked on block:", block.blockData);
// Handle based on current tool
switch (this.currentTool) {
case "pickaxe":
this.mineBlock(block, intersection);
break;
case "shovel":
// Handle shovel interaction
break;
case "compass":
this.showBlockInfo(block);
break;
default:
// Default interaction
break;
}
}
handleCharacterClick(character) {
console.log("Clicked on character:", character.characterType);
// Trigger interaction with character
if (this.onCharacterInteraction) {
this.onCharacterInteraction(character);
}
}
mineBlock(block, intersection) {
// Create mining effect
this.createMiningParticles(intersection.point);
// Start mining progress
if (!this.miningProgress) {
this.miningProgress = {
block: block,
progress: 0,
position: intersection.point.clone(),
complete: false,
};
}
}
showBlockInfo(block) {
// Show information about the block
if (this.onShowBlockInfo) {
this.onShowBlockInfo(block);
}
}
createMiningParticles(position) {
// Create particle geometry
const particleCount = 20;
const particles = new THREE.Group();
for (let i = 0; i < particleCount; i++) {
const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
const material = new THREE.MeshBasicMaterial({
color: 0xaaaaaa,
transparent: true,
opacity: 0.8,
});
const particle = new THREE.Mesh(geometry, material);
// Set initial position
particle.position.copy(position);
// Add random velocity
particle.userData.velocity = new THREE.Vector3(
(Math.random() - 0.5) * 0.1,
Math.random() * 0.1 + 0.05,
(Math.random() - 0.5) * 0.1,
);
// Add to group
particles.add(particle);
}
// Add to scene
this.scene.add(particles);
// Store reference to remove later
this.miningParticles = particles;
// Set timeout to remove particles
setTimeout(() => {
this.scene.remove(particles);
this.miningParticles = null;
}, 1000);
}
updateMiningProgress(delta) {
if (!this.miningProgress) return;
// Update progress based on tool and block
const miningSpeed = 0.5; // Base mining speed
this.miningProgress.progress += miningSpeed * delta;
// Check if mining is complete
if (this.miningProgress.progress >= 1) {
// Mining complete
this.miningProgress.complete = true;
// Remove the block
this.scene.remove(this.miningProgress.block);
// Call completion callback
if (this.onBlockMined) {
this.onBlockMined(this.miningProgress.block);
}
// Reset mining progress
this.miningProgress = null;
}
}
createBlock(x, y, z, type) {
// Create block geometry and material
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = this.getBlockMaterial(type);
// Create mesh
const block = new THREE.Mesh(geometry, material);
block.position.set(x, y, z);
block.castShadow = true;
block.receiveShadow = true;
// Store block data
block.blockData = { type, x, y, z };
// Add to scene
this.scene.add(block);
// Store in world data
this.setVoxel(x, y, z, { type });
return block;
}
getBlockMaterial(type) {
// Load texture for block type
const textureLoader = new THREE.TextureLoader();
let texture;
try {
texture = textureLoader.load(`assets/blocks/${type}.png`);
// Set pixel texture filtering
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
} catch (error) {
console.warn(`Texture for ${type} not found, using fallback`);
}
// Create material based on block type
switch (type) {
case "grass":
return new THREE.MeshStandardMaterial({
map: texture || null,
color: texture ? 0xffffff : 0x55aa55,
});
case "stone":
return new THREE.MeshStandardMaterial({
map: texture || null,
color: texture ? 0xffffff : 0x888888,
});
case "gold":
return new THREE.MeshStandardMaterial({
map: texture || null,
color: texture ? 0xffffff : 0xffcc00,
metalness: 0.7,
roughness: 0.3,
});
case "building":
return new THREE.MeshStandardMaterial({
map: texture || null,
color: texture ? 0xffffff : 0xaa8866,
});
default:
return new THREE.MeshStandardMaterial({
color: 0xffffff,
});
}
}
setVoxel(x, y, z, data) {
// Store voxel data in chunks
const chunkSize = 16;
const chunkX = Math.floor(x / chunkSize);
const chunkY = Math.floor(y / chunkSize);
const chunkZ = Math.floor(z / chunkSize);
const chunkKey = `${chunkX},${chunkY},${chunkZ}`;
if (!this.chunks[chunkKey]) {
this.chunks[chunkKey] = {};
}
const localX = x - chunkX * chunkSize;
const localY = y - chunkY * chunkSize;
const localZ = z - chunkZ * chunkSize;
const localKey = `${localX},${localY},${localZ}`;
this.chunks[chunkKey][localKey] = data;
}
getVoxel(x, y, z) {
// Get voxel data from chunks
const chunkSize = 16;
const chunkX = Math.floor(x / chunkSize);
const chunkY = Math.floor(y / chunkSize);
const chunkZ = Math.floor(z / chunkSize);
const chunkKey = `${chunkX},${chunkY},${chunkZ}`;
if (!this.chunks[chunkKey]) {
return null;
}
const localX = x - chunkX * chunkSize;
const localY = y - chunkY * chunkSize;
const localZ = z - chunkZ * chunkSize;
const localKey = `${localX},${localY},${localZ}`;
return this.chunks[chunkKey][localKey];
}
generateTerrain(centerX, centerZ, radius) {
// Generate flat terrain with some random elevation
const blocks = [];
for (let x = centerX - radius; x <= centerX + radius; x++) {
for (let z = centerZ - radius; z <= centerZ + radius; z++) {
// Calculate base height using noise
const height = Math.floor(this.noise.simplex2(x * 0.1, z * 0.1) * 3);
// Create the ground block
const block = this.createBlock(x, height, z, "grass");
blocks.push(block);
// Create stone blocks underneath
for (let y = height - 1; y >= height - 3; y--) {
const stoneBlock = this.createBlock(x, y, z, "stone");
blocks.push(stoneBlock);
}
}
}
return blocks;
}
createBlockchainBuilding(blockData, x, z) {
// Calculate building dimensions based on block data
const height = Math.max(
4,
Math.min(16, Math.floor(blockData.size / 1000) + 4),
);
const width = Math.max(
3,
Math.min(7, Math.floor(blockData.txCount / 3) + 3),
);
const depth = width;
const buildingBlocks = [];
// Create foundation
for (let bx = 0; bx < width; bx++) {
for (let bz = 0; bz < depth; bz++) {
const block = this.createBlock(x + bx, 0, z + bz, "stone");
buildingBlocks.push(block);
}
}
// Create walls
for (let y = 1; y < height; y++) {
for (let bx = 0; bx < width; bx++) {
for (let bz = 0; bz < depth; bz++) {
// Only create blocks for walls and floors
if (
bx === 0 ||
bx === width - 1 ||
bz === 0 ||
bz === depth - 1 ||
y % 3 === 0
) {
const block = this.createBlock(x + bx, y, z + bz, "building");
buildingBlocks.push(block);
}
}
}
}
// Create roof
for (let bx = 0; bx < width; bx++) {
for (let bz = 0; bz < depth; bz++) {
const block = this.createBlock(x + bx, height, z + bz, "building");
buildingBlocks.push(block);
}
}
// Add a gold block on top to represent the block hash
const hashBlock = this.createBlock(
x + Math.floor(width / 2),
height + 1,
z + Math.floor(depth / 2),
"gold",
);
buildingBlocks.push(hashBlock);
// Store building data
const building = {
blocks: buildingBlocks,
data: blockData,
position: new THREE.Vector3(x, 0, z),
dimensions: { width, height, depth },
};
this.buildings.push(building);
return building;
}
createCharacter(type, x, y, z) {
// Create character model
const geometry = new THREE.BoxGeometry(0.6, 1.8, 0.6);
const material = this.getCharacterMaterial(type);
const character = new THREE.Mesh(geometry, material);
character.position.set(x, y, z);
character.castShadow = true;
character.receiveShadow = true;
// Add character data
character.characterType = type;
character.userData.isCharacter = true;
// Add to scene
this.scene.add(character);
this.characters.push(character);
return character;
}
getCharacterMaterial(type) {
// Load texture for character type
const textureLoader = new THREE.TextureLoader();
let texture;
try {
texture = textureLoader.load(`assets/characters/${type}.png`);
// Set pixel texture filtering
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
} catch (error) {
console.warn(`Texture for character ${type} not found, using fallback`);
}
// Create material based on character type
switch (type) {
case "miner":
return new THREE.MeshStandardMaterial({
map: texture || null,
color: texture ? 0xffffff : 0xffaa00,
});
case "transaction":
return new THREE.MeshStandardMaterial({
map: texture || null,
color: texture ? 0xffffff : 0x00aaff,
});
default:
return new THREE.MeshStandardMaterial({
color: 0xaaaaaa,
});
}
}
updatePlayerPosition(delta) {
if (!this.controls.isLocked) return;
// Get camera direction
const direction = new THREE.Vector3();
this.camera.getWorldDirection(direction);
direction.y = 0;
direction.normalize();
// Get right vector (perpendicular to direction)
const right = new THREE.Vector3(-direction.z, 0, direction.x);
// Update velocity based on input
this.playerVelocity.x = 0;
this.playerVelocity.z = 0;
if (this.keyStates.moveForward) {
this.playerVelocity.add(direction.multiplyScalar(this.playerSpeed));
}
if (this.keyStates.moveBackward) {
this.playerVelocity.add(direction.multiplyScalar(-this.playerSpeed));
}
if (this.keyStates.moveRight) {
this.playerVelocity.add(right.multiplyScalar(this.playerSpeed));
}
if (this.keyStates.moveLeft) {
this.playerVelocity.add(right.multiplyScalar(-this.playerSpeed));
}
// Handle jumping
if (this.keyStates.jump && this.playerOnGround) {
this.playerVelocity.y = this.jumpForce;
this.playerOnGround = false;
}
// Apply gravity
if (!this.playerOnGround) {
this.playerVelocity.y -= this.gravity;
}
// Update position
const deltaPosition = this.playerVelocity
.clone()
.multiplyScalar(delta * 60);
// Get the object to move (either controls object or camera container)
const moveObject = this.controls.getObject
? this.controls.getObject()
: this.cameraContainer;
moveObject.position.add(deltaPosition);
// Check collision with ground
if (moveObject.position.y < this.playerHeight) {
moveObject.position.y = this.playerHeight;
this.playerVelocity.y = 0;
this.playerOnGround = true;
}
// Check collision with walls
// (This is a simplified collision check)
this.checkCollisions();
}
checkCollisions() {
// Simple collision check with blocks
// Get the object to check (either controls object or camera container)
const moveObject = this.controls.getObject
? this.controls.getObject()
: this.cameraContainer;
const position = moveObject.position.clone();
// Check nearby voxels
for (
let x = Math.floor(position.x) - 1;
x <= Math.floor(position.x) + 1;
x++
) {
for (
let z = Math.floor(position.z) - 1;
z <= Math.floor(position.z) + 1;
z++
) {
for (
let y = Math.floor(position.y) - 1;
y <= Math.floor(position.y) + 1;
y++
) {
const voxel = this.getVoxel(x, y, z);
if (voxel) {
// Calculate box collision
const blockMin = new THREE.Vector3(x - 0.5, y - 0.5, z - 0.5);
const blockMax = new THREE.Vector3(x + 0.5, y + 0.5, z + 0.5);
// Player bounding box
const playerMin = new THREE.Vector3(
position.x - this.playerRadius,
position.y - this.playerHeight / 2,
position.z - this.playerRadius,
);
const playerMax = new THREE.Vector3(
position.x + this.playerRadius,
position.y + this.playerHeight / 2,
position.z + this.playerRadius,
);
// Check collision
if (
playerMax.x > blockMin.x &&
playerMin.x < blockMax.x &&
playerMax.y > blockMin.y &&
playerMin.y < blockMax.y &&
playerMax.z > blockMin.z &&
playerMin.z < blockMax.z
) {
// Handle collision
this.resolveCollision(position, x, y, z);
}
}
}
}
}
}
}