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__