small update
boss prototype, auto planter, visual reworks, bug fixes
This commit is contained in:
+177
-16
@@ -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, ""))
|
||||
|
||||
Reference in New Issue
Block a user