about summary refs log blame commit diff stats
path: root/html/voxels/js/game.js
blob: 2ced6fdd6aa6aa3c887e360dac640fa7bbaee489 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

















                                                            





                                  

          


                              


































































                                                                                                


                                                       
















                                                                                     

                                                                              



                                     

                                                 



                       
                                                                

                        
                                                               



                                         

                                                                                                    

                        
                                                                                    
                                                              



                                                                              
                                                               


                                 


                                                                                 

































                                                                                         

                                                                      



                                                                                 





                                                        



                                          






















                                                                                                                                     







































                                                                        

                               





                                                     

















































































                                                                                                       





                                     
class IsometricGame {
    constructor() {
        this.canvas = document.getElementById('gameCanvas');
        this.ctx = this.canvas.getContext('2d');
        
        // Grid properties
        this.gridSize = 10;
        this.tileWidth = 50;
        this.tileHeight = 25;
        
        // Player properties
        this.player = {
            x: 0,
            y: 0,
            targetX: 0,
            targetY: 0,
            size: 20,
            path: [], // Array to store waypoints
            currentWaypoint: null,
            jumpHeight: 0,
            jumpProgress: 0,
            isJumping: false,
            startX: 0,
            startY: 0
        };
        
        // Add particle system
        this.particles = [];
        
        // Handle window resize
        this.resizeCanvas();
        window.addEventListener('resize', () => this.resizeCanvas());
        
        this.setupEventListeners();
        this.gameLoop();
    }
    
    resizeCanvas() {
        this.canvas.width = window.innerWidth;
        this.canvas.height = window.innerHeight;
        
        // Recalculate grid offset to center it
        this.offsetX = this.canvas.width / 2;
        this.offsetY = this.canvas.height / 3;
        
        // Scale tile size based on screen size
        const minDimension = Math.min(this.canvas.width, this.canvas.height);
        const scaleFactor = minDimension / 800; // 800 is our reference size
        this.tileWidth = 50 * scaleFactor;
        this.tileHeight = 25 * scaleFactor;
        this.player.size = 20 * scaleFactor;
    }
    
    toIsometric(x, y) {
        return {
            x: (x - y) * this.tileWidth / 2,
            y: (x + y) * this.tileHeight / 2
        };
    }
    
    fromIsometric(screenX, screenY) {
        // Convert screen coordinates back to grid coordinates
        screenX -= this.offsetX;
        screenY -= this.offsetY;
        
        const x = (screenX / this.tileWidth + screenY / this.tileHeight) / 1;
        const y = (screenY / this.tileHeight - screenX / this.tileWidth) / 1;
        
        return { x: Math.round(x), y: Math.round(y) };
    }
    
    drawGrid() {
        for (let x = 0; x < this.gridSize; x++) {
            for (let y = 0; y < this.gridSize; y++) {
                const iso = this.toIsometric(x, y);
                
                // Draw tile
                this.ctx.beginPath();
                this.ctx.moveTo(iso.x + this.offsetX, iso.y + this.offsetY - this.tileHeight/2);
                this.ctx.lineTo(iso.x + this.offsetX + this.tileWidth/2, iso.y + this.offsetY);
                this.ctx.lineTo(iso.x + this.offsetX, iso.y + this.offsetY + this.tileHeight/2);
                this.ctx.lineTo(iso.x + this.offsetX - this.tileWidth/2, iso.y + this.offsetY);
                this.ctx.closePath();
                
                this.ctx.strokeStyle = '#666';
                this.ctx.stroke();
                this.ctx.fillStyle = '#fff';
                this.ctx.fill();
            }
        }
    }
    
    drawPlayer() {
        // Convert player grid position to isometric coordinates
        const iso = this.toIsometric(this.player.x, this.player.y);
        
        // Apply jump height offset
        const jumpOffset = this.player.jumpHeight || 0;
        
        // Calculate squash and stretch based on jump progress
        let squashStretch = 1;
        if (this.player.isJumping) {
            // Stretch at the start and middle of jump, squash at landing
            const jumpPhase = Math.sin(this.player.jumpProgress * Math.PI);
            if (this.player.jumpProgress < 0.2) {
                // Initial stretch when jumping
                squashStretch = 1 + (0.3 * (1 - this.player.jumpProgress / 0.2));
            } else if (this.player.jumpProgress > 0.8) {
                // Squash when landing
                squashStretch = 1 - (0.3 * ((this.player.jumpProgress - 0.8) / 0.2));
            } else {
                // Slight stretch at peak of jump
                squashStretch = 1 + (0.1 * jumpPhase);
            }
        }
        
        // Draw player shadow (gets smaller when jumping)
        const shadowScale = Math.max(0.2, 1 - (jumpOffset / this.tileHeight));
        this.ctx.beginPath();
        this.ctx.ellipse(
            iso.x + this.offsetX,
            iso.y + this.offsetY + 2,
            this.player.size * 0.8 * shadowScale,
            this.player.size * 0.3 * shadowScale,
            0,
            0,
            Math.PI * 2
        );
        this.ctx.fillStyle = `rgba(0,0,0,${0.2 * shadowScale})`;
        this.ctx.fill();

        // Draw player body with jump offset and squash/stretch
        this.ctx.beginPath();
        this.ctx.fillStyle = '#ff4444';
        this.ctx.strokeStyle = '#aa0000';
        
        const bodyHeight = this.player.size * 2 * squashStretch;
        const bodyWidth = this.player.size * 0.8 * (1 / squashStretch); // Inverse stretch for width
        
        this.ctx.save();
        this.ctx.translate(iso.x + this.offsetX, iso.y + this.offsetY - jumpOffset);
        this.ctx.scale(1, 0.5); // Apply isometric perspective
        this.ctx.fillRect(-bodyWidth/2, -bodyHeight, bodyWidth, bodyHeight);
        this.ctx.strokeRect(-bodyWidth/2, -bodyHeight, bodyWidth, bodyHeight);
        this.ctx.restore();

        // Draw player head with jump offset and squash/stretch
        this.ctx.beginPath();
        this.ctx.ellipse(
            iso.x + this.offsetX,
            iso.y + this.offsetY - this.player.size * squashStretch - jumpOffset,
            this.player.size * (1 / squashStretch),
            this.player.size * 0.5 * squashStretch,
            0,
            0,
            Math.PI * 2
        );
        this.ctx.fillStyle = '#ff4444';
        this.ctx.fill();
        this.ctx.strokeStyle = '#aa0000';
        this.ctx.stroke();
    }
    
    findPath(startX, startY, endX, endY) {
        // Simple pathfinding that follows grid edges
        const path = [];
        
        // First move along X axis
        if (startX !== endX) {
            const stepX = startX < endX ? 1 : -1;
            for (let x = startX + stepX; stepX > 0 ? x <= endX : x >= endX; x += stepX) {
                path.push({ x: x, y: startY });
            }
        }
        
        // Then move along Y axis
        if (startY !== endY) {
            const stepY = startY < endY ? 1 : -1;
            for (let y = startY + stepY; stepY > 0 ? y <= endY : y >= endY; y += stepY) {
                path.push({ x: endX, y: y });
            }
        }

        return path;
    }
    
    updatePlayer() {
        const jumpDuration = 0.1; // Faster jump for snappier movement
        const maxJumpHeight = this.tileHeight;

        // If we don't have a current waypoint but have a path, get next waypoint
        if (!this.player.currentWaypoint && this.player.path.length > 0) {
            this.player.currentWaypoint = this.player.path.shift();
            this.player.isJumping = true;
            this.player.jumpProgress = 0;
            
            // Store starting position for interpolation
            this.player.startX = this.player.x;
            this.player.startY = this.player.y;
        }

        // Move towards current waypoint
        if (this.player.currentWaypoint) {
            // Update jump animation
            if (this.player.isJumping) {
                this.player.jumpProgress += jumpDuration;
                
                // Clamp progress to 1
                if (this.player.jumpProgress > 1) this.player.jumpProgress = 1;
                
                // Parabolic jump arc
                this.player.jumpHeight = Math.sin(this.player.jumpProgress * Math.PI) * maxJumpHeight;
                
                // Precise interpolation between points
                this.player.x = this.player.startX + (this.player.currentWaypoint.x - this.player.startX) * this.player.jumpProgress;
                this.player.y = this.player.startY + (this.player.currentWaypoint.y - this.player.startY) * this.player.jumpProgress;
                
                // Landing
                if (this.player.jumpProgress >= 1) {
                    this.player.isJumping = false;
                    this.player.jumpHeight = 0;
                    this.player.x = this.player.currentWaypoint.x;
                    this.player.y = this.player.currentWaypoint.y;
                    this.createDustParticles(this.player.x, this.player.y);
                    this.player.currentWaypoint = null;
                }
            }
        }
    }
    
    setupEventListeners() {
        this.canvas.addEventListener('click', (e) => {
            const rect = this.canvas.getBoundingClientRect();
            const clickX = e.clientX - rect.left;
            const clickY = e.clientY - rect.top;
            
            const gridPos = this.fromIsometric(clickX, clickY);
            
            // Only move if within grid bounds
            if (gridPos.x >= 0 && gridPos.x < this.gridSize &&
                gridPos.y >= 0 && gridPos.y < this.gridSize) {
                
                // Set target and calculate path
                this.player.targetX = Math.round(gridPos.x);
                this.player.targetY = Math.round(gridPos.y);
                
                // Calculate new path
                this.player.path = this.findPath(
                    Math.round(this.player.x),
                    Math.round(this.player.y),
                    this.player.targetX,
                    this.player.targetY
                );
                
                // Clear current waypoint to start new path
                this.player.currentWaypoint = null;
            }
        });
    }
    
    gameLoop() {
        // Clear canvas
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        
        // Draw game elements
        this.drawGrid();
        this.updateParticles();
        this.drawParticles();
        this.updatePlayer();
        this.drawPlayer();
        
        // Continue game loop
        requestAnimationFrame(() => this.gameLoop());
    }

    // Add new particle system methods
    createDustParticles(x, y) {
        const particleCount = 12; // Increased for more particles
        for (let i = 0; i < particleCount; i++) {
            // Randomize the angle slightly
            const baseAngle = (Math.PI * 2 * i) / particleCount;
            const randomAngle = baseAngle + (Math.random() - 0.5) * 0.5;
            
            // Random speed and size variations
            const speed = 0.3 + Math.random() * 0.4;
            const initialSize = (this.player.size * 0.15) + (Math.random() * this.player.size * 0.15);
            
            // Random grey color
            const greyValue = 220 + Math.floor(Math.random() * 35);
            const color = `rgb(${greyValue}, ${greyValue}, ${greyValue})`;
            
            this.particles.push({
                x: x,
                y: y,
                dx: Math.cos(randomAngle) * speed,
                dy: Math.sin(randomAngle) * speed,
                life: 0.8 + Math.random() * 0.4, // Random initial life
                size: initialSize,
                color: color,
                initialSize: initialSize,
                rotationSpeed: (Math.random() - 0.5) * 0.2,
                rotation: Math.random() * Math.PI * 2
            });
        }
    }

    updateParticles() {
        for (let i = this.particles.length - 1; i >= 0; i--) {
            const particle = this.particles[i];
            
            // Update position with slight gravity effect
            particle.x += particle.dx;
            particle.y += particle.dy;
            particle.dy += 0.01; // Slight upward drift
            
            // Update rotation
            particle.rotation += particle.rotationSpeed;
            
            // Non-linear fade out
            particle.life -= 0.03;
            particle.size = particle.initialSize * (particle.life * 1.5); // Grow slightly as they fade
            
            // Remove dead particles
            if (particle.life <= 0) {
                this.particles.splice(i, 1);
            }
        }
    }

    drawParticles() {
        for (const particle of this.particles) {
            const iso = this.toIsometric(particle.x, particle.y);
            
            this.ctx.save();
            this.ctx.translate(iso.x + this.offsetX, iso.y + this.offsetY);
            this.ctx.rotate(particle.rotation);
            
            // Draw a slightly irregular dust puff
            this.ctx.beginPath();
            const points = 5;
            for (let i = 0; i < points * 2; i++) {
                const angle = (i * Math.PI) / points;
                const radius = particle.size * (i % 2 ? 0.7 : 1);
                const x = Math.cos(angle) * radius;
                const y = Math.sin(angle) * radius;
                if (i === 0) this.ctx.moveTo(x, y);
                else this.ctx.lineTo(x, y);
            }
            this.ctx.closePath();
            
            this.ctx.fillStyle = `rgba(${particle.color.slice(4, -1)}, ${particle.life * 0.5})`;
            this.ctx.fill();
            
            this.ctx.restore();
        }
    }
}

// Start the game when the page loads
window.onload = () => {
    new IsometricGame();
};