656 lines
31 KiB
Python
656 lines
31 KiB
Python
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__ |