gametest / app.py
Aleksmorshen's picture
Update app.py
05aa4d8 verified
import os
import json
import atexit
from flask import Flask, Response, request
from flask_socketio import SocketIO, emit, join_room, leave_room
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'your_default_secret_key')
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
CHUNK_SIZE = 16
WORLD_FILE = 'world_data.json'
server_world = {}
server_players = {}
server_player_count = 0
LAND_HEIGHT = 4
SEA_LEVEL = 2
def get_chunk_key(x, z):
return f"{int(x // CHUNK_SIZE)},{int(z // CHUNK_SIZE)}"
def get_block_server(wx, wy, wz):
chunk_key = get_chunk_key(wx, wz)
chunk = server_world.get(chunk_key)
if not chunk:
return None
lx = int((wx % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE
lz = int((wz % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE
return chunk['blocks'].get(str(lx), {}).get(str(wy), {}).get(str(lz), None)
def set_block_server(wx, wy, wz, block_type):
chunk_key = get_chunk_key(wx, wz)
if chunk_key not in server_world:
server_world[chunk_key] = { 'blocks': {}, 'maxHeight': 0 }
chunk = server_world[chunk_key]
lx = int((wx % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE
lz = int((wz % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE
lx, wy, lz = str(lx), str(wy), str(lz)
if lx not in chunk['blocks']: chunk['blocks'][lx] = {}
if wy not in chunk['blocks'][lx]: chunk['blocks'][lx][wy] = {}
old_type = chunk['blocks'][lx][wy].get(lz)
if block_type is None:
if lz in chunk['blocks'][lx][wy]:
del chunk['blocks'][lx][wy][lz]
if not chunk['blocks'][lx][wy]:
del chunk['blocks'][lx][wy]
if not chunk['blocks'][lx]:
del chunk['blocks'][lx]
else:
chunk['blocks'][lx][wy][lz] = block_type
chunk['maxHeight'] = max(chunk['maxHeight'], int(wy))
return old_type != block_type
def generate_chunk_server(chunkX, chunkZ):
chunk_key = f"{chunkX},{chunkZ}"
if chunk_key in server_world:
return
chunk = {
'blocks': {},
'maxHeight': 0
}
for x in range(CHUNK_SIZE):
x_str = str(x)
chunk['blocks'][x_str] = {}
for z in range(CHUNK_SIZE):
z_str = str(z)
for y in range(LAND_HEIGHT):
y_str = str(y)
if y_str not in chunk['blocks'][x_str]: chunk['blocks'][x_str][y_str] = {}
block_type = 'earth'
if y == LAND_HEIGHT - 1: block_type = 'grass'
chunk['blocks'][x_str][y_str][z_str] = block_type
for y in range(SEA_LEVEL):
y_str = str(y)
world_x = chunkX * CHUNK_SIZE + x
world_z = chunkZ * CHUNK_SIZE + z
ck = get_chunk_key(world_x, world_z)
c = server_world.get(ck, chunk)
lx = int((world_x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE
lz = int((world_z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE
block_exists = c.get('blocks', {}).get(str(lx), {}).get(str(y), {}).get(str(lz))
if block_exists is None:
if y_str not in chunk['blocks'][x_str]: chunk['blocks'][x_str][y_str] = {}
chunk['blocks'][x_str][y_str][z_str] = 'sea'
chunk['maxHeight'] = LAND_HEIGHT - 1
server_world[chunk_key] = chunk
def load_world():
global server_world
if os.path.exists(WORLD_FILE):
try:
with open(WORLD_FILE, 'r') as f:
server_world = json.load(f)
except (json.JSONDecodeError, FileNotFoundError):
server_world = {}
def save_world():
with open(WORLD_FILE, 'w') as f:
json.dump(server_world, f)
@socketio.on('connect')
def handle_connect():
global server_player_count
server_player_count += 1
@socketio.on('disconnect')
def handle_disconnect():
global server_player_count
server_player_count -= 1
if request.sid in server_players:
emit('player_left', {'sid': request.sid}, broadcast=True, include_self=False)
del server_players[request.sid]
@socketio.on('join')
def handle_join(data):
sid = request.sid
nickname = data.get('nickname', f'Player_{sid[:4]}')
initial_pos = {'x': 0, 'y': LAND_HEIGHT * 0.5 + 2.0, 'z': 0}
server_players[sid] = {
'nickname': nickname,
'position': initial_pos,
'rotationY': 0,
'blockType': 'grass'
}
emit('player_joined', {
'sid': sid,
'nickname': nickname,
'position': initial_pos,
'rotationY': 0
}, broadcast=True)
other_players = {s: player for s, player in server_players.items() if s != sid}
emit('current_players', other_players)
initial_chunk_x = int(initial_pos['x'] // CHUNK_SIZE)
initial_chunk_z = int(initial_pos['z'] // CHUNK_SIZE)
chunk_data_to_send = {}
for cx in range(initial_chunk_x - 1, initial_chunk_x + 2):
for cz in range(initial_chunk_z - 1, initial_chunk_z + 2):
key = f"{cx},{cz}"
generate_chunk_server(cx, cz)
if key in server_world:
chunk_data_to_send[key] = server_world[key]
emit('initial_world_data', chunk_data_to_send)
@socketio.on('request_chunk')
def handle_request_chunk(data):
cx = data['chunkX']
cz = data['chunkZ']
key = f"{cx},{cz}"
generate_chunk_server(cx, cz)
if key in server_world:
emit('chunk_data', { 'chunkKey': key, 'chunkData': server_world[key] })
@socketio.on('send_player_state')
def handle_player_state(data):
sid = request.sid
if sid in server_players:
player_data = server_players[sid]
player_data['position'] = data['position']
player_data['rotationY'] = data['rotationY']
emit('player_state_update', {
'sid': sid,
'position': data['position'],
'rotationY': data['rotationY']
}, broadcast=True, include_self=False)
@socketio.on('send_block_change')
def handle_block_change(data):
sid = request.sid
wx = data['wx']
wy = data['wy']
wz = data['wz']
block_type = data['type']
player_pos = server_players[sid]['position']
dist_sq = (player_pos['x'] - wx*1)**2 + (player_pos['y'] - wy*0.5)**2 + (player_pos['z'] - wz*1)**2
if dist_sq > 25*25:
return
effective_wy = wy
if block_type is not None:
chunk_key_at_wx_wz = get_chunk_key(wx, wz)
chunk_at_wx_wz = server_world.get(chunk_key_at_wx_wz)
current_stack_height = 0
if chunk_at_wx_wz:
lx = int((wx % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE
lz = int((wz % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE
for h in range(chunk_at_wx_wz['maxHeight'] + 2, -1, -1):
if str(h) in chunk_at_wx_wz['blocks'].get(str(lx),{}) and str(lz) in chunk_at_wx_wz['blocks'].get(str(lx),{}).get(str(h),{}):
block = chunk_at_wx_wz['blocks'][str(lx)][str(h)][str(lz)]
if block is not None and block != 'sea':
current_stack_height = h + 1
break
effective_wy = max(current_stack_height, SEA_LEVEL if block_type == 'sea' else 0)
if set_block_server(wx, effective_wy, wz, block_type):
chunk_key_changed = get_chunk_key(wx, wz)
emit('block_changed', {
'chunkKey': chunk_key_changed,
'wx': wx,
'wy': effective_wy,
'wz': wz,
'type': block_type
}, broadcast=True)
html_content = """
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Battime - Voxel World Builder</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
background-color: #1a1a1a;
color: #fff;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
}
#game-container {
width: 100vw;
height: 100vh;
position: relative;
overflow: hidden;
cursor: pointer;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
#toolbar {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(40, 40, 40, 0.85);
padding: 10px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
display: flex;
gap: 10px;
z-index: 10;
}
.block-button {
width: 60px;
height: 60px;
border: 2px solid #555;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease-in-out;
background-size: cover;
background-position: center;
image-rendering: pixelated;
}
.block-button:hover {
border-color: #00aaff;
transform: scale(1.1);
}
.block-button.selected {
border-color: #3498db;
transform: scale(1.15);
box-shadow: 0 0 15px #3498db, 0 0 5px #3498db inset;
}
#joystick-container {
position: absolute;
bottom: 30px;
left: 30px;
width: 150px;
height: 150px;
display: none;
z-index: 10;
}
#joystick-base {
position: absolute;
width: 150px;
height: 150px;
background: rgba(50, 50, 50, 0.5);
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.3);
}
#joystick-handle {
position: absolute;
width: 70px;
height: 70px;
background: rgba(100, 100, 100, 0.8);
border-radius: 50%;
left: 40px;
top: 40px;
}
#info-panel {
position: absolute;
top: 20px;
right: 20px;
background-color: rgba(40, 40, 40, 0.8);
padding: 10px 15px;
border-radius: 8px;
font-size: 14px;
text-align: right;
z-index: 10;
pointer-events: auto;
}
.info-button {
background-color: #3498db;
color: white;
padding: 5px 10px;
border-radius: 5px;
cursor: pointer;
margin-top: 10px;
text-align: center;
transition: background-color 0.2s;
display: inline-block;
}
.info-button:hover {
background-color: #2980b9;
}
#custom-block-modal {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #2c2c2c;
padding: 25px;
border-radius: 15px;
z-index: 100;
box-shadow: 0 10px 30px rgba(0,0,0,0.7);
border: 1px solid rgba(255,255,255,0.2);
width: 300px;
}
#custom-block-modal h3 { margin-top: 0; text-align: center; }
#custom-block-modal .form-group { margin-bottom: 15px; }
#custom-block-modal label { display: block; margin-bottom: 5px; }
#custom-block-modal input[type="text"], #custom-block-modal input[type="file"] {
width: 100%;
padding: 8px;
border-radius: 5px;
border: 1px solid #555;
background-color: #444;
color: #fff;
box-sizing: border-box;
}
#custom-block-modal button {
width: 100%;
padding: 10px;
border: none;
border-radius: 5px;
background-color: #3498db;
color: white;
cursor: pointer;
font-size: 16px;
}
#custom-block-modal button:hover { background-color: #2980b9; }
#nickname-modal {
display: flex;
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background-color: rgba(0,0,0,0.8);
z-index: 101;
align-items: center;
justify-content: center;
flex-direction: column;
}
#nickname-modal h2 { color: white; }
#nickname-modal input {
padding: 10px;
margin: 10px 0;
border-radius: 5px;
border: 1px solid #555;
background-color: #444;
color: #fff;
font-size: 16px;
}
#nickname-modal button {
padding: 10px 20px;
border: none;
border-radius: 5px;
background-color: #3498db;
color: white;
cursor: pointer;
font-size: 16px;
}
#nickname-modal button:hover {
background-color: #2980b9;
}
#modal-backdrop {
display: none;
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background-color: rgba(0,0,0,0.6);
z-index: 99;
}
.player-name-label {
position: absolute;
top: 0; left: 0;
color: white;
font-size: 12px;
text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
pointer-events: none;
transform: translate(-50%, -100%);
white-space: nowrap;
}
#crosshair {
position: absolute;
top: 50%;
left: 50%;
width: 4px;
height: 4px;
background-color: rgba(255, 255, 255, 0.7);
border-radius: 50%;
transform: translate(-50%, -50%);
z-index: 10;
pointer-events: none;
}
</style>
</head>
<body>
<div id="nickname-modal">
<h2>Выберите никнейм</h2>
<input type="text" id="nickname-input" placeholder="Введите ваш никнейм" maxlength="15">
<button id="join-button">Присоединиться</button>
</div>
<div id="game-container">
<div id="crosshair"></div>
<div id="toolbar">
<div class="block-button" id="grass" title="Трава"></div>
<div class="block-button" id="earth" title="Земля"></div>
<div class="block-button" id="sea" title="Море"></div>
<div class="block-button" id="remove" title="Удалить блок"></div>
<div class="block-button" id="add-custom-block" title="Создать свой блок" style="font-size: 40px; text-align: center; line-height: 55px;">+</div>
</div>
<div id="info-panel">
<div>Battime Engine v4.3</div>
<div>Управление: WASD/Мышь</div>
<div>На мобильных: Джойстик/Тап</div>
<input type="file" id="face-upload" accept="image/png" style="display: none;">
<label for="face-upload" class="info-button">Загрузить лицо</label>
</div>
<div id="joystick-container">
<div id="joystick-base"></div>
<div id="joystick-handle"></div>
</div>
</div>
<div id="modal-backdrop"></div>
<div id="custom-block-modal">
<h3>Создать новый блок</h3>
<div class="form-group">
<label for="block-name">Имя блока (англ.)</label>
<input type="text" id="block-name" placeholder="my_awesome_block">
</div>
<div class="form-group">
<label for="texture-upload">Текстура (PNG)</label>
<input type="file" id="texture-upload" accept="image/png">
</div>
<div class="form-group">
<label><input type="checkbox" id="block-solid" checked> Твердый блок</label>
</div>
<button id="create-block-btn">Создать</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.umd.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.3.2/socket.io.min.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.128.0/build/three.module.js",
"three/examples/jsm/": "https://unpkg.com/three@0.128.0/examples/jsm/"
}
}
</script>
<script type="module">
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
const socket = io();
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);
scene.fog = new THREE.Fog(0x87ceeb, 50, 150);
const gameContainer = document.getElementById('game-container');
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
gameContainer.appendChild(renderer.domElement);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const hemisphereLight = new THREE.HemisphereLight(0xffffbb, 0x080820, 1.0);
scene.add(hemisphereLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
directionalLight.position.set(50, 50, 25);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.left = -60;
directionalLight.shadow.camera.right = 60;
directionalLight.shadow.camera.top = 60;
directionalLight.shadow.camera.bottom = -60;
directionalLight.shadow.bias = -0.0005;
scene.add(directionalLight);
const renderScene = new RenderPass(scene, camera);
const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.8, 0.4, 0.85);
const composer = new EffectComposer(renderer);
composer.addPass(renderScene);
composer.addPass(bloomPass);
const CHUNK_SIZE = 16;
const RENDER_DISTANCE = 5;
const blockSize = 1;
const blockHeight = 0.5;
const chunks = new Map();
const LAND_HEIGHT = 4;
const SEA_LEVEL = 2;
const textureLoader = new THREE.TextureLoader();
function createTexture(color1, color2) {
const canvas = document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
const context = canvas.getContext('2d');
context.fillStyle = color1;
context.fillRect(0, 0, 64, 64);
context.fillStyle = color2;
for(let i = 0; i < 100; i++) {
const x = Math.random() * 64;
const y = Math.random() * 64;
const size = Math.random() * 3;
context.fillRect(x, y, size, size);
}
context.fillStyle = color1;
context.fillRect(0, 0, 64, 2);
context.fillRect(0, 62, 64, 2);
context.fillRect(0, 0, 2, 64);
context.fillRect(62, 0, 2, 64);
const texture = new THREE.CanvasTexture(canvas);
texture.magFilter = THREE.NearestFilter;
return texture;
}
function createGradientTexture(colorTop, colorBottom) {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 256;
const context = canvas.getContext('2d');
const gradient = context.createLinearGradient(0, 0, 0, 256);
gradient.addColorStop(0, colorTop);
gradient.addColorStop(1, colorBottom);
context.fillStyle = gradient;
context.fillRect(0, 0, 1, 256);
const texture = new THREE.CanvasTexture(canvas);
texture.magFilter = THREE.LinearFilter;
return texture;
}
function createSkybox() {
const skyGeometry = new THREE.SphereGeometry(500, 32, 15);
const skyMaterial = new THREE.MeshBasicMaterial({
map: createGradientTexture('#87ceeb', '#ffffff'),
side: THREE.BackSide,
});
const skybox = new THREE.Mesh(skyGeometry, skyMaterial);
scene.add(skybox);
}
const baseTextures = {
grass: createTexture('#4a7d4a', '#6b8e23'),
earth: createTexture('#7a5a4a', '#8d6e63'),
sea: createTexture('#1e88e5', '#42a5f5'),
player_skin: createTexture('#f0c8a2', '#e0b892'),
player_face: createTexture('#212121', '#ffeb3b'),
};
const blockTypes = {
grass: { solid: true, texture: baseTextures.grass },
earth: { solid: true, texture: baseTextures.earth },
sea: { solid: false, texture: baseTextures.sea, opacity: 0.8 }
};
const blockGeometries = {};
const blockMaterials = {};
function initializeBlockAssets() {
for (const type in blockTypes) {
blockGeometries[type] = new THREE.BoxGeometry(blockSize, blockHeight, blockSize);
const info = blockTypes[type];
if (info.solid) {
blockMaterials[type] = new THREE.MeshStandardMaterial({
map: info.texture,
roughness: 0.8,
metalness: 0.1
});
} else {
blockMaterials[type] = new THREE.MeshLambertMaterial({
map: info.texture,
transparent: true,
opacity: info.opacity || 1.0
});
blockMaterials[type].alphaTest = 0.01;
}
}
document.getElementById('grass').style.backgroundImage = `url(${baseTextures.grass.image.toDataURL()})`;
document.getElementById('earth').style.backgroundImage = `url(${baseTextures.earth.image.toDataURL()})`;
document.getElementById('sea').style.backgroundImage = `url(${baseTextures.sea.image.toDataURL()})`;
document.getElementById('remove').style.backgroundImage = `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="red"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>')`;
}
initializeBlockAssets();
createSkybox();
function createPlayerAvatar() {
const playerGroup = new THREE.Group();
const headSize = 0.5;
const bodyWidth = 0.5;
const bodyHeight = 0.6;
const limbSize = 0.2;
const skinMaterial = new THREE.MeshStandardMaterial({ map: baseTextures.player_skin, roughness: 0.8, metalness: 0.1 });
const faceMaterial = new THREE.MeshStandardMaterial({ map: baseTextures.player_face, roughness: 0.8, metalness: 0.1 });
const headMaterials = [ skinMaterial, skinMaterial, skinMaterial, skinMaterial, faceMaterial, skinMaterial ];
const head = new THREE.Mesh(new THREE.BoxGeometry(headSize, headSize, headSize), headMaterials);
head.position.y = bodyHeight / 2 + headSize / 2;
const torso = new THREE.Mesh(new THREE.BoxGeometry(bodyWidth, bodyHeight, 0.3), skinMaterial);
const armGeo = new THREE.BoxGeometry(limbSize, bodyHeight * 0.9, limbSize);
const leftArm = new THREE.Mesh(armGeo, skinMaterial);
const rightArm = new THREE.Mesh(armGeo, skinMaterial);
leftArm.position.set(-bodyWidth/2 - limbSize/2, -0.05, 0);
rightArm.position.set(bodyWidth/2 + limbSize/2, -0.05, 0);
const legGeo = new THREE.BoxGeometry(limbSize, bodyHeight * 0.95, limbSize);
const leftLeg = new THREE.Mesh(legGeo, skinMaterial);
const rightLeg = new THREE.Mesh(legGeo, skinMaterial);
leftLeg.position.set(-bodyWidth/4, -bodyHeight/2 - (bodyHeight * 0.95)/2, 0);
rightLeg.position.set(bodyWidth/4, -bodyHeight/2 - (bodyHeight * 0.95)/2, 0);
playerGroup.add(head, torso, leftArm, rightArm, leftLeg, rightLeg);
playerGroup.castShadow = true;
playerGroup.children.forEach(c => { c.castShadow = true; c.receiveShadow = true; });
playerGroup.userData = { head, leftArm, rightArm, leftLeg, rightLeg, skinMaterial, faceMaterial, bodyHeight, headSize, bodyWidth };
return playerGroup;
}
const player = createPlayerAvatar();
scene.add(player);
const cameraPivot = new THREE.Object3D();
player.add(cameraPivot);
cameraPivot.add(camera);
camera.position.set(0, 1.8, 8);
camera.lookAt(player.position.clone().add(new THREE.Vector3(0, 1.5, 0)));
const otherPlayers = {};
let currentBlockType = 'grass';
const keys = {};
let localPlayerId = null;
const playerVelocity = new THREE.Vector3();
const playerMoveDirection = new THREE.Vector3();
const playerOnGround = false;
const playerSpeed = 4.0;
const gravity = -20;
const playerWidth = 0.6;
const playerHeight = 1.8;
function getChunkKey(x, z) {
return `${Math.floor(x / CHUNK_SIZE)},${Math.floor(z / CHUNK_SIZE)}`;
}
function getBlockClient(wx, wy, wz) {
const chunkKey = getChunkKey(wx, wz);
const chunk = chunks.get(chunkKey);
if (!chunk) return null;
const lx = ((wx % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
const lz = ((wz % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
return chunk.blocks?.[lx]?.[wy]?.[lz];
}
function isBlockSolidClient(wx, wy, wz) {
const type = getBlockClient(wx, wy, wz);
return type !== null && blockTypes[type]?.solid === true;
}
function updateChunkMesh(chunk) {
for (const type in chunk.instancedMeshes) {
const mesh = chunk.instancedMeshes[type];
scene.remove(mesh);
mesh.geometry.dispose();
if (mesh.material.map) mesh.material.map.dispose();
mesh.material.dispose();
}
chunk.instancedMeshes = {};
const instances = {};
for (const type in blockTypes) { instances[type] = []; }
const dummy = new THREE.Object3D();
for (const x_str in chunk.blocks) {
const x = parseInt(x_str, 10);
for (const y_str in chunk.blocks[x_str]) {
const y = parseInt(y_str, 10);
for (const z_str in chunk.blocks[x_str][y_str]) {
const z = parseInt(z_str, 10);
const type = chunk.blocks[x_str][y_str][z_str];
if (type && blockTypes[type] && blockGeometries[type] && blockMaterials[type]) {
const wx = chunk.x * CHUNK_SIZE + x;
const wz = chunk.z * CHUNK_SIZE + z;
dummy.position.set(wx * blockSize, y * blockHeight, wz * blockSize);
dummy.updateMatrix();
instances[type].push(dummy.matrix.clone());
}
}
}
}
for (const type in instances) {
const data = instances[type];
if (data.length > 0) {
const mesh = new THREE.InstancedMesh(blockGeometries[type], blockMaterials[type], data.length);
for (let i = 0; i < data.length; i++) { mesh.setMatrixAt(i, data[i]); }
mesh.instanceMatrix.needsUpdate = true;
if (blockTypes[type]?.solid) { mesh.castShadow = true; mesh.receiveShadow = true; }
chunk.instancedMeshes[type] = mesh;
scene.add(mesh);
}
}
}
function clientSetBlock(wx, wy, wz, type) {
const chunkKey = getChunkKey(wx, wz);
const chunk = chunks.get(chunkKey);
if (!chunk || !chunk.blocks) return false;
const lx = ((wx % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
const lz = ((wz % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
if (!chunk.blocks[lx]) chunk.blocks[lx] = {};
if (!chunk.blocks[lx][wy]) chunk.blocks[lx][wy] = {};
const oldType = chunk.blocks[lx][wy][lz];
if (type === null) {
if (lz in chunk.blocks[lx][wy]) {
delete chunk.blocks[lx][wy][lz];
if (Object.keys(chunk.blocks[lx][wy]).length === 0) {
delete chunk.blocks[lx][wy];
if (Object.keys(chunk.blocks[lx]).length === 0) delete chunk.blocks[lx];
}
}
} else {
chunk.blocks[lx][wy][lz] = type;
chunk.maxHeight = Math.max(chunk.maxHeight, wy);
}
if (oldType !== type) {
updateChunkMesh(chunk);
return true;
}
return false;
}
let lastPlayerChunkKey = '';
let requestedChunks = new Set();
function updateVisibleChunks() {
if (!player || !player.position) return;
const playerChunkX = Math.floor(player.position.x / blockSize / CHUNK_SIZE);
const playerChunkZ = Math.floor(player.position.z / blockSize / CHUNK_SIZE);
const visibleChunksKeys = new Set();
for (let x = playerChunkX - RENDER_DISTANCE; x <= playerChunkX + RENDER_DISTANCE; x++) {
for (let z = playerChunkZ - RENDER_DISTANCE; z <= playerChunkZ + RENDER_DISTANCE; z++) {
const key = `${x},${z}`;
visibleChunksKeys.add(key);
if (!chunks.has(key) && !requestedChunks.has(key)) {
requestedChunks.add(key);
socket.emit('request_chunk', { chunkX: x, chunkZ: z });
}
}
}
const chunksToRemove = [];
for (const key of chunks.keys()) {
if (!visibleChunksKeys.has(key)) chunksToRemove.push(key);
}
for (const key of chunksToRemove) {
const chunk = chunks.get(key);
for (const type in chunk.instancedMeshes) {
const mesh = chunk.instancedMeshes[type];
scene.remove(mesh);
mesh.geometry.dispose();
if (mesh.material.map) mesh.material.map.dispose();
mesh.material.dispose();
}
chunks.delete(key);
}
}
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2(0, 0);
function onPointerDown(event) {
if (document.pointerLockElement !== renderer.domElement) return;
if (event.target.tagName !== 'CANVAS') return;
event.preventDefault();
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children.filter(c => c.isInstancedMesh), false);
if (intersects.length > 0) {
const intersection = intersects[0];
const instanceMatrix = new THREE.Matrix4();
intersection.object.getMatrixAt(intersection.instanceId, instanceMatrix);
const worldPos = new THREE.Vector3().setFromMatrixPosition(instanceMatrix);
const clickedBlockGridX = Math.round(worldPos.x / blockSize);
const clickedBlockGridY = Math.round(worldPos.y / blockHeight);
const clickedBlockGridZ = Math.round(worldPos.z / blockSize);
let targetBlockGridX, targetBlockGridY, targetBlockGridZ;
let blockTypeToSend;
if (currentBlockType === 'remove') {
targetBlockGridX = clickedBlockGridX;
targetBlockGridY = clickedBlockGridY;
targetBlockGridZ = clickedBlockGridZ;
blockTypeToSend = null;
} else {
const placePos = worldPos.clone().add(intersection.face.normal.multiplyScalar(0.5));
targetBlockGridX = Math.round(placePos.x / blockSize);
targetBlockGridY = Math.round(placePos.y / blockHeight);
targetBlockGridZ = Math.round(placePos.z / blockSize);
blockTypeToSend = currentBlockType;
}
socket.emit('send_block_change', {
wx: targetBlockGridX, wy: targetBlockGridY, wz: targetBlockGridZ,
type: blockTypeToSend
});
}
}
document.addEventListener('keydown', (event) => keys[event.code] = true);
document.addEventListener('keyup', (event) => keys[event.code] = false);
window.addEventListener('mousedown', onPointerDown);
function updateToolbarListeners() {
document.querySelectorAll('.block-button').forEach(button => {
if (button.dataset.listenerAttached) return;
button.addEventListener('click', (e) => {
e.stopPropagation();
if (button.id === 'add-custom-block') {
document.getElementById('modal-backdrop').style.display = 'block';
document.getElementById('custom-block-modal').style.display = 'block';
return;
}
document.querySelectorAll('.block-button').forEach(btn => btn.classList.remove('selected'));
button.classList.add('selected');
currentBlockType = button.id;
});
button.dataset.listenerAttached = true;
});
}
updateToolbarListeners();
const isTouchDevice = 'ontouchstart' in window;
const joystick = {
active: false,
startPos: { x: 0, y: 0 },
center: { x: 75, y: 75 },
radius: 75,
touchId: null
};
const cameraTouch = {
active: false,
startPos: { x: 0, y: 0 },
touchId: null
};
if (isTouchDevice) {
const joystickContainer = document.getElementById('joystick-container');
const joystickHandle = document.getElementById('joystick-handle');
joystickContainer.style.display = 'block';
gameContainer.addEventListener('touchstart', (e) => {
e.preventDefault();
for (let i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
const target = touch.target;
if ((target === joystickContainer || target === joystickHandle.parentNode) && joystick.touchId === null) {
joystick.active = true;
joystick.touchId = touch.identifier;
joystick.startPos = { x: touch.clientX, y: touch.clientY };
} else if (cameraTouch.touchId === null) {
cameraTouch.active = true;
cameraTouch.touchId = touch.identifier;
cameraTouch.startPos = { x: touch.clientX, y: touch.clientY };
}
}
}, { passive: false });
gameContainer.addEventListener('touchmove', (e) => {
e.preventDefault();
for (let i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
if (joystick.active && touch.identifier === joystick.touchId) {
let dx = touch.clientX - joystick.startPos.x;
let dy = touch.clientY - joystick.startPos.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const handleDist = Math.min(distance, joystick.radius / 2);
joystickHandle.style.transform = `translate(${handleDist * Math.cos(angle)}px, ${handleDist * Math.sin(angle)}px)`;
playerMoveDirection.set(dx, 0, dy).normalize();
} else if (cameraTouch.active && touch.identifier === cameraTouch.touchId) {
const deltaX = touch.clientX - cameraTouch.startPos.x;
const deltaY = touch.clientY - cameraTouch.startPos.y;
cameraTouch.startPos = { x: touch.clientX, y: touch.clientY };
player.rotation.y -= deltaX * 0.002;
cameraPivot.rotation.x -= deltaY * 0.002;
cameraPivot.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, cameraPivot.rotation.x));
}
}
}, { passive: false });
const onTouchEnd = (e) => {
for (let i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
if (touch.identifier === joystick.touchId) {
joystick.active = false;
joystick.touchId = null;
joystickHandle.style.transform = `translate(0px, 0px)`;
playerMoveDirection.set(0, 0, 0);
} else if (touch.identifier === cameraTouch.touchId) {
cameraTouch.active = false;
cameraTouch.touchId = null;
}
}
};
gameContainer.addEventListener('touchend', onTouchEnd);
gameContainer.addEventListener('touchcancel', onTouchEnd);
} else {
gameContainer.addEventListener('click', () => {
if (document.pointerLockElement !== renderer.domElement) {
renderer.domElement.requestPointerLock();
}
});
document.addEventListener('pointerlockchange', () => {
keys.enabled = document.pointerLockElement === renderer.domElement;
});
document.addEventListener('mousemove', (event) => {
if (document.pointerLockElement !== renderer.domElement) return;
player.rotation.y -= event.movementX * 0.002;
cameraPivot.rotation.x -= event.movementY * 0.002;
cameraPivot.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, cameraPivot.rotation.x));
});
}
function updateCameraFrustum() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
}
document.getElementById('modal-backdrop').addEventListener('click', () => {
document.getElementById('modal-backdrop').style.display = 'none';
document.getElementById('custom-block-modal').style.display = 'none';
});
document.getElementById('create-block-btn').addEventListener('click', () => {
const nameInput = document.getElementById('block-name');
const fileInput = document.getElementById('texture-upload');
const solidCheckbox = document.getElementById('block-solid');
const name = nameInput.value.trim().replace(/[^a-zA-Z0-9_]/g, '');
const file = fileInput.files[0];
if (!name || !file || blockTypes[name]) {
alert('Пожалуйста, введите уникальное имя (только латинские буквы, цифры, _) и выберите PNG файл.');
return;
}
const imageUrl = URL.createObjectURL(file);
textureLoader.load(imageUrl, (newTexture) => {
URL.revokeObjectURL(imageUrl);
newTexture.magFilter = THREE.NearestFilter;
blockTypes[name] = { solid: solidCheckbox.checked, texture: newTexture };
blockGeometries[name] = new THREE.BoxGeometry(blockSize, blockHeight, blockSize);
if (blockTypes[name].solid) {
blockMaterials[name] = new THREE.MeshStandardMaterial({ map: newTexture, roughness: 0.8, metalness: 0.1 });
} else {
blockMaterials[name] = new THREE.MeshLambertMaterial({ map: newTexture, transparent: true, opacity: 0.8 });
blockMaterials[name].alphaTest = 0.01;
}
const newButton = document.createElement('div');
newButton.className = 'block-button';
newButton.id = name; newButton.title = name;
newButton.style.backgroundImage = `url(${imageUrl})`;
const addButton = document.getElementById('add-custom-block');
document.getElementById('toolbar').insertBefore(newButton, addButton);
updateToolbarListeners();
newButton.click();
document.getElementById('modal-backdrop').style.display = 'none';
document.getElementById('custom-block-modal').style.display = 'none';
nameInput.value = ''; fileInput.value = '';
});
});
document.getElementById('face-upload').addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
const imageUrl = URL.createObjectURL(file);
textureLoader.load(imageUrl, (newFaceTexture) => {
URL.revokeObjectURL(imageUrl);
newFaceTexture.magFilter = THREE.NearestFilter;
player.userData.faceMaterial.map = newFaceTexture;
player.userData.faceMaterial.needsUpdate = true;
});
});
document.getElementById('join-button').addEventListener('click', () => {
const nickname = document.getElementById('nickname-input').value.trim();
if (nickname) {
document.getElementById('nickname-modal').style.display = 'none';
socket.emit('join', { nickname: nickname });
} else {
alert('Пожалуйста, введите никнейм.');
}
});
window.addEventListener('resize', () => {
updateCameraFrustum();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
});
function checkCollision(pos) {
const playerGridX = Math.floor(pos.x / blockSize);
const playerGridY = Math.floor((pos.y - 0.1) / blockHeight);
const playerGridZ = Math.floor(pos.z / blockSize);
return isBlockSolidClient(playerGridX, playerGridY, playerGridZ) || isBlockSolidClient(playerGridX, playerGridY+1, playerGridZ);
}
const clock = new THREE.Clock();
let lastStateUpdateTime = 0;
const stateUpdateInterval = 1000 / 30;
function animate() {
requestAnimationFrame(animate);
const delta = Math.min(clock.getDelta(), 0.1);
const time = clock.getElapsedTime();
TWEEN.update();
updateVisibleChunks();
if (!isTouchDevice) {
playerMoveDirection.set(0, 0, 0);
if (keys['KeyW'] || keys['ArrowUp']) playerMoveDirection.z = -1;
if (keys['KeyS'] || keys['ArrowDown']) playerMoveDirection.z = 1;
if (keys['KeyA'] || keys['ArrowLeft']) playerMoveDirection.x = -1;
if (keys['KeyD'] || keys['ArrowRight']) playerMoveDirection.x = 1;
playerMoveDirection.normalize();
}
const isMoving = playerMoveDirection.lengthSq() > 0.01;
const damping = Math.exp(-4 * delta) - 1;
playerVelocity.y += gravity * delta;
let moveVector = playerMoveDirection.clone().applyEuler(player.rotation).multiplyScalar(playerSpeed);
playerVelocity.x += (moveVector.x - playerVelocity.x) * (1-Math.exp(-15*delta));
playerVelocity.z += (moveVector.z - playerVelocity.z) * (1-Math.exp(-15*delta));
let deltaPosition = playerVelocity.clone().multiplyScalar(delta);
let playerOnGround = false;
const halfWidth = playerWidth / 2;
const halfHeight = playerHeight / 2;
const yCheckPos = player.position.y + deltaPosition.y;
const feetGridY = Math.floor((yCheckPos - halfHeight) / blockHeight);
const checkPointsY = [
{x: player.position.x, z: player.position.z},
{x: player.position.x + halfWidth, z: player.position.z + halfWidth},
{x: player.position.x - halfWidth, z: player.position.z + halfWidth},
{x: player.position.x + halfWidth, z: player.position.z - halfWidth},
{x: player.position.x - halfWidth, z: player.position.z - halfWidth},
];
let groundCollision = false;
for(const p of checkPointsY) {
if (isBlockSolidClient(Math.floor(p.x/blockSize), feetGridY, Math.floor(p.z/blockSize))) {
groundCollision = true; break;
}
}
if(groundCollision) {
playerVelocity.y = Math.max(0, playerVelocity.y);
deltaPosition.y = 0;
playerOnGround = true;
player.position.y = (feetGridY + 1) * blockHeight + halfHeight;
}
player.position.add(deltaPosition);
if (isMoving) {
const angle = Math.atan2(playerVelocity.x, playerVelocity.z);
player.children[0].rotation.y = angle;
const swing = Math.sin(time * 15) * 0.7;
player.userData.leftArm.rotation.x = swing;
player.userData.rightArm.rotation.x = -swing;
player.userData.leftLeg.rotation.x = -swing;
player.userData.rightLeg.rotation.x = swing;
} else {
player.userData.leftArm.rotation.x = 0;
player.userData.rightArm.rotation.x = 0;
player.userData.leftLeg.rotation.x = 0;
player.userData.rightLeg.rotation.x = 0;
}
const idealOffset = new THREE.Vector3(0, 1.8, 8);
const idealCamPos = new THREE.Vector3().copy(idealOffset);
idealCamPos.applyQuaternion(cameraPivot.quaternion);
idealCamPos.applyQuaternion(player.quaternion);
idealCamPos.add(player.position);
const camRay = new THREE.Raycaster(player.position, idealCamPos.clone().sub(player.position).normalize(), 0, idealOffset.length());
const camIntersects = camRay.intersectObjects(scene.children.filter(c => c.isInstancedMesh), false);
if (camIntersects.length > 0) {
camera.position.lerp(camIntersects[0].point.add(camIntersects[0].face.normal.multiplyScalar(0.5)), 0.2);
} else {
camera.position.lerp(idealCamPos, 0.1);
}
for (const sid in otherPlayers) {
const otherPlayer = otherPlayers[sid];
otherPlayer.mesh.position.lerp(otherPlayer.targetPos, 0.15);
otherPlayer.mesh.rotation.y = THREE.MathUtils.lerp(otherPlayer.mesh.rotation.y, otherPlayer.targetRot.y, 0.15);
const otherIsMoving = otherPlayer.mesh.position.distanceTo(otherPlayer.targetPos) > 0.1;
const otherAnimSpeed = 15;
if (otherIsMoving) {
const swing = Math.sin(time * otherAnimSpeed + sid.charCodeAt(0)) * 0.7;
otherPlayer.mesh.userData.leftArm.rotation.x = swing;
otherPlayer.mesh.userData.rightArm.rotation.x = -swing;
otherPlayer.mesh.userData.leftLeg.rotation.x = -swing;
otherPlayer.mesh.userData.rightLeg.rotation.x = swing;
} else {
otherPlayer.mesh.userData.leftArm.rotation.x = 0;
otherPlayer.mesh.userData.rightArm.rotation.x = 0;
otherPlayer.mesh.userData.leftLeg.rotation.x = 0;
otherPlayer.mesh.userData.rightLeg.rotation.x = 0;
}
const headWorldPos = otherPlayer.mesh.userData.head.getWorldPosition(new THREE.Vector3());
const screenPosition = headWorldPos.project(camera);
const posX = (screenPosition.x * 0.5 + 0.5) * window.innerWidth;
const posY = (-screenPosition.y * 0.5 + 0.5) * window.innerHeight;
otherPlayer.label.style.left = `${posX}px`;
otherPlayer.label.style.top = `${posY}px`;
otherPlayer.label.style.display = screenPosition.z > 0 && screenPosition.z < 1 ? 'block' : 'none';
}
const now = performance.now();
if ((isMoving || now - lastStateUpdateTime > stateUpdateInterval) && localPlayerId) {
if (player.lastSentPosition === undefined || player.position.distanceTo(player.lastSentPosition) > 0.05 || Math.abs(player.rotation.y - player.lastSentRotationY) > 0.05) {
socket.emit('send_player_state', {
position: player.position, rotationY: player.rotation.y
});
player.lastSentPosition = player.position.clone();
player.lastSentRotationY = player.rotation.y;
lastStateUpdateTime = now;
}
}
if(blockMaterials.sea && blockMaterials.sea.map) {
blockMaterials.sea.map.offset.x = time * 0.02;
blockMaterials.sea.map.offset.y = time * 0.01;
blockMaterials.sea.map.wrapS = THREE.RepeatWrapping;
blockMaterials.sea.map.wrapT = THREE.RepeatWrapping;
}
composer.render();
}
socket.on('connect', () => {});
socket.on('disconnect', () => {
for(const sid in otherPlayers) {
scene.remove(otherPlayers[sid].mesh);
otherPlayers[sid].label.remove();
}
for (const key of chunks.keys()) {
const chunk = chunks.get(key);
for (const type in chunk.instancedMeshes) {
const mesh = chunk.instancedMeshes[type];
scene.remove(mesh);
mesh.geometry.dispose();
if (mesh.material.map) mesh.material.map.dispose();
mesh.material.dispose();
}
}
otherPlayers = {}; chunks.clear(); requestedChunks.clear(); localPlayerId = null;
document.getElementById('nickname-modal').style.display = 'flex';
});
socket.on('player_joined', (data) => {
if (data.sid === socket.id) {
localPlayerId = data.sid;
player.position.copy(data.position);
player.rotation.y = data.rotationY;
} else {
const otherPlayerMesh = createPlayerAvatar();
otherPlayerMesh.position.copy(data.position);
otherPlayerMesh.rotation.y = data.rotationY;
scene.add(otherPlayerMesh);
const nameLabel = document.createElement('div');
nameLabel.className = 'player-name-label';
nameLabel.textContent = data.nickname;
document.body.appendChild(nameLabel);
otherPlayers[data.sid] = {
mesh: otherPlayerMesh, label: nameLabel, nickname: data.nickname,
targetPos: new THREE.Vector3().copy(data.position),
targetRot: new THREE.Euler(0, data.rotationY, 0)
};
}
});
socket.on('player_left', (data) => {
if (otherPlayers[data.sid]) {
scene.remove(otherPlayers[data.sid].mesh);
otherPlayers[data.sid].label.remove();
delete otherPlayers[data.sid];
}
});
socket.on('current_players', (data) => {
for (const sid in data) {
if (sid !== socket.id) {
const playerData = data[sid];
const otherPlayerMesh = createPlayerAvatar();
otherPlayerMesh.position.copy(playerData.position);
otherPlayerMesh.rotation.y = playerData.rotationY;
scene.add(otherPlayerMesh);
const nameLabel = document.createElement('div');
nameLabel.className = 'player-name-label';
nameLabel.textContent = playerData.nickname;
document.body.appendChild(nameLabel);
otherPlayers[sid] = {
mesh: otherPlayerMesh, label: nameLabel, nickname: playerData.nickname,
targetPos: new THREE.Vector3().copy(playerData.position),
targetRot: new THREE.Euler(0, playerData.rotationY, 0)
};
}
}
});
socket.on('player_state_update', (data) => {
if (otherPlayers[data.sid]) {
otherPlayers[data.sid].targetPos.copy(data.position);
otherPlayers[data.sid].targetRot.set(0, data.rotationY, 0);
}
});
socket.on('initial_world_data', (data) => {
for (const chunkKey in data) {
const chunkData = data[chunkKey];
const [cx, cz] = chunkKey.split(',').map(Number);
chunks.set(chunkKey, {
key: chunkKey, blocks: chunkData.blocks || {}, maxHeight: chunkData.maxHeight || 0,
instancedMeshes: {}, x: cx, z: cz
});
updateChunkMesh(chunks.get(chunkKey));
requestedChunks.delete(chunkKey);
}
});
socket.on('chunk_data', (data) => {
const chunkKey = data.chunkKey;
const chunkData = data.chunkData;
if (!chunks.has(chunkKey)) {
const [cx, cz] = chunkKey.split(',').map(Number);
chunks.set(chunkKey, {
key: chunkKey, blocks: chunkData.blocks || {}, maxHeight: chunkData.maxHeight || 0,
instancedMeshes: {}, x: cx, z: cz
});
updateChunkMesh(chunks.get(chunkKey));
}
requestedChunks.delete(chunkKey);
});
socket.on('block_changed', (data) => {
clientSetBlock(data.wx, data.wy, data.wz, data.type);
});
document.getElementById('grass').classList.add('selected');
updateCameraFrustum();
animate();
</script>
</body>
</html>
"""
@app.route('/')
def index():
return Response(html_content, mimetype='text/html')
if __name__ == '__main__':
load_world()
atexit.register(save_world)
if not server_world:
for x in range(-1, 2):
for z in range(-1, 2):
generate_chunk_server(x, z)
port = int(os.environ.get('PORT', 7860))
socketio.run(app, host='0.0.0.0', port=port, debug=False, allow_unsafe_werkzeug=True)