Initial commit: Plants turn-based battle game
This commit is contained in:
+315
@@ -0,0 +1,315 @@
|
||||
import pygame
|
||||
|
||||
from engine import collection
|
||||
from engine.growth import MAX_LEVEL
|
||||
from engine.plants import POOL, RARITY_COLORS, plant_rarity
|
||||
from engine.skills import skill_from_dict
|
||||
from engine.unit import ROLE_NAMES
|
||||
from ui.menu import _bracket_text
|
||||
from ui.scaling import make_window, request_quit, to_logical
|
||||
|
||||
WIN_W, WIN_H = 1040, 720
|
||||
BG = (24, 30, 24)
|
||||
TEXT = (230, 235, 230)
|
||||
MUTED = (150, 155, 150)
|
||||
HINT = (255, 220, 120)
|
||||
PANEL = (32, 38, 32)
|
||||
BTN = (60, 90, 60)
|
||||
BTN_HOVER = (85, 125, 85)
|
||||
ACCENT = (120, 220, 190)
|
||||
DANGER = (150, 70, 70)
|
||||
|
||||
ORDINALS = {1: "2nd", 2: "3rd"}
|
||||
|
||||
|
||||
class ExtractionScreen:
|
||||
def __init__(self):
|
||||
pygame.init()
|
||||
self.window, self.scale = make_window(WIN_W, WIN_H)
|
||||
self.screen = pygame.Surface((WIN_W, WIN_H))
|
||||
pygame.display.set_caption("Plants — Ability Extraction")
|
||||
self.clock = pygame.time.Clock()
|
||||
self.font = pygame.font.SysFont("segoeui", 30)
|
||||
self.bold = pygame.font.SysFont("segoeui", 20, bold=True)
|
||||
self.small = pygame.font.SysFont("segoeui", 16)
|
||||
self.tiny = pygame.font.SysFont("segoeui", 13)
|
||||
self.back_rect = pygame.Rect(16, 16, 90, 34)
|
||||
self.step_rect = pygame.Rect(116, 16, 120, 34)
|
||||
self.tab_rects = {
|
||||
"extract": pygame.Rect(WIN_W // 2 - 210, 78, 200, 36),
|
||||
"inject": pygame.Rect(WIN_W // 2 + 10, 78, 200, 36),
|
||||
}
|
||||
self.tab = "extract"
|
||||
self.phase = "list" # extract: list -> skills ; inject: ability -> plant -> slot
|
||||
self.source = None # selected source plant (extract)
|
||||
self.ability = None # selected extracted-ability index (inject)
|
||||
self.target = None # selected target plant (inject)
|
||||
self.item_rects = [] # (key, rect) for the current phase list
|
||||
self.confirm = None # dict, or None
|
||||
self.confirm_ok = pygame.Rect(0, 0, 0, 0)
|
||||
self.confirm_no = pygame.Rect(0, 0, 0, 0)
|
||||
self.mouse_pos = (0, 0)
|
||||
|
||||
# ---- helpers -------------------------------------------------------
|
||||
def _deep(self):
|
||||
if self.tab == "extract":
|
||||
return self.phase == "skills"
|
||||
return self.phase in ("plant", "slot")
|
||||
|
||||
def _reset(self, tab):
|
||||
self.tab = tab
|
||||
self.phase = "list" if tab == "extract" else "ability"
|
||||
self.source = None
|
||||
self.ability = None
|
||||
self.target = None
|
||||
self.confirm = None
|
||||
|
||||
# ---- click ---------------------------------------------------------
|
||||
def _handle(self, key):
|
||||
if self.tab == "extract":
|
||||
if self.phase == "list":
|
||||
self.source = key
|
||||
self.phase = "skills"
|
||||
elif self.phase == "skills":
|
||||
self.confirm = {"kind": "extract", "name": self.source, "index": key}
|
||||
else:
|
||||
if self.phase == "ability":
|
||||
self.ability = key
|
||||
self.phase = "plant"
|
||||
elif self.phase == "plant":
|
||||
self.target = key
|
||||
self.phase = "slot"
|
||||
elif self.phase == "slot":
|
||||
self.confirm = {"kind": "inject", "ability": self.ability,
|
||||
"target": self.target, "slot": key}
|
||||
|
||||
def _confirm_ok(self):
|
||||
c = self.confirm
|
||||
if c["kind"] == "extract":
|
||||
if collection.extract_skill(c["name"], c["index"]):
|
||||
self.phase = "list"
|
||||
self.source = None
|
||||
else:
|
||||
if collection.inject_skill(c["ability"], c["target"], c["slot"]):
|
||||
self.phase = "ability"
|
||||
self.ability = None
|
||||
self.target = None
|
||||
self.confirm = None
|
||||
|
||||
def run(self):
|
||||
running = True
|
||||
while running:
|
||||
self.clock.tick(60)
|
||||
self.mouse_pos = to_logical(pygame.mouse.get_pos(), self.scale)
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
request_quit()
|
||||
running = False
|
||||
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||||
pos = to_logical(event.pos, self.scale)
|
||||
if self.confirm is not None:
|
||||
if self.confirm_ok.collidepoint(pos):
|
||||
self._confirm_ok()
|
||||
elif self.confirm_no.collidepoint(pos):
|
||||
self.confirm = None
|
||||
continue
|
||||
if self.back_rect.collidepoint(pos):
|
||||
running = False
|
||||
continue
|
||||
if self.step_rect.collidepoint(pos) and self._deep():
|
||||
self._step_back()
|
||||
continue
|
||||
if self.tab_rects["extract"].collidepoint(pos):
|
||||
if self.tab != "extract":
|
||||
self._reset("extract")
|
||||
continue
|
||||
if self.tab_rects["inject"].collidepoint(pos):
|
||||
if self.tab != "inject":
|
||||
self._reset("inject")
|
||||
continue
|
||||
for key, rect in self.item_rects:
|
||||
if rect.collidepoint(pos):
|
||||
self._handle(key)
|
||||
break
|
||||
self._draw()
|
||||
pygame.transform.smoothscale(self.screen, self.window.get_size(), self.window)
|
||||
pygame.display.flip()
|
||||
|
||||
def _step_back(self):
|
||||
if self.tab == "extract":
|
||||
self.phase = "list"
|
||||
self.source = None
|
||||
else:
|
||||
if self.phase == "slot":
|
||||
self.phase = "plant"
|
||||
self.target = None
|
||||
elif self.phase == "plant":
|
||||
self.phase = "ability"
|
||||
self.ability = None
|
||||
|
||||
# ---- drawing -------------------------------------------------------
|
||||
def _draw(self):
|
||||
self.item_rects = []
|
||||
self.screen.fill(BG)
|
||||
pygame.draw.rect(self.screen, BTN, self.back_rect, border_radius=6)
|
||||
t = self.small.render("Back", True, TEXT)
|
||||
self.screen.blit(t, t.get_rect(center=self.back_rect.center))
|
||||
if self._deep():
|
||||
pygame.draw.rect(self.screen, BTN, self.step_rect, border_radius=6)
|
||||
st = self.small.render("← Back", True, TEXT)
|
||||
self.screen.blit(st, st.get_rect(center=self.step_rect.center))
|
||||
title = self.font.render("Ability Extraction", True, HINT)
|
||||
self.screen.blit(title, title.get_rect(center=(WIN_W // 2, 38)))
|
||||
sub = self.small.render("Sacrifice a maxed plant to copy an ability; inject it into a same-role plant.",
|
||||
True, MUTED)
|
||||
self.screen.blit(sub, sub.get_rect(center=(WIN_W // 2, 68)))
|
||||
for name, rect in self.tab_rects.items():
|
||||
active = self.tab == name
|
||||
pygame.draw.rect(self.screen, BTN_HOVER if active else BTN, rect, border_radius=8)
|
||||
pygame.draw.rect(self.screen, (150, 190, 150) if active else (70, 80, 70),
|
||||
rect, 2 if active else 1, border_radius=8)
|
||||
tt = self.small.render(name.capitalize(), True, TEXT)
|
||||
self.screen.blit(tt, tt.get_rect(center=rect.center))
|
||||
if self.confirm is not None:
|
||||
self._draw_confirm()
|
||||
elif self.tab == "extract":
|
||||
self._draw_extract()
|
||||
else:
|
||||
self._draw_inject()
|
||||
|
||||
# ---- extract tab ---------------------------------------------------
|
||||
def _draw_extract(self):
|
||||
if self.phase == "list":
|
||||
sources = sorted(n for n in collection.owned() if collection.can_extract(n) == "")
|
||||
if not sources:
|
||||
t = self.small.render(
|
||||
f"No plant is ready yet. A plant needs Lv {MAX_LEVEL} and 2 evolutions.", True, MUTED)
|
||||
self.screen.blit(t, t.get_rect(center=(WIN_W // 2, 220)))
|
||||
return
|
||||
rows = []
|
||||
for n in sources:
|
||||
spec = POOL[n]
|
||||
rows.append((n, n, RARITY_COLORS.get(plant_rarity(n), TEXT),
|
||||
f"{ROLE_NAMES.get(spec['role'], spec['role'])} · Lv {MAX_LEVEL} · 2 evolutions"))
|
||||
self._draw_items(rows, "Plants ready to extract from:")
|
||||
else:
|
||||
self._draw_extract_skills()
|
||||
|
||||
def _draw_extract_skills(self):
|
||||
rows = []
|
||||
for i, sk in enumerate(POOL[self.source]["skills"]):
|
||||
reason = collection.extraction_blocked_reason(self.source, i, sk)
|
||||
if reason:
|
||||
rows.append((i, sk.name, MUTED, f"Not extractable — {reason}", False))
|
||||
else:
|
||||
rows.append((i, sk.name, ACCENT, _skill_sub(sk)))
|
||||
self._draw_items(rows, f"Extract from {self.source}:")
|
||||
|
||||
# ---- inject tab ----------------------------------------------------
|
||||
def _draw_inject(self):
|
||||
if self.phase == "ability":
|
||||
ext = collection.extracted_skills()
|
||||
if not ext:
|
||||
t = self.small.render("No extracted abilities yet — extract one first.", True, MUTED)
|
||||
self.screen.blit(t, t.get_rect(center=(WIN_W // 2, 220)))
|
||||
return
|
||||
rows = [(i, r["skill_name"], ACCENT,
|
||||
f"from {r['source']} · {ROLE_NAMES.get(r['role'], r['role'])}")
|
||||
for i, r in enumerate(ext)]
|
||||
self._draw_items(rows, "Pick an extracted ability:")
|
||||
elif self.phase == "plant":
|
||||
rec = collection.extracted_skills()[self.ability]
|
||||
targets = sorted(n for n in collection.owned() if collection.can_inject(n, rec) == "")
|
||||
if not targets:
|
||||
t = self.small.render("No eligible plants (same role, no injection).", True, MUTED)
|
||||
self.screen.blit(t, (60, 150))
|
||||
return
|
||||
rows = [(n, n, RARITY_COLORS.get(plant_rarity(n), TEXT),
|
||||
ROLE_NAMES.get(POOL[n]["role"], POOL[n]["role"])) for n in targets]
|
||||
self._draw_items(rows, f"Inject {rec['skill_name']} into:")
|
||||
else:
|
||||
self._draw_inject_slots()
|
||||
|
||||
def _draw_inject_slots(self):
|
||||
rec = collection.extracted_skills()[self.ability]
|
||||
spec = POOL[self.target]
|
||||
rows = []
|
||||
for slot in (1, 2):
|
||||
if slot >= len(spec["skills"]):
|
||||
continue
|
||||
sk = spec["skills"][slot]
|
||||
reason = collection.injection_slot_reason(self.target, rec, slot)
|
||||
if reason:
|
||||
rows.append((slot, f"Replace {ORDINALS[slot]} skill ({sk.name})", MUTED,
|
||||
f"Not allowed — {reason}", False))
|
||||
else:
|
||||
rows.append((slot, f"Replace {ORDINALS[slot]} skill ({sk.name})", HINT,
|
||||
f"CD {sk.cooldown} · {sk.description}"))
|
||||
self._draw_items(rows, f"Inject {rec['skill_name']} into {self.target} — replace:")
|
||||
|
||||
# ---- shared list ---------------------------------------------------
|
||||
def _draw_items(self, rows, header):
|
||||
h = self.bold.render(header, True, MUTED)
|
||||
self.screen.blit(h, (60, 118))
|
||||
y = 146
|
||||
for row in rows:
|
||||
key, label, color, sub = row[0], row[1], row[2], row[3]
|
||||
enabled = row[4] if len(row) > 4 else True
|
||||
rect = pygame.Rect(60, y, 560, 46)
|
||||
if enabled:
|
||||
self.item_rects.append((key, rect))
|
||||
hover = enabled and rect.collidepoint(self.mouse_pos)
|
||||
pygame.draw.rect(self.screen, BTN_HOVER if hover else PANEL, rect, border_radius=6)
|
||||
pygame.draw.rect(self.screen, (70, 90, 70) if hover else (60, 70, 60),
|
||||
rect, 2 if hover else 1, border_radius=6)
|
||||
lt = self.bold.render(label, True, color)
|
||||
self.screen.blit(lt, (rect.x + 12, rect.y + 5))
|
||||
if sub:
|
||||
st = self.tiny.render(sub, True, MUTED)
|
||||
self.screen.blit(st, (rect.x + 12, rect.y + 26))
|
||||
y += 54
|
||||
|
||||
# ---- confirm overlay -----------------------------------------------
|
||||
def _draw_confirm(self):
|
||||
shade = pygame.Surface((WIN_W, WIN_H), pygame.SRCALPHA)
|
||||
shade.fill((0, 0, 0, 170))
|
||||
self.screen.blit(shade, (0, 0))
|
||||
panel = pygame.Rect(WIN_W // 2 - 250, 200, 500, 200)
|
||||
pygame.draw.rect(self.screen, PANEL, panel, border_radius=12)
|
||||
pygame.draw.rect(self.screen, (90, 110, 90), panel, 2, border_radius=12)
|
||||
c = self.confirm
|
||||
if c["kind"] == "extract":
|
||||
sk = POOL[c["name"]]["skills"][c["index"]]
|
||||
title = f"Extract {sk.name}?"
|
||||
body = [f"from {c['name']} — this resets it to Lv 1 / Stage 1.",
|
||||
"The ability is stored for one injection."]
|
||||
else:
|
||||
rec = collection.extracted_skills()[c["ability"]]
|
||||
spec = POOL[c["target"]]
|
||||
title = f"Inject {rec['skill_name']} into {c['target']}?"
|
||||
body = [f"Replaces the {ORDINALS[c['slot']]} skill ({spec['skills'][c['slot']].name}).",
|
||||
"Consumes the extracted ability.",
|
||||
"Removing it later destroys the ability."]
|
||||
tt = self.bold.render(title, True, HINT)
|
||||
self.screen.blit(tt, tt.get_rect(center=(panel.centerx, panel.y + 40)))
|
||||
yy = panel.y + 76
|
||||
for line in body:
|
||||
b = self.small.render(line, True, TEXT)
|
||||
self.screen.blit(b, b.get_rect(center=(panel.centerx, yy)))
|
||||
yy += 28
|
||||
self.confirm_ok = pygame.Rect(panel.centerx - 145, panel.bottom - 52, 135, 38)
|
||||
self.confirm_no = pygame.Rect(panel.centerx + 10, panel.bottom - 52, 135, 38)
|
||||
pygame.draw.rect(self.screen, BTN_HOVER, self.confirm_ok, border_radius=8)
|
||||
ot = self.bold.render("Confirm", True, TEXT)
|
||||
self.screen.blit(ot, ot.get_rect(center=self.confirm_ok.center))
|
||||
pygame.draw.rect(self.screen, DANGER, self.confirm_no, border_radius=8)
|
||||
nt = self.bold.render("Cancel", True, TEXT)
|
||||
self.screen.blit(nt, nt.get_rect(center=self.confirm_no.center))
|
||||
|
||||
|
||||
def _skill_sub(sk) -> str:
|
||||
bracket = _bracket_text(sk)
|
||||
head = f"CD {sk.cooldown}"
|
||||
if bracket:
|
||||
head += f" [{bracket}]"
|
||||
return f"{head} · {sk.description}"
|
||||
+1192
File diff suppressed because it is too large
Load Diff
+2477
File diff suppressed because it is too large
Load Diff
+656
@@ -0,0 +1,656 @@
|
||||
import pygame
|
||||
|
||||
from assets import icon as icon_path
|
||||
from assets import load_sprite
|
||||
from engine import collection
|
||||
from engine.growth import compute_stats
|
||||
from engine.plants import HP_MULT, PASSIVES, POOL, RARITY_COLORS, RARITY_LABELS, plant_rarity
|
||||
from engine.roster import floor_enemy_level, roll_enemies
|
||||
from engine.unit import ROLE_ATTACKER, ROLE_ICONS, ROLE_NAMES, ROLE_SUPPORT, ROLE_TANK
|
||||
from ui.menu import _bracket_text, _draw_evo_badge
|
||||
from ui.runes import draw_rune_chips
|
||||
from ui.scaling import (fit_surface, load_rune_icon, load_type_icon, make_window,
|
||||
request_quit, to_logical, wrap_text)
|
||||
from ui.typepopup import draw_type_popup
|
||||
|
||||
TYPE_COLORS = {
|
||||
"radiant": (245, 200, 90),
|
||||
"nocturnal": (150, 120, 220),
|
||||
"ice": (130, 200, 235),
|
||||
"garden": (120, 200, 120),
|
||||
"wildflower": (235, 130, 170),
|
||||
}
|
||||
|
||||
BG = (24, 30, 24)
|
||||
TEXT = (230, 235, 230)
|
||||
MUTED = (150, 155, 150)
|
||||
SLOT = (42, 50, 42)
|
||||
SLOT_EMPTY = (34, 40, 34)
|
||||
HIGHLIGHT = (255, 220, 120)
|
||||
HINT = (255, 220, 120)
|
||||
BTN = (60, 90, 60)
|
||||
BTN_HOVER = (85, 125, 85)
|
||||
|
||||
WIN_W, WIN_H = 1040, 720
|
||||
SLOT_W, SLOT_H = 220, 52
|
||||
SLOT_GAP = 8
|
||||
|
||||
|
||||
class TeamSelect:
|
||||
def __init__(self, initial_enemies=None, difficulty=1, tower_mode=False,
|
||||
pick_goal=4, min_goal=1, restrict_to=None, show_slots=True,
|
||||
header=None, start_label=None, enemy_types=None, no_roll=False,
|
||||
enemy_levels=None):
|
||||
pygame.init()
|
||||
self.window, self.scale = make_window(WIN_W, WIN_H)
|
||||
self.screen = pygame.Surface((WIN_W, WIN_H))
|
||||
pygame.display.set_caption(header or "Plants — Tower")
|
||||
self.clock = pygame.time.Clock()
|
||||
self.font = pygame.font.SysFont("segoeui", 30)
|
||||
self.bold = pygame.font.SysFont("segoeui", 20, bold=True)
|
||||
self.small = pygame.font.SysFont("segoeui", 16)
|
||||
self.tiny = pygame.font.SysFont("segoeui", 13)
|
||||
self.difficulty = difficulty
|
||||
self.tower_mode = tower_mode
|
||||
self.header = header
|
||||
self.start_label = start_label
|
||||
self.pick_goal = pick_goal
|
||||
self.min_goal = min_goal
|
||||
self.show_slots = show_slots
|
||||
self.enemy_types = list(enemy_types) if enemy_types else []
|
||||
self.enemy_levels = list(enemy_levels) if enemy_levels else []
|
||||
self.no_roll = no_roll
|
||||
self.names = list(POOL.keys())
|
||||
self.favs = collection.favorites()
|
||||
if restrict_to is not None:
|
||||
self.pool_names = sorted(restrict_to, key=lambda n: (n not in self.favs, n))
|
||||
else:
|
||||
self.pool_names = sorted((n for n in self.names if n in collection.owned()),
|
||||
key=lambda n: (n not in self.favs, n))
|
||||
self.sprites = {n: self._load(n) for n in self.names}
|
||||
self.rune_icons = {}
|
||||
self.role_icons = {}
|
||||
self.allies = []
|
||||
if self.tower_mode:
|
||||
self.phase = 2
|
||||
self.enemies = list(initial_enemies) if initial_enemies else []
|
||||
else:
|
||||
self.phase = 1
|
||||
self.enemies = list(initial_enemies) if initial_enemies else []
|
||||
if not self.enemies:
|
||||
self._roll_enemies()
|
||||
self._layout_enemy_cards()
|
||||
self._layout_enemy_strip()
|
||||
self._layout_slots()
|
||||
self._layout_pool()
|
||||
self.roll_rect = pygame.Rect(WIN_W // 2 - 260, WIN_H - 60, 170, 40)
|
||||
self.next_rect = pygame.Rect(WIN_W // 2 + 90, WIN_H - 60, 180, 40)
|
||||
self.start_rect = pygame.Rect(WIN_W // 2 - 90, WIN_H - 60, 180, 40)
|
||||
self.preset_rects = [pygame.Rect(WIN_W - 254 + i * 62, 16, 56, 28) for i in range(4)]
|
||||
self.back_rect = pygame.Rect(12, 12, 90, 32)
|
||||
self.detail_open = False
|
||||
self.detail_name = None
|
||||
self.detail_list = []
|
||||
self.detail_index = 0
|
||||
self.detail_rect = pygame.Rect(WIN_W // 2 - 280, 120, 560, 480)
|
||||
self.detail_left = pygame.Rect(self.detail_rect.left - 44, self.detail_rect.centery - 24, 36, 48)
|
||||
self.detail_right = pygame.Rect(self.detail_rect.right + 8, self.detail_rect.centery - 24, 36, 48)
|
||||
self.detail_close = pygame.Rect(self.detail_rect.right - 44, self.detail_rect.top + 8, 36, 28)
|
||||
self.detail_add = pygame.Rect(self.detail_rect.centerx - 70, self.detail_rect.bottom - 44, 140, 32)
|
||||
self.type_info = None
|
||||
self.type_tags = []
|
||||
self.detail_type_rect = None
|
||||
|
||||
def _role_icon(self, role, size=14):
|
||||
key = (role, size)
|
||||
if key not in self.role_icons:
|
||||
name = ROLE_ICONS.get(role)
|
||||
if name:
|
||||
try:
|
||||
img = pygame.image.load(icon_path("role_icons", name)).convert_alpha()
|
||||
self.role_icons[key] = fit_surface(img, size)
|
||||
except (pygame.error, OSError):
|
||||
self.role_icons[key] = None
|
||||
else:
|
||||
self.role_icons[key] = None
|
||||
return self.role_icons[key]
|
||||
|
||||
def _load(self, name):
|
||||
return load_sprite(name, "full")
|
||||
|
||||
def _rune_icon(self, color):
|
||||
if color not in self.rune_icons:
|
||||
self.rune_icons[color] = load_rune_icon(color, 14)
|
||||
return self.rune_icons[color]
|
||||
|
||||
def _roll_enemies(self):
|
||||
self.enemies = roll_enemies(self.difficulty, exclude=tuple(self.allies))
|
||||
|
||||
def _enemy_level(self, idx):
|
||||
"""Displayed level of enemy `idx` (explicit list, else the floor default)."""
|
||||
if idx < len(self.enemy_levels):
|
||||
return self.enemy_levels[idx]
|
||||
return floor_enemy_level(self.difficulty)
|
||||
|
||||
def _draw_enemy_level(self, tag, idx):
|
||||
"""'Lv N' label just to the right of an enemy's role/type tag."""
|
||||
lv = self.tiny.render(f"Lv {self._enemy_level(idx)}", True, HINT)
|
||||
self.screen.blit(lv, (tag.right + 5, tag.y + 1))
|
||||
|
||||
def _layout_enemy_cards(self):
|
||||
cw, ch, gap = 210, 250, 18
|
||||
total = 4 * cw + 3 * gap
|
||||
x0 = (WIN_W - total) // 2
|
||||
self.enemy_card_rects = [pygame.Rect(x0 + i * (cw + gap), 96, cw, ch) for i in range(4)]
|
||||
|
||||
def _layout_enemy_strip(self):
|
||||
cw, ch, gap = 220, 84, 16
|
||||
total = 4 * cw + 3 * gap
|
||||
x0 = (WIN_W - total) // 2
|
||||
self.enemy_strip_rects = [pygame.Rect(x0 + i * (cw + gap), 56, cw, ch) for i in range(4)]
|
||||
|
||||
def _layout_slots(self):
|
||||
n = self.pick_goal
|
||||
if n > 4:
|
||||
sl_h, gap = 40, 4
|
||||
else:
|
||||
sl_h, gap = SLOT_H, SLOT_GAP
|
||||
self.ally_rects = [pygame.Rect(24, 170 + i * (sl_h + gap), SLOT_W, sl_h) for i in range(n)]
|
||||
|
||||
def _layout_pool(self):
|
||||
self.pool_rects = {}
|
||||
cols = min(4, max(1, len(self.pool_names)))
|
||||
cw, ch, gap = 170, 64, 10
|
||||
x0 = 290
|
||||
y0 = 170
|
||||
for i, name in enumerate(self.pool_names):
|
||||
x = x0 + (i % cols) * (cw + gap)
|
||||
y = y0 + (i // cols) * (ch + gap)
|
||||
self.pool_rects[name] = pygame.Rect(x, y, cw, ch)
|
||||
# pool scroll area: above the bottom buttons (WIN_H - 100)
|
||||
self.pool_area = pygame.Rect(x0 - 4, y0 - 4,
|
||||
cols * cw + (cols - 1) * gap + 8,
|
||||
WIN_H - y0 - 100)
|
||||
rows = (len(self.pool_names) + cols - 1) // cols
|
||||
self.pool_scroll_max = max(0, rows * (ch + gap) - gap - self.pool_area.h)
|
||||
self.pool_scroll = getattr(self, "pool_scroll", 0)
|
||||
self.pool_scroll = max(0, min(self.pool_scroll, self.pool_scroll_max))
|
||||
|
||||
def run(self):
|
||||
running = True
|
||||
result = None
|
||||
while running:
|
||||
self.clock.tick(60)
|
||||
self.mouse_pos = to_logical(pygame.mouse.get_pos(), self.scale)
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
request_quit()
|
||||
running = False
|
||||
elif event.type == pygame.MOUSEWHEEL:
|
||||
if not self.detail_open:
|
||||
self.pool_scroll = max(0, min(self.pool_scroll_max,
|
||||
self.pool_scroll - event.y * 40))
|
||||
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||||
pos = to_logical(event.pos, self.scale)
|
||||
if self.detail_open:
|
||||
self._click_detail(pos)
|
||||
elif self.back_rect.collidepoint(pos):
|
||||
result = "back"
|
||||
running = False
|
||||
else:
|
||||
choice = self._click(pos)
|
||||
if choice == "start":
|
||||
result = (list(self.allies), list(self.enemies))
|
||||
running = False
|
||||
self._draw()
|
||||
pygame.transform.smoothscale(self.screen, self.window.get_size(), self.window)
|
||||
pygame.display.flip()
|
||||
return result
|
||||
|
||||
def _open_detail(self, name, namelist):
|
||||
self.detail_list = list(namelist)
|
||||
if name not in self.detail_list:
|
||||
self.detail_list.append(name)
|
||||
self.detail_name = name
|
||||
self.detail_index = self.detail_list.index(name)
|
||||
self.detail_is_enemy = (namelist is self.enemies)
|
||||
self.detail_open = True
|
||||
|
||||
def _nav_detail(self, step):
|
||||
if not self.detail_list:
|
||||
return
|
||||
if not (0 <= self.detail_index < len(self.detail_list)):
|
||||
self.detail_index = 0
|
||||
self.detail_index = (self.detail_index + step) % len(self.detail_list)
|
||||
self.detail_name = self.detail_list[self.detail_index]
|
||||
self.type_info = None
|
||||
|
||||
def _click(self, pos):
|
||||
if self.type_info is not None:
|
||||
self.type_info = None
|
||||
return None
|
||||
for rect, ptype in self.type_tags:
|
||||
if rect.collidepoint(pos):
|
||||
self.type_info = ptype
|
||||
return None
|
||||
if self.phase == 1:
|
||||
if not self.no_roll and self.roll_rect.collidepoint(pos):
|
||||
self._roll_enemies()
|
||||
elif self.next_rect.collidepoint(pos):
|
||||
self.phase = 2
|
||||
else:
|
||||
for i, rect in enumerate(self.enemy_card_rects):
|
||||
if rect.collidepoint(pos) and i < len(self.enemies):
|
||||
self._open_detail(self.enemies[i], self.enemies)
|
||||
return None
|
||||
if self.start_rect.collidepoint(pos) and len(self.allies) >= self.min_goal:
|
||||
return "start"
|
||||
for i, rect in enumerate(self.preset_rects):
|
||||
if rect.collidepoint(pos):
|
||||
preset = collection.get_presets()[i]
|
||||
if preset:
|
||||
plants = [n for n in preset if n in self.pool_names]
|
||||
if self.pick_goal == 8:
|
||||
# tower roster: presets merge into the roster, re-click to unload
|
||||
if plants and all(n in self.allies for n in plants):
|
||||
self.allies = [n for n in self.allies if n not in plants]
|
||||
else:
|
||||
for n in plants:
|
||||
if n not in self.allies and len(self.allies) < self.pick_goal:
|
||||
self.allies.append(n)
|
||||
else:
|
||||
self.allies = plants
|
||||
return None
|
||||
for i, rect in enumerate(self.ally_rects):
|
||||
if rect.collidepoint(pos) and i < len(self.allies):
|
||||
self.allies.pop(i)
|
||||
return None
|
||||
for i, rect in enumerate(self.enemy_strip_rects):
|
||||
if rect.collidepoint(pos) and i < len(self.enemies):
|
||||
self._open_detail(self.enemies[i], self.enemies)
|
||||
return None
|
||||
for name, rect in self.pool_rects.items():
|
||||
if rect.move(0, -self.pool_scroll).collidepoint(pos):
|
||||
if name in self.allies:
|
||||
self.allies.remove(name) # click a selected plant to unselect it
|
||||
else:
|
||||
self._open_detail(name, self.pool_names)
|
||||
return None
|
||||
return None
|
||||
|
||||
def _click_detail(self, pos):
|
||||
if self.type_info is not None:
|
||||
self.type_info = None
|
||||
return
|
||||
if self.detail_type_rect and self.detail_type_rect.collidepoint(pos) and self.detail_name:
|
||||
if self.detail_is_enemy:
|
||||
idx = self.detail_list.index(self.detail_name)
|
||||
ptype = self.enemy_types[idx] if idx < len(self.enemy_types) else None
|
||||
else:
|
||||
ptype = collection.unit_type(self.detail_name)
|
||||
if ptype:
|
||||
self.type_info = ptype
|
||||
return
|
||||
if self.detail_close.collidepoint(pos):
|
||||
self.detail_open = False
|
||||
return
|
||||
if self.detail_left.collidepoint(pos):
|
||||
self._nav_detail(-1)
|
||||
return
|
||||
if self.detail_right.collidepoint(pos):
|
||||
self._nav_detail(1)
|
||||
return
|
||||
if self.detail_add.collidepoint(pos) and not self.detail_is_enemy:
|
||||
name = self.detail_name
|
||||
if (self.phase == 2 and name in self.pool_names
|
||||
and name not in self.allies and len(self.allies) < self.pick_goal):
|
||||
self.allies.append(name)
|
||||
self.detail_open = False
|
||||
|
||||
def _draw(self):
|
||||
self.type_tags = []
|
||||
self.detail_type_rect = None
|
||||
self.screen.fill(BG)
|
||||
pygame.draw.rect(self.screen, BTN, self.back_rect, border_radius=6)
|
||||
t = self.small.render("Back", True, TEXT)
|
||||
self.screen.blit(t, t.get_rect(center=self.back_rect.center))
|
||||
if self.phase == 1:
|
||||
self._draw_reveal()
|
||||
else:
|
||||
self._draw_pick()
|
||||
if self.detail_open:
|
||||
self._draw_detail_panel()
|
||||
if self.type_info is not None:
|
||||
draw_type_popup(self.screen, WIN_W, WIN_H, self.type_info,
|
||||
self.bold, self.small, self.tiny)
|
||||
|
||||
def _draw_type_tag(self, label, ptype, pos, font, center=False):
|
||||
surf = font.render(label, True, TYPE_COLORS.get(ptype, TEXT))
|
||||
rect = surf.get_rect(center=pos) if center else surf.get_rect(topleft=pos)
|
||||
tag = rect.inflate(12, 6)
|
||||
pygame.draw.rect(self.screen, (52, 60, 52), tag, border_radius=5)
|
||||
self.screen.blit(surf, rect)
|
||||
self.type_tags.append((tag, ptype))
|
||||
return tag
|
||||
|
||||
def _draw_role_type_tag(self, name, pos, font, center=False, ptype=None):
|
||||
role_name = ROLE_NAMES.get(POOL[name]["role"], POOL[name]["role"])
|
||||
rs = font.render(role_name, True, RARITY_COLORS.get(plant_rarity(name), TEXT))
|
||||
sep = font.render(" · ", True, MUTED)
|
||||
ptype = ptype or collection.unit_type(name)
|
||||
tic = load_type_icon(ptype, 16) if ptype else None
|
||||
if tic:
|
||||
type_w = tic.get_width() + 4
|
||||
type_text = None
|
||||
else:
|
||||
shown = ptype if ptype else "No type"
|
||||
type_text = font.render(shown, True, TYPE_COLORS.get(ptype, MUTED) if ptype else MUTED)
|
||||
type_w = type_text.get_width()
|
||||
total_w = rs.get_width() + sep.get_width() + type_w
|
||||
h = rs.get_height()
|
||||
if center:
|
||||
x, y = pos[0] - total_w // 2, pos[1] - h // 2
|
||||
else:
|
||||
x, y = pos
|
||||
tag = pygame.Rect(x - 6, y - 3, total_w + 12, h + 6)
|
||||
pygame.draw.rect(self.screen, (52, 60, 52), tag, border_radius=5)
|
||||
self.screen.blit(rs, (x, y))
|
||||
self.screen.blit(sep, (x + rs.get_width(), y))
|
||||
ix = x + rs.get_width() + sep.get_width()
|
||||
if tic:
|
||||
self.screen.blit(tic, tic.get_rect(midleft=(ix, y + h // 2)))
|
||||
else:
|
||||
self.screen.blit(type_text, (ix, y))
|
||||
return tag
|
||||
|
||||
def _draw_reveal(self):
|
||||
t = self.font.render("Enemy team", True, TEXT)
|
||||
self.screen.blit(t, t.get_rect(center=(WIN_W // 2, 40)))
|
||||
t = self.small.render(f"Floor {self.difficulty} — this is who you'll fight. Click a plant for details.", True, MUTED)
|
||||
self.screen.blit(t, t.get_rect(center=(WIN_W // 2, 74)))
|
||||
for i, name in enumerate(self.enemies):
|
||||
rect = self.enemy_card_rects[i]
|
||||
pygame.draw.rect(self.screen, (36, 44, 36), rect, border_radius=8)
|
||||
pygame.draw.rect(self.screen, (200, 90, 90), rect, 2, border_radius=8)
|
||||
spr = self.sprites.get(name)
|
||||
if spr:
|
||||
ic = fit_surface(spr, 120)
|
||||
self.screen.blit(ic, ic.get_rect(center=(rect.centerx, rect.y + 60)))
|
||||
t = self.bold.render(name, True, TEXT)
|
||||
self.screen.blit(t, t.get_rect(center=(rect.centerx, rect.y + 140)))
|
||||
tag = self._draw_role_type_tag(name, (rect.centerx, rect.y + 178), self.small, center=True,
|
||||
ptype=self.enemy_types[i] if i < len(self.enemy_types) else None)
|
||||
self._draw_enemy_level(tag, i)
|
||||
stats = POOL[name]["stats"]
|
||||
st = self.tiny.render(f"Atk {stats[0]} Spd {stats[1]} Def {stats[2]} HP {int(stats[3] * HP_MULT)}",
|
||||
True, MUTED)
|
||||
self.screen.blit(st, st.get_rect(center=(rect.centerx, rect.y + 216)))
|
||||
mouse = self.mouse_pos
|
||||
btns = ([(self.next_rect, "Continue")] if self.no_roll
|
||||
else [(self.roll_rect, "Roll"), (self.next_rect, "Continue")])
|
||||
for rect, label in btns:
|
||||
pygame.draw.rect(self.screen, BTN_HOVER if rect.collidepoint(mouse) else BTN, rect, border_radius=8)
|
||||
t = self.small.render(label, True, TEXT)
|
||||
self.screen.blit(t, t.get_rect(center=rect.center))
|
||||
|
||||
def _draw_pick(self):
|
||||
if self.tower_mode:
|
||||
if self.header:
|
||||
t = self.font.render(self.header, True, HIGHLIGHT)
|
||||
self.screen.blit(t, t.get_rect(center=(WIN_W // 2, 26)))
|
||||
note = self.small.render(f"Pick {self.min_goal}-{self.pick_goal} plants from your collection.", True, MUTED)
|
||||
elif self.pick_goal == 8:
|
||||
t = self.font.render("Tower — pick your roster (1-8 plants)", True, HIGHLIGHT)
|
||||
self.screen.blit(t, t.get_rect(center=(WIN_W // 2, 26)))
|
||||
note = self.small.render("4 fight each stage, you can swap from the bench between battles.", True, MUTED)
|
||||
else:
|
||||
t = self.font.render("Tower — pick your team (1-4 of your roster)", True, HIGHLIGHT)
|
||||
self.screen.blit(t, t.get_rect(center=(WIN_W // 2, 26)))
|
||||
note = self.small.render("Enemies get stronger each stage (up to 20).", True, MUTED)
|
||||
self.screen.blit(note, note.get_rect(center=(WIN_W // 2, 58)))
|
||||
else:
|
||||
t = self.font.render("Pick your allies", True, TEXT)
|
||||
self.screen.blit(t, t.get_rect(center=(WIN_W // 2, 26)))
|
||||
self._draw_enemy_strip()
|
||||
if self.show_slots:
|
||||
self._draw_slots(self.ally_rects, self.allies, "Your Allies")
|
||||
self._draw_pool()
|
||||
self._draw_tower_enemies()
|
||||
for i, rect in enumerate(self.preset_rects):
|
||||
preset = collection.get_presets()[i]
|
||||
has = bool(preset)
|
||||
loaded = False
|
||||
if self.pick_goal == 8 and preset:
|
||||
plants = [n for n in preset if n in self.pool_names]
|
||||
loaded = bool(plants) and all(n in self.allies for n in plants)
|
||||
if self.pick_goal == 8:
|
||||
bg = (BTN_HOVER if loaded else BTN) if has else (45, 50, 45)
|
||||
border = HIGHLIGHT if loaded else ((150, 190, 150) if has else (70, 80, 70))
|
||||
pygame.draw.rect(self.screen, bg, rect, border_radius=6)
|
||||
pygame.draw.rect(self.screen, border, rect, 2 if loaded else 1, border_radius=6)
|
||||
else:
|
||||
pygame.draw.rect(self.screen, BTN if has else (45, 50, 45), rect, border_radius=6)
|
||||
pygame.draw.rect(self.screen, (150, 190, 150) if has else (70, 80, 70),
|
||||
rect, 1, border_radius=6)
|
||||
label = f"P{i + 1}" if not preset else f"P{i + 1}·{len(preset)}"
|
||||
t = self.tiny.render(label, True, TEXT if preset else MUTED)
|
||||
self.screen.blit(t, t.get_rect(center=rect.center))
|
||||
mouse = self.mouse_pos
|
||||
ready = len(self.allies) >= self.min_goal
|
||||
pygame.draw.rect(self.screen, BTN_HOVER if ready else (45, 50, 45), self.start_rect, border_radius=8)
|
||||
if self.tower_mode:
|
||||
if self.start_label:
|
||||
label = self.start_label
|
||||
elif self.pick_goal == 8:
|
||||
label = "Start Tower"
|
||||
else:
|
||||
label = f"Fight ({len(self.allies)} picked)"
|
||||
else:
|
||||
label = "Start Battle"
|
||||
t = self.small.render(label, True, TEXT)
|
||||
self.screen.blit(t, t.get_rect(center=self.start_rect.center))
|
||||
|
||||
def _draw_enemy_strip(self):
|
||||
for i, name in enumerate(self.enemies):
|
||||
rect = self.enemy_strip_rects[i]
|
||||
pygame.draw.rect(self.screen, (40, 40, 34), rect, border_radius=6)
|
||||
pygame.draw.rect(self.screen, (200, 90, 90), rect, 2, border_radius=6)
|
||||
spr = self.sprites.get(name)
|
||||
if spr:
|
||||
ic = fit_surface(spr, 56)
|
||||
self.screen.blit(ic, ic.get_rect(center=(rect.x + 34, rect.y + 42)))
|
||||
t = self.small.render(name, True, TEXT)
|
||||
self.screen.blit(t, (rect.x + 66, rect.y + 12))
|
||||
tag = self._draw_role_type_tag(name, (rect.x + 66, rect.y + 34), self.tiny,
|
||||
ptype=self.enemy_types[i] if i < len(self.enemy_types) else None)
|
||||
self._draw_enemy_level(tag, i)
|
||||
hint = self.tiny.render("click for details", True, (120, 130, 120))
|
||||
self.screen.blit(hint, (rect.right - 96, rect.y + 66))
|
||||
|
||||
def _draw_slots(self, rects, chosen, title):
|
||||
t = self.bold.render(title, True, MUTED)
|
||||
self.screen.blit(t, (rects[0].x, rects[0].y - 26))
|
||||
for i, rect in enumerate(rects):
|
||||
pygame.draw.rect(self.screen, SLOT if i < len(chosen) else SLOT_EMPTY, rect, border_radius=6)
|
||||
pygame.draw.rect(self.screen, (70, 80, 70), rect, 1, border_radius=6)
|
||||
if i < len(chosen):
|
||||
self._draw_mini(rect, chosen[i])
|
||||
elif i == len(chosen):
|
||||
t = self.small.render("+", True, MUTED)
|
||||
self.screen.blit(t, t.get_rect(center=rect.center))
|
||||
|
||||
def _draw_mini(self, rect, name):
|
||||
spr = self.sprites.get(name)
|
||||
x = rect.x + 6
|
||||
if spr:
|
||||
ic = fit_surface(spr, 56)
|
||||
self.screen.blit(ic, ic.get_rect(midleft=(x, rect.centery)))
|
||||
t = self.small.render(name, True, TEXT)
|
||||
self.screen.blit(t, (x + 62, rect.y + 8))
|
||||
self._draw_role_type_tag(name, (x + 62, rect.y + 30), self.tiny)
|
||||
_draw_evo_badge(self.screen, self.tiny, (rect.right - 24, rect.y + 16), collection.get_progress(name)["stage"] - 1)
|
||||
rune_colors = collection.equipped_rune_colors(name)
|
||||
if rune_colors:
|
||||
chip_w = len(rune_colors) * 14 + (len(rune_colors) - 1) * 2
|
||||
draw_rune_chips(self.screen, self._rune_icon, rune_colors,
|
||||
rect.right - 40 - chip_w, rect.y + 32)
|
||||
|
||||
def _draw_pool(self):
|
||||
old_clip = self.screen.get_clip()
|
||||
self.screen.set_clip(self.pool_area)
|
||||
for name, rect in self.pool_rects.items():
|
||||
draw_rect = rect.move(0, -self.pool_scroll)
|
||||
if not draw_rect.colliderect(self.pool_area):
|
||||
continue
|
||||
chosen = name in self.allies
|
||||
bg = (50, 60, 50) if chosen else (36, 42, 36)
|
||||
pygame.draw.rect(self.screen, bg, draw_rect, border_radius=6)
|
||||
border = HIGHLIGHT if chosen else (60, 70, 60)
|
||||
pygame.draw.rect(self.screen, border, draw_rect, 2 if chosen else 1, border_radius=6)
|
||||
spr = self.sprites.get(name)
|
||||
if spr:
|
||||
ic = fit_surface(spr, 40)
|
||||
self.screen.blit(ic, ic.get_rect(center=(draw_rect.x + 24, draw_rect.y + 24)))
|
||||
t = self.tiny.render(name, True, TEXT)
|
||||
self.screen.blit(t, (draw_rect.x + 50, draw_rect.y + 6))
|
||||
role_name = ROLE_NAMES.get(POOL[name]["role"], POOL[name]["role"])
|
||||
ric = self._role_icon(POOL[name]["role"], 14)
|
||||
rs = self.tiny.render(role_name, True, RARITY_COLORS.get(plant_rarity(name), MUTED))
|
||||
sep = self.tiny.render(" · ", True, MUTED)
|
||||
ptype = collection.unit_type(name)
|
||||
tic = load_type_icon(ptype, 14) if ptype else None
|
||||
r_x = draw_rect.x + 50
|
||||
if ric:
|
||||
self.screen.blit(ric, ric.get_rect(midleft=(r_x, draw_rect.y + 28 + 7)))
|
||||
r_x += 17
|
||||
self.screen.blit(rs, (r_x, draw_rect.y + 28))
|
||||
r_x += rs.get_width()
|
||||
self.screen.blit(sep, (r_x, draw_rect.y + 28))
|
||||
r_x += sep.get_width()
|
||||
if tic:
|
||||
self.screen.blit(tic, tic.get_rect(midleft=(r_x, draw_rect.y + 35)))
|
||||
else:
|
||||
shown = ptype if ptype else "No type"
|
||||
ts = self.tiny.render(shown, True, TYPE_COLORS.get(ptype, MUTED) if ptype else MUTED)
|
||||
self.screen.blit(ts, (r_x, draw_rect.y + 28))
|
||||
prog = collection.get_progress(name)
|
||||
_draw_evo_badge(self.screen, self.tiny, (draw_rect.x + 14, draw_rect.y + 54), prog["stage"] - 1)
|
||||
lv = self.tiny.render(f"Lv {prog['level']}", True, HINT)
|
||||
self.screen.blit(lv, (draw_rect.x + 50, draw_rect.y + 46))
|
||||
rune_colors = collection.equipped_rune_colors(name)
|
||||
if rune_colors:
|
||||
draw_rune_chips(self.screen, self._rune_icon, rune_colors,
|
||||
draw_rect.x + 104, draw_rect.y + 48)
|
||||
self.screen.set_clip(old_clip)
|
||||
if self.pool_scroll_max > 0:
|
||||
hint = ("▲" if self.pool_scroll > 0 else "") + ("▼" if self.pool_scroll < self.pool_scroll_max else "")
|
||||
if hint:
|
||||
ht = self.tiny.render(hint, True, MUTED)
|
||||
self.screen.blit(ht, ht.get_rect(center=(self.pool_area.centerx, self.pool_area.bottom + 6)))
|
||||
|
||||
def _draw_tower_enemies(self):
|
||||
if not self.tower_mode or not self.enemies:
|
||||
return
|
||||
# below the roster pool (which ends around y=308 for an 8-plant roster)
|
||||
y = 340
|
||||
header = self.bold.render("Enemies this stage:", True, (200, 90, 90))
|
||||
self.screen.blit(header, (290, y))
|
||||
y += 30
|
||||
for i, name in enumerate(self.enemies):
|
||||
rect = pygame.Rect(290, y, 240, 54)
|
||||
pygame.draw.rect(self.screen, (40, 40, 34), rect, border_radius=6)
|
||||
pygame.draw.rect(self.screen, (200, 90, 90), rect, 2, border_radius=6)
|
||||
spr = self.sprites.get(name)
|
||||
if spr:
|
||||
ic = fit_surface(spr, 34)
|
||||
self.screen.blit(ic, ic.get_rect(center=(rect.x + 23, rect.y + 27)))
|
||||
t = self.tiny.render(name, True, TEXT)
|
||||
self.screen.blit(t, (rect.x + 46, rect.y + 8))
|
||||
tag = self._draw_role_type_tag(name, (rect.x + 46, rect.y + 30), self.tiny)
|
||||
self._draw_enemy_level(tag, i)
|
||||
y += 60
|
||||
|
||||
def _draw_detail_panel(self):
|
||||
name = self.detail_name
|
||||
if name is None:
|
||||
return
|
||||
spec = POOL[name]
|
||||
shade = pygame.Surface((WIN_W, WIN_H))
|
||||
shade.fill((0, 0, 0))
|
||||
shade.set_alpha(120)
|
||||
self.screen.blit(shade, (0, 0))
|
||||
pygame.draw.rect(self.screen, (30, 36, 30), self.detail_rect, border_radius=10)
|
||||
pygame.draw.rect(self.screen, (90, 110, 90), self.detail_rect, 2, border_radius=10)
|
||||
|
||||
pygame.draw.rect(self.screen, (140, 70, 70), self.detail_close, border_radius=6)
|
||||
t = self.small.render("X", True, TEXT)
|
||||
self.screen.blit(t, t.get_rect(center=self.detail_close.center))
|
||||
|
||||
spr = self.sprites.get(name)
|
||||
if spr:
|
||||
ic = fit_surface(spr, 120)
|
||||
self.screen.blit(ic, ic.get_rect(center=(self.detail_rect.x + 84, self.detail_rect.y + 100)))
|
||||
t = self.bold.render(name, True, TEXT)
|
||||
self.screen.blit(t, (self.detail_rect.x + 160, self.detail_rect.y + 34))
|
||||
rar = plant_rarity(name)
|
||||
rt = self.tiny.render(RARITY_LABELS.get(rar, rar), True, RARITY_COLORS.get(rar, TEXT))
|
||||
self.screen.blit(rt, (self.detail_rect.x + 160 + t.get_width() + 8, self.detail_rect.y + 38))
|
||||
enemy_type = None
|
||||
if self.detail_is_enemy:
|
||||
idx = self.detail_list.index(name)
|
||||
if idx < len(self.enemy_types):
|
||||
enemy_type = self.enemy_types[idx]
|
||||
self.detail_type_rect = self._draw_role_type_tag(
|
||||
name, (self.detail_rect.x + 160, self.detail_rect.y + 62), self.small,
|
||||
ptype=enemy_type)
|
||||
|
||||
if not self.detail_is_enemy and name in collection.owned():
|
||||
prog = collection.get_progress(name)
|
||||
lv = self.tiny.render(f"Level {prog['level']} · Stage {prog['stage']}", True, HINT)
|
||||
self.screen.blit(lv, (self.detail_rect.x + 160, self.detail_rect.y + 120))
|
||||
atk, spd, dfn, hp = compute_stats(spec["stats"], prog["level"], prog["stage"])
|
||||
else:
|
||||
atk, spd, dfn, hp = spec["stats"]
|
||||
st = self.small.render(f"Atk {atk} Spd {spd}", True, TEXT)
|
||||
self.screen.blit(st, (self.detail_rect.x + 160, self.detail_rect.y + 148))
|
||||
st = self.small.render(f"Def {dfn} HP {int(hp * HP_MULT)}", True, TEXT)
|
||||
self.screen.blit(st, (self.detail_rect.x + 160, self.detail_rect.y + 174))
|
||||
|
||||
y = self.detail_rect.y + 210
|
||||
p = spec.get("passive", "")
|
||||
if p:
|
||||
pname, pdesc = PASSIVES.get(p, (p, ""))
|
||||
self.screen.blit(self.bold.render(f"Passive — {pname}", True, HINT),
|
||||
(self.detail_rect.x + 24, y))
|
||||
lines = wrap_text(" " + pdesc, self.small, self.detail_rect.w - 48)
|
||||
for i, ln in enumerate(lines):
|
||||
self.screen.blit(self.small.render(ln, True, TEXT),
|
||||
(self.detail_rect.x + 24, y + 22 + i * 20))
|
||||
y += 26 + len(lines) * 20 + 2
|
||||
for sk in spec["skills"]:
|
||||
bracket = _bracket_text(sk)
|
||||
label = f"{sk.name} [{bracket}] CD {sk.cooldown}" if bracket else f"{sk.name} CD {sk.cooldown}"
|
||||
self.screen.blit(self.bold.render(label, True, HINT), (self.detail_rect.x + 24, y))
|
||||
d = self.small.render(" " + sk.description, True, TEXT)
|
||||
self.screen.blit(d, (self.detail_rect.x + 24, y + 22))
|
||||
y += 50
|
||||
|
||||
if self.phase == 2 and not self.detail_is_enemy and name in self.pool_names:
|
||||
addable = name not in self.allies and len(self.allies) < self.pick_goal
|
||||
label = "In Team" if name in self.allies else ("Add to Team" if addable else "Team full")
|
||||
pygame.draw.rect(self.screen, BTN_HOVER if addable else (45, 50, 45),
|
||||
self.detail_add, border_radius=6)
|
||||
t = self.tiny.render(label, True, TEXT if addable else MUTED)
|
||||
self.screen.blit(t, t.get_rect(center=self.detail_add.center))
|
||||
|
||||
pygame.draw.rect(self.screen, BTN, self.detail_left, border_radius=6)
|
||||
t = self.bold.render("<", True, TEXT)
|
||||
self.screen.blit(t, t.get_rect(center=self.detail_left.center))
|
||||
pygame.draw.rect(self.screen, BTN, self.detail_right, border_radius=6)
|
||||
t = self.bold.render(">", True, TEXT)
|
||||
self.screen.blit(t, t.get_rect(center=self.detail_right.center))
|
||||
idx = self.detail_list.index(name) if name in self.detail_list else 0
|
||||
pos = self.tiny.render(f"{idx + 1}/{len(self.detail_list)}", True, MUTED)
|
||||
self.screen.blit(pos, pos.get_rect(center=(self.detail_rect.centerx, self.detail_rect.bottom - 18)))
|
||||
|
||||
# __MORE__
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import pygame
|
||||
|
||||
from engine import collection
|
||||
from ui.scaling import make_window, request_quit, to_logical
|
||||
|
||||
WIN_W, WIN_H = 1040, 720
|
||||
BG = (24, 30, 24)
|
||||
TEXT = (230, 235, 230)
|
||||
MUTED = (150, 155, 150)
|
||||
HINT = (255, 220, 120)
|
||||
PANEL = (32, 38, 32)
|
||||
BTN = (60, 90, 60)
|
||||
BTN_HOVER = (85, 125, 85)
|
||||
|
||||
|
||||
class PresetScreen:
|
||||
def __init__(self):
|
||||
pygame.init()
|
||||
self.window, self.scale = make_window(WIN_W, WIN_H)
|
||||
self.screen = pygame.Surface((WIN_W, WIN_H))
|
||||
pygame.display.set_caption("Plants — Team Presets")
|
||||
self.clock = pygame.time.Clock()
|
||||
self.font = pygame.font.SysFont("segoeui", 30)
|
||||
self.bold = pygame.font.SysFont("segoeui", 20, bold=True)
|
||||
self.small = pygame.font.SysFont("segoeui", 16)
|
||||
self.tiny = pygame.font.SysFont("segoeui", 13)
|
||||
self.back_rect = pygame.Rect(16, 16, 90, 34)
|
||||
self.slots = []
|
||||
for i in range(4):
|
||||
card = pygame.Rect(WIN_W // 2 - 240, 130 + i * 92, 480, 76)
|
||||
set_btn = pygame.Rect(card.right - 180, card.y + 24, 80, 28)
|
||||
clear_btn = pygame.Rect(card.right - 90, card.y + 24, 80, 28)
|
||||
self.slots.append((card, set_btn, clear_btn))
|
||||
|
||||
def run(self):
|
||||
running = True
|
||||
while running:
|
||||
self.clock.tick(60)
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
request_quit()
|
||||
running = False
|
||||
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||||
pos = to_logical(event.pos, self.scale)
|
||||
if self.back_rect.collidepoint(pos):
|
||||
running = False
|
||||
continue
|
||||
for i, (card, set_btn, clear_btn) in enumerate(self.slots):
|
||||
if set_btn.collidepoint(pos):
|
||||
self._set_slot(i)
|
||||
elif clear_btn.collidepoint(pos):
|
||||
collection.clear_preset(i)
|
||||
self._draw()
|
||||
pygame.transform.smoothscale(self.screen, self.window.get_size(), self.window)
|
||||
pygame.display.flip()
|
||||
|
||||
def _set_slot(self, i):
|
||||
from ui.picker import TeamSelect
|
||||
ts = TeamSelect(tower_mode=True, pick_goal=4, header="Pick a team",
|
||||
start_label="Save")
|
||||
res = ts.run()
|
||||
self._reinit() # the nested picker quit pygame
|
||||
if res and res != "back" and res[0]:
|
||||
collection.save_preset(i, res[0])
|
||||
|
||||
def _reinit(self):
|
||||
pygame.init()
|
||||
self.window, self.scale = make_window(WIN_W, WIN_H)
|
||||
self.screen = pygame.Surface((WIN_W, WIN_H))
|
||||
pygame.display.set_caption("Plants — Team Presets")
|
||||
self.clock = pygame.time.Clock()
|
||||
self.font = pygame.font.SysFont("segoeui", 30)
|
||||
self.bold = pygame.font.SysFont("segoeui", 20, bold=True)
|
||||
self.small = pygame.font.SysFont("segoeui", 16)
|
||||
self.tiny = pygame.font.SysFont("segoeui", 13)
|
||||
|
||||
def _draw(self):
|
||||
self.screen.fill(BG)
|
||||
title = self.font.render("Team Presets", True, TEXT)
|
||||
self.screen.blit(title, title.get_rect(center=(WIN_W // 2, 52)))
|
||||
sub = self.small.render("Set up to 4 teams; load them with the P1–P4 buttons in battle.", True, MUTED)
|
||||
self.screen.blit(sub, sub.get_rect(center=(WIN_W // 2, 88)))
|
||||
pygame.draw.rect(self.screen, BTN, self.back_rect, border_radius=6)
|
||||
t = self.small.render("Back", True, TEXT)
|
||||
self.screen.blit(t, t.get_rect(center=self.back_rect.center))
|
||||
presets = collection.get_presets()
|
||||
for i, (card, set_btn, clear_btn) in enumerate(self.slots):
|
||||
pygame.draw.rect(self.screen, PANEL, card, border_radius=8)
|
||||
pygame.draw.rect(self.screen, (60, 70, 60), card, 1, border_radius=8)
|
||||
head = self.bold.render(f"Preset {i + 1}", True, HINT)
|
||||
self.screen.blit(head, (card.x + 16, card.y + 10))
|
||||
team = presets[i] if i < len(presets) else None
|
||||
if team:
|
||||
names = " · ".join(team)
|
||||
tt = self.tiny.render(names, True, TEXT)
|
||||
else:
|
||||
tt = self.tiny.render("Empty", True, MUTED)
|
||||
self.screen.blit(tt, (card.x + 16, card.y + 42))
|
||||
pygame.draw.rect(self.screen, BTN_HOVER, set_btn, border_radius=6)
|
||||
s = self.tiny.render("Set", True, TEXT)
|
||||
self.screen.blit(s, s.get_rect(center=set_btn.center))
|
||||
pygame.draw.rect(self.screen, (150, 70, 70), clear_btn, border_radius=6)
|
||||
c = self.tiny.render("Clear", True, TEXT)
|
||||
self.screen.blit(c, c.get_rect(center=clear_btn.center))
|
||||
@@ -0,0 +1,130 @@
|
||||
import pygame
|
||||
|
||||
from engine.plants import RARITY_COLORS
|
||||
from engine.relics import RELIC_POOL
|
||||
from ui.relics_overview import RelicsOverview
|
||||
from ui.scaling import make_window, request_quit, to_logical
|
||||
|
||||
WIN_W, WIN_H = 1040, 720
|
||||
BG = (24, 30, 24)
|
||||
TEXT = (230, 235, 230)
|
||||
MUTED = (150, 155, 150)
|
||||
HINT = (255, 220, 120)
|
||||
CARD = (36, 44, 36)
|
||||
BORDER = (90, 120, 90)
|
||||
SKIP = (60, 60, 60)
|
||||
|
||||
|
||||
class RelicChoiceScreen:
|
||||
"""Pick one of the offered roguelike blessings (or skip)."""
|
||||
|
||||
def __init__(self, relics, stage, owned=()):
|
||||
pygame.init()
|
||||
self.window, self.scale = make_window(WIN_W, WIN_H)
|
||||
self.screen = pygame.Surface((WIN_W, WIN_H))
|
||||
pygame.display.set_caption("Plants — Blessing")
|
||||
self.clock = pygame.time.Clock()
|
||||
self.font = pygame.font.SysFont("segoeui", 36)
|
||||
self.bold = pygame.font.SysFont("segoeui", 20, bold=True)
|
||||
self.small = pygame.font.SysFont("segoeui", 16)
|
||||
self.tiny = pygame.font.SysFont("segoeui", 13)
|
||||
self.relics = relics
|
||||
self.stage = stage
|
||||
self.owned = list(owned)
|
||||
self.overview = None
|
||||
self.cards = []
|
||||
cw, ch = 250, 300
|
||||
gap = 26
|
||||
total = len(relics) * cw + (len(relics) - 1) * gap
|
||||
x0 = (WIN_W - total) // 2
|
||||
y = 190
|
||||
for i, r in enumerate(relics):
|
||||
self.cards.append((r, pygame.Rect(x0 + i * (cw + gap), y, cw, ch)))
|
||||
self.skip_rect = pygame.Rect(WIN_W // 2 - 80, WIN_H - 70, 160, 40)
|
||||
self.all_btn = pygame.Rect(24, WIN_H - 70, 160, 40)
|
||||
|
||||
def run(self):
|
||||
running = True
|
||||
result = None
|
||||
while running:
|
||||
self.clock.tick(60)
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
request_quit()
|
||||
running = False
|
||||
elif event.type == pygame.MOUSEWHEEL:
|
||||
if self.overview is not None:
|
||||
self.overview.handle_wheel(event.y)
|
||||
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||||
pos = to_logical(event.pos, self.scale)
|
||||
if self.overview is not None:
|
||||
if self.overview.handle_click(pos) == "close":
|
||||
self.overview = None
|
||||
continue
|
||||
for r, rect in self.cards:
|
||||
if rect.collidepoint(pos):
|
||||
result = r
|
||||
running = False
|
||||
break
|
||||
else:
|
||||
if self.skip_rect.collidepoint(pos):
|
||||
result = None
|
||||
running = False
|
||||
elif self.all_btn.collidepoint(pos):
|
||||
self.overview = RelicsOverview(
|
||||
self.screen, self.bold, self.small, self.tiny,
|
||||
RELIC_POOL, self.owned)
|
||||
self._draw()
|
||||
pygame.transform.smoothscale(self.screen, self.window.get_size(), self.window)
|
||||
pygame.display.flip()
|
||||
return result
|
||||
|
||||
def _wrap(self, text, font, width):
|
||||
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
|
||||
|
||||
def _draw(self):
|
||||
self.screen.fill(BG)
|
||||
if self.overview is not None:
|
||||
self.overview.draw(pygame.mouse.get_pos())
|
||||
return
|
||||
t = self.font.render("Choose a blessing", True, HINT)
|
||||
self.screen.blit(t, t.get_rect(center=(WIN_W // 2, 60)))
|
||||
st = self.small.render(
|
||||
f"Stage {self.stage} defeated — pick a relic for the rest of the tower.",
|
||||
True, MUTED)
|
||||
self.screen.blit(st, st.get_rect(center=(WIN_W // 2, 110)))
|
||||
mouse = pygame.mouse.get_pos()
|
||||
for r, rect in self.cards:
|
||||
hover = rect.collidepoint(mouse)
|
||||
pygame.draw.rect(self.screen, CARD, rect, border_radius=10)
|
||||
pygame.draw.rect(self.screen, HINT if hover else BORDER, rect, 2, border_radius=10)
|
||||
name = self.bold.render(r.name, True, HINT)
|
||||
self.screen.blit(name, name.get_rect(midtop=(rect.centerx, rect.y + 16)))
|
||||
rar = self.small.render(r.rarity.capitalize(), True, RARITY_COLORS.get(r.rarity, MUTED))
|
||||
self.screen.blit(rar, rar.get_rect(midtop=(rect.centerx, rect.y + 50)))
|
||||
y = rect.y + 92
|
||||
for ln in self._wrap(r.desc, self.small, rect.w - 24)[:7]:
|
||||
lr = self.small.render(ln, True, TEXT)
|
||||
self.screen.blit(lr, lr.get_rect(midtop=(rect.centerx, y)))
|
||||
y += 24
|
||||
pygame.draw.rect(self.screen, SKIP, self.skip_rect, border_radius=8)
|
||||
pygame.draw.rect(self.screen, (150, 150, 150), self.skip_rect, 2, border_radius=8)
|
||||
sk = self.small.render("Skip", True, TEXT)
|
||||
self.screen.blit(sk, sk.get_rect(center=self.skip_rect.center))
|
||||
pygame.draw.rect(self.screen, (70, 90, 70), self.all_btn, border_radius=8)
|
||||
pygame.draw.rect(self.screen, BORDER, self.all_btn, 2, border_radius=8)
|
||||
ab = self.small.render("All blessings", True, TEXT)
|
||||
self.screen.blit(ab, ab.get_rect(center=self.all_btn.center))
|
||||
hint = self.tiny.render("Blessings persist for the rest of this tower run", True, MUTED)
|
||||
self.screen.blit(hint, hint.get_rect(center=(WIN_W // 2, WIN_H - 20)))
|
||||
@@ -0,0 +1,178 @@
|
||||
import pygame
|
||||
|
||||
from engine.plants import RARITY_COLORS
|
||||
from engine.relics import RELIC_POOL
|
||||
|
||||
BG = (24, 30, 24)
|
||||
TEXT = (230, 235, 230)
|
||||
MUTED = (150, 155, 150)
|
||||
HINT = (255, 220, 120)
|
||||
PANEL = (30, 36, 30)
|
||||
PANEL_BORDER = (150, 190, 150)
|
||||
CARD = (36, 44, 36)
|
||||
CARD_OWNED = (46, 54, 42)
|
||||
GOLD = (245, 200, 90)
|
||||
OWNED = (110, 220, 255) # bright cyan: distinct from the legendary gold border
|
||||
|
||||
TIER_ORDER = ("common", "rare", "epic", "legendary")
|
||||
TIER_LABELS = {"common": "Common", "rare": "Rare", "epic": "Epic", "legendary": "Legendary"}
|
||||
|
||||
|
||||
def _wrap(text, font, width):
|
||||
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
|
||||
|
||||
|
||||
class RelicsOverview:
|
||||
"""Full-screen overlay listing every blessing, grouped by rarity, with the
|
||||
ones collected this run highlighted. Click a relic to inspect it up close.
|
||||
Host screens draw this onto their own surface (no pygame display ownership),
|
||||
so it can open mid-battle without tearing down the window."""
|
||||
|
||||
def __init__(self, screen, bold, small, tiny, pool=None, owned_keys=()):
|
||||
self.screen = screen
|
||||
self.bold = bold
|
||||
self.small = small
|
||||
self.tiny = tiny
|
||||
self.pool = list(pool if pool is not None else RELIC_POOL)
|
||||
self.owned = set(owned_keys)
|
||||
self.scroll = 0
|
||||
self.detail = None
|
||||
w, h = screen.get_size()
|
||||
pw = min(760, w - 60)
|
||||
ph = min(500, h - 140)
|
||||
self.panel = pygame.Rect((w - pw) // 2, (h - ph) // 2, pw, ph)
|
||||
pad = 24
|
||||
self.cards_top = self.panel.y + 78
|
||||
self.inner = pygame.Rect(self.panel.x + pad, self.cards_top,
|
||||
self.panel.w - 2 * pad,
|
||||
self.panel.bottom - 20 - self.cards_top)
|
||||
self.close_rect = pygame.Rect(self.panel.right - 74, self.panel.y + 14, 58, 28)
|
||||
rank = {t: i for i, t in enumerate(TIER_ORDER)}
|
||||
self.pool.sort(key=lambda r: (rank.get(r.rarity, 9), r.name.lower()))
|
||||
cols = 3
|
||||
gap = 14
|
||||
cw = (self.inner.w - gap * (cols - 1)) // cols
|
||||
ch = 118
|
||||
self.cards = []
|
||||
x, y = self.inner.x, self.inner.y
|
||||
for i, r in enumerate(self.pool):
|
||||
self.cards.append((r, pygame.Rect(x, y, cw, ch)))
|
||||
if (i + 1) % cols == 0:
|
||||
x = self.inner.x
|
||||
y += ch + gap
|
||||
else:
|
||||
x += cw + gap
|
||||
self.content_h = y - self.inner.y if self.cards else 0
|
||||
self.scroll_max = max(0, self.content_h - self.inner.h)
|
||||
|
||||
def handle_wheel(self, y):
|
||||
if self.detail is not None:
|
||||
return
|
||||
self.scroll = max(0, min(self.scroll_max, self.scroll - y * 40))
|
||||
|
||||
def handle_click(self, pos):
|
||||
if self.detail is not None:
|
||||
self.detail = None
|
||||
return None
|
||||
if self.close_rect.collidepoint(pos):
|
||||
return "close"
|
||||
if not self.inner.collidepoint(pos):
|
||||
return "close" if not self.panel.collidepoint(pos) else None
|
||||
for r, rect in self.cards:
|
||||
draw_rect = rect.move(0, -self.scroll)
|
||||
if draw_rect.collidepoint(pos) and draw_rect.colliderect(self.inner):
|
||||
self.detail = r
|
||||
return None
|
||||
return None
|
||||
|
||||
def _draw_card(self, r, rect):
|
||||
owned = r.key in self.owned
|
||||
bg = CARD_OWNED if owned else CARD
|
||||
border = OWNED if owned else RARITY_COLORS.get(r.rarity, (150, 150, 150))
|
||||
pygame.draw.rect(self.screen, bg, rect, border_radius=8)
|
||||
pygame.draw.rect(self.screen, border, rect, 3 if owned else 1, border_radius=8)
|
||||
name = self.small.render(r.name, True, RARITY_COLORS.get(r.rarity, TEXT))
|
||||
self.screen.blit(name, (rect.x + 10, rect.y + 8))
|
||||
if owned:
|
||||
tag = self.tiny.render("OWNED", True, OWNED)
|
||||
self.screen.blit(tag, (rect.right - tag.get_width() - 8, rect.y + 10))
|
||||
y = rect.y + 34
|
||||
for ln in _wrap(r.desc, self.tiny, rect.w - 20)[:3]:
|
||||
self.screen.blit(self.tiny.render(ln, True, MUTED), (rect.x + 10, y))
|
||||
y += 16
|
||||
tip = self.tiny.render(f"{TIER_LABELS.get(r.rarity, r.rarity)}" +
|
||||
(" · click to inspect" if owned else " · not owned"),
|
||||
True, OWNED if owned else (110, 120, 110))
|
||||
self.screen.blit(tip, (rect.x + 10, rect.bottom - 18))
|
||||
|
||||
def _draw_detail(self):
|
||||
r = self.detail
|
||||
if r is None:
|
||||
return
|
||||
shade = pygame.Surface(self.screen.get_size(), pygame.SRCALPHA)
|
||||
shade.fill((0, 0, 0, 130))
|
||||
self.screen.blit(shade, (0, 0))
|
||||
w, h = 440, 230
|
||||
panel = pygame.Rect((self.screen.get_width() - w) // 2,
|
||||
(self.screen.get_height() - h) // 2, w, h)
|
||||
pygame.draw.rect(self.screen, (36, 44, 36), panel, border_radius=10)
|
||||
pygame.draw.rect(self.screen, RARITY_COLORS.get(r.rarity, PANEL_BORDER),
|
||||
panel, 2, border_radius=10)
|
||||
name = self.bold.render(r.name, True, RARITY_COLORS.get(r.rarity, TEXT))
|
||||
self.screen.blit(name, name.get_rect(midtop=(panel.centerx, panel.y + 22)))
|
||||
rar = self.small.render(TIER_LABELS.get(r.rarity, r.rarity).capitalize(), True, MUTED)
|
||||
self.screen.blit(rar, rar.get_rect(midtop=(panel.centerx, panel.y + 54)))
|
||||
y = panel.y + 92
|
||||
for ln in _wrap(r.desc, self.small, panel.w - 48)[:5]:
|
||||
line = self.small.render(ln, True, TEXT)
|
||||
self.screen.blit(line, line.get_rect(midtop=(panel.centerx, y)))
|
||||
y += 26
|
||||
owned = r.key in self.owned
|
||||
st = self.tiny.render("OWNED this run" if owned else "Not owned yet", True,
|
||||
OWNED if owned else MUTED)
|
||||
self.screen.blit(st, st.get_rect(midtop=(panel.centerx, y + 4)))
|
||||
close = self.tiny.render("Click anywhere to close", True, MUTED)
|
||||
self.screen.blit(close, close.get_rect(center=(panel.centerx, panel.bottom - 20)))
|
||||
|
||||
def draw(self, mouse):
|
||||
overlay = pygame.Surface(self.screen.get_size(), pygame.SRCALPHA)
|
||||
overlay.fill((0, 0, 0, 180))
|
||||
self.screen.blit(overlay, (0, 0))
|
||||
pygame.draw.rect(self.screen, PANEL, self.panel, border_radius=12)
|
||||
pygame.draw.rect(self.screen, PANEL_BORDER, self.panel, 2, border_radius=12)
|
||||
title = self.bold.render("All blessings", True, HINT)
|
||||
self.screen.blit(title, title.get_rect(midtop=(self.panel.centerx, self.panel.y + 14)))
|
||||
sub = self.tiny.render(
|
||||
f"{len(self.owned)} owned this run · cyan border = collected · click a card to inspect",
|
||||
True, MUTED)
|
||||
self.screen.blit(sub, sub.get_rect(midtop=(self.panel.centerx, self.panel.y + 44)))
|
||||
pygame.draw.rect(self.screen, (90, 110, 90), self.close_rect, border_radius=6)
|
||||
pygame.draw.rect(self.screen, PANEL_BORDER, self.close_rect, 1, border_radius=6)
|
||||
c = self.small.render("Close", True, TEXT)
|
||||
self.screen.blit(c, c.get_rect(center=self.close_rect.center))
|
||||
old = self.screen.get_clip()
|
||||
self.screen.set_clip(self.inner)
|
||||
for r, rect in self.cards:
|
||||
draw_rect = rect.move(0, -self.scroll)
|
||||
if not draw_rect.colliderect(self.inner):
|
||||
continue
|
||||
self._draw_card(r, draw_rect)
|
||||
self.screen.set_clip(old)
|
||||
if self.scroll_max > 0:
|
||||
hint = ("▲" if self.scroll > 0 else "") + ("▼" if self.scroll < self.scroll_max else "")
|
||||
if hint:
|
||||
ht = self.tiny.render(hint, True, MUTED)
|
||||
self.screen.blit(ht, ht.get_rect(midbottom=(self.inner.centerx, self.inner.bottom)))
|
||||
if self.detail is not None:
|
||||
self._draw_detail()
|
||||
+437
@@ -0,0 +1,437 @@
|
||||
import pygame
|
||||
|
||||
from assets import icon as icon_path
|
||||
from engine import collection
|
||||
from engine.plants import POOL, RARITY_COLORS, plant_rarity
|
||||
from engine import runes
|
||||
from ui.scaling import fit_surface, load_rune_icon, make_window, request_quit, to_logical
|
||||
|
||||
WIN_W, WIN_H = 1040, 720
|
||||
BG = (24, 30, 24)
|
||||
TEXT = (230, 235, 230)
|
||||
MUTED = (150, 155, 150)
|
||||
HINT = (255, 220, 120)
|
||||
PANEL = (32, 38, 32)
|
||||
BTN = (60, 90, 60)
|
||||
BTN_HOVER = (85, 125, 85)
|
||||
ACCENT = (120, 220, 190)
|
||||
DANGER = (150, 70, 70)
|
||||
SLOT_BORDER = (90, 110, 90)
|
||||
|
||||
SLOT_SIZE = 64
|
||||
|
||||
|
||||
def draw_rune_chips(screen, get_icon, colors, x, y, size=14, gap=2, max_chips=3):
|
||||
"""Draw small rune icons for the given colors starting at (x, y). `get_icon(color)`
|
||||
returns a surface (already sized) or None. Used to show equipped runes at a glance."""
|
||||
for i, color in enumerate(colors[:max_chips]):
|
||||
ic = get_icon(color)
|
||||
if ic:
|
||||
screen.blit(ic, (x + i * (size + gap), y))
|
||||
|
||||
|
||||
class RuneScreen:
|
||||
def __init__(self, plant: str):
|
||||
pygame.init()
|
||||
self.plant = plant
|
||||
self.window, self.scale = make_window(WIN_W, WIN_H)
|
||||
self.screen = pygame.Surface((WIN_W, WIN_H))
|
||||
pygame.display.set_caption(f"Plants — Runes · {plant}")
|
||||
self.clock = pygame.time.Clock()
|
||||
self.font = pygame.font.SysFont("segoeui", 30)
|
||||
self.bold = pygame.font.SysFont("segoeui", 20, bold=True)
|
||||
self.small = pygame.font.SysFont("segoeui", 16)
|
||||
self.tiny = pygame.font.SysFont("segoeui", 13)
|
||||
self.back_rect = pygame.Rect(16, 16, 90, 34)
|
||||
self.info_rect = pygame.Rect(WIN_W - 100, 16, 84, 34)
|
||||
self.mouse_pos = (0, 0)
|
||||
self.selected = None # rune id selected from the inventory
|
||||
self.confirm = None # {"kind","slot","rune",...} prompt
|
||||
self.confirm_ok = pygame.Rect(0, 0, 0, 0)
|
||||
self.confirm_no = pygame.Rect(0, 0, 0, 0)
|
||||
self.info = False # rune-effects info popup
|
||||
self.equip_msg = "" # transient message (e.g. rune-type limit)
|
||||
self.equip_msg_until = 0
|
||||
self.filter_tier = 0 # 0 all, 1 tier 1, 2 tier 2
|
||||
self.filter_color = None # None all, else a rune color
|
||||
self.sort_best = False # sort by proportional roll
|
||||
self.filter_rects = [] # (kind, value, rect)
|
||||
self.sort_rect = pygame.Rect(WIN_W - 170, 284, 150, 24)
|
||||
self.slot_rects = [] # (slot_index, rect)
|
||||
self.inv_rects = [] # (rune_id, rect)
|
||||
self.scroll = 0
|
||||
self.scroll_max = 0
|
||||
self._icons = {}
|
||||
|
||||
def _icon_for(self, color):
|
||||
if color not in self._icons:
|
||||
self._icons[color] = load_rune_icon(color, 40)
|
||||
return self._icons[color]
|
||||
|
||||
def run(self):
|
||||
running = True
|
||||
while running:
|
||||
self.clock.tick(60)
|
||||
self.mouse_pos = to_logical(pygame.mouse.get_pos(), self.scale)
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
request_quit()
|
||||
running = False
|
||||
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||||
pos = to_logical(event.pos, self.scale)
|
||||
if self.back_rect.collidepoint(pos):
|
||||
running = False
|
||||
continue
|
||||
if self.info:
|
||||
self.info = False
|
||||
continue
|
||||
if self.info_rect.collidepoint(pos):
|
||||
self.info = True
|
||||
continue
|
||||
if self.confirm is not None:
|
||||
if self.confirm_ok.collidepoint(pos):
|
||||
if self.confirm["kind"] == "move":
|
||||
collection.equip_rune(self.plant, self.confirm["slot"],
|
||||
self.confirm["rune"]["id"])
|
||||
self.selected = None
|
||||
else:
|
||||
collection.unequip_rune(self.plant, self.confirm["slot"])
|
||||
self.confirm = None
|
||||
elif self.confirm_no.collidepoint(pos):
|
||||
self.confirm = None
|
||||
continue
|
||||
self._click(pos)
|
||||
elif event.type == pygame.MOUSEWHEEL:
|
||||
self.scroll = max(0, min(self.scroll_max, self.scroll - event.y * 44))
|
||||
self._draw()
|
||||
pygame.transform.smoothscale(self.screen, self.window.get_size(), self.window)
|
||||
pygame.display.flip()
|
||||
|
||||
def _click(self, pos):
|
||||
if self._click_filter(pos):
|
||||
return
|
||||
equipped = collection.equipped_runes(self.plant)
|
||||
inventory = collection.rune_inventory()
|
||||
by_id = {r["id"]: r for r in inventory}
|
||||
for slot, rect in self.slot_rects:
|
||||
if rect.collidepoint(pos):
|
||||
if self.selected is not None:
|
||||
rune = by_id.get(self.selected)
|
||||
owner = collection.rune_location(self.selected)
|
||||
if owner and owner != self.plant:
|
||||
self.confirm = {"kind": "move", "slot": slot,
|
||||
"rune": rune, "from": owner}
|
||||
else:
|
||||
reason = collection.equip_rune(self.plant, slot, self.selected)
|
||||
self.equip_msg = reason or ""
|
||||
self.equip_msg_until = (pygame.time.get_ticks() + 2000) if reason else 0
|
||||
self.selected = None
|
||||
elif slot < len(equipped) and equipped[slot] is not None:
|
||||
self.confirm = {"kind": "remove", "slot": slot,
|
||||
"rune": by_id.get(equipped[slot])}
|
||||
return
|
||||
for rune_id, rect in self.inv_rects:
|
||||
if rect.collidepoint(pos):
|
||||
self.selected = None if self.selected == rune_id else rune_id
|
||||
return
|
||||
|
||||
def _click_filter(self, pos):
|
||||
for kind, val, rect in self.filter_rects:
|
||||
if rect.collidepoint(pos):
|
||||
if kind == "tier":
|
||||
self.filter_tier = val
|
||||
elif kind == "color":
|
||||
self.filter_color = val
|
||||
return True
|
||||
if self.sort_rect.collidepoint(pos):
|
||||
self.sort_best = not self.sort_best
|
||||
return True
|
||||
return False
|
||||
|
||||
def _displayed_runes(self, inventory):
|
||||
shown = [r for r in inventory
|
||||
if (self.filter_tier == 0 or runes.tier(r["color"]) == self.filter_tier)
|
||||
and (self.filter_color is None or r["color"] == self.filter_color)]
|
||||
if self.sort_best:
|
||||
shown.sort(key=runes.roll_ratio, reverse=True)
|
||||
return shown
|
||||
|
||||
def _small_icon(self, color):
|
||||
key = ("small", color)
|
||||
if key not in self._icons:
|
||||
ic = self._icon_for(color)
|
||||
self._icons[key] = fit_surface(ic, 22) if ic else None
|
||||
return self._icons[key]
|
||||
|
||||
def _draw_filters(self):
|
||||
self.filter_rects = []
|
||||
y, h = 284, 24
|
||||
tl = self.tiny.render("Tier:", True, MUTED)
|
||||
self.screen.blit(tl, (60, y + 5))
|
||||
x = 92
|
||||
for val, label in ((0, "All"), (1, "T1"), (2, "T2")):
|
||||
active = self.filter_tier == val
|
||||
rect = pygame.Rect(x, y, 30, h)
|
||||
pygame.draw.rect(self.screen, (90, 120, 90) if active else (45, 50, 45),
|
||||
rect, border_radius=4)
|
||||
pygame.draw.rect(self.screen, HINT if active else (70, 80, 70),
|
||||
rect, 1, border_radius=4)
|
||||
t = self.tiny.render(label, True, TEXT if active else MUTED)
|
||||
self.screen.blit(t, t.get_rect(center=rect.center))
|
||||
self.filter_rects.append(("tier", val, rect))
|
||||
x += 36
|
||||
ty = self.tiny.render("Type:", True, MUTED)
|
||||
self.screen.blit(ty, (210, y + 5))
|
||||
x = 244
|
||||
all_rect = pygame.Rect(x, y, 36, h)
|
||||
active = self.filter_color is None
|
||||
pygame.draw.rect(self.screen, (90, 120, 90) if active else (45, 50, 45),
|
||||
all_rect, border_radius=4)
|
||||
pygame.draw.rect(self.screen, HINT if active else (70, 80, 70),
|
||||
all_rect, 1, border_radius=4)
|
||||
t = self.tiny.render("All", True, TEXT if active else MUTED)
|
||||
self.screen.blit(t, t.get_rect(center=all_rect.center))
|
||||
self.filter_rects.append(("color", None, all_rect))
|
||||
x += 42
|
||||
for color in runes.RUNE_COLORS:
|
||||
rect = pygame.Rect(x, y, 24, h)
|
||||
active = self.filter_color == color
|
||||
pygame.draw.rect(self.screen, (45, 50, 45), rect, border_radius=4)
|
||||
pygame.draw.rect(self.screen, HINT if active else (70, 80, 70),
|
||||
rect, 1 if active else 2, border_radius=4)
|
||||
sw = self._small_icon(color)
|
||||
if sw:
|
||||
self.screen.blit(sw, sw.get_rect(center=rect.center))
|
||||
self.filter_rects.append(("color", color, rect))
|
||||
x += 28
|
||||
active = self.sort_best
|
||||
pygame.draw.rect(self.screen, (90, 120, 90) if active else (45, 50, 45),
|
||||
self.sort_rect, border_radius=4)
|
||||
pygame.draw.rect(self.screen, HINT if active else (70, 80, 70),
|
||||
self.sort_rect, 1, border_radius=4)
|
||||
sl = self.tiny.render("Sort: best roll" if active else "Sort: default",
|
||||
True, TEXT if active else MUTED)
|
||||
self.screen.blit(sl, sl.get_rect(center=self.sort_rect.center))
|
||||
|
||||
def _draw(self):
|
||||
self.slot_rects = []
|
||||
self.inv_rects = []
|
||||
self.screen.fill(BG)
|
||||
|
||||
pygame.draw.rect(self.screen, BTN, self.back_rect, border_radius=6)
|
||||
bt = self.small.render("Back", True, TEXT)
|
||||
self.screen.blit(bt, bt.get_rect(center=self.back_rect.center))
|
||||
|
||||
ihov = self.info_rect.collidepoint(self.mouse_pos)
|
||||
pygame.draw.rect(self.screen, (90, 120, 90) if (self.info or ihov) else BTN,
|
||||
self.info_rect, border_radius=6)
|
||||
il = self.tiny.render("Rune info", True, TEXT)
|
||||
self.screen.blit(il, il.get_rect(center=self.info_rect.center))
|
||||
|
||||
spec = POOL[self.plant]
|
||||
rar = plant_rarity(self.plant)
|
||||
title = self.font.render(self.plant, True, RARITY_COLORS.get(rar, TEXT))
|
||||
self.screen.blit(title, title.get_rect(center=(WIN_W // 2, 44)))
|
||||
sub = self.small.render(f"Rune slots — {spec['role']}", True, MUTED)
|
||||
self.screen.blit(sub, sub.get_rect(center=(WIN_W // 2, 76)))
|
||||
|
||||
slots = collection.rune_slots(self.plant)
|
||||
equipped = collection.equipped_runes(self.plant)
|
||||
inventory = collection.rune_inventory()
|
||||
by_id = {r["id"]: r for r in inventory}
|
||||
|
||||
# --- slots -----------------------------------------------------
|
||||
st = self.bold.render("Rune slots", True, HINT)
|
||||
self.screen.blit(st, (60, 110))
|
||||
locked = self.tiny.render(
|
||||
"Evolve to unlock more slots (permanent)", True, MUTED)
|
||||
self.screen.blit(locked, (200, 116))
|
||||
|
||||
total_w = slots * SLOT_SIZE + (slots - 1) * 20
|
||||
x0 = (WIN_W - total_w) // 2
|
||||
y = 140
|
||||
for i in range(slots):
|
||||
rect = pygame.Rect(x0 + i * (SLOT_SIZE + 20), y, SLOT_SIZE, SLOT_SIZE)
|
||||
pygame.draw.rect(self.screen, PANEL, rect, border_radius=10)
|
||||
pygame.draw.rect(self.screen, SLOT_BORDER, rect, 2, border_radius=10)
|
||||
rune = by_id.get(equipped[i]) if i < len(equipped) else None
|
||||
if rune:
|
||||
ic = self._icon_for(rune["color"])
|
||||
if ic:
|
||||
self.screen.blit(ic, ic.get_rect(center=rect.center))
|
||||
else:
|
||||
num = self.bold.render(str(i + 1), True, MUTED)
|
||||
num.set_alpha(70) # faint hint that the slot is empty
|
||||
self.screen.blit(num, num.get_rect(center=rect.center))
|
||||
lbl = self.tiny.render(f"Slot {i + 1}", True, MUTED)
|
||||
self.screen.blit(lbl, lbl.get_rect(center=(rect.centerx, rect.bottom + 14)))
|
||||
self.slot_rects.append((i, rect))
|
||||
|
||||
# --- equipped-effects summary (right of the slots) ------------
|
||||
equipped_runes = [by_id[e] for e in equipped if e in by_id]
|
||||
line_h = 18
|
||||
panel = pygame.Rect(x0 + total_w + 40, y, 240,
|
||||
26 + line_h * max(1, len(equipped_runes)) + 8)
|
||||
pygame.draw.rect(self.screen, PANEL, panel, border_radius=8)
|
||||
pygame.draw.rect(self.screen, (60, 70, 60), panel, 1, border_radius=8)
|
||||
t = self.bold.render("Equipped effects", True, HINT)
|
||||
self.screen.blit(t, (panel.x + 8, panel.y + 6))
|
||||
py = panel.y + 28
|
||||
if equipped_runes:
|
||||
for rune in equipped_runes:
|
||||
eff = self.tiny.render(
|
||||
runes.effect_text(rune["color"], rune.get("pct", 30)), True, TEXT)
|
||||
self.screen.blit(eff, (panel.x + 8, py))
|
||||
py += line_h
|
||||
else:
|
||||
e = self.tiny.render("No runes equipped", True, MUTED)
|
||||
self.screen.blit(e, (panel.x + 8, py))
|
||||
|
||||
if pygame.time.get_ticks() < self.equip_msg_until:
|
||||
b = self.tiny.render(self.equip_msg, True, DANGER)
|
||||
self.screen.blit(b, b.get_rect(center=(WIN_W // 2, y + SLOT_SIZE + 44)))
|
||||
elif self.selected is not None:
|
||||
sel = by_id.get(self.selected)
|
||||
if sel:
|
||||
owner = collection.rune_location(self.selected)
|
||||
if owner and owner != self.plant:
|
||||
hint = self.bold.render(
|
||||
f"Click a slot to move {sel['name']} from {owner}", True, ACCENT)
|
||||
else:
|
||||
hint = self.bold.render(
|
||||
f"Click a slot to equip {sel['name']}", True, ACCENT)
|
||||
self.screen.blit(hint, hint.get_rect(center=(WIN_W // 2, y + SLOT_SIZE + 44)))
|
||||
elif slots == 1:
|
||||
h = self.tiny.render("Click a rune below, then click a slot to equip it.",
|
||||
True, MUTED)
|
||||
self.screen.blit(h, h.get_rect(center=(WIN_W // 2, y + SLOT_SIZE + 44)))
|
||||
|
||||
# --- inventory -------------------------------------------------
|
||||
it = self.bold.render("Runes you own", True, HINT)
|
||||
self.screen.blit(it, (60, 260))
|
||||
dr = self.tiny.render(
|
||||
f"Drop rates — {runes.BATTLE_DROP_CHANCE * 100:.0f}% for any rune "
|
||||
f"from floor battles · {runes.PACK_DROP_CHANCE * 100:.0f}% for any "
|
||||
f"rune from packs",
|
||||
True, MUTED)
|
||||
self.screen.blit(dr, dr.get_rect(midright=(WIN_W - 60, 270)))
|
||||
self._draw_filters()
|
||||
display = self._displayed_runes(inventory)
|
||||
list_y = 318
|
||||
self.scroll_max = max(0, len(display) * 46 - (WIN_H - list_y - 20))
|
||||
self.scroll = max(0, min(self.scroll, self.scroll_max))
|
||||
|
||||
old_clip = self.screen.get_clip()
|
||||
self.screen.set_clip(pygame.Rect(0, list_y, WIN_W, WIN_H - list_y - 20))
|
||||
ry = list_y - self.scroll
|
||||
for rune in display:
|
||||
rect = pygame.Rect(60, ry, 780, 40)
|
||||
pygame.draw.rect(self.screen, PANEL, rect, border_radius=6)
|
||||
if rune["id"] == self.selected:
|
||||
pygame.draw.rect(self.screen, ACCENT, rect, 2, border_radius=6)
|
||||
else:
|
||||
pygame.draw.rect(self.screen, (60, 70, 60), rect, 1, border_radius=6)
|
||||
ic = self._icon_for(rune["color"])
|
||||
if ic:
|
||||
self.screen.blit(ic, (rect.x + 8, rect.y + 4))
|
||||
owner = collection.rune_location(rune["id"])
|
||||
on_this = owner == self.plant
|
||||
name = self.small.render(rune["name"], True,
|
||||
TEXT if not on_this else MUTED)
|
||||
self.screen.blit(name, (rect.x + 58, rect.y + 11))
|
||||
eff = self.tiny.render(runes.effect_text(rune["color"], rune.get("pct", 30)),
|
||||
True, MUTED)
|
||||
self.screen.blit(eff, (rect.x + 58 + name.get_width() + 10, rect.y + 14))
|
||||
if on_this:
|
||||
tag = self.tiny.render("equipped", True, MUTED)
|
||||
self.screen.blit(tag, (rect.right - 70, rect.y + 13))
|
||||
elif owner:
|
||||
tag = self.tiny.render(f"Equipped in: {owner}", True, MUTED)
|
||||
self.screen.blit(tag, tag.get_rect(midright=(rect.right - 14, rect.centery)))
|
||||
self.inv_rects.append((rune["id"], rect))
|
||||
ry += 46
|
||||
self.screen.set_clip(old_clip)
|
||||
|
||||
if not inventory:
|
||||
empty = self.small.render(
|
||||
"No runes yet — win floor battles or open seed packs to find some.",
|
||||
True, MUTED)
|
||||
self.screen.blit(empty, (60, list_y + 10))
|
||||
elif not display:
|
||||
empty = self.small.render("No runes match the current filter.",
|
||||
True, MUTED)
|
||||
self.screen.blit(empty, (60, list_y + 10))
|
||||
if self.scroll_max > 0:
|
||||
hint = "▲" if self.scroll > 0 else ""
|
||||
if self.scroll < self.scroll_max:
|
||||
hint += "▼"
|
||||
ht = self.tiny.render(hint, True, MUTED)
|
||||
self.screen.blit(ht, ht.get_rect(center=(WIN_W // 2, WIN_H - 18)))
|
||||
|
||||
if self.confirm is not None:
|
||||
self._draw_confirm()
|
||||
if self.info:
|
||||
self._draw_info()
|
||||
|
||||
def _draw_info(self):
|
||||
overlay = pygame.Surface((WIN_W, WIN_H), pygame.SRCALPHA)
|
||||
overlay.fill((0, 0, 0, 160))
|
||||
self.screen.blit(overlay, (0, 0))
|
||||
w, h = 560, 400
|
||||
panel = pygame.Rect((WIN_W - w) // 2, (WIN_H - h) // 2, w, h)
|
||||
pygame.draw.rect(self.screen, PANEL, panel, border_radius=12)
|
||||
pygame.draw.rect(self.screen, (150, 190, 150), panel, 2, border_radius=12)
|
||||
title = self.bold.render("Rune effects", True, HINT)
|
||||
self.screen.blit(title, title.get_rect(center=(panel.centerx, panel.y + 30)))
|
||||
y = panel.y + 62
|
||||
for color in runes.RUNE_COLORS:
|
||||
ic = self._icon_for(color)
|
||||
if ic:
|
||||
self.screen.blit(ic, (panel.x + 40, y))
|
||||
nm = self.bold.render(runes.name(color), True, TEXT)
|
||||
self.screen.blit(nm, (panel.x + 92, y))
|
||||
eff = self.small.render(runes.summary(color), True, MUTED)
|
||||
self.screen.blit(eff, (panel.x + 92, y + 24))
|
||||
rg = self.tiny.render(
|
||||
f"Tier {runes.tier(color)} · Rolls {runes.roll_range(color)}",
|
||||
True, ACCENT)
|
||||
self.screen.blit(rg, rg.get_rect(topright=(panel.right - 16, y + 10)))
|
||||
y += 58
|
||||
note = self.tiny.render(
|
||||
"Runes of the same effect stack additively; a plant can equip at "
|
||||
"most %d runes of the same type." % runes.MAX_RUNES_PER_TYPE,
|
||||
True, MUTED)
|
||||
self.screen.blit(note, note.get_rect(center=(panel.centerx, panel.bottom - 24)))
|
||||
close = self.tiny.render("Click anywhere to close", True, MUTED)
|
||||
self.screen.blit(close, close.get_rect(center=(panel.centerx, panel.bottom - 8)))
|
||||
|
||||
def _draw_confirm(self):
|
||||
overlay = pygame.Surface((WIN_W, WIN_H), pygame.SRCALPHA)
|
||||
overlay.fill((0, 0, 0, 150))
|
||||
self.screen.blit(overlay, (0, 0))
|
||||
w, h = 480, 190
|
||||
panel = pygame.Rect((WIN_W - w) // 2, (WIN_H - h) // 2, w, h)
|
||||
pygame.draw.rect(self.screen, PANEL, panel, border_radius=12)
|
||||
pygame.draw.rect(self.screen, DANGER, panel, 2, border_radius=12)
|
||||
rune = self.confirm["rune"]
|
||||
name = rune["name"] if rune else "Rune"
|
||||
if self.confirm["kind"] == "move":
|
||||
title = self.bold.render("Move rune?", True, TEXT)
|
||||
msg = self.small.render(
|
||||
f"Remove {name} from {self.confirm['from']} to slot it here?", True, TEXT)
|
||||
ok_label = "Move"
|
||||
else:
|
||||
slot = self.confirm["slot"] + 1
|
||||
title = self.bold.render("Remove rune?", True, TEXT)
|
||||
msg = self.small.render(f"Unequip {name} from slot {slot}?", True, TEXT)
|
||||
ok_label = "Remove"
|
||||
self.screen.blit(title, title.get_rect(center=(panel.centerx, panel.y + 34)))
|
||||
self.screen.blit(msg, msg.get_rect(center=(panel.centerx, panel.y + 66)))
|
||||
self.confirm_ok = pygame.Rect(panel.centerx - 110, panel.y + 110, 100, 36)
|
||||
self.confirm_no = pygame.Rect(panel.centerx + 10, panel.y + 110, 100, 36)
|
||||
pygame.draw.rect(self.screen, DANGER, self.confirm_ok, border_radius=8)
|
||||
ok = self.bold.render(ok_label, True, TEXT)
|
||||
self.screen.blit(ok, ok.get_rect(center=self.confirm_ok.center))
|
||||
pygame.draw.rect(self.screen, BTN, self.confirm_no, border_radius=8)
|
||||
no = self.bold.render("Cancel", True, TEXT)
|
||||
self.screen.blit(no, no.get_rect(center=self.confirm_no.center))
|
||||
+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
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import pygame
|
||||
|
||||
from assets import load_sprite
|
||||
from engine import collection
|
||||
from engine.types import ALL_TYPES
|
||||
from ui.scaling import fit_surface, load_type_icon, make_window, request_quit, to_logical
|
||||
|
||||
WIN_W, WIN_H = 1040, 720
|
||||
BG = (24, 30, 24)
|
||||
TEXT = (230, 235, 230)
|
||||
MUTED = (150, 155, 150)
|
||||
HINT = (255, 220, 120)
|
||||
PANEL = (32, 38, 32)
|
||||
BTN = (60, 90, 60)
|
||||
BTN_HOVER = (85, 125, 85)
|
||||
|
||||
TYPE_NAMES = {
|
||||
"radiant": "Radiant",
|
||||
"nocturnal": "Nocturnal",
|
||||
"ice": "Ice",
|
||||
"garden": "Garden",
|
||||
"wildflower": "Wildflower",
|
||||
}
|
||||
TYPE_COLORS = {
|
||||
"radiant": (245, 200, 90),
|
||||
"nocturnal": (150, 120, 220),
|
||||
"ice": (130, 200, 235),
|
||||
"garden": (120, 200, 120),
|
||||
"wildflower": (235, 130, 170),
|
||||
}
|
||||
|
||||
|
||||
class StarterScreen:
|
||||
def __init__(self):
|
||||
pygame.init()
|
||||
self.window, self.scale = make_window(WIN_W, WIN_H)
|
||||
self.screen = pygame.Surface((WIN_W, WIN_H))
|
||||
pygame.display.set_caption("Plants — Choose your starting pinwheel")
|
||||
self.clock = pygame.time.Clock()
|
||||
self.font = pygame.font.SysFont("segoeui", 30)
|
||||
self.bold = pygame.font.SysFont("segoeui", 20, bold=True)
|
||||
self.small = pygame.font.SysFont("segoeui", 16)
|
||||
self.tiny = pygame.font.SysFont("segoeui", 13)
|
||||
self.type_rects = {}
|
||||
cw, ch, gap = 180, 200, 12
|
||||
total = len(ALL_TYPES) * cw + (len(ALL_TYPES) - 1) * gap
|
||||
x0 = (WIN_W - total) // 2
|
||||
y0 = 120
|
||||
for i, pt in enumerate(ALL_TYPES):
|
||||
x = x0 + i * (cw + gap)
|
||||
self.type_rects[pt] = pygame.Rect(x, y0, cw, ch)
|
||||
self.pinwheel_sprite = self._load("Pinwheel")
|
||||
self.mouse_pos = (0, 0)
|
||||
|
||||
def _load(self, name):
|
||||
return load_sprite(name, "full")
|
||||
|
||||
def run(self):
|
||||
chosen = False
|
||||
running = True
|
||||
while running:
|
||||
self.clock.tick(60)
|
||||
self.mouse_pos = to_logical(pygame.mouse.get_pos(), self.scale)
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
request_quit()
|
||||
running = False
|
||||
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||||
pos = to_logical(event.pos, self.scale)
|
||||
for pt, rect in self.type_rects.items():
|
||||
if rect.collidepoint(pos):
|
||||
collection.init_starter_pinwheel(pt)
|
||||
chosen = True
|
||||
running = False
|
||||
break
|
||||
self._draw()
|
||||
pygame.transform.smoothscale(self.screen, self.window.get_size(), self.window)
|
||||
pygame.display.flip()
|
||||
return chosen
|
||||
|
||||
def _draw(self):
|
||||
self.screen.fill(BG)
|
||||
title = self.font.render("Choose your starting pinwheel", True, HINT)
|
||||
self.screen.blit(title, title.get_rect(center=(WIN_W // 2, 52)))
|
||||
sub = self.small.render("You start with a single pinwheel of the chosen type. "
|
||||
"Battle to win packs and grow your garden.",
|
||||
True, MUTED)
|
||||
self.screen.blit(sub, sub.get_rect(center=(WIN_W // 2, 88)))
|
||||
for pt, rect in self.type_rects.items():
|
||||
hover = rect.collidepoint(self.mouse_pos)
|
||||
pygame.draw.rect(self.screen, BTN_HOVER if hover else PANEL, rect, border_radius=10)
|
||||
pygame.draw.rect(self.screen, (110, 150, 110) if hover else (70, 84, 70),
|
||||
rect, 2, border_radius=10)
|
||||
if self.pinwheel_sprite:
|
||||
ic = fit_surface(self.pinwheel_sprite, 96)
|
||||
self.screen.blit(ic, ic.get_rect(center=(rect.centerx, rect.y + 66)))
|
||||
tcol = TYPE_COLORS.get(pt, TEXT)
|
||||
nm = self.bold.render(TYPE_NAMES.get(pt, pt), True, tcol)
|
||||
self.screen.blit(nm, nm.get_rect(center=(rect.centerx, rect.y + 130)))
|
||||
tic = load_type_icon(pt, 28)
|
||||
if tic:
|
||||
self.screen.blit(tic, tic.get_rect(center=(rect.centerx, rect.y + 160)))
|
||||
hint = self.tiny.render("Click to start", True, MUTED)
|
||||
self.screen.blit(hint, hint.get_rect(center=(rect.centerx, rect.bottom - 14)))
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
import pygame
|
||||
|
||||
from engine.plants import RARITY_COLORS, plant_rarity
|
||||
from ui.scaling import make_window, request_quit, to_logical
|
||||
|
||||
WIN_W, WIN_H = 1040, 720
|
||||
BG = (24, 30, 24)
|
||||
TEXT = (230, 235, 230)
|
||||
MUTED = (150, 155, 150)
|
||||
HINT = (255, 220, 120)
|
||||
BTN = (60, 90, 60)
|
||||
BTN_HOVER = (85, 125, 85)
|
||||
DANGER = (150, 70, 70)
|
||||
DANGER_HOVER = (180, 90, 90)
|
||||
|
||||
|
||||
class ConfirmScreen:
|
||||
"""A simple yes/no popup (e.g. "really leave the tower?")."""
|
||||
|
||||
def __init__(self, title, message, yes_label="Leave", no_label="Stay"):
|
||||
pygame.init()
|
||||
self.window, self.scale = make_window(WIN_W, WIN_H)
|
||||
self.screen = pygame.Surface((WIN_W, WIN_H))
|
||||
pygame.display.set_caption("Plants — Tower")
|
||||
self.clock = pygame.time.Clock()
|
||||
self.font = pygame.font.SysFont("segoeui", 30)
|
||||
self.bold = pygame.font.SysFont("segoeui", 20, bold=True)
|
||||
self.small = pygame.font.SysFont("segoeui", 16)
|
||||
self.tiny = pygame.font.SysFont("segoeui", 13)
|
||||
self.title = title
|
||||
self.message = message
|
||||
self.yes_label = yes_label
|
||||
self.no_label = no_label
|
||||
self.yes_rect = pygame.Rect(WIN_W // 2 + 16, WIN_H - 96, 180, 44)
|
||||
self.no_rect = pygame.Rect(WIN_W // 2 - 196, WIN_H - 96, 180, 44)
|
||||
|
||||
def run(self):
|
||||
running = True
|
||||
result = False
|
||||
while running:
|
||||
self.clock.tick(60)
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
request_quit()
|
||||
running = False
|
||||
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||||
pos = to_logical(event.pos, self.scale)
|
||||
if self.yes_rect.collidepoint(pos):
|
||||
result = True
|
||||
running = False
|
||||
elif self.no_rect.collidepoint(pos):
|
||||
result = False
|
||||
running = False
|
||||
self._draw()
|
||||
pygame.transform.smoothscale(self.screen, self.window.get_size(), self.window)
|
||||
pygame.display.flip()
|
||||
return result
|
||||
|
||||
def _draw(self):
|
||||
self.screen.fill(BG)
|
||||
title = self.font.render(self.title, True, HINT)
|
||||
self.screen.blit(title, title.get_rect(center=(WIN_W // 2, 100)))
|
||||
for i, line in enumerate(self.message.split("\n")):
|
||||
t = self.small.render(line, True, TEXT)
|
||||
self.screen.blit(t, t.get_rect(center=(WIN_W // 2, 150 + i * 26)))
|
||||
self._button(self.no_rect, self.no_label, BTN, BTN_HOVER)
|
||||
self._button(self.yes_rect, self.yes_label, DANGER, DANGER_HOVER)
|
||||
|
||||
def _button(self, rect, label, base, hover):
|
||||
mouse = pygame.mouse.get_pos()
|
||||
bg = hover if rect.collidepoint(mouse) else base
|
||||
pygame.draw.rect(self.screen, bg, rect, border_radius=8)
|
||||
pygame.draw.rect(self.screen, (150, 190, 150), rect, 2, border_radius=8)
|
||||
t = self.bold.render(label, True, TEXT)
|
||||
self.screen.blit(t, t.get_rect(center=rect.center))
|
||||
|
||||
|
||||
class TowerScreen:
|
||||
def __init__(self, stage, total_stages, reward_text, totals_text, mode="battle",
|
||||
enemies=()):
|
||||
pygame.init()
|
||||
self.window, self.scale = make_window(WIN_W, WIN_H)
|
||||
self.screen = pygame.Surface((WIN_W, WIN_H))
|
||||
pygame.display.set_caption("Plants — Tower")
|
||||
self.clock = pygame.time.Clock()
|
||||
self.font = pygame.font.SysFont("segoeui", 36)
|
||||
self.bold = pygame.font.SysFont("segoeui", 20, bold=True)
|
||||
self.small = pygame.font.SysFont("segoeui", 16)
|
||||
self.tiny = pygame.font.SysFont("segoeui", 13)
|
||||
self.stage = stage
|
||||
self.total = total_stages
|
||||
self.reward_text = reward_text
|
||||
self.totals_text = totals_text
|
||||
self.mode = mode # battle | lost | cleared | summary
|
||||
self.enemies = list(enemies)
|
||||
self.left_rect = pygame.Rect(WIN_W // 2 - 320, WIN_H - 90, 200, 44)
|
||||
self.mid_rect = pygame.Rect(WIN_W // 2 - 100, WIN_H - 90, 200, 44)
|
||||
self.right_rect = pygame.Rect(WIN_W // 2 + 120, WIN_H - 90, 200, 44)
|
||||
|
||||
def run(self):
|
||||
running = True
|
||||
result = "exit"
|
||||
while running:
|
||||
self.clock.tick(60)
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
request_quit()
|
||||
running = False
|
||||
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||||
pos = to_logical(event.pos, self.scale)
|
||||
if self.mode == "battle":
|
||||
if self.left_rect.collidepoint(pos):
|
||||
result = "exit"
|
||||
running = False
|
||||
elif self.mid_rect.collidepoint(pos):
|
||||
result = "adjust"
|
||||
running = False
|
||||
elif self.right_rect.collidepoint(pos):
|
||||
result = "next"
|
||||
running = False
|
||||
elif self.mode == "lost":
|
||||
if self.left_rect.collidepoint(pos):
|
||||
result = "retry"
|
||||
running = False
|
||||
elif self.mid_rect.collidepoint(pos):
|
||||
result = "change"
|
||||
running = False
|
||||
elif self.right_rect.collidepoint(pos):
|
||||
result = "giveup"
|
||||
running = False
|
||||
else:
|
||||
result = "exit"
|
||||
running = False
|
||||
self._draw()
|
||||
pygame.transform.smoothscale(self.screen, self.window.get_size(), self.window)
|
||||
pygame.display.flip()
|
||||
return result
|
||||
|
||||
def _draw(self):
|
||||
self.screen.fill(BG)
|
||||
if self.mode == "battle":
|
||||
self._draw_battle()
|
||||
elif self.mode == "lost":
|
||||
self._draw_lost()
|
||||
else:
|
||||
self._draw_summary()
|
||||
|
||||
def _button(self, rect, label, base, hover):
|
||||
mouse = pygame.mouse.get_pos()
|
||||
bg = hover if rect.collidepoint(mouse) else base
|
||||
pygame.draw.rect(self.screen, bg, rect, border_radius=8)
|
||||
pygame.draw.rect(self.screen, (150, 190, 150), rect, 2, border_radius=8)
|
||||
t = self.bold.render(label, True, TEXT)
|
||||
self.screen.blit(t, t.get_rect(center=rect.center))
|
||||
|
||||
def _draw_battle(self):
|
||||
title = self.font.render("Tower", True, HINT)
|
||||
self.screen.blit(title, title.get_rect(center=(WIN_W // 2, 70)))
|
||||
st = self.bold.render(f"Stage {self.stage} defeated!", True, TEXT)
|
||||
self.screen.blit(st, st.get_rect(center=(WIN_W // 2, 150)))
|
||||
info = self.small.render(f"Next: Stage {self.stage + 1} / {self.total}", True, MUTED)
|
||||
self.screen.blit(info, info.get_rect(center=(WIN_W // 2, 190)))
|
||||
rw = self.small.render(f"Stage reward: {self.reward_text}", True, TEXT)
|
||||
self.screen.blit(rw, rw.get_rect(center=(WIN_W // 2, 240)))
|
||||
tt = self.small.render(f"Total so far: {self.totals_text}", True, MUTED)
|
||||
self.screen.blit(tt, tt.get_rect(center=(WIN_W // 2, 270)))
|
||||
self._button(self.left_rect, "Exit tower", BTN, BTN_HOVER)
|
||||
self._button(self.mid_rect, "Adjust team", BTN, BTN_HOVER)
|
||||
self._button(self.right_rect, "Next stage", BTN, BTN_HOVER)
|
||||
|
||||
def _draw_lost(self):
|
||||
title = self.font.render("Tower ended", True, HINT)
|
||||
self.screen.blit(title, title.get_rect(center=(WIN_W // 2, 70)))
|
||||
sub = self.bold.render(f"Defeated at stage {self.stage}", True, TEXT)
|
||||
self.screen.blit(sub, sub.get_rect(center=(WIN_W // 2, 130)))
|
||||
rw = self.small.render(f"Rewards earned: {self.totals_text}", True, TEXT)
|
||||
self.screen.blit(rw, rw.get_rect(center=(WIN_W // 2, 180)))
|
||||
if self.enemies:
|
||||
eh = self.bold.render("Enemy team this stage:", True, HINT)
|
||||
self.screen.blit(eh, eh.get_rect(midtop=(WIN_W // 2, 224)))
|
||||
total_w = sum(self.small.size(n)[0] for n in self.enemies) + 14 * (len(self.enemies) - 1)
|
||||
x = (WIN_W - total_w) // 2
|
||||
for name in self.enemies:
|
||||
rar = plant_rarity(name)
|
||||
surf = self.small.render(name, True, RARITY_COLORS.get(rar, TEXT))
|
||||
self.screen.blit(surf, (x, 254))
|
||||
x += surf.get_width() + 14
|
||||
self._button(self.left_rect, "Retry", BTN, BTN_HOVER)
|
||||
self._button(self.mid_rect, "Change team", BTN, BTN_HOVER)
|
||||
self._button(self.right_rect, "Give up", DANGER, DANGER_HOVER)
|
||||
|
||||
def _draw_summary(self):
|
||||
if self.mode == "cleared":
|
||||
title = self.font.render("Tower cleared!", True, HINT)
|
||||
sub = self.bold.render(f"Cleared all {self.total} stages!", True, TEXT)
|
||||
else:
|
||||
title = self.font.render("Tower ended", True, HINT)
|
||||
sub = self.bold.render(f"Reached stage {self.stage}", True, TEXT)
|
||||
self.screen.blit(title, title.get_rect(center=(WIN_W // 2, 90)))
|
||||
self.screen.blit(sub, sub.get_rect(center=(WIN_W // 2, 170)))
|
||||
rw = self.small.render(f"Rewards earned: {self.totals_text}", True, TEXT)
|
||||
self.screen.blit(rw, rw.get_rect(center=(WIN_W // 2, 230)))
|
||||
hint = self.tiny.render("Click anywhere to return to the menu", True, MUTED)
|
||||
self.screen.blit(hint, hint.get_rect(center=(WIN_W // 2, 280)))
|
||||
@@ -0,0 +1,70 @@
|
||||
import pygame
|
||||
|
||||
from engine.types import TYPE_BEATS, type_relations
|
||||
from ui.scaling import draw_type_icon, load_type_icon
|
||||
|
||||
TYPE_COLORS = {
|
||||
"radiant": (245, 200, 90),
|
||||
"nocturnal": (150, 120, 220),
|
||||
"ice": (130, 200, 235),
|
||||
"garden": (120, 200, 120),
|
||||
"wildflower": (235, 130, 170),
|
||||
}
|
||||
STRONG = (120, 210, 120)
|
||||
WEAK = (225, 130, 130)
|
||||
MUTED = (150, 155, 150)
|
||||
TEXT = (230, 235, 230)
|
||||
|
||||
|
||||
def _cycle_types() -> list:
|
||||
order = []
|
||||
cur = next(iter(TYPE_BEATS))
|
||||
for _ in range(len(TYPE_BEATS)):
|
||||
order.append(cur)
|
||||
cur = TYPE_BEATS[cur]
|
||||
return order
|
||||
|
||||
|
||||
def draw_type_popup(screen, w, h, ptype, bold, small, tiny):
|
||||
"""Draw a modal popup explaining a type's strong/weak matchups."""
|
||||
overlay = pygame.Surface((w, h), pygame.SRCALPHA)
|
||||
overlay.fill((0, 0, 0, 150))
|
||||
screen.blit(overlay, (0, 0))
|
||||
pw, ph = 340, 200
|
||||
panel = pygame.Rect((w - pw) // 2, (h - ph) // 2, pw, ph)
|
||||
pygame.draw.rect(screen, (32, 38, 32), panel, border_radius=12)
|
||||
pygame.draw.rect(screen, (70, 90, 70), panel, 2, border_radius=12)
|
||||
tcol = TYPE_COLORS.get(ptype, TEXT)
|
||||
title = bold.render(ptype.capitalize(), True, tcol)
|
||||
screen.blit(title, title.get_rect(center=(panel.centerx, panel.y + 30)))
|
||||
|
||||
strong, weak = type_relations(ptype)
|
||||
y = panel.y + 62
|
||||
if strong:
|
||||
screen.blit(small.render("Strong against", True, MUTED), (panel.x + 36, y))
|
||||
draw_type_icon(screen, strong, (panel.x + 36, y + 34), small, STRONG, 22)
|
||||
if weak:
|
||||
screen.blit(small.render("Weak against", True, MUTED), (panel.right - 140, y))
|
||||
draw_type_icon(screen, weak, (panel.right - 140, y + 34), small, WEAK, 22)
|
||||
|
||||
cyc_y = panel.bottom - 44
|
||||
segs = []
|
||||
for t in _cycle_types():
|
||||
ic = load_type_icon(t, 16)
|
||||
if ic:
|
||||
segs.append((ic, ic.get_width()))
|
||||
else:
|
||||
txt = tiny.render(t.capitalize(), True, MUTED)
|
||||
segs.append((txt, txt.get_width()))
|
||||
gap = tiny.size(">")[0] + 6
|
||||
total_w = sum(s[1] for s in segs) + (len(segs) - 1) * gap
|
||||
x = panel.centerx - total_w // 2
|
||||
for i, (surf, w) in enumerate(segs):
|
||||
screen.blit(surf, surf.get_rect(midleft=(x, cyc_y)))
|
||||
x += w
|
||||
if i < len(segs) - 1:
|
||||
gt = tiny.render(">", True, MUTED)
|
||||
screen.blit(gt, (x + 2, cyc_y - gt.get_height() // 2))
|
||||
x += gap
|
||||
hint = tiny.render("Click anywhere to close", True, MUTED)
|
||||
screen.blit(hint, hint.get_rect(center=(panel.centerx, panel.bottom - 22)))
|
||||
Reference in New Issue
Block a user