Files
Flora/ui/runes.py
T

437 lines
20 KiB
Python

import pygame
from assets import icon as icon_path
from engine import collection
from engine.plants import POOL, RARITY_COLORS, plant_rarity
from engine import runes
from ui.scaling import fit_surface, load_rune_icon, make_window, request_quit, to_logical
WIN_W, WIN_H = 1040, 720
BG = (24, 30, 24)
TEXT = (230, 235, 230)
MUTED = (150, 155, 150)
HINT = (255, 220, 120)
PANEL = (32, 38, 32)
BTN = (60, 90, 60)
BTN_HOVER = (85, 125, 85)
ACCENT = (120, 220, 190)
DANGER = (150, 70, 70)
SLOT_BORDER = (90, 110, 90)
SLOT_SIZE = 64
def draw_rune_chips(screen, get_icon, colors, x, y, size=14, gap=2, max_chips=3):
"""Draw small rune icons for the given colors starting at (x, y). `get_icon(color)`
returns a surface (already sized) or None. Used to show equipped runes at a glance."""
for i, color in enumerate(colors[:max_chips]):
ic = get_icon(color)
if ic:
screen.blit(ic, (x + i * (size + gap), y))
class RuneScreen:
def __init__(self, plant: str):
pygame.init()
self.plant = plant
self.window, self.scale = make_window(WIN_W, WIN_H)
self.screen = pygame.Surface((WIN_W, WIN_H))
pygame.display.set_caption(f"Plants — Runes · {plant}")
self.clock = pygame.time.Clock()
self.font = pygame.font.SysFont("segoeui", 30)
self.bold = pygame.font.SysFont("segoeui", 20, bold=True)
self.small = pygame.font.SysFont("segoeui", 16)
self.tiny = pygame.font.SysFont("segoeui", 13)
self.back_rect = pygame.Rect(16, 16, 90, 34)
self.info_rect = pygame.Rect(WIN_W - 100, 16, 84, 34)
self.mouse_pos = (0, 0)
self.selected = None # rune id selected from the inventory
self.confirm = None # {"kind","slot","rune",...} prompt
self.confirm_ok = pygame.Rect(0, 0, 0, 0)
self.confirm_no = pygame.Rect(0, 0, 0, 0)
self.info = False # rune-effects info popup
self.equip_msg = "" # transient message (e.g. rune-type limit)
self.equip_msg_until = 0
self.filter_tier = 0 # 0 all, 1 tier 1, 2 tier 2
self.filter_color = None # None all, else a rune color
self.sort_best = False # sort by proportional roll
self.filter_rects = [] # (kind, value, rect)
self.sort_rect = pygame.Rect(WIN_W - 170, 284, 150, 24)
self.slot_rects = [] # (slot_index, rect)
self.inv_rects = [] # (rune_id, rect)
self.scroll = 0
self.scroll_max = 0
self._icons = {}
def _icon_for(self, color):
if color not in self._icons:
self._icons[color] = load_rune_icon(color, 40)
return self._icons[color]
def run(self):
running = True
while running:
self.clock.tick(60)
self.mouse_pos = to_logical(pygame.mouse.get_pos(), self.scale)
for event in pygame.event.get():
if event.type == pygame.QUIT:
request_quit()
running = False
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
pos = to_logical(event.pos, self.scale)
if self.back_rect.collidepoint(pos):
running = False
continue
if self.info:
self.info = False
continue
if self.info_rect.collidepoint(pos):
self.info = True
continue
if self.confirm is not None:
if self.confirm_ok.collidepoint(pos):
if self.confirm["kind"] == "move":
collection.equip_rune(self.plant, self.confirm["slot"],
self.confirm["rune"]["id"])
self.selected = None
else:
collection.unequip_rune(self.plant, self.confirm["slot"])
self.confirm = None
elif self.confirm_no.collidepoint(pos):
self.confirm = None
continue
self._click(pos)
elif event.type == pygame.MOUSEWHEEL:
self.scroll = max(0, min(self.scroll_max, self.scroll - event.y * 44))
self._draw()
pygame.transform.smoothscale(self.screen, self.window.get_size(), self.window)
pygame.display.flip()
def _click(self, pos):
if self._click_filter(pos):
return
equipped = collection.equipped_runes(self.plant)
inventory = collection.rune_inventory()
by_id = {r["id"]: r for r in inventory}
for slot, rect in self.slot_rects:
if rect.collidepoint(pos):
if self.selected is not None:
rune = by_id.get(self.selected)
owner = collection.rune_location(self.selected)
if owner and owner != self.plant:
self.confirm = {"kind": "move", "slot": slot,
"rune": rune, "from": owner}
else:
reason = collection.equip_rune(self.plant, slot, self.selected)
self.equip_msg = reason or ""
self.equip_msg_until = (pygame.time.get_ticks() + 2000) if reason else 0
self.selected = None
elif slot < len(equipped) and equipped[slot] is not None:
self.confirm = {"kind": "remove", "slot": slot,
"rune": by_id.get(equipped[slot])}
return
for rune_id, rect in self.inv_rects:
if rect.collidepoint(pos):
self.selected = None if self.selected == rune_id else rune_id
return
def _click_filter(self, pos):
for kind, val, rect in self.filter_rects:
if rect.collidepoint(pos):
if kind == "tier":
self.filter_tier = val
elif kind == "color":
self.filter_color = val
return True
if self.sort_rect.collidepoint(pos):
self.sort_best = not self.sort_best
return True
return False
def _displayed_runes(self, inventory):
shown = [r for r in inventory
if (self.filter_tier == 0 or runes.tier(r["color"]) == self.filter_tier)
and (self.filter_color is None or r["color"] == self.filter_color)]
if self.sort_best:
shown.sort(key=runes.roll_ratio, reverse=True)
return shown
def _small_icon(self, color):
key = ("small", color)
if key not in self._icons:
ic = self._icon_for(color)
self._icons[key] = fit_surface(ic, 22) if ic else None
return self._icons[key]
def _draw_filters(self):
self.filter_rects = []
y, h = 284, 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.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.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.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.filter_rects.append(("color", None, all_rect))
x += 42
for color in runes.RUNE_COLORS:
rect = pygame.Rect(x, y, 24, h)
active = self.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 if active else 2, border_radius=4)
sw = self._small_icon(color)
if sw:
self.screen.blit(sw, sw.get_rect(center=rect.center))
self.filter_rects.append(("color", color, rect))
x += 28
active = self.sort_best
pygame.draw.rect(self.screen, (90, 120, 90) if active else (45, 50, 45),
self.sort_rect, border_radius=4)
pygame.draw.rect(self.screen, HINT if active else (70, 80, 70),
self.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.sort_rect.center))
def _draw(self):
self.slot_rects = []
self.inv_rects = []
self.screen.fill(BG)
pygame.draw.rect(self.screen, BTN, self.back_rect, border_radius=6)
bt = self.small.render("Back", True, TEXT)
self.screen.blit(bt, bt.get_rect(center=self.back_rect.center))
ihov = self.info_rect.collidepoint(self.mouse_pos)
pygame.draw.rect(self.screen, (90, 120, 90) if (self.info or ihov) else BTN,
self.info_rect, border_radius=6)
il = self.tiny.render("Rune info", True, TEXT)
self.screen.blit(il, il.get_rect(center=self.info_rect.center))
spec = POOL[self.plant]
rar = plant_rarity(self.plant)
title = self.font.render(self.plant, True, RARITY_COLORS.get(rar, TEXT))
self.screen.blit(title, title.get_rect(center=(WIN_W // 2, 44)))
sub = self.small.render(f"Rune slots — {spec['role']}", True, MUTED)
self.screen.blit(sub, sub.get_rect(center=(WIN_W // 2, 76)))
slots = collection.rune_slots(self.plant)
equipped = collection.equipped_runes(self.plant)
inventory = collection.rune_inventory()
by_id = {r["id"]: r for r in inventory}
# --- slots -----------------------------------------------------
st = self.bold.render("Rune slots", True, HINT)
self.screen.blit(st, (60, 110))
locked = self.tiny.render(
"Evolve to unlock more slots (permanent)", True, MUTED)
self.screen.blit(locked, (200, 116))
total_w = slots * SLOT_SIZE + (slots - 1) * 20
x0 = (WIN_W - total_w) // 2
y = 140
for i in range(slots):
rect = pygame.Rect(x0 + i * (SLOT_SIZE + 20), y, SLOT_SIZE, SLOT_SIZE)
pygame.draw.rect(self.screen, PANEL, rect, border_radius=10)
pygame.draw.rect(self.screen, SLOT_BORDER, rect, 2, border_radius=10)
rune = by_id.get(equipped[i]) if i < len(equipped) else None
if rune:
ic = self._icon_for(rune["color"])
if ic:
self.screen.blit(ic, ic.get_rect(center=rect.center))
else:
num = self.bold.render(str(i + 1), True, MUTED)
num.set_alpha(70) # faint hint that the slot is empty
self.screen.blit(num, num.get_rect(center=rect.center))
lbl = self.tiny.render(f"Slot {i + 1}", True, MUTED)
self.screen.blit(lbl, lbl.get_rect(center=(rect.centerx, rect.bottom + 14)))
self.slot_rects.append((i, rect))
# --- equipped-effects summary (right of the slots) ------------
equipped_runes = [by_id[e] for e in equipped if e in by_id]
line_h = 18
panel = pygame.Rect(x0 + total_w + 40, y, 240,
26 + line_h * max(1, len(equipped_runes)) + 8)
pygame.draw.rect(self.screen, PANEL, panel, border_radius=8)
pygame.draw.rect(self.screen, (60, 70, 60), panel, 1, border_radius=8)
t = self.bold.render("Equipped effects", True, HINT)
self.screen.blit(t, (panel.x + 8, panel.y + 6))
py = panel.y + 28
if equipped_runes:
for rune in equipped_runes:
eff = self.tiny.render(
runes.effect_text(rune["color"], rune.get("pct", 30)), True, TEXT)
self.screen.blit(eff, (panel.x + 8, py))
py += line_h
else:
e = self.tiny.render("No runes equipped", True, MUTED)
self.screen.blit(e, (panel.x + 8, py))
if pygame.time.get_ticks() < self.equip_msg_until:
b = self.tiny.render(self.equip_msg, True, DANGER)
self.screen.blit(b, b.get_rect(center=(WIN_W // 2, y + SLOT_SIZE + 44)))
elif self.selected is not None:
sel = by_id.get(self.selected)
if sel:
owner = collection.rune_location(self.selected)
if owner and owner != self.plant:
hint = self.bold.render(
f"Click a slot to move {sel['name']} from {owner}", True, ACCENT)
else:
hint = self.bold.render(
f"Click a slot to equip {sel['name']}", True, ACCENT)
self.screen.blit(hint, hint.get_rect(center=(WIN_W // 2, y + SLOT_SIZE + 44)))
elif slots == 1:
h = self.tiny.render("Click a rune below, then click a slot to equip it.",
True, MUTED)
self.screen.blit(h, h.get_rect(center=(WIN_W // 2, y + SLOT_SIZE + 44)))
# --- inventory -------------------------------------------------
it = self.bold.render("Runes you own", True, HINT)
self.screen.blit(it, (60, 260))
dr = self.tiny.render(
f"Drop rates — {runes.BATTLE_DROP_CHANCE * 100:.0f}% for any rune "
f"from floor battles · {runes.PACK_DROP_CHANCE * 100:.0f}% for any "
f"rune from packs",
True, MUTED)
self.screen.blit(dr, dr.get_rect(midright=(WIN_W - 60, 270)))
self._draw_filters()
display = self._displayed_runes(inventory)
list_y = 318
self.scroll_max = max(0, len(display) * 46 - (WIN_H - list_y - 20))
self.scroll = max(0, min(self.scroll, self.scroll_max))
old_clip = self.screen.get_clip()
self.screen.set_clip(pygame.Rect(0, list_y, WIN_W, WIN_H - list_y - 20))
ry = list_y - self.scroll
for rune in display:
rect = pygame.Rect(60, ry, 780, 40)
pygame.draw.rect(self.screen, PANEL, rect, border_radius=6)
if rune["id"] == self.selected:
pygame.draw.rect(self.screen, ACCENT, rect, 2, border_radius=6)
else:
pygame.draw.rect(self.screen, (60, 70, 60), rect, 1, border_radius=6)
ic = self._icon_for(rune["color"])
if ic:
self.screen.blit(ic, (rect.x + 8, rect.y + 4))
owner = collection.rune_location(rune["id"])
on_this = owner == self.plant
name = self.small.render(rune["name"], True,
TEXT if not on_this else MUTED)
self.screen.blit(name, (rect.x + 58, rect.y + 11))
eff = self.tiny.render(runes.effect_text(rune["color"], rune.get("pct", 30)),
True, MUTED)
self.screen.blit(eff, (rect.x + 58 + name.get_width() + 10, rect.y + 14))
if on_this:
tag = self.tiny.render("equipped", True, MUTED)
self.screen.blit(tag, (rect.right - 70, rect.y + 13))
elif owner:
tag = self.tiny.render(f"Equipped in: {owner}", True, MUTED)
self.screen.blit(tag, tag.get_rect(midright=(rect.right - 14, rect.centery)))
self.inv_rects.append((rune["id"], rect))
ry += 46
self.screen.set_clip(old_clip)
if not inventory:
empty = self.small.render(
"No runes yet — win floor battles or open seed packs to find some.",
True, MUTED)
self.screen.blit(empty, (60, list_y + 10))
elif not display:
empty = self.small.render("No runes match the current filter.",
True, MUTED)
self.screen.blit(empty, (60, list_y + 10))
if self.scroll_max > 0:
hint = "▲" if self.scroll > 0 else ""
if self.scroll < self.scroll_max:
hint += "▼"
ht = self.tiny.render(hint, True, MUTED)
self.screen.blit(ht, ht.get_rect(center=(WIN_W // 2, WIN_H - 18)))
if self.confirm is not None:
self._draw_confirm()
if self.info:
self._draw_info()
def _draw_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, 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, (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._icon_for(color)
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_confirm(self):
overlay = pygame.Surface((WIN_W, WIN_H), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 150))
self.screen.blit(overlay, (0, 0))
w, h = 480, 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.confirm["rune"]
name = rune["name"] if rune else "Rune"
if self.confirm["kind"] == "move":
title = self.bold.render("Move rune?", True, TEXT)
msg = self.small.render(
f"Remove {name} from {self.confirm['from']} to slot it here?", True, TEXT)
ok_label = "Move"
else:
slot = self.confirm["slot"] + 1
title = self.bold.render("Remove rune?", True, TEXT)
msg = self.small.render(f"Unequip {name} from slot {slot}?", True, TEXT)
ok_label = "Remove"
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 + 66)))
self.confirm_ok = pygame.Rect(panel.centerx - 110, panel.y + 110, 100, 36)
self.confirm_no = pygame.Rect(panel.centerx + 10, panel.y + 110, 100, 36)
pygame.draw.rect(self.screen, DANGER, self.confirm_ok, border_radius=8)
ok = self.bold.render(ok_label, True, TEXT)
self.screen.blit(ok, ok.get_rect(center=self.confirm_ok.center))
pygame.draw.rect(self.screen, BTN, self.confirm_no, border_radius=8)
no = self.bold.render("Cancel", True, TEXT)
self.screen.blit(no, no.get_rect(center=self.confirm_no.center))