150 lines
5.1 KiB
Python
150 lines
5.1 KiB
Python
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 |