Interiors Fix Begin

This commit is contained in:
2025-08-25 22:39:29 +03:00
parent 528c2b1db4
commit ebf7e01261
9 changed files with 1589 additions and 401 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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', {

View File

@@ -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();
// Достаём шаги 12
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>Пол:&nbsp;</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'
}
};