debug system
This commit is contained in:
parent
c09bc6edd5
commit
ac5b83afee
42
CLAUDE.md
42
CLAUDE.md
@ -25,7 +25,7 @@ The application loads scripts in a specific order due to class dependencies:
|
||||
|
||||
1. **Utilities**: `vector.js`, `physics.js` (foundational math/physics)
|
||||
2. **Entities**: `player.js`, `puck.js` (game objects)
|
||||
3. **Systems**: `renderer.js`, `physics-system.js`, `ai-system.js`, `rules-system.js`
|
||||
3. **Systems**: `renderer.js`, `physics-system.js`, `ai-system.js`, `rules-system.js`, `debug-system.js`
|
||||
4. **Engine**: `game-state.js`, `game-engine.js`, `main.js` (orchestration)
|
||||
|
||||
### Key Classes and Responsibilities
|
||||
@ -35,9 +35,10 @@ The application loads scripts in a specific order due to class dependencies:
|
||||
- **GameState** (`game-state.js`): Score tracking, time management, penalty system
|
||||
- **Player** (`player.js`): Hockey player with AI, physics, and role-based behavior
|
||||
- **Puck** (`puck.js`): Physics-based puck with collision detection
|
||||
- **Renderer** (`renderer.js`): 2D canvas rendering with camera system
|
||||
- **Renderer** (`renderer.js`): 2D canvas rendering with camera system and debug visualizations
|
||||
- **AISystem** (`ai-system.js`): Team formations and strategic AI behaviors
|
||||
- **RulesSystem** (`rules-system.js`): Hockey rule enforcement (offside, icing, penalties)
|
||||
- **DebugSystem** (`debug-system.js`): Comprehensive debugging interface with real-time inspection
|
||||
|
||||
### Game Loop Architecture
|
||||
The engine runs at 60 FPS using `requestAnimationFrame` with these phases:
|
||||
@ -55,10 +56,45 @@ The engine runs at 60 FPS using `requestAnimationFrame` with these phases:
|
||||
|
||||
## Controls Reference
|
||||
- `SPACE` - Pause/Resume game
|
||||
- `D` - Toggle debug information
|
||||
- `D` - Toggle debug information and visual overlays
|
||||
- `R` - Reset game
|
||||
- `F11` - Toggle fullscreen
|
||||
- `Mouse Wheel` - Camera zoom
|
||||
- **Debug Mode Button** - Open/close comprehensive debug panel
|
||||
|
||||
## Debug System Features
|
||||
|
||||
### Enhanced Debug Panel
|
||||
When debug mode is active, you can pause the game and inspect all variables:
|
||||
|
||||
- **Real-time Debug Panel**: Right-side panel with live game state information
|
||||
- **Player State Inspection**: Click any player to see detailed attributes, AI state, and physics data
|
||||
- **Game State Monitoring**: Period, time, score, pause status, faceoff information
|
||||
- **Puck Tracking**: Position, velocity, ownership, and physics data
|
||||
|
||||
### Interactive Debug Features
|
||||
- **Player Selection**: Click players in debug panel or directly on the canvas
|
||||
- **Target Visualization**: Selected players show bright cyan lines to their target positions
|
||||
- **Visual Feedback**: Hover effects, selection highlighting, and smooth animations
|
||||
|
||||
### Console Debug Functions
|
||||
Access via `debugHelpers` global object:
|
||||
- `debugHelpers.getPlayers()` - Get all player objects
|
||||
- `debugHelpers.getHomeTeam()` / `debugHelpers.getAwayTeam()` - Get team players
|
||||
- `debugHelpers.getPlayer(id)` - Get specific player by ID
|
||||
- `debugHelpers.getPuck()` - Get puck object with full state
|
||||
- `debugHelpers.getGameState()` - Get complete game state
|
||||
- `debugHelpers.getPuckCarrier()` - Get player currently carrying the puck
|
||||
- `debugHelpers.exportGameState()` - Export full game state for analysis
|
||||
|
||||
### Debug Visualizations
|
||||
When visual debug mode is active (`D` key):
|
||||
- **Velocity Vectors**: Red arrows showing player movement direction
|
||||
- **Target Lines**: Green dashed lines to player destinations
|
||||
- **AI Targets**: Yellow dotted lines to AI decision targets
|
||||
- **Energy Bars**: Visual energy indicators above players
|
||||
- **Puck Carrier Highlight**: Yellow dashed circle around puck carrier
|
||||
- **Selected Player**: Cyan line to target with crosshair marker
|
||||
|
||||
## File Structure Notes
|
||||
- All code is vanilla JavaScript (ES6 classes)
|
||||
|
||||
43
README.md
43
README.md
@ -51,7 +51,8 @@ hockey-manager/
|
||||
│ │ ├── renderer.js # 2D rendering system
|
||||
│ │ ├── physics-system.js # Physics calculations
|
||||
│ │ ├── ai-system.js # AI formation and strategy
|
||||
│ │ └── rules-system.js # Hockey rules enforcement
|
||||
│ │ ├── rules-system.js # Hockey rules enforcement
|
||||
│ │ └── debug-system.js # Comprehensive debugging interface
|
||||
│ └── utils/
|
||||
│ ├── vector.js # 2D vector mathematics
|
||||
│ └── physics.js # Physics utility functions
|
||||
@ -63,8 +64,9 @@ hockey-manager/
|
||||
2. **Start playing**: The game will automatically initialize and start
|
||||
3. **Use controls**:
|
||||
- **Space**: Pause/Resume
|
||||
- **D**: Toggle debug mode
|
||||
- **D**: Toggle debug mode with visual overlays
|
||||
- **R**: Reset game
|
||||
- **Debug Mode Button**: Open comprehensive debug panel
|
||||
- **Mouse wheel**: Zoom in/out
|
||||
- **F11**: Toggle fullscreen
|
||||
|
||||
@ -84,6 +86,43 @@ hockey-manager/
|
||||
- **Play/Pause** - Start/stop game simulation
|
||||
- **Speed Control** - Adjust game speed (0.5x, 1x, 2x, 4x)
|
||||
- **Reset Game** - Return to initial game state
|
||||
- **Debug Mode** - Open comprehensive debug panel for game inspection
|
||||
|
||||
## Debug System
|
||||
|
||||
The hockey engine includes a comprehensive debug system for inspecting game state and player behavior:
|
||||
|
||||
### Debug Panel Features
|
||||
- **Real-time Game State**: Live monitoring of period, time, score, and game status
|
||||
- **Player State Inspection**: Click any player to view detailed attributes, AI decisions, and physics data
|
||||
- **Puck Tracking**: Complete puck state including position, velocity, and ownership
|
||||
- **Interactive Selection**: Click players in the panel or directly on the canvas for detailed inspection
|
||||
|
||||
### Visual Debug Overlays
|
||||
When debug mode is active (`D` key):
|
||||
- **Velocity Vectors**: Red arrows showing player movement direction and speed
|
||||
- **Target Lines**: Green dashed lines indicating where players are trying to move
|
||||
- **AI Targets**: Yellow dotted lines showing AI decision targets
|
||||
- **Energy Bars**: Visual indicators of player energy levels
|
||||
- **Selected Player Visualization**: Bright cyan lines showing target destination with crosshair markers
|
||||
|
||||
### Console Debug Functions
|
||||
Access programmatic debugging via the global `debugHelpers` object:
|
||||
```javascript
|
||||
debugHelpers.getPlayers() // All player objects
|
||||
debugHelpers.getHomeTeam() // Home team players only
|
||||
debugHelpers.getAwayTeam() // Away team players only
|
||||
debugHelpers.getPlayer(id) // Specific player by ID
|
||||
debugHelpers.getPuck() // Puck object with full state
|
||||
debugHelpers.getPuckCarrier() // Player currently with puck
|
||||
debugHelpers.exportGameState() // Complete game state export
|
||||
```
|
||||
|
||||
This debug system is essential for:
|
||||
- **AI Development**: Understanding player decision-making and behavior patterns
|
||||
- **Performance Analysis**: Monitoring physics calculations and render performance
|
||||
- **Game Balance**: Analyzing player attributes and gameplay mechanics
|
||||
- **Bug Investigation**: Detailed state inspection for troubleshooting issues
|
||||
|
||||
## Game Mechanics
|
||||
|
||||
|
||||
166
css/styles.css
166
css/styles.css
@ -129,4 +129,170 @@ button:active {
|
||||
border-radius: 3px;
|
||||
margin: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Debug Panel Styles */
|
||||
.debug-panel {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 400px;
|
||||
max-height: 80vh;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
border: 2px solid #4a90e2;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
z-index: 20;
|
||||
pointer-events: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.debug-panel.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.debug-header {
|
||||
background: #4a90e2;
|
||||
padding: 10px 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.debug-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#debug-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#debug-close:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.debug-content {
|
||||
max-height: calc(80vh - 60px);
|
||||
overflow-y: auto;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.debug-section {
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #333;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.debug-section:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.debug-section h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #4a90e2;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.debug-section h5 {
|
||||
margin: 10px 0 5px 0;
|
||||
color: #ccc;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.debug-team {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.debug-player {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
border: 2px solid transparent;
|
||||
position: relative;
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.debug-player:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.debug-player:active {
|
||||
transform: scale(0.98) translateY(0px);
|
||||
}
|
||||
|
||||
.debug-player.selected {
|
||||
background: rgba(74, 144, 226, 0.4);
|
||||
border: 2px solid #4a90e2;
|
||||
box-shadow: 0 4px 12px rgba(74, 144, 226, 0.3);
|
||||
}
|
||||
|
||||
.debug-player.selected:hover {
|
||||
background: rgba(74, 144, 226, 0.5);
|
||||
}
|
||||
|
||||
.debug-player-header {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.debug-player-info {
|
||||
color: #ccc;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.debug-value {
|
||||
color: #4a90e2;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.debug-coords {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
#debug-selected-player {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.debug-attribute {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.debug-attribute-name {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.debug-attribute-value {
|
||||
color: #4a90e2;
|
||||
font-weight: bold;
|
||||
}
|
||||
34
index.html
34
index.html
@ -32,6 +32,39 @@
|
||||
<button id="play-pause">Play/Pause</button>
|
||||
<button id="speed-control">Speed: 1x</button>
|
||||
<button id="reset-game">Reset</button>
|
||||
<button id="debug-toggle">Debug Mode</button>
|
||||
</div>
|
||||
|
||||
<div id="debug-panel" class="debug-panel hidden">
|
||||
<div class="debug-header">
|
||||
<h3>Debug Panel</h3>
|
||||
<button id="debug-close">×</button>
|
||||
</div>
|
||||
<div class="debug-content">
|
||||
<div class="debug-section">
|
||||
<h4>Game State</h4>
|
||||
<div id="debug-game-state"></div>
|
||||
</div>
|
||||
<div class="debug-section">
|
||||
<h4>Puck</h4>
|
||||
<div id="debug-puck"></div>
|
||||
</div>
|
||||
<div class="debug-section">
|
||||
<h4>Players</h4>
|
||||
<div class="debug-team">
|
||||
<h5>Home Team</h5>
|
||||
<div id="debug-home-players"></div>
|
||||
</div>
|
||||
<div class="debug-team">
|
||||
<h5>Away Team</h5>
|
||||
<div id="debug-away-players"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="debug-section">
|
||||
<h4>Selected Player</h4>
|
||||
<div id="debug-selected-player">Click a player to see detailed info</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -43,6 +76,7 @@
|
||||
<script src="src/systems/physics-system.js"></script>
|
||||
<script src="src/systems/ai-system.js"></script>
|
||||
<script src="src/systems/rules-system.js"></script>
|
||||
<script src="src/systems/debug-system.js"></script>
|
||||
<script src="src/engine/game-state.js"></script>
|
||||
<script src="src/engine/game-engine.js"></script>
|
||||
<script src="src/engine/main.js"></script>
|
||||
|
||||
@ -19,6 +19,10 @@ class GameEngine {
|
||||
this.setupPlayers();
|
||||
this.setupEventListeners();
|
||||
this.setupControls();
|
||||
|
||||
// Initialize debug system after all other systems are set up
|
||||
window.debugMode = false; // Initialize debug mode
|
||||
this.debugSystem = new DebugSystem(this);
|
||||
}
|
||||
|
||||
setupPlayers() {
|
||||
@ -231,7 +235,9 @@ class GameEngine {
|
||||
|
||||
this.renderEffects();
|
||||
this.renderer.drawUI(this.gameState);
|
||||
this.renderer.drawDebugInfo(this.gameState, this.players, this.puck);
|
||||
|
||||
const selectedPlayer = this.debugSystem ? this.debugSystem.selectedPlayer : null;
|
||||
this.renderer.drawDebugInfo(this.gameState, this.players, this.puck, selectedPlayer);
|
||||
}
|
||||
|
||||
renderEffects() {
|
||||
|
||||
464
src/systems/debug-system.js
Normal file
464
src/systems/debug-system.js
Normal file
@ -0,0 +1,464 @@
|
||||
class DebugSystem {
|
||||
constructor(gameEngine) {
|
||||
this.gameEngine = gameEngine;
|
||||
this.isVisible = false;
|
||||
this.selectedPlayer = null;
|
||||
this.updateInterval = null;
|
||||
|
||||
this.setupEventListeners();
|
||||
this.setupGlobalDebugFunctions();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Debug toggle button
|
||||
document.getElementById('debug-toggle').addEventListener('click', () => {
|
||||
this.toggleDebugPanel();
|
||||
});
|
||||
|
||||
// Close debug panel
|
||||
document.getElementById('debug-close').addEventListener('click', () => {
|
||||
this.hideDebugPanel();
|
||||
});
|
||||
|
||||
// Canvas click for player selection
|
||||
this.gameEngine.canvas.addEventListener('click', (e) => {
|
||||
if (this.isVisible) {
|
||||
this.handleCanvasClick(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor debug mode changes (triggered by existing D key handler)
|
||||
let lastDebugMode = window.debugMode;
|
||||
setInterval(() => {
|
||||
if (window.debugMode !== lastDebugMode) {
|
||||
if (window.debugMode && !this.isVisible) {
|
||||
this.showDebugPanel();
|
||||
} else if (!window.debugMode && this.isVisible) {
|
||||
this.hideDebugPanel();
|
||||
}
|
||||
lastDebugMode = window.debugMode;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
toggleDebugPanel() {
|
||||
if (this.isVisible) {
|
||||
this.hideDebugPanel();
|
||||
} else {
|
||||
this.showDebugPanel();
|
||||
}
|
||||
}
|
||||
|
||||
showDebugPanel() {
|
||||
this.isVisible = true;
|
||||
document.getElementById('debug-panel').classList.remove('hidden');
|
||||
|
||||
// Start real-time updates
|
||||
this.updateInterval = setInterval(() => {
|
||||
this.updateDebugInfo();
|
||||
}, 100); // Update 10 times per second
|
||||
|
||||
// Initial update
|
||||
this.updateDebugInfo();
|
||||
}
|
||||
|
||||
hideDebugPanel() {
|
||||
this.isVisible = false;
|
||||
document.getElementById('debug-panel').classList.add('hidden');
|
||||
|
||||
// Stop real-time updates
|
||||
if (this.updateInterval) {
|
||||
clearInterval(this.updateInterval);
|
||||
this.updateInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleCanvasClick(e) {
|
||||
const rect = this.gameEngine.canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
// Find closest player to click position
|
||||
let closestPlayer = null;
|
||||
let closestDistance = Infinity;
|
||||
|
||||
this.gameEngine.players.forEach(player => {
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(player.position.x - x, 2) +
|
||||
Math.pow(player.position.y - y, 2)
|
||||
);
|
||||
|
||||
if (distance < player.radius + 10 && distance < closestDistance) {
|
||||
closestPlayer = player;
|
||||
closestDistance = distance;
|
||||
}
|
||||
});
|
||||
|
||||
if (closestPlayer) {
|
||||
this.selectPlayer(closestPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
selectPlayer(player) {
|
||||
// Remove previous selection styling
|
||||
document.querySelectorAll('.debug-player').forEach(el => {
|
||||
el.classList.remove('selected');
|
||||
});
|
||||
|
||||
this.selectedPlayer = player;
|
||||
|
||||
// Add selection styling to the clicked player
|
||||
const playerElement = document.querySelector(`[data-player-id="${player.id}"]`);
|
||||
if (playerElement) {
|
||||
playerElement.classList.add('selected');
|
||||
}
|
||||
|
||||
this.updateSelectedPlayerInfo();
|
||||
}
|
||||
|
||||
updateDebugInfo() {
|
||||
if (!this.isVisible) return;
|
||||
|
||||
this.updateGameStateInfo();
|
||||
this.updatePuckInfo();
|
||||
this.updatePlayersInfo();
|
||||
if (this.selectedPlayer) {
|
||||
this.updateSelectedPlayerInfo();
|
||||
}
|
||||
}
|
||||
|
||||
updateGameStateInfo() {
|
||||
const gameState = this.gameEngine.gameState;
|
||||
const info = `
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Period:</span>
|
||||
<span class="debug-attribute-value">${gameState.getPeriodName()}</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Time:</span>
|
||||
<span class="debug-attribute-value">${gameState.formatTime(gameState.timeRemaining)}</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Score:</span>
|
||||
<span class="debug-attribute-value">${gameState.homeScore} - ${gameState.awayScore}</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Paused:</span>
|
||||
<span class="debug-attribute-value">${gameState.isPaused}</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Speed:</span>
|
||||
<span class="debug-attribute-value">${gameState.gameSpeed}x</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Faceoff Active:</span>
|
||||
<span class="debug-attribute-value">${gameState.faceoff.isActive}</span>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('debug-game-state').innerHTML = info;
|
||||
}
|
||||
|
||||
updatePuckInfo() {
|
||||
const puck = this.gameEngine.puck;
|
||||
const info = `
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Position:</span>
|
||||
<span class="debug-attribute-value debug-coords">(${puck.position.x.toFixed(1)}, ${puck.position.y.toFixed(1)})</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Velocity:</span>
|
||||
<span class="debug-attribute-value debug-coords">(${puck.velocity.x.toFixed(1)}, ${puck.velocity.y.toFixed(1)})</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Speed:</span>
|
||||
<span class="debug-attribute-value">${puck.getSpeed().toFixed(1)}</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Active:</span>
|
||||
<span class="debug-attribute-value">${this.gameEngine.puckActive}</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Owner:</span>
|
||||
<span class="debug-attribute-value">${puck.lastTouchedBy || 'None'}</span>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('debug-puck').innerHTML = info;
|
||||
}
|
||||
|
||||
updatePlayersInfo() {
|
||||
const homePlayers = this.gameEngine.players.filter(p => p.team === 'home');
|
||||
const awayPlayers = this.gameEngine.players.filter(p => p.team === 'away');
|
||||
|
||||
this.renderPlayersList('debug-home-players', homePlayers);
|
||||
this.renderPlayersList('debug-away-players', awayPlayers);
|
||||
}
|
||||
|
||||
renderPlayersList(containerId, players) {
|
||||
const container = document.getElementById(containerId);
|
||||
|
||||
// Only create elements if they don't exist yet (prevent flickering)
|
||||
if (container.children.length === 0) {
|
||||
this.createPlayerElements(container, players);
|
||||
}
|
||||
|
||||
// Update existing elements instead of recreating them
|
||||
players.forEach((player, index) => {
|
||||
const playerDiv = container.children[index];
|
||||
if (!playerDiv) return;
|
||||
|
||||
// Update selection state
|
||||
if (this.selectedPlayer && this.selectedPlayer.id === player.id) {
|
||||
playerDiv.classList.add('selected');
|
||||
} else {
|
||||
playerDiv.classList.remove('selected');
|
||||
}
|
||||
|
||||
// Update dynamic content only
|
||||
const coordsElement = playerDiv.querySelector('.debug-coords');
|
||||
const speedElement = playerDiv.querySelector('.debug-speed');
|
||||
const energyElement = playerDiv.querySelector('.debug-energy');
|
||||
const puckElement = playerDiv.querySelector('.debug-puck');
|
||||
const behaviorElement = playerDiv.querySelector('.debug-behavior');
|
||||
|
||||
if (coordsElement) coordsElement.textContent = `Pos: (${player.position.x.toFixed(0)}, ${player.position.y.toFixed(0)})`;
|
||||
if (speedElement) speedElement.textContent = player.velocity.magnitude().toFixed(1);
|
||||
if (energyElement) energyElement.textContent = `${player.state.energy.toFixed(0)}%`;
|
||||
if (puckElement) puckElement.textContent = player.state.hasPuck;
|
||||
if (behaviorElement) behaviorElement.textContent = player.aiState.behavior;
|
||||
});
|
||||
}
|
||||
|
||||
createPlayerElements(container, players) {
|
||||
players.forEach(player => {
|
||||
const playerDiv = document.createElement('div');
|
||||
playerDiv.className = 'debug-player';
|
||||
playerDiv.setAttribute('data-player-id', player.id);
|
||||
|
||||
playerDiv.innerHTML = `
|
||||
<div class="debug-player-header">${player.role} - ${player.name}</div>
|
||||
<div class="debug-player-info">
|
||||
<div class="debug-coords">Pos: (${player.position.x.toFixed(0)}, ${player.position.y.toFixed(0)})</div>
|
||||
<div>Speed: <span class="debug-value debug-speed">${player.velocity.magnitude().toFixed(1)}</span></div>
|
||||
<div>Energy: <span class="debug-value debug-energy">${player.state.energy.toFixed(0)}%</span></div>
|
||||
<div>Has Puck: <span class="debug-value debug-puck">${player.state.hasPuck}</span></div>
|
||||
<div>Behavior: <span class="debug-value debug-behavior">${player.aiState.behavior}</span></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Make the entire div clickable with better event handling
|
||||
playerDiv.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.selectPlayer(player);
|
||||
});
|
||||
|
||||
// Add mouse events for better feedback
|
||||
playerDiv.addEventListener('mouseenter', (e) => {
|
||||
if (!playerDiv.classList.contains('selected')) {
|
||||
playerDiv.style.background = 'rgba(255, 255, 255, 0.15)';
|
||||
}
|
||||
});
|
||||
|
||||
playerDiv.addEventListener('mouseleave', (e) => {
|
||||
if (!playerDiv.classList.contains('selected')) {
|
||||
playerDiv.style.background = '';
|
||||
}
|
||||
});
|
||||
|
||||
playerDiv.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
playerDiv.style.transform = 'scale(0.98)';
|
||||
});
|
||||
|
||||
playerDiv.addEventListener('mouseup', (e) => {
|
||||
e.preventDefault();
|
||||
playerDiv.style.transform = '';
|
||||
});
|
||||
|
||||
container.appendChild(playerDiv);
|
||||
});
|
||||
}
|
||||
|
||||
updateSelectedPlayerInfo() {
|
||||
if (!this.selectedPlayer) return;
|
||||
|
||||
const player = this.selectedPlayer;
|
||||
const info = `
|
||||
<div style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px;">
|
||||
<h4 style="margin: 0 0 5px 0; color: #4a90e2;">${player.name} (${player.role})</h4>
|
||||
<div style="color: #ccc; font-size: 11px;">Team: ${player.team.toUpperCase()}</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<h5 style="margin: 0 0 8px 0; color: #ccc;">Position & Physics</h5>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Position:</span>
|
||||
<span class="debug-attribute-value debug-coords">(${player.position.x.toFixed(1)}, ${player.position.y.toFixed(1)})</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Velocity:</span>
|
||||
<span class="debug-attribute-value debug-coords">(${player.velocity.x.toFixed(1)}, ${player.velocity.y.toFixed(1)})</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Speed:</span>
|
||||
<span class="debug-attribute-value">${player.velocity.magnitude().toFixed(1)}</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Target:</span>
|
||||
<span class="debug-attribute-value debug-coords">(${player.targetPosition.x.toFixed(1)}, ${player.targetPosition.y.toFixed(1)})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<h5 style="margin: 0 0 8px 0; color: #ccc;">Game State</h5>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Has Puck:</span>
|
||||
<span class="debug-attribute-value">${player.state.hasPuck}</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Energy:</span>
|
||||
<span class="debug-attribute-value">${player.state.energy.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Checking:</span>
|
||||
<span class="debug-attribute-value">${player.state.checking}</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Injured:</span>
|
||||
<span class="debug-attribute-value">${player.state.injured}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<h5 style="margin: 0 0 8px 0; color: #ccc;">AI State</h5>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Behavior:</span>
|
||||
<span class="debug-attribute-value">${player.aiState.behavior}</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Target:</span>
|
||||
<span class="debug-attribute-value">${player.aiState.target ? player.aiState.target.constructor.name : 'None'}</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Last Action:</span>
|
||||
<span class="debug-attribute-value">${player.aiState.lastAction.toFixed(0)}ms ago</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Reaction Time:</span>
|
||||
<span class="debug-attribute-value">${player.aiState.reactionTime.toFixed(0)}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 style="margin: 0 0 8px 0; color: #ccc;">Attributes</h5>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Speed:</span>
|
||||
<span class="debug-attribute-value">${player.attributes.speed.toFixed(0)}</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Shooting:</span>
|
||||
<span class="debug-attribute-value">${player.attributes.shooting.toFixed(0)}</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Passing:</span>
|
||||
<span class="debug-attribute-value">${player.attributes.passing.toFixed(0)}</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Defense:</span>
|
||||
<span class="debug-attribute-value">${player.attributes.defense.toFixed(0)}</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Checking:</span>
|
||||
<span class="debug-attribute-value">${player.attributes.checking.toFixed(0)}</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Puck Handling:</span>
|
||||
<span class="debug-attribute-value">${player.attributes.puckHandling.toFixed(0)}</span>
|
||||
</div>
|
||||
<div class="debug-attribute">
|
||||
<span class="debug-attribute-name">Awareness:</span>
|
||||
<span class="debug-attribute-value">${player.attributes.awareness.toFixed(0)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('debug-selected-player').innerHTML = info;
|
||||
}
|
||||
|
||||
setupGlobalDebugFunctions() {
|
||||
// Add global debug helper functions to window object
|
||||
window.debugHelpers = {
|
||||
// Get all players
|
||||
getPlayers: () => this.gameEngine.players,
|
||||
|
||||
// Get players by team
|
||||
getHomeTeam: () => this.gameEngine.players.filter(p => p.team === 'home'),
|
||||
getAwayTeam: () => this.gameEngine.players.filter(p => p.team === 'away'),
|
||||
|
||||
// Get players by position
|
||||
getPlayersByPosition: (position) => this.gameEngine.players.filter(p => p.role === position),
|
||||
getGoalies: () => this.gameEngine.players.filter(p => p.role === 'G'),
|
||||
getDefense: () => this.gameEngine.players.filter(p => p.role === 'LD' || p.role === 'RD'),
|
||||
getForwards: () => this.gameEngine.players.filter(p => ['LW', 'C', 'RW'].includes(p.role)),
|
||||
|
||||
// Get specific player
|
||||
getPlayer: (id) => this.gameEngine.players.find(p => p.id === id),
|
||||
|
||||
// Get puck
|
||||
getPuck: () => this.gameEngine.puck,
|
||||
|
||||
// Get game state
|
||||
getGameState: () => this.gameEngine.gameState,
|
||||
|
||||
// Get player with puck
|
||||
getPuckCarrier: () => this.gameEngine.players.find(p => p.state.hasPuck),
|
||||
|
||||
// Export game state for debugging
|
||||
exportGameState: () => {
|
||||
const state = {
|
||||
gameState: this.gameEngine.gameState.getGameState(),
|
||||
players: this.gameEngine.players.map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
team: p.team,
|
||||
role: p.role,
|
||||
position: { x: p.position.x, y: p.position.y },
|
||||
velocity: { x: p.velocity.x, y: p.velocity.y },
|
||||
targetPosition: { x: p.targetPosition.x, y: p.targetPosition.y },
|
||||
state: { ...p.state },
|
||||
aiState: { ...p.aiState },
|
||||
attributes: { ...p.attributes }
|
||||
})),
|
||||
puck: {
|
||||
position: { x: this.gameEngine.puck.position.x, y: this.gameEngine.puck.position.y },
|
||||
velocity: { x: this.gameEngine.puck.velocity.x, y: this.gameEngine.puck.velocity.y },
|
||||
speed: this.gameEngine.puck.getSpeed(),
|
||||
lastTouchedBy: this.gameEngine.puck.lastTouchedBy
|
||||
}
|
||||
};
|
||||
console.log('Game State Export:', state);
|
||||
return state;
|
||||
},
|
||||
|
||||
// Show debug panel
|
||||
showDebug: () => this.showDebugPanel(),
|
||||
|
||||
// Hide debug panel
|
||||
hideDebug: () => this.hideDebugPanel()
|
||||
};
|
||||
|
||||
// Log available debug functions
|
||||
console.log('Debug Helper Functions Available:');
|
||||
console.log('- debugHelpers.getPlayers() - Get all players');
|
||||
console.log('- debugHelpers.getHomeTeam() - Get home team players');
|
||||
console.log('- debugHelpers.getAwayTeam() - Get away team players');
|
||||
console.log('- debugHelpers.getPlayersByPosition(pos) - Get players by position');
|
||||
console.log('- debugHelpers.getPlayer(id) - Get specific player by ID');
|
||||
console.log('- debugHelpers.getPuck() - Get puck object');
|
||||
console.log('- debugHelpers.getGameState() - Get game state');
|
||||
console.log('- debugHelpers.getPuckCarrier() - Get player with puck');
|
||||
console.log('- debugHelpers.exportGameState() - Export full game state');
|
||||
console.log('- debugHelpers.showDebug() - Show debug panel');
|
||||
console.log('- debugHelpers.hideDebug() - Hide debug panel');
|
||||
}
|
||||
}
|
||||
@ -283,12 +283,22 @@ class Renderer {
|
||||
);
|
||||
}
|
||||
|
||||
drawDebugInfo(gameState, players, puck) {
|
||||
drawDebugInfo(gameState, players, puck, selectedPlayer = null) {
|
||||
// Always draw selected player target line if there is one, even when debug mode is off
|
||||
if (selectedPlayer) {
|
||||
this.ctx.save();
|
||||
this.applyCamera();
|
||||
this.drawSelectedPlayerTarget(selectedPlayer);
|
||||
this.ctx.restore();
|
||||
}
|
||||
|
||||
// Only draw the rest of debug info if debug mode is on
|
||||
if (!window.debugMode) return;
|
||||
|
||||
// Draw basic debug info overlay
|
||||
this.ctx.save();
|
||||
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
|
||||
this.ctx.fillRect(10, 10, 200, 150);
|
||||
this.ctx.fillRect(10, 10, 250, 180);
|
||||
|
||||
this.ctx.fillStyle = '#fff';
|
||||
this.ctx.font = '12px monospace';
|
||||
@ -298,8 +308,219 @@ class Renderer {
|
||||
this.ctx.fillText(`Game Time: ${gameState.formatTime(gameState.timeRemaining)}`, 20, 90);
|
||||
this.ctx.fillText(`Period: ${gameState.period}`, 20, 110);
|
||||
this.ctx.fillText(`Paused: ${gameState.isPaused}`, 20, 130);
|
||||
this.ctx.fillText(`Speed: ${gameState.gameSpeed}x`, 20, 150);
|
||||
this.ctx.fillText(`Faceoff: ${gameState.faceoff.isActive}`, 20, 150);
|
||||
this.ctx.fillText(`Puck Active: ${gameState.puckActive !== undefined ? gameState.puckActive : 'N/A'}`, 20, 170);
|
||||
|
||||
this.ctx.restore();
|
||||
|
||||
// Draw enhanced debug visualizations on the rink
|
||||
this.ctx.save();
|
||||
this.applyCamera();
|
||||
|
||||
this.drawDebugVectors(players, puck);
|
||||
this.drawDebugPlayerInfo(players);
|
||||
this.drawDebugPuckInfo(puck);
|
||||
|
||||
this.ctx.restore();
|
||||
}
|
||||
|
||||
drawDebugVectors(players, puck) {
|
||||
// Draw velocity vectors for players
|
||||
players.forEach(player => {
|
||||
if (player.velocity.magnitude() > 5) {
|
||||
this.drawVector(player.position, player.velocity, '#ff4444', 0.5);
|
||||
}
|
||||
|
||||
// Draw target position
|
||||
if (player.targetPosition) {
|
||||
this.drawLine(player.position, player.targetPosition, '#44ff44', 1, [5, 5]);
|
||||
}
|
||||
|
||||
// Draw AI target if exists
|
||||
if (player.aiState.target && player.aiState.target.position) {
|
||||
this.drawLine(player.position, player.aiState.target.position, '#ffff44', 1, [2, 2]);
|
||||
}
|
||||
});
|
||||
|
||||
// Draw puck velocity vector
|
||||
if (puck.velocity.magnitude() > 10) {
|
||||
this.drawVector(puck.position, puck.velocity, '#4444ff', 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
drawDebugPlayerInfo(players) {
|
||||
this.ctx.font = '10px Arial';
|
||||
|
||||
players.forEach(player => {
|
||||
const x = player.position.x;
|
||||
const y = player.position.y - player.radius - 5;
|
||||
|
||||
// Draw player ID and energy
|
||||
this.ctx.fillStyle = player.team === 'home' ? '#ff4444' : '#4444ff';
|
||||
this.ctx.fillText(`${player.role}`, x - 10, y);
|
||||
|
||||
// Draw energy bar
|
||||
const barWidth = 20;
|
||||
const barHeight = 3;
|
||||
const energyPercent = player.state.energy / 100;
|
||||
|
||||
this.ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
||||
this.ctx.fillRect(x - barWidth/2, y - 15, barWidth, barHeight);
|
||||
|
||||
this.ctx.fillStyle = energyPercent > 0.5 ? '#44ff44' : energyPercent > 0.25 ? '#ffff44' : '#ff4444';
|
||||
this.ctx.fillRect(x - barWidth/2, y - 15, barWidth * energyPercent, barHeight);
|
||||
|
||||
// Highlight puck carrier
|
||||
if (player.state.hasPuck) {
|
||||
this.ctx.strokeStyle = '#ffff00';
|
||||
this.ctx.lineWidth = 3;
|
||||
this.ctx.setLineDash([3, 3]);
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(x, player.position.y, player.radius + 5, 0, Math.PI * 2);
|
||||
this.ctx.stroke();
|
||||
this.ctx.setLineDash([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
drawDebugPuckInfo(puck) {
|
||||
const x = puck.position.x;
|
||||
const y = puck.position.y - 20;
|
||||
|
||||
this.ctx.font = '8px Arial';
|
||||
this.ctx.fillStyle = '#ffffff';
|
||||
this.ctx.fillText(`Speed: ${Math.round(puck.velocity.magnitude())}`, x - 20, y);
|
||||
|
||||
if (puck.lastTouchedBy) {
|
||||
this.ctx.fillText(`Last: ${puck.lastTouchedBy}`, x - 20, y + 10);
|
||||
}
|
||||
}
|
||||
|
||||
drawVector(position, vector, color = '#ffffff', scale = 1) {
|
||||
const endX = position.x + vector.x * scale;
|
||||
const endY = position.y + vector.y * scale;
|
||||
|
||||
this.ctx.strokeStyle = color;
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.setLineDash([]);
|
||||
|
||||
// Draw vector line
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(position.x, position.y);
|
||||
this.ctx.lineTo(endX, endY);
|
||||
this.ctx.stroke();
|
||||
|
||||
// Draw arrow head
|
||||
const angle = Math.atan2(vector.y, vector.x);
|
||||
const arrowSize = 8;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(endX, endY);
|
||||
this.ctx.lineTo(
|
||||
endX - arrowSize * Math.cos(angle - Math.PI / 6),
|
||||
endY - arrowSize * Math.sin(angle - Math.PI / 6)
|
||||
);
|
||||
this.ctx.moveTo(endX, endY);
|
||||
this.ctx.lineTo(
|
||||
endX - arrowSize * Math.cos(angle + Math.PI / 6),
|
||||
endY - arrowSize * Math.sin(angle + Math.PI / 6)
|
||||
);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
drawLine(start, end, color = '#ffffff', width = 1, dash = []) {
|
||||
this.ctx.strokeStyle = color;
|
||||
this.ctx.lineWidth = width;
|
||||
this.ctx.setLineDash(dash);
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(start.x, start.y);
|
||||
this.ctx.lineTo(end.x, end.y);
|
||||
this.ctx.stroke();
|
||||
|
||||
this.ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
drawSelectedPlayerTarget(selectedPlayer) {
|
||||
if (!selectedPlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedPlayer.targetPosition) {
|
||||
console.log('Selected player has no target position:', selectedPlayer.name);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if target is different from current position
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(selectedPlayer.targetPosition.x - selectedPlayer.position.x, 2) +
|
||||
Math.pow(selectedPlayer.targetPosition.y - selectedPlayer.position.y, 2)
|
||||
);
|
||||
|
||||
if (distance < 5) {
|
||||
// Target too close to player, not worth drawing line
|
||||
return;
|
||||
}
|
||||
|
||||
// Save current context state
|
||||
this.ctx.save();
|
||||
|
||||
// Draw a bright, prominent line from selected player to their target
|
||||
this.ctx.strokeStyle = '#00ffff'; // Cyan color for high visibility
|
||||
this.ctx.lineWidth = 6;
|
||||
this.ctx.setLineDash([12, 6]); // Larger dashed line pattern
|
||||
this.ctx.lineCap = 'round';
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(selectedPlayer.position.x, selectedPlayer.position.y);
|
||||
this.ctx.lineTo(selectedPlayer.targetPosition.x, selectedPlayer.targetPosition.y);
|
||||
this.ctx.stroke();
|
||||
|
||||
// Also draw a solid white line underneath for extra visibility
|
||||
this.ctx.strokeStyle = '#ffffff';
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.setLineDash([]);
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(selectedPlayer.position.x, selectedPlayer.position.y);
|
||||
this.ctx.lineTo(selectedPlayer.targetPosition.x, selectedPlayer.targetPosition.y);
|
||||
this.ctx.stroke();
|
||||
|
||||
// Draw target position marker (circle)
|
||||
this.ctx.setLineDash([]);
|
||||
this.ctx.fillStyle = '#00ffff';
|
||||
this.ctx.strokeStyle = '#ffffff';
|
||||
this.ctx.lineWidth = 3;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(selectedPlayer.targetPosition.x, selectedPlayer.targetPosition.y, 10, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
this.ctx.stroke();
|
||||
|
||||
// Draw crosshair in the target circle
|
||||
this.ctx.strokeStyle = '#ffffff';
|
||||
this.ctx.lineWidth = 2;
|
||||
|
||||
const targetX = selectedPlayer.targetPosition.x;
|
||||
const targetY = selectedPlayer.targetPosition.y;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(targetX - 6, targetY);
|
||||
this.ctx.lineTo(targetX + 6, targetY);
|
||||
this.ctx.moveTo(targetX, targetY - 6);
|
||||
this.ctx.lineTo(targetX, targetY + 6);
|
||||
this.ctx.stroke();
|
||||
|
||||
// Highlight the selected player with a special border
|
||||
this.ctx.strokeStyle = '#00ffff';
|
||||
this.ctx.lineWidth = 4;
|
||||
this.ctx.setLineDash([8, 4]);
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(selectedPlayer.position.x, selectedPlayer.position.y, selectedPlayer.radius + 10, 0, Math.PI * 2);
|
||||
this.ctx.stroke();
|
||||
|
||||
// Restore context state
|
||||
this.ctx.restore();
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user