646 lines
24 KiB
Python
646 lines
24 KiB
Python
"""UI renderer for the Village Simulation.
|
|
|
|
Beautiful dark theme with panels for statistics, factions, religion, and diplomacy.
|
|
"""
|
|
|
|
import math
|
|
import pygame
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from frontend.client import SimulationState
|
|
|
|
|
|
class Colors:
|
|
# Base UI colors - dark cyberpunk theme
|
|
BG = (15, 17, 23)
|
|
PANEL_BG = (22, 26, 35)
|
|
PANEL_HEADER = (28, 33, 45)
|
|
PANEL_BORDER = (45, 55, 70)
|
|
PANEL_ACCENT = (60, 80, 110)
|
|
|
|
# Text
|
|
TEXT_PRIMARY = (225, 228, 235)
|
|
TEXT_SECONDARY = (140, 150, 165)
|
|
TEXT_HIGHLIGHT = (100, 200, 255)
|
|
TEXT_WARNING = (255, 180, 80)
|
|
TEXT_DANGER = (255, 100, 100)
|
|
TEXT_SUCCESS = (100, 220, 140)
|
|
|
|
# Day/Night
|
|
DAY_COLOR = (255, 220, 100)
|
|
NIGHT_COLOR = (100, 140, 200)
|
|
|
|
# Faction colors
|
|
FACTIONS = {
|
|
"northlands": (100, 160, 220),
|
|
"riverfolk": (70, 160, 180),
|
|
"forestkin": (90, 160, 80),
|
|
"mountaineer": (150, 120, 90),
|
|
"plainsmen": (200, 180, 100),
|
|
"neutral": (120, 120, 120),
|
|
}
|
|
|
|
# Religion colors
|
|
RELIGIONS = {
|
|
"solaris": (255, 200, 80),
|
|
"aquarius": (80, 170, 240),
|
|
"terranus": (160, 120, 70),
|
|
"ignis": (240, 100, 50),
|
|
"naturis": (100, 200, 100),
|
|
"atheist": (140, 140, 140),
|
|
}
|
|
|
|
# Scrollbar
|
|
SCROLLBAR_BG = (35, 40, 50)
|
|
SCROLLBAR_HANDLE = (70, 90, 120)
|
|
|
|
|
|
class UIRenderer:
|
|
"""Renders UI elements (HUD, panels, text info)."""
|
|
|
|
def __init__(
|
|
self,
|
|
screen: pygame.Surface,
|
|
font: pygame.font.Font,
|
|
top_panel_height: int = 50,
|
|
right_panel_width: int = 280,
|
|
bottom_panel_height: int = 60,
|
|
):
|
|
self.screen = screen
|
|
self.font = font
|
|
self.top_panel_height = top_panel_height
|
|
self.right_panel_width = right_panel_width
|
|
self.bottom_panel_height = bottom_panel_height
|
|
|
|
# Fonts
|
|
self.small_font = pygame.font.Font(None, 16)
|
|
self.medium_font = pygame.font.Font(None, 20)
|
|
self.title_font = pygame.font.Font(None, 24)
|
|
self.large_font = pygame.font.Font(None, 28)
|
|
|
|
# Scrolling state for right panel
|
|
self.scroll_offset = 0
|
|
self.max_scroll = 0
|
|
self.scroll_dragging = False
|
|
|
|
# Animation
|
|
self.animation_tick = 0
|
|
|
|
def _draw_panel_bg(
|
|
self,
|
|
rect: pygame.Rect,
|
|
title: str = None,
|
|
accent_color: tuple = None,
|
|
) -> int:
|
|
"""Draw a panel background. Returns Y position after header."""
|
|
# Main background
|
|
pygame.draw.rect(self.screen, Colors.PANEL_BG, rect, border_radius=4)
|
|
pygame.draw.rect(self.screen, Colors.PANEL_BORDER, rect, 1, border_radius=4)
|
|
|
|
y = rect.y + 6
|
|
|
|
if title:
|
|
# Header area
|
|
header_rect = pygame.Rect(rect.x, rect.y, rect.width, 24)
|
|
pygame.draw.rect(
|
|
self.screen, Colors.PANEL_HEADER, header_rect,
|
|
border_top_left_radius=4, border_top_right_radius=4,
|
|
)
|
|
|
|
# Accent line
|
|
if accent_color:
|
|
pygame.draw.line(
|
|
self.screen, accent_color,
|
|
(rect.x + 2, rect.y + 24),
|
|
(rect.x + rect.width - 2, rect.y + 24),
|
|
2,
|
|
)
|
|
|
|
# Title
|
|
text = self.medium_font.render(title, True, Colors.TEXT_PRIMARY)
|
|
self.screen.blit(text, (rect.x + 10, rect.y + 5))
|
|
y = rect.y + 30
|
|
|
|
return y
|
|
|
|
def _draw_progress_bar(
|
|
self,
|
|
x: int,
|
|
y: int,
|
|
width: int,
|
|
height: int,
|
|
value: float,
|
|
max_value: float,
|
|
color: tuple,
|
|
bg_color: tuple = (35, 40, 50),
|
|
show_label: bool = False,
|
|
label: str = "",
|
|
) -> None:
|
|
"""Draw a styled progress bar."""
|
|
# Background
|
|
pygame.draw.rect(self.screen, bg_color, (x, y, width, height), border_radius=2)
|
|
|
|
# Fill
|
|
if max_value > 0:
|
|
ratio = min(1.0, value / max_value)
|
|
fill_width = int(ratio * width)
|
|
if fill_width > 0:
|
|
pygame.draw.rect(
|
|
self.screen, color,
|
|
(x, y, fill_width, height),
|
|
border_radius=2,
|
|
)
|
|
|
|
# Label
|
|
if show_label and label:
|
|
text = self.small_font.render(label, True, Colors.TEXT_PRIMARY)
|
|
text_rect = text.get_rect(midleft=(x + 4, y + height // 2))
|
|
self.screen.blit(text, text_rect)
|
|
|
|
def draw_top_bar(self, state: "SimulationState") -> None:
|
|
"""Draw the top information bar."""
|
|
self.animation_tick += 1
|
|
|
|
rect = pygame.Rect(0, 0, self.screen.get_width(), self.top_panel_height)
|
|
pygame.draw.rect(self.screen, Colors.PANEL_BG, rect)
|
|
pygame.draw.line(
|
|
self.screen, Colors.PANEL_BORDER,
|
|
(0, self.top_panel_height - 1),
|
|
(self.screen.get_width(), self.top_panel_height - 1),
|
|
)
|
|
|
|
# Day/Night indicator with animated glow
|
|
is_night = state.time_of_day == "night"
|
|
time_color = Colors.NIGHT_COLOR if is_night else Colors.DAY_COLOR
|
|
time_text = "NIGHT" if is_night else "DAY"
|
|
|
|
# Glow effect
|
|
glow_alpha = int(100 + 50 * math.sin(self.animation_tick * 0.05))
|
|
glow_surface = pygame.Surface((40, 40), pygame.SRCALPHA)
|
|
pygame.draw.circle(glow_surface, (*time_color, glow_alpha), (20, 20), 18)
|
|
self.screen.blit(glow_surface, (10, 5))
|
|
|
|
# Time circle
|
|
pygame.draw.circle(self.screen, time_color, (30, 25), 12)
|
|
pygame.draw.circle(self.screen, Colors.PANEL_BORDER, (30, 25), 12, 1)
|
|
|
|
# Time/turn info
|
|
info_text = f"{time_text} | Day {state.day}, Step {state.step_in_day} | Turn {state.turn}"
|
|
text = self.font.render(info_text, True, Colors.TEXT_PRIMARY)
|
|
self.screen.blit(text, (55, 14))
|
|
|
|
# Agent count
|
|
living = len(state.get_living_agents())
|
|
total = len(state.agents)
|
|
agent_text = f"Population: {living}/{total}"
|
|
color = Colors.TEXT_SUCCESS if living > total * 0.5 else Colors.TEXT_WARNING
|
|
if living < total * 0.25:
|
|
color = Colors.TEXT_DANGER
|
|
text = self.medium_font.render(agent_text, True, color)
|
|
self.screen.blit(text, (300, 16))
|
|
|
|
# Active wars indicator
|
|
active_wars = len(state.active_wars)
|
|
if active_wars > 0:
|
|
war_pulse = 0.5 + 0.5 * math.sin(self.animation_tick * 0.1)
|
|
war_color = (int(200 * war_pulse), int(60 * war_pulse), int(60 * war_pulse))
|
|
war_text = f"⚔ {active_wars} WAR{'S' if active_wars > 1 else ''}"
|
|
text = self.medium_font.render(war_text, True, war_color)
|
|
self.screen.blit(text, (450, 16))
|
|
|
|
# Mode and status (right side)
|
|
right_x = self.screen.get_width() - 180
|
|
|
|
mode_color = Colors.TEXT_HIGHLIGHT if state.mode == "auto" else Colors.TEXT_SECONDARY
|
|
mode_text = f"Mode: {state.mode.upper()}"
|
|
text = self.medium_font.render(mode_text, True, mode_color)
|
|
self.screen.blit(text, (right_x, 10))
|
|
|
|
# Running status
|
|
if state.is_running:
|
|
status_text = "● RUNNING"
|
|
status_color = Colors.TEXT_SUCCESS
|
|
else:
|
|
status_text = "○ STOPPED"
|
|
status_color = Colors.TEXT_SECONDARY
|
|
|
|
text = self.medium_font.render(status_text, True, status_color)
|
|
self.screen.blit(text, (right_x, 28))
|
|
|
|
def draw_right_panel(self, state: "SimulationState") -> None:
|
|
"""Draw the right information panel with scrollable content."""
|
|
panel_x = self.screen.get_width() - self.right_panel_width
|
|
panel_height = self.screen.get_height() - self.top_panel_height - self.bottom_panel_height
|
|
|
|
# Main panel background
|
|
rect = pygame.Rect(
|
|
panel_x, self.top_panel_height,
|
|
self.right_panel_width, panel_height,
|
|
)
|
|
pygame.draw.rect(self.screen, Colors.PANEL_BG, rect)
|
|
pygame.draw.line(
|
|
self.screen, Colors.PANEL_BORDER,
|
|
(panel_x, self.top_panel_height),
|
|
(panel_x, self.screen.get_height() - self.bottom_panel_height),
|
|
)
|
|
|
|
# Content area with padding
|
|
content_x = panel_x + 12
|
|
content_width = self.right_panel_width - 24
|
|
y = self.top_panel_height + 10
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# STATISTICS SECTION
|
|
# ═══════════════════════════════════════════════════════════════
|
|
y = self._draw_stats_section(state, content_x, y, content_width)
|
|
y += 15
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# FACTIONS SECTION
|
|
# ═══════════════════════════════════════════════════════════════
|
|
y = self._draw_factions_section(state, content_x, y, content_width)
|
|
y += 15
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# RELIGION SECTION
|
|
# ═══════════════════════════════════════════════════════════════
|
|
y = self._draw_religion_section(state, content_x, y, content_width)
|
|
y += 15
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# DIPLOMACY SECTION
|
|
# ═══════════════════════════════════════════════════════════════
|
|
y = self._draw_diplomacy_section(state, content_x, y, content_width)
|
|
y += 15
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MARKET SECTION
|
|
# ═══════════════════════════════════════════════════════════════
|
|
y = self._draw_market_section(state, content_x, y, content_width)
|
|
|
|
def _draw_stats_section(
|
|
self, state: "SimulationState", x: int, y: int, width: int
|
|
) -> int:
|
|
"""Draw statistics section."""
|
|
# Section header
|
|
text = self.title_font.render("📊 Statistics", True, Colors.TEXT_HIGHLIGHT)
|
|
self.screen.blit(text, (x, y))
|
|
y += 24
|
|
|
|
stats = state.statistics
|
|
|
|
# Population bar
|
|
living = len(state.get_living_agents())
|
|
total = len(state.agents)
|
|
pop_color = Colors.TEXT_SUCCESS if living > total * 0.5 else Colors.TEXT_WARNING
|
|
|
|
pygame.draw.rect(
|
|
self.screen, Colors.SCROLLBAR_BG,
|
|
(x, y, width, 14), border_radius=2,
|
|
)
|
|
if total > 0:
|
|
ratio = living / total
|
|
pygame.draw.rect(
|
|
self.screen, pop_color,
|
|
(x, y, int(width * ratio), 14), border_radius=2,
|
|
)
|
|
pop_text = self.small_font.render(f"Pop: {living}/{total}", True, Colors.TEXT_PRIMARY)
|
|
self.screen.blit(pop_text, (x + 4, y + 1))
|
|
y += 20
|
|
|
|
# Deaths and money
|
|
deaths = stats.get("total_agents_died", 0)
|
|
total_money = stats.get("total_money_in_circulation", 0)
|
|
|
|
text = self.small_font.render(f"Deaths: {deaths}", True, Colors.TEXT_DANGER)
|
|
self.screen.blit(text, (x, y))
|
|
|
|
text = self.small_font.render(f"💰 {total_money}", True, (255, 215, 0))
|
|
self.screen.blit(text, (x + width // 2, y))
|
|
y += 16
|
|
|
|
# Average faith
|
|
avg_faith = state.get_avg_faith()
|
|
text = self.small_font.render(f"Avg Faith: {avg_faith:.0f}%", True, Colors.TEXT_SECONDARY)
|
|
self.screen.blit(text, (x, y))
|
|
y += 16
|
|
|
|
return y
|
|
|
|
def _draw_factions_section(
|
|
self, state: "SimulationState", x: int, y: int, width: int
|
|
) -> int:
|
|
"""Draw factions section with distribution bars."""
|
|
# Section header
|
|
text = self.title_font.render("⚔ Factions", True, (180, 160, 120))
|
|
self.screen.blit(text, (x, y))
|
|
y += 22
|
|
|
|
faction_stats = state.get_faction_stats()
|
|
total = sum(faction_stats.values()) or 1
|
|
|
|
# Sort by count
|
|
sorted_factions = sorted(
|
|
faction_stats.items(),
|
|
key=lambda x: x[1],
|
|
reverse=True
|
|
)
|
|
|
|
for faction, count in sorted_factions[:5]: # Top 5
|
|
color = Colors.FACTIONS.get(faction, Colors.FACTIONS["neutral"])
|
|
ratio = count / total
|
|
|
|
# Faction bar
|
|
bar_width = int(width * 0.6 * ratio * (total / max(1, sorted_factions[0][1])))
|
|
bar_width = max(4, min(bar_width, int(width * 0.6)))
|
|
|
|
pygame.draw.rect(
|
|
self.screen, (*color, 180),
|
|
(x, y, bar_width, 10), border_radius=2,
|
|
)
|
|
|
|
# Faction name and count
|
|
label = f"{faction[:8]}: {count}"
|
|
text = self.small_font.render(label, True, color)
|
|
self.screen.blit(text, (x + bar_width + 8, y - 1))
|
|
y += 14
|
|
|
|
return y
|
|
|
|
def _draw_religion_section(
|
|
self, state: "SimulationState", x: int, y: int, width: int
|
|
) -> int:
|
|
"""Draw religion section with distribution."""
|
|
# Section header
|
|
text = self.title_font.render("✦ Religions", True, (200, 180, 220))
|
|
self.screen.blit(text, (x, y))
|
|
y += 22
|
|
|
|
religion_stats = state.get_religion_stats()
|
|
total = sum(religion_stats.values()) or 1
|
|
|
|
# Sort by count
|
|
sorted_religions = sorted(
|
|
religion_stats.items(),
|
|
key=lambda x: x[1],
|
|
reverse=True
|
|
)
|
|
|
|
for religion, count in sorted_religions[:5]: # Top 5
|
|
color = Colors.RELIGIONS.get(religion, Colors.RELIGIONS["atheist"])
|
|
ratio = count / total
|
|
|
|
# Religion color dot
|
|
pygame.draw.circle(self.screen, color, (x + 5, y + 5), 4)
|
|
|
|
# Religion name, count, and percentage
|
|
pct = ratio * 100
|
|
label = f"{religion[:8]}: {count} ({pct:.0f}%)"
|
|
text = self.small_font.render(label, True, Colors.TEXT_SECONDARY)
|
|
self.screen.blit(text, (x + 14, y))
|
|
y += 14
|
|
|
|
return y
|
|
|
|
def _draw_diplomacy_section(
|
|
self, state: "SimulationState", x: int, y: int, width: int
|
|
) -> int:
|
|
"""Draw diplomacy section with wars and treaties."""
|
|
# Section header
|
|
text = self.title_font.render("🏛 Diplomacy", True, (120, 180, 200))
|
|
self.screen.blit(text, (x, y))
|
|
y += 22
|
|
|
|
# Active wars
|
|
active_wars = state.active_wars
|
|
if active_wars:
|
|
text = self.small_font.render("Active Wars:", True, Colors.TEXT_DANGER)
|
|
self.screen.blit(text, (x, y))
|
|
y += 14
|
|
|
|
for war in active_wars[:3]: # Show up to 3 wars
|
|
f1 = war.get("faction1", "?")[:6]
|
|
f2 = war.get("faction2", "?")[:6]
|
|
c1 = Colors.FACTIONS.get(war.get("faction1", "neutral"), (150, 150, 150))
|
|
c2 = Colors.FACTIONS.get(war.get("faction2", "neutral"), (150, 150, 150))
|
|
|
|
# War indicator
|
|
pygame.draw.circle(self.screen, c1, (x + 5, y + 5), 4)
|
|
text = self.small_font.render(" ⚔ ", True, (200, 80, 80))
|
|
self.screen.blit(text, (x + 12, y - 1))
|
|
pygame.draw.circle(self.screen, c2, (x + 35, y + 5), 4)
|
|
|
|
war_text = f"{f1} vs {f2}"
|
|
text = self.small_font.render(war_text, True, Colors.TEXT_SECONDARY)
|
|
self.screen.blit(text, (x + 45, y))
|
|
y += 14
|
|
else:
|
|
text = self.small_font.render("☮ No active wars", True, Colors.TEXT_SUCCESS)
|
|
self.screen.blit(text, (x, y))
|
|
y += 14
|
|
|
|
# Peace treaties
|
|
peace_treaties = state.peace_treaties
|
|
if peace_treaties:
|
|
text = self.small_font.render(
|
|
f"Peace Treaties: {len(peace_treaties)}",
|
|
True, Colors.TEXT_SUCCESS
|
|
)
|
|
self.screen.blit(text, (x, y))
|
|
y += 14
|
|
|
|
# Recent diplomatic events
|
|
recent_events = state.diplomatic_events[:2]
|
|
if recent_events:
|
|
y += 4
|
|
for event in recent_events:
|
|
event_type = event.get("type", "unknown")
|
|
if event_type == "war_declared":
|
|
color = Colors.TEXT_DANGER
|
|
icon = "⚔"
|
|
elif event_type == "peace_made":
|
|
color = Colors.TEXT_SUCCESS
|
|
icon = "☮"
|
|
else:
|
|
color = Colors.TEXT_SECONDARY
|
|
icon = "•"
|
|
|
|
desc = event.get("description", event_type)[:25]
|
|
text = self.small_font.render(f"{icon} {desc}", True, color)
|
|
self.screen.blit(text, (x, y))
|
|
y += 12
|
|
|
|
return y
|
|
|
|
def _draw_market_section(
|
|
self, state: "SimulationState", x: int, y: int, width: int
|
|
) -> int:
|
|
"""Draw market section with prices."""
|
|
# Section header
|
|
text = self.title_font.render("💹 Market", True, (100, 200, 150))
|
|
self.screen.blit(text, (x, y))
|
|
y += 22
|
|
|
|
# Order count
|
|
order_count = len(state.market_orders)
|
|
text = self.small_font.render(
|
|
f"Active Orders: {order_count}",
|
|
True, Colors.TEXT_SECONDARY
|
|
)
|
|
self.screen.blit(text, (x, y))
|
|
y += 16
|
|
|
|
# Price summary (show resources with stock)
|
|
prices = state.market_prices
|
|
shown = 0
|
|
for resource, data in sorted(prices.items()):
|
|
if shown >= 6: # Limit display
|
|
break
|
|
|
|
total_available = data.get("total_available", 0)
|
|
if total_available > 0:
|
|
price = data.get("lowest_price", "?")
|
|
|
|
# Resource color coding
|
|
if "oil" in resource.lower() or "fuel" in resource.lower():
|
|
res_color = (180, 160, 100)
|
|
elif "meat" in resource.lower():
|
|
res_color = (200, 120, 100)
|
|
elif "water" in resource.lower():
|
|
res_color = (100, 160, 200)
|
|
else:
|
|
res_color = Colors.TEXT_SECONDARY
|
|
|
|
res_text = f"{resource[:6]}: {total_available}x @ {price}c"
|
|
text = self.small_font.render(res_text, True, res_color)
|
|
self.screen.blit(text, (x, y))
|
|
y += 14
|
|
shown += 1
|
|
|
|
if shown == 0:
|
|
text = self.small_font.render("No items for sale", True, Colors.TEXT_SECONDARY)
|
|
self.screen.blit(text, (x, y))
|
|
y += 14
|
|
|
|
return y
|
|
|
|
def draw_bottom_bar(self, state: "SimulationState") -> None:
|
|
"""Draw bottom information bar with event log."""
|
|
bar_y = self.screen.get_height() - self.bottom_panel_height
|
|
rect = pygame.Rect(0, bar_y, self.screen.get_width(), self.bottom_panel_height)
|
|
|
|
pygame.draw.rect(self.screen, Colors.PANEL_BG, rect)
|
|
pygame.draw.line(
|
|
self.screen, Colors.PANEL_BORDER,
|
|
(0, bar_y), (self.screen.get_width(), bar_y),
|
|
)
|
|
|
|
# Recent events (religious + diplomatic)
|
|
x = 15
|
|
y = bar_y + 8
|
|
|
|
text = self.medium_font.render("Recent Events:", True, Colors.TEXT_SECONDARY)
|
|
self.screen.blit(text, (x, y))
|
|
x += 120
|
|
|
|
# Show recent religious events
|
|
for event in state.religious_events[:2]:
|
|
event_type = event.get("type", "")
|
|
desc = event.get("description", event_type)[:30]
|
|
|
|
if event_type == "conversion":
|
|
color = Colors.RELIGIONS.get(event.get("to_religion", "atheist"), (150, 150, 150))
|
|
elif event_type == "prayer":
|
|
color = (180, 160, 220)
|
|
else:
|
|
color = Colors.TEXT_SECONDARY
|
|
|
|
text = self.small_font.render(f"✦ {desc}", True, color)
|
|
self.screen.blit(text, (x, y))
|
|
x += text.get_width() + 20
|
|
|
|
# Show recent diplomatic events
|
|
for event in state.diplomatic_events[:2]:
|
|
event_type = event.get("type", "")
|
|
desc = event.get("description", event_type)[:30]
|
|
|
|
if "war" in event_type.lower():
|
|
color = Colors.TEXT_DANGER
|
|
icon = "⚔"
|
|
elif "peace" in event_type.lower():
|
|
color = Colors.TEXT_SUCCESS
|
|
icon = "☮"
|
|
else:
|
|
color = Colors.TEXT_SECONDARY
|
|
icon = "🏛"
|
|
|
|
text = self.small_font.render(f"{icon} {desc}", True, color)
|
|
self.screen.blit(text, (x, y))
|
|
x += text.get_width() + 20
|
|
|
|
# If no events, show placeholder
|
|
if not state.religious_events and not state.diplomatic_events:
|
|
text = self.small_font.render(
|
|
"No recent events",
|
|
True, Colors.TEXT_SECONDARY
|
|
)
|
|
self.screen.blit(text, (x, y))
|
|
|
|
def draw_connection_status(self, connected: bool) -> None:
|
|
"""Draw connection status overlay when disconnected."""
|
|
if connected:
|
|
return
|
|
|
|
# Semi-transparent overlay
|
|
overlay = pygame.Surface(self.screen.get_size(), pygame.SRCALPHA)
|
|
overlay.fill((0, 0, 0, 200))
|
|
self.screen.blit(overlay, (0, 0))
|
|
|
|
# Connection box
|
|
box_w, box_h = 400, 150
|
|
box_x = (self.screen.get_width() - box_w) // 2
|
|
box_y = (self.screen.get_height() - box_h) // 2
|
|
|
|
pygame.draw.rect(
|
|
self.screen, Colors.PANEL_BG,
|
|
(box_x, box_y, box_w, box_h), border_radius=10,
|
|
)
|
|
pygame.draw.rect(
|
|
self.screen, Colors.PANEL_ACCENT,
|
|
(box_x, box_y, box_w, box_h), 2, border_radius=10,
|
|
)
|
|
|
|
# Pulsing dot
|
|
pulse = 0.5 + 0.5 * math.sin(self.animation_tick * 0.1)
|
|
dot_color = (int(255 * pulse), int(180 * pulse), int(80 * pulse))
|
|
pygame.draw.circle(
|
|
self.screen, dot_color,
|
|
(box_x + 30, box_y + 40), 8,
|
|
)
|
|
|
|
# Text
|
|
text = self.large_font.render("Connecting to server...", True, Colors.TEXT_WARNING)
|
|
text_rect = text.get_rect(center=(self.screen.get_width() // 2, box_y + 40))
|
|
self.screen.blit(text, text_rect)
|
|
|
|
hint = self.medium_font.render(
|
|
"Make sure the backend is running on localhost:8000",
|
|
True, Colors.TEXT_SECONDARY
|
|
)
|
|
hint_rect = hint.get_rect(center=(self.screen.get_width() // 2, box_y + 80))
|
|
self.screen.blit(hint, hint_rect)
|
|
|
|
cmd = self.small_font.render(
|
|
"Run: python -m backend.main",
|
|
True, Colors.TEXT_HIGHLIGHT
|
|
)
|
|
cmd_rect = cmd.get_rect(center=(self.screen.get_width() // 2, box_y + 110))
|
|
self.screen.blit(cmd, cmd_rect)
|
|
|
|
def draw(self, state: "SimulationState") -> None:
|
|
"""Draw all UI elements."""
|
|
self.draw_top_bar(state)
|
|
self.draw_right_panel(state)
|
|
self.draw_bottom_bar(state)
|