import sys import ctypes from ctypes import wintypes import pygame from assets import plot_icon_path, rune_icon_path, type_icon_path # Set when the user closes the window (X button). Screens keep running loops, # but the top-level driver (main.py) checks this after every screen and bails # out of the whole app so closing never lands you on another menu. QUIT_REQUESTED = False _desktop = None def request_quit(): global QUIT_REQUESTED QUIT_REQUESTED = True def _work_area(): """Usable desktop area (excludes the Windows taskbar), or None if unavailable.""" if sys.platform != "win32": return None try: rect = wintypes.RECT() ctypes.windll.user32.SystemParametersInfoW(0x0030, 0, ctypes.byref(rect), 0) # SPI_GETWORKAREA return rect.right - rect.left, rect.bottom - rect.top except Exception: return None def _desktop_size(): """The actual monitor resolution, cached after the first call. On macOS, pygame.display.Info() reports the *current window* size, so using it here makes each make_window() treat the last window as the available space and the window shrinks on every screen transition. get_desktop_sizes() is window-independent.""" global _desktop if _desktop is None: sizes = pygame.display.get_desktop_sizes() if sizes: _desktop = sizes[0] else: info = pygame.display.Info() _desktop = (info.current_w, info.current_h) return _desktop def make_window(logical_w: int, logical_h: int): """Create a window sized to fit the usable desktop (downscales only, keeps aspect ratio). Returns (window_surface, scale_factor). Render at the logical size and smoothscale into the window each frame; divide mouse coordinates by `scale` to get logical coordinates.""" pygame.display.init() area = _work_area() or _desktop_size() avail_w = max(1, area[0] - 24) # window border safety avail_h = max(1, area[1] - 48) # title bar safety scale = min(1.0, avail_w / logical_w, avail_h / logical_h) scale = max(0.2, scale) size = (int(logical_w * scale), int(logical_h * scale)) window = pygame.display.set_mode(size) return window, scale def to_logical(pos, scale: float): return (int(pos[0] / scale), int(pos[1] / scale)) def fit_surface(img, size): """Scale `img` to fit within a `size`x`size` box, preserving aspect ratio. Square sources become `size`x`size`; non-square sources shrink the smaller axis so nothing is stretched or squished (e.g. a portrait water drop).""" w, h = img.get_size() scale = size / max(w, h) nw = max(1, int(round(w * scale))) nh = max(1, int(round(h * scale))) return pygame.transform.smoothscale(img, (nw, nh)) _TYPE_ICON_CACHE = {} _PLOT_ICON_CACHE = {} _RUNE_ICON_CACHE = {} def load_type_icon(ptype: str, size: int): """Plant type icon scaled to fit a `size` box (aspect preserved), cached.""" key = (ptype, size) if key not in _TYPE_ICON_CACHE: try: img = pygame.image.load(type_icon_path(ptype)).convert_alpha() _TYPE_ICON_CACHE[key] = fit_surface(img, size) except (pygame.error, OSError): _TYPE_ICON_CACHE[key] = None return _TYPE_ICON_CACHE[key] def load_plot_icon(name: str, size: int): """Soil plot sprite scaled to fit a `size` box (aspect preserved), cached.""" key = (name, size) if key not in _PLOT_ICON_CACHE: try: img = pygame.image.load(plot_icon_path(name)).convert_alpha() _PLOT_ICON_CACHE[key] = fit_surface(img, size) except (pygame.error, OSError): _PLOT_ICON_CACHE[key] = None return _PLOT_ICON_CACHE[key] def load_rune_icon(color: str, size: int): """Rune sprite scaled to fit a `size` box (aspect preserved), cached. Falls back to the brown rune sprite if a color's icon is missing.""" key = (color, size) if key not in _RUNE_ICON_CACHE: try: img = pygame.image.load(rune_icon_path(color)).convert_alpha() _RUNE_ICON_CACHE[key] = fit_surface(img, size) except (pygame.error, OSError): _RUNE_ICON_CACHE[key] = None return _RUNE_ICON_CACHE[key] def draw_type_icon(screen, ptype, pos, font, color, size, center=False): """Draw a plant type icon (or the type name as fallback text) at `pos`, returning its rect. Uses midleft alignment unless `center` is True.""" ic = load_type_icon(ptype, size) if ic is None: surf = font.render(ptype, True, color) rect = surf.get_rect(center=pos) if center else surf.get_rect(midleft=pos) screen.blit(surf, rect) return rect rect = ic.get_rect(center=pos) if center else ic.get_rect(midleft=pos) screen.blit(ic, rect) return rect def wrap_text(text, font, width): """Split `text` into lines that each fit within `width` pixels.""" words = text.split() lines = [] cur = "" for w in words: if font.size(cur + " " + w)[0] > width and cur: lines.append(cur) cur = w else: cur = (cur + " " + w).strip() if cur: lines.append(cur) return lines