2600 lines
129 KiB
Python
2600 lines
129 KiB
Python
import time
|
||
|
||
import pygame
|
||
|
||
from assets import icon as icon_path
|
||
from assets import load_sprite
|
||
from engine import bosses, collection
|
||
from engine import runes
|
||
from engine.growth import MAX_LEVEL, can_evolve, compute_stats, evolve_cost, xp_to_next
|
||
from engine.plants import (HP_MULT, PASSIVES, POOL, RARITY_COLORS, RARITY_LABELS,
|
||
VARIABLE_TYPE_UNITS, plant_rarity)
|
||
from engine.roster import DIFFICULTY_RARITIES
|
||
from engine.seeds import PACK_TYPES, REWARD_PACK_TYPES, pack_odds
|
||
from engine.skills import EFF_DOT, EFF_HOT, skill_from_dict
|
||
from engine.types import ALL_TYPES, type_relations
|
||
from engine.unit import ROLE_ATTACKER, ROLE_ICONS, ROLE_NAMES, ROLE_SUPPORT, ROLE_TANK
|
||
from ui import scaling
|
||
from ui.scaling import (draw_type_icon, fit_surface, load_plot_icon, load_rune_icon,
|
||
load_type_icon, make_window, request_quit, to_logical, wrap_text)
|
||
from ui.typepopup import draw_type_popup
|
||
|
||
WIN_W, WIN_H = 1040, 720
|
||
BG = (24, 30, 24)
|
||
TEXT = (230, 235, 230)
|
||
MUTED = (150, 155, 150)
|
||
HINT = (255, 220, 120)
|
||
BTN = (60, 90, 60)
|
||
BTN_HOVER = (85, 125, 85)
|
||
PANEL = (32, 38, 32)
|
||
ACCENT = (120, 220, 190)
|
||
DANGER = (150, 70, 70)
|
||
|
||
TYPE_COLORS = {
|
||
"radiant": (245, 200, 90),
|
||
"nocturnal": (150, 120, 220),
|
||
"ice": (130, 200, 235),
|
||
"garden": (235, 130, 170),
|
||
"wildflower": (120, 200, 120),
|
||
}
|
||
def _draw_star(screen, center, radius, color):
|
||
import math
|
||
pts = []
|
||
for i in range(10):
|
||
ang = -math.pi / 2 + i * math.pi / 5
|
||
r = radius if i % 2 == 0 else radius * 0.45
|
||
pts.append((center[0] + r * math.cos(ang), center[1] + r * math.sin(ang)))
|
||
pygame.draw.polygon(screen, color, pts)
|
||
|
||
|
||
def _draw_evo_badge(screen, font, center, evo_count):
|
||
"""Badge showing how many times a plant has been evolved (0/1/2)."""
|
||
if evo_count > 0:
|
||
bg, border, num = (205, 165, 45), (255, 220, 130), (25, 20, 5)
|
||
else:
|
||
bg, border, num = (78, 84, 78), (115, 120, 115), (195, 195, 195)
|
||
pygame.draw.circle(screen, bg, center, 10)
|
||
pygame.draw.circle(screen, border, center, 10, 2)
|
||
t = font.render(str(evo_count), True, num)
|
||
screen.blit(t, t.get_rect(center=center))
|
||
|
||
OVERVIEW = [
|
||
"A turn-based battle game: 4v4 battles plus a 20-stage Tower.",
|
||
"Pick a difficulty stage, field 4 plants, and fight using",
|
||
"plant types with type advantages, cooldowns, and debuffs.",
|
||
"Open seed packs to earn new plants and duplicates.",
|
||
"Water and evolve your plants to make them stronger.",
|
||
]
|
||
|
||
|
||
def _bracket_text(sk):
|
||
"""The '[value]' shown for a skill, or None if the skill has no such number."""
|
||
if sk.kind in ("damage", "heal", "shield"):
|
||
if sk.mechanic == "full_heal":
|
||
return None
|
||
return str(sk.power)
|
||
for eff in sk.effects:
|
||
if eff.kind in (EFF_HOT, EFF_DOT):
|
||
return str(eff.value)
|
||
return None
|
||
|
||
|
||
class Menu:
|
||
def __init__(self, page="menu"):
|
||
pygame.init()
|
||
self.window, self.scale = make_window(WIN_W, WIN_H)
|
||
self.screen = pygame.Surface((WIN_W, WIN_H))
|
||
pygame.display.set_caption("Flora")
|
||
self.clock = pygame.time.Clock()
|
||
self.font = pygame.font.SysFont("segoeui", 36)
|
||
self.bold = pygame.font.SysFont("segoeui", 20, bold=True)
|
||
self.small = pygame.font.SysFont("segoeui", 16)
|
||
self.tiny = pygame.font.SysFont("segoeui", 13)
|
||
self.page = page
|
||
self.names = list(POOL.keys())
|
||
self.sprites = {n: self._load(n) for n in self.names}
|
||
self.water_icon = self._load_icon("currency_icons", "water_drop", 16)
|
||
self.fert_icon = self._load_icon("currency_icons", "fertilizer", 16)
|
||
self.coin_icon = self._load_icon("currency_icons", "coin", 16)
|
||
self._load_main_icons()
|
||
self._passive_check_at = 0
|
||
self.rune_icons = {}
|
||
self.role_icons = {}
|
||
self.owned = collection.owned()
|
||
self.discovered = collection.discovered()
|
||
self.back_rect = pygame.Rect(16, 16, 90, 34)
|
||
self.detail_name = None
|
||
self.detail_from = "plants"
|
||
self.detail_list = []
|
||
self.detail_index = 0
|
||
self.detail_left = pygame.Rect(18, 300, 36, 48)
|
||
self.detail_right = pygame.Rect(WIN_W - 54, 300, 36, 48)
|
||
self.grid_hit = []
|
||
self.grid_scroll = 0
|
||
self.grid_scroll_max = 0
|
||
self._prev_page = None
|
||
self.water_rect = pygame.Rect(250, 318, 165, 30)
|
||
self.evolve_rect = pygame.Rect(425, 318, 165, 30)
|
||
self.seed_rect = pygame.Rect(250, 352, 220, 30)
|
||
self.runes_rect = pygame.Rect(480, 352, 170, 30)
|
||
self.auto_farmer_rect = pygame.Rect(0, 0, 0, 0)
|
||
self.auto_planter_rect = pygame.Rect(0, 0, 0, 0)
|
||
self.runes_deconstruct_rect = pygame.Rect(WIN_W - 220, 16, 110, 34)
|
||
self.runes_info_rect = pygame.Rect(WIN_W - 100, 16, 84, 34)
|
||
self.runes_deconstruct = False
|
||
self.runes_deconstruct_msg = ""
|
||
self.runes_target = None # rune picked to upgrade
|
||
self.runes_sacrifice = [] # rune ids picked to sacrifice
|
||
self.runes_info = False
|
||
self.runes_filter_tier = 0 # 0 all, 1 tier 1, 2 tier 2
|
||
self.runes_filter_color = None
|
||
self.runes_sort_best = False
|
||
self.runes_filter_rects = []
|
||
self.runes_sort_rect = pygame.Rect(WIN_W - 170, 96, 150, 24)
|
||
self.runes_shop_rect = pygame.Rect(WIN_W - 226, 54, 210, 28)
|
||
self.runes_shop = False # rune shop window open
|
||
self.runes_shop_buy_t1_rect = pygame.Rect(0, 0, 0, 0)
|
||
self.runes_shop_buy_t2_rect = pygame.Rect(0, 0, 0, 0)
|
||
self.runes_buy_popup = None # rune dict for the "bought" popup
|
||
self.runes_buy_msg = ""
|
||
self.runes_buy_msg_until = 0
|
||
self.runes_confirm = None
|
||
self.runes_confirm_ok = pygame.Rect(0, 0, 0, 0)
|
||
self.runes_confirm_no = pygame.Rect(0, 0, 0, 0)
|
||
self.runes_result = None # {"name","color","before","after"} reroll popup
|
||
self.rune_card_rects = []
|
||
self.last_open = None
|
||
self.pack_rects = {}
|
||
self.pack5_rects = {}
|
||
self.info_rects = {}
|
||
self.seed_pack_sprites = {}
|
||
self.seeds_scroll = 0
|
||
self.seeds_scroll_max = 0
|
||
self.info_pack = None
|
||
self.redeem_rect = pygame.Rect(WIN_W - 250, 50, 234, 30)
|
||
self.history_rect = pygame.Rect(WIN_W - 250, 84, 234, 26)
|
||
self.history_open = False
|
||
self.shop_mode = False
|
||
self.shop_rect = pygame.Rect(WIN_W - 250, 114, 234, 28)
|
||
self.shop_btns = {}
|
||
self.shop_msg = ""
|
||
self.shop_msg_until = 0
|
||
self._layout_packs()
|
||
self.diff_rects = {i: pygame.Rect(220, 130 + (i - 1) * 74, 460, 58) for i in range(1, 6)}
|
||
self.floor_rects = {i: pygame.Rect(220, 130 + (i - 1) * 74, 460, 58) for i in range(1, 6)}
|
||
self.floor_bands = {
|
||
1: "Floors 1-100",
|
||
2: "Floors 101-200",
|
||
3: "Floors 201-300",
|
||
4: "Floors 301-400",
|
||
5: "Floors 401-500",
|
||
}
|
||
self.type_info = None
|
||
self.detail_type_rect = None
|
||
self.type_tag_rects = {}
|
||
self.favs = collection.favorites()
|
||
self.star_rect = pygame.Rect(WIN_W - 56, 118, 40, 40)
|
||
self.water_mode = False
|
||
self.water_mode_btn = pygame.Rect(222, 58, 160, 34)
|
||
self.abilities_btn = pygame.Rect(16, 58, 200, 34)
|
||
self.abilities_msg = ""
|
||
self.abilities_msg_until = 0
|
||
self.inject_remove_rect = pygame.Rect(WIN_W - 160, 208, 144, 26)
|
||
self.water_exit_btn = pygame.Rect(WIN_W // 2 - 100, WIN_H - 56, 200, 36)
|
||
self.coll_sort = None
|
||
self.collection_tab = "plants"
|
||
self.collection_tab_btns = {
|
||
"plants": pygame.Rect(WIN_W // 2 - 250, 100, 150, 30),
|
||
"bosses": pygame.Rect(WIN_W // 2 - 70, 100, 150, 30),
|
||
"runes": pygame.Rect(WIN_W // 2 + 110, 100, 150, 30),
|
||
}
|
||
self.coll_rune_tier = 0
|
||
self.coll_rune_color = None
|
||
self.coll_rune_sort_best = False
|
||
self.coll_rune_sort_rect = pygame.Rect(WIN_W - 170, 96, 150, 24)
|
||
self.coll_rune_filter_rects = []
|
||
self.coll_rune_cards = []
|
||
self.coll_rune_detail = None
|
||
self.tower_rect = pygame.Rect(WIN_W // 2 - 130, WIN_H - 92, 260, 58)
|
||
self.tower_info_rect = pygame.Rect(WIN_W // 2 + 140, WIN_H - 88, 24, 24)
|
||
self.tower_info = False
|
||
self.presets_btn = pygame.Rect(WIN_W - 260, 52, 244, 30)
|
||
self.coll_sort_btns = {
|
||
"rarity": pygame.Rect(598, 50, 66, 28),
|
||
"type": pygame.Rect(670, 50, 62, 28),
|
||
"role": pygame.Rect(738, 50, 62, 28),
|
||
"found": pygame.Rect(806, 50, 78, 28),
|
||
}
|
||
self.plants_sort = None
|
||
self.plants_sort_btns = {
|
||
"rarity": pygame.Rect(585, 50, 58, 28),
|
||
"type": pygame.Rect(647, 50, 56, 28),
|
||
"role": pygame.Rect(707, 50, 56, 28),
|
||
"level": pygame.Rect(767, 50, 56, 28),
|
||
"stage": pygame.Rect(827, 50, 58, 28),
|
||
}
|
||
self.soil_msg = None # dict describing a soil popup (harvest result)
|
||
self.soil_fert_info = False
|
||
self.water_info = False
|
||
self.soil_notice = ""
|
||
self.soil_notice_until = 0
|
||
self._layout_buttons()
|
||
|
||
def _reinit(self):
|
||
pygame.init()
|
||
self.window, self.scale = make_window(WIN_W, WIN_H)
|
||
self.screen = pygame.Surface((WIN_W, WIN_H))
|
||
pygame.display.set_caption("Flora")
|
||
self.clock = pygame.time.Clock()
|
||
self.font = pygame.font.SysFont("segoeui", 36)
|
||
self.bold = pygame.font.SysFont("segoeui", 20, bold=True)
|
||
self.small = pygame.font.SysFont("segoeui", 16)
|
||
self.tiny = pygame.font.SysFont("segoeui", 13)
|
||
self.sprites = {n: self._load(n) for n in self.names}
|
||
self.water_icon = self._load_icon("currency_icons", "water_drop", 16)
|
||
self.fert_icon = self._load_icon("currency_icons", "fertilizer", 16)
|
||
self.coin_icon = self._load_icon("currency_icons", "coin", 16)
|
||
self._load_main_icons()
|
||
self.rune_icons = {}
|
||
self.seed_pack_sprites = {}
|
||
self.seeds_scroll = getattr(self, "seeds_scroll", 0)
|
||
|
||
def _load(self, name):
|
||
return load_sprite(name, "full")
|
||
|
||
def _load_icon(self, category, name, size):
|
||
try:
|
||
img = pygame.image.load(icon_path(category, name)).convert_alpha()
|
||
return fit_surface(img, size)
|
||
except (pygame.error, OSError):
|
||
return None
|
||
|
||
def _load_main_icons(self):
|
||
self.main_icons = {
|
||
"Plants": self._load_icon("menu_icons", "plants", 36),
|
||
"Battle": self._load_icon("menu_icons", "battle", 36),
|
||
"Runes": self._load_icon("menu_icons", "runes", 56),
|
||
"Seeds": self._load_icon("menu_icons", "seed_pack", 56),
|
||
"Soil": self._load_icon("menu_icons", "soil", 56),
|
||
"Collection": self._load_icon("menu_icons", "collection", 56),
|
||
}
|
||
self.tower_icon = self._load_icon("menu_icons", "tower", 28)
|
||
|
||
def _rune_icon(self, color, size=16):
|
||
key = (color, size)
|
||
if key not in self.rune_icons:
|
||
self.rune_icons[key] = load_rune_icon(color, size)
|
||
return self.rune_icons[key]
|
||
|
||
def _role_icon(self, role, size=16):
|
||
key = (role, size)
|
||
if key not in self.role_icons:
|
||
name = ROLE_ICONS.get(role)
|
||
self.role_icons[key] = self._load_icon("role_icons", name, size) if name else None
|
||
return self.role_icons[key]
|
||
|
||
def _fit_text(self, text, font, max_w):
|
||
"""Truncate `text` to fit within `max_w` pixels, appending an ellipsis."""
|
||
if font.size(text)[0] <= max_w:
|
||
return text
|
||
while text and font.size(text + "...")[0] > max_w:
|
||
text = text[:-1]
|
||
return (text + "...") if text else ""
|
||
|
||
def _layout_buttons(self):
|
||
# Bottom row: Plants and Battle side by side.
|
||
bw, bh, gap = 200, 54, 20
|
||
total = 2 * bw + gap
|
||
x0 = (WIN_W - total) // 2
|
||
y = WIN_H - 72
|
||
self.buttons = {
|
||
"Plants": pygame.Rect(x0, y, bw, bh),
|
||
"Battle": pygame.Rect(x0 + bw + gap, y, bw, bh),
|
||
}
|
||
# Side buttons (square-ish): Soil & Seeds left, Runes & Collection right.
|
||
# Fitted into the lower half, keeping clear of the bottom row.
|
||
sq, sq_gap = 104, 16
|
||
ry = WIN_H - 72 - 2 * sq - sq_gap - 10
|
||
self.side_buttons = {
|
||
"Soil": pygame.Rect(34, ry, sq, sq),
|
||
"Seeds": pygame.Rect(34, ry + sq + sq_gap, sq, sq),
|
||
}
|
||
rx = WIN_W - 34 - sq
|
||
self.side_buttons["Runes"] = pygame.Rect(rx, ry, sq, sq)
|
||
self.side_buttons["Collection"] = pygame.Rect(rx, ry + sq + sq_gap, sq, sq)
|
||
|
||
def _seed_pack_sprite(self, pt, size=110):
|
||
"""Seed pack sprite scaled to fit a `size` box (aspect preserved), cached."""
|
||
key = (pt, size)
|
||
if key not in self.seed_pack_sprites:
|
||
try:
|
||
img = pygame.image.load(
|
||
icon_path("seed_packs/closed", f"{pt}_seedpack")).convert_alpha()
|
||
img = fit_surface(img, size)
|
||
except (pygame.error, OSError):
|
||
img = None
|
||
self.seed_pack_sprites[key] = img
|
||
return self.seed_pack_sprites[key]
|
||
|
||
def _layout_packs(self):
|
||
cw, ch, gap = 210, 200, 14
|
||
cols = 4
|
||
total = cols * cw + (cols - 1) * gap
|
||
x0 = (WIN_W - total) // 2
|
||
y0 = 120
|
||
for i, pt in enumerate(PACK_TYPES):
|
||
x = x0 + (i % cols) * (cw + gap)
|
||
y = y0 + (i // cols) * (ch + gap)
|
||
card = pygame.Rect(x, y, cw, ch)
|
||
btn = pygame.Rect(x + 8, y + ch - 28, 62, 24)
|
||
btn5 = pygame.Rect(x + 74, y + ch - 28, 62, 24)
|
||
self.pack_rects[pt] = (card, btn)
|
||
self.pack5_rects[pt] = btn5
|
||
self.info_rects[pt] = pygame.Rect(x + cw - 26, y + 8, 18, 18)
|
||
rows = (len(PACK_TYPES) + cols - 1) // cols
|
||
grid_h = rows * (ch + gap) - gap
|
||
content_h = grid_h + 16 + 110
|
||
self.seeds_grid_y0 = y0
|
||
self.seeds_grid_h = grid_h
|
||
self.seeds_scroll_area = pygame.Rect(0, 96, WIN_W, WIN_H - 96 - 8)
|
||
self.seeds_scroll_max = max(0, content_h - self.seeds_scroll_area.h)
|
||
self.seeds_scroll = getattr(self, "seeds_scroll", 0)
|
||
self.seeds_scroll = max(0, min(self.seeds_scroll, self.seeds_scroll_max))
|
||
|
||
def run(self):
|
||
running = True
|
||
result = None
|
||
while running:
|
||
if scaling.QUIT_REQUESTED:
|
||
running = False
|
||
break
|
||
self.clock.tick(60)
|
||
self.mouse_pos = to_logical(pygame.mouse.get_pos(), self.scale)
|
||
if pygame.time.get_ticks() >= self._passive_check_at:
|
||
collection.accrue_coins()
|
||
collection.accrue_fertilizer()
|
||
collection.auto_farm()
|
||
collection.auto_plant()
|
||
self._passive_check_at = pygame.time.get_ticks() + 1000
|
||
if self.page != self._prev_page:
|
||
self.grid_scroll = 0
|
||
self.seeds_scroll = 0
|
||
if self.page != "runes":
|
||
self.runes_deconstruct = False
|
||
self.runes_deconstruct_msg = ""
|
||
self.runes_target = None
|
||
self.runes_sacrifice = []
|
||
self.runes_info = False
|
||
self.runes_confirm = None
|
||
self.runes_result = None
|
||
self.runes_buy_popup = None
|
||
self.runes_buy_msg = ""
|
||
self._prev_page = self.page
|
||
for event in pygame.event.get():
|
||
if event.type == pygame.QUIT:
|
||
request_quit()
|
||
running = False
|
||
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||
choice = self._click(to_logical(event.pos, self.scale))
|
||
if isinstance(choice, int) or choice == "tower":
|
||
result = choice
|
||
running = False
|
||
elif event.type == pygame.MOUSEWHEEL:
|
||
if self.page == "seeds":
|
||
self.seeds_scroll = max(0, min(self.seeds_scroll_max,
|
||
self.seeds_scroll - event.y * 60))
|
||
elif self.page in ("plants", "collection", "runes"):
|
||
self.grid_scroll = max(0, min(self.grid_scroll_max,
|
||
self.grid_scroll - event.y * 50))
|
||
self._draw()
|
||
pygame.transform.smoothscale(self.screen, self.window.get_size(), self.window)
|
||
pygame.display.flip()
|
||
return result
|
||
|
||
def _click(self, pos):
|
||
if self.info_pack is not None:
|
||
self.info_pack = None
|
||
return None
|
||
if self.type_info is not None:
|
||
self.type_info = None
|
||
return None
|
||
if self.shop_mode:
|
||
if self.shop_msg and pygame.time.get_ticks() < self.shop_msg_until:
|
||
return None # a "bought" notice is showing; ignore clicks briefly
|
||
for pt, rect in self.shop_btns.items():
|
||
if rect.collidepoint(pos):
|
||
err = collection.buy_pack(pt)
|
||
self.shop_msg = err or f"Bought a {pt.capitalize()} pack!"
|
||
self.shop_msg_until = pygame.time.get_ticks() + 1800
|
||
return None
|
||
self.shop_mode = False
|
||
return None
|
||
if self.page == "seeds":
|
||
if self.history_open:
|
||
self.history_open = False
|
||
return None
|
||
if self.back_rect.collidepoint(pos):
|
||
self.page = "menu"
|
||
return None
|
||
if self.history_rect.collidepoint(pos):
|
||
self.history_open = True
|
||
return None
|
||
if self.redeem_rect.collidepoint(pos):
|
||
collection.redeem_tower_pack()
|
||
return None
|
||
if self.shop_rect.collidepoint(pos):
|
||
self.shop_mode = True
|
||
return None
|
||
for pt, (card, btn) in self.pack_rects.items():
|
||
if btn.move(0, -self.seeds_scroll).collidepoint(pos):
|
||
result = collection.open_pack(pt)
|
||
if result:
|
||
self.last_open = result
|
||
self.owned = collection.owned()
|
||
self.discovered = collection.discovered()
|
||
return None
|
||
if self.pack5_rects[pt].move(0, -self.seeds_scroll).collidepoint(pos):
|
||
if collection.pack_counts().get(pt, 0) >= 5:
|
||
results = collection.open_packs(pt, 5)
|
||
if results:
|
||
self.last_open = results
|
||
self.owned = collection.owned()
|
||
self.discovered = collection.discovered()
|
||
return None
|
||
if self.info_rects[pt].move(0, -self.seeds_scroll).collidepoint(pos):
|
||
self.info_pack = pt
|
||
return None
|
||
return None
|
||
if self.page in ("plants", "collection"):
|
||
if self.back_rect.collidepoint(pos):
|
||
self.page = "menu"
|
||
return None
|
||
if self.page == "collection":
|
||
if self.coll_rune_detail is not None:
|
||
self.coll_rune_detail = None
|
||
return None
|
||
for key, rect in self.collection_tab_btns.items():
|
||
if rect.collidepoint(pos):
|
||
self.collection_tab = key
|
||
return None
|
||
if self.collection_tab == "bosses":
|
||
return None # boss cards are informational only
|
||
if self.collection_tab == "runes":
|
||
if self._click_coll_rune_filter(pos):
|
||
return None
|
||
owned_colors = {r["color"] for r in collection.rune_inventory()}
|
||
for color, rect in self.coll_rune_cards:
|
||
if rect.collidepoint(pos) and color in owned_colors:
|
||
self.coll_rune_detail = color
|
||
return None
|
||
return None
|
||
if self.page == "plants":
|
||
if self.water_info:
|
||
self.water_info = False
|
||
return None
|
||
for key, rect in self.plants_sort_btns.items():
|
||
if rect.collidepoint(pos):
|
||
self.plants_sort = None if self.plants_sort == key else key
|
||
return None
|
||
if self.water_info_rect.collidepoint(pos):
|
||
self.water_info = True
|
||
return None
|
||
if self.abilities_btn.collidepoint(pos):
|
||
if collection.get_tower_best() >= collection.EXTRACT_GATE:
|
||
from ui.extract import ExtractionScreen
|
||
ExtractionScreen().run()
|
||
self._reinit() # the extract screen quit pygame
|
||
self.owned = collection.owned()
|
||
self.favs = collection.favorites()
|
||
else:
|
||
self.abilities_msg = f"Reach Tower stage {collection.EXTRACT_GATE} to unlock"
|
||
self.abilities_msg_until = pygame.time.get_ticks() + 2000
|
||
return None
|
||
if self.water_mode_btn.collidepoint(pos):
|
||
self.water_mode = not self.water_mode
|
||
return None
|
||
if self.water_mode:
|
||
if self.water_exit_btn.collidepoint(pos):
|
||
self.water_mode = False
|
||
return None
|
||
for name, rect in self.grid_hit:
|
||
if rect.collidepoint(pos):
|
||
collection.water(name)
|
||
return None
|
||
return None
|
||
for name, rect in self.type_tag_rects.items():
|
||
if rect.collidepoint(pos) and name not in VARIABLE_TYPE_UNITS:
|
||
self.type_info = POOL[name]["type"]
|
||
return None
|
||
for name, rect in self.grid_hit:
|
||
if rect.collidepoint(pos):
|
||
self.detail_name = name
|
||
self.detail_from = self.page
|
||
self.detail_list = [n for n, _ in self.grid_hit]
|
||
self.detail_index = self.detail_list.index(name)
|
||
self.page = "plant_detail"
|
||
return None
|
||
return None
|
||
if self.page == "plant_detail":
|
||
if self.star_rect.collidepoint(pos) and self.detail_name:
|
||
collection.toggle_favorite(self.detail_name)
|
||
self.favs = collection.favorites()
|
||
return None
|
||
if self.detail_type_rect and self.detail_type_rect.collidepoint(pos) and self.detail_name \
|
||
and self.detail_name not in VARIABLE_TYPE_UNITS:
|
||
self.type_info = POOL[self.detail_name]["type"]
|
||
return None
|
||
for pt, rect in self.type_choice_rects.items():
|
||
if rect.collidepoint(pos) and self.detail_name:
|
||
collection.set_unit_type(self.detail_name, pt)
|
||
return None
|
||
if self.back_rect.collidepoint(pos):
|
||
self.page = self.detail_from
|
||
elif self.detail_left.collidepoint(pos):
|
||
self._nav_detail(-1)
|
||
elif self.detail_right.collidepoint(pos):
|
||
self._nav_detail(1)
|
||
elif self.water_rect.collidepoint(pos) and self.detail_name and self.detail_from != "collection":
|
||
collection.water(self.detail_name)
|
||
elif self.evolve_rect.collidepoint(pos) and self.detail_name and self.detail_from != "collection":
|
||
collection.evolve(self.detail_name)
|
||
elif self.seed_rect.collidepoint(pos) and self.detail_name and self.detail_from != "collection":
|
||
collection.retrieve_seed(self.detail_name)
|
||
elif (self.runes_rect.collidepoint(pos) and self.detail_name
|
||
and self.detail_from != "collection"):
|
||
from ui.runes import RuneScreen
|
||
RuneScreen(self.detail_name).run()
|
||
if not scaling.QUIT_REQUESTED:
|
||
self._reinit() # RuneScreen re-created the window
|
||
elif (self.inject_remove_rect.collidepoint(pos) and self.detail_name
|
||
and self.detail_from != "collection" and collection.injection_of(self.detail_name)):
|
||
collection.remove_injection(self.detail_name)
|
||
return None
|
||
if self.page == "battle_setup":
|
||
if self.tower_info:
|
||
self.tower_info = False
|
||
return None
|
||
if self.back_rect.collidepoint(pos):
|
||
self.page = "menu"
|
||
return None
|
||
if self.tower_info_rect.collidepoint(pos):
|
||
self.tower_info = not self.tower_info
|
||
return None
|
||
if self.presets_btn.collidepoint(pos):
|
||
from ui.presets import PresetScreen
|
||
PresetScreen().run()
|
||
self._reinit() # PresetScreen quit pygame
|
||
return None
|
||
if self.tower_rect.collidepoint(pos):
|
||
return "tower"
|
||
for band, rect in self.floor_rects.items():
|
||
if rect.collidepoint(pos) and band == 1 \
|
||
and collection.floor_progress() <= collection.MAX_FLOOR:
|
||
return collection.floor_progress()
|
||
return None
|
||
if self.page == "soil":
|
||
if self.soil_msg is not None:
|
||
self.soil_msg = None # click closes the harvest popup
|
||
return None
|
||
if self.soil_fert_info:
|
||
self.soil_fert_info = False
|
||
return None
|
||
if self.soil_fert_info_rect.collidepoint(pos):
|
||
self.soil_fert_info = True
|
||
return None
|
||
if self.back_rect.collidepoint(pos):
|
||
self.page = "menu"
|
||
return None
|
||
if self.auto_farmer_rect.collidepoint(pos):
|
||
if not collection.auto_farmer_owned():
|
||
reason = collection.buy_auto_farmer()
|
||
self.soil_notice = reason or "Auto farmer bought!"
|
||
else:
|
||
active = collection.toggle_auto_farmer()
|
||
self.soil_notice = "Auto farmer " + (
|
||
"activated" if active else "deactivated")
|
||
self.soil_notice_until = pygame.time.get_ticks() + 1800
|
||
return None
|
||
if self.auto_planter_rect.collidepoint(pos):
|
||
if not collection.auto_planter_owned():
|
||
reason = collection.buy_auto_planter()
|
||
self.soil_notice = reason or "Auto planter bought!"
|
||
else:
|
||
active = collection.toggle_auto_planter()
|
||
self.soil_notice = "Auto planter " + (
|
||
"activated" if active else "deactivated")
|
||
self.soil_notice_until = pygame.time.get_ticks() + 1800
|
||
return None
|
||
for idx, rect in self.soil_seed_rects:
|
||
if rect.collidepoint(pos):
|
||
collection.plant_seed(idx)
|
||
return None
|
||
for idx, rect in self.soil_water_rects:
|
||
if rect.collidepoint(pos):
|
||
collection.water_plot(idx)
|
||
return None
|
||
for idx, rect in self.soil_harvest_rects:
|
||
if rect.collidepoint(pos):
|
||
state = collection.soil_state()
|
||
if 0 <= idx < len(state["plots"]):
|
||
plant = state["plots"][idx]["plant"]
|
||
if collection.harvest_plot(idx) == "":
|
||
self.owned = collection.owned()
|
||
self.soil_msg = {
|
||
"plant": plant,
|
||
"duplicates": collection.get_progress(plant)["duplicates"],
|
||
}
|
||
return None
|
||
for idx, rect in self.soil_unlock_rects:
|
||
if rect.collidepoint(pos):
|
||
reason = collection.unlock_plot()
|
||
if reason:
|
||
self.soil_notice = reason
|
||
self.soil_notice_until = pygame.time.get_ticks() + 1800
|
||
return None
|
||
return None
|
||
if self.page == "runes":
|
||
if self.runes_buy_popup is not None:
|
||
self.runes_buy_popup = None
|
||
return None
|
||
if self.runes_shop:
|
||
if self.runes_shop_buy_t1_rect.collidepoint(pos):
|
||
self._buy_rune(1)
|
||
elif self.runes_shop_buy_t2_rect.collidepoint(pos):
|
||
self._buy_rune(2)
|
||
else:
|
||
self.runes_shop = False
|
||
return None
|
||
if self.runes_result is not None:
|
||
self.runes_result = None
|
||
return None
|
||
if self.runes_info:
|
||
self.runes_info = False
|
||
return None
|
||
if self.runes_confirm is not None:
|
||
if self.runes_confirm_ok.collidepoint(pos):
|
||
rune_id = self.runes_confirm["rune"]["id"]
|
||
before = self.runes_confirm["rune"].get("pct", 0)
|
||
err = collection.reroll_rune(rune_id, self.runes_confirm["sacrifice"])
|
||
self.runes_deconstruct_msg = err or ""
|
||
if not err:
|
||
after = next((r["pct"] for r in collection.rune_inventory()
|
||
if r["id"] == rune_id), before)
|
||
self.runes_result = {
|
||
"name": self.runes_confirm["rune"]["name"],
|
||
"color": self.runes_confirm["rune"]["color"],
|
||
"before": before,
|
||
"after": after,
|
||
}
|
||
self.runes_deconstruct = False
|
||
self.runes_target = None
|
||
self.runes_sacrifice = []
|
||
self.runes_confirm = None
|
||
elif self.runes_confirm_no.collidepoint(pos):
|
||
self.runes_confirm = None
|
||
return None
|
||
if self.back_rect.collidepoint(pos):
|
||
self.page = "menu"
|
||
return None
|
||
if self.runes_info_rect.collidepoint(pos):
|
||
self.runes_info = True
|
||
return None
|
||
if self.runes_deconstruct_rect.collidepoint(pos):
|
||
self.runes_deconstruct = not self.runes_deconstruct
|
||
self.runes_deconstruct_msg = ""
|
||
self.runes_target = None
|
||
self.runes_sacrifice = []
|
||
return None
|
||
if self.runes_shop_rect.collidepoint(pos):
|
||
self.runes_shop = True
|
||
self.runes_info = False
|
||
self.runes_buy_msg = ""
|
||
return None
|
||
if self._click_runes_filter(pos):
|
||
return None
|
||
if self.runes_deconstruct:
|
||
self._click_runes_deconstruct(pos)
|
||
return None
|
||
if self.page != "menu":
|
||
if self.back_rect.collidepoint(pos):
|
||
self.page = "menu"
|
||
return None
|
||
for label, rect in self.buttons.items():
|
||
if rect.collidepoint(pos):
|
||
if label == "Battle":
|
||
self.page = "battle_setup"
|
||
return None
|
||
self.page = label.lower()
|
||
return None
|
||
for label, rect in self.side_buttons.items():
|
||
if rect.collidepoint(pos):
|
||
if label == "Soil" and not collection.soil_unlocked():
|
||
self.soil_msg = None
|
||
return None
|
||
self.page = label.lower()
|
||
return None
|
||
return None
|
||
|
||
def _draw(self):
|
||
if self.water_mode and (self.page != "plants" or collection.available_water() <= 0):
|
||
self.water_mode = False
|
||
self.screen.fill(BG)
|
||
if self.page == "menu":
|
||
self._draw_menu()
|
||
elif self.page == "battle_setup":
|
||
self._draw_battle_setup()
|
||
elif self.page == "seeds":
|
||
self._draw_seeds()
|
||
elif self.page == "plant_detail":
|
||
self._draw_detail()
|
||
elif self.page == "plants":
|
||
self._draw_plants()
|
||
elif self.page == "soil":
|
||
self._draw_soil()
|
||
elif self.page == "runes":
|
||
self._draw_runes()
|
||
else:
|
||
self._draw_collection()
|
||
if self.info_pack is not None:
|
||
self._draw_pack_info()
|
||
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)
|
||
if self.shop_mode:
|
||
self._draw_shop()
|
||
if self.tower_info:
|
||
self._draw_tower_info()
|
||
if self.soil_msg:
|
||
self._draw_soil_msg()
|
||
if self.soil_fert_info:
|
||
self._draw_fert_info()
|
||
if self.water_info:
|
||
self._draw_water_info()
|
||
|
||
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
|
||
self.coll_rune_detail = None
|
||
self.type_choice_rects = {}
|
||
|
||
def _draw_resources(self):
|
||
"""Small boxes along the top of the start screen showing the current
|
||
coin, water, and fertilizer balances. The coin box shows its cap and a
|
||
countdown to the next coin until the cap is reached."""
|
||
data = collection.load()
|
||
coins = data.get("coins", 0)
|
||
items = [
|
||
("Coins", coins, self.coin_icon),
|
||
("Water", data.get("water", 0), self.water_icon),
|
||
("Fertilizer", data.get("fertilizer", 0), self.fert_icon),
|
||
]
|
||
bw, bh, gap = 144, 44, 14
|
||
total = len(items) * bw + (len(items) - 1) * gap
|
||
x0 = (WIN_W - total) // 2
|
||
y = 16
|
||
for i, (label, val, icon) in enumerate(items):
|
||
rect = pygame.Rect(x0 + i * (bw + gap), y, bw, bh)
|
||
pygame.draw.rect(self.screen, PANEL, rect, border_radius=8)
|
||
pygame.draw.rect(self.screen, (60, 70, 60), rect, 1, border_radius=8)
|
||
if icon:
|
||
self.screen.blit(icon, (rect.x + 12, rect.centery - 8))
|
||
if label == "Coins":
|
||
txt = self.small.render(f"{coins}/{collection.COIN_CAP}", True, TEXT)
|
||
elif label == "Fertilizer":
|
||
txt = self.small.render(f"{val}/{collection.FERTILIZER_CAP}", True, TEXT)
|
||
else:
|
||
txt = self.small.render(str(val), True, TEXT)
|
||
self.screen.blit(txt, (rect.x + 36, rect.centery - txt.get_height() // 2))
|
||
if coins < collection.COIN_CAP:
|
||
last = data.get("coins_accrued_at", 0.0)
|
||
if last > 0:
|
||
secs = max(1, int(60 - (time.time() - last) % 60))
|
||
nt = self.tiny.render(f"Next in {secs // 60}:{secs % 60:02d}", True, MUTED)
|
||
self.screen.blit(nt, nt.get_rect(midtop=(x0 + bw // 2, y + bh + 2)))
|
||
fert = data.get("fertilizer", 0)
|
||
if fert < collection.FERTILIZER_CAP:
|
||
last = data.get("fertilizer_accrued_at", 0.0)
|
||
if last > 0:
|
||
secs = max(1, int(3600 - (time.time() - last) % 3600))
|
||
hh, rem = divmod(secs, 3600)
|
||
mm, ss = divmod(rem, 60)
|
||
nt = self.tiny.render(f"Next in {hh:02d}:{mm:02d}:{ss:02d}", True, MUTED)
|
||
fert_x = x0 + 2 * (bw + gap) + bw // 2
|
||
self.screen.blit(nt, nt.get_rect(midtop=(fert_x, y + bh + 2)))
|
||
|
||
def _draw_menu(self):
|
||
self._draw_resources()
|
||
title = self.font.render("Flora", True, TEXT)
|
||
self.screen.blit(title, title.get_rect(center=(WIN_W // 2, 86)))
|
||
y = 130
|
||
for line in OVERVIEW:
|
||
t = self.small.render(line, True, MUTED)
|
||
self.screen.blit(t, t.get_rect(center=(WIN_W // 2, y)))
|
||
y += 24
|
||
mouse = self.mouse_pos
|
||
# bottom row: Plants + Battle
|
||
for label, rect in self.buttons.items():
|
||
hover = rect.collidepoint(mouse)
|
||
if label == "Battle":
|
||
bg = (90, 140, 90) if hover else BTN_HOVER
|
||
else:
|
||
bg = BTN_HOVER if hover else BTN
|
||
pygame.draw.rect(self.screen, bg, rect, border_radius=12)
|
||
pygame.draw.rect(self.screen, (150, 190, 150), rect, 2, border_radius=12)
|
||
ic = self.main_icons.get(label)
|
||
if ic:
|
||
self.screen.blit(ic, ic.get_rect(center=rect.center))
|
||
else:
|
||
t = self.bold.render(label, True, TEXT)
|
||
self.screen.blit(t, t.get_rect(center=rect.center))
|
||
# side buttons: Soil & Seeds (left), Runes & Collection (right)
|
||
soil_locked = not collection.soil_unlocked()
|
||
for label, rect in self.side_buttons.items():
|
||
hover = rect.collidepoint(mouse)
|
||
is_soil = label == "Soil"
|
||
if is_soil and soil_locked:
|
||
bg = (45, 50, 45)
|
||
border = (70, 80, 70)
|
||
else:
|
||
bg = BTN_HOVER if hover else BTN
|
||
border = (150, 190, 150)
|
||
pygame.draw.rect(self.screen, bg, rect, border_radius=18)
|
||
pygame.draw.rect(self.screen, border, rect, 2, border_radius=18)
|
||
ic = self.main_icons.get(label)
|
||
if ic:
|
||
ipos = ic.get_rect(center=rect.center)
|
||
if is_soil and soil_locked:
|
||
# Dim only the icon's opaque pixels (silhouette) so the
|
||
# transparent background stays clear.
|
||
shade = pygame.Surface(ic.get_size(), pygame.SRCALPHA)
|
||
shade.fill((0, 0, 0, 150))
|
||
shade.blit(ic, (0, 0), special_flags=pygame.BLEND_RGBA_MIN)
|
||
self.screen.blit(ic, ipos)
|
||
self.screen.blit(shade, ipos)
|
||
lt = self.bold.render("Soil", True, (235, 100, 100))
|
||
self.screen.blit(lt, lt.get_rect(center=rect.center))
|
||
else:
|
||
self.screen.blit(ic, ipos)
|
||
else:
|
||
t = self.bold.render(label, True, TEXT)
|
||
self.screen.blit(t, t.get_rect(center=rect.center))
|
||
if is_soil:
|
||
self.soil_btn_locked = soil_locked
|
||
if soil_locked:
|
||
self.soil_btn_locked = True
|
||
n = collection.SOIL_GATE_PLANTS - len(collection.owned())
|
||
hint = self.tiny.render(f"Own {collection.SOIL_GATE_PLANTS} plants to unlock Soil"
|
||
f" ({n} to go)", True, (110, 115, 110))
|
||
self.screen.blit(hint, hint.get_rect(midleft=(self.side_buttons["Soil"].left,
|
||
self.side_buttons["Soil"].top - 6)))
|
||
|
||
def _draw_battle_setup(self):
|
||
self._draw_page("Select Floors",
|
||
"Pick a floor range to fight in, or take on the Tower")
|
||
mouse = self.mouse_pos
|
||
current = collection.floor_progress()
|
||
for band, rect in self.floor_rects.items():
|
||
hover = rect.collidepoint(mouse)
|
||
playable = band == 1 and current <= collection.MAX_FLOOR
|
||
bg = (90, 120, 90) if (hover and playable) else BTN
|
||
pygame.draw.rect(self.screen, bg, rect, border_radius=8)
|
||
pygame.draw.rect(self.screen, (150, 190, 150) if playable else (70, 80, 70),
|
||
rect, 2, border_radius=8)
|
||
label = self.floor_bands[band]
|
||
t = self.bold.render(label, True, TEXT if playable else MUTED)
|
||
self.screen.blit(t, (rect.x + 16, rect.y + 8))
|
||
if band == 1:
|
||
sub = self.tiny.render(f"Your floor: {current}/{collection.MAX_FLOOR}"
|
||
f" · Win: packs + floor up", True, MUTED)
|
||
else:
|
||
sub = self.tiny.render("Coming soon", True, (110, 115, 110))
|
||
self.screen.blit(sub, (rect.x + 16, rect.y + 32))
|
||
tower_hover = self.tower_rect.collidepoint(mouse)
|
||
pygame.draw.rect(self.screen, (90, 120, 90) if tower_hover else BTN,
|
||
self.tower_rect, border_radius=8)
|
||
pygame.draw.rect(self.screen, (245, 200, 90), self.tower_rect, 2, border_radius=8)
|
||
tt = self.bold.render("Tower — 20 stages", True, TEXT)
|
||
tic = self.tower_icon
|
||
row1_y = self.tower_rect.y + 16
|
||
if tic:
|
||
total_w = tic.get_width() + 6 + tt.get_width()
|
||
tx = self.tower_rect.centerx - total_w // 2
|
||
self.screen.blit(tic, tic.get_rect(midleft=(tx, row1_y + tt.get_height() // 2)))
|
||
self.screen.blit(tt, (tx + tic.get_width() + 6, row1_y))
|
||
else:
|
||
self.screen.blit(tt, tt.get_rect(center=(self.tower_rect.centerx, row1_y + tt.get_height() // 2)))
|
||
from engine.collection import tower_max_stage
|
||
mst = tower_max_stage()
|
||
ts = self.tiny.render(f"Stage {mst} open · Water + packs · Best {collection.get_tower_best()}",
|
||
True, MUTED)
|
||
self.screen.blit(ts, ts.get_rect(center=(self.tower_rect.centerx, self.tower_rect.y + 40)))
|
||
ihov = self.tower_info_rect.collidepoint(mouse)
|
||
pygame.draw.circle(self.screen, (115, 135, 115) if ihov else (70, 82, 70),
|
||
self.tower_info_rect.center, 12)
|
||
pygame.draw.circle(self.screen, (175, 195, 175), self.tower_info_rect.center, 12, 1)
|
||
itxt = self.tiny.render("i", True, TEXT)
|
||
self.screen.blit(itxt, itxt.get_rect(center=self.tower_info_rect.center))
|
||
pygame.draw.rect(self.screen, BTN, self.presets_btn, border_radius=6)
|
||
pygame.draw.rect(self.screen, (150, 190, 150), self.presets_btn, 1, border_radius=6)
|
||
ptxt = self.tiny.render("Team presets (P1–P4)", True, TEXT)
|
||
self.screen.blit(ptxt, ptxt.get_rect(center=self.presets_btn.center))
|
||
|
||
def _draw_tower_info(self):
|
||
from engine.collection import tower_max_stage
|
||
mst = tower_max_stage()
|
||
overlay = pygame.Surface((WIN_W, WIN_H), pygame.SRCALPHA)
|
||
overlay.fill((0, 0, 0, 160))
|
||
self.screen.blit(overlay, (0, 0))
|
||
w, h = 440, 360
|
||
panel = pygame.Rect((WIN_W - w) // 2, (WIN_H - h) // 2, w, h)
|
||
pygame.draw.rect(self.screen, PANEL, panel, border_radius=12)
|
||
pygame.draw.rect(self.screen, (245, 200, 90), panel, 2, border_radius=12)
|
||
title = self.bold.render("Tower access", True, HINT)
|
||
self.screen.blit(title, title.get_rect(center=(panel.centerx, panel.y + 30)))
|
||
sub = self.tiny.render(f"You can currently reach stage {mst}", True, MUTED)
|
||
self.screen.blit(sub, sub.get_rect(center=(panel.centerx, panel.y + 56)))
|
||
rows = [
|
||
("1 - 5", "Always available"),
|
||
("6 - 10", "Own at least one evolved plant"),
|
||
("11 - 15", "Own at least one max-level plant"),
|
||
("16 - 20", "Own a max-level plant and any injected plant"),
|
||
]
|
||
y = panel.y + 96
|
||
for band, req in rows:
|
||
rar = HINT if mst >= int(band.split(" - ")[1]) else MUTED
|
||
b = self.bold.render(band, True, rar)
|
||
self.screen.blit(b, (panel.x + 34, y))
|
||
r = self.small.render(req, True, TEXT if rar == HINT else MUTED)
|
||
self.screen.blit(r, (panel.x + 130, y))
|
||
y += 34
|
||
close = self.tiny.render("Click anywhere to close", True, MUTED)
|
||
self.screen.blit(close, close.get_rect(center=(panel.centerx, panel.bottom - 20)))
|
||
|
||
def _draw_page(self, title_text, subtitle):
|
||
t = self.font.render(title_text, True, TEXT)
|
||
self.screen.blit(t, t.get_rect(center=(WIN_W // 2, 52)))
|
||
t = self.small.render(subtitle, True, MUTED)
|
||
self.screen.blit(t, t.get_rect(center=(WIN_W // 2, 84)))
|
||
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))
|
||
|
||
def _draw_seeds(self):
|
||
self._draw_page("Seeds", "Win packs scale with difficulty · Tower packs have high rarity")
|
||
packs = collection.pack_counts()
|
||
tcol_lesser = (150, 150, 150)
|
||
tcol_tower = (245, 200, 90)
|
||
tpacks = packs.get("tower", 0)
|
||
can_redeem = tpacks > 0
|
||
pygame.draw.rect(self.screen, BTN_HOVER if can_redeem else (45, 50, 45),
|
||
self.redeem_rect, border_radius=6)
|
||
pygame.draw.rect(self.screen, (150, 190, 150) if can_redeem else (70, 80, 70),
|
||
self.redeem_rect, 1, border_radius=6)
|
||
rtxt = f"Redeem tower pack (+10 water) · {tpacks}" if can_redeem else "No tower packs"
|
||
r = self.tiny.render(rtxt, True, TEXT if can_redeem else MUTED)
|
||
self.screen.blit(r, r.get_rect(center=self.redeem_rect.center))
|
||
mouse = self.mouse_pos
|
||
h_hover = self.history_rect.collidepoint(mouse)
|
||
pygame.draw.rect(self.screen, (90, 120, 90) if h_hover else BTN,
|
||
self.history_rect, border_radius=6)
|
||
pygame.draw.rect(self.screen, (150, 190, 150), self.history_rect, 1, border_radius=6)
|
||
ht = self.tiny.render("History (last 10)", True, TEXT)
|
||
self.screen.blit(ht, ht.get_rect(center=self.history_rect.center))
|
||
shov = self.shop_rect.collidepoint(mouse)
|
||
pygame.draw.rect(self.screen, (90, 120, 90) if shov else BTN,
|
||
self.shop_rect, border_radius=6)
|
||
pygame.draw.rect(self.screen, (150, 190, 150), self.shop_rect, 1, border_radius=6)
|
||
sh = self.tiny.render("Pack Shop", True, TEXT)
|
||
self.screen.blit(sh, sh.get_rect(center=self.shop_rect.center))
|
||
# scrollable grid of packs, each showing its sprite at native size
|
||
old_clip = self.screen.get_clip()
|
||
self.screen.set_clip(self.seeds_scroll_area)
|
||
for pt, (card, btn) in self.pack_rects.items():
|
||
rect = card.move(0, -self.seeds_scroll)
|
||
if not rect.colliderect(self.seeds_scroll_area):
|
||
continue
|
||
pygame.draw.rect(self.screen, PANEL, rect, border_radius=8)
|
||
pygame.draw.rect(self.screen, (60, 70, 60), rect, 1, border_radius=8)
|
||
spr = self._seed_pack_sprite(pt)
|
||
if spr:
|
||
self.screen.blit(spr, spr.get_rect(center=(rect.centerx, rect.y + 112)))
|
||
if pt == "tower":
|
||
tcol = tcol_tower
|
||
elif pt == "lesser":
|
||
tcol = tcol_lesser
|
||
else:
|
||
tcol = TYPE_COLORS.get(pt, TEXT)
|
||
t = self.bold.render(pt.capitalize(), True, tcol)
|
||
self.screen.blit(t, (rect.x + 8, rect.y + 8))
|
||
cnt = packs.get(pt, 0)
|
||
c = self.small.render(f"x{cnt}", True, HINT)
|
||
self.screen.blit(c, (rect.right - 40, rect.y + 8))
|
||
if pt == "lesser":
|
||
sub = "Any type, worse odds"
|
||
elif pt == "tower":
|
||
sub = "Any type, high rarity"
|
||
elif pt == "general":
|
||
sub = "Any plant type"
|
||
else:
|
||
sub = f"{pt} type boosted"
|
||
s = self.tiny.render(sub, True, MUTED)
|
||
self.screen.blit(s, (rect.x + 8, rect.y + 30))
|
||
info = self.info_rects[pt].move(0, -self.seeds_scroll)
|
||
ihov = info.collidepoint(mouse)
|
||
icol = (115, 135, 115) if ihov else (70, 82, 70)
|
||
pygame.draw.circle(self.screen, icol, info.center, 9)
|
||
pygame.draw.circle(self.screen, (175, 195, 175), info.center, 9, 1)
|
||
itxt = self.tiny.render("i", True, TEXT)
|
||
self.screen.blit(itxt, itxt.get_rect(center=info.center))
|
||
b = btn.move(0, -self.seeds_scroll)
|
||
active = cnt > 0
|
||
pygame.draw.rect(self.screen, BTN_HOVER if active else (45, 50, 45), b, border_radius=6)
|
||
t = self.tiny.render("Open", True, TEXT if active else MUTED)
|
||
self.screen.blit(t, t.get_rect(center=b.center))
|
||
b5 = self.pack5_rects[pt].move(0, -self.seeds_scroll)
|
||
active5 = cnt >= 5
|
||
pygame.draw.rect(self.screen, (100, 100, 60) if active5 else (45, 50, 45), b5, border_radius=6)
|
||
t5 = self.tiny.render("Open x5", True, TEXT if active5 else MUTED)
|
||
self.screen.blit(t5, t5.get_rect(center=b5.center))
|
||
self.screen.set_clip(old_clip)
|
||
if self.seeds_scroll_max > 0:
|
||
hint = ("▲" if self.seeds_scroll > 0 else "") + (
|
||
"▼" if self.seeds_scroll < self.seeds_scroll_max else "")
|
||
if hint:
|
||
ht = self.tiny.render(hint, True, MUTED)
|
||
self.screen.blit(ht, ht.get_rect(center=(WIN_W // 2, WIN_H - 8)))
|
||
# "Last opened" result sits just below the pack grid.
|
||
self._draw_last_open(self.seeds_grid_y0 + self.seeds_grid_h + 22 - self.seeds_scroll)
|
||
if self.history_open:
|
||
self._draw_pack_history()
|
||
|
||
def _draw_last_open(self, y):
|
||
t = self.small.render("Last opened:", True, MUTED)
|
||
self.screen.blit(t, (60, y))
|
||
if self.last_open:
|
||
batch = self.last_open if isinstance(self.last_open, list) else [self.last_open]
|
||
x = 60
|
||
for plant, is_new, bonus in batch:
|
||
spr = self.sprites.get(plant)
|
||
if spr:
|
||
ic = fit_surface(spr, 40)
|
||
self.screen.blit(ic, ic.get_rect(center=(x + 20, y + 28)))
|
||
rcol = RARITY_COLORS.get(plant_rarity(plant), TEXT)
|
||
nm = self.tiny.render(plant, True, rcol)
|
||
self.screen.blit(nm, (x, y + 52))
|
||
if is_new:
|
||
nt = self.tiny.render("NEW", True, HINT)
|
||
self.screen.blit(nt, (x + 44, y + 10))
|
||
x += 132
|
||
total = len(batch)
|
||
n_new = sum(1 for r in batch if r[1])
|
||
if total == 1:
|
||
status = "NEW plant!" if n_new else "Duplicate (+1)"
|
||
else:
|
||
status = f"Opened {total} — {n_new} new, {total - n_new} duplicate(s)"
|
||
col = HINT if n_new else MUTED
|
||
st = self.small.render(status, True, col)
|
||
self.screen.blit(st, (60, y + 90))
|
||
rune_bonus = [r[2] for r in batch if r[2] is not None]
|
||
if rune_bonus:
|
||
rx = 60
|
||
for r in rune_bonus:
|
||
ic = self._rune_icon(r["color"])
|
||
if ic:
|
||
self.screen.blit(ic, (rx, y + 111))
|
||
rn = self.tiny.render(r["name"], True, ACCENT)
|
||
self.screen.blit(rn, (rx + 20, y + 113))
|
||
rx += 20 + rn.get_width() + 12
|
||
|
||
def _draw_pack_history(self):
|
||
overlay = pygame.Surface((WIN_W, WIN_H), pygame.SRCALPHA)
|
||
overlay.fill((0, 0, 0, 160))
|
||
self.screen.blit(overlay, (0, 0))
|
||
w, h = 620, 430
|
||
panel = pygame.Rect((WIN_W - w) // 2, (WIN_H - h) // 2, w, h)
|
||
pygame.draw.rect(self.screen, PANEL, panel, border_radius=12)
|
||
pygame.draw.rect(self.screen, (150, 190, 150), panel, 2, border_radius=12)
|
||
title = self.bold.render("Last 10 packs opened", True, HINT)
|
||
self.screen.blit(title, title.get_rect(center=(panel.centerx, panel.y + 30)))
|
||
history = collection.open_history()
|
||
if not history:
|
||
t = self.small.render("No packs opened yet.", True, MUTED)
|
||
self.screen.blit(t, t.get_rect(center=(panel.centerx, panel.y + 100)))
|
||
else:
|
||
y = panel.y + 64
|
||
for rec in reversed(history[-10:]):
|
||
rect = pygame.Rect(panel.x + 20, y, panel.w - 40, 30)
|
||
pygame.draw.rect(self.screen, (40, 48, 40), rect, border_radius=6)
|
||
rcol = RARITY_COLORS.get(plant_rarity(rec["plant"]), TEXT)
|
||
nm = self.small.render(rec["plant"], True, rcol)
|
||
self.screen.blit(nm, (rect.x + 10, rect.y + 6))
|
||
if rec["is_new"]:
|
||
nt = self.tiny.render("NEW", True, HINT)
|
||
self.screen.blit(nt, (rect.x + 10 + nm.get_width() + 8, rect.y + 8))
|
||
if rec["rune"]:
|
||
rt = self.tiny.render(f"+ {rec['rune']}", True, ACCENT)
|
||
self.screen.blit(rt, rt.get_rect(midright=(rect.right - 10, rect.centery)))
|
||
y += 36
|
||
close = self.tiny.render("Click anywhere to close", True, MUTED)
|
||
self.screen.blit(close, close.get_rect(center=(panel.centerx, panel.bottom - 22)))
|
||
|
||
def _draw_pack_info(self):
|
||
pt = self.info_pack
|
||
overlay = pygame.Surface((WIN_W, WIN_H), pygame.SRCALPHA)
|
||
overlay.fill((0, 0, 0, 160))
|
||
self.screen.blit(overlay, (0, 0))
|
||
w, h = 360, 360
|
||
panel = pygame.Rect((WIN_W - w) // 2, (WIN_H - h) // 2, w, h)
|
||
pygame.draw.rect(self.screen, PANEL, panel, border_radius=12)
|
||
pygame.draw.rect(self.screen, (60, 70, 60), panel, 2, border_radius=12)
|
||
tcol = TYPE_COLORS.get(pt, (150, 150, 150) if pt == "lesser" else TEXT)
|
||
title = self.bold.render(f"{pt.capitalize()} pack odds", True, tcol)
|
||
self.screen.blit(title, title.get_rect(center=(panel.centerx, panel.y + 34)))
|
||
odds = pack_odds(pt)
|
||
y = panel.y + 78
|
||
for rar in ("common", "rare", "super_rare", "super_super_rare", "ultra_rare"):
|
||
pct = odds.get(rar, 0.0) * 100.0
|
||
col = RARITY_COLORS.get(rar, TEXT)
|
||
name = self.small.render(RARITY_LABELS.get(rar, rar), True, col)
|
||
self.screen.blit(name, (panel.x + 40, y))
|
||
bar_w = int(150 * pct / 100.0)
|
||
if bar_w > 0:
|
||
pygame.draw.rect(self.screen, col, (panel.x + 150, y + 5, bar_w, 10), border_radius=3)
|
||
pct_s = self.small.render(f"{pct:.1f}%", True, TEXT)
|
||
self.screen.blit(pct_s, (panel.right - 50, y))
|
||
y += 40
|
||
# runes
|
||
pygame.draw.line(self.screen, (60, 70, 60),
|
||
(panel.x + 30, y + 2), (panel.right - 30, y + 2))
|
||
y += 14
|
||
rn = self.small.render("Runes", True, HINT)
|
||
self.screen.blit(rn, (panel.x + 40, y))
|
||
rpct = runes.PACK_DROP_CHANCE * 100.0
|
||
bar_w = int(150 * rpct / 100.0)
|
||
if bar_w > 0:
|
||
pygame.draw.rect(self.screen, HINT, (panel.x + 150, y + 5, bar_w, 10), border_radius=3)
|
||
rp = self.small.render(f"{rpct:.1f}%", True, TEXT)
|
||
self.screen.blit(rp, (panel.right - 50, y))
|
||
low, high = runes.per_rune_chance(runes.PACK_DROP_CHANCE)
|
||
sub = self.tiny.render(
|
||
f"{rpct:.0f}% for any rune — brown & pearl are rarer than the others",
|
||
True, MUTED)
|
||
self.screen.blit(sub, (panel.x + 150, y + 22))
|
||
if pt in ALL_TYPES:
|
||
note = self.tiny.render(f"Plants are drawn from the {pt} type when possible", True, MUTED)
|
||
self.screen.blit(note, note.get_rect(center=(panel.centerx, panel.bottom - 46)))
|
||
close = self.tiny.render("Click anywhere to close", True, MUTED)
|
||
self.screen.blit(close, close.get_rect(center=(panel.centerx, panel.bottom - 24)))
|
||
|
||
def _draw_shop(self):
|
||
overlay = pygame.Surface((WIN_W, WIN_H), pygame.SRCALPHA)
|
||
overlay.fill((0, 0, 0, 160))
|
||
self.screen.blit(overlay, (0, 0))
|
||
w, h = 640, 400
|
||
panel = pygame.Rect((WIN_W - w) // 2, (WIN_H - h) // 2, w, h)
|
||
pygame.draw.rect(self.screen, PANEL, panel, border_radius=12)
|
||
pygame.draw.rect(self.screen, ACCENT, panel, 2, border_radius=12)
|
||
title = self.bold.render("Pack Shop", True, ACCENT)
|
||
self.screen.blit(title, title.get_rect(center=(panel.centerx, panel.y + 26)))
|
||
coins = collection.available_coins()
|
||
bal = self.small.render(f"Coins: {coins}", True, TEXT)
|
||
total_w = (16 + 4) + bal.get_width()
|
||
bx = panel.centerx - total_w // 2
|
||
if self.coin_icon:
|
||
self.screen.blit(self.coin_icon, (bx, panel.y + 52))
|
||
self.screen.blit(bal, (bx + 20, panel.y + 52))
|
||
buyable = ("lesser", "general", "radiant", "nocturnal", "ice", "garden", "wildflower")
|
||
cols = 4
|
||
cw, chh, gap = 132, 118, 12
|
||
x0 = panel.x + 24
|
||
y0 = panel.y + 96
|
||
self.shop_btns = {}
|
||
mouse = self.mouse_pos
|
||
for i, pt in enumerate(buyable):
|
||
x = x0 + (i % cols) * (cw + gap)
|
||
y = y0 + (i // cols) * (chh + gap)
|
||
rect = pygame.Rect(x, y, cw, chh)
|
||
cost = collection.PACK_COSTS[pt]
|
||
hover = rect.collidepoint(mouse)
|
||
afford = coins >= cost
|
||
pygame.draw.rect(self.screen, (90, 120, 90) if (hover and afford) else (BTN if afford else (45, 50, 45)),
|
||
rect, border_radius=8)
|
||
pygame.draw.rect(self.screen, (150, 190, 150) if afford else (70, 80, 70),
|
||
rect, 1, border_radius=8)
|
||
spr = self._seed_pack_sprite(pt, 48)
|
||
if spr:
|
||
self.screen.blit(spr, spr.get_rect(center=(rect.centerx, rect.y + 30)))
|
||
nm = self.tiny.render(pt.capitalize(), True, TEXT if afford else MUTED)
|
||
self.screen.blit(nm, nm.get_rect(center=(rect.centerx, rect.y + 60)))
|
||
pr = self.tiny.render(f"{cost} coins", True, HINT if afford else MUTED)
|
||
self.screen.blit(pr, pr.get_rect(center=(rect.centerx, rect.y + 86)))
|
||
self.shop_btns[pt] = rect
|
||
note = self.tiny.render("Tower packs can't be bought.", True, MUTED)
|
||
self.screen.blit(note, note.get_rect(center=(panel.centerx, panel.bottom - 18)))
|
||
if self.shop_msg and pygame.time.get_ticks() < self.shop_msg_until:
|
||
mt = self.small.render(self.shop_msg, True, DANGER if "Not enough" in self.shop_msg else (120, 220, 120))
|
||
self.screen.blit(mt, mt.get_rect(center=(panel.centerx, panel.y + 356)))
|
||
|
||
def _draw_soil(self):
|
||
self._draw_page("Soil", "Retrieve seeds from plants, plant them, and grow copies.")
|
||
state = collection.soil_state()
|
||
fertilizer = collection.available_fertilizer()
|
||
cd = int(state["cd_left"])
|
||
if cd > 0:
|
||
status = f"Next seed in {cd}s"
|
||
else:
|
||
status = "Seed ready — open a plant and hit 'Retrieve Seed'"
|
||
seeds_x = 60
|
||
plots_x = 343
|
||
st = self.small.render(status, True, HINT if cd == 0 else MUTED)
|
||
self.screen.blit(st, (seeds_x, 120))
|
||
wt = self.small.render(f"Fertilizer: {fertilizer}/{collection.FERTILIZER_CAP}", True, TEXT)
|
||
self.screen.blit(wt, (WIN_W - 180, 120))
|
||
if self.fert_icon:
|
||
self.screen.blit(self.fert_icon, (WIN_W - 180 + wt.get_width() + 4, 122))
|
||
self.soil_fert_info_rect = pygame.Rect(WIN_W - 40, 116, 22, 22)
|
||
ihov = self.soil_fert_info_rect.collidepoint(self.mouse_pos)
|
||
pygame.draw.circle(self.screen, (115, 135, 115) if ihov else (70, 82, 70),
|
||
self.soil_fert_info_rect.center, 11)
|
||
pygame.draw.circle(self.screen, (175, 195, 175), self.soil_fert_info_rect.center, 11, 1)
|
||
itxt = self.tiny.render("i", True, TEXT)
|
||
self.screen.blit(itxt, itxt.get_rect(center=self.soil_fert_info_rect.center))
|
||
af_owned = collection.auto_farmer_owned()
|
||
af_active = collection.auto_farmer_active()
|
||
self.auto_farmer_rect = pygame.Rect(WIN_W - 270, 148, 250, 28)
|
||
af_hover = self.auto_farmer_rect.collidepoint(self.mouse_pos)
|
||
pygame.draw.rect(self.screen,
|
||
(90, 120, 90) if (af_active or af_hover) else BTN,
|
||
self.auto_farmer_rect, border_radius=6)
|
||
pygame.draw.rect(self.screen,
|
||
(150, 190, 150) if af_active else (70, 80, 70),
|
||
self.auto_farmer_rect, 1, border_radius=6)
|
||
if af_owned:
|
||
af_txt = self.tiny.render(
|
||
"Auto Farmer: active" if af_active else "Auto Farmer: off",
|
||
True, ACCENT if af_active else MUTED)
|
||
else:
|
||
can_afford = collection.available_coins() >= collection.AUTO_FARMER_COST
|
||
af_txt = self.tiny.render(
|
||
f"Buy Auto Farmer — {collection.AUTO_FARMER_COST} coins",
|
||
True, TEXT if can_afford else MUTED)
|
||
self.screen.blit(af_txt, af_txt.get_rect(center=self.auto_farmer_rect.center))
|
||
ap_owned = collection.auto_planter_owned()
|
||
ap_active = collection.auto_planter_active()
|
||
self.auto_planter_rect = pygame.Rect(WIN_W - 270, 180, 250, 28)
|
||
ap_hover = self.auto_planter_rect.collidepoint(self.mouse_pos)
|
||
pygame.draw.rect(self.screen,
|
||
(90, 120, 90) if (ap_active or ap_hover) else BTN,
|
||
self.auto_planter_rect, border_radius=6)
|
||
pygame.draw.rect(self.screen,
|
||
(150, 190, 150) if ap_active else (70, 80, 70),
|
||
self.auto_planter_rect, 1, border_radius=6)
|
||
if ap_owned:
|
||
ap_txt = self.tiny.render(
|
||
"Auto Planter: active" if ap_active else "Auto Planter: off",
|
||
True, ACCENT if ap_active else MUTED)
|
||
else:
|
||
can_afford = collection.available_coins() >= collection.AUTO_PLANTER_COST
|
||
ap_txt = self.tiny.render(
|
||
f"Buy Auto Planter — {collection.AUTO_PLANTER_COST} coins",
|
||
True, TEXT if can_afford else MUTED)
|
||
self.screen.blit(ap_txt, ap_txt.get_rect(center=self.auto_planter_rect.center))
|
||
if self.soil_notice and pygame.time.get_ticks() < self.soil_notice_until:
|
||
nt = self.bold.render(self.soil_notice, True, (235, 130, 130))
|
||
self.screen.blit(nt, nt.get_rect(center=(WIN_W // 2, 148)))
|
||
|
||
# --- retrieved seeds (unplanted) ---
|
||
t = self.bold.render("Seeds retrieved", True, HINT)
|
||
self.screen.blit(t, (seeds_x, 160))
|
||
self.soil_seed_rects = []
|
||
y = 190
|
||
if not state["seeds"]:
|
||
self.screen.blit(self.small.render("No seeds yet.", True, MUTED), (seeds_x + 10, y))
|
||
for idx, s in enumerate(state["seeds"]):
|
||
rect = pygame.Rect(seeds_x, y, 220, 34)
|
||
pygame.draw.rect(self.screen, (42, 50, 42), rect, border_radius=6)
|
||
pygame.draw.rect(self.screen, (90, 110, 90), rect, 1, border_radius=6)
|
||
rar = plant_rarity(s["plant"])
|
||
self.screen.blit(self.small.render(s["plant"], True, RARITY_COLORS.get(rar, TEXT)),
|
||
(rect.x + 8, rect.y + 7))
|
||
btn = pygame.Rect(rect.right - 90, rect.y + 4, 82, 26)
|
||
pygame.draw.rect(self.screen, BTN_HOVER, btn, border_radius=6)
|
||
self.screen.blit(self.tiny.render("Plant", True, TEXT),
|
||
self.tiny.render("Plant", True, TEXT).get_rect(center=btn.center))
|
||
self.soil_seed_rects.append((idx, btn))
|
||
y += 40
|
||
|
||
# --- plots ---
|
||
self._draw_soil_plots(state, fertilizer)
|
||
|
||
def _plot_sprite_name(self, p):
|
||
if p["grown"]:
|
||
return "soil_ready"
|
||
if p["stage"] == 0:
|
||
return "soil_seed1"
|
||
if p["stage"] == 1:
|
||
return "soil_seed2"
|
||
return "soil_growth"
|
||
|
||
def _draw_soil_plots(self, state, fertilizer):
|
||
plots_x = (WIN_W - (2 * 170 + 14)) // 2
|
||
t = self.bold.render(f"Growing ({state['plots_unlocked']}/{state['plots_max']} plots)",
|
||
True, HINT)
|
||
self.screen.blit(t, (plots_x, 160))
|
||
self.soil_water_rects = []
|
||
self.soil_harvest_rects = []
|
||
self.soil_unlock_rects = []
|
||
tile_w, tile_h, gap = 170, 180, 14
|
||
cols = 2
|
||
y0 = 190
|
||
unlocked = state["plots_unlocked"]
|
||
for slot in range(state["plots_max"]):
|
||
tx = plots_x + (slot % cols) * (tile_w + gap)
|
||
ty = y0 + (slot // cols) * (tile_h + gap)
|
||
rect = pygame.Rect(tx, ty, tile_w, tile_h)
|
||
if slot < len(state["plots"]):
|
||
self._draw_plot_tile(rect, state["plots"][slot], fertilizer, slot)
|
||
elif slot < unlocked:
|
||
pygame.draw.rect(self.screen, (30, 36, 30), rect, border_radius=10)
|
||
pygame.draw.rect(self.screen, (70, 80, 70), rect, 1, border_radius=10)
|
||
ic = load_plot_icon("soil_barren", 96)
|
||
if ic:
|
||
self.screen.blit(ic, ic.get_rect(center=(rect.centerx, rect.y + 60)))
|
||
self.screen.blit(self.small.render("Empty plot", True, MUTED),
|
||
(rect.x + 8, rect.y + 132))
|
||
else:
|
||
pygame.draw.rect(self.screen, (24, 28, 24), rect, border_radius=10)
|
||
pygame.draw.rect(self.screen, (50, 55, 50), rect, 1, border_radius=10)
|
||
ic = load_plot_icon("soil_barren", 96)
|
||
if ic:
|
||
ipos = ic.get_rect(center=(rect.centerx, rect.y + 48))
|
||
dim = ic.copy()
|
||
dim.set_alpha(70)
|
||
self.screen.blit(dim, ipos)
|
||
self.screen.blit(self.small.render("Locked plot", True, (100, 105, 100)),
|
||
(rect.x + 8, rect.y + 102))
|
||
next_buy = slot == unlocked
|
||
c_coins, c_fert = collection.plot_unlock_cost(slot + 1)
|
||
can = next_buy and collection.available_coins() >= c_coins and fertilizer >= c_fert
|
||
cx = rect.x + 8
|
||
if self.coin_icon:
|
||
self.screen.blit(self.coin_icon, (cx, rect.y + 131))
|
||
cx += 18
|
||
cxt = self.tiny.render(f"{c_coins}", True, HINT if can else (110, 115, 110))
|
||
self.screen.blit(cxt, (cx, rect.y + 130))
|
||
cx += cxt.get_width() + 10
|
||
if self.fert_icon:
|
||
self.screen.blit(self.fert_icon, (cx, rect.y + 131))
|
||
cx += 18
|
||
fxt = self.tiny.render(f"{c_fert}", True, HINT if can else (110, 115, 110))
|
||
self.screen.blit(fxt, (cx, rect.y + 130))
|
||
ubtn = pygame.Rect(rect.centerx - 40, rect.bottom - 30, 80, 24)
|
||
pygame.draw.rect(self.screen, BTN_HOVER if can else (45, 50, 45),
|
||
ubtn, border_radius=6)
|
||
self.screen.blit(self.tiny.render("Unlock", True, TEXT if can else MUTED),
|
||
self.tiny.render("Unlock", True, TEXT if can else MUTED).get_rect(center=ubtn.center))
|
||
if next_buy:
|
||
self.soil_unlock_rects.append((slot, ubtn))
|
||
|
||
def _draw_plot_tile(self, rect, p, fertilizer, slot):
|
||
pygame.draw.rect(self.screen, (34, 42, 34), rect, border_radius=10)
|
||
pygame.draw.rect(self.screen, (90, 110, 90), rect, 1, border_radius=10)
|
||
rar = plant_rarity(p["plant"])
|
||
ic = load_plot_icon(self._plot_sprite_name(p), 96)
|
||
if ic:
|
||
self.screen.blit(ic, ic.get_rect(center=(rect.centerx, rect.y + 60)))
|
||
self.screen.blit(self.small.render(p["plant"], True, RARITY_COLORS.get(rar, TEXT)),
|
||
(rect.x + 8, rect.y + 114))
|
||
if p["grown"]:
|
||
self.screen.blit(self.tiny.render("GROWN!", True, (120, 220, 120)),
|
||
(rect.x + 8, rect.y + 128))
|
||
else:
|
||
self.screen.blit(self.tiny.render(f"stage {p['stage'] + 1}/{p['total_stages']}"
|
||
f" · fertilized {p['water']}x", True, MUTED),
|
||
(rect.x + 8, rect.y + 128))
|
||
if p["grown"]:
|
||
hbtn = pygame.Rect(rect.centerx - 40, rect.bottom - 34, 80, 26)
|
||
pygame.draw.rect(self.screen, (90, 140, 90), hbtn, border_radius=6)
|
||
self.screen.blit(self.tiny.render("Harvest", True, TEXT),
|
||
self.tiny.render("Harvest", True, TEXT).get_rect(center=hbtn.center))
|
||
self.soil_harvest_rects.append((slot, hbtn))
|
||
elif p["stalled"]:
|
||
wbtn = pygame.Rect(rect.centerx - 40, rect.bottom - 34, 80, 26)
|
||
pygame.draw.rect(self.screen, BTN_HOVER if fertilizer > 0 else (45, 50, 45),
|
||
wbtn, border_radius=6)
|
||
self.screen.blit(self.tiny.render("Fertilize", True, TEXT if fertilizer > 0 else MUTED),
|
||
self.tiny.render("Fertilize", True, TEXT if fertilizer > 0 else MUTED).get_rect(center=wbtn.center))
|
||
self.soil_water_rects.append((slot, wbtn))
|
||
else:
|
||
secs = int(p["seconds_left"])
|
||
st = self.tiny.render(f"{secs}s", True, MUTED)
|
||
self.screen.blit(st, st.get_rect(center=(rect.centerx, rect.bottom - 20)))
|
||
|
||
def _draw_fert_info(self):
|
||
overlay = pygame.Surface((WIN_W, WIN_H), pygame.SRCALPHA)
|
||
overlay.fill((0, 0, 0, 150))
|
||
self.screen.blit(overlay, (0, 0))
|
||
w, h = 400, 190
|
||
panel = pygame.Rect((WIN_W - w) // 2, (WIN_H - h) // 2, w, h)
|
||
pygame.draw.rect(self.screen, PANEL, panel, border_radius=12)
|
||
pygame.draw.rect(self.screen, (150, 190, 150), panel, 2, border_radius=12)
|
||
title = self.bold.render("Fertilizer", True, HINT)
|
||
self.screen.blit(title, title.get_rect(center=(panel.centerx, panel.y + 30)))
|
||
lines = [
|
||
"Used to advance growing Soil plots by one stage.",
|
||
"",
|
||
"Earned by winning battles — higher floor = more",
|
||
"(floor 1 gives 1, floor 5 gives 5).",
|
||
]
|
||
y = panel.y + 66
|
||
for ln in lines:
|
||
if not ln:
|
||
y += 10
|
||
continue
|
||
t = self.small.render(ln, True, TEXT)
|
||
self.screen.blit(t, t.get_rect(center=(panel.centerx, y)))
|
||
y += 24
|
||
close = self.tiny.render("Click anywhere to close", True, MUTED)
|
||
self.screen.blit(close, close.get_rect(center=(panel.centerx, panel.bottom - 20)))
|
||
|
||
def _draw_water_info(self):
|
||
overlay = pygame.Surface((WIN_W, WIN_H), pygame.SRCALPHA)
|
||
overlay.fill((0, 0, 0, 150))
|
||
self.screen.blit(overlay, (0, 0))
|
||
w, h = 420, 240
|
||
panel = pygame.Rect((WIN_W - w) // 2, (WIN_H - h) // 2, w, h)
|
||
pygame.draw.rect(self.screen, PANEL, panel, border_radius=12)
|
||
pygame.draw.rect(self.screen, (150, 190, 150), panel, 2, border_radius=12)
|
||
title = self.bold.render("Water", True, HINT)
|
||
self.screen.blit(title, title.get_rect(center=(panel.centerx, panel.y + 30)))
|
||
lines = [
|
||
"The scarce, valuable currency.",
|
||
"",
|
||
"Spent to level up plants (+1 level per water)",
|
||
"and to buy seed packs (5 water each).",
|
||
"",
|
||
"Earned from Tower rewards — stage 1-5 wins pay",
|
||
"1 water, and Tower packs redeem for 10.",
|
||
]
|
||
y = panel.y + 62
|
||
for ln in lines:
|
||
if not ln:
|
||
y += 10
|
||
continue
|
||
t = self.small.render(ln, True, TEXT)
|
||
self.screen.blit(t, t.get_rect(center=(panel.centerx, y)))
|
||
y += 22
|
||
close = self.tiny.render("Click anywhere to close", True, MUTED)
|
||
self.screen.blit(close, close.get_rect(center=(panel.centerx, panel.bottom - 18)))
|
||
|
||
def _draw_soil_msg(self):
|
||
msg = self.soil_msg
|
||
if not msg:
|
||
return
|
||
overlay = pygame.Surface((WIN_W, WIN_H), pygame.SRCALPHA)
|
||
overlay.fill((0, 0, 0, 150))
|
||
self.screen.blit(overlay, (0, 0))
|
||
w, h = 360, 200
|
||
panel = pygame.Rect((WIN_W - w) // 2, (WIN_H - h) // 2, w, h)
|
||
pygame.draw.rect(self.screen, PANEL, panel, border_radius=12)
|
||
pygame.draw.rect(self.screen, (150, 190, 150), panel, 2, border_radius=12)
|
||
title = self.bold.render("Harvest!", True, (120, 220, 120))
|
||
self.screen.blit(title, title.get_rect(center=(panel.centerx, panel.y + 30)))
|
||
plant = msg["plant"]
|
||
rar = plant_rarity(plant)
|
||
spr = self.sprites.get(plant)
|
||
if spr:
|
||
ic = fit_surface(spr, 64)
|
||
self.screen.blit(ic, ic.get_rect(center=(panel.centerx, panel.y + 84)))
|
||
name = self.bold.render(plant, True, RARITY_COLORS.get(rar, TEXT))
|
||
self.screen.blit(name, name.get_rect(center=(panel.centerx, panel.y + 124)))
|
||
dup = self.small.render(f"You gained a copy — {msg['duplicates']} duplicates", True, TEXT)
|
||
self.screen.blit(dup, dup.get_rect(center=(panel.centerx, panel.y + 152)))
|
||
hint = self.tiny.render("Click anywhere to close", True, MUTED)
|
||
self.screen.blit(hint, hint.get_rect(center=(panel.centerx, panel.bottom - 18)))
|
||
|
||
def _draw_plants(self):
|
||
data = collection.load()
|
||
if self.water_mode:
|
||
self._draw_page("Plants", "Water mode — gold ring = ready to evolve once leveled")
|
||
else:
|
||
self._draw_page("Plants", "Plants you own")
|
||
water = data.get("water", 0)
|
||
wm_hover = self.water_mode_btn.collidepoint(self.mouse_pos)
|
||
pygame.draw.rect(self.screen, (90, 120, 90) if wm_hover or self.water_mode else BTN,
|
||
self.water_mode_btn, border_radius=6)
|
||
pygame.draw.rect(self.screen, (150, 190, 150) if self.water_mode else (70, 80, 70),
|
||
self.water_mode_btn, 1, border_radius=6)
|
||
wtxt = " (exit)" if self.water_mode else ""
|
||
wv = self.tiny.render(f"{water}{wtxt}", True, TEXT)
|
||
total_w = (16 + 2) + wv.get_width()
|
||
x = self.water_mode_btn.centerx - total_w // 2
|
||
if self.water_icon:
|
||
self.screen.blit(self.water_icon, (x, self.water_mode_btn.centery - 8))
|
||
x += 18
|
||
self.screen.blit(wv, (x, self.water_mode_btn.centery - wv.get_height() // 2))
|
||
# water info circle
|
||
self.water_info_rect = pygame.Rect(self.water_mode_btn.right - 26,
|
||
self.water_mode_btn.centery - 11, 22, 22)
|
||
ihov = self.water_info_rect.collidepoint(self.mouse_pos)
|
||
pygame.draw.circle(self.screen, (115, 135, 115) if ihov else (70, 82, 70),
|
||
self.water_info_rect.center, 11)
|
||
pygame.draw.circle(self.screen, (175, 195, 175), self.water_info_rect.center, 11, 1)
|
||
itxt = self.tiny.render("i", True, TEXT)
|
||
self.screen.blit(itxt, itxt.get_rect(center=self.water_info_rect.center))
|
||
unlocked = data.get("tower_best", 0) >= collection.EXTRACT_GATE
|
||
ab_hover = self.abilities_btn.collidepoint(self.mouse_pos)
|
||
pygame.draw.rect(self.screen, (90, 120, 90) if (ab_hover and unlocked) else (BTN if unlocked else (45, 50, 45)),
|
||
self.abilities_btn, border_radius=6)
|
||
pygame.draw.rect(self.screen, (150, 190, 150) if unlocked else (70, 80, 70),
|
||
self.abilities_btn, 1, border_radius=6)
|
||
alabel = "Abilities" if unlocked else f"Abilities (Tower {collection.EXTRACT_GATE})"
|
||
t = self.tiny.render(alabel, True, TEXT if unlocked else MUTED)
|
||
self.screen.blit(t, t.get_rect(center=self.abilities_btn.center))
|
||
if pygame.time.get_ticks() < self.abilities_msg_until:
|
||
mt = self.tiny.render(self.abilities_msg, True, HINT)
|
||
self.screen.blit(mt, (self.abilities_btn.x, self.abilities_btn.bottom + 4))
|
||
owned = self._sort_plants([n for n in self.names if n in self.owned])
|
||
if not owned:
|
||
t = self.small.render("You don't own any plants yet — battle to discover them.", True, MUTED)
|
||
self.screen.blit(t, t.get_rect(center=(WIN_W // 2, 200)))
|
||
return
|
||
mouse = self.mouse_pos
|
||
labels = {"rarity": "Rarity", "type": "Type", "role": "Role",
|
||
"level": "Level", "stage": "Stage"}
|
||
for key, rect in self.plants_sort_btns.items():
|
||
active = self.plants_sort == key
|
||
hover = rect.collidepoint(mouse)
|
||
pygame.draw.rect(self.screen, (90, 120, 90) if hover or active else BTN,
|
||
rect, border_radius=6)
|
||
pygame.draw.rect(self.screen, (150, 190, 150) if active else (70, 80, 70),
|
||
rect, 1, border_radius=6)
|
||
t = self.tiny.render(labels[key], True, TEXT)
|
||
self.screen.blit(t, t.get_rect(center=rect.center))
|
||
self._draw_grid(owned, reveal_all=True, level_mode=self.water_mode, data=data)
|
||
if self.water_mode:
|
||
water = collection.available_water()
|
||
pygame.draw.rect(self.screen, BTN_HOVER, self.water_exit_btn, border_radius=6)
|
||
pygame.draw.rect(self.screen, (150, 190, 150), self.water_exit_btn, 2, border_radius=6)
|
||
et = self.tiny.render("Exit water mode", True, TEXT)
|
||
self.screen.blit(et, et.get_rect(center=self.water_exit_btn.center))
|
||
|
||
def _sort_collection(self, names):
|
||
s = self.coll_sort
|
||
if s == "rarity":
|
||
order = {"common": 0, "rare": 1, "super_rare": 2, "super_super_rare": 3, "ultra_rare": 4}
|
||
return sorted(names, key=lambda n: (order.get(plant_rarity(n), 0), n))
|
||
if s == "type":
|
||
return sorted(names, key=lambda n: (POOL[n]["type"], n))
|
||
if s == "role":
|
||
order = {"attacker": 0, "support": 1, "tank": 2}
|
||
return sorted(names, key=lambda n: (order.get(POOL[n]["role"], 3), n))
|
||
if s == "found":
|
||
return sorted(names, key=lambda n: (n not in self.owned, n))
|
||
return list(names)
|
||
|
||
def _sort_plants(self, names):
|
||
s = self.plants_sort
|
||
if s == "rarity":
|
||
order = {"common": 0, "rare": 1, "super_rare": 2, "super_super_rare": 3, "ultra_rare": 4}
|
||
return sorted(names, key=lambda n: (order.get(plant_rarity(n), 0), n))
|
||
if s == "type":
|
||
return sorted(names, key=lambda n: (POOL[n]["type"], n))
|
||
if s == "role":
|
||
order = {"attacker": 0, "support": 1, "tank": 2}
|
||
return sorted(names, key=lambda n: (order.get(POOL[n]["role"], 3), n))
|
||
if s == "level":
|
||
return sorted(names, key=lambda n: (-collection.get_progress(n)["level"], n))
|
||
if s == "stage":
|
||
return sorted(names, key=lambda n: (-collection.get_progress(n)["stage"],
|
||
-collection.get_progress(n)["duplicates"],
|
||
-collection.get_progress(n)["level"], n))
|
||
return sorted(names, key=lambda n: (n not in self.favs, n))
|
||
|
||
def _draw_runes(self):
|
||
self._draw_page("Runes", "Every rune you own — equipped runes show their plant")
|
||
dhov = self.runes_deconstruct_rect.collidepoint(self.mouse_pos)
|
||
pygame.draw.rect(self.screen,
|
||
(90, 120, 90) if (self.runes_deconstruct or dhov) else BTN,
|
||
self.runes_deconstruct_rect, border_radius=6)
|
||
dlbl = "Cancel" if self.runes_deconstruct else "Deconstruct"
|
||
dt = self.tiny.render(dlbl, True, TEXT)
|
||
self.screen.blit(dt, dt.get_rect(center=self.runes_deconstruct_rect.center))
|
||
ihov = self.runes_info_rect.collidepoint(self.mouse_pos)
|
||
pygame.draw.rect(self.screen, (90, 120, 90) if (self.runes_info or ihov) else BTN,
|
||
self.runes_info_rect, border_radius=6)
|
||
il = self.tiny.render("Rune info", True, TEXT)
|
||
self.screen.blit(il, il.get_rect(center=self.runes_info_rect.center))
|
||
|
||
# rune shop button
|
||
shov = self.runes_shop_rect.collidepoint(self.mouse_pos)
|
||
pygame.draw.rect(self.screen, (90, 120, 90) if shov else BTN,
|
||
self.runes_shop_rect, border_radius=6)
|
||
pygame.draw.rect(self.screen, (150, 190, 150), self.runes_shop_rect, 1, border_radius=6)
|
||
sh = self.tiny.render("Rune shop", True, TEXT)
|
||
self.screen.blit(sh, sh.get_rect(center=self.runes_shop_rect.center))
|
||
|
||
inventory = collection.rune_inventory()
|
||
self._draw_runes_filters()
|
||
if not inventory:
|
||
t = self.small.render(
|
||
"No runes yet — win floor battles or open seed packs to find some.",
|
||
True, MUTED)
|
||
self.screen.blit(t, t.get_rect(center=(WIN_W // 2, 200)))
|
||
return
|
||
display = self._runes_displayed(inventory)
|
||
top = 140
|
||
if self.runes_deconstruct:
|
||
bcol = DANGER if self.runes_deconstruct_msg else ACCENT
|
||
if self.runes_target is None:
|
||
btxt = self.runes_deconstruct_msg or \
|
||
f"Deconstruct — pick a rune to improve, then sacrifice " \
|
||
f"{runes.RUNE_SACRIFICE_COST} runes"
|
||
else:
|
||
btxt = self.runes_deconstruct_msg or \
|
||
f"Pick {runes.RUNE_SACRIFICE_COST} runes to sacrifice " \
|
||
f"({len(self.runes_sacrifice)}/{runes.RUNE_SACRIFICE_COST}) — " \
|
||
"click the target again to change it"
|
||
b = self.tiny.render(btxt, True, bcol)
|
||
self.screen.blit(b, b.get_rect(center=(WIN_W // 2, 124)))
|
||
elif self.runes_buy_msg and pygame.time.get_ticks() < self.runes_buy_msg_until:
|
||
b = self.tiny.render(self.runes_buy_msg, True, DANGER)
|
||
self.screen.blit(b, b.get_rect(center=(WIN_W // 2, 124)))
|
||
cols, cw, ch, gap = 6, 132, 122, 10
|
||
width = min(cols, len(display)) if display else 1
|
||
x0 = (WIN_W - (width * cw + (width - 1) * gap)) // 2
|
||
rows = (len(display) + width - 1) // width if display else 0
|
||
bottom = top + rows * (ch + gap) - gap
|
||
self.grid_scroll = max(0, min(self.grid_scroll, max(0, bottom - (WIN_H - 62))))
|
||
self.grid_scroll_max = max(0, bottom - (WIN_H - 62))
|
||
scroll = self.grid_scroll
|
||
old_clip = self.screen.get_clip()
|
||
self.screen.set_clip(pygame.Rect(0, 138, WIN_W, WIN_H - 138 - 44))
|
||
self.rune_card_rects = []
|
||
if not display:
|
||
t = self.small.render("No runes match the current filter.", True, MUTED)
|
||
self.screen.blit(t, t.get_rect(center=(WIN_W // 2, top + 40)))
|
||
for i, rune in enumerate(display):
|
||
x = x0 + (i % width) * (cw + gap)
|
||
y = top + (i // width) * (ch + gap) - scroll
|
||
rect = pygame.Rect(x, y, cw, ch)
|
||
self.rune_card_rects.append((rune["id"], rect))
|
||
pygame.draw.rect(self.screen, PANEL, rect, border_radius=8)
|
||
pygame.draw.rect(self.screen, (60, 70, 60), rect, 1, border_radius=8)
|
||
ic = self._rune_icon(rune["color"], 40)
|
||
if ic:
|
||
self.screen.blit(ic, ic.get_rect(center=(rect.centerx, rect.y + 30)))
|
||
owner = collection.rune_location(rune["id"])
|
||
max_pct = runes.max_pct(rune["color"])
|
||
is_max = rune.get("pct", 0) >= max_pct
|
||
is_target = self.runes_deconstruct and self.runes_target \
|
||
and rune["id"] == self.runes_target["id"]
|
||
is_sac = self.runes_deconstruct and self.runes_target \
|
||
and rune["id"] in self.runes_sacrifice
|
||
if is_target:
|
||
pygame.draw.rect(self.screen, HINT, rect, 3, border_radius=8)
|
||
elif is_sac:
|
||
pygame.draw.rect(self.screen, ACCENT, rect, 3, border_radius=8)
|
||
name_col = MUTED if (self.runes_deconstruct and self.runes_target is None
|
||
and is_max) else TEXT
|
||
nm = self.tiny.render(rune["name"], True, name_col)
|
||
self.screen.blit(nm, nm.get_rect(center=(rect.centerx, rect.y + 62)))
|
||
eff = self.tiny.render(runes.card_text(rune["color"], rune.get("pct", 30)),
|
||
True, MUTED)
|
||
self.screen.blit(eff, eff.get_rect(center=(rect.centerx, rect.y + 78)))
|
||
if is_target:
|
||
st_text = "Upgrade target"
|
||
elif is_sac:
|
||
st_text = f"Sacrifice #{self.runes_sacrifice.index(rune['id']) + 1}"
|
||
elif self.runes_deconstruct and self.runes_target is None and is_max:
|
||
st_text = "MAX"
|
||
elif owner:
|
||
st_text = f"Equipped in: {owner}"
|
||
else:
|
||
st_text = "Free"
|
||
st = self.tiny.render(self._fit_text(st_text, self.tiny, cw - 8),
|
||
True, HINT if owner and not (is_target or is_sac)
|
||
else (ACCENT if is_sac else
|
||
(HINT if is_target else MUTED)))
|
||
self.screen.blit(st, st.get_rect(center=(rect.centerx, rect.y + 96)))
|
||
self.screen.set_clip(old_clip)
|
||
if self.grid_scroll_max > 0:
|
||
hint = "▲" if scroll > 0 else ""
|
||
if scroll < self.grid_scroll_max:
|
||
hint += "▼"
|
||
if hint:
|
||
ht = self.tiny.render(hint, True, MUTED)
|
||
self.screen.blit(ht, ht.get_rect(center=(WIN_W // 2, WIN_H - 44)))
|
||
if self.runes_info:
|
||
self._draw_runes_info()
|
||
if self.runes_confirm is not None:
|
||
self._draw_runes_confirm()
|
||
if self.runes_result is not None:
|
||
self._draw_runes_result()
|
||
if self.runes_shop:
|
||
self._draw_runes_shop()
|
||
if self.runes_buy_popup is not None:
|
||
self._draw_runes_bought()
|
||
|
||
def _draw_runes_shop(self):
|
||
overlay = pygame.Surface((WIN_W, WIN_H), pygame.SRCALPHA)
|
||
overlay.fill((0, 0, 0, 150))
|
||
self.screen.blit(overlay, (0, 0))
|
||
w, h = 560, 380
|
||
panel = pygame.Rect((WIN_W - w) // 2, (WIN_H - h) // 2, w, h)
|
||
pygame.draw.rect(self.screen, PANEL, panel, border_radius=12)
|
||
pygame.draw.rect(self.screen, ACCENT, panel, 2, border_radius=12)
|
||
title = self.bold.render("Rune Shop", True, ACCENT)
|
||
self.screen.blit(title, title.get_rect(center=(panel.centerx, panel.y + 26)))
|
||
coins = collection.available_coins()
|
||
bal = self.small.render(f"{coins} / {collection.COIN_CAP}", True, TEXT)
|
||
total_w = (16 + 4) + bal.get_width()
|
||
bx = panel.centerx - total_w // 2
|
||
if self.coin_icon:
|
||
self.screen.blit(self.coin_icon, (bx, panel.y + 44))
|
||
self.screen.blit(bal, (bx + 20, panel.y + 44))
|
||
if self.runes_buy_msg and pygame.time.get_ticks() < self.runes_buy_msg_until:
|
||
mt = self.tiny.render(self.runes_buy_msg, True, DANGER)
|
||
self.screen.blit(mt, mt.get_rect(center=(panel.centerx, panel.y + 66)))
|
||
cycle = pygame.time.get_ticks() // 1600
|
||
t1_color = runes.TIER1_COLORS[cycle % len(runes.TIER1_COLORS)]
|
||
t2_color = runes.TIER2_COLORS[cycle % len(runes.TIER2_COLORS)]
|
||
self._draw_shop_option(panel, 0, 1, t1_color, runes.T1_RUNE_COST)
|
||
self._draw_shop_option(panel, 1, 2, t2_color, runes.T2_RUNE_COST)
|
||
hint = self.tiny.render("Click outside to close", True, MUTED)
|
||
self.screen.blit(hint, hint.get_rect(center=(panel.centerx, panel.bottom - 14)))
|
||
|
||
def _draw_shop_option(self, panel, index, tier, color, cost):
|
||
card_w = (panel.w - 96) // 2
|
||
card = pygame.Rect(panel.x + 36 + index * (card_w + 24), panel.y + 78,
|
||
card_w, 262)
|
||
pygame.draw.rect(self.screen, (24, 30, 24), card, border_radius=10)
|
||
pygame.draw.rect(self.screen, (70, 80, 70), card, 1, border_radius=10)
|
||
tl = self.bold.render(f"Tier {tier}", True, ACCENT)
|
||
self.screen.blit(tl, tl.get_rect(center=(card.centerx, card.y + 20)))
|
||
colors = runes.TIER1_COLORS if tier == 1 else runes.TIER2_COLORS
|
||
total = len(colors) * 14 + (len(colors) - 1) * 4
|
||
dx = card.centerx - total // 2
|
||
for c in colors:
|
||
sw = self._rune_icon(c, 14)
|
||
if sw:
|
||
self.screen.blit(sw, (dx, card.y + 36))
|
||
dx += 18
|
||
ic = self._rune_icon(color, 72)
|
||
if ic:
|
||
self.screen.blit(ic, ic.get_rect(center=(card.centerx, card.y + 90)))
|
||
nm = self.small.render(runes.name(color), True, TEXT)
|
||
self.screen.blit(nm, nm.get_rect(center=(card.centerx, card.y + 132)))
|
||
sm = self.tiny.render(runes.summary(color), True, MUTED)
|
||
self.screen.blit(sm, sm.get_rect(center=(card.centerx, card.y + 154)))
|
||
rng = self.tiny.render(f"rolls {runes.roll_range(color)}", True, MUTED)
|
||
self.screen.blit(rng, rng.get_rect(center=(card.centerx, card.y + 172)))
|
||
afford = collection.available_coins() >= cost
|
||
btn = pygame.Rect(card.x + 16, card.bottom - 48, card.w - 32, 32)
|
||
hover = btn.collidepoint(self.mouse_pos)
|
||
pygame.draw.rect(self.screen,
|
||
(90, 120, 90) if (hover and afford) else (BTN if afford else (45, 50, 45)),
|
||
btn, border_radius=6)
|
||
pygame.draw.rect(self.screen, (150, 190, 150) if afford else (70, 80, 70),
|
||
btn, 1, border_radius=6)
|
||
lbl = self.tiny.render(f"Buy — {cost}", True, TEXT if afford else MUTED)
|
||
total_w = (16 + 4) + lbl.get_width()
|
||
bx = btn.centerx - total_w // 2
|
||
if self.coin_icon:
|
||
self.screen.blit(self.coin_icon, (bx, btn.centery - 8))
|
||
self.screen.blit(lbl, (bx + 20, btn.centery - lbl.get_height() // 2))
|
||
if tier == 1:
|
||
self.runes_shop_buy_t1_rect = btn
|
||
else:
|
||
self.runes_shop_buy_t2_rect = btn
|
||
|
||
def _draw_runes_bought(self):
|
||
overlay = pygame.Surface((WIN_W, WIN_H), pygame.SRCALPHA)
|
||
overlay.fill((0, 0, 0, 150))
|
||
self.screen.blit(overlay, (0, 0))
|
||
w, h = 400, 190
|
||
panel = pygame.Rect((WIN_W - w) // 2, (WIN_H - h) // 2, w, h)
|
||
pygame.draw.rect(self.screen, PANEL, panel, border_radius=12)
|
||
pygame.draw.rect(self.screen, ACCENT, panel, 2, border_radius=12)
|
||
title = self.bold.render("Rune purchased!", True, ACCENT)
|
||
self.screen.blit(title, title.get_rect(center=(panel.centerx, panel.y + 30)))
|
||
rune = self.runes_buy_popup
|
||
ic = self._rune_icon(rune["color"], 40)
|
||
if ic:
|
||
self.screen.blit(ic, ic.get_rect(center=(panel.centerx, panel.y + 74)))
|
||
line = self.small.render(
|
||
f"{rune['name']} · {runes.card_text(rune['color'], rune['pct'])}", True, TEXT)
|
||
self.screen.blit(line, line.get_rect(center=(panel.centerx, panel.y + 126)))
|
||
close = self.tiny.render("Click anywhere to close", True, MUTED)
|
||
self.screen.blit(close, close.get_rect(center=(panel.centerx, panel.bottom - 22)))
|
||
|
||
def _runes_displayed(self, inventory):
|
||
shown = [r for r in inventory
|
||
if (self.runes_filter_tier == 0 or runes.tier(r["color"]) == self.runes_filter_tier)
|
||
and (self.runes_filter_color is None or r["color"] == self.runes_filter_color)]
|
||
if self.runes_sort_best:
|
||
shown.sort(key=runes.roll_ratio, reverse=True)
|
||
return shown
|
||
|
||
def _click_runes_filter(self, pos):
|
||
for kind, val, rect in self.runes_filter_rects:
|
||
if rect.collidepoint(pos):
|
||
if kind == "tier":
|
||
self.runes_filter_tier = val
|
||
elif kind == "color":
|
||
self.runes_filter_color = val
|
||
return True
|
||
if self.runes_sort_rect.collidepoint(pos):
|
||
self.runes_sort_best = not self.runes_sort_best
|
||
return True
|
||
return False
|
||
|
||
def _draw_runes_filters(self):
|
||
self.runes_filter_rects = []
|
||
y, h = 96, 24
|
||
tl = self.tiny.render("Tier:", True, MUTED)
|
||
self.screen.blit(tl, (60, y + 5))
|
||
x = 92
|
||
for val, label in ((0, "All"), (1, "T1"), (2, "T2")):
|
||
active = self.runes_filter_tier == val
|
||
rect = pygame.Rect(x, y, 30, h)
|
||
pygame.draw.rect(self.screen, (90, 120, 90) if active else (45, 50, 45),
|
||
rect, border_radius=4)
|
||
pygame.draw.rect(self.screen, HINT if active else (70, 80, 70),
|
||
rect, 1, border_radius=4)
|
||
t = self.tiny.render(label, True, TEXT if active else MUTED)
|
||
self.screen.blit(t, t.get_rect(center=rect.center))
|
||
self.runes_filter_rects.append(("tier", val, rect))
|
||
x += 36
|
||
ty = self.tiny.render("Type:", True, MUTED)
|
||
self.screen.blit(ty, (210, y + 5))
|
||
x = 244
|
||
all_rect = pygame.Rect(x, y, 36, h)
|
||
active = self.runes_filter_color 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.runes_filter_rects.append(("color", None, all_rect))
|
||
x += 42
|
||
for color in runes.RUNE_COLORS:
|
||
rect = pygame.Rect(x, y, 24, h)
|
||
active = self.runes_filter_color == color
|
||
pygame.draw.rect(self.screen, (45, 50, 45), rect, border_radius=4)
|
||
pygame.draw.rect(self.screen, HINT if active else (70, 80, 70),
|
||
rect, 1, border_radius=4)
|
||
sw = self._rune_icon(color, 22)
|
||
if sw:
|
||
self.screen.blit(sw, sw.get_rect(center=rect.center))
|
||
self.runes_filter_rects.append(("color", color, rect))
|
||
x += 28
|
||
active = self.runes_sort_best
|
||
pygame.draw.rect(self.screen, (90, 120, 90) if active else (45, 50, 45),
|
||
self.runes_sort_rect, border_radius=4)
|
||
pygame.draw.rect(self.screen, HINT if active else (70, 80, 70),
|
||
self.runes_sort_rect, 1, border_radius=4)
|
||
sl = self.tiny.render("Sort: best roll" if active else "Sort: default",
|
||
True, TEXT if active else MUTED)
|
||
self.screen.blit(sl, sl.get_rect(center=self.runes_sort_rect.center))
|
||
|
||
def _buy_rune(self, tier):
|
||
result = collection.buy_rune(tier)
|
||
if isinstance(result, dict):
|
||
self.runes_buy_popup = result
|
||
self.runes_buy_msg = ""
|
||
else:
|
||
self.runes_buy_msg = result
|
||
self.runes_buy_msg_until = pygame.time.get_ticks() + 1800
|
||
|
||
def _click_runes_deconstruct(self, pos):
|
||
inventory = collection.rune_inventory()
|
||
by_id = {r["id"]: r for r in inventory}
|
||
for rune_id, rect in self.rune_card_rects:
|
||
if rect.collidepoint(pos):
|
||
rune = by_id.get(rune_id)
|
||
if not rune:
|
||
return
|
||
if self.runes_target is None:
|
||
max_pct = runes.max_pct(rune["color"])
|
||
if rune.get("pct", 0) >= max_pct:
|
||
self.runes_deconstruct_msg = f"{rune['name']} is already at max roll"
|
||
return
|
||
self.runes_target = rune
|
||
self.runes_sacrifice = []
|
||
self.runes_deconstruct_msg = ""
|
||
else:
|
||
if rune_id == self.runes_target["id"]:
|
||
# click the target again to change it
|
||
self.runes_target = None
|
||
self.runes_sacrifice = []
|
||
self.runes_deconstruct_msg = ""
|
||
return
|
||
if rune_id in self.runes_sacrifice:
|
||
self.runes_sacrifice.remove(rune_id)
|
||
elif len(self.runes_sacrifice) >= runes.RUNE_SACRIFICE_COST:
|
||
self.runes_deconstruct_msg = \
|
||
f"Already picked {runes.RUNE_SACRIFICE_COST} runes"
|
||
return
|
||
else:
|
||
self.runes_sacrifice.append(rune_id)
|
||
self.runes_deconstruct_msg = ""
|
||
if len(self.runes_sacrifice) == runes.RUNE_SACRIFICE_COST:
|
||
self._open_runes_confirm()
|
||
return
|
||
|
||
def _open_runes_confirm(self):
|
||
equipped = []
|
||
for sid in self.runes_sacrifice:
|
||
owner = collection.rune_location(sid)
|
||
if owner:
|
||
equipped.append((sid, owner))
|
||
self.runes_confirm = {
|
||
"kind": "deconstruct",
|
||
"rune": self.runes_target,
|
||
"sacrifice": list(self.runes_sacrifice),
|
||
"equipped": equipped,
|
||
}
|
||
|
||
def _draw_runes_info(self):
|
||
overlay = pygame.Surface((WIN_W, WIN_H), pygame.SRCALPHA)
|
||
overlay.fill((0, 0, 0, 160))
|
||
self.screen.blit(overlay, (0, 0))
|
||
w, h = 560, 450
|
||
panel = pygame.Rect((WIN_W - w) // 2, (WIN_H - h) // 2, w, h)
|
||
pygame.draw.rect(self.screen, PANEL, panel, border_radius=12)
|
||
pygame.draw.rect(self.screen, (150, 190, 150), panel, 2, border_radius=12)
|
||
title = self.bold.render("Rune effects", True, HINT)
|
||
self.screen.blit(title, title.get_rect(center=(panel.centerx, panel.y + 30)))
|
||
y = panel.y + 62
|
||
for color in runes.RUNE_COLORS:
|
||
ic = self._rune_icon(color, 40)
|
||
if ic:
|
||
self.screen.blit(ic, (panel.x + 40, y))
|
||
nm = self.bold.render(runes.name(color), True, TEXT)
|
||
self.screen.blit(nm, (panel.x + 92, y))
|
||
eff = self.small.render(runes.summary(color), True, MUTED)
|
||
self.screen.blit(eff, (panel.x + 92, y + 24))
|
||
rg = self.tiny.render(
|
||
f"Tier {runes.tier(color)} · Rolls {runes.roll_range(color)}",
|
||
True, ACCENT)
|
||
self.screen.blit(rg, rg.get_rect(topright=(panel.right - 16, y + 10)))
|
||
y += 58
|
||
note = self.tiny.render(
|
||
"Runes of the same effect stack additively; a plant can equip at "
|
||
"most %d runes of the same type." % runes.MAX_RUNES_PER_TYPE,
|
||
True, MUTED)
|
||
self.screen.blit(note, note.get_rect(center=(panel.centerx, panel.bottom - 24)))
|
||
close = self.tiny.render("Click anywhere to close", True, MUTED)
|
||
self.screen.blit(close, close.get_rect(center=(panel.centerx, panel.bottom - 8)))
|
||
|
||
def _draw_runes_result(self):
|
||
overlay = pygame.Surface((WIN_W, WIN_H), pygame.SRCALPHA)
|
||
overlay.fill((0, 0, 0, 150))
|
||
self.screen.blit(overlay, (0, 0))
|
||
w, h = 420, 200
|
||
panel = pygame.Rect((WIN_W - w) // 2, (WIN_H - h) // 2, w, h)
|
||
pygame.draw.rect(self.screen, PANEL, panel, border_radius=12)
|
||
pygame.draw.rect(self.screen, ACCENT, panel, 2, border_radius=12)
|
||
title = self.bold.render("Rune improved!", True, ACCENT)
|
||
self.screen.blit(title, title.get_rect(center=(panel.centerx, panel.y + 30)))
|
||
res = self.runes_result
|
||
ic = self._rune_icon(res["color"], 40)
|
||
if ic:
|
||
self.screen.blit(ic, ic.get_rect(center=(panel.centerx, panel.y + 72)))
|
||
line = self.small.render(
|
||
f"{res['name']}: {res['before']}% -> {res['after']}%", True, TEXT)
|
||
self.screen.blit(line, line.get_rect(center=(panel.centerx, panel.y + 122)))
|
||
close = self.tiny.render("Click anywhere to close", True, MUTED)
|
||
self.screen.blit(close, close.get_rect(center=(panel.centerx, panel.bottom - 22)))
|
||
|
||
def _draw_runes_confirm(self):
|
||
overlay = pygame.Surface((WIN_W, WIN_H), pygame.SRCALPHA)
|
||
overlay.fill((0, 0, 0, 150))
|
||
self.screen.blit(overlay, (0, 0))
|
||
equipped = self.runes_confirm.get("equipped", [])
|
||
w, h = 520, 210 if equipped else 190
|
||
panel = pygame.Rect((WIN_W - w) // 2, (WIN_H - h) // 2, w, h)
|
||
pygame.draw.rect(self.screen, PANEL, panel, border_radius=12)
|
||
pygame.draw.rect(self.screen, DANGER, panel, 2, border_radius=12)
|
||
rune = self.runes_confirm["rune"]
|
||
name = rune["name"]
|
||
max_pct = runes.max_pct(rune["color"])
|
||
current = rune.get("pct", 0)
|
||
title = self.bold.render("Reroll rune?", True, TEXT)
|
||
msg = self.small.render(
|
||
f"Sacrifice {runes.RUNE_SACRIFICE_COST} runes to improve {name} to "
|
||
f"{current + 1}–{max_pct}%?",
|
||
True, TEXT)
|
||
self.screen.blit(title, title.get_rect(center=(panel.centerx, panel.y + 34)))
|
||
self.screen.blit(msg, msg.get_rect(center=(panel.centerx, panel.y + 60)))
|
||
if equipped:
|
||
plants = ", ".join(sorted({p for _, p in equipped}))
|
||
warn = self.tiny.render(
|
||
f"{len(equipped)} equipped rune(s) on {plants} will be removed",
|
||
True, DANGER)
|
||
self.screen.blit(warn, warn.get_rect(center=(panel.centerx, panel.y + 84)))
|
||
btn_y = panel.y + 120
|
||
else:
|
||
btn_y = panel.y + 108
|
||
self.runes_confirm_ok = pygame.Rect(panel.centerx - 110, btn_y, 100, 36)
|
||
self.runes_confirm_no = pygame.Rect(panel.centerx + 10, btn_y, 100, 36)
|
||
pygame.draw.rect(self.screen, DANGER, self.runes_confirm_ok, border_radius=8)
|
||
ok = self.bold.render("Reroll", True, TEXT)
|
||
self.screen.blit(ok, ok.get_rect(center=self.runes_confirm_ok.center))
|
||
pygame.draw.rect(self.screen, BTN, self.runes_confirm_no, border_radius=8)
|
||
no = self.bold.render("Cancel", True, TEXT)
|
||
self.screen.blit(no, no.get_rect(center=self.runes_confirm_no.center))
|
||
|
||
def _draw_collection(self):
|
||
self._draw_page("Collection", f"{len(self.owned)} / {len(self.names)} plants owned")
|
||
mouse = self.mouse_pos
|
||
for key, rect in self.collection_tab_btns.items():
|
||
active = self.collection_tab == key
|
||
hover = rect.collidepoint(mouse)
|
||
pygame.draw.rect(self.screen, (90, 120, 90) if (hover or active) else BTN,
|
||
rect, border_radius=6)
|
||
pygame.draw.rect(self.screen, (150, 190, 150) if active else (70, 80, 70),
|
||
rect, 1, border_radius=6)
|
||
t = self.tiny.render(
|
||
"Plants" if key == "plants" else "Bosses" if key == "bosses" else "Runes",
|
||
True, TEXT)
|
||
self.screen.blit(t, t.get_rect(center=rect.center))
|
||
if self.collection_tab == "runes":
|
||
self._draw_collection_runes()
|
||
elif self.collection_tab == "bosses":
|
||
self._draw_collection_bosses()
|
||
else:
|
||
self._draw_grid(self.names, reveal_all=False, show_evo=False,
|
||
reveal_owned=True, y0=144)
|
||
|
||
def _draw_collection_bosses(self):
|
||
mouse = self.mouse_pos
|
||
fought = collection.bosses_fought()
|
||
cw, ch, gap = 230, 260, 20
|
||
cols = 3
|
||
total = cols * cw + (cols - 1) * gap
|
||
x0 = (WIN_W - total) // 2
|
||
y0 = 150
|
||
self.boss_cards = []
|
||
for i, (name, spec) in enumerate(bosses.BOSS_SPECS.items()):
|
||
x = x0 + (i % cols) * (cw + gap)
|
||
y = y0 + (i // cols) * (ch + gap)
|
||
rect = pygame.Rect(x, y, cw, ch)
|
||
self.boss_cards.append((name, rect))
|
||
known = name in fought
|
||
pygame.draw.rect(self.screen, PANEL, rect, border_radius=8)
|
||
pygame.draw.rect(self.screen, (150, 120, 80) if known else (60, 70, 60),
|
||
rect, 1, border_radius=8)
|
||
spr = self._boss_sprite(name)
|
||
sprite_rect = pygame.Rect(rect.centerx - 60, rect.y + 12, 120, 120)
|
||
if spr:
|
||
ic = fit_surface(spr, 120)
|
||
if known:
|
||
self.screen.blit(ic, ic.get_rect(center=sprite_rect.center))
|
||
else:
|
||
dim = ic.copy()
|
||
dim.set_alpha(70)
|
||
self.screen.blit(dim, dim.get_rect(center=sprite_rect.center))
|
||
pygame.draw.rect(self.screen, (70, 78, 70), sprite_rect, 1)
|
||
nm = self.small.render(name if known else "???", True, TEXT if known else MUTED)
|
||
self.screen.blit(nm, nm.get_rect(center=(rect.centerx, rect.y + 150)))
|
||
if known:
|
||
atk, spd, dfn, hp = spec["stats"]
|
||
st = self.tiny.render(f"Atk {atk} Spd {spd} Def {dfn} HP {int(hp * HP_MULT)}",
|
||
True, MUTED)
|
||
self.screen.blit(st, st.get_rect(center=(rect.centerx, rect.y + 176)))
|
||
lines = wrap_text(spec["desc"], self.tiny, cw - 24)
|
||
ty = rect.y + 198
|
||
for ln in lines[:3]:
|
||
self.screen.blit(self.tiny.render(ln, True, (170, 175, 170)),
|
||
(rect.x + 12, ty))
|
||
ty += 16
|
||
drops = "Drops: " + ", ".join(spec["drop_runes"])
|
||
dr = self.tiny.render(drops, True, (235, 200, 120))
|
||
self.screen.blit(dr, dr.get_rect(center=(rect.centerx, rect.bottom - 20)))
|
||
else:
|
||
t = self.tiny.render("Not yet fought", True, (110, 115, 110))
|
||
self.screen.blit(t, t.get_rect(center=(rect.centerx, rect.y + 180)))
|
||
|
||
def _boss_sprite(self, name):
|
||
key = f"boss_{name}"
|
||
if key not in self.seed_pack_sprites:
|
||
self.seed_pack_sprites[key] = load_sprite(bosses.boss_sprite(name))
|
||
return self.seed_pack_sprites[key]
|
||
|
||
def _draw_collection_runes(self):
|
||
mouse = self.mouse_pos
|
||
self._draw_coll_rune_filters()
|
||
colors = self._collection_rune_colors()
|
||
owned_colors = {r["color"] for r in collection.rune_inventory()}
|
||
cw, ch, gap = 170, 190, 16
|
||
cols = 5
|
||
total = cols * cw + (cols - 1) * gap
|
||
x0 = (WIN_W - total) // 2
|
||
y0 = 184
|
||
self.coll_rune_cards = []
|
||
for i, color in enumerate(colors):
|
||
x = x0 + (i % cols) * (cw + gap)
|
||
y = y0 + (i // cols) * (ch + gap)
|
||
rect = pygame.Rect(x, y, cw, ch)
|
||
self.coll_rune_cards.append((color, rect))
|
||
obtained = color in owned_colors
|
||
pygame.draw.rect(self.screen, PANEL, rect, border_radius=8)
|
||
pygame.draw.rect(self.screen, (60, 70, 60), rect, 1, border_radius=8)
|
||
ic = self._rune_icon(color, 64)
|
||
if ic:
|
||
ipos = ic.get_rect(center=(rect.centerx, rect.y + 50))
|
||
if not obtained:
|
||
dim = ic.copy()
|
||
dim.set_alpha(70)
|
||
self.screen.blit(dim, ipos)
|
||
else:
|
||
self.screen.blit(ic, ipos)
|
||
nm = self.small.render(runes.name(color), True, TEXT if obtained else MUTED)
|
||
self.screen.blit(nm, nm.get_rect(center=(rect.centerx, rect.y + 88)))
|
||
if obtained:
|
||
eff = self.tiny.render(runes.summary(color), True, MUTED)
|
||
self.screen.blit(eff, eff.get_rect(center=(rect.centerx, rect.y + 108)))
|
||
if not obtained:
|
||
self.screen.blit(self.tiny.render("Not obtained", True, (110, 115, 110)),
|
||
self.tiny.render("Not obtained", True, (110, 115, 110)).get_rect(center=(rect.centerx, rect.y + 166)))
|
||
else:
|
||
t1 = self.tiny.render(f"Tier {runes.tier(color)} · max {runes.roll_range(color)}", True, HINT)
|
||
self.screen.blit(t1, t1.get_rect(center=(rect.centerx, rect.y + 166)))
|
||
if self.coll_rune_detail is not None:
|
||
self._draw_coll_rune_detail()
|
||
|
||
def _draw_coll_rune_filters(self):
|
||
self.coll_rune_filter_rects = []
|
||
y, h = 140, 24
|
||
tl = self.tiny.render("Tier:", True, MUTED)
|
||
self.screen.blit(tl, (60, y + 5))
|
||
x = 92
|
||
for val, label in ((0, "All"), (1, "T1"), (2, "T2")):
|
||
active = self.coll_rune_tier == val
|
||
rect = pygame.Rect(x, y, 30, h)
|
||
pygame.draw.rect(self.screen, (90, 120, 90) if active else (45, 50, 45),
|
||
rect, border_radius=4)
|
||
pygame.draw.rect(self.screen, HINT if active else (70, 80, 70),
|
||
rect, 1, border_radius=4)
|
||
t = self.tiny.render(label, True, TEXT if active else MUTED)
|
||
self.screen.blit(t, t.get_rect(center=rect.center))
|
||
self.coll_rune_filter_rects.append(("tier", val, rect))
|
||
x += 36
|
||
ty = self.tiny.render("Type:", True, MUTED)
|
||
self.screen.blit(ty, (210, y + 5))
|
||
x = 244
|
||
all_rect = pygame.Rect(x, y, 36, h)
|
||
active = self.coll_rune_color 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.coll_rune_filter_rects.append(("color", None, all_rect))
|
||
x += 42
|
||
for color in runes.RUNE_COLORS:
|
||
rect = pygame.Rect(x, y, 24, h)
|
||
active = self.coll_rune_color == color
|
||
pygame.draw.rect(self.screen, (45, 50, 45), rect, border_radius=4)
|
||
pygame.draw.rect(self.screen, HINT if active else (70, 80, 70),
|
||
rect, 1, border_radius=4)
|
||
sw = self._rune_icon(color, 22)
|
||
if sw:
|
||
self.screen.blit(sw, sw.get_rect(center=rect.center))
|
||
self.coll_rune_filter_rects.append(("color", color, rect))
|
||
x += 28
|
||
active = self.coll_rune_sort_best
|
||
pygame.draw.rect(self.screen, (90, 120, 90) if active else (45, 50, 45),
|
||
self.coll_rune_sort_rect, border_radius=4)
|
||
pygame.draw.rect(self.screen, HINT if active else (70, 80, 70),
|
||
self.coll_rune_sort_rect, 1, border_radius=4)
|
||
sl = self.tiny.render("Sort: best roll" if active else "Sort: default",
|
||
True, TEXT if active else MUTED)
|
||
self.screen.blit(sl, sl.get_rect(center=self.coll_rune_sort_rect.center))
|
||
|
||
def _click_coll_rune_filter(self, pos):
|
||
for kind, val, rect in self.coll_rune_filter_rects:
|
||
if rect.collidepoint(pos):
|
||
if kind == "tier":
|
||
self.coll_rune_tier = val
|
||
elif kind == "color":
|
||
self.coll_rune_color = val
|
||
return True
|
||
if self.coll_rune_sort_rect.collidepoint(pos):
|
||
self.coll_rune_sort_best = not self.coll_rune_sort_best
|
||
return True
|
||
return False
|
||
|
||
def _draw_coll_rune_detail(self):
|
||
color = self.coll_rune_detail
|
||
overlay = pygame.Surface((WIN_W, WIN_H), pygame.SRCALPHA)
|
||
overlay.fill((0, 0, 0, 150))
|
||
self.screen.blit(overlay, (0, 0))
|
||
w, h = 360, 240
|
||
panel = pygame.Rect((WIN_W - w) // 2, (WIN_H - h) // 2, w, h)
|
||
pygame.draw.rect(self.screen, PANEL, panel, border_radius=12)
|
||
pygame.draw.rect(self.screen, ACCENT, panel, 2, border_radius=12)
|
||
title = self.bold.render(runes.name(color), True, TEXT)
|
||
self.screen.blit(title, title.get_rect(center=(panel.centerx, panel.y + 30)))
|
||
ic = self._rune_icon(color, 64)
|
||
if ic:
|
||
self.screen.blit(ic, ic.get_rect(center=(panel.centerx, panel.y + 78)))
|
||
eff = self.small.render(runes.summary(color), True, TEXT)
|
||
self.screen.blit(eff, eff.get_rect(center=(panel.centerx, panel.y + 122)))
|
||
dmg = self.tiny.render(runes.effect_text(color, runes.max_pct(color)), True, MUTED)
|
||
self.screen.blit(dmg, dmg.get_rect(center=(panel.centerx, panel.y + 146)))
|
||
t1 = self.small.render(f"Tier {runes.tier(color)} · Max roll {runes.roll_range(color)}",
|
||
True, HINT)
|
||
self.screen.blit(t1, t1.get_rect(center=(panel.centerx, panel.y + 178)))
|
||
close = self.tiny.render("Click anywhere to close", True, MUTED)
|
||
self.screen.blit(close, close.get_rect(center=(panel.centerx, panel.bottom - 18)))
|
||
|
||
def _collection_rune_colors(self):
|
||
colors = [c for c in runes.RUNE_COLORS
|
||
if (self.coll_rune_tier == 0 or runes.tier(c) == self.coll_rune_tier)
|
||
and (self.coll_rune_color is None or c == self.coll_rune_color)]
|
||
if self.coll_rune_sort_best:
|
||
inv = collection.rune_inventory()
|
||
best = {c: max((r.get("pct", 0) for r in inv if r["color"] == c), default=0) for c in colors}
|
||
colors.sort(key=lambda c: best[c] / runes.max_pct(c), reverse=True)
|
||
return colors
|
||
|
||
def _draw_detail(self):
|
||
name = self.detail_name
|
||
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))
|
||
|
||
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))
|
||
if name in self.detail_list:
|
||
pos = self.tiny.render(f"{self.detail_list.index(name) + 1}/{len(self.detail_list)}", True, MUTED)
|
||
self.screen.blit(pos, pos.get_rect(topright=(WIN_W - 70, 24)))
|
||
|
||
if name is None or name not in POOL:
|
||
return
|
||
spec = POOL[name]
|
||
found = name in self.owned if self.detail_from == "collection" else True
|
||
|
||
sprite_rect = pygame.Rect(70, 110, 150, 150)
|
||
spr = self.sprites.get(name)
|
||
if spr:
|
||
ic = fit_surface(spr, 150)
|
||
self.screen.blit(ic, ic.get_rect(center=sprite_rect.center))
|
||
if spr and not found:
|
||
dim = ic.copy()
|
||
dim.set_alpha(70)
|
||
self.screen.blit(dim, dim.get_rect(center=sprite_rect.center))
|
||
pygame.draw.rect(self.screen, (70, 78, 70), sprite_rect, 2)
|
||
|
||
name_txt = "???" if not found else name
|
||
t = self.font.render(name_txt, True, TEXT if found else MUTED)
|
||
self.screen.blit(t, (250, 120))
|
||
if found:
|
||
rar = plant_rarity(name)
|
||
rt = self.small.render(RARITY_LABELS.get(rar, rar), True, RARITY_COLORS.get(rar, TEXT))
|
||
self.screen.blit(rt, (250 + t.get_width() + 12, 126))
|
||
if name in self.owned:
|
||
fav = collection.is_favorite(name)
|
||
pygame.draw.rect(self.screen, (70, 80, 70) if fav else (50, 58, 50),
|
||
self.star_rect, border_radius=6)
|
||
pygame.draw.rect(self.screen, (150, 190, 150) if fav else (70, 80, 70),
|
||
self.star_rect, 1, border_radius=6)
|
||
_draw_star(self.screen, self.star_rect.center, 13, HINT if fav else MUTED)
|
||
|
||
if not found:
|
||
t = self.small.render("Not discovered yet.", True, MUTED)
|
||
self.screen.blit(t, (250, 170))
|
||
t = self.small.render("Battle this plant to reveal its details.", True, MUTED)
|
||
self.screen.blit(t, (250, 196))
|
||
return
|
||
|
||
role_name = ROLE_NAMES.get(spec["role"], spec["role"])
|
||
ric = self._role_icon(spec["role"], 16)
|
||
role_surf = self.small.render(role_name, True, RARITY_COLORS.get(plant_rarity(name), TEXT))
|
||
sep_surf = self.small.render(" · ", True, MUTED)
|
||
icon_w = 16 + 6 if ric else 0
|
||
if name in VARIABLE_TYPE_UNITS:
|
||
tics = [load_type_icon(t, 16) for t in ALL_TYPES]
|
||
type_w = sum((ic.get_width() + 3) if ic else self.small.size(t)[0] + 3 for ic, t in zip(tics, ALL_TYPES))
|
||
else:
|
||
tic = load_type_icon(spec["type"], 18)
|
||
type_w = (tic.get_width() + 4) if tic else self.small.size(spec["type"])[0]
|
||
total_w = icon_w + role_surf.get_width() + sep_surf.get_width() + type_w
|
||
h = role_surf.get_height()
|
||
pill = pygame.Rect(250 - 8, 168 - 4, total_w + 16, h + 8)
|
||
pygame.draw.rect(self.screen, (52, 60, 52), pill, border_radius=5)
|
||
x = 250
|
||
if ric:
|
||
self.screen.blit(ric, ric.get_rect(midleft=(x, 168 + h // 2)))
|
||
x += icon_w
|
||
self.screen.blit(role_surf, (x, 168))
|
||
x += role_surf.get_width()
|
||
self.screen.blit(sep_surf, (x, 168))
|
||
x += sep_surf.get_width()
|
||
if name in VARIABLE_TYPE_UNITS:
|
||
for tic, t in zip(tics, ALL_TYPES):
|
||
if tic:
|
||
self.screen.blit(tic, tic.get_rect(midleft=(x, 168 + h // 2)))
|
||
x += tic.get_width() + 3
|
||
else:
|
||
ts = self.small.render(t, True, TYPE_COLORS.get(t, TEXT))
|
||
self.screen.blit(ts, (x, 168))
|
||
x += self.small.size(t)[0] + 3
|
||
else:
|
||
tic = load_type_icon(spec["type"], 18)
|
||
if tic:
|
||
self.screen.blit(tic, tic.get_rect(midleft=(x, 168 + h // 2)))
|
||
else:
|
||
ts = self.small.render(spec["type"], True, TYPE_COLORS.get(spec["type"], TEXT))
|
||
self.screen.blit(ts, (x, 168))
|
||
self.detail_type_rect = pill
|
||
self.type_choice_rects = {}
|
||
if name in VARIABLE_TYPE_UNITS and self.detail_from != "collection" and name in self.owned:
|
||
prog = collection.get_progress(name)
|
||
evolved = prog["stage"] >= 2
|
||
current = collection.unit_type(name)
|
||
tl = self.small.render("Type:", True, MUTED)
|
||
self.screen.blit(tl, (250, 194))
|
||
x = 250 + tl.get_width() + 8
|
||
for pt in ALL_TYPES:
|
||
rect = pygame.Rect(x, 192, 28, 28)
|
||
active = current == pt
|
||
bg = (90, 120, 90) if (evolved and active) else (45, 50, 45)
|
||
border = HINT if (evolved and active) else (70, 80, 70)
|
||
pygame.draw.rect(self.screen, bg, rect, border_radius=5)
|
||
pygame.draw.rect(self.screen, border, rect, 1, border_radius=5)
|
||
ic = load_type_icon(pt, 22)
|
||
if ic:
|
||
self.screen.blit(ic, ic.get_rect(center=rect.center))
|
||
if evolved:
|
||
self.type_choice_rects[pt] = rect
|
||
x += 34
|
||
if not evolved:
|
||
note = self.tiny.render("Evolve once to choose a type", True, MUTED)
|
||
self.screen.blit(note, (x + 4, 199))
|
||
if name in self.owned and self.detail_from != "collection":
|
||
_draw_evo_badge(self.screen, self.tiny, (WIN_W - 56, 175), collection.get_progress(name)["stage"] - 1)
|
||
|
||
show_owned = self.detail_from != "collection"
|
||
if show_owned and name in self.owned:
|
||
prog = collection.get_progress(name)
|
||
lv = self.small.render(f"Level {prog['level']} · Stage {prog['stage']} · {prog['duplicates']} dup", True, HINT)
|
||
self.screen.blit(lv, (250, 226))
|
||
if collection.injection_of(name):
|
||
pygame.draw.rect(self.screen, DANGER, self.inject_remove_rect, border_radius=6)
|
||
it = self.tiny.render("Remove injection", True, TEXT)
|
||
self.screen.blit(it, it.get_rect(center=self.inject_remove_rect.center))
|
||
|
||
if prog["level"] < MAX_LEVEL:
|
||
xp_next = xp_to_next(prog["level"])
|
||
frac = min(1.0, prog["xp"] / xp_next)
|
||
bar = pygame.Rect(250, 250, 330, 12)
|
||
pygame.draw.rect(self.screen, (45, 50, 45), bar, border_radius=6)
|
||
if frac > 0:
|
||
pygame.draw.rect(self.screen, (95, 145, 205),
|
||
(bar.x, bar.y, int(bar.w * frac), bar.h), border_radius=6)
|
||
xpt = self.tiny.render(f"XP {prog['xp']}/{xp_next}", True, TEXT)
|
||
self.screen.blit(xpt, (bar.right + 8, bar.y - 2))
|
||
else:
|
||
xpt = self.small.render("Max level", True, MUTED)
|
||
self.screen.blit(xpt, (250, 246))
|
||
|
||
atk, spd, dfn, hp = compute_stats(spec["stats"], prog["level"], prog["stage"])
|
||
crit = spec.get("crit", 0.10)
|
||
st = self.small.render(f"Attack {atk} Speed {spd}", True, TEXT)
|
||
self.screen.blit(st, (250, 276))
|
||
st = self.small.render(f"Defense {dfn} Health {int(hp * HP_MULT)} Crit {int(crit * 100)}%", True, TEXT)
|
||
self.screen.blit(st, (250, 300))
|
||
|
||
water_reason = collection.can_water(name)
|
||
water_ok = water_reason == ""
|
||
pygame.draw.rect(self.screen, BTN_HOVER if water_ok else (45, 50, 45), self.water_rect, border_radius=6)
|
||
wtxt = "Water (+1 level)" if water_ok else f"Water ({water_reason})"
|
||
t = self.tiny.render(wtxt, True, TEXT if water_ok else MUTED)
|
||
self.screen.blit(t, t.get_rect(center=self.water_rect.center))
|
||
|
||
ev_ok = can_evolve(prog["level"], prog["stage"], prog["duplicates"])
|
||
pygame.draw.rect(self.screen, BTN_HOVER if ev_ok else (45, 50, 45), self.evolve_rect, border_radius=6)
|
||
if ev_ok:
|
||
etxt = "Evolve!"
|
||
elif prog["stage"] >= 3:
|
||
etxt = "Max evolved"
|
||
else:
|
||
cost = evolve_cost(prog["stage"])
|
||
etxt = f"Evolve (Lv milestone + {cost} dup)" if cost else "Evolve (unlocked later)"
|
||
t = self.tiny.render(etxt, True, TEXT if ev_ok else MUTED)
|
||
self.screen.blit(t, t.get_rect(center=self.evolve_rect.center))
|
||
|
||
seed_state = collection.soil_state()
|
||
seed_ok = name not in VARIABLE_TYPE_UNITS and seed_state["cd_left"] <= 0
|
||
pygame.draw.rect(self.screen, BTN_HOVER if seed_ok else (45, 50, 45),
|
||
self.seed_rect, border_radius=6)
|
||
if name in VARIABLE_TYPE_UNITS:
|
||
stxt = "No seed"
|
||
elif seed_ok:
|
||
stxt = "Retrieve Seed"
|
||
else:
|
||
stxt = f"Seed in {int(seed_state['cd_left'])}s"
|
||
st = self.tiny.render(stxt, True, TEXT if seed_ok else MUTED)
|
||
self.screen.blit(st, st.get_rect(center=self.seed_rect.center))
|
||
|
||
equipped = sum(1 for r in collection.equipped_runes(name) if r is not None)
|
||
slots = collection.rune_slots(name)
|
||
pygame.draw.rect(self.screen, BTN_HOVER, self.runes_rect, border_radius=6)
|
||
pygame.draw.rect(self.screen, (150, 190, 150), self.runes_rect, 1, border_radius=6)
|
||
rt = self.tiny.render(f"Runes ({equipped}/{slots})", True, TEXT)
|
||
self.screen.blit(rt, rt.get_rect(center=self.runes_rect.center))
|
||
skills_y = 392
|
||
else:
|
||
atk, spd, dfn, hp = spec["stats"]
|
||
crit = spec.get("crit", 0.10)
|
||
st = self.small.render(f"Attack {atk} Speed {spd}", True, TEXT)
|
||
self.screen.blit(st, (250, 226))
|
||
st = self.small.render(f"Defense {dfn} Health {int(hp * HP_MULT)} Crit {int(crit * 100)}%", True, TEXT)
|
||
self.screen.blit(st, (250, 250))
|
||
skills_y = 290
|
||
|
||
p = spec.get("passive", "")
|
||
if p:
|
||
pname, pdesc = PASSIVES.get(p, (p, ""))
|
||
pt = self.bold.render(f"Passive — {pname}", True, HINT)
|
||
self.screen.blit(pt, (60, skills_y))
|
||
lines = wrap_text(" " + pdesc, self.small, WIN_W - 120)
|
||
for i, ln in enumerate(lines):
|
||
self.screen.blit(self.small.render(ln, True, TEXT),
|
||
(60, skills_y + 24 + i * 20))
|
||
skills_y += 26 + len(lines) * 20 + 4
|
||
|
||
y = skills_y
|
||
inj = collection.injection_of(name) if show_owned else None
|
||
for i, sk in enumerate(spec["skills"]):
|
||
is_inj = bool(inj) and i == inj.get("index")
|
||
if is_inj:
|
||
sk = skill_from_dict(inj["skill"])
|
||
bracket = _bracket_text(sk)
|
||
if bracket:
|
||
label = f"{sk.name} [{bracket}] CD {sk.cooldown}"
|
||
else:
|
||
label = f"{sk.name} CD {sk.cooldown}"
|
||
color = ACCENT if is_inj else HINT
|
||
t = self.bold.render(label, True, color)
|
||
self.screen.blit(t, (60, y))
|
||
if is_inj:
|
||
tag = self.tiny.render("injected", True, ACCENT)
|
||
self.screen.blit(tag, (60 + t.get_width() + 10, y + 2))
|
||
d = self.small.render(" " + sk.description, True, ACCENT if is_inj else TEXT)
|
||
self.screen.blit(d, (60, y + 24))
|
||
y += 60
|
||
|
||
def _draw_grid(self, names, reveal_all=False, level_mode=False, show_evo=True, reveal_owned=False,
|
||
data=None, y0=96):
|
||
data = data or collection.load()
|
||
cols, cw, ch, gap = 4, 210, 150, 14
|
||
width = min(cols, len(names))
|
||
x0 = (WIN_W - (width * cw + (width - 1) * gap)) // 2
|
||
rows = (len(names) + width - 1) // width
|
||
bottom = y0 + rows * (ch + gap) - gap
|
||
self.grid_scroll = max(0, min(self.grid_scroll, max(0, bottom - (WIN_H - 62))))
|
||
self.grid_scroll_max = max(0, bottom - (WIN_H - 62))
|
||
scroll = self.grid_scroll
|
||
self.grid_hit = []
|
||
self.type_tag_rects = {}
|
||
old_clip = self.screen.get_clip()
|
||
self.screen.set_clip(pygame.Rect(0, 90, WIN_W, WIN_H - 90 - 46))
|
||
for i, name in enumerate(names):
|
||
x = x0 + (i % width) * (cw + gap)
|
||
y = y0 + (i // width) * (ch + gap) - scroll
|
||
rect = pygame.Rect(x, y, cw, ch)
|
||
self.grid_hit.append((name, rect))
|
||
if reveal_owned:
|
||
found = name in self.owned
|
||
else:
|
||
found = reveal_all or name in self.discovered
|
||
pygame.draw.rect(self.screen, PANEL, rect, border_radius=6)
|
||
pygame.draw.rect(self.screen, (60, 70, 60), rect, 1, border_radius=6)
|
||
spr = self.sprites.get(name)
|
||
sprite_rect = pygame.Rect(rect.centerx - 44, rect.y + 12, 88, 88)
|
||
if spr:
|
||
ic = fit_surface(spr, 88)
|
||
if found:
|
||
self.screen.blit(ic, ic.get_rect(center=sprite_rect.center))
|
||
else:
|
||
# Dim only the sprite (matches the runes tab): blitting the
|
||
# full icon first then the dim copy would cancel out.
|
||
dim = ic.copy()
|
||
dim.set_alpha(70)
|
||
self.screen.blit(dim, dim.get_rect(center=sprite_rect.center))
|
||
pygame.draw.rect(self.screen, (70, 78, 70), sprite_rect, 1)
|
||
name_txt = "???" if not found else name
|
||
t = self.tiny.render(name_txt, True, TEXT if found else MUTED)
|
||
self.screen.blit(t, t.get_rect(center=(rect.centerx, rect.y + 110)))
|
||
if found:
|
||
if level_mode:
|
||
lv = collection.get_progress(name, data)["level"]
|
||
lvt = self.tiny.render(f"Lv {lv}", True, HINT)
|
||
self.screen.blit(lvt, lvt.get_rect(center=(rect.centerx, rect.y + 130)))
|
||
can = collection.can_water(name, data) == ""
|
||
smart = collection.smart_water_candidate(name, data)
|
||
if smart:
|
||
pygame.draw.rect(self.screen, (245, 200, 90), rect, 3, border_radius=6)
|
||
elif can:
|
||
pygame.draw.rect(self.screen, (110, 190, 110), rect, 2, border_radius=6)
|
||
else:
|
||
pygame.draw.rect(self.screen, (70, 78, 70), rect, 1, border_radius=6)
|
||
else:
|
||
role_name = ROLE_NAMES.get(POOL[name]["role"], POOL[name]["role"])
|
||
ric = self._role_icon(POOL[name]["role"], 12)
|
||
rt = self.tiny.render(role_name, True, RARITY_COLORS.get(plant_rarity(name), TEXT))
|
||
sep = self.tiny.render(" · ", True, MUTED)
|
||
role_icon_w = 12 + 3 if ric else 0
|
||
if name in VARIABLE_TYPE_UNITS:
|
||
tics = [(load_type_icon(t, 12), t) for t in ALL_TYPES]
|
||
type_w = sum((ic.get_width() + 2) if ic else self.tiny.size(t)[0] + 2
|
||
for ic, t in tics)
|
||
else:
|
||
tic = load_type_icon(POOL[name]["type"], 14)
|
||
type_w = (tic.get_width() + 2) if tic else self.tiny.size(POOL[name]["type"])[0]
|
||
total_w = role_icon_w + rt.get_width() + sep.get_width() + type_w
|
||
row_y = rect.y + 130
|
||
rt_rect = rt.get_rect(topleft=(rect.centerx - total_w // 2 + role_icon_w, row_y))
|
||
sep_rect = sep.get_rect(midleft=(rt_rect.right, rt_rect.centery))
|
||
if ric:
|
||
self.screen.blit(ric, ric.get_rect(midright=(rt_rect.left - 3, rt_rect.centery)))
|
||
self.screen.blit(rt, rt_rect)
|
||
self.screen.blit(sep, sep_rect)
|
||
tx = sep_rect.right + 2
|
||
if name in VARIABLE_TYPE_UNITS:
|
||
first_tr = last_tr = None
|
||
for tic, t in tics:
|
||
if tic:
|
||
tr = tic.get_rect(midleft=(tx, sep_rect.centery))
|
||
self.screen.blit(tic, tr)
|
||
else:
|
||
ty = self.tiny.render(t, True, MUTED)
|
||
tr = ty.get_rect(midleft=(tx, sep_rect.centery))
|
||
self.screen.blit(ty, tr)
|
||
first_tr = first_tr or tr
|
||
last_tr = tr
|
||
tx += tr.width + 2
|
||
self.type_tag_rects[name] = first_tr.union(last_tr).inflate(6, 4)
|
||
else:
|
||
tic = load_type_icon(POOL[name]["type"], 14)
|
||
if tic:
|
||
tr = tic.get_rect(midleft=(tx, sep_rect.centery))
|
||
self.screen.blit(tic, tr)
|
||
else:
|
||
ty = self.tiny.render(POOL[name]["type"], True,
|
||
TYPE_COLORS.get(POOL[name]["type"], MUTED))
|
||
tr = ty.get_rect(midleft=(tx, sep_rect.centery))
|
||
self.screen.blit(ty, tr)
|
||
self.type_tag_rects[name] = tr.inflate(6, 4)
|
||
if name in self.owned and show_evo:
|
||
evo = collection.get_progress(name, data)["stage"] - 1
|
||
_draw_evo_badge(self.screen, self.tiny, (rect.x + 22, rect.y + 28), evo)
|
||
if name in self.favs:
|
||
_draw_star(self.screen, (rect.x + 18, rect.y + 12), 8, HINT)
|
||
if name in self.owned and not level_mode:
|
||
lv = self.tiny.render(f"Lv {collection.get_progress(name, data)['level']}", True, HINT)
|
||
self.screen.blit(lv, (rect.right - 34, rect.y + 8))
|
||
self.screen.set_clip(old_clip)
|
||
if self.grid_scroll_max > 0:
|
||
hint = "▲" if scroll > 0 else ""
|
||
if scroll < self.grid_scroll_max:
|
||
hint += "▼"
|
||
if hint:
|
||
ht = self.tiny.render(hint, True, MUTED)
|
||
self.screen.blit(ht, ht.get_rect(center=(WIN_W // 2, WIN_H - 46))) |