From 94da0e140900643e5929fe0bc2c84fdf4fce6e8d Mon Sep 17 00:00:00 2001 From: min Date: Sat, 13 Jun 2026 10:19:54 +0300 Subject: [PATCH] =?UTF-8?q?feat(skin):=20Mixamo-=D0=BF=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D1=82=D1=80=D0=B0=2080=20=D1=81=D0=BA=D0=B8=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=20+=20fallback=20=D0=BD=D0=B0=20legacy=20R15?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Что: - _resolveModelSource: * Mixamo (skin_y-bot/x-bot/eve/...) → /character-assets/skins/.glb (с rublox-site, non-humanoid-rigged kind) * Legacy R15 (skin_bacon-hair, skin_sigma-labubu и др.) — сохранена старая ветка через manifest + /kubikon-assets/. Это нужно пока бэк storys работает в legacy-режиме (RUBLOX_NEW_SKINS_AVAILABLE != true). - skinFolderRef.current default: skin_bacon-hair → skin_y-bot - BabylonScene._playerModelType default + миграция character-* → skin_y-bot - PlayerController._modelTypeId default → skin_y-bot - MultiplayerSync: все дефолты → skin_y-bot LOCAL DEV: - На localhost плеер сначала пробует localStorage('rublox_selected_skin') (тот же ключ что в rublox-site), потом БД. Это позволяет тестить выбор скина в сайте без записи в прод-БД. Зависит от: - PR storys (новый бэк-резолв + feature-flag) - PR user (endpoint //gender) - Заливки 80 GLB на rublox.pro/character-assets/skins/ (отдельная инфра-задача) --- src/KubikonPlayer/KubikonPlayer.jsx | 30 ++++++++++++--- src/engine/BabylonScene.js | 11 +++--- src/engine/MultiplayerSync.js | 4 +- src/engine/PlayerController.js | 60 +++++++++++++++++++++++------ 4 files changed, 80 insertions(+), 25 deletions(-) diff --git a/src/KubikonPlayer/KubikonPlayer.jsx b/src/KubikonPlayer/KubikonPlayer.jsx index da4fb05..9aa72f4 100644 --- a/src/KubikonPlayer/KubikonPlayer.jsx +++ b/src/KubikonPlayer/KubikonPlayer.jsx @@ -209,9 +209,11 @@ const KubikonPlayer = () => { const roomRef = useRef(null); /** MultiplayerSync (мост между room и Babylon-сценой). */ const mpSyncRef = useRef(null); - /** Выбранный R15-скин текущего игрока (из rublox_equipped_skin). - * Грузится при старте, уходит в мультиплеер как modelType. */ - const skinFolderRef = useRef('skin_bacon-hair'); + /** Выбранный Mixamo-скин текущего игрока (из rublox_equipped_skin). + * Грузится при старте, уходит в мультиплеер как modelType. + * 2026-06-13: дефолт сменён с skin_bacon-hair на skin_y-bot + * (Игрек-Бот, новый Mixamo-каталог). */ + const skinFolderRef = useRef('skin_y-bot'); const [meta, setMeta] = useState(null); // { title, description, user_id, ... } const [forbidden, setForbidden] = useState(false); @@ -589,8 +591,24 @@ const KubikonPlayer = () => { // тогда player.setModelType подхватит правильный скин. // Этот же skinFolder уйдёт в мультиплеер как modelType, // чтобы соперники видели наш реальный скин. - let mySkin = 'skin_bacon-hair'; - if (userId) { + // + // LOCAL DEV (localhost): сначала пробуем localStorage + // ('rublox_selected_skin' — тот же ключ что в rublox-site), + // чтобы юзер мог тестить выбор скинов без записи в прод-БД. + let mySkin = 'skin_y-bot'; + const isLocalDev = (typeof window !== 'undefined' + && (window.location.hostname === 'localhost' + || window.location.hostname === '127.0.0.1')); + if (isLocalDev) { + try { + const localPick = localStorage.getItem('rublox_selected_skin'); + if (localPick && typeof localPick === 'string') { + mySkin = localPick; + console.log('[KubikonPlayer] local-dev skin:', mySkin); + } + } catch (e) {} + } + if (mySkin === 'skin_y-bot' && userId) { try { const skinRes = await Kubikon3DApi.getEquippedSkin(userId); const sf = skinRes?.data?.skin_folder; @@ -831,7 +849,7 @@ const KubikonPlayer = () => { // загружен при старте в skinFolderRef). Сервер всё равно перепроверит // скин по userId из JWT и при расхождении возьмёт значение из БД — // так каждый игрок виден соперникам в своём реальном скине. - const modelType = skinFolderRef.current || 'skin_bacon-hair'; + const modelType = skinFolderRef.current || 'skin_y-bot'; // Если у нас есть валидный reconnectionToken от прошлой сессии — // используем Colyseus reconnect (это та же сессия для сервера, // allowReconnection(5) её подхватит, не будет +join/-leave цикла). diff --git a/src/engine/BabylonScene.js b/src/engine/BabylonScene.js index 309c93b..b117424 100644 --- a/src/engine/BabylonScene.js +++ b/src/engine/BabylonScene.js @@ -199,10 +199,9 @@ export class BabylonScene { // Точка спавна игрока в режиме Play (обновляется setSpawnPoint) this._spawnPoint = { x: 0, y: 5, z: 0 }; // Модель персонажа для режима Play. - // Дефолт — R15-скин bacon-hair (классический Roblox-вид). - // 'skin_*' грузится из characters//body.glb (R15-скелет), - // 'character-*' — старые Kenney-модели. - this._playerModelType = 'skin_bacon-hair'; + // 2026-06-13: дефолт сменён на skin_y-bot (Mixamo Y-Bot, + // нейтральный по полу). Старые скины (skin_bacon-hair и др.) удалены. + this._playerModelType = 'skin_y-bot'; // Размер пола: пол идёт от -worldHalf до +worldHalf по X и Z. // По умолчанию 80×80 (worldHalf = 40). Можно менять через setWorldSize(). this._worldHalf = 40; @@ -7523,7 +7522,9 @@ export class BabylonScene { // форсим R15 bacon-hair. Явно выбранные 'skin_*' не трогаем. if (state.scene.playerModelType) { const pmt = state.scene.playerModelType; - this._playerModelType = pmt.startsWith('character-') ? 'skin_bacon-hair' : pmt; + // character-a..g (Kenney) и legacy R15 (skin_bacon-hair и др.) + // мигрируем на новый дефолт skin_y-bot. + this._playerModelType = pmt.startsWith('character-') ? 'skin_y-bot' : pmt; } // Задача 07: конфиг скинов { default, unlocked, shopVisible, coins, customGlbs }. if (state.scene.skins && typeof state.scene.skins === 'object') { diff --git a/src/engine/MultiplayerSync.js b/src/engine/MultiplayerSync.js index 36dbbaa..7aa1ac5 100644 --- a/src/engine/MultiplayerSync.js +++ b/src/engine/MultiplayerSync.js @@ -64,7 +64,7 @@ function loadSkinManifest() { * @returns {Promise<{file:string, isR15:boolean, overrides:object}|null>} */ async function resolveRemoteModelSource(modelType) { - const typeId = modelType || 'skin_bacon-hair'; + const typeId = modelType || 'skin_y-bot'; if (typeId.startsWith('skin_')) { const manifest = await loadSkinManifest(); const entry = manifest.find((s) => s.id === typeId); @@ -713,7 +713,7 @@ export class MultiplayerSync { maxHp: player.maxHp ?? 100, isDead: !!player.isDead, username: player.username || sessionId, - modelType: player.modelType || 'skin_bacon-hair', + modelType: player.modelType || 'skin_y-bot', animState: player.animState || 'idle', // Если модель не успеет загрузиться, висит fallback-капсула. fallbackMesh: null, diff --git a/src/engine/PlayerController.js b/src/engine/PlayerController.js index 0c1ef03..0e72ed8 100644 --- a/src/engine/PlayerController.js +++ b/src/engine/PlayerController.js @@ -33,6 +33,29 @@ import { AccessoryManager } from './AccessoryManager'; // Цикл режимов: первое лицо → за спиной → спереди (показ персонажа лицом). // 'front' удобен чтобы оценить скин: камера спереди, смотрит на лицо персонажа. +/* Mixamo-скины (новые персонажи rublox-site /character-assets/skins/). + * 2026-06-11: эти 80 ID перенесены сюда из data/skinsCatalog.js фронта + * чтобы плеер их распознавал и грузил по правильному пути. + * Дефолтные: skin_x-bot (male), skin_y-bot (female/null). */ +const MIXAMO_SKINS = new Set([ + 'skin_aj', 'skin_akai', 'skin_arissa', 'skin_big-vegas', + 'skin_castle-guard-1', 'skin_castle-guard-2', + 'skin_ch01', 'skin_ch02', 'skin_ch03', 'skin_ch04', 'skin_ch07', 'skin_ch08', + 'skin_ch09', 'skin_ch10', 'skin_ch11', 'skin_ch13', 'skin_ch14', 'skin_ch15', + 'skin_ch16', 'skin_ch17', 'skin_ch18', 'skin_ch19', 'skin_ch20', 'skin_ch21', + 'skin_ch22', 'skin_ch23', 'skin_ch24', 'skin_ch29', 'skin_ch31', 'skin_ch32', + 'skin_ch33', 'skin_ch34', 'skin_ch35', 'skin_ch39', 'skin_ch40', 'skin_ch42', + 'skin_ch43', 'skin_ch44', 'skin_ch45', 'skin_ch46', 'skin_ch47', 'skin_ch48', + 'skin_claire', 'skin_demon', 'skin_ely', 'skin_erika-archer', 'skin_erika-archer-bow', + 'skin_eve', 'skin_exo-gray', 'skin_exo-red', 'skin_ganfaul', 'skin_heraklios', + 'skin_kachujin', 'skin_kaya', 'skin_knight', 'skin_lola', 'skin_maria', + 'skin_maria-wprop', 'skin_maw', 'skin_medea', 'skin_mutant', 'skin_nightshade', + 'skin_paladin', 'skin_passive-marker-man', 'skin_peasant-girl', 'skin_peasant-man', + 'skin_prisoner', 'skin_pumpkinhulk', 'skin_skeleton-zombie', 'skin_sporty-granny', + 'skin_survivor', 'skin_swat', 'skin_ty', 'skin_uriel', 'skin_vampire', + 'skin_war-zombie', 'skin_warrok', 'skin_white-clown', 'skin_x-bot', 'skin_y-bot', +]); + const CAMERA_MODES = ['third', 'first', 'front']; // Для режима 'sideview' (Кубикон Dash): // - камера фиксирована сбоку (смотрит на +Z с расстояния SIDEVIEW_DIST по -Z) @@ -168,8 +191,8 @@ export class PlayerController { this._stepUpDecay = 4.5; // Модель игрока (грузится в start) - // Дефолт — R15-скин bacon-hair (классический Roblox-вид). - this._modelTypeId = 'skin_bacon-hair'; + // 2026-06-13: дефолт сменён на skin_y-bot (Mixamo Y-Bot). + this._modelTypeId = 'skin_y-bot'; this._modelRoot = null; this._modelMeshes = []; // Кубикон Dash: скрипт через game.player.setSkinVisible(false) выставляет @@ -786,19 +809,32 @@ export class PlayerController { return null; } if (typeId.startsWith('skin_')) { + // 2026-06-11: палитра скинов Рублокса заменена на 80 Mixamo. + // Mixamo-скины: /character-assets/skins/.glb (на rublox-site). + // Legacy-скины (skin_bacon-hair / skin_sigma-labubu / skin_cop / ...) + // ещё могут приходить из БД пока feature-flag в storys выключен — + // их грузим из старого /kubikon-assets/characters//body.glb + // (R15-скелет). После заливки 80 GLB на rublox.pro и включения + // RUBLOX_NEW_SKINS_AVAILABLE=true legacy-ветка перестанет + // срабатывать (бэк начнёт отдавать только новые типы). + if (MIXAMO_SKINS.has(typeId)) { + const base = (typeof window !== 'undefined' + && window.location.hostname === 'localhost') + ? 'http://localhost:3000' + : 'https://rublox.pro'; + return { + file: `${base}/character-assets/skins/${typeId}.glb`, + isR15: false, + kind: 'non-humanoid-rigged', // Mixamo-rig, не R15 + overrides: {}, + isMixamo: true, + }; + } + // Legacy R15-скин — через старый manifest. const manifest = await this._loadSkinManifest(); const entry = manifest.find((s) => s.id === typeId); if (entry) { - // kind определяет систему анимации: - // 'r15' → R15-скелет (как раньше) - // 'non-humanoid-mesh' → single-mesh, процедурное покачивание - // 'non-humanoid-rigged' → свой скелет + встроенные AnimationGroup - // Отсутствие kind = 'r15' (обратная совместимость со старым манифестом). const kind = entry.kind || 'r15'; - // absolute_file=true (источник /rublox/avatars) — file уже - // полный URL (legacy /kubikon-assets/... или дизайнерский - // /api-storys/...). Без флага — это легаси-формат - // skins_manifest.json без префикса. const file = entry.absolute_file ? entry.file : '/kubikon-assets/' + entry.file; @@ -812,7 +848,7 @@ export class PlayerController { rotationYOffset: Number.isFinite(entry.rotationYOffset) ? entry.rotationYOffset : 0, }; } - // нет в манифесте — пробуем прямой путь + // нет ни в Mixamo, ни в manifest — пробуем прямой legacy-путь return { file: `/kubikon-assets/characters/${typeId}/body.glb`, isR15: true,