SandWorld FPS
by TurboMeteor43875 lines30.1 KB
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>SandWorld FPS</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; user-select: none; -webkit-touch-callout: none; }
body {
background: #0a0a1a;
overflow: hidden;
touch-action: none;
font-family: Arial, sans-serif;
position: fixed;
width: 100%;
height: 100%;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
image-rendering: pixelated;
cursor: crosshair;
touch-action: none;
}
#crosshair {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 5;
pointer-events: none;
color: rgba(255,255,255,0.8);
font-size: 28px;
font-family: monospace;
text-shadow: 0 0 6px rgba(0,0,0,0.9);
}
#info {
position: fixed;
top: 10px;
left: 10px;
color: #fff;
font-size: 12px;
background: rgba(0,0,0,0.7);
padding: 6px 12px;
border-radius: 8px;
z-index: 10;
pointer-events: none;
font-family: monospace;
border: 1px solid rgba(255,255,255,0.1);
}
#guide {
position: fixed;
bottom: 120px;
left: 50%;
transform: translateX(-50%);
color: #ff0;
font-size: 14px;
background: rgba(0,0,0,0.8);
padding: 8px 18px;
border-radius: 12px;
z-index: 10;
text-align: center;
max-width: 92%;
pointer-events: none;
transition: opacity 0.4s;
border: 1px solid rgba(255,255,255,0.1);
font-weight: bold;
text-shadow: 0 0 10px rgba(0,0,0,0.5);
}
#hotbar {
position: fixed;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 4px;
background: rgba(0,0,0,0.75);
padding: 6px 10px;
border-radius: 12px;
z-index: 10;
border: 1px solid rgba(255,255,255,0.15);
backdrop-filter: blur(4px);
pointer-events: none;
}
#hotbar > * { pointer-events: auto; }
.slot {
width: 48px;
height: 48px;
background: rgba(255,255,255,0.08);
border: 2px solid rgba(255,255,255,0.15);
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 8px;
color: #fff;
cursor: pointer;
position: relative;
transition: all 0.1s;
touch-action: manipulation;
}
.slot.active {
border-color: #ffd740;
background: rgba(255,215,64,0.2);
box-shadow: 0 0 15px rgba(255,215,64,0.2);
}
.slot .count {
font-size: 10px;
color: #ffd740;
font-weight: bold;
}
.slot .name {
font-size: 7px;
color: #aaa;
text-align: center;
line-height: 1.1;
}
.slot:active { transform: scale(0.9); }
/* Мобильные контроллеры */
#mobileControls {
position: fixed;
bottom: 0;
left: 0;
right: 0;
top: 0;
z-index: 8;
pointer-events: none;
display: none;
}
#mobileControls.active {
display: block;
}
#mobileControls > * {
pointer-events: auto;
}
#leftJoystick {
position: absolute;
bottom: 90px;
left: 30px;
width: 120px;
height: 120px;
border-radius: 60px;
background: rgba(255,255,255,0.12);
border: 2px solid rgba(255,255,255,0.2);
backdrop-filter: blur(4px);
touch-action: none;
}
#leftJoystick .knob {
position: absolute;
top: 50%;
left: 50%;
width: 50px;
height: 50px;
border-radius: 25px;
background: rgba(255,255,255,0.35);
transform: translate(-50%, -50%);
border: 2px solid rgba(255,255,255,0.3);
transition: none;
}
#rightJoystick {
position: absolute;
bottom: 90px;
right: 30px;
width: 120px;
height: 120px;
border-radius: 60px;
background: rgba(255,255,255,0.12);
border: 2px solid rgba(255,255,255,0.2);
backdrop-filter: blur(4px);
touch-action: none;
}
#rightJoystick .knob {
position: absolute;
top: 50%;
left: 50%;
width: 50px;
height: 50px;
border-radius: 25px;
background: rgba(255,255,255,0.35);
transform: translate(-50%, -50%);
border: 2px solid rgba(255,255,255,0.3);
transition: none;
}
.action-btn {
position: absolute;
width: 64px;
height: 64px;
border-radius: 50%;
border: 2px solid rgba(255,255,255,0.3);
background: rgba(255,255,255,0.15);
backdrop-filter: blur(4px);
color: #fff;
font-size: 22px;
display: flex;
align-items: center;
justify-content: center;
touch-action: manipulation;
transition: all 0.1s;
font-weight: bold;
text-shadow: 0 0 10px rgba(0,0,0,0.5);
}
.action-btn:active {
transform: scale(0.85);
background: rgba(255,255,255,0.3);
}
#btnJump {
position: absolute;
bottom: 220px;
right: 20px;
background: rgba(76,175,80,0.3);
border-color: rgba(76,175,80,0.5);
font-size: 18px;
}
#btnBreak {
position: absolute;
bottom: 130px;
left: 180px;
background: rgba(244,67,54,0.3);
border-color: rgba(244,67,54,0.5);
width: 72px;
height: 72px;
font-size: 24px;
}
#btnPlace {
position: absolute;
bottom: 130px;
left: 270px;
background: rgba(33,150,243,0.3);
border-color: rgba(33,150,243,0.5);
width: 72px;
height: 72px;
font-size: 24px;
}
@media (max-width: 700px) {
#leftJoystick { width: 100px; height: 100px; bottom: 80px; left: 20px; }
#leftJoystick .knob { width: 40px; height: 40px; }
#rightJoystick { width: 100px; height: 100px; bottom: 80px; right: 20px; }
#rightJoystick .knob { width: 40px; height: 40px; }
.action-btn { width: 54px; height: 54px; font-size: 18px; }
#btnJump { width: 60px; height: 60px; font-size: 16px; bottom: 200px; right: 15px; }
#btnBreak { width: 60px; height: 60px; font-size: 20px; bottom: 110px; left: 140px; }
#btnPlace { width: 60px; height: 60px; font-size: 20px; bottom: 110px; left: 215px; }
.slot { width: 40px; height: 40px; font-size: 7px; }
#info { font-size: 10px; padding: 4px 8px; }
#guide { font-size: 11px; bottom: 100px; padding: 6px 12px; }
}
@media (max-width: 450px) {
#leftJoystick { width: 80px; height: 80px; bottom: 70px; left: 12px; }
#leftJoystick .knob { width: 32px; height: 32px; }
#rightJoystick { width: 80px; height: 80px; bottom: 70px; right: 12px; }
#rightJoystick .knob { width: 32px; height: 32px; }
.action-btn { width: 46px; height: 46px; font-size: 14px; }
#btnJump { width: 50px; height: 50px; font-size: 14px; bottom: 170px; right: 10px; }
#btnBreak { width: 50px; height: 50px; font-size: 16px; bottom: 90px; left: 110px; }
#btnPlace { width: 50px; height: 50px; font-size: 16px; bottom: 90px; left: 175px; }
.slot { width: 34px; height: 34px; font-size: 6px; }
#hotbar { padding: 4px 6px; gap: 3px; bottom: 6px; }
}
</style>
</head>
<body>
<div id="info">🏔️ <span id="blockCount">0</span> | 🎯 <span id="targetInfo">-</span></div>
<div id="crosshair">✚</div>
<div id="guide">💡 Кликни по экрану → управление мышью | WASD — движение</div>
<canvas id="game"></canvas>
<div id="hotbar"></div>
<!-- Мобильные контроллеры -->
<div id="mobileControls">
<div id="leftJoystick"><div class="knob" id="leftKnob"></div></div>
<div id="rightJoystick"><div class="knob" id="rightKnob"></div></div>
<div class="action-btn" id="btnJump">⬆</div>
<div class="action-btn" id="btnBreak">⛏</div>
<div class="action-btn" id="btnPlace">📦</div>
</div>
<script>
// ============================================================
// SANDWORLD FPS — УДОБНОЕ УПРАВЛЕНИЕ
// ============================================================
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const blockCountEl = document.getElementById('blockCount');
const targetInfoEl = document.getElementById('targetInfo');
const guideEl = document.getElementById('guide');
const hotbarEl = document.getElementById('hotbar');
const mobileControls = document.getElementById('mobileControls');
// ============================================================
// РАЗМЕРЫ
// ============================================================
const TILE = 32;
const WORLD_W = 100;
const WORLD_H = 70;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// Определяем мобильное устройство
const isMobile = /Android|iPhone|iPad|iPod|webOS/i.test(navigator.userAgent) || window.innerWidth < 800;
if (isMobile) {
mobileControls.classList.add('active');
guideEl.textContent = '🎮 Левый джойстик — движение | Правый — взгляд | ⛏ — добыть | 📦 — поставить';
} else {
guideEl.textContent = '💡 Кликни по экрану → блокировка мыши | WASD — движение | Пробел — прыжок';
}
// ============================================================
// ТИПЫ БЛОКОВ
// ============================================================
const BLOCKS = {
AIR: 0, GRASS: 1, DIRT: 2, STONE: 3, WOOD: 4, LEAF: 5,
SAND: 6, PLANKS: 7, COBBLESTONE: 8, GLASS: 9, BRICK: 10,
IRON_ORE: 11, GOLD_ORE: 12, CHEST: 13, CRAFTING_TABLE: 14,
TORCH: 15, WATER: 16, SNOW: 17, ICE: 18
};
const BLOCK_NAMES = {
0: 'Воздух', 1: 'Трава', 2: 'Земля', 3: 'Камень',
4: 'Древесина', 5: 'Листва', 6: 'Песок', 7: 'Доски',
8: 'Булыжник', 9: 'Стекло', 10: 'Кирпич',
11: 'Железная руда', 12: 'Золотая руда', 13: 'Сундук',
14: 'Верстак', 15: 'Факел', 16: 'Вода', 17: 'Снег', 18: 'Лёд'
};
const BLOCK_COLORS = {
0: null, 1: '#5D8A3C', 2: '#8D6E63', 3: '#78909C',
4: '#6D4C41', 5: '#2E7D32', 6: '#FDD835', 7: '#D7A86E',
8: '#9E9E9E', 9: '#B3E5FC', 10: '#A1887F',
11: '#FF8A65', 12: '#FFD54F', 13: '#FFB300',
14: '#795548', 15: '#FFD740', 16: '#42A5F5',
17: '#ECEFF1', 18: '#81D4FA'
};
// ============================================================
// МИР
// ============================================================
let world = [];
let seed = Date.now() % 10000;
function generateWorld() {
world = [];
for (let x = 0; x < WORLD_W; x++) {
world[x] = [];
let height = 25 + Math.floor(6 * Math.sin(x / 12 + seed * 0.1) + 4 * Math.sin(x / 5 + seed * 0.3));
for (let y = 0; y < WORLD_H; y++) {
if (y > height + 12) {
world[x][y] = Math.random() < 0.25 ? BLOCKS.AIR : BLOCKS.STONE;
} else if (y > height) {
world[x][y] = (y < height + 4) ? BLOCKS.DIRT : BLOCKS.STONE;
} else if (y === height) {
world[x][y] = (height > 28) ? BLOCKS.SNOW : BLOCKS.GRASS;
} else if (y > height - 4) {
world[x][y] = BLOCKS.DIRT;
} else {
world[x][y] = BLOCKS.AIR;
}
}
}
for (let x = 4; x < WORLD_W - 4; x += 2 + Math.floor(Math.random() * 4)) {
let y = getSurfaceHeight(x);
if (y > 3 && y < WORLD_H - 10 && Math.random() < 0.5 && getBlock(x, y) === BLOCKS.GRASS) {
let h = 4 + Math.floor(Math.random() * 5);
for (let i = 0; i < h; i++) if (y - i >= 0) world[x][y - i] = BLOCKS.WOOD;
for (let dx = -2; dx <= 2; dx++)
for (let dy = -3; dy <= 1; dy++)
if (Math.abs(dx) + Math.abs(dy) <= 3 && x + dx >= 0 && x + dx < WORLD_W && y - h + dy >= 0)
world[x + dx][y - h + dy] = BLOCKS.LEAF;
}
}
for (let i = 0; i < 40; i++) {
let x = 3 + Math.floor(Math.random() * (WORLD_W - 6));
let y = 3 + Math.floor(Math.random() * (getSurfaceHeight(x) + 12));
if (world[x] && world[x][y] === BLOCKS.STONE) {
world[x][y] = Math.random() < 0.7 ? BLOCKS.IRON_ORE : BLOCKS.GOLD_ORE;
}
}
for (let i = 0; i < 6; i++) {
let cx = 10 + Math.floor(Math.random() * (WORLD_W - 20));
let cy = getSurfaceHeight(cx) - 1;
for (let dx = -2; dx <= 2; dx++)
for (let dy = -1; dy <= 1; dy++)
if (Math.abs(dx) + Math.abs(dy) <= 3 && cx + dx >= 0 && cx + dx < WORLD_W && cy + dy >= 0)
world[cx + dx][cy + dy] = BLOCKS.WATER;
}
}
function getSurfaceHeight(x) {
for (let y = 0; y < WORLD_H; y++) {
let b = getBlock(x, y);
if (b === BLOCKS.GRASS || b === BLOCKS.SNOW || b === BLOCKS.DIRT || b === BLOCKS.STONE) return y;
}
return WORLD_H - 5;
}
function getBlock(x, y) {
if (x < 0 || x >= WORLD_W || y < 0 || y >= WORLD_H) return BLOCKS.AIR;
return world[x][y] || BLOCKS.AIR;
}
function setBlock(x, y, type) {
if (x < 0 || x >= WORLD_W || y < 0 || y >= WORLD_H) return;
world[x][y] = type;
}
function isSolid(x, y) {
let b = getBlock(x, y);
return b !== BLOCKS.AIR && b !== BLOCKS.WATER;
}
// ============================================================
// ИГРОК
// ============================================================
const player = {
x: WORLD_W / 2,
y: 10,
vx: 0,
vy: 0,
speed: 4.0,
jump: 7.0,
gravity: 0.4,
onGround: false,
selectedSlot: 0,
inventory: [],
pitch: 0,
yaw: 0,
handSwing: 0,
isMining: false
};
for (let i = 0; i < 9; i++) {
let types = [BLOCKS.WOOD, BLOCKS.DIRT, BLOCKS.STONE, BLOCKS.PLANKS, BLOCKS.COBBLESTONE, BLOCKS.GRASS, BLOCKS.SAND, BLOCKS.BRICK, BLOCKS.GLASS];
player.inventory.push({ type: types[i % types.length], count: 15 + Math.floor(Math.random() * 10) });
}
// ============================================================
// УПРАВЛЕНИЕ
// ============================================================
const keys = {};
let isPointerLocked = false;
let isMouseDown = false;
let isMouseDownRight = false;
let mouseDX = 0, mouseDY = 0;
// ----- ПК управление -----
canvas.addEventListener('click', () => {
if (!isPointerLocked && !isMobile) {
canvas.requestPointerLock();
}
});
document.addEventListener('pointerlockchange', () => {
isPointerLocked = document.pointerLockElement === canvas;
if (isPointerLocked) guideEl.textContent = '🖱️ WASD — движение | Пробел — прыжок | ЛКМ/ПКМ — добыча/установка';
});
document.addEventListener('mousemove', e => {
if (isPointerLocked) {
mouseDX += e.movementX;
mouseDY += e.movementY;
}
});
document.addEventListener('keydown', e => { keys[e.key.toLowerCase()] = true; });
document.addEventListener('keyup', e => { keys[e.key.toLowerCase()] = false; });
canvas.addEventListener('mousedown', e => {
if (e.button === 0) isMouseDown = true;
if (e.button === 2) isMouseDownRight = true;
});
canvas.addEventListener('mouseup', e => {
if (e.button === 0) isMouseDown = false;
if (e.button === 2) isMouseDownRight = false;
});
canvas.addEventListener('contextmenu', e => e.preventDefault());
// ----- Мобильное управление -----
let leftJoyX = 0, leftJoyY = 0;
let rightJoyX = 0, rightJoyY = 0;
let mobileJump = false, mobileBreak = false, mobilePlace = false;
function setupJoystick(element, knob, callback) {
let rect = element.getBoundingClientRect();
let centerX = rect.width / 2;
let centerY = rect.height / 2;
let maxR = rect.width / 2 - 10;
function handleMove(clientX, clientY) {
let dx = clientX - rect.left - centerX;
let dy = clientY - rect.top - centerY;
let dist = Math.sqrt(dx * dx + dy * dy);
if (dist > maxR) { dx = dx / dist * maxR; dy = dy / dist * maxR; }
knob.style.transform = `translate(${dx}px, ${dy}px)`;
callback(dx / maxR, dy / maxR);
}
function reset() {
knob.style.transform = 'translate(0px, 0px)';
callback(0, 0);
}
element.addEventListener('touchstart', e => {
e.preventDefault();
let t = e.touches[0];
handleMove(t.clientX, t.clientY);
});
element.addEventListener('touchmove', e => {
e.preventDefault();
let t = e.touches[0];
handleMove(t.clientX, t.clientY);
});
element.addEventListener('touchend', e => {
e.preventDefault();
reset();
});
element.addEventListener('touchcancel', e => {
reset();
});
}
setupJoystick(
document.getElementById('leftJoystick'),
document.getElementById('leftKnob'),
(x, y) => { leftJoyX = x; leftJoyY = y; }
);
setupJoystick(
document.getElementById('rightJoystick'),
document.getElementById('rightKnob'),
(x, y) => {
rightJoyX = x;
rightJoyY = y;
// Поворот от правого джойстика
if (Math.abs(x) > 0.1 || Math.abs(y) > 0.1) {
mouseDX += x * 3;
mouseDY += y * 3;
}
}
);
document.getElementById('btnJump').addEventListener('touchstart', e => { e.preventDefault(); mobileJump = true; });
document.getElementById('btnJump').addEventListener('touchend', e => { e.preventDefault(); mobileJump = false; });
document.getElementById('btnBreak').addEventListener('touchstart', e => { e.preventDefault(); mobileBreak = true; });
document.getElementById('btnBreak').addEventListener('touchend', e => { e.preventDefault(); mobileBreak = false; });
document.getElementById('btnPlace').addEventListener('touchstart', e => { e.preventDefault(); mobilePlace = true; });
document.getElementById('btnPlace').addEventListener('touchend', e => { e.preventDefault(); mobilePlace = false; });
// ============================================================
// ИГРОВАЯ ЛОГИКА
// ============================================================
function getTargetBlock() {
let cx = player.x, cy = player.y;
let pitch = player.pitch, yaw = player.yaw;
let dirX = Math.sin(yaw) * Math.cos(pitch);
let dirY = -Math.sin(pitch);
let dirZ = Math.cos(yaw) * Math.cos(pitch);
for (let d = 0; d < 8; d += 0.05) {
let tx = cx + dirX * d;
let ty = cy + dirY * d;
let bx = Math.floor(tx);
let by = Math.floor(ty);
if (isSolid(bx, by)) return { x: bx, y: by, dist: d };
}
return null;
}
function getPlaceBlock() {
let target = getTargetBlock();
if (!target) return null;
let bx = target.x, by = target.y;
let cx = player.x, cy = player.y;
let dx = bx + 0.5 - cx;
let dy = by + 0.5 - cy;
if (Math.abs(dx) > Math.abs(dy)) bx += dx > 0 ? -1 : 1;
else by += dy > 0 ? -1 : 1;
if (isSolid(bx, by)) return null;
return { x: bx, y: by };
}
function updatePlayer(dt) {
let sensitivity = 0.004;
player.yaw += mouseDX * sensitivity;
player.pitch += mouseDY * sensitivity;
player.pitch = Math.max(-1.2, Math.min(1.2, player.pitch));
mouseDX = 0;
mouseDY = 0;
let moveX = 0, moveY = 0;
// ПК управление
if (keys['w']) moveY -= 1;
if (keys['s']) moveY += 1;
if (keys['a']) moveX -= 1;
if (keys['d']) moveX += 1;
// Мобильное управление
if (isMobile) {
if (Math.abs(leftJoyX) > 0.1) moveX = leftJoyX;
if (Math.abs(leftJoyY) > 0.1) moveY = -leftJoyY;
}
let len = Math.sqrt(moveX * moveX + moveY * moveY);
if (len > 1) { moveX /= len; moveY /= len; }
let forward = Math.cos(player.yaw);
let right = Math.sin(player.yaw);
let vx = (moveX * right + moveY * forward) * player.speed;
let vy = (moveX * -forward + moveY * right) * player.speed;
// Прыжок
let jumpPressed = keys[' '] || keys['space'] || mobileJump;
if (jumpPressed && player.onGround) {
player.vy = -player.jump;
player.onGround = false;
}
player.vy += player.gravity;
if (player.vy > 12) player.vy = 12;
// Горизонталь
let newX = player.x + vx * dt;
let bx = Math.floor(newX);
let by = Math.floor(player.y);
if (!isSolid(bx, by) && !isSolid(bx, by + 1)) {
player.x = newX;
}
// Вертикаль
let newY = player.y + player.vy * dt;
let bx2 = Math.floor(player.x);
let by2 = Math.floor(newY);
let by3 = Math.floor(newY + 1);
if (!isSolid(bx2, by2) && !isSolid(bx2, by3)) {
player.y = newY;
player.onGround = false;
} else {
if (player.vy > 0) player.onGround = true;
player.vy = 0;
}
if (player.x < 0.5) player.x = 0.5;
if (player.x > WORLD_W - 0.5) player.x = WORLD_W - 0.5;
if (player.y < 0.5) { player.y = 0.5; player.vy = 0; }
if (player.y > WORLD_H + 10) {
player.y = getSurfaceHeight(Math.floor(player.x)) - 1;
player.vy = 0;
showGuide('Возврат из бездны!');
}
// Анимация руки
let mining = isMouseDown || mobileBreak;
let placing = isMouseDownRight || mobilePlace;
if (mining || placing) {
player.handSwing += dt * 8;
if (player.handSwing > 1) player.handSwing = 1;
} else {
player.handSwing *= 0.95;
if (player.handSwing < 0.01) player.handSwing = 0;
}
// Взаимодействие
interactTimer -= dt;
if (interactTimer <= 0) {
if (mining) {
let target = getTargetBlock();
if (target && target.dist < 6) {
let b = getBlock(target.x, target.y);
if (b !== BLOCKS.AIR && b !== BLOCKS.WATER) {
setBlock(target.x, target.y, BLOCKS.AIR);
addToInventory(b, 1);
showGuide(`Добыто: ${BLOCK_NAMES[b]}`);
interactTimer = 0.15;
player.handSwing = 1;
}
}
}
if (placing) {
let place = getPlaceBlock();
if (place) {
let item = player.inventory[player.selectedSlot];
if (item && item.count > 0 && item.type !== BLOCKS.AIR && item.type !== BLOCKS.WATER) {
setBlock(place.x, place.y, item.type);
item.count--;
if (item.count <= 0) player.inventory[player.selectedSlot] = { type: BLOCKS.AIR, count: 0 };
showGuide(`Установлен: ${BLOCK_NAMES[item.type]}`);
interactTimer = 0.15;
player.handSwing = 0.7;
}
}
}
}
}
let interactTimer = 0;
function addToInventory(type, count) {
for (let i = 0; i < player.inventory.length; i++) {
if (player.inventory[i].type === type) {
player.inventory[i].count += count;
updateHotbar();
return;
}
}
for (let i = 0; i < player.inventory.length; i++) {
if (player.inventory[i].type === BLOCKS.AIR || player.inventory[i].count === 0) {
player.inventory[i] = { type: type, count: count };
updateHotbar();
return;
}
}
}
// ============================================================
// ОТРИСОВКА
// ============================================================
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
let grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
grad.addColorStop(0, '#0a0a2a');
grad.addColorStop(0.3, '#1a237e');
grad.addColorStop(0.6, '#42A5F5');
grad.addColorStop(0.9, '#87CEEB');
grad.addColorStop(1, '#B3E5FC');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
let offsetX = player.x * TILE - canvas.width / 2;
let offsetY = player.y * TILE - canvas.height / 2;
let startX = Math.max(0, Math.floor(offsetX / TILE));
let endX = Math.min(WORLD_W, Math.ceil((offsetX + canvas.width) / TILE) + 1);
let startY = Math.max(0, Math.floor(offsetY / TILE));
let endY = Math.min(WORLD_H, Math.ceil((offsetY + canvas.height) / TILE) + 1);
let total = 0;
for (let x = startX; x < endX; x++) {
for (let y = startY; y < endY; y++) {
let b = getBlock(x, y);
if (b === BLOCKS.AIR) continue;
total++;
let sx = x * TILE - offsetX;
let sy = y * TILE - offsetY;
ctx.fillStyle = BLOCK_COLORS[b] || '#888';
ctx.fillRect(sx, sy, TILE, TILE);
if (b === BLOCKS.GRASS) {
ctx.fillStyle = 'rgba(0,80,0,0.25)';
ctx.fillRect(sx, sy + TILE - 4, TILE, 4);
}
if (b === BLOCKS.SNOW) {
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(sx, sy, TILE, 3);
}
if (b === BLOCKS.WOOD) {
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.fillRect(sx + TILE / 2 - 1, sy, 2, TILE);
}
if (b === BLOCKS.LEAF) {
ctx.fillStyle = 'rgba(0,200,50,0.15)';
ctx.fillRect(sx, sy, TILE, TILE);
}
if (b === BLOCKS.IRON_ORE) {
ctx.fillStyle = 'rgba(255,150,100,0.3)';
ctx.fillRect(sx + 4, sy + 4, 8, 8);
ctx.fillRect(sx + 18, sy + 18, 8, 8);
}
if (b === BLOCKS.GOLD_ORE) {
ctx.fillStyle = 'rgba(255,215,0,0.3)';
ctx.fillRect(sx + 8, sy + 6, 6, 6);
ctx.fillRect(sx + 20, sy + 14, 6, 6);
}
ctx.strokeStyle = 'rgba(0,0,0,0.06)';
ctx.lineWidth = 0.5;
ctx.strokeRect(sx, sy, TILE, TILE);
}
}
blockCountEl.textContent = total;
// Руки
let handX = canvas.width / 2;
let handY = canvas.height - 80;
let swing = player.handSwing * 40;
let rot = player.handSwing * 0.6;
ctx.save();
ctx.translate(handX + 30, handY - 10);
ctx.rotate(rot);
ctx.fillStyle = '#4FC3F7';
ctx.fillRect(0, 0, 60, 14);
ctx.fillRect(50, -4, 16, 22);
ctx.fillStyle = '#795548';
ctx.fillRect(54, -2, 12, 4);
ctx.fillStyle = '#78909C';
ctx.fillRect(62, -8, 6, 16);
ctx.restore();
ctx.save();
ctx.translate(handX - 30, handY - 10);
ctx.rotate(-rot * 0.5);
ctx.fillStyle = '#4FC3F7';
ctx.fillRect(-60, 0, 50, 12);
ctx.restore();
let target = getTargetBlock();
if (target && target.dist < 6) {
targetInfoEl.textContent = BLOCK_NAMES[getBlock(target.x, target.y)] || '?';
} else {
targetInfoEl.textContent = '-';
}
}
// ============================================================
// HOTBAR
// ============================================================
function updateHotbar() {
hotbarEl.innerHTML = '';
for (let i = 0; i < 9; i++) {
let div = document.createElement('div');
div.className = 'slot' + (i === player.selectedSlot ? ' active' : '');
let item = player.inventory[i] || { type: BLOCKS.AIR, count: 0 };
if (item.type !== BLOCKS.AIR && item.count > 0) {
let name = BLOCK_NAMES[item.type] || '?';
div.innerHTML = `<span class="name">${name.substring(0, 4)}</span><span class="count">${item.count}</span>`;
} else {
div.innerHTML = `<span class="name">·</span>`;
}
div.onclick = () => { player.selectedSlot = i; updateHotbar(); };
hotbarEl.appendChild(div);
}
}
// ============================================================
// ПРОВОДНИК
// ============================================================
let guideTimeout = null;
function showGuide(msg) {
guideEl.textContent = '💡 ' + msg;
guideEl.style.opacity = 1;
clearTimeout(guideTimeout);
guideTimeout = setTimeout(() => { guideEl.style.opacity = 0; }, 3000);
}
// ============================================================
// ГЛАВНЫЙ ЦИКЛ
// ============================================================
let lastTime = 0;
function gameLoop(timestamp) {
let dt = Math.min((timestamp - lastTime) / 1000, 0.05);
lastTime = timestamp;
updatePlayer(dt);
draw();
updateHotbar();
requestAnimationFrame(gameLoop);
}
// ============================================================
// ЗАПУСК
// ============================================================
generateWorld();
updateHotbar();
showGuide(isMobile ? '🎮 Джойстики для управления' : '💡 Кликни для блокировки мыши');
requestAnimationFrame(gameLoop);
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
console.log('🎮 SandWorld FPS запущена!');
</script>
</body>
</html>Game Source: SandWorld FPS
Creator: TurboMeteor43
Libraries: none
Complexity: complex (875 lines, 30.1 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: sandworld-fps-turbometeor43" to link back to the original. Then publish at arcadelab.ai/publish.