small update

boss prototype, auto planter, visual reworks, bug fixes
This commit is contained in:
2026-08-29 10:45:34 +02:00
parent 6840b715de
commit feb4500c6c
31 changed files with 1293 additions and 213 deletions
+177 -16
View File
@@ -2,10 +2,11 @@ import pygame
from assets import icon as icon_path
from assets import load_sprite
from engine import collection
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
@@ -17,8 +18,8 @@ TYPE_COLORS = {
"radiant": (245, 200, 90),
"nocturnal": (150, 120, 220),
"ice": (130, 200, 235),
"garden": (120, 200, 120),
"wildflower": (235, 130, 170),
"garden": (235, 130, 170),
"wildflower": (120, 200, 120),
}
BG = (24, 30, 24)
@@ -44,7 +45,7 @@ class TeamSelect:
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")
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)
@@ -68,6 +69,8 @@ class TeamSelect:
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 = []
@@ -82,6 +85,12 @@ class TeamSelect:
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)
@@ -157,13 +166,28 @@ class TeamSelect:
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 = {}
cols = min(4, max(1, len(self.pool_names)))
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(self.pool_names):
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)
@@ -171,7 +195,7 @@ class TeamSelect:
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
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))
@@ -190,6 +214,10 @@ class TeamSelect:
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:
@@ -269,12 +297,55 @@ class TeamSelect:
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.pool_names)
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
@@ -334,8 +405,12 @@ class TeamSelect:
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))
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
@@ -381,7 +456,7 @@ class TeamSelect:
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"]
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)))
@@ -414,6 +489,7 @@ class TeamSelect:
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):
@@ -495,6 +571,69 @@ class TeamSelect:
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)
@@ -574,7 +713,11 @@ class TeamSelect:
name = self.detail_name
if name is None:
return
spec = POOL[name]
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)
@@ -592,8 +735,11 @@ class TeamSelect:
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))
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:
@@ -604,19 +750,34 @@ class TeamSelect:
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():
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 = spec["stats"]
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, ""))