Files
Dimension-47/client/src/shared/hooks/use-character-socket.ts
Alexander Zielonka 55419d3896 feat: Complete character system, animated login, WebSocket sync
Character System:
- Inventory system with 5,482 equipment items
- Feats tab with categories and details
- Actions tab with 99 PF2e actions
- Item detail modal with equipment info
- Feat detail modal with descriptions
- Edit character modal with image cropping

Auth & UI:
- Animated login screen with splash → form transition
- Letter-by-letter "DIMENSION 47" animation
- Starfield background with floating orbs
- Logo tap glow effect
- "Remember me" functionality (localStorage/sessionStorage)

Real-time Sync:
- WebSocket gateway for character updates
- Live sync for HP, conditions, inventory, equipment status, money, level

Database:
- Added credits field to characters
- Added custom fields for items
- Added feat fields and relations
- Included full database backup

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 15:36:29 +01:00

146 lines
4.6 KiB
TypeScript

import { useEffect, useRef, useCallback } from 'react';
import { io, Socket } from 'socket.io-client';
import { api } from '@/shared/lib/api';
import type { Character, CharacterItem, CharacterCondition } from '@/shared/types';
const SOCKET_URL = import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:3001';
export type CharacterUpdateType = 'hp' | 'conditions' | 'item' | 'inventory' | 'money' | 'level' | 'equipment_status';
export interface CharacterUpdate {
characterId: string;
type: CharacterUpdateType;
data: any;
}
interface UseCharacterSocketOptions {
characterId: string;
onHpUpdate?: (data: { hpCurrent: number; hpTemp: number; hpMax: number }) => void;
onConditionsUpdate?: (data: { action: 'add' | 'update' | 'remove'; condition?: CharacterCondition; conditionId?: string }) => void;
onInventoryUpdate?: (data: { action: 'add' | 'remove' | 'update'; item?: CharacterItem; itemId?: string }) => void;
onEquipmentStatusUpdate?: (data: { action: 'update'; item: CharacterItem }) => void;
onMoneyUpdate?: (data: { credits: number }) => void;
onLevelUpdate?: (data: { level: number }) => void;
onFullUpdate?: (character: Character) => void;
}
export function useCharacterSocket({
characterId,
onHpUpdate,
onConditionsUpdate,
onInventoryUpdate,
onEquipmentStatusUpdate,
onMoneyUpdate,
onLevelUpdate,
onFullUpdate,
}: UseCharacterSocketOptions) {
const socketRef = useRef<Socket | null>(null);
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const connect = useCallback(() => {
const token = api.getToken();
if (!token || !characterId) return;
// Disconnect existing socket if any
if (socketRef.current?.connected) {
socketRef.current.disconnect();
}
const socket = io(`${SOCKET_URL}/characters`, {
auth: { token },
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
});
socket.on('connect', () => {
console.log('[WebSocket] Connected to character namespace');
// Join the character room
socket.emit('join_character', { characterId }, (response: { success: boolean; error?: string }) => {
if (response.success) {
console.log(`[WebSocket] Joined character room: ${characterId}`);
} else {
console.error(`[WebSocket] Failed to join character room: ${response.error}`);
}
});
});
socket.on('disconnect', (reason) => {
console.log(`[WebSocket] Disconnected: ${reason}`);
});
socket.on('connect_error', (error) => {
console.error('[WebSocket] Connection error:', error.message);
});
// Handle character updates
socket.on('character_update', (update: CharacterUpdate) => {
console.log(`[WebSocket] Received update: ${update.type}`, update.data);
switch (update.type) {
case 'hp':
onHpUpdate?.(update.data);
break;
case 'conditions':
onConditionsUpdate?.(update.data);
break;
case 'inventory':
onInventoryUpdate?.(update.data);
break;
case 'equipment_status':
onEquipmentStatusUpdate?.(update.data);
break;
case 'money':
onMoneyUpdate?.(update.data);
break;
case 'level':
onLevelUpdate?.(update.data);
break;
case 'item':
// Item update that's not equipment status (e.g., quantity, notes)
onInventoryUpdate?.(update.data);
break;
}
});
// Handle full character refresh (e.g., after reconnect)
socket.on('character_refresh', (character: Character) => {
console.log('[WebSocket] Received full character refresh');
onFullUpdate?.(character);
});
socketRef.current = socket;
return socket;
}, [characterId, onHpUpdate, onConditionsUpdate, onInventoryUpdate, onEquipmentStatusUpdate, onMoneyUpdate, onLevelUpdate, onFullUpdate]);
const disconnect = useCallback(() => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
if (socketRef.current) {
// Leave the character room before disconnecting
socketRef.current.emit('leave_character', { characterId });
socketRef.current.disconnect();
socketRef.current = null;
}
}, [characterId]);
useEffect(() => {
connect();
return () => {
disconnect();
};
}, [connect, disconnect]);
return {
socket: socketRef.current,
isConnected: socketRef.current?.connected ?? false,
reconnect: connect,
};
}