Skip to content

Instantly share code, notes, and snippets.

@gary149
Created March 25, 2025 22:38
Show Gist options
  • Select an option

  • Save gary149/3954c94c8d6024efe4fab3471c12a877 to your computer and use it in GitHub Desktop.

Select an option

Save gary149/3954c94c8d6024efe4fab3471c12a877 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Super Platformer Adventure</title>
<style>
body {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #5c94fc; /* Bright Sky blue background */
font-family: 'Arial', sans-serif;
color: white;
overflow: hidden; /* Prevent scrollbars */
}
#gameContainer {
position: relative;
width: 800px; /* Fixed width for the game area */
height: 480px; /* Fixed height */
overflow: hidden; /* Ensure canvas stays within container */
box-shadow: 0 0 10px rgba(0,0,0,0.5);
border: 3px solid black;
}
#gameCanvas {
display: block; /* Remove potential extra space below canvas */
background-color: #5c94fc; /* Match body background */
}
#controlsInfo {
margin-top: 10px;
font-size: 0.9em;
text-align: center;
background-color: rgba(0,0,0,0.3);
padding: 5px;
border-radius: 3px;
max-width: 800px;
}
#status {
position: absolute;
top: 10px;
left: 10px;
font-size: 1.5em;
font-weight: bold;
text-shadow: 2px 2px 3px rgba(0,0,0,0.7);
z-index: 10; /* Ensure status is above canvas */
}
/* Basic responsiveness */
@media (max-width: 820px) {
#gameContainer {
width: 100%;
height: 60vw; /* Adjust height based on width */
max-height: 100vh; /* Prevent excessive height */
border: none;
box-shadow: none;
}
#status {
font-size: 1.2em;
top: 5px;
left: 5px;
}
#controlsInfo {
font-size: 0.8em;
width: 95%;
}
}
@media (orientation: portrait) and (max-height: 500px) {
#gameContainer {
height: 80vh; /* Adjust height for short landscape */
}
#controlsInfo { display: none; } /* Hide controls on very small screens */
}
/* Style for virtual buttons on mobile */
.touch-controls {
display: none; /* Hidden by default */
position: fixed;
bottom: 10px;
width: 100%;
padding: 0 10px;
box-sizing: border-box;
z-index: 20;
justify-content: space-between;
align-items: center;
}
.touch-controls button {
background-color: rgba(255, 255, 255, 0.5);
border: 2px solid rgba(0, 0, 0, 0.5);
border-radius: 50%;
width: 60px;
height: 60px;
font-size: 24px;
font-weight: bold;
color: rgba(0,0,0,0.7);
display: flex;
justify-content: center;
align-items: center;
user-select: none; /* Prevent text selection */
-webkit-user-select: none; /* iOS Safari */
-webkit-tap-highlight-color: transparent; /* Remove tap highlight */
}
.touch-controls .move {
display: flex;
gap: 20px;
}
@media (hover: none) and (pointer: coarse) {
.touch-controls {
display: flex; /* Show on touch devices */
}
#controlsInfo {
display: none; /* Hide text controls info */
}
}
</style>
</head>
<body>
<div id="gameContainer">
<div id="status">Lives: 3 | Score: 0</div>
<canvas id="gameCanvas"></canvas>
</div>
<div id="controlsInfo">
Use Arrow Keys (Left/Right) to Move, Arrow Up to Jump. Stomp on enemies!
</div>
<!-- Virtual Controls for Touch -->
<div class="touch-controls">
<div class="move">
<button id="btnLeft">β—€</button>
<button id="btnRight">β–Ά</button>
</div>
<button id="btnUp">β–²</button>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const statusDisplay = document.getElementById('status');
const gameContainer = document.getElementById('gameContainer');
// Game constants
const GRAVITY = 0.6;
const TILE_SIZE = 32;
const PLAYER_SPEED = 4;
const JUMP_FORCE = 13;
const ENEMY_SPEED = 0.8;
const PLAYER_ANIM_SPEED = 4; // Lower number = faster animation
const ENEMY_ANIM_SPEED = 10;
const CLOUD_SPEED_FACTOR = 0.3; // How much slower clouds move than camera
// Game state
let lives = 3;
let score = 0;
let keys = { left: false, right: false, up: false };
let player;
let enemies = [];
let clouds = [];
let levelData; // Will be populated in init
let cameraX = 0;
let gameOver = false;
let gameWon = false;
let levelWidth;
let levelHeight;
let frameCount = 0; // For animation timing
// --- ASSET SIMULATION using Canvas Drawing ---
// This section simulates sprites. In a real game, you'd load images.
const Sprites = {
drawMarioLike(ctx, x, y, w, h, state, frame, facingRight) {
ctx.save();
ctx.translate(x + w / 2, y + h / 2);
if (!facingRight) {
ctx.scale(-1, 1); // Flip horizontally if facing left
}
ctx.translate(-w / 2, -h / 2);
// Body (Red)
ctx.fillStyle = '#E60012';
ctx.fillRect(w * 0.15, h * 0.4, w * 0.7, h * 0.5);
// Overalls (Blue)
ctx.fillStyle = '#0078F8';
ctx.fillRect(w * 0.2, h * 0.55, w * 0.6, h * 0.35); // Main part
ctx.fillRect(w * 0.1, h * 0.45, w * 0.2, h * 0.1); // Strap Left
ctx.fillRect(w * 0.7, h * 0.45, w * 0.2, h * 0.1); // Strap Right
// Head (Skin tone)
ctx.fillStyle = '#FFDCB8';
ctx.fillRect(w * 0.25, h * 0.1, w * 0.5, h * 0.3);
// Hat (Red)
ctx.fillStyle = '#E60012';
ctx.fillRect(w * 0.2, 0, w * 0.6, h * 0.2);
// Boots (Brown)
ctx.fillStyle = '#8B4513';
ctx.fillRect(0, h * 0.9, w * 0.4, h * 0.1);
ctx.fillRect(w * 0.6, h * 0.9, w * 0.4, h * 0.1);
// Simple Animation (Legs moving when running)
if (state === 'run' && frame % (PLAYER_ANIM_SPEED * 2) < PLAYER_ANIM_SPEED) {
ctx.fillRect(0, h * 0.8, w * 0.4, h * 0.1); // Left leg forward
} else if (state === 'run') {
ctx.fillRect(w * 0.6, h * 0.8, w * 0.4, h * 0.1); // Right leg forward
}
// Eye (Simple)
ctx.fillStyle = 'black';
ctx.fillRect(w * 0.6, h * 0.2, w * 0.1, h * 0.1);
ctx.restore();
},
drawGoombaLike(ctx, x, y, w, h, alive, frame) {
if (!alive) { // Squashed
ctx.fillStyle = '#8B4513'; // Darker brown
ctx.fillRect(x, y + h * 0.6, w, h * 0.4); // Flat shape
ctx.fillStyle = '#A0522D'; // Lighter top
ctx.fillRect(x, y + h * 0.6, w, h * 0.1);
} else {
// Body (Brown)
ctx.fillStyle = '#996633'; // Goomba brown
ctx.beginPath();
ctx.moveTo(x + w * 0.1, y + h); // Bottom left
ctx.lineTo(x, y + h * 0.5); // Mid left
ctx.quadraticCurveTo(x + w / 2, y - h * 0.1, x + w, y + h * 0.5); // Top curve
ctx.lineTo(x + w * 0.9, y + h); // Bottom right
ctx.closePath();
ctx.fill();
// Feet (Darker Brown) - Animated slightly
const feetFrame = Math.floor(frame / ENEMY_ANIM_SPEED) % 2;
const footOffset = feetFrame === 0 ? w * 0.1 : w * 0.3;
ctx.fillStyle = '#663300';
ctx.fillRect(x + footOffset, y + h * 0.9, w * 0.2, h * 0.15);
ctx.fillRect(x + w - footOffset - w * 0.2, y + h * 0.9, w * 0.2, h * 0.15);
// Eyes (Angry)
ctx.fillStyle = 'white';
ctx.fillRect(x + w * 0.2, y + h * 0.3, w * 0.2, h * 0.15); // Left white
ctx.fillRect(x + w * 0.6, y + h * 0.3, w * 0.2, h * 0.15); // Right white
ctx.fillStyle = 'black';
ctx.fillRect(x + w * 0.25, y + h * 0.32, w * 0.1, h * 0.1); // Left pupil
ctx.fillRect(x + w * 0.65, y + h * 0.32, w * 0.1, h * 0.1); // Right pupil
// Eyebrows
ctx.fillRect(x + w * 0.15, y + h * 0.25, w * 0.3, h * 0.05);
ctx.fillRect(x + w * 0.55, y + h * 0.25, w * 0.3, h * 0.05);
}
},
drawGround(ctx, x, y, w, h) {
ctx.fillStyle = '#D2691E'; // Dirt brown
ctx.fillRect(x, y, w, h);
ctx.fillStyle = '#228B22'; // Grass green top
ctx.fillRect(x, y, w, h * 0.3);
// Add subtle texture
ctx.fillStyle = 'rgba(0,0,0,0.1)';
for (let i = 0; i < 3; i++) {
ctx.fillRect(x + Math.random() * w * 0.8, y + h * 0.3 + Math.random() * h * 0.6, w * 0.1, h * 0.1);
}
},
drawBrick(ctx, x, y, w, h) {
ctx.fillStyle = '#B22222'; // Brick red
ctx.fillRect(x, y, w, h);
ctx.strokeStyle = '#8B0000'; // Darker mortar lines
ctx.lineWidth = 2;
// Horizontal lines
ctx.beginPath();
ctx.moveTo(x, y + h / 2);
ctx.lineTo(x + w, y + h / 2);
// Vertical lines (offset)
ctx.moveTo(x + w / 2, y);
ctx.lineTo(x + w / 2, y + h / 2);
ctx.moveTo(x + w / 4, y + h / 2);
ctx.lineTo(x + w / 4, y + h);
ctx.moveTo(x + w * 3/4, y + h / 2);
ctx.lineTo(x + w * 3/4, y + h);
ctx.stroke();
},
drawPipe(ctx, x, y, w, h, type) {
ctx.fillStyle = '#00A800'; // Pipe green
ctx.fillRect(x, y, w, h);
// Add highlight/shadow for 3D effect
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; // Highlight
ctx.fillRect(x + w * 0.1, y, w * 0.3, h);
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; // Shadow
ctx.fillRect(x + w * 0.6, y, w * 0.3, h);
// Pipe top has a rim
if (type === 4 || type === 5) { // Pipe Top Left or Right
ctx.fillStyle = '#008000'; // Darker green rim base
ctx.fillRect(x, y, w, h * 0.3);
ctx.fillStyle = '#00A800'; // Main color rim top
ctx.fillRect(x, y, w, h * 0.2);
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; // Highlight on rim
ctx.fillRect(x + w * 0.1, y, w * 0.8, h * 0.1);
}
// Add border lines
ctx.strokeStyle = '#006400'; // Dark green border
ctx.lineWidth = 1;
ctx.strokeRect(x, y, w, h);
},
drawCloud(ctx, x, y, w, h) {
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; // Semi-transparent white
ctx.beginPath();
const step = w / 5;
ctx.moveTo(x, y + h);
ctx.quadraticCurveTo(x + step * 0.5, y + h * 0.4, x + step * 1.5, y + h * 0.6);
ctx.quadraticCurveTo(x + step * 2.5, y + h * 0.1, x + step * 3.5, y + h * 0.5);
ctx.quadraticCurveTo(x + step * 4.5, y + h * 0.3, x + step * 5, y + h * 0.8);
ctx.lineTo(x+w, y+h); // Flat bottom-ish
ctx.closePath();
ctx.fill();
},
drawGoal(ctx, x, y, w, h) {
// Pole
ctx.fillStyle = '#C0C0C0'; // Silver pole
ctx.fillRect(x + w * 0.45, y, w * 0.1, h);
// Flag
ctx.fillStyle = '#00A800'; // Green flag
ctx.beginPath();
ctx.moveTo(x + w * 0.5, y + h * 0.1);
ctx.lineTo(x + w * 0.9, y + h * 0.3);
ctx.lineTo(x + w * 0.5, y + h * 0.5);
ctx.closePath();
ctx.fill();
}
};
// Level Definition
// 0: empty, 1: ground, 2: brick, 3: goal
// 4: pipe top-left, 5: pipe top-right, 6: pipe stem-left, 7: pipe stem-right
const levelLayout = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
];
function isSolidTile(tileType) {
return tileType === 1 || tileType === 2 || tileType >= 4; // Ground, Brick, Pipes are solid
}
function resizeCanvas() {
const containerWidth = gameContainer.clientWidth;
// Maintain aspect ratio (e.g., 800 / 480 = 5/3)
const containerHeight = containerWidth * (480 / 800);
gameContainer.style.height = `${containerHeight}px`;
canvas.width = containerWidth;
canvas.height = containerHeight;
// Optional: Adjust TILE_SIZE or redraw based on new size if needed
// For simplicity, we keep TILE_SIZE constant and scale rendering if necessary
// Or just let the larger container show more of the world.
// We'll keep TILE_SIZE fixed relative to the 800x480 design.
// Scaling factor might be needed for drawing if we wanted elements to resize.
}
function initGame() {
resizeCanvas(); // Initial sizing
levelData = levelLayout; // Use the defined layout
levelHeight = TILE_SIZE * levelData.length;
levelWidth = TILE_SIZE * levelData[0].length;
player = {
x: TILE_SIZE * 2,
y: levelHeight - TILE_SIZE * 5, // Start higher up
width: TILE_SIZE * 0.8,
height: TILE_SIZE * 0.95,
vx: 0, // velocity x
vy: 0, // velocity y
onGround: false,
facingRight: true,
animationState: 'idle', // 'idle', 'run', 'jump'
animationFrame: 0
};
enemies = [
// Type 'goomba' added for potential sprite switching later
{ type: 'goomba', x: TILE_SIZE * 10, y: levelHeight - TILE_SIZE * 2, width: TILE_SIZE * 0.9, height: TILE_SIZE * 0.9, vx: -ENEMY_SPEED, vy: 0, alive: true, animationFrame: 0 },
{ type: 'goomba', x: TILE_SIZE * 25, y: levelHeight - TILE_SIZE * 2, width: TILE_SIZE * 0.9, height: TILE_SIZE * 0.9, vx: -ENEMY_SPEED, vy: 0, alive: true, animationFrame: 0 },
{ type: 'goomba', x: TILE_SIZE * 46, y: levelHeight - TILE_SIZE * 5, width: TILE_SIZE * 0.9, height: TILE_SIZE * 0.9, vx: -ENEMY_SPEED, vy: 0, alive: true, animationFrame: 0 },
{ type: 'goomba', x: TILE_SIZE * 55, y: levelHeight - TILE_SIZE * 2, width: TILE_SIZE * 0.9, height: TILE_SIZE * 0.9, vx: ENEMY_SPEED, vy: 0, alive: true, animationFrame: 0 },
];
// Initialize Clouds
clouds = [];
for (let i = 0; i < 15; i++) {
clouds.push({
x: Math.random() * levelWidth * 1.5, // Spread clouds wider than level initially
y: Math.random() * levelHeight * 0.4 + TILE_SIZE, // Clouds in upper part
width: TILE_SIZE * (Math.random() * 3 + 2), // Varying sizes
height: TILE_SIZE * (Math.random() * 1 + 1),
speed: (Math.random() * 0.3 + 0.1) * CLOUD_SPEED_FACTOR // Varying speeds
});
}
lives = 3;
score = 0;
gameOver = false;
gameWon = false;
cameraX = 0;
frameCount = 0;
updateStatus();
// Ensure game loop doesn't start multiple times if init is called again
if (!gameLoop._isRunning) {
gameLoop();
}
}
gameLoop._isRunning = false; // Add a flag to the function object
function updateStatus() {
statusDisplay.textContent = `Lives: ${lives} | Score: ${score}`;
}
function handleInput() {
player.vx = 0;
let running = false;
if (keys.left) {
player.vx = -PLAYER_SPEED;
player.facingRight = false;
running = true;
}
if (keys.right) {
player.vx = PLAYER_SPEED;
player.facingRight = true;
running = true;
}
// Jump
if (keys.up && player.onGround) {
player.vy = -JUMP_FORCE;
player.onGround = false;
}
// Update animation state
if (!player.onGround) {
player.animationState = 'jump';
} else if (running) {
player.animationState = 'run';
} else {
player.animationState = 'idle';
}
}
function checkCollision(rect1, rect2) {
return rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y;
}
function resolveTileCollision(entity, tileX, tileY, tileType) {
const tile = {
x: tileX * TILE_SIZE,
y: tileY * TILE_SIZE,
width: TILE_SIZE,
height: TILE_SIZE
};
if (!checkCollision(entity, tile)) return { collided: false };
// Determine collision side
const overlapX = (entity.x + entity.width / 2) - (tile.x + tile.width / 2);
const overlapY = (entity.y + entity.height / 2) - (tile.y + tile.height / 2);
const combinedHalfWidths = (entity.width + tile.width) / 2;
const combinedHalfHeights = (entity.height + tile.height) / 2;
let collisionSide = null; // 'top', 'bottom', 'left', 'right' (relative to the entity)
// Use penetration depth to determine primary collision axis
const penetrationX = combinedHalfWidths - Math.abs(overlapX);
const penetrationY = combinedHalfHeights - Math.abs(overlapY);
if (penetrationY < penetrationX) {
// Vertical collision is primary
if (overlapY > 0) { // Entity center is below tile center -> hit from top (landed)
entity.y = tile.y + tile.height; // Snap entity top to tile bottom
if(entity.vy < 0) entity.vy = 0; // Stop upward movement if hitting bottom of tile
collisionSide = 'top';
} else { // Entity center is above tile center -> hit from bottom (landed)
entity.y = tile.y - entity.height; // Snap entity bottom to tile top
if(entity.vy > 0) {
entity.vy = 0; // Stop downward movement
entity.onGround = true; // Landed
}
collisionSide = 'bottom';
}
} else {
// Horizontal collision is primary
if (overlapX > 0) { // Entity center is right of tile center -> hit from left
entity.x = tile.x + tile.width; // Snap entity left to tile right
if (entity.vx < 0) entity.vx = 0;
collisionSide = 'left';
} else { // Entity center is left of tile center -> hit from right
entity.x = tile.x - entity.width; // Snap entity right to tile left
if (entity.vx > 0) entity.vx = 0;
collisionSide = 'right';
}
// Reverse enemy direction on horizontal wall hit
if (entity !== player) entity.vx *= -1;
}
return { collided: true, side: collisionSide, tileType: tileType };
}
function updateEntity(entity) {
// Apply gravity
entity.vy += GRAVITY;
// --- Horizontal Movement and Collision ---
entity.x += entity.vx;
let startCol = Math.floor(entity.x / TILE_SIZE);
let endCol = Math.floor((entity.x + entity.width) / TILE_SIZE);
let startRow = Math.floor(entity.y / TILE_SIZE);
let endRow = Math.floor((entity.y + entity.height) / TILE_SIZE);
for (let r = startRow; r <= endRow; r++) {
for (let c = startCol; c <= endCol; c++) {
if (r >= 0 && r < levelData.length && c >= 0 && c < levelData[0].length) {
const tileType = levelData[r][c];
if (isSolidTile(tileType)) {
resolveTileCollision(entity, c, r, tileType);
} else if (tileType === 3 && entity === player) { // Goal check for player only
const goalTile = { x: c * TILE_SIZE, y: r * TILE_SIZE, width: TILE_SIZE, height: TILE_SIZE };
if(checkCollision(player, goalTile)) gameWon = true;
}
}
}
}
// --- Vertical Movement and Collision ---
// Assume not on ground before vertical check
if (entity === player) entity.onGround = false;
entity.y += entity.vy;
startCol = Math.floor(entity.x / TILE_SIZE);
endCol = Math.floor((entity.x + entity.width) / TILE_SIZE);
startRow = Math.floor(entity.y / TILE_SIZE);
endRow = Math.floor((entity.y + entity.height) / TILE_SIZE);
for (let r = startRow; r <= endRow; r++) {
for (let c = startCol; c <= endCol; c++) {
if (r >= 0 && r < levelData.length && c >= 0 && c < levelData[0].length) {
const tileType = levelData[r][c];
if (isSolidTile(tileType)) {
resolveTileCollision(entity, c, r, tileType);
} else if (tileType === 3 && entity === player) { // Goal check again
const goalTile = { x: c * TILE_SIZE, y: r * TILE_SIZE, width: TILE_SIZE, height: TILE_SIZE };
if(checkCollision(player, goalTile)) gameWon = true;
}
}
}
}
// --- Boundary Checks ---
// Prevent falling through explicit floor (less critical if level bottom is solid)
if (entity.y + entity.height > levelHeight) {
entity.y = levelHeight - entity.height;
entity.vy = 0;
if (entity === player) {
entity.onGround = true;
// playerDied(); // Optional: Fall out of world if bottom isn't solid
}
}
// Fall out of world check (if bottom is open)
if (entity.y > levelHeight + TILE_SIZE * 2) {
if (entity === player) {
playerDied();
} else {
entity.alive = false; // Enemy fell out
}
}
// Prevent going off left edge
if (entity.x < 0) {
entity.x = 0;
if (entity === player) entity.vx = 0;
else entity.vx *= -1; // Enemy turns around
}
// Prevent going too far right (optional, camera handles player)
// if (entity.x + entity.width > levelWidth) {
// entity.x = levelWidth - entity.width;
// if (entity === player) entity.vx = 0;
// else entity.vx *= -1;
// }
// --- Enemy Specific Logic ---
if (entity !== player && entity.alive) {
// Simple Edge Detection: Check if tile below and ahead is empty
let checkTileX = (entity.vx > 0) ? Math.floor((entity.x + entity.width) / TILE_SIZE) : Math.floor(entity.x / TILE_SIZE);
let checkAheadX = (entity.vx > 0) ? Math.floor((entity.x + entity.width + 1) / TILE_SIZE) : Math.floor((entity.x - 1) / TILE_SIZE);
let checkBelowY = Math.floor((entity.y + entity.height + 1) / TILE_SIZE); // Tile just below enemy feet
if (checkBelowY >= 0 && checkBelowY < levelData.length && checkAheadX >= 0 && checkAheadX < levelData[0].length) {
const aheadTileBelowType = levelData[checkBelowY][checkAheadX];
if (!isSolidTile(aheadTileBelowType)) { // If there's no ground ahead
entity.vx *= -1; // Turn around
// Nudge slightly to prevent getting stuck
entity.x += entity.vx > 0 ? 1 : -1;
}
} else {
// Turn around if near level boundary (or calculation goes out of bounds)
entity.vx *= -1;
entity.x += entity.vx > 0 ? 1 : -1;
}
entity.animationFrame++; // Update enemy animation frame
}
}
function updatePlayerSpecific() {
updateEntity(player); // Use the common update logic
player.animationFrame++; // Update player animation frame
// Check collisions with enemies
enemies.forEach((enemy) => {
if (!enemy.alive) return;
if (checkCollision(player, enemy)) {
// Check for stomp (player falling, slightly above enemy midsection)
if (player.vy > 0 && (player.y + player.height) < (enemy.y + enemy.height * 0.5)) {
// Stomp successful
enemy.alive = false; // Mark as squashed
enemy.vx = 0; // Stop moving
player.vy = -JUMP_FORCE / 1.5; // Small bounce
score += 100;
updateStatus();
// Optional: remove enemy visually after a delay? No, just draw squashed.
} else {
// Player hit enemy from side or bottom - player gets hurt
playerDied();
}
}
});
}
function updateEnemies() {
enemies.forEach(enemy => {
if (enemy.alive) {
updateEntity(enemy); // Use the common update logic
}
// Note: Enemy<->Enemy collision is not handled
});
}
function updateClouds() {
clouds.forEach(cloud => {
cloud.x -= cloud.speed; // Move clouds slowly left (relative to world)
// Wrap clouds around
if (cloud.x + cloud.width < 0) {
cloud.x = levelWidth + Math.random() * 100; // Reappear on the right
cloud.y = Math.random() * levelHeight * 0.4 + TILE_SIZE; // Randomize height slightly
}
});
}
function playerDied() {
if (gameOver || gameWon) return; // Prevent multiple triggers in one frame
// Add brief visual feedback (e.g., flash player color - omitted for brevity)
lives--;
updateStatus();
if (lives <= 0) {
gameOver = true;
} else {
// Reset player position (simple respawn) with slight delay/invincibility (conceptual)
// For now, just reset instantly
player.x = TILE_SIZE * 2;
player.y = levelHeight - TILE_SIZE * 5; // Start a bit higher
player.vx = 0;
player.vy = 0;
player.onGround = false; // Need to fall again
// TODO: Add brief invincibility state/timer?
}
}
function updateCamera() {
// Target camera X to center player, clamped within level bounds
const targetCameraX = player.x - canvas.width / 2 + player.width / 2;
// Smooth camera movement (lerp)
const lerpFactor = 0.1;
cameraX += (targetCameraX - cameraX) * lerpFactor;
// Clamp camera position
cameraX = Math.max(0, Math.min(cameraX, levelWidth - canvas.width));
// Handle edge case where level is narrower than canvas
if (levelWidth < canvas.width) {
cameraX = (levelWidth - canvas.width) / 2;
}
}
function drawBackground() {
// Sky color is set on canvas style, but could draw gradients etc. here
ctx.fillStyle = '#5c94fc';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw Clouds with Parallax
clouds.forEach(cloud => {
// Adjust cloud X based on camera movement but slower
const parallaxX = cloud.x - cameraX * CLOUD_SPEED_FACTOR;
// Only draw if visible within the canvas plus some buffer
if (parallaxX + cloud.width > 0 && parallaxX < canvas.width) {
Sprites.drawCloud(ctx, parallaxX, cloud.y, cloud.width, cloud.height);
}
});
}
function drawLevel() {
const startCol = Math.floor(cameraX / TILE_SIZE);
const endCol = Math.min(startCol + Math.ceil(canvas.width / TILE_SIZE) + 1, levelData[0].length);
const startRow = 0; // Always draw all rows vertically
const endRow = levelData.length;
for (let r = startRow; r < endRow; r++) {
for (let c = startCol; c < endCol; c++) {
const tileType = levelData[r][c];
if (tileType === 0) continue; // Skip empty tiles
const tileX = Math.floor(c * TILE_SIZE - cameraX); // Use Math.floor for crisp pixels
const tileY = Math.floor(r * TILE_SIZE);
switch (tileType) {
case 1: // Ground
Sprites.drawGround(ctx, tileX, tileY, TILE_SIZE, TILE_SIZE);
break;
case 2: // Brick
Sprites.drawBrick(ctx, tileX, tileY, TILE_SIZE, TILE_SIZE);
break;
case 3: // Goal
Sprites.drawGoal(ctx, tileX, tileY, TILE_SIZE, TILE_SIZE);
break;
case 4: // Pipe Top Left
case 5: // Pipe Top Right
case 6: // Pipe Stem Left
case 7: // Pipe Stem Right
Sprites.drawPipe(ctx, tileX, tileY, TILE_SIZE, TILE_SIZE, tileType);
break;
// Add more cases for other tile types if needed
}
}
}
}
function drawPlayer() {
Sprites.drawMarioLike(
ctx,
Math.floor(player.x - cameraX), // Floor for crispness
Math.floor(player.y),
player.width,
player.height,
player.animationState,
player.animationFrame,
player.facingRight
);
}
function drawEnemies() {
enemies.forEach(enemy => {
// Only draw if on screen
const screenX = enemy.x - cameraX;
if (screenX + enemy.width > 0 && screenX < canvas.width) {
Sprites.drawGoombaLike(
ctx,
Math.floor(screenX),
Math.floor(enemy.y),
enemy.width,
enemy.height,
enemy.alive,
enemy.animationFrame
);
}
});
}
function drawGameOverScreen() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = `${Math.min(48, canvas.width / 10)}px Arial`; // Responsive font size
ctx.fillStyle = 'red';
ctx.textAlign = 'center';
ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2 - 20);
ctx.font = `${Math.min(24, canvas.width / 20)}px Arial`;
ctx.fillStyle = 'white';
ctx.fillText('Press R or Tap to Restart', canvas.width / 2, canvas.height / 2 + 30);
}
function drawGameWonScreen() {
ctx.fillStyle = 'rgba(0, 80, 0, 0.7)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = `${Math.min(48, canvas.width / 10)}px Arial`;
ctx.fillStyle = 'gold';
ctx.textAlign = 'center';
ctx.fillText('YOU WIN!', canvas.width / 2, canvas.height / 2 - 30);
ctx.font = `${Math.min(24, canvas.width / 18)}px Arial`;
ctx.fillStyle = 'white';
ctx.fillText(`Final Score: ${score}`, canvas.width / 2, canvas.height / 2 + 20);
ctx.font = `${Math.min(20, canvas.width / 25)}px Arial`;
ctx.fillText('Press R or Tap to Restart', canvas.width / 2, canvas.height / 2 + 60);
}
function gameLoop() {
gameLoop._isRunning = true;
frameCount++;
if (gameOver) {
drawBackground();
drawLevel();
drawPlayer(); // Draw player in final state
drawEnemies(); // Draw enemies in final state
drawGameOverScreen();
requestAnimationFrame(gameLoop); // Keep listening for restart
return;
}
if (gameWon) {
drawBackground();
drawLevel();
drawPlayer();
drawEnemies();
drawGameWonScreen();
requestAnimationFrame(gameLoop); // Keep listening for restart
return;
}
// 1. Handle Input
handleInput();
// 2. Update Game State
updatePlayerSpecific();
updateEnemies();
updateClouds();
updateCamera();
// 3. Render Graphics
drawBackground();
drawLevel();
drawEnemies();
drawPlayer(); // Draw player last so they are on top
// Request next frame
requestAnimationFrame(gameLoop);
}
// --- Event Listeners ---
window.addEventListener('keydown', (e) => {
// Prevent default browser behavior for arrow keys and space
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', ' '].includes(e.key)) {
e.preventDefault();
}
switch (e.key) {
case 'ArrowLeft': keys.left = true; break;
case 'ArrowRight': keys.right = true; break;
case 'ArrowUp':
case ' ': keys.up = true; break; // Space bar for jump
case 'r':
case 'R':
if (gameOver || gameWon) {
initGame(); // Restart game
}
break;
}
});
window.addEventListener('keyup', (e) => {
switch (e.key) {
case 'ArrowLeft': keys.left = false; break;
case 'ArrowRight': keys.right = false; break;
case 'ArrowUp':
case ' ': keys.up = false; break;
}
});
// Resize Listener
window.addEventListener('resize', resizeCanvas);
// --- Touch Controls ---
const btnLeft = document.getElementById('btnLeft');
const btnRight = document.getElementById('btnRight');
const btnUp = document.getElementById('btnUp');
function handleTouchStart(e) {
e.preventDefault(); // Prevent scrolling/zooming
const target = e.target;
if (target === btnLeft) keys.left = true;
else if (target === btnRight) keys.right = true;
else if (target === btnUp) keys.up = true;
else if (gameOver || gameWon) { // Tap anywhere to restart
initGame();
}
}
function handleTouchEnd(e) {
e.preventDefault();
// Find which button touch ended on (if any)
// We reset all keys on any touch end for simplicity,
// prevents issues with multiple touches ending simultaneously.
// A more robust solution would track individual touch points (e.g., e.changedTouches)
keys.left = false;
keys.right = false;
keys.up = false;
}
const touchControlsDiv = document.querySelector('.touch-controls');
touchControlsDiv.addEventListener('touchstart', handleTouchStart, { passive: false });
touchControlsDiv.addEventListener('touchend', handleTouchEnd, { passive: false });
touchControlsDiv.addEventListener('touchcancel', handleTouchEnd, { passive: false });
// Also listen for taps on the canvas itself for restarting
canvas.addEventListener('touchstart', (e) => {
if (gameOver || gameWon) {
e.preventDefault(); // Prevent potential double restart with button taps
initGame();
}
}, { passive: false });
// --- Start the game ---
initGame();
</script>
</body>
</html>
@HP2000C

HP2000C commented Aug 10, 2026

Copy link
Copy Markdown

βœ…πŸ§‘πŸ»β€πŸ’» GOOD πŸ™‚βœŠπŸ»

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment