"""UI: all rendering — battlefield, cards, menus, HUD. Chinese ink painting style.""" import pygame from card_game.config import ( WINDOW_WIDTH, WINDOW_HEIGHT, INK_BLACK, PAPER_WHITE, GRAY, DARK_GRAY, LIGHT_GRAY, ZHU_HONG, SONGHUA_GREEN, DIAN_BLUE, TENG_HUANG, GOLD, SILVER, ORANGE, JIANG_BROWN, BG_COLOR, FIELD_COLOR, FRONTLINE_COLOR, FACTION_COLORS, RARITY_LIMITS, HAND_HEIGHT, ACTION_BAR_HEIGHT, ZONE_HEIGHT, CARD_WIDTH, CARD_HEIGHT, FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT, CAPITAL_WIDTH, CAPITAL_HEIGHT, ENEMY_SUPPORT_Y, FRONTLINE_Y, PLAYER_SUPPORT_Y, PLAYER_HAND_Y, ACTION_BAR_Y, MAX_SUPPORT_SLOTS, MAX_FRONTLINE_SLOTS, ENEMY_INFO_HEIGHT, ENEMY_HAND_HEIGHT, FACTIONS, CARD_DATABASE, SLOT_SPACING, HAND_CARD_SPACING, INK_WASH_2, INK_WASH_3, INK_WASH_4, INK_WASH_5, ) from card_game.effects import EffectManager from card_game import ink_style # Derived layout HAND_Y = PLAYER_HAND_Y + 5 def _centered_x(n_items, item_w, spacing): total = n_items * item_w + (n_items - 1) * (spacing - item_w) return (WINDOW_WIDTH - total) // 2 def _support_slot_x(slot_index): cap_x = WINDOW_WIDTH // 2 - CAPITAL_WIDTH // 2 gap = 10 if slot_index < 2: return cap_x - gap - FIELD_CARD_WIDTH - (1 - slot_index) * SLOT_SPACING else: right_start = cap_x + CAPITAL_WIDTH + gap return right_start + (slot_index - 2) * SLOT_SPACING def _frontline_slot_x(slot_index, n_slots): start = _centered_x(n_slots, FIELD_CARD_WIDTH, SLOT_SPACING) return start + slot_index * SLOT_SPACING class UI: def __init__(self, screen): self.screen = screen # Use SimHei (黑体) for Chinese text - clean and always available on Windows self.font_sm = pygame.font.SysFont("simhei", 14) self.font_md = pygame.font.SysFont("simhei", 18) self.font_lg = pygame.font.SysFont("simhei", 24) self.font_xl = pygame.font.SysFont("simhei", 36) self.effects = EffectManager() # Interaction state self.selected_card = None self.selected_unit = None self.hover_card = None self.hover_field_unit = None self.hover_pos = (0, 0) self.valid_targets = [] self.target_mode = None # Button rects self.end_turn_btn = pygame.Rect(0, 0, 0, 0) self.faction_buttons = [] self.menu_buttons = {} # Deck builder state self.deck_builder_cards = [] self.deck_builder_faction = None self.deck_card_rects = [] self.deck_build_rects = [] # --- Main Draw --- def draw_menu(self): ink_style.blit_paper_background(self.screen) ink_style.blit_mountains(self.screen) # Title in seal stamp style title_text = "战国卡牌" title_w, title_h = 320, 70 title_x = WINDOW_WIDTH // 2 - title_w // 2 title_y = 60 ink_style.draw_seal_stamp(self.screen, (title_x, title_y, title_w, title_h), title_text, self.font_xl) # Subtitle sub = self.font_lg.render("七 雄 争 霸", True, INK_WASH_3) self.screen.blit(sub, (WINDOW_WIDTH // 2 - sub.get_width() // 2, 145)) # Faction buttons as scroll shapes self.faction_buttons = [] factions = list(FACTIONS.keys()) cols = 4 bw, bh = 240, 90 start_x = (WINDOW_WIDTH - cols * (bw + 20)) // 2 start_y = 200 for i, fid in enumerate(factions): row, col = divmod(i, cols) x = start_x + col * (bw + 20) y = start_y + row * (bh + 20) rect = pygame.Rect(x, y, bw, bh) self.faction_buttons.append((rect, fid)) color = FACTION_COLORS[fid] # Draw as a scroll with faction color tint ink_style.draw_scroll(self.screen, (x, y, bw, bh), scroll_color=(min(255, color[0] + 80), min(255, color[1] + 80), min(255, color[2] + 80))) # Faction name name = self.font_lg.render(FACTIONS[fid]["name"], True, INK_BLACK) self.screen.blit(name, (x + bw // 2 - name.get_width() // 2, y + 12)) # Passive name passive = self.font_sm.render(FACTIONS[fid]["passive_name"], True, TENG_HUANG) self.screen.blit(passive, (x + bw // 2 - passive.get_width() // 2, y + 45)) # Passive desc (truncated) desc = self.font_sm.render(FACTIONS[fid]["passive_desc"][:16], True, INK_WASH_3) self.screen.blit(desc, (x + bw // 2 - desc.get_width() // 2, y + 68)) inst = self.font_sm.render("选择你的国家开始游戏", True, INK_WASH_3) self.screen.blit(inst, (WINDOW_WIDTH // 2 - inst.get_width() // 2, WINDOW_HEIGHT - 60)) def draw_deck_select(self, player_faction): ink_style.blit_paper_background(self.screen) ink_style.blit_mountains(self.screen) title = self.font_xl.render("选择对手", True, INK_BLACK) self.screen.blit(title, (WINDOW_WIDTH // 2 - title.get_width() // 2, 40)) your_faction = self.font_md.render(f"你的国家:{FACTIONS[player_faction]['name']}", True, INK_WASH_4) self.screen.blit(your_faction, (WINDOW_WIDTH // 2 - your_faction.get_width() // 2, 90)) self.faction_buttons = [] factions = [f for f in FACTIONS.keys() if f != player_faction] cols = 3 bw, bh = 300, 100 start_x = (WINDOW_WIDTH - cols * (bw + 20)) // 2 start_y = 140 for i, fid in enumerate(factions): row, col = divmod(i, cols) x = start_x + col * (bw + 20) y = start_y + row * (bh + 20) rect = pygame.Rect(x, y, bw, bh) self.faction_buttons.append((rect, fid)) color = FACTION_COLORS[fid] ink_style.draw_scroll(self.screen, (x, y, bw, bh), scroll_color=(min(255, color[0] + 80), min(255, color[1] + 80), min(255, color[2] + 80))) name = self.font_lg.render(FACTIONS[fid]["name"], True, INK_BLACK) self.screen.blit(name, (x + bw // 2 - name.get_width() // 2, y + 10)) leader = self.font_md.render(f"君主:{FACTIONS[fid]['leader']}", True, INK_WASH_3) self.screen.blit(leader, (x + bw // 2 - leader.get_width() // 2, y + 45)) passive = self.font_sm.render(FACTIONS[fid]["passive_desc"], True, TENG_HUANG) self.screen.blit(passive, (x + bw // 2 - passive.get_width() // 2, y + 75)) # Buttons self.menu_buttons = {} back_rect = pygame.Rect(20, WINDOW_HEIGHT - 60, 120, 40) ink_style.draw_ink_rect(self.screen, back_rect, INK_WASH_3, alpha=200) t = self.font_md.render("返回", True, PAPER_WHITE) self.screen.blit(t, (back_rect.centerx - t.get_width() // 2, back_rect.centery - t.get_height() // 2)) self.menu_buttons["back"] = back_rect build_rect = pygame.Rect(WINDOW_WIDTH - 200, WINDOW_HEIGHT - 60, 180, 40) ink_style.draw_ink_rect(self.screen, build_rect, (60, 100, 60), alpha=200) t = self.font_md.render("自由组卡", True, PAPER_WHITE) self.screen.blit(t, (build_rect.centerx - t.get_width() // 2, build_rect.centery - t.get_height() // 2)) self.menu_buttons["deck_build"] = build_rect # Random opponent button random_rect = pygame.Rect(WINDOW_WIDTH // 2 - 80, WINDOW_HEIGHT - 60, 160, 40) ink_style.draw_ink_rect(self.screen, random_rect, ZHU_HONG, alpha=200) t = self.font_md.render("随机对手", True, PAPER_WHITE) self.screen.blit(t, (random_rect.centerx - t.get_width() // 2, random_rect.centery - t.get_height() // 2)) self.menu_buttons["random"] = random_rect def draw_deck_builder(self, faction_id): from card_game.config import DECK_SIZE from collections import Counter ink_style.blit_paper_background(self.screen) self.deck_builder_faction = faction_id faction = FACTIONS[faction_id] faction_color = FACTION_COLORS[faction_id] title = self.font_lg.render(f"组卡 - {faction['name']}", True, faction_color) self.screen.blit(title, (WINDOW_WIDTH // 2 - title.get_width() // 2, 10)) deck_count = len(self.deck_builder_cards) count_color = SONGHUA_GREEN if deck_count == DECK_SIZE else (TENG_HUANG if deck_count > 0 else INK_WASH_3) count_text = self.font_md.render(f"已选:{deck_count}/{DECK_SIZE}", True, count_color) self.screen.blit(count_text, (WINDOW_WIDTH // 2 - count_text.get_width() // 2, 42)) # Left: available cards (show stats inline) self.deck_card_rects = [] left_x = 20 left_y = 75 self.screen.blit(self.font_md.render("可用卡牌 (左键添加)", True, INK_WASH_3), (left_x, left_y)) left_y += 25 available = [c for c in CARD_DATABASE.values() if c["faction"] == faction_id or c["faction"] == "ally"] available.sort(key=lambda c: (c["cost"], c["name"])) small_w, small_h = 250, 42 cols = 2 for i, card_data in enumerate(available): row, col = divmod(i, cols) x = left_x + col * (small_w + 6) y = left_y + row * (small_h + 3) if y + small_h > WINDOW_HEIGHT - 60: break cid = card_data["id"] in_deck = self.deck_builder_cards.count(cid) max_copies = RARITY_LIMITS.get(card_data["rarity"], 3) can_add = in_deck < max_copies and deck_count < DECK_SIZE rect = pygame.Rect(x, y, small_w, small_h) if card_data["faction"] == faction_id: bg = tuple(max(0, c - 30) for c in faction_color) else: bg = (100, 90, 55) ink_style.draw_ink_rect(self.screen, rect, bg, alpha=180) border_color = PAPER_WHITE if can_add else INK_WASH_3 pygame.draw.rect(self.screen, border_color, rect, 1, border_radius=3) # Line 1: cost + name + count cost_s = self.font_sm.render(str(card_data["cost"]), True, TENG_HUANG) self.screen.blit(cost_s, (x + 4, y + 3)) name_str = card_data["name"][:6] self.screen.blit(self.font_sm.render(name_str, True, PAPER_WHITE), (x + 22, y + 3)) ct = f"×{in_deck}/{max_copies}" if in_deck > 0 else f"0/{max_copies}" cs = self.font_sm.render(ct, True, count_color if in_deck > 0 else GRAY) self.screen.blit(cs, (x + small_w - cs.get_width() - 4, y + 3)) # Line 2: type + stats if card_data["type"] == "unit": icon = {"infantry": "步", "cavalry": "骑", "chariot": "车", "archer": "弓", "siege": "攻"}.get(card_data.get("unit_type"), "?") stats = f"{icon} 攻{card_data['attack']} 防{card_data['defense']} 血{card_data['max_hp']}" else: stats = f"谋略: {card_data['description'][:12]}" self.screen.blit(self.font_sm.render(stats, True, LIGHT_GRAY), (x + 22, y + 22)) self.deck_card_rects.append((rect, cid)) # Right: current deck self.deck_build_rects = [] right_x = WINDOW_WIDTH // 2 + 20 right_y = 75 self.screen.blit(self.font_md.render("当前牌组 (右键移除)", True, INK_WASH_3), (right_x, right_y)) right_y += 25 deck_counter = Counter(self.deck_builder_cards) deck_items = sorted(deck_counter.items(), key=lambda x: (CARD_DATABASE[x[0]]["cost"], CARD_DATABASE[x[0]]["name"])) for i, (cid, count) in enumerate(deck_items): card_data = CARD_DATABASE[cid] row, col = divmod(i, 2) x = right_x + col * (small_w + 6) y = right_y + row * (small_h + 3) if y + small_h > WINDOW_HEIGHT - 60: break rect = pygame.Rect(x, y, small_w, small_h) if card_data["faction"] == faction_id: bg = tuple(max(0, c - 30) for c in faction_color) else: bg = (100, 90, 55) ink_style.draw_ink_rect(self.screen, rect, bg, alpha=180) pygame.draw.rect(self.screen, GOLD, rect, 1, border_radius=3) self.screen.blit(self.font_sm.render(str(card_data["cost"]), True, TENG_HUANG), (x + 4, y + 3)) name_str = card_data["name"][:6] self.screen.blit(self.font_sm.render(name_str, True, PAPER_WHITE), (x + 22, y + 3)) cs = self.font_sm.render(f"×{count}", True, SONGHUA_GREEN) self.screen.blit(cs, (x + small_w - cs.get_width() - 4, y + 3)) if card_data["type"] == "unit": icon = {"infantry": "步", "cavalry": "骑", "chariot": "车", "archer": "弓", "siege": "攻"}.get(card_data.get("unit_type"), "?") stats = f"{icon} 攻{card_data['attack']} 防{card_data['defense']} 血{card_data['max_hp']}" else: stats = f"谋略: {card_data['description'][:12]}" self.screen.blit(self.font_sm.render(stats, True, LIGHT_GRAY), (x + 22, y + 22)) self.deck_build_rects.append((rect, cid)) # Bottom buttons self.menu_buttons = {} for key, lbl, color, rx in [ ("back", "返回", INK_WASH_3, 20), ("clear", "清空", (120, 50, 40), 160), ("preset", "加载预设", (50, 90, 50), 300), ]: r = pygame.Rect(rx, WINDOW_HEIGHT - 55, 130 if key == "preset" else 120, 40) ink_style.draw_ink_rect(self.screen, r, color, alpha=200) pygame.draw.rect(self.screen, PAPER_WHITE, r, 1, border_radius=5) t = self.font_md.render(lbl, True, PAPER_WHITE) self.screen.blit(t, (r.centerx - t.get_width() // 2, r.centery - t.get_height() // 2)) self.menu_buttons[key] = r confirm_color = (50, 110, 50) if deck_count == DECK_SIZE else INK_WASH_3 confirm_rect = pygame.Rect(WINDOW_WIDTH - 180, WINDOW_HEIGHT - 55, 160, 40) ink_style.draw_ink_rect(self.screen, confirm_rect, confirm_color, alpha=200) pygame.draw.rect(self.screen, PAPER_WHITE if deck_count == DECK_SIZE else GRAY, confirm_rect, 1, border_radius=5) t = self.font_md.render("确认组卡", True, PAPER_WHITE if deck_count == DECK_SIZE else GRAY) self.screen.blit(t, (confirm_rect.centerx - t.get_width() // 2, confirm_rect.centery - t.get_height() // 2)) self.menu_buttons["confirm"] = confirm_rect def get_deck_card_at(self, pos): for rect, cid in self.deck_card_rects: if rect.collidepoint(pos): return cid return None def get_deck_build_card_at(self, pos): for rect, cid in self.deck_build_rects: if rect.collidepoint(pos): return cid return None # --- Game Drawing --- def draw_game(self, battlefield): ink_style.blit_paper_background(self.screen) ink_style.blit_mountains(self.screen) self._draw_enemy_info(battlefield.ai) self._draw_enemy_hand(battlefield.ai) # Zone backgrounds with ink wash ink_style.draw_zone_bg(self.screen, (0, ENEMY_SUPPORT_Y, WINDOW_WIDTH, ZONE_HEIGHT), FIELD_COLOR, INK_WASH_3) ink_style.draw_zone_bg(self.screen, (0, FRONTLINE_Y, WINDOW_WIDTH, ZONE_HEIGHT), FRONTLINE_COLOR, ZHU_HONG) ink_style.draw_zone_bg(self.screen, (0, PLAYER_SUPPORT_Y, WINDOW_WIDTH, ZONE_HEIGHT), FIELD_COLOR, INK_WASH_3) self._draw_zone(battlefield.ai.support_line, ENEMY_SUPPORT_Y, "对方营地", battlefield.ai) self._draw_frontline(battlefield) self._draw_zone(battlefield.player.support_line, PLAYER_SUPPORT_Y, "我方营地", battlefield.player) self._draw_player_hand(battlefield.player, battlefield) self._draw_action_bar(battlefield) self._draw_highlights(battlefield) self.effects.draw(self.screen, self.font_lg) def draw_game_over(self, winner, battlefield): overlay = pygame.Surface((WINDOW_WIDTH, WINDOW_HEIGHT), pygame.SRCALPHA) overlay.fill((20, 15, 10, 150)) self.screen.blit(overlay, (0, 0)) if winner == "player": text = "胜 利 !" color = TENG_HUANG else: text = "败 北 ..." color = ZHU_HONG # Draw result as seal stamp seal_w, seal_h = 280, 70 seal_x = WINDOW_WIDTH // 2 - seal_w // 2 seal_y = 250 ink_style.draw_seal_stamp(self.screen, (seal_x, seal_y, seal_w, seal_h), text, self.font_xl) p = battlefield.player for i, s in enumerate([f"回合数:{battlefield.turn_number}", f"都城剩余HP:{max(0, p.capital_hp)}"]): surf = self.font_md.render(s, True, PAPER_WHITE) self.screen.blit(surf, (WINDOW_WIDTH // 2 - surf.get_width() // 2, 340 + i * 30)) self.menu_buttons = {} restart_rect = pygame.Rect(WINDOW_WIDTH // 2 - 150, 430, 130, 45) menu_rect = pygame.Rect(WINDOW_WIDTH // 2 + 20, 430, 130, 45) for rect, label in [(restart_rect, "再来一局"), (menu_rect, "返回主菜单")]: ink_style.draw_ink_rect(self.screen, rect, INK_WASH_3, alpha=200) pygame.draw.rect(self.screen, PAPER_WHITE, rect, 2, border_radius=5) t = self.font_md.render(label, True, PAPER_WHITE) self.screen.blit(t, (rect.centerx - t.get_width() // 2, rect.centery - t.get_height() // 2)) self.menu_buttons["restart"] = restart_rect self.menu_buttons["menu"] = menu_rect # --- Zone Drawing --- def _draw_zone(self, slots, y, label, player): if label: lbl = self.font_sm.render(label, True, INK_WASH_3) self.screen.blit(lbl, (10, y + 5)) self._draw_capital(player, y) is_player_zone = (not player.is_ai) for i, unit in enumerate(slots): if unit is None: continue ux = _support_slot_x(i) uy = y + (ZONE_HEIGHT - FIELD_CARD_HEIGHT) // 2 self._draw_field_unit(unit, ux, uy, is_player_zone, player) def _draw_frontline(self, battlefield): y = FRONTLINE_Y half = ZONE_HEIGHT // 2 # Center divider line pygame.draw.line(self.screen, INK_WASH_3, (0, y + half), (WINDOW_WIDTH, y + half), 1) lbl = self.font_sm.render("前 线", True, (ZHU_HONG[0], ZHU_HONG[1], ZHU_HONG[2])) self.screen.blit(lbl, (WINDOW_WIDTH // 2 - lbl.get_width() // 2, y + 3)) n_fl = MAX_FRONTLINE_SLOTS for i, unit in enumerate(battlefield.ai.frontline): if unit is None: continue ux = _frontline_slot_x(i, n_fl) uy = y + (half - FIELD_CARD_HEIGHT) // 2 self._draw_field_unit(unit, ux, uy, False, battlefield.ai) for i, unit in enumerate(battlefield.player.frontline): if unit is None: continue ux = _frontline_slot_x(i, n_fl) uy = y + half + (half - FIELD_CARD_HEIGHT) // 2 self._draw_field_unit(unit, ux, uy, True, battlefield.player) def _draw_capital(self, player, zone_y): cx = WINDOW_WIDTH // 2 - CAPITAL_WIDTH // 2 cy = zone_y + (ZONE_HEIGHT - CAPITAL_HEIGHT) // 2 color = FACTION_COLORS[player.faction_id] rect = pygame.Rect(cx, cy, CAPITAL_WIDTH, CAPITAL_HEIGHT) # Draw as seal stamp ink_style.draw_seal_stamp(self.screen, (cx, cy, CAPITAL_WIDTH, CAPITAL_HEIGHT), player.faction["name"], self.font_sm, color=color) # HP text hp_text = self.font_md.render(f"{max(0, player.capital_hp)}/{player.max_capital_hp}", True, PAPER_WHITE) self.screen.blit(hp_text, (cx + CAPITAL_WIDTH // 2 - hp_text.get_width() // 2, cy + 30)) # HP bar bar_w = CAPITAL_WIDTH - 10 bar_h = 6 bar_x = cx + 5 bar_y = cy + CAPITAL_HEIGHT - 12 hp_ratio = max(0, player.capital_hp / player.max_capital_hp) ink_style.draw_ink_hp_bar(self.screen, bar_x, bar_y, bar_w, bar_h, hp_ratio) def _draw_field_unit(self, unit, x, y, is_player, player): w, h = FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT color = unit.get_color() dark_color = tuple(max(0, c - 30) for c in color) rect = pygame.Rect(x, y, w, h) # Ink wash card background ink_style.draw_ink_rect(self.screen, rect, dark_color, alpha=220) # Border if unit is self.selected_unit: pygame.draw.rect(self.screen, TENG_HUANG, rect, 2, border_radius=4) elif unit.can_attack and is_player and not unit.has_attacked: pygame.draw.rect(self.screen, SONGHUA_GREEN, rect, 1, border_radius=4) else: pygame.draw.rect(self.screen, self._rarity_border_color(unit.rarity), rect, 1, border_radius=4) # Name (3 chars) name = unit.name[:3] name_surf = self.font_sm.render(name, True, PAPER_WHITE) self.screen.blit(name_surf, (x + w // 2 - name_surf.get_width() // 2, y + 3)) # Unit type icon icon_char = {"infantry": "步", "cavalry": "骑", "chariot": "车", "archer": "弓", "siege": "攻"}.get(unit.unit_type, "?") self.screen.blit(self.font_sm.render(icon_char, True, TENG_HUANG), (x + 3, y + 3)) # Operation cost op_cost = unit.op_cost owner = player if owner.faction_id == "yan" and unit.unit_type == "cavalry": op_cost = max(0, op_cost - 1) oc_surf = self.font_sm.render(str(op_cost), True, TENG_HUANG) pygame.draw.circle(self.screen, (40, 35, 30), (x + w - 12, y + 12), 9) self.screen.blit(oc_surf, (x + w - 12 - oc_surf.get_width() // 2, y + 12 - oc_surf.get_height() // 2)) # Attack / Defense atk = unit.get_effective_attack() dfn = unit.get_effective_defense() self.screen.blit(self.font_sm.render(str(atk), True, (200, 80, 60)), (x + 5, y + h - 22)) self.screen.blit(self.font_sm.render(str(dfn), True, (60, 80, 160)), (x + w - 20, y + h - 22)) # HP bar if unit.max_hp > 0: bar_w = w - 8 bar_x = x + 4 bar_y = y + h - 6 hp_ratio = unit.current_hp / unit.max_hp ink_style.draw_ink_hp_bar(self.screen, bar_x, bar_y, bar_w, 4, hp_ratio) def _draw_player_hand(self, player, battlefield): n = len(player.hand) if n == 0: return start_x = (WINDOW_WIDTH - n * HAND_CARD_SPACING) // 2 for i, card in enumerate(player.hand): x = start_x + i * HAND_CARD_SPACING y = HAND_Y if card is self.hover_card: y -= 15 if card is self.selected_card: y -= 25 self._draw_hand_card(card, x, y, player.can_play_card(card)) def _draw_hand_card(self, card, x, y, playable): w, h = CARD_WIDTH, CARD_HEIGHT color = card.get_color() dark_color = tuple(max(0, c - 30) for c in color) rect = pygame.Rect(x, y, w, h) # Ink wash background ink_style.draw_ink_rect(self.screen, rect, dark_color, alpha=220) if card is self.selected_card: pygame.draw.rect(self.screen, TENG_HUANG, rect, 2, border_radius=5) elif not playable: pygame.draw.rect(self.screen, INK_WASH_3, rect, 1, border_radius=5) else: pygame.draw.rect(self.screen, self._rarity_border_color(card.rarity), rect, 1, border_radius=5) # Cost circle top-left cost_surf = self.font_md.render(str(card.cost), True, TENG_HUANG) pygame.draw.circle(self.screen, (35, 30, 25), (x + 14, y + 14), 12) self.screen.blit(cost_surf, (x + 14 - cost_surf.get_width() // 2, y + 14 - cost_surf.get_height() // 2)) # Op cost top-right op_surf = self.font_sm.render(f"行{card.op_cost}", True, TENG_HUANG if playable else INK_WASH_3) self.screen.blit(op_surf, (x + w - op_surf.get_width() - 3, y + 3)) # Name name = card.name[:4] name_surf = self.font_sm.render(name, True, PAPER_WHITE if playable else GRAY) self.screen.blit(name_surf, (x + w // 2 - name_surf.get_width() // 2, y + 28)) if card.card_type == "unit": icon = {"infantry": "步", "cavalry": "骑", "chariot": "车", "archer": "弓", "siege": "攻"}.get(card.unit_type, "?") self.screen.blit(self.font_sm.render(icon, True, TENG_HUANG), (x + 3, y + h - 40)) self.screen.blit(self.font_md.render(str(card.attack), True, (200, 80, 60)), (x + 5, y + h - 22)) self.screen.blit(self.font_md.render(str(card.defense), True, (60, 80, 160)), (x + w - 15, y + h - 22)) else: self.screen.blit(self.font_sm.render("谋略", True, ORANGE), (x + w // 2 - 12, y + h - 40)) desc = card.description[:8] desc_surf = self.font_sm.render(desc, True, LIGHT_GRAY if playable else DARK_GRAY) self.screen.blit(desc_surf, (x + w // 2 - desc_surf.get_width() // 2, y + h - 8)) def _draw_enemy_info(self, ai): # Semi-transparent bar s = pygame.Surface((WINDOW_WIDTH, ENEMY_INFO_HEIGHT), pygame.SRCALPHA) s.fill((*INK_WASH_5[:3], 160)) self.screen.blit(s, (0, 0)) color = FACTION_COLORS[ai.faction_id] self.screen.blit(self.font_md.render(f"{ai.faction['name']} (AI)", True, color), (10, 10)) self.screen.blit(self.font_md.render(f"都城:{max(0, ai.capital_hp)}/{ai.max_capital_hp}", True, PAPER_WHITE), (200, 10)) self.screen.blit(self.font_md.render(f"粮草:{ai.provisions}", True, TENG_HUANG), (400, 10)) self.screen.blit(self.font_md.render(f"牌库:{ai.deck.remaining()}", True, INK_WASH_2), (550, 10)) def _draw_enemy_hand(self, ai): y = ENEMY_INFO_HEIGHT n = len(ai.hand) if n == 0: return card_w = 30 start_x = (WINDOW_WIDTH - n * (card_w + 5)) // 2 for i in range(n): x = start_x + i * (card_w + 5) rect = pygame.Rect(x, y, card_w, 40) # Card back as ink wash ink_style.draw_ink_rect(self.screen, rect, JIANG_BROWN, alpha=180) pygame.draw.rect(self.screen, INK_WASH_3, rect, 1, border_radius=3) def _draw_action_bar(self, battlefield): s = pygame.Surface((WINDOW_WIDTH, ACTION_BAR_HEIGHT), pygame.SRCALPHA) s.fill((*INK_WASH_5[:3], 180)) self.screen.blit(s, (0, ACTION_BAR_Y)) p = battlefield.player info_parts = [ (f"{p.faction['name']}", FACTION_COLORS[p.faction_id]), (f"粮草:{p.provisions}/{p.max_provisions}", TENG_HUANG), (f"回合:{battlefield.turn_number}", PAPER_WHITE), (f"牌库:{p.deck.remaining()}", INK_WASH_2), ] x = 10 for text, color in info_parts: surf = self.font_md.render(text, True, color) self.screen.blit(surf, (x, ACTION_BAR_Y + 15)) x += surf.get_width() + 30 passive = self.font_sm.render(p.faction["passive_name"], True, TENG_HUANG) self.screen.blit(passive, (x + 10, ACTION_BAR_Y + 17)) # End turn button as seal stamp btn_w, btn_h = 120, 36 btn_x = WINDOW_WIDTH - btn_w - 15 btn_y = ACTION_BAR_Y + (ACTION_BAR_HEIGHT - btn_h) // 2 self.end_turn_btn = pygame.Rect(btn_x, btn_y, btn_w, btn_h) if battlefield.current_turn == "player": ink_style.draw_seal_stamp(self.screen, (btn_x, btn_y, btn_w, btn_h), "结束回合", self.font_md) else: ink_style.draw_ink_rect(self.screen, self.end_turn_btn, INK_WASH_3, alpha=180) pygame.draw.rect(self.screen, GRAY, self.end_turn_btn, 1, border_radius=5) t = self.font_md.render("结束回合", True, GRAY) self.screen.blit(t, (self.end_turn_btn.centerx - t.get_width() // 2, self.end_turn_btn.centery - t.get_height() // 2)) def _draw_highlights(self, battlefield): if not self.valid_targets: return for target in self.valid_targets: if isinstance(target, tuple) and target[0] == "capital": player = target[1] cx = WINDOW_WIDTH // 2 - CAPITAL_WIDTH // 2 cy = ENEMY_SUPPORT_Y + (ZONE_HEIGHT - CAPITAL_HEIGHT) // 2 s = pygame.Surface((CAPITAL_WIDTH, CAPITAL_HEIGHT), pygame.SRCALPHA) s.fill((100, 200, 100, 50)) self.screen.blit(s, (cx, cy)) elif hasattr(target, 'zone') and hasattr(target, 'slot'): unit = target is_ai = battlefield._get_unit_owner(unit) == battlefield.ai if unit.zone == "support": zone_y = ENEMY_SUPPORT_Y if is_ai else PLAYER_SUPPORT_Y zone_h = ZONE_HEIGHT ux = _support_slot_x(unit.slot) uy = zone_y + (zone_h - FIELD_CARD_HEIGHT) // 2 else: half = ZONE_HEIGHT // 2 n_fl = MAX_FRONTLINE_SLOTS zone_y = FRONTLINE_Y if is_ai else (FRONTLINE_Y + half) zone_h = half ux = _frontline_slot_x(unit.slot, n_fl) uy = zone_y + (zone_h - FIELD_CARD_HEIGHT) // 2 s = pygame.Surface((FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT), pygame.SRCALPHA) s.fill((100, 200, 100, 50)) self.screen.blit(s, (ux, uy)) # Deploy highlights if self.target_mode == "deploy": for i, slot in enumerate(battlefield.player.support_line): if slot is None: sx = _support_slot_x(i) sy = PLAYER_SUPPORT_Y + (ZONE_HEIGHT - FIELD_CARD_HEIGHT) // 2 s = pygame.Surface((FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT), pygame.SRCALPHA) s.fill((100, 200, 100, 50)) self.screen.blit(s, (sx, sy)) # Move highlights if self.target_mode == "move": half = ZONE_HEIGHT // 2 n_fl = MAX_FRONTLINE_SLOTS for i, slot in enumerate(battlefield.player.frontline): if slot is None: sx = _frontline_slot_x(i, n_fl) sy = FRONTLINE_Y + half + (half - FIELD_CARD_HEIGHT) // 2 s = pygame.Surface((FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT), pygame.SRCALPHA) s.fill((100, 180, 220, 50)) self.screen.blit(s, (sx, sy)) # --- Hit Testing --- def get_hand_card_at(self, pos, player): n = len(player.hand) if n == 0: return None start_x = (WINDOW_WIDTH - n * HAND_CARD_SPACING) // 2 mx, my = pos for i, card in enumerate(player.hand): x = start_x + i * HAND_CARD_SPACING y = HAND_Y if card is self.hover_card: y -= 15 if card is self.selected_card: y -= 25 rect = pygame.Rect(x, y, CARD_WIDTH, CARD_HEIGHT) if rect.collidepoint(mx, my): return card return None def get_field_unit_at(self, pos, battlefield): mx, my = pos half = ZONE_HEIGHT // 2 n_fl = MAX_FRONTLINE_SLOTS zones = [ (battlefield.player.support_line, PLAYER_SUPPORT_Y, ZONE_HEIGHT, battlefield.player, "support"), (battlefield.player.frontline, FRONTLINE_Y + half, half, battlefield.player, "frontline"), (battlefield.ai.support_line, ENEMY_SUPPORT_Y, ZONE_HEIGHT, battlefield.ai, "support"), (battlefield.ai.frontline, FRONTLINE_Y, half, battlefield.ai, "frontline"), ] for slots, zone_y, zone_h, owner, zone_type in zones: for i, unit in enumerate(slots): if unit is None: continue if zone_type == "support": ux = _support_slot_x(i) else: ux = _frontline_slot_x(i, n_fl) uy = zone_y + (zone_h - FIELD_CARD_HEIGHT) // 2 rect = pygame.Rect(ux, uy, FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT) if rect.collidepoint(mx, my): return unit, owner return None, None def get_support_slot_at(self, pos, player): mx, my = pos zone_y = PLAYER_SUPPORT_Y for i, slot in enumerate(player.support_line): if slot is not None: continue sx = _support_slot_x(i) sy = zone_y + (ZONE_HEIGHT - FIELD_CARD_HEIGHT) // 2 rect = pygame.Rect(sx, sy, FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT) if rect.collidepoint(mx, my): return i return -1 def get_frontline_slot_at(self, pos, player): mx, my = pos half = ZONE_HEIGHT // 2 n_fl = MAX_FRONTLINE_SLOTS zone_y = FRONTLINE_Y + half for i, slot in enumerate(player.frontline): sx = _frontline_slot_x(i, n_fl) sy = zone_y + (half - FIELD_CARD_HEIGHT) // 2 rect = pygame.Rect(sx, sy, FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT) if rect.collidepoint(mx, my): return i return -1 def get_enemy_capital_at(self, pos, ai): mx, my = pos cx = WINDOW_WIDTH // 2 - CAPITAL_WIDTH // 2 cy = ENEMY_SUPPORT_Y + (ZONE_HEIGHT - CAPITAL_HEIGHT) // 2 rect = pygame.Rect(cx, cy, CAPITAL_WIDTH, CAPITAL_HEIGHT) return rect.collidepoint(mx, my) def get_faction_at(self, pos): for rect, fid in self.faction_buttons: if rect.collidepoint(pos): return fid return None # --- Helpers --- def _rarity_border_color(self, rarity): if rarity == "legendary": return ZHU_HONG elif rarity == "rare": return TENG_HUANG return SILVER def clear_selection(self): self.selected_card = None self.selected_unit = None self.valid_targets = [] self.target_mode = None