游戏可以运行
This commit is contained in:
776
card_game/ui.py
Normal file
776
card_game/ui.py
Normal 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
|
||||
Reference in New Issue
Block a user