游戏可以运行

This commit is contained in:
2026-05-24 08:10:22 +08:00
commit 035b2f7af9
28 changed files with 4222 additions and 0 deletions

776
card_game/ui.py Normal file
View File

@@ -0,0 +1,776 @@
"""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
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
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"] in ("neutral", "ally")]
available.sort(key=lambda c: (c["cost"], c["name"]))
small_w, small_h = 160, 32
cols = 4
for i, card_data in enumerate(available):
row, col = divmod(i, cols)
x = left_x + col * (small_w + 8)
y = left_y + row * (small_h + 4)
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"] not in ("neutral", "ally"):
bg = tuple(max(0, c - 30) for c in faction_color)
elif card_data["faction"] == "ally":
bg = (100, 90, 55)
else:
bg = (80, 75, 65)
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)
cost_s = self.font_sm.render(str(card_data["cost"]), True, TENG_HUANG)
self.screen.blit(cost_s, (x + 4, y + 8))
icon = {"unit": "", "order": ""}.get(card_data["type"], "?")
ally_tag = card_data.get("ally_state", "")
if card_data["faction"] == "ally" and ally_tag:
icon = ally_tag
self.screen.blit(self.font_sm.render(icon, True, TENG_HUANG), (x + 22, y + 8))
self.screen.blit(self.font_sm.render(card_data["name"][:4], True, PAPER_WHITE), (x + 40, y + 8))
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 + 8))
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, 3)
x = right_x + col * (small_w + 8)
y = right_y + row * (small_h + 4)
if y + small_h > WINDOW_HEIGHT - 60:
break
rect = pygame.Rect(x, y, small_w, small_h)
if card_data["faction"] not in ("neutral", "ally"):
bg = tuple(max(0, c - 30) for c in faction_color)
elif card_data["faction"] == "ally":
bg = (100, 90, 55)
else:
bg = (80, 75, 65)
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 + 8))
icon = {"unit": "", "order": ""}.get(card_data["type"], "?")
ally_tag = card_data.get("ally_state", "")
if card_data["faction"] == "ally" and ally_tag:
icon = ally_tag
self.screen.blit(self.font_sm.render(icon, True, TENG_HUANG), (x + 22, y + 8))
self.screen.blit(self.font_sm.render(card_data["name"][:4], True, PAPER_WHITE), (x + 40, y + 8))
cs = self.font_sm.render(f"×{count}", True, SONGHUA_GREEN)
self.screen.blit(cs, (x + small_w - cs.get_width() - 4, y + 8))
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