Meta Description" name="description" />
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Mobile 2D Craft</title>
<style>
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
margin: 0;
padding: 0;
}
body {
background-color: #111;
color: #fff;
font-family: monospace;
overflow: hidden;
position: fixed;
width: 100vw;
height: 100vh;
}
#game-container {
position: relative;
width: 100%;
height: 100%;
}
canvas {
display: block;
width: 100%;
height: 100%;
background: #87CEEB; /* Sky Blue */
}
/* Mobile UI Layer */
.ui-element {
position: absolute;
z-index: 10;
}
/* Hotbar at the top */
#hotbar {
top: 10px;
left: 50%;
transform: translateX(-50%);
display: flex;
background: rgba(0,0,0,0.6);
padding: 5px;
border: 2px solid #555;
border-radius: 5px;
}
.slot {
width: 40px;
height: 40px;
margin: 0 3px;
border: 2px solid #888;
display: flex;
justify-content: center;
align-items: center;
font-size: 10px;
font-weight: bold;
position: relative;
}
.slot.active {
border-color: #fff;
background: rgba(255,255,255,0.2);
}
/* Touch Controls */
.dpad {
bottom: 20px;
left: 20px;
display: grid;
grid-template-columns: 50px 50px 50px;
grid-template-rows: 50px 50px;
gap: 5px;
}
.action-btns {
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
.btn {
background: rgba(255, 255, 255, 0.3);
border: 2px solid #fff;
color: white;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.2rem;
font-weight: bold;
active-background: rgba(255, 255, 255, 0.6);
}
.btn:active {
background: rgba(255,255,255,0.6);
}
#btn-left { grid-column: 1; grid-row: 2; width: 50px; height: 50px;}
#btn-right { grid-column: 3; grid-row: 2; width: 50px; height: 50px;}
#btn-jump { width: 60px; height: 60px; border-radius: 50%; }
#btn-action { width: 60px; height: 40px; font-size: 0.8rem; }
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas"></canvas>
<!-- Hotbar -->
<div id="hotbar" class="ui-element">
<div class="slot active" id="slot-1" style="background-color: #8B5A2B;">Dirt</div>
<div class="slot" id="slot-2" style="background-color: #A0522D;">Wood</div>
<div class="slot" id="slot-3" style="background-color: #228B22;">Leaf</div>
<div class="slot" id="slot-4" style="background-color: #808080;">Stone</div>
</div>
<!-- D-Pad Directional Controls -->
<div class="dpad ui-element">
<div id="btn-left" class="btn">◀</div>
<div id="btn-right" class="btn">▶</div>
</div>
<!-- Action Controls -->
<div class="action-btns ui-element">
<div id="btn-action" class="btn">MODE: DIG</div>
<div id="btn-jump" class="btn">▲</div>
</div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// Resize Canvas dynamically
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
// Game Configuration
const BLOCK_SIZE = 32;
const WORLD_WIDTH = 100; // Total blocks wide
const WORLD_HEIGHT = 30; // Total blocks high
// Block Types definitions
const BLOCKS = {
0: { name: 'Air', color: null },
1: { name: 'Dirt', color: '#8B5A2B' },
2: { name: 'Grass', color: '#4CAF50' },
3: { name: 'Stone', color: '#808080' },
4: { name: 'Wood', color: '#A0522D' },
5: { name: 'Leaf', color: '#228B22' }
};
// Inventory / Selection
let currentBuildID = 1; // Default: Dirt
let interactionMode = 'DIG'; // 'DIG' or 'PLACE'
const inventoryOrder = [1, 4, 5, 3]; // Dirt, Wood, Leaf, Stone
// Map Grid Array
let world = [];
// World Generation (Basic 2D Terrain)
function generateWorld() {
for (let x = 0; x < WORLD_WIDTH; x++) {
world[x] = [];
// Simple sine wave for terrain hills
let surfaceY = Math.floor(12 + Math.sin(x * 0.15) * 3);
for (let y = 0; y < WORLD_HEIGHT; y++) {
if (y < surfaceY) {
world[x][y] = 0; // Air
} else if (y === surfaceY) {
world[x][y] = 2; // Grass
} else if (y > surfaceY && y < surfaceY + 4) {
world[x][y] = 1; // Dirt
} else {
world[x][y] = 3; // Stone
}
}
// Spawn a few random trees
if (x > 5 && x < WORLD_WIDTH - 5 && Math.random() < 0.15 && x % 4 === 0) {
let treeY = surfaceY;
world[x][treeY-1] = 4;
world[x][treeY-2] = 4;
world[x][treeY-3] = 4; // Trunk
// Leaves
world[x][treeY-4] = 5;
world[x-1][treeY-4] = 5;
world[x+1][treeY-4] = 5;
world[x][treeY-5] = 5;
}
}
}
generateWorld();
// Player Object
const player = {
x: 10 * BLOCK_SIZE,
y: 5 * BLOCK_SIZE,
width: BLOCK_SIZE * 0.8,
height: BLOCK_SIZE * 1.6,
vx: 0,
vy: 0,
speed: 3,
jumpForce: -8,
grounded: false
};
// Camera Object
const camera = { x: 0, y: 0 };
// Controller Inputs State
const input = { left: false, right: false };
// Setup Mobile Button Listeners
function bindTouch(id, action) {
const el = document.getElementById(id);
el.addEventListener('touchstart', (e) => { e.preventDefault(); action(true); }, {passive: false});
el.addEventListener('touchend', (e) => { e.preventDefault(); action(false); }, {passive: false});
// Fallbacks for mouse testing
el.addEventListener('mousedown', () => action(true));
el.addEventListener('mouseup', () => action(false));
}
bindTouch('btn-left', (val) => input.left = val);
bindTouch('btn-right', (val) => input.right = val);
// Jump action triggers instantly on press
document.getElementById('btn-jump').addEventListener('touchstart', (e) => {
e.preventDefault();
if (player.grounded) {
player.vy = player.jumpForce;
player.grounded = false;
}
});
document.getElementById('btn-jump').addEventListener('mousedown', () => {
if (player.grounded) { player.vy = player.jumpForce; player.grounded = false; }
});
// Toggle Action Mode (Dig vs Place)
const actionBtn = document.getElementById('btn-action');
actionBtn.addEventListener('click', () => {
interactionMode = interactionMode === 'DIG' ? 'PLACE' : 'DIG';
actionBtn.textContent = `MODE: ${interactionMode}`;
});
// Cycle Inventory Hotbar slots on tap
const slots = [
document.getElementById('slot-1'),
document.getElementById('slot-2'),
document.getElementById('slot-3'),
document.getElementById('slot-4')
];
slots.forEach((slot, index) => {
slot.addEventListener('click', () => {
slots.forEach(s => s.classList.remove('active'));
slot.classList.add('active');
currentBuildID = inventoryOrder[index];
});
});
// Canvas World Tap (Mining / Building)
canvas.addEventListener('touchstart', handleCanvasTap, {passive: false});
canvas.addEventListener('mousedown', handleCanvasTap);
function handleCanvasTap(e) {
e.preventDefault();
let touchX = e.clientX || (e.touches && e.touches[0].clientX);
let touchY = e.clientY || (e.touches && e.touches[0].clientY);
if (!touchX || touchY > canvas.height - 100 && touchX < 150 || touchX > canvas.width - 100) {
return; // Ignore taps inside UI button clusters
}
// Convert screen coordinate to World coordinate
const worldX = touchX + camera.x;
const worldY = touchY + camera.y;
// Convert World coordinate to Grid space
const gridX = Math.floor(worldX / BLOCK_SIZE);
const gridY = Math.floor(worldY / BLOCK_SIZE);
// Out of bounds safety check
if (gridX >= 0 && gridX < WORLD_WIDTH && gridY >= 0 && gridY < WORLD_HEIGHT) {
if (interactionMode === 'DIG') {
world[gridX][gridY] = 0; // Turn to Air
} else if (interactionMode === 'PLACE' && world[gridX][gridY] === 0) {
// Check distance to player so they don't build infinitely away
const dist = Math.hypot(gridX - Math.floor(player.x/BLOCK_SIZE), gridY - Math.floor(player.y/BLOCK_SIZE));
if (dist < 6) {
world[gridX][gridY] = currentBuildID;
}
}
}
}
// Collision Check Helper
function getBlockAt(pixelX, pixelY) {
const gx = Math.floor(pixelX / BLOCK_SIZE);
const gy = Math.floor(pixelY / BLOCK_SIZE);
if (gx < 0 || gx >= WORLD_WIDTH || gy < 0 || gy >= WORLD_HEIGHT) return 3; // treat border out of bounds as solid stone
return world[gx][gy];
}
// Game Update Loop
function update() {
// Apply Horizontal Movement
if (input.left) player.vx = -player.speed;
else if (input.right) player.vx = player.speed;
else player.vx = 0;
// Apply Gravity
player.vy += 0.4; // gravity constant
// Predict X movement & resolve collisions
player.x += player.vx;
if (getBlockAt(player.x, player.y) !== 0 || getBlockAt(player.x + player.width, player.y) !== 0 ||
getBlockAt(player.x, player.y + player.height - 2) !== 0 || getBlockAt(player.x + player.width, player.y + player.height - 2) !== 0) {
// If hitting a block laterally, undo movement step
player.x -= player.vx;
}
// Predict Y movement & resolve collisions
player.grounded = false;
player.y += player.vy;
if (player.vy >= 0) { // Moving Downward
if (getBlockAt(player.x + 4, player.y + player.height) !== 0 || getBlockAt(player.x + player.width - 4, player.y + player.height) !== 0) {
player.y = Math.floor(player.y / BLOCK_SIZE) * BLOCK_SIZE;
player.vy = 0;
player.grounded = true;
}
} else { // Moving Upward
if (getBlockAt(player.x + 4, player.y) !== 0 || getBlockAt(player.x + player.width - 4, player.y) !== 0) {
player.vy = 0;
player.y = Math.ceil(player.y / BLOCK_SIZE) * BLOCK_SIZE;
}
}
// Camera Tracking Player smoothly
camera.x = player.x - canvas.width / 2;
camera.y = player.y - canvas.height / 2;
// Camera limits
camera.x = Math.max(0, Math.min(camera.x, WORLD_WIDTH * BLOCK_SIZE - canvas.width));
camera.y = Math.max(0, Math.min(camera.y, WORLD_HEIGHT * BLOCK_SIZE - canvas.height));
}
// Render Graphics Loop
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw World Blocks visible on screen
const startX = Math.max(0, Math.floor(camera.x / BLOCK_SIZE));
const endX = Math.min(WORLD_WIDTH, Math.ceil((camera.x + canvas.width) / BLOCK_SIZE));
const startY = Math.max(0, Math.floor(camera.y / BLOCK_SIZE));
const endY = Math.min(WORLD_HEIGHT, Math.ceil((camera.y + canvas.height) / BLOCK_SIZE));
for (let x = startX; x < endX; x++) {
for (let y = startY; y < endY; y++) {
const blockID = world[x][y];
if (blockID !== 0) {
ctx.fillStyle = BLOCKS[blockID].color;
ctx.fillRect(x * BLOCK_SIZE - camera.x, y * BLOCK_SIZE - camera.y, BLOCK_SIZE, BLOCK_SIZE);
// Give blocks outline borders
ctx.strokeStyle = 'rgba(0,0,0,0.15)';
ctx.strokeRect(x * BLOCK_SIZE - camera.x, y * BLOCK_SIZE - camera.y, BLOCK_SIZE, BLOCK_SIZE);
}
}
}
// Draw Player Character
ctx.fillStyle = '#FFCC99'; // Skin color
ctx.fillRect(player.x - camera.x, player.y - camera.y, player.width, player.height / 2);
ctx.fillStyle = '#0000FF'; // Blue shirt
ctx.fillRect(player.x - camera.x, player.y + player.height / 2 - camera.y, player.width, player.height / 2);
}
// Standard Main Engine Animation Loop
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
loop();
// Extra mobile touch event blocking to stabilize rendering frames
document.addEventListener('touchmove', (e) => { if(e.scale !== 1) { e.preventDefault(); } }, { passive: false });
</script>
</body>
</html>
1
1
15KB
15KB
155.0ms
204.0ms
156.0ms