791 lines
34 KiB
Python
791 lines
34 KiB
Python
"""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
|