Files
Flora/ui/picker.py
T
Lena feb4500c6c small update
boss prototype, auto planter, visual reworks, bug fixes
2026-08-29 10:45:34 +02:00

817 lines
39 KiB
Python

import pygame
from assets import icon as icon_path
from assets import load_sprite
from engine import bosses, 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.types import ALL_TYPES
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": (235, 130, 170),
"wildflower": (120, 200, 120),
}
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 "Flora — 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}
for bname in bosses.BOSS_SPECS:
self.sprites[bname] = self._load(bosses.boss_sprite(bname))
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.pool_filter = None # optional elemental type filter for the ally pool
self.pool_exclude_type = None # right-click: show everything BUT this type
self.pool_type_rects = []
self.pool_role = None # optional role filter for the ally pool
self.pool_exclude_role = None # right-click: show everything BUT this role
self.pool_role_rects = []
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 _display_pool(self):
"""The ally pool, filtered by element type and/or role when active.
Left-click filters include a type/role; right-click excludes it."""
result = self.pool_names
if self.pool_filter is not None:
result = [n for n in result if POOL[n]["type"] == self.pool_filter]
elif self.pool_exclude_type is not None:
result = [n for n in result if POOL[n]["type"] != self.pool_exclude_type]
if self.pool_role is not None:
result = [n for n in result if POOL[n]["role"] == self.pool_role]
elif self.pool_exclude_role is not None:
result = [n for n in result if POOL[n]["role"] != self.pool_exclude_role]
return result
def _layout_pool(self):
self.pool_rects = {}
shown = self._display_pool()
cols = min(4, max(1, len(shown)))
cw, ch, gap = 170, 64, 10
x0 = 290
y0 = 170
for i, name in enumerate(shown):
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(shown) + 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 == 3:
pos = to_logical(event.pos, self.scale)
if not self.detail_open:
self._click_right(pos)
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 ptype, rect in self.pool_type_rects:
if rect.collidepoint(pos):
self.pool_filter = ptype # None = "All", else the chosen type
self.pool_exclude_type = None
self.pool_scroll = 0
self._layout_pool()
return None
for role, rect in self.pool_role_rects:
if rect.collidepoint(pos):
self.pool_role = role # None = "All", else the chosen role
self.pool_exclude_role = None
self.pool_scroll = 0
self._layout_pool()
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._display_pool())
return None
return None
def _click_right(self, pos):
"""Right-click on a filter icon EXCLUDES that type/role (everything but it)."""
if self.type_info is not None:
self.type_info = None
return None
for ptype, rect in self.pool_type_rects:
if rect.collidepoint(pos):
if ptype is None:
self.pool_filter = None
self.pool_exclude_type = None
else:
self.pool_exclude_type = ptype
self.pool_filter = None
self.pool_scroll = 0
self._layout_pool()
return None
for role, rect in self.pool_role_rects:
if rect.collidepoint(pos):
if role is None:
self.pool_role = None
self.pool_exclude_role = None
else:
self.pool_exclude_role = role
self.pool_role = None
self.pool_scroll = 0
self._layout_pool()
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):
if bosses.is_boss(name):
role_name = "Boss"
rs = font.render(role_name, True, (235, 200, 120))
else:
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 = bosses.boss_stats(name) if bosses.is_boss(name) else 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_filters()
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_filters(self):
"""Type + role filter row above the ally pool: 'All' plus one icon per
element and per role. Left-click highlights green (include only), and
right-click highlights red (exclude that type/role)."""
self.pool_type_rects = []
self.pool_role_rects = []
y, h = 142, 24
INC = (120, 220, 120)
EXC = (235, 110, 110)
tl = self.tiny.render("Type:", True, MUTED)
self.screen.blit(tl, (290, y + 5))
x = 330
all_rect = pygame.Rect(x, y, 36, h)
active = self.pool_filter is None and self.pool_exclude_type 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.pool_type_rects.append((None, all_rect))
x += 42
for ptype in ALL_TYPES:
rect = pygame.Rect(x, y, 24, h)
inc = self.pool_filter == ptype
exc = self.pool_exclude_type == ptype
pygame.draw.rect(self.screen, (45, 50, 45), rect, border_radius=4)
border = INC if inc else EXC if exc else (70, 80, 70)
pygame.draw.rect(self.screen, border, rect, 2 if (inc or exc) else 1,
border_radius=4)
tic = load_type_icon(ptype, 22)
if tic:
self.screen.blit(tic, tic.get_rect(center=rect.center))
self.pool_type_rects.append((ptype, rect))
x += 28
x += 36 # a little space between the type and role filters
rl = self.tiny.render("Role:", True, MUTED)
self.screen.blit(rl, (x, y + 5))
x += 42
all_role = pygame.Rect(x, y, 36, h)
active = self.pool_role is None and self.pool_exclude_role is None
pygame.draw.rect(self.screen, (90, 120, 90) if active else (45, 50, 45),
all_role, border_radius=4)
pygame.draw.rect(self.screen, HINT if active else (70, 80, 70),
all_role, 1, border_radius=4)
t = self.tiny.render("All", True, TEXT if active else MUTED)
self.screen.blit(t, t.get_rect(center=all_role.center))
self.pool_role_rects.append((None, all_role))
x += 42
for role in (ROLE_ATTACKER, ROLE_SUPPORT, ROLE_TANK):
rect = pygame.Rect(x, y, 24, h)
inc = self.pool_role == role
exc = self.pool_exclude_role == role
pygame.draw.rect(self.screen, (45, 50, 45), rect, border_radius=4)
border = INC if inc else EXC if exc else (70, 80, 70)
pygame.draw.rect(self.screen, border, rect, 2 if (inc or exc) else 1,
border_radius=4)
ric = self._role_icon(role, 22)
if ric:
self.screen.blit(ric, ric.get_rect(center=rect.center))
self.pool_role_rects.append((role, rect))
x += 28
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
is_boss_unit = bosses.is_boss(name)
spec = None if is_boss_unit else POOL[name]
boss_stats_t = bosses.boss_stats(name) if is_boss_unit else None
boss_skills = bosses.make_boss(name, 20).skills if is_boss_unit else None
boss_desc = bosses.BOSS_SPECS[name]["desc"] if is_boss_unit else None
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))
if is_boss_unit:
rt = self.tiny.render("Boss", True, (235, 200, 120))
else:
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 not is_boss_unit 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 = boss_stats_t if is_boss_unit else 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
if is_boss_unit:
self.screen.blit(self.bold.render("Boss", True, HINT), (self.detail_rect.x + 24, y))
lines = wrap_text(" " + boss_desc, 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 boss_skills:
bracket = _bracket_text(sk)
label = f"{sk.name} [{bracket}]" if bracket else sk.name
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 + 20))
y += 42
return
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__