Files
card/card_game/ui.py
2026-05-24 09:05:39 +08:00

791 lines
34 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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": "",
"archer": ""}.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": "",
"archer": ""}.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": "",
"archer": ""}.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": "",
"archer": ""}.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