Initial commit: Plants turn-based battle game
This commit is contained in:
+150
@@ -0,0 +1,150 @@
|
||||
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
|
||||
Reference in New Issue
Block a user