Фича, добавленная в ветке feat-multiple-character. Позволяет пользователю иметь несколько персонажей, переключаться между ними.
- Раньше: 1 персонаж на Telegram-пользователя (
owner= уникальный) - Теперь:
owner= не уникальный, несколько персонажей, один активный
Char {
owner: string; // Telegram user ID (НЕ уникальный)
active: boolean; // default: true — только один активный
deleted: boolean; // default: false — soft-delete
...
}
cd server && bun run migrate-activeУстанавливает active: true всем существующим не-удалённым персонажам.
- Только один активный персонаж на owner (поддерживается в
createCharacter,activate,remove) createCharacterдеактивирует остальных перед созданием новогоactivate()деактивирует остальных, оставляет только целевогоremove()при удалении активного авто-активирует любого другого
arena.characters кеширует CharacterService экземпляры. При деактивации персонажа его charObj.active остаётся true в кеше.
activate()— удаляет деактивированных изarena.characters:
for (const id of Object.keys(arena.characters)) {
if (arena.characters[id].owner === this.owner && id !== this.id) {
delete arena.characters[id];
}
}getAllCharacters()— синхронизируетactiveиз БД:
if (cached) {
cached.charObj.active = char.active; // свежие данные из БД
return cached;
}deactivateOtherCharacters()— явныйnew Types.ObjectId(excludeId)в$ne:
{ owner, _id: { $ne: new Types.ObjectId(excludeId) }, deleted: false }Mongoose не всегда авто-кастит строку в ObjectId внутри оператора $ne.
| Метод | Путь | Описание |
|---|---|---|
| GET | /character/my |
Список всех персонажей пользователя |
| PATCH | /character/:id/activate |
Сделать персонажа активным (403 если чужой) |
| GET | /character/list?ids=... |
Публичные объекты по ID (без изменений) |
getMyCharacters() → GET /character/my
activateCharacter(id) → PATCH /character/:id/activate- Вне ProtectedRoute (нет WebSocket)
getMyCharacters()на mount- 0 персонажей → сразу форма создания
- Есть персонажи → список + «Создать нового» (скрывает/показывает форму)
- Активный: «Войти» →
navigate('/character') - Неактивный: «Сменить» →
activateCharacter(id)→window.location.reload() - После создания:
window.location.href = '/'(пройти ProtectedRoute)
- Внутри ProtectedRoute
- Секция «Персонажи»: список + кнопки «Активен»(disabled)/«Сменить»
- «Создать нового» →
navigate('/create') - «Удалить текущего» →
deleteCharacter()→window.location.reload()
После операций, меняющих активного персонажа, нужен полный reload (не SPA-навигация):
- WebSocket должен переподключиться с новым активным персонажем
- ProtectedRoute загружает нового персонажа через
getCharacter
Точки reload:
- Создание персонажа →
window.location.href = '/' - Активация →
window.location.reload() - Удаление →
window.location.reload()
server/models/character.ts— полеactiveserver/api/character.ts—findCharacters,deactivateOtherCharacters,activateCharacter,activateAnyCharacterserver/arena/CharacterService/CharacterService.ts—getCharacter(owner)+active,getAllCharacters,activate(),remove()авто-активацияserver/server/character.ts— роуты/my,/:id/activateserver/cli/migrateActive.ts— миграцияshared/character/characterSchema.ts—active?: boolean
client/api/character.ts—getMyCharacters(),activateCharacter()client/modules/character/pages/CharacterCreatePage.tsx— список + формаclient/modules/settings/pages/SettingsPage.tsx— секция «Персонажи»client/modules/settings/hooks/useSettingsCharacter.ts— reload после удаленияclient/components/ProtectedRoute.tsx—hydratedstate