Uppdate
This commit is contained in:
parent
e9585f8570
commit
1640101f65
@ -105,7 +105,13 @@ class GameState:
|
|||||||
if building.owner_id != player_id:
|
if building.owner_id != player_id:
|
||||||
return {"success": False, "error": "You don't own this building"}
|
return {"success": False, "error": "You don't own this building"}
|
||||||
|
|
||||||
building.name = name
|
# Validate name length (max 20 characters)
|
||||||
|
name = name.strip()
|
||||||
|
if len(name) > 20:
|
||||||
|
return {"success": False, "error": "Name too long (max 20 characters)"}
|
||||||
|
|
||||||
|
# Empty name is allowed (removes the label)
|
||||||
|
building.name = name if name else None
|
||||||
logger.info(f"Player {player_id} renamed building at ({x},{y}) to '{name}'.")
|
logger.info(f"Player {player_id} renamed building at ({x},{y}) to '{name}'.")
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|
||||||
|
|||||||
@ -22,10 +22,17 @@ def initialize_components(test_db_path=None):
|
|||||||
"""Initialize server components with optional test database"""
|
"""Initialize server components with optional test database"""
|
||||||
global game_state, ws_manager, economy_engine, database
|
global game_state, ws_manager, economy_engine, database
|
||||||
|
|
||||||
|
# Initialize database first
|
||||||
|
database = Database(test_db_path) if test_db_path else Database()
|
||||||
|
database.init_db()
|
||||||
|
|
||||||
|
# Create game state and load data from database
|
||||||
game_state = GameState()
|
game_state = GameState()
|
||||||
|
game_state.load_state(database.load_game_state())
|
||||||
|
|
||||||
|
# Now create WebSocketManager (it will rebuild nickname mapping from loaded players)
|
||||||
ws_manager = WebSocketManager(game_state)
|
ws_manager = WebSocketManager(game_state)
|
||||||
economy_engine = EconomyEngine(game_state)
|
economy_engine = EconomyEngine(game_state)
|
||||||
database = Database(test_db_path) if test_db_path else Database()
|
|
||||||
|
|
||||||
return game_state, ws_manager, economy_engine, database
|
return game_state, ws_manager, economy_engine, database
|
||||||
|
|
||||||
@ -84,8 +91,7 @@ async def lifespan(app: FastAPI):
|
|||||||
initialize_components()
|
initialize_components()
|
||||||
|
|
||||||
logger.info("Server starting up...")
|
logger.info("Server starting up...")
|
||||||
database.init_db()
|
# Database and game state are already initialized in initialize_components()
|
||||||
game_state.load_state(database.load_game_state())
|
|
||||||
|
|
||||||
# Start the hybrid economy loop
|
# Start the hybrid economy loop
|
||||||
task = asyncio.create_task(economy_loop())
|
task = asyncio.create_task(economy_loop())
|
||||||
@ -175,6 +181,8 @@ async def handle_message(websocket: WebSocket, data: dict):
|
|||||||
if result["success"]:
|
if result["success"]:
|
||||||
await ws_manager.broadcast({"type": "building_updated", "x": data["x"], "y": data["y"], "name": data["name"]})
|
await ws_manager.broadcast({"type": "building_updated", "x": data["x"], "y": data["y"], "name": data["name"]})
|
||||||
database.save_game_state(game_state)
|
database.save_game_state(game_state)
|
||||||
|
else:
|
||||||
|
await websocket.send_json({"type": "error", "message": result["error"]})
|
||||||
|
|
||||||
elif msg_type == "chat":
|
elif msg_type == "chat":
|
||||||
nickname = await ws_manager.get_nickname(websocket)
|
nickname = await ws_manager.get_nickname(websocket)
|
||||||
|
|||||||
@ -13,6 +13,17 @@ class WebSocketManager:
|
|||||||
self.player_nicknames: Dict[str, str] = {}
|
self.player_nicknames: Dict[str, str] = {}
|
||||||
self.nickname_to_id: Dict[str, str] = {}
|
self.nickname_to_id: Dict[str, str] = {}
|
||||||
self.game_state = game_state
|
self.game_state = game_state
|
||||||
|
|
||||||
|
# Rebuild nickname-to-ID mapping from game state
|
||||||
|
self._rebuild_nickname_mapping()
|
||||||
|
|
||||||
|
def _rebuild_nickname_mapping(self):
|
||||||
|
"""Rebuild the nickname-to-ID mapping from existing players in game state."""
|
||||||
|
for player_id, player in self.game_state.players.items():
|
||||||
|
self.nickname_to_id[player.nickname] = player_id
|
||||||
|
|
||||||
|
if self.nickname_to_id:
|
||||||
|
logger.info(f"Rebuilt nickname-to-ID mapping for {len(self.nickname_to_id)} players: {list(self.nickname_to_id.keys())}")
|
||||||
|
|
||||||
async def connect(self, websocket: WebSocket, nickname: str):
|
async def connect(self, websocket: WebSocket, nickname: str):
|
||||||
"""Connect a new player, allowing multiple connections per nickname."""
|
"""Connect a new player, allowing multiple connections per nickname."""
|
||||||
|
|||||||
@ -186,6 +186,13 @@ export class GameRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildingGroup.add(buildingMesh);
|
buildingGroup.add(buildingMesh);
|
||||||
|
|
||||||
|
// Add name label if the building has a name
|
||||||
|
if (name) {
|
||||||
|
const label = this.createBuildingLabel(name, playerColor);
|
||||||
|
buildingGroup.add(label);
|
||||||
|
}
|
||||||
|
|
||||||
return buildingGroup;
|
return buildingGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -766,6 +773,75 @@ export class GameRenderer {
|
|||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createBuildingLabel(name, playerColor) {
|
||||||
|
// Create canvas for text rendering
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Much larger canvas for better visibility
|
||||||
|
canvas.width = 1024;
|
||||||
|
canvas.height = 256;
|
||||||
|
|
||||||
|
// Clear background
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Much bigger text for better visibility
|
||||||
|
const fontSize = 120;
|
||||||
|
ctx.font = `bold ${fontSize}px Arial`;
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
|
||||||
|
// Draw colored background
|
||||||
|
const padding = 32;
|
||||||
|
const textWidth = ctx.measureText(name).width;
|
||||||
|
const bgWidth = Math.max(textWidth + padding * 2, 300);
|
||||||
|
const bgHeight = fontSize + padding * 2;
|
||||||
|
|
||||||
|
// Fill with player color
|
||||||
|
ctx.fillStyle = `rgb(${Math.floor(playerColor.r * 255)}, ${Math.floor(playerColor.g * 255)}, ${Math.floor(playerColor.b * 255)})`;
|
||||||
|
ctx.fillRect(
|
||||||
|
(canvas.width - bgWidth) / 2,
|
||||||
|
(canvas.height - bgHeight) / 2,
|
||||||
|
bgWidth,
|
||||||
|
bgHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw black text
|
||||||
|
ctx.fillStyle = 'black';
|
||||||
|
ctx.fillText(name, canvas.width / 2, canvas.height / 2);
|
||||||
|
|
||||||
|
// Create texture and material
|
||||||
|
const texture = new THREE.CanvasTexture(canvas);
|
||||||
|
const material = new THREE.MeshBasicMaterial({
|
||||||
|
map: texture,
|
||||||
|
transparent: true,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
depthTest: false // Always render on top
|
||||||
|
});
|
||||||
|
|
||||||
|
// Much larger geometry - 2x bigger than before
|
||||||
|
const geometry = new THREE.PlaneGeometry(8, 2);
|
||||||
|
const labelMesh = new THREE.Mesh(geometry, material);
|
||||||
|
|
||||||
|
// Position above building
|
||||||
|
labelMesh.position.set(0, 4.5, 0);
|
||||||
|
|
||||||
|
// Make labels always face camera (billboard effect) and appear horizontal
|
||||||
|
// This will be updated in the render loop to always face the camera
|
||||||
|
labelMesh.userData.isBillboard = true;
|
||||||
|
|
||||||
|
// Initial rotation to face the isometric camera
|
||||||
|
// Camera is positioned at (cameraPos.x + 50, 50, cameraPos.z + 50)
|
||||||
|
// We want labels to face northeast towards camera but remain horizontal
|
||||||
|
labelMesh.rotation.y = -Math.PI / 4; // Face camera direction
|
||||||
|
labelMesh.rotation.x = 0; // Keep horizontal (no tilt)
|
||||||
|
|
||||||
|
// Mark as building label
|
||||||
|
labelMesh.userData.isBuildingLabel = true;
|
||||||
|
|
||||||
|
return labelMesh;
|
||||||
|
}
|
||||||
|
|
||||||
createCursor(playerId, color) {
|
createCursor(playerId, color) {
|
||||||
const geometry = new THREE.RingGeometry(0.5, 0.7, 16);
|
const geometry = new THREE.RingGeometry(0.5, 0.7, 16);
|
||||||
const material = new THREE.MeshBasicMaterial({
|
const material = new THREE.MeshBasicMaterial({
|
||||||
@ -845,8 +921,37 @@ export class GameRenderer {
|
|||||||
updateBuildingName(x, y, name) {
|
updateBuildingName(x, y, name) {
|
||||||
const key = `${x},${y}`;
|
const key = `${x},${y}`;
|
||||||
const building = this.buildings.get(key);
|
const building = this.buildings.get(key);
|
||||||
if (building) {
|
|
||||||
building.userData.name = name;
|
if (!building) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update building userData
|
||||||
|
building.userData.name = name;
|
||||||
|
|
||||||
|
// Remove all existing labels
|
||||||
|
const labelsToRemove = [];
|
||||||
|
building.children.forEach((child, index) => {
|
||||||
|
if (child.userData && child.userData.isBuildingLabel) {
|
||||||
|
labelsToRemove.push(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
labelsToRemove.forEach(label => {
|
||||||
|
building.remove(label);
|
||||||
|
// Clean up resources
|
||||||
|
if (label.material) {
|
||||||
|
if (label.material.map) label.material.map.dispose();
|
||||||
|
label.material.dispose();
|
||||||
|
}
|
||||||
|
if (label.geometry) label.geometry.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add new label if name provided
|
||||||
|
if (name && name.trim()) {
|
||||||
|
const playerColor = this.getPlayerColor(building.userData.owner_id);
|
||||||
|
const newLabel = this.createBuildingLabel(name.trim(), playerColor);
|
||||||
|
building.add(newLabel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -936,11 +1041,37 @@ export class GameRenderer {
|
|||||||
startRenderLoop() {
|
startRenderLoop() {
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
|
|
||||||
|
// Update billboard labels to face camera
|
||||||
|
this.updateBillboards();
|
||||||
|
|
||||||
this.renderer.render(this.scene, this.camera);
|
this.renderer.render(this.scene, this.camera);
|
||||||
};
|
};
|
||||||
animate();
|
animate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateBillboards() {
|
||||||
|
// Make every billboard label face the camera horizontally (yaw only)
|
||||||
|
const camPos = this.camera.position;
|
||||||
|
this.buildings.forEach(building => {
|
||||||
|
building.traverse(child => {
|
||||||
|
if (child.userData && child.userData.isBillboard) {
|
||||||
|
// Get world position of label
|
||||||
|
const worldPos = new THREE.Vector3();
|
||||||
|
child.getWorldPosition(worldPos);
|
||||||
|
|
||||||
|
// Compute angle to camera on XZ plane
|
||||||
|
const dx = camPos.x - worldPos.x;
|
||||||
|
const dz = camPos.z - worldPos.z;
|
||||||
|
const angle = Math.atan2(dx, dz); // radians
|
||||||
|
|
||||||
|
// Apply rotation: yaw = angle, keep flat (no pitch/roll)
|
||||||
|
child.rotation.set(0, angle, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onResize() {
|
onResize() {
|
||||||
const aspect = window.innerWidth / window.innerHeight;
|
const aspect = window.innerWidth / window.innerHeight;
|
||||||
this.camera.left = -40 * aspect;
|
this.camera.left = -40 * aspect;
|
||||||
|
|||||||
@ -21,7 +21,21 @@ export class InputHandler {
|
|||||||
this.canvas.addEventListener('mouseup', (e) => this.onMouseUp(e));
|
this.canvas.addEventListener('mouseup', (e) => this.onMouseUp(e));
|
||||||
this.canvas.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
this.canvas.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
||||||
this.canvas.addEventListener('wheel', (e) => this.onWheel(e));
|
this.canvas.addEventListener('wheel', (e) => this.onWheel(e));
|
||||||
this.canvas.addEventListener('contextmenu', (e) => e.preventDefault());
|
// Prevent context menu on canvas and document
|
||||||
|
this.canvas.addEventListener('contextmenu', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also prevent on document level for better coverage
|
||||||
|
document.addEventListener('contextmenu', (e) => {
|
||||||
|
if (e.target === this.canvas || this.canvas.contains(e.target)) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
// Keyboard events
|
// Keyboard events
|
||||||
document.addEventListener('keydown', (e) => this.onKeyDown(e));
|
document.addEventListener('keydown', (e) => this.onKeyDown(e));
|
||||||
}
|
}
|
||||||
@ -56,16 +70,16 @@ export class InputHandler {
|
|||||||
const dx = Math.abs(event.clientX - this.lastMouseX);
|
const dx = Math.abs(event.clientX - this.lastMouseX);
|
||||||
const dy = Math.abs(event.clientY - this.lastMouseY);
|
const dy = Math.abs(event.clientY - this.lastMouseY);
|
||||||
if (dx < dragThreshold && dy < dragThreshold) {
|
if (dx < dragThreshold && dy < dragThreshold) {
|
||||||
// Right click on tile - show context menu
|
// Right click on tile - show context menu with improved hit detection
|
||||||
const tile = this.app.renderer.screenToWorld(event.clientX, event.clientY);
|
const tile = this.app.renderer.screenToWorld(event.clientX, event.clientY);
|
||||||
const building = this.app.gameState.buildings[`${tile.x},${tile.y}`];
|
const buildingInfo = this.findBuildingNearTile(tile.x, tile.y);
|
||||||
|
|
||||||
if (building && building.owner_id === this.app.player.player_id) {
|
if (buildingInfo && buildingInfo.building.owner_id === this.app.player.player_id) {
|
||||||
this.app.uiManager.showContextMenu(
|
this.app.uiManager.showContextMenu(
|
||||||
event.clientX,
|
event.clientX,
|
||||||
event.clientY,
|
event.clientY,
|
||||||
tile.x,
|
buildingInfo.x,
|
||||||
tile.y
|
buildingInfo.y
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -79,8 +93,17 @@ export class InputHandler {
|
|||||||
this.currentTileX = tile.x;
|
this.currentTileX = tile.x;
|
||||||
this.currentTileY = tile.y;
|
this.currentTileY = tile.y;
|
||||||
|
|
||||||
// Highlight tile
|
// Highlight tile with building detection feedback
|
||||||
this.app.renderer.highlightTile(tile.x, tile.y);
|
const buildingInfo = this.findBuildingNearTile(tile.x, tile.y);
|
||||||
|
if (buildingInfo && buildingInfo.building.owner_id === this.app.player?.player_id) {
|
||||||
|
// Highlight the actual building tile if we found one nearby
|
||||||
|
this.app.renderer.highlightTile(buildingInfo.x, buildingInfo.y);
|
||||||
|
this.canvas.style.cursor = 'pointer'; // Show pointer cursor over owned buildings
|
||||||
|
} else {
|
||||||
|
// Normal tile highlight
|
||||||
|
this.app.renderer.highlightTile(tile.x, tile.y);
|
||||||
|
this.canvas.style.cursor = this.isRightMouseDown ? 'grabbing' : 'default';
|
||||||
|
}
|
||||||
// Send cursor position to server (throttled)
|
// Send cursor position to server (throttled)
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - this.lastCursorUpdate > this.cursorUpdateThrottle) {
|
if (now - this.lastCursorUpdate > this.cursorUpdateThrottle) {
|
||||||
@ -140,4 +163,43 @@ export class InputHandler {
|
|||||||
event.preventDefault(); // Prevents the browser from scrolling
|
event.preventDefault(); // Prevents the browser from scrolling
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findBuildingNearTile(centerX, centerY) {
|
||||||
|
/**
|
||||||
|
* Improved building hit detection with expanded click area.
|
||||||
|
*
|
||||||
|
* Problem: Previously, users had to click exactly on the building's ground tile
|
||||||
|
* which was frustrating and imprecise, especially for 3D buildings.
|
||||||
|
*
|
||||||
|
* Solution: This function checks a 3x3 grid around the clicked point,
|
||||||
|
* making buildings much easier to target by expanding the effective click area.
|
||||||
|
*
|
||||||
|
* Search order: exact tile first, then adjacent tiles, then diagonal tiles
|
||||||
|
* to ensure the closest building is selected when multiple are nearby.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// First try exact tile (for precision when needed)
|
||||||
|
let building = this.app.gameState.buildings[`${centerX},${centerY}`];
|
||||||
|
if (building) {
|
||||||
|
return { building, x: centerX, y: centerY };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check a 3x3 area around the clicked tile for easier targeting
|
||||||
|
// Check closer tiles first for better UX
|
||||||
|
const searchOrder = [
|
||||||
|
[0, -1], [0, 1], [-1, 0], [1, 0], // Adjacent tiles (distance 1)
|
||||||
|
[-1, -1], [-1, 1], [1, -1], [1, 1] // Diagonal tiles (distance 1.41)
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [dx, dy] of searchOrder) {
|
||||||
|
const x = centerX + dx;
|
||||||
|
const y = centerY + dy;
|
||||||
|
building = this.app.gameState.buildings[`${x},${y}`];
|
||||||
|
if (building) {
|
||||||
|
return { building, x, y };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // No building found in area
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ class ContextMenu extends HTMLElement {
|
|||||||
id="nameInput"
|
id="nameInput"
|
||||||
placeholder="Building name..."
|
placeholder="Building name..."
|
||||||
style="width: 100%; margin-bottom: 8px;"
|
style="width: 100%; margin-bottom: 8px;"
|
||||||
maxlength="30"
|
maxlength="20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -69,6 +69,13 @@ class ContextMenu extends HTMLElement {
|
|||||||
this.currentX = tileX;
|
this.currentX = tileX;
|
||||||
this.currentY = tileY;
|
this.currentY = tileY;
|
||||||
|
|
||||||
|
// Store current building info for editing
|
||||||
|
this.currentBuilding = null;
|
||||||
|
if (this.app && this.app.gameState && this.app.gameState.buildings) {
|
||||||
|
const key = `${tileX},${tileY}`;
|
||||||
|
this.currentBuilding = this.app.gameState.buildings[key];
|
||||||
|
}
|
||||||
|
|
||||||
this.style.left = x + 'px';
|
this.style.left = x + 'px';
|
||||||
this.style.top = y + 'px';
|
this.style.top = y + 'px';
|
||||||
this.style.display = 'block';
|
this.style.display = 'block';
|
||||||
@ -87,8 +94,10 @@ class ContextMenu extends HTMLElement {
|
|||||||
this.querySelector('#editForm').style.display = 'block';
|
this.querySelector('#editForm').style.display = 'block';
|
||||||
|
|
||||||
const input = this.querySelector('#nameInput');
|
const input = this.querySelector('#nameInput');
|
||||||
input.value = '';
|
// Pre-fill with current building name if it exists
|
||||||
|
input.value = (this.currentBuilding && this.currentBuilding.name) ? this.currentBuilding.name : '';
|
||||||
input.focus();
|
input.focus();
|
||||||
|
input.select(); // Select all text for easy replacement
|
||||||
} else if (action === 'delete') {
|
} else if (action === 'delete') {
|
||||||
if (confirm('Delete this building?')) {
|
if (confirm('Delete this building?')) {
|
||||||
if (this.app) {
|
if (this.app) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user