Interiors Fix Begin
This commit is contained in:
4
.env
4
.env
@@ -1,3 +1,3 @@
|
||||
DATABASE_URL=postgres://my_user:scupAs2s@91.107.120.205:5432/game_db
|
||||
DATABASE_URL=postgres://my_user:scupAs2s@188.120.243.108:5432/game_db
|
||||
JWT_SECRET=tgkkkxd2131
|
||||
DATABASE_URL_VIRTUAL_WORLD=postgres://my_user:scupAs2s@91.107.120.205:5432/virtual_world
|
||||
DATABASE_URL_VIRTUAL_WORLD=postgres://my_user:scupAs2s@188.120.243.108:5432/virtual_world
|
||||
|
||||
17
db.js
17
db.js
@@ -2,9 +2,24 @@
|
||||
require('dotenv').config();
|
||||
const { Pool } = require('pg');
|
||||
|
||||
console.log('Подключение к базе данных:', process.env.DATABASE_URL);
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
ssl: false
|
||||
ssl: false,
|
||||
// Добавляем обработку ошибок подключения
|
||||
connectionTimeoutMillis: 10000,
|
||||
idleTimeoutMillis: 30000,
|
||||
max: 20
|
||||
});
|
||||
|
||||
// Обработка ошибок подключения
|
||||
pool.on('error', (err) => {
|
||||
console.error('Ошибка подключения к базе данных:', err);
|
||||
});
|
||||
|
||||
pool.on('connect', () => {
|
||||
console.log('Успешное подключение к базе данных');
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'threenew-api',
|
||||
name: 'eev-api',
|
||||
script: 'server.js',
|
||||
cwd: '/threenew',
|
||||
env: { NODE_ENV: 'production', PORT: 4000 }
|
||||
},
|
||||
{
|
||||
name: 'threenew-web',
|
||||
script: 'serve',
|
||||
cwd: '/threenew',
|
||||
args: '-s build -l 3000',
|
||||
env: { NODE_ENV: 'production' }
|
||||
cwd: '/three/EEV_Proj',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 4000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -21,11 +21,14 @@
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.6.1",
|
||||
"three": "0.166.1",
|
||||
"wavesurfer.js": "^7.10.1"
|
||||
"wavesurfer.js": "^7.10.1",
|
||||
"concurrently": "^8.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build"
|
||||
"build": "react-scripts build",
|
||||
"server": "node server.js",
|
||||
"dev": "concurrently \"npm run server\" \"npm run start\""
|
||||
},
|
||||
"overrides": {
|
||||
"nth-check": "^2.0.1",
|
||||
|
||||
7
public/packs/citypack.json
Normal file
7
public/packs/citypack.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"baseColor": "/textures/base.png",
|
||||
"normal": "/textures/grid.png",
|
||||
"specular": "/textures/specular.png",
|
||||
"roughness": 0.5,
|
||||
"metalness": 0.1
|
||||
}
|
||||
176
server.js
176
server.js
@@ -4,7 +4,18 @@ try {
|
||||
console.log('dotenv успешно импортирован');
|
||||
} catch (e) {
|
||||
console.error('Ошибка при импорте dotenv:', e);
|
||||
throw e;
|
||||
console.log('Продолжаем без .env файла');
|
||||
}
|
||||
|
||||
// Устанавливаем fallback значения для критических переменных окружения
|
||||
if (!process.env.JWT_SECRET) {
|
||||
process.env.JWT_SECRET = 'fallback-secret-key-for-development';
|
||||
console.warn('JWT_SECRET не найден, используем fallback ключ (НЕ ДЛЯ ПРОДАКШЕНА!)');
|
||||
}
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
process.env.DATABASE_URL = 'postgresql://postgres:password@localhost:5432/revproj';
|
||||
console.warn('DATABASE_URL не найден, используем fallback (НЕ ДЛЯ ПРОДАКШЕНА!)');
|
||||
}
|
||||
try {
|
||||
express = require('express');
|
||||
@@ -51,6 +62,10 @@ try {
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
|
||||
const http = require('http').createServer(app);
|
||||
const io = require('socket.io')(http, {
|
||||
cors: {
|
||||
@@ -520,7 +535,25 @@ app.get('/api/me', authenticate, async (req, res) => {
|
||||
WHERE id = $1
|
||||
`, [userId]);
|
||||
if (!rows.length) return res.status(404).json({ error: 'User not found' });
|
||||
res.json(rows[0]);
|
||||
|
||||
const user = rows[0];
|
||||
|
||||
// Автоматически исправляем неправильный avatarURL
|
||||
if (!user.avatarURL || user.avatarURL === 'try' || user.avatarURL === 'undefined' || user.avatarURL === 'null') {
|
||||
console.log(`Исправляем неправильный avatarURL для пользователя ${userId}: ${user.avatarURL} -> /models/character.glb`);
|
||||
|
||||
try {
|
||||
await db.query(
|
||||
'UPDATE users SET avatar_url = $1 WHERE id = $2',
|
||||
['/models/character.glb', userId]
|
||||
);
|
||||
user.avatarURL = '/models/character.glb';
|
||||
} catch (e) {
|
||||
console.error('Ошибка обновления avatarURL:', e);
|
||||
}
|
||||
}
|
||||
|
||||
res.json(user);
|
||||
});
|
||||
|
||||
app.get('/api/players/:socketId', authenticate, async (req, res) => {
|
||||
@@ -556,40 +589,72 @@ app.get('/api/players/:socketId', authenticate, async (req, res) => {
|
||||
stress_level AS "stressLevel",
|
||||
satiety,
|
||||
thirst,
|
||||
diseases,
|
||||
last_city_id AS "last_city_id",
|
||||
last_pos_x AS "last_pos_x",
|
||||
last_pos_z AS "last_pos_z"
|
||||
diseases
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
`, [dbId]);
|
||||
|
||||
if (!rows.length) return res.status(404).json({ error: 'User not found in database' });
|
||||
res.json(rows[0]);
|
||||
|
||||
const user = rows[0];
|
||||
|
||||
// Автоматически исправляем неправильный avatarURL
|
||||
if (!user.avatarURL || user.avatarURL === 'try' || user.avatarURL === 'undefined' || user.avatarURL === 'null') {
|
||||
console.log(`Исправляем неправильный avatarURL для игрока ${dbId}: ${user.avatarURL} -> /models/character.glb`);
|
||||
|
||||
try {
|
||||
await db.query(
|
||||
'UPDATE users SET avatar_url = $1 WHERE id = $2',
|
||||
['/models/character.glb', dbId]
|
||||
);
|
||||
user.avatarURL = '/models/character.glb';
|
||||
} catch (e) {
|
||||
console.error('Ошибка обновления avatarURL:', e);
|
||||
}
|
||||
}
|
||||
|
||||
res.json(user);
|
||||
});
|
||||
|
||||
app.post('/api/register', async (req, res) => {
|
||||
console.log('register request:');
|
||||
const { email, password, firstName, lastName, gender, age, city, avatarURL } = req.body;
|
||||
const { rowCount } = await db.query(`SELECT 1 FROM users WHERE email = $1`, [email]);
|
||||
if (rowCount) return res.status(400).json({ error: 'Почта уже занята' });
|
||||
try {
|
||||
console.log('register request:', req.body?.email);
|
||||
const { email, password, firstName, lastName, gender, age, city, avatarURL } = req.body || {};
|
||||
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
const insertSQL = `
|
||||
INSERT INTO users(email, password_hash, first_name, last_name, gender, age, city, avatar_url)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8)
|
||||
RETURNING id, email, created_at
|
||||
`;
|
||||
const result = await db.query(insertSQL, [
|
||||
email, hash, firstName, lastName, gender, age, city, avatarURL
|
||||
]);
|
||||
if (!email || !password || !firstName || !lastName) {
|
||||
return res.status(400).json({ error: 'Не заполнены обязательные поля' });
|
||||
}
|
||||
|
||||
const user = result.rows[0];
|
||||
await economy.createAccount(user.id, 'USD');
|
||||
const token = jwt.sign({ id: user.id, email: user.email }, process.env.JWT_SECRET, {
|
||||
expiresIn: '12h'
|
||||
});
|
||||
res.json({ success: true, token });
|
||||
const { rowCount } = await db.query(`SELECT 1 FROM users WHERE email = $1`, [email]);
|
||||
if (rowCount) return res.status(400).json({ error: 'Почта уже занята' });
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
const insertSQL = `
|
||||
INSERT INTO users(email, password_hash, first_name, last_name, gender, age, city, avatar_url)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8)
|
||||
RETURNING id, email, created_at
|
||||
`;
|
||||
const result = await db.query(insertSQL, [
|
||||
email, hash, firstName, lastName, gender ?? null, age ?? null, city ?? null, avatarURL ?? null
|
||||
]);
|
||||
|
||||
const user = result.rows[0];
|
||||
// Не даём регистрации упасть, если экономика не завелась
|
||||
try {
|
||||
await Economy.createAccount(user.id, 'USD');
|
||||
} catch (e) {
|
||||
console.error('Economy.createAccount failed:', e);
|
||||
}
|
||||
|
||||
if (!process.env.JWT_SECRET) {
|
||||
console.error('JWT_SECRET не задан в окружении (.env)');
|
||||
return res.status(500).json({ error: 'Ошибка конфигурации сервера' });
|
||||
}
|
||||
const token = jwt.sign({ id: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '12h' });
|
||||
res.json({ success: true, token });
|
||||
} catch (e) {
|
||||
console.error('Ошибка регистрации:', e);
|
||||
res.status(500).json({ error: 'Внутренняя ошибка регистрации' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/login', async (req, res) => {
|
||||
@@ -640,8 +705,17 @@ app.get('/api/cities/:cityId/objects', authenticate, async (req, res) => {
|
||||
const cityId = req.params.cityId;
|
||||
try {
|
||||
const { rows } = await db.query(`
|
||||
SELECT id, name, model_url, pos_x, pos_y, pos_z, rot_x, rot_y, rot_z, organization_id,
|
||||
COALESCE(collidable, true) AS collidable
|
||||
SELECT id,
|
||||
name,
|
||||
model_url,
|
||||
pos_x, pos_y, pos_z,
|
||||
rot_x, rot_y, rot_z,
|
||||
COALESCE(scale_x, 1) AS scale_x,
|
||||
COALESCE(scale_y, 1) AS scale_y,
|
||||
COALESCE(scale_z, 1) AS scale_z,
|
||||
organization_id,
|
||||
COALESCE(collidable, true) AS collidable,
|
||||
COALESCE(textures, '-') AS textures
|
||||
FROM city_objects
|
||||
WHERE city_id = $1
|
||||
`, [cityId]);
|
||||
@@ -664,6 +738,29 @@ app.get('/api/models', authenticate, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Обновить avatarURL пользователя
|
||||
app.put('/api/profile/avatar', authenticate, async (req, res) => {
|
||||
const { avatarURL } = req.body;
|
||||
const userId = req.user.id;
|
||||
|
||||
try {
|
||||
// Проверяем, что avatarURL не пустой и валидный
|
||||
if (!avatarURL || avatarURL === 'try' || avatarURL === 'undefined' || avatarURL === 'null') {
|
||||
return res.status(400).json({ error: 'Неправильный avatarURL' });
|
||||
}
|
||||
|
||||
await db.query(
|
||||
'UPDATE users SET avatar_url = $1 WHERE id = $2',
|
||||
[avatarURL, userId]
|
||||
);
|
||||
|
||||
res.json({ success: true, avatarURL });
|
||||
} catch (e) {
|
||||
console.error('Ошибка обновления avatarURL:', e);
|
||||
res.status(500).json({ error: 'Ошибка обновления avatarURL' });
|
||||
}
|
||||
});
|
||||
|
||||
// Регистрируем маршрут на старте приложения:
|
||||
app.get(
|
||||
'/api/city_objects/:objectId/interior',
|
||||
@@ -691,12 +788,11 @@ app.post('/api/interiors/:interiorId/enter', authenticate, async (req, res) => {
|
||||
const interiorId = parseInt(req.params.interiorId, 10);
|
||||
try {
|
||||
const interior = (await db.query(
|
||||
'SELECT city_id, spawn_x, spawn_y, spawn_z, spawn_rot, exit_x, exit_y, exit_z, exit_rot FROM interiors WHERE id = $1',
|
||||
[interiorId]
|
||||
)).rows[0];
|
||||
'SELECT spawn_x, spawn_y, spawn_z, spawn_rot, exit_x, exit_y, exit_z, exit_rot FROM interiors WHERE id = $1',
|
||||
[interiorId]
|
||||
)).rows[0];
|
||||
if (!interior) return res.status(404).json({ error: 'Интерьер не найден' });
|
||||
res.json({
|
||||
cityId: interior.city_id || 1,
|
||||
spawn: {
|
||||
x: interior.spawn_x,
|
||||
y: interior.spawn_y,
|
||||
@@ -708,7 +804,7 @@ app.post('/api/interiors/:interiorId/enter', authenticate, async (req, res) => {
|
||||
y: interior.exit_y,
|
||||
z: interior.exit_z,
|
||||
rot: interior.exit_rot
|
||||
},
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -1233,4 +1329,16 @@ async function ensureInteriorsSpawnColumns() {
|
||||
console.error('Ошибка добавления spawn/exit-колонок в interiors', e);
|
||||
}
|
||||
}
|
||||
ensureInteriorsSpawnColumns();
|
||||
ensureInteriorsSpawnColumns();
|
||||
|
||||
// Добавляем колонки масштаба для городских объектов, если ещё не созданы
|
||||
async function ensureCityObjectsScaleColumns() {
|
||||
try {
|
||||
await db.query('ALTER TABLE city_objects ADD COLUMN IF NOT EXISTS scale_x NUMERIC DEFAULT 1');
|
||||
await db.query('ALTER TABLE city_objects ADD COLUMN IF NOT EXISTS scale_y NUMERIC DEFAULT 1');
|
||||
await db.query('ALTER TABLE city_objects ADD COLUMN IF NOT EXISTS scale_z NUMERIC DEFAULT 1');
|
||||
} catch (e) {
|
||||
console.error('Ошибка добавления scale колонок в city_objects', e);
|
||||
}
|
||||
}
|
||||
ensureCityObjectsScaleColumns();
|
||||
1503
src/Game.js
1503
src/Game.js
File diff suppressed because it is too large
Load Diff
@@ -207,7 +207,10 @@ export default function MapEditor() {
|
||||
pos_z: obj.position.z,
|
||||
rot_x: obj.rotation.x,
|
||||
rot_y: obj.rotation.y,
|
||||
rot_z: obj.rotation.z
|
||||
rot_z: obj.rotation.z,
|
||||
scale_x: obj.scale.x,
|
||||
scale_y: obj.scale.y,
|
||||
scale_z: obj.scale.z
|
||||
}));
|
||||
const token = localStorage.getItem('token');
|
||||
fetch('/api/save-map', {
|
||||
|
||||
@@ -1,118 +1,183 @@
|
||||
// src/pages/RegisterStep3.jsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
// данные аватаров
|
||||
const avatars = {
|
||||
// ======= ПРЕСЕТЫ GLB-МОДЕЛЕЙ =======
|
||||
// Поменяй url на свои (например, на хостинге или CDN с включённым CORS).
|
||||
const AVATAR_PRESETS = {
|
||||
male: [
|
||||
{ name:'Мужчина 1', url:'https://models.readyplayer.me/68013c216026f5144dce1613.glb' },
|
||||
{ name:'Мужчина 2', url:'https://models.readyplayer.me/68013cf0647a08a2e39f842d.glb' },
|
||||
{ name: "Astronaut (demo)", url: "https://modelviewer.dev/shared-assets/models/Astronaut.glb" },
|
||||
{ name: "Robot Expressive", url: "https://modelviewer.dev/shared-assets/models/RobotExpressive.glb" },
|
||||
{ name: "Person", url: "https://models.readyplayer.me/68a96d372185159c38c47c19.glb" },
|
||||
],
|
||||
female: [
|
||||
{ name:'Женщина 1', url:'https://models.readyplayer.me/680d174ea4d963314ffdd26d.glb' },
|
||||
{ name:'Женщина 2', url:'https://models.readyplayer.me/680d16d12c0e4a08e3b1de22.glb' },
|
||||
{ name: "Virtual Human 1 (demo)", url: "https://models.readyplayer.me/68a96e884dd25e5878afb28f.glb" }, // при желании замени на свой GLB
|
||||
{ name: "Virtual Human 2 (demo)", url: "https://modelviewer.dev/shared-assets/models/Woman.glb" }, // при желании замени на свой GLB
|
||||
],
|
||||
};
|
||||
|
||||
// простые стили (без внешних css)
|
||||
const styles = {
|
||||
page: { minHeight: "100vh", display: "flex", justifyContent: "center", background: "#0b0f1a", color: "#e9eef5", padding: 24 },
|
||||
card: { width: "min(1000px,96vw)", background: "#111827", border: "1px solid #1f2937", borderRadius: 16, padding: 24 },
|
||||
header: { fontSize: 28, fontWeight: 800, margin: "0 0 20px" },
|
||||
row: { display: "flex", gap: 12, alignItems: "center", marginBottom: 12 },
|
||||
select: { background: "#0f172a", color: "#e5e7eb", border: "1px solid #334155", borderRadius: 10, padding: "10px 12px", outline: "none" },
|
||||
grid: { display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(220px,1fr))", gap: 12, margin: "8px 0 18px" },
|
||||
cardItem: (active) => ({
|
||||
border: `2px solid ${active ? "#22d3ee" : "transparent"}`,
|
||||
background: "#0b1220",
|
||||
borderRadius: 14,
|
||||
padding: 12,
|
||||
cursor: "pointer",
|
||||
}),
|
||||
mvWrap: { width: "100%", aspectRatio: "1 / 1", borderRadius: 10, overflow: "hidden", background: "#0a0f1a", marginBottom: 8 },
|
||||
input: { width: "100%", padding: "10px 12px", borderRadius: 10, border: "1px solid #334155", outline: "none", background: "#0f172a", color: "#e5e7eb" },
|
||||
hint: { fontSize: 13, opacity: 0.8, marginTop: 6 },
|
||||
actions: { display: "flex", gap: 12, marginTop: 10, flexWrap: "wrap" },
|
||||
primaryBtn: { background: "linear-gradient(90deg,#2563eb,#06b6d4)", border: "none", color: "#fff", padding: "12px 18px", fontWeight: 700, borderRadius: 12, cursor: "pointer" },
|
||||
secondaryBtn: { background: "transparent", border: "1px solid #334155", color: "#e5e7eb", padding: "12px 18px", fontWeight: 600, borderRadius: 12, cursor: "pointer" },
|
||||
error: { marginTop: 10, padding: "10px 12px", background: "#7f1d1d", border: "1px solid #ef4444", color: "#fff", borderRadius: 10, whiteSpace: "pre-wrap" },
|
||||
};
|
||||
|
||||
export default function RegisterStep3() {
|
||||
const navigate = useNavigate();
|
||||
const [gender, setGender] = useState('male');
|
||||
const [avatarURL, setAvatarURL] = useState('');
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
// Достаём шаги 1–2
|
||||
const step1 = useMemo(() => { try { return JSON.parse(sessionStorage.getItem("reg_step1") || "{}"); } catch { return {}; } }, []);
|
||||
const step2 = useMemo(() => { try { return JSON.parse(sessionStorage.getItem("reg_step2") || "{}"); } catch { return {}; } }, []);
|
||||
|
||||
// если шаги не пройдены — назад
|
||||
useEffect(() => {
|
||||
if (!step1?.email || !step1?.password || !step2?.firstName || !step2?.lastName) {
|
||||
navigate("/register/step1");
|
||||
}
|
||||
}, [navigate, step1, step2]);
|
||||
|
||||
// подключаем <model-viewer> (один раз)
|
||||
useEffect(() => {
|
||||
if (!customElements.get("model-viewer")) {
|
||||
const s = document.createElement("script");
|
||||
s.type = "module";
|
||||
s.src = "https://unpkg.com/@google/model-viewer/dist/model-viewer.min.js";
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [gender, setGender] = useState(step2?.gender === "female" ? "female" : "male");
|
||||
const [avatarURL, setAvatarURL] = useState(
|
||||
step2?.avatarURL || AVATAR_PRESETS[step2?.gender === "female" ? "female" : "male"][0].url
|
||||
);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [err, setErr] = useState("");
|
||||
|
||||
// при смене пола подставляем первый пресет
|
||||
useEffect(() => {
|
||||
setAvatarURL(AVATAR_PRESETS[gender][0].url);
|
||||
}, [gender]);
|
||||
|
||||
async function handleSubmit() {
|
||||
setErr("");
|
||||
if (submitting) return;
|
||||
setSubmitting(true);
|
||||
|
||||
const payload = {
|
||||
email: step1.email,
|
||||
password: step1.password,
|
||||
firstName: step2.firstName,
|
||||
lastName: step2.lastName,
|
||||
gender,
|
||||
age: step2.age ?? null,
|
||||
city: step2.city ?? null,
|
||||
// ВАЖНО: тут лежит URL на GLB-модель будущего персонажа
|
||||
avatarURL: avatarURL || null,
|
||||
};
|
||||
|
||||
try {
|
||||
// завершающий вызов регистрации (оставь твой URL/тело запроса как было)
|
||||
const res = await fetch('/api/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ gender, avatarURL })
|
||||
const res = await fetch("/api/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.success) {
|
||||
alert('Ошибка регистрации');
|
||||
|
||||
const text = await res.text();
|
||||
let data = null;
|
||||
try { data = JSON.parse(text); } catch {}
|
||||
|
||||
if (!res.ok || !data?.success) {
|
||||
const msg = (data && (data.error || data.message)) || text.slice(0, 400) || "Ошибка регистрации";
|
||||
setErr(msg);
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// сохраняем токен
|
||||
localStorage.setItem('token', data.token);
|
||||
|
||||
// добираем профиль
|
||||
const meRes = await fetch('/api/me', {
|
||||
headers: { Authorization: `Bearer ${data.token}` }
|
||||
});
|
||||
const me = meRes.ok ? await meRes.json() : null;
|
||||
|
||||
// собираем профиль для игры
|
||||
const user_profile = {
|
||||
id: me?.id,
|
||||
email: me?.email,
|
||||
firstName: me?.firstName,
|
||||
lastName: me?.lastName,
|
||||
gender: me?.gender ?? gender,
|
||||
age: me?.age,
|
||||
city: me?.city,
|
||||
avatarURL: avatarURL || me?.avatarURL,
|
||||
balance: me?.balance ?? 0,
|
||||
satiety: me?.satiety ?? 100,
|
||||
thirst: me?.thirst ?? 100,
|
||||
last_city_id: me?.lastCityId ?? 1
|
||||
};
|
||||
|
||||
sessionStorage.setItem('user_profile', JSON.stringify(user_profile));
|
||||
navigate('/game');
|
||||
if (data.token) localStorage.setItem("token", data.token);
|
||||
sessionStorage.removeItem("reg_step1");
|
||||
sessionStorage.removeItem("reg_step2");
|
||||
navigate("/game");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Ошибка регистрации (шаг 3)');
|
||||
setErr(String(e));
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '40px auto' }}>
|
||||
<h2>Шаг 3: профиль</h2>
|
||||
<label style={{ display: 'block', marginBottom: 8 }}>
|
||||
Пол:
|
||||
<select value={gender} onChange={e => setGender(e.target.value)} style={{ marginLeft: 8 }}>
|
||||
<option value="male">Мужской</option>
|
||||
<option value="female">Женский</option>
|
||||
</select>
|
||||
</label>
|
||||
<label style={{ display: 'block', marginBottom: 8 }}>
|
||||
URL аватара:
|
||||
<input value={avatarURL} onChange={e => setAvatarURL(e.target.value)} style={{ width: '100%' }} />
|
||||
</label>
|
||||
<button type="submit">Завершить</button>
|
||||
</form>
|
||||
<div style={styles.page}>
|
||||
<div style={styles.card}>
|
||||
<h1 style={styles.header}>Шаг 3: профиль (3D-аватар GLB)</h1>
|
||||
|
||||
<div style={styles.row}>
|
||||
<div>Пол: </div>
|
||||
<select value={gender} onChange={(e) => setGender(e.target.value)} style={styles.select}>
|
||||
<option value="male">Мужской</option>
|
||||
<option value="female">Женский</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style={styles.grid}>
|
||||
{AVATAR_PRESETS[gender].map((a) => (
|
||||
<div key={a.url} style={styles.cardItem(a.url === avatarURL)} onClick={() => setAvatarURL(a.url)}>
|
||||
<div style={styles.mvWrap}>
|
||||
<model-viewer
|
||||
src={a.url}
|
||||
camera-controls
|
||||
auto-rotate
|
||||
ar
|
||||
disable-zoom={false}
|
||||
style={{ width: "100%", height: "100%", background: "transparent" }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ fontWeight: 700, marginBottom: 4 }}>{a.name}</div>
|
||||
<div style={{ fontSize: 12, opacity: 0.8, wordBreak: "break-all" }}>{a.url}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style={{ margin: "8px 0 6px" }}>Или свой GLB-URL:</div>
|
||||
<input
|
||||
placeholder="https://.../my-avatar.glb"
|
||||
value={avatarURL}
|
||||
onChange={(e) => setAvatarURL(e.target.value)}
|
||||
style={styles.input}
|
||||
/>
|
||||
<div style={styles.hint}>
|
||||
Вставь ссылку на .glb с включённым CORS (должен грузиться в браузере). Это значение сохранится в поле
|
||||
<code> avatarURL </code> профиля — затем клиент игры сможет подхватить и загрузить модель в Three.js.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{err ? <div style={styles.error}>{err}</div> : null}
|
||||
|
||||
<div style={styles.actions}>
|
||||
<button style={styles.primaryBtn} onClick={handleSubmit} disabled={submitting}>
|
||||
{submitting ? "Сохраняю..." : "Завершить"}
|
||||
</button>
|
||||
<button style={styles.secondaryBtn} onClick={() => navigate("/register/step2")} disabled={submitting}>
|
||||
← Назад
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = {
|
||||
wrapper: {
|
||||
width: '100vw', minHeight:'100vh',
|
||||
background: '#111', color:'#fff',
|
||||
display: 'flex', flexDirection:'column',
|
||||
alignItems:'center', paddingTop:40,
|
||||
gap:20
|
||||
},
|
||||
grid: {
|
||||
display:'flex', gap:30, marginBottom:20
|
||||
},
|
||||
avatarCard: {
|
||||
cursor:'pointer', textAlign:'center',
|
||||
padding:10, borderRadius:8, background:'#222'
|
||||
},
|
||||
button: {
|
||||
padding:'10px 30px',
|
||||
background:'#17a2b8',
|
||||
color:'#fff',
|
||||
border:'none',
|
||||
borderRadius:4
|
||||
},
|
||||
back: {
|
||||
marginTop:10,
|
||||
background:'transparent',
|
||||
border:'none',
|
||||
color:'#aaa',
|
||||
cursor:'pointer'
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user