游戏可以运行
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# 战国卡牌 - 水墨风云
|
||||
|
||||
春秋战国主题卡牌对战游戏,基于 pygame 构建,国风水墨视觉风格。
|
||||
|
||||
## 运行方式
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
python -m card_game.main
|
||||
```
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
card_game/
|
||||
config.py — 常量、水墨色板、卡牌数据库(80+张)、阵营定义、预设卡组
|
||||
card.py — Card 类(单位/指令卡)
|
||||
deck.py — Deck 类(组牌、洗牌、抽牌)
|
||||
factions.py — 阵营被动技能系统
|
||||
utils.py — 工具函数
|
||||
player.py — Player 类(手牌、营地、前线、HP、粮草)
|
||||
ink_style.py — 水墨风格绘制原语(宣纸纹理、毛笔笔触、印章、卷轴等)
|
||||
effects.py — 视觉特效(浮动文字、攻击线、墨花飞溅)
|
||||
battlefield.py — 核心游戏逻辑(战斗、指令、回合管理)
|
||||
ai.py — AI 决策(规则驱动)
|
||||
ui.py — 所有 UI 渲染(使用 ink_style 水墨风格)
|
||||
main.py — 主循环、状态机
|
||||
```
|
||||
|
||||
## 游戏规则
|
||||
|
||||
- 7 个阵营(战国七雄):秦、齐、楚、燕、韩、赵、魏
|
||||
- 30 张牌组,起始 4 张手牌,最多 8 张
|
||||
- 粮草系统:每回合获得 turn+1 粮草(上限 10)
|
||||
- 战场:营地(5槽+都城) → 前线(5槽) → 对手
|
||||
- 胜利条件:将对方都城 HP 降为 0
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+148
@@ -0,0 +1,148 @@
|
||||
"""AI player: simple rule-based decision making."""
|
||||
|
||||
from card_game.config import CARD_DATABASE
|
||||
|
||||
|
||||
class AIPlayer:
|
||||
def __init__(self, battlefield):
|
||||
self.battlefield = battlefield
|
||||
|
||||
def execute_turn(self):
|
||||
actions = []
|
||||
ai = self.battlefield.ai
|
||||
|
||||
actions.extend(self._play_untargeted_orders(ai))
|
||||
actions.extend(self._deploy_units(ai))
|
||||
actions.extend(self._play_targeted_orders(ai))
|
||||
actions.extend(self._move_units(ai))
|
||||
actions.extend(self._attack(ai))
|
||||
|
||||
return actions
|
||||
|
||||
def _play_untargeted_orders(self, ai):
|
||||
actions = []
|
||||
for card in ai.hand[:]:
|
||||
if card.card_type != "order":
|
||||
continue
|
||||
if card.cost > ai.provisions:
|
||||
continue
|
||||
if not self.battlefield.needs_target(card):
|
||||
self.battlefield.apply_order_effect(card, ai)
|
||||
ai.play_order(card)
|
||||
actions.append(("order", card))
|
||||
return actions
|
||||
|
||||
def _deploy_units(self, ai):
|
||||
actions = []
|
||||
units = [c for c in ai.hand if c.card_type == "unit" and ai.can_play_card(c)]
|
||||
units.sort(key=lambda c: c.cost)
|
||||
for card in units:
|
||||
if not ai.can_play_card(card):
|
||||
continue
|
||||
from card_game.factions import apply_faction_passive
|
||||
apply_faction_passive(card, ai.faction_id)
|
||||
slot = ai.deploy_unit(card)
|
||||
if slot >= 0:
|
||||
actions.append(("deploy", card, slot))
|
||||
self._handle_deploy(card, ai)
|
||||
return actions
|
||||
|
||||
def _play_targeted_orders(self, ai):
|
||||
actions = []
|
||||
for card in ai.hand[:]:
|
||||
if card.card_type != "order":
|
||||
continue
|
||||
if card.cost > ai.provisions:
|
||||
continue
|
||||
if not self.battlefield.needs_target(card):
|
||||
continue
|
||||
targets = self.battlefield.get_valid_targets(card, ai)
|
||||
if targets:
|
||||
target = self._pick_best_target(card, targets, ai)
|
||||
if target:
|
||||
self.battlefield.apply_order_effect(card, ai, target)
|
||||
ai.play_order(card)
|
||||
actions.append(("order_targeted", card, target))
|
||||
return actions
|
||||
|
||||
def _move_units(self, ai):
|
||||
actions = []
|
||||
if not self.battlefield.can_move_to_frontline(ai):
|
||||
return actions
|
||||
for unit in ai.get_support_units():
|
||||
op_cost = ai._get_op_cost(unit)
|
||||
if ai.provisions >= op_cost:
|
||||
slot = ai.move_to_frontline(unit)
|
||||
if slot >= 0:
|
||||
if self.battlefield.frontline_controller is None:
|
||||
self.battlefield.claim_frontline(ai)
|
||||
actions.append(("move", unit, slot))
|
||||
return actions
|
||||
|
||||
def _attack(self, ai):
|
||||
actions = []
|
||||
player = self.battlefield.player
|
||||
for unit in ai.get_frontline_units():
|
||||
if not unit.can_attack or unit.has_attacked:
|
||||
continue
|
||||
enemy_units = player.get_frontline_units()
|
||||
if enemy_units:
|
||||
killable = [u for u in enemy_units if u.current_hp <= unit.get_effective_attack()]
|
||||
if killable:
|
||||
target = min(killable, key=lambda u: u.current_hp)
|
||||
else:
|
||||
target = max(enemy_units, key=lambda u: u.get_effective_attack())
|
||||
dead = self.battlefield.resolve_attack(unit, target)
|
||||
actions.append(("attack_unit", unit, target))
|
||||
else:
|
||||
self.battlefield.attack_capital(unit)
|
||||
actions.append(("attack_capital", unit))
|
||||
|
||||
# Ranged units in support attack
|
||||
for unit in ai.get_support_units():
|
||||
if not unit.can_attack or unit.has_attacked or not unit.is_ranged():
|
||||
continue
|
||||
enemy_units = player.get_frontline_units()
|
||||
if enemy_units:
|
||||
killable = [u for u in enemy_units if u.current_hp <= unit.get_effective_attack()]
|
||||
target = min(killable, key=lambda u: u.current_hp) if killable else min(enemy_units, key=lambda u: u.current_hp)
|
||||
self.battlefield.resolve_attack(unit, target)
|
||||
actions.append(("attack_unit", unit, target))
|
||||
else:
|
||||
self.battlefield.attack_capital(unit)
|
||||
actions.append(("attack_capital", unit))
|
||||
|
||||
return actions
|
||||
|
||||
def _handle_deploy(self, card, ai):
|
||||
for ability in card.abilities:
|
||||
if ability.startswith("draw_on_deploy:"):
|
||||
count = int(ability.split(":")[1])
|
||||
for _ in range(count):
|
||||
ai.draw_card()
|
||||
elif ability.startswith("damage_on_deploy:"):
|
||||
dmg = int(ability.split(":")[1])
|
||||
opponent = self.battlefield.get_opponent(ai)
|
||||
targets = opponent.get_all_units()
|
||||
if targets:
|
||||
import random
|
||||
target = random.choice(targets)
|
||||
target.take_damage(dmg)
|
||||
|
||||
def _pick_best_target(self, card, targets, ai):
|
||||
if not targets:
|
||||
return None
|
||||
if card.effect_type == "destroy_damaged":
|
||||
dmg_targets = [t for t in targets if hasattr(t, 'current_hp') and t.current_hp < t.max_hp]
|
||||
if dmg_targets:
|
||||
return min(dmg_targets, key=lambda u: u.current_hp)
|
||||
return None
|
||||
if card.effect_type == "bounce":
|
||||
return max(targets, key=lambda u: u.get_effective_attack() if hasattr(u, 'get_effective_attack') else 0)
|
||||
if card.effect_type in ("buff_single", "move_to_front"):
|
||||
return max(targets, key=lambda u: u.get_effective_attack() if hasattr(u, 'get_effective_attack') else 0)
|
||||
if card.effect_type == "damage":
|
||||
unit_targets = [t for t in targets if hasattr(t, 'get_effective_attack')]
|
||||
if unit_targets:
|
||||
return max(unit_targets, key=lambda u: u.get_effective_attack())
|
||||
return targets[0] if targets else None
|
||||
@@ -0,0 +1,337 @@
|
||||
"""Battlefield: core game logic — combat, orders, turn management."""
|
||||
|
||||
from card_game.player import Player
|
||||
from card_game.factions import apply_faction_passive
|
||||
|
||||
|
||||
class Battlefield:
|
||||
def __init__(self, player_faction, ai_faction):
|
||||
self.player = Player(player_faction, is_ai=False)
|
||||
self.ai = Player(ai_faction, is_ai=True)
|
||||
self.turn_number = 0
|
||||
self.current_turn = "player"
|
||||
self.game_over = False
|
||||
self.winner = None
|
||||
self.log = []
|
||||
self.pending_order = None
|
||||
self.effects = []
|
||||
self.frontline_controller = None
|
||||
|
||||
def start_game(self):
|
||||
self.player.start_game()
|
||||
self.ai.start_game()
|
||||
self.turn_number = 1
|
||||
self.current_turn = "player"
|
||||
self.frontline_controller = None
|
||||
self.player.start_turn(self.turn_number)
|
||||
|
||||
def get_active_player(self):
|
||||
return self.player if self.current_turn == "player" else self.ai
|
||||
|
||||
def get_opponent(self, player):
|
||||
return self.ai if player == self.player else self.player
|
||||
|
||||
# --- Frontline Control ---
|
||||
|
||||
def can_move_to_frontline(self, player):
|
||||
if self.frontline_controller is None:
|
||||
return True
|
||||
if player == self.player and self.frontline_controller == "player":
|
||||
return True
|
||||
if player == self.ai and self.frontline_controller == "ai":
|
||||
return True
|
||||
return False
|
||||
|
||||
def claim_frontline(self, player):
|
||||
if player == self.player:
|
||||
self.frontline_controller = "player"
|
||||
else:
|
||||
self.frontline_controller = "ai"
|
||||
|
||||
def _update_frontline_control(self):
|
||||
if self.frontline_controller == "player":
|
||||
if not self.player.get_frontline_units():
|
||||
self.frontline_controller = None
|
||||
elif self.frontline_controller == "ai":
|
||||
if not self.ai.get_frontline_units():
|
||||
self.frontline_controller = None
|
||||
|
||||
# --- Turn Management ---
|
||||
|
||||
def end_player_turn(self):
|
||||
if self.current_turn != "player" or self.game_over:
|
||||
return
|
||||
self.current_turn = "ai"
|
||||
|
||||
def start_ai_turn(self):
|
||||
self.turn_number += 1
|
||||
self.ai.start_turn(self.turn_number)
|
||||
self.current_turn = "ai"
|
||||
|
||||
def end_ai_turn(self):
|
||||
if self.game_over:
|
||||
return
|
||||
self.current_turn = "player"
|
||||
self.turn_number += 1
|
||||
self.player.start_turn(self.turn_number)
|
||||
|
||||
# --- Combat ---
|
||||
|
||||
def resolve_attack(self, attacker, defender):
|
||||
dead = []
|
||||
atk = attacker.get_effective_attack()
|
||||
|
||||
if "siege" in attacker.abilities and defender == "capital":
|
||||
atk *= 2
|
||||
|
||||
owner = self._get_unit_owner(attacker)
|
||||
if (owner and owner.faction_id == "han"
|
||||
and attacker.unit_type == "archer" and defender == "capital"):
|
||||
atk += 1
|
||||
|
||||
if isinstance(defender, str) and defender == "capital":
|
||||
target_player = self.get_opponent(owner)
|
||||
target_player.capital_hp -= atk
|
||||
self._add_effect("damage", target_player, "capital", atk)
|
||||
self._check_game_over()
|
||||
attacker.has_attacked = True
|
||||
attacker.can_attack = False
|
||||
return dead
|
||||
|
||||
defender.take_damage(atk)
|
||||
self._add_effect("damage", self._get_unit_owner(defender), defender, atk)
|
||||
|
||||
if not defender.is_alive():
|
||||
dead.append(defender)
|
||||
if owner and owner.faction_id == "qin":
|
||||
owner.provisions += 1
|
||||
|
||||
if "no_retaliation" not in attacker.abilities:
|
||||
if not (attacker.is_ranged() and attacker.zone == "support"):
|
||||
retal = defender.get_effective_defense() if defender.is_alive() else 0
|
||||
if retal > 0:
|
||||
attacker.take_damage(retal)
|
||||
self._add_effect("damage", self._get_unit_owner(attacker), attacker, retal)
|
||||
if not attacker.is_alive():
|
||||
dead.append(attacker)
|
||||
|
||||
attacker.has_attacked = True
|
||||
attacker.can_attack = False
|
||||
self._cleanup_dead()
|
||||
self._check_game_over()
|
||||
return dead
|
||||
|
||||
def attack_capital(self, attacker):
|
||||
owner = self._get_unit_owner(attacker)
|
||||
if not owner:
|
||||
return
|
||||
self.resolve_attack(attacker, "capital")
|
||||
|
||||
# --- Order Effects ---
|
||||
|
||||
def apply_order_effect(self, card, caster, target=None):
|
||||
etype = card.effect_type
|
||||
params = card.effect_params
|
||||
opponent = self.get_opponent(caster)
|
||||
|
||||
if etype == "damage":
|
||||
dmg = params["damage"]
|
||||
if target and hasattr(target, 'take_damage'):
|
||||
target.take_damage(dmg)
|
||||
self._add_effect("damage", self._get_unit_owner(target), target, dmg)
|
||||
self._cleanup_dead()
|
||||
return True
|
||||
|
||||
elif etype == "damage_hq":
|
||||
dmg = params["damage"]
|
||||
opponent.capital_hp -= dmg
|
||||
self._add_effect("damage", opponent, "capital", dmg)
|
||||
self._check_game_over()
|
||||
return True
|
||||
|
||||
elif etype == "damage_all_front":
|
||||
dmg = params["damage"]
|
||||
tgt = params.get("target", "enemy")
|
||||
if tgt == "enemy":
|
||||
for u in opponent.get_frontline_units():
|
||||
u.take_damage(dmg)
|
||||
self._add_effect("damage", opponent, u, dmg)
|
||||
else:
|
||||
for u in caster.get_frontline_units():
|
||||
u.take_damage(dmg)
|
||||
self._add_effect("damage", caster, u, dmg)
|
||||
self._cleanup_dead()
|
||||
return True
|
||||
|
||||
elif etype == "draw":
|
||||
count = params["count"]
|
||||
for _ in range(count):
|
||||
caster.draw_card()
|
||||
return True
|
||||
|
||||
elif etype == "gain_provisions":
|
||||
caster.provisions += params["amount"]
|
||||
return True
|
||||
|
||||
elif etype == "buff_all":
|
||||
atk_b = params.get("attack_bonus", 0)
|
||||
def_b = params.get("defense_bonus", 0)
|
||||
dur = params.get("duration", 1)
|
||||
for u in caster.get_all_units():
|
||||
u.buffs.append((atk_b, def_b, dur))
|
||||
return True
|
||||
|
||||
elif etype == "buff_type":
|
||||
unit_type = params["unit_type"]
|
||||
atk_b = params.get("attack_bonus", 0)
|
||||
def_b = params.get("defense_bonus", 0)
|
||||
dur = params.get("duration", 1)
|
||||
for u in caster.get_all_units():
|
||||
if u.unit_type == unit_type:
|
||||
u.buffs.append((atk_b, def_b, dur))
|
||||
return True
|
||||
|
||||
elif etype == "buff_single":
|
||||
if target and hasattr(target, 'buffs'):
|
||||
atk_b = params.get("attack_bonus", 0)
|
||||
def_b = params.get("defense_bonus", 0)
|
||||
dur = params.get("duration", 1)
|
||||
target.buffs.append((atk_b, def_b, dur))
|
||||
return True
|
||||
|
||||
elif etype == "heal_hq":
|
||||
amount = params["amount"]
|
||||
caster.capital_hp = min(caster.max_capital_hp, caster.capital_hp + amount)
|
||||
return True
|
||||
|
||||
elif etype == "bounce":
|
||||
if target and hasattr(target, 'zone'):
|
||||
owner = self._get_unit_owner(target)
|
||||
if owner:
|
||||
owner.remove_unit(target)
|
||||
target.zone = "hand"
|
||||
if len(owner.hand) < 8:
|
||||
owner.hand.append(target)
|
||||
return True
|
||||
|
||||
elif etype == "destroy_damaged":
|
||||
if target and hasattr(target, 'is_alive'):
|
||||
if target.current_hp < target.max_hp:
|
||||
owner = self._get_unit_owner(target)
|
||||
if owner:
|
||||
owner.remove_unit(target)
|
||||
return True
|
||||
|
||||
elif etype == "move_to_front":
|
||||
if target and hasattr(target, 'zone') and target.zone == "support":
|
||||
caster.move_to_frontline_free(target)
|
||||
return True
|
||||
|
||||
elif etype == "summon":
|
||||
from card_game.card import Card
|
||||
count = params.get("count", 1)
|
||||
unit_id = params["unit_id"]
|
||||
for _ in range(count):
|
||||
new_card = Card(unit_id)
|
||||
new_card.turn_played = caster.turn_number
|
||||
placed = False
|
||||
for i, s in enumerate(caster.support_line):
|
||||
if s is None:
|
||||
caster.support_line[i] = new_card
|
||||
new_card.zone = "support"
|
||||
new_card.slot = i
|
||||
placed = True
|
||||
break
|
||||
if not placed:
|
||||
break
|
||||
return True
|
||||
|
||||
elif etype == "heal_all":
|
||||
amount = params.get("amount", 1)
|
||||
for u in caster.get_all_units():
|
||||
u.current_hp = min(u.max_hp, u.current_hp + amount)
|
||||
return True
|
||||
|
||||
elif etype == "draw_self_damage":
|
||||
count = params.get("count", 1)
|
||||
self_damage = params.get("self_damage", 0)
|
||||
for _ in range(count):
|
||||
caster.draw_card()
|
||||
if self_damage > 0:
|
||||
caster.capital_hp -= self_damage
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def needs_target(self, card):
|
||||
etype = card.effect_type
|
||||
if etype in ("damage", "bounce", "destroy_damaged", "move_to_front", "buff_single"):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_valid_targets(self, card, caster):
|
||||
etype = card.effect_type
|
||||
opponent = self.get_opponent(caster)
|
||||
params = card.effect_params
|
||||
|
||||
if etype == "damage":
|
||||
tt = params.get("target_type", "any")
|
||||
targets = []
|
||||
if tt in ("any", "enemy_unit"):
|
||||
targets.extend(opponent.get_all_units())
|
||||
if tt == "any":
|
||||
targets.append(("capital", opponent))
|
||||
return targets
|
||||
|
||||
elif etype == "bounce":
|
||||
return opponent.get_frontline_units()
|
||||
|
||||
elif etype == "destroy_damaged":
|
||||
return [u for u in opponent.get_all_units() if u.current_hp < u.max_hp]
|
||||
|
||||
elif etype == "move_to_front":
|
||||
return [u for u in caster.get_support_units()]
|
||||
|
||||
elif etype == "buff_single":
|
||||
return caster.get_all_units()
|
||||
|
||||
return []
|
||||
|
||||
# --- Helpers ---
|
||||
|
||||
def _get_unit_owner(self, unit):
|
||||
for u in self.player.get_all_units():
|
||||
if u is unit:
|
||||
return self.player
|
||||
for u in self.ai.get_all_units():
|
||||
if u is unit:
|
||||
return self.ai
|
||||
return None
|
||||
|
||||
def _cleanup_dead(self):
|
||||
self.player.cleanup_dead()
|
||||
self.ai.cleanup_dead()
|
||||
self._update_frontline_control()
|
||||
|
||||
def _check_game_over(self):
|
||||
if self.player.capital_hp <= 0:
|
||||
self.game_over = True
|
||||
self.winner = "ai"
|
||||
elif self.ai.capital_hp <= 0:
|
||||
self.game_over = True
|
||||
self.winner = "player"
|
||||
|
||||
def _add_effect(self, etype, target_player, target, value):
|
||||
self.effects.append({
|
||||
"type": etype,
|
||||
"target_player": target_player,
|
||||
"target": target,
|
||||
"value": value,
|
||||
"timer": 60,
|
||||
})
|
||||
|
||||
def update_effects(self):
|
||||
for eff in self.effects[:]:
|
||||
eff["timer"] -= 1
|
||||
if eff["timer"] <= 0:
|
||||
self.effects.remove(eff)
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Card class: represents a single card (unit or order)."""
|
||||
|
||||
from card_game.config import CARD_DATABASE, FACTION_COLORS, KEYWORDS
|
||||
|
||||
|
||||
class Card:
|
||||
def __init__(self, card_id):
|
||||
data = CARD_DATABASE[card_id]
|
||||
self.id = data["id"]
|
||||
self.name = data["name"]
|
||||
self.faction = data["faction"]
|
||||
self.card_type = data["type"] # "unit" or "order"
|
||||
self.cost = data["cost"]
|
||||
self.op_cost = data.get("op_cost", 0)
|
||||
self.description = data["description"]
|
||||
self.rarity = data["rarity"]
|
||||
|
||||
# Unit-specific
|
||||
self.unit_type = data.get("unit_type", None)
|
||||
self.attack = data.get("attack", 0)
|
||||
self.defense = data.get("defense", 0)
|
||||
self.max_hp = data.get("max_hp", 0)
|
||||
self.abilities = data.get("abilities", [])
|
||||
|
||||
# Order-specific
|
||||
self.effect_type = data.get("effect_type", None)
|
||||
self.effect_params = data.get("effect_params", {})
|
||||
|
||||
# Runtime state
|
||||
self.current_hp = self.max_hp
|
||||
self.zone = "hand" # "hand", "support", "frontline"
|
||||
self.slot = -1
|
||||
self.can_attack = False
|
||||
self.has_moved = False
|
||||
self.has_attacked = False
|
||||
self.turn_played = 0
|
||||
|
||||
# Buff tracking: list of (attack_bonus, defense_bonus, turns_remaining)
|
||||
self.buffs = []
|
||||
|
||||
def take_damage(self, amount):
|
||||
self.current_hp -= amount
|
||||
|
||||
def is_alive(self):
|
||||
return self.current_hp > 0
|
||||
|
||||
def get_effective_attack(self):
|
||||
bonus = sum(b[0] for b in self.buffs)
|
||||
return self.attack + bonus
|
||||
|
||||
def get_effective_defense(self):
|
||||
bonus = sum(b[1] for b in self.buffs)
|
||||
return self.defense + bonus
|
||||
|
||||
def reset_turn_flags(self):
|
||||
self.can_attack = False
|
||||
self.has_moved = False
|
||||
self.has_attacked = False
|
||||
# Tick buffs
|
||||
self.buffs = [(a, d, t - 1) for a, d, t in self.buffs if t > 1]
|
||||
|
||||
def can_move_and_attack(self):
|
||||
return "charge" in self.abilities
|
||||
|
||||
def is_ranged(self):
|
||||
return "ranged" in self.abilities
|
||||
|
||||
def get_keywords(self):
|
||||
result = []
|
||||
for ability in self.abilities:
|
||||
base = ability.split(":")[0]
|
||||
if base in KEYWORDS:
|
||||
kw = KEYWORDS[base]
|
||||
desc = kw["desc"]
|
||||
if ":" in ability:
|
||||
param = ability.split(":")[1]
|
||||
desc = desc.replace("X", param)
|
||||
result.append((kw["icon"], kw["name"], desc, kw["color"]))
|
||||
return result
|
||||
|
||||
def get_color(self):
|
||||
if self.faction == "neutral":
|
||||
return (110, 105, 95)
|
||||
return FACTION_COLORS.get(self.faction, (128, 128, 128))
|
||||
|
||||
def __repr__(self):
|
||||
return f"Card({self.name})"
|
||||
+1588
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
"""Deck class: build, shuffle, draw."""
|
||||
|
||||
import random
|
||||
from card_game.card import Card
|
||||
|
||||
|
||||
class Deck:
|
||||
def __init__(self):
|
||||
self.cards = []
|
||||
self.draw_pile = []
|
||||
|
||||
def build(self, card_id_list):
|
||||
self.cards = [Card(cid) for cid in card_id_list]
|
||||
self.draw_pile = list(self.cards)
|
||||
random.shuffle(self.draw_pile)
|
||||
|
||||
def draw(self):
|
||||
if not self.draw_pile:
|
||||
return None
|
||||
return self.draw_pile.pop()
|
||||
|
||||
def is_empty(self):
|
||||
return len(self.draw_pile) == 0
|
||||
|
||||
def remaining(self):
|
||||
return len(self.draw_pile)
|
||||
@@ -0,0 +1,116 @@
|
||||
"""Visual effects: floating damage numbers, attack lines, ink splash."""
|
||||
|
||||
import math
|
||||
import random
|
||||
|
||||
import pygame
|
||||
|
||||
from card_game.config import INK_BLACK, ZHU_HONG, TENG_HUANG
|
||||
|
||||
|
||||
class FloatingText:
|
||||
def __init__(self, x, y, text, color=ZHU_HONG, duration=60):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.text = text
|
||||
self.color = color
|
||||
self.timer = duration
|
||||
self.max_timer = duration
|
||||
|
||||
def update(self):
|
||||
self.y -= 0.5
|
||||
self.timer -= 1
|
||||
return self.timer > 0
|
||||
|
||||
def draw(self, surface, font):
|
||||
alpha = min(255, int(255 * self.timer / self.max_timer))
|
||||
text_surf = font.render(self.text, True, self.color)
|
||||
alpha_surf = pygame.Surface(text_surf.get_size(), pygame.SRCALPHA)
|
||||
alpha_surf.fill((255, 255, 255, alpha))
|
||||
text_surf.blit(alpha_surf, (0, 0), special_flags=pygame.BLEND_RGBA_MULT)
|
||||
surface.blit(text_surf, (self.x - text_surf.get_width() // 2, int(self.y)))
|
||||
|
||||
|
||||
class AttackLine:
|
||||
def __init__(self, x1, y1, x2, y2, duration=20):
|
||||
self.x1, self.y1 = x1, y1
|
||||
self.x2, self.y2 = x2, y2
|
||||
self.timer = duration
|
||||
self.max_timer = duration
|
||||
|
||||
def update(self):
|
||||
self.timer -= 1
|
||||
return self.timer > 0
|
||||
|
||||
def draw(self, surface):
|
||||
progress = 1 - self.timer / self.max_timer
|
||||
cx = self.x1 + (self.x2 - self.x1) * min(progress * 2, 1)
|
||||
cy = self.y1 + (self.y2 - self.y1) * min(progress * 2, 1)
|
||||
from card_game.ink_style import draw_brush_stroke
|
||||
draw_brush_stroke(surface, (int(self.x1), int(self.y1)),
|
||||
(int(cx), int(cy)), 3, TENG_HUANG, alpha=200)
|
||||
|
||||
|
||||
class InkSplash:
|
||||
def __init__(self, x, y, duration=30):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.timer = duration
|
||||
self.max_timer = duration
|
||||
rng = random.Random(int(x * 100 + y))
|
||||
self.particles = []
|
||||
for _ in range(8):
|
||||
angle = rng.uniform(0, 2 * math.pi)
|
||||
speed = rng.uniform(1, 4)
|
||||
size = rng.randint(3, 8)
|
||||
self.particles.append((angle, speed, size))
|
||||
|
||||
def update(self):
|
||||
self.timer -= 1
|
||||
return self.timer > 0
|
||||
|
||||
def draw(self, surface):
|
||||
progress = 1 - self.timer / self.max_timer
|
||||
alpha = max(0, int(200 * (1 - progress)))
|
||||
|
||||
for angle, speed, size in self.particles:
|
||||
dist = speed * progress * 30
|
||||
px = int(self.x + dist * math.cos(angle))
|
||||
py = int(self.y + dist * math.sin(angle))
|
||||
current_size = max(1, int(size * (1 - progress * 0.5)))
|
||||
s = pygame.Surface((current_size * 2, current_size * 2), pygame.SRCALPHA)
|
||||
pygame.draw.circle(s, (*INK_BLACK[:3], alpha),
|
||||
(current_size, current_size), current_size)
|
||||
surface.blit(s, (px - current_size, py - current_size))
|
||||
|
||||
|
||||
class EffectManager:
|
||||
def __init__(self):
|
||||
self.texts = []
|
||||
self.lines = []
|
||||
self.splashes = []
|
||||
|
||||
def add_damage(self, x, y, amount):
|
||||
self.texts.append(FloatingText(x, y, f"-{amount}", ZHU_HONG))
|
||||
|
||||
def add_heal(self, x, y, amount):
|
||||
self.texts.append(FloatingText(x, y, f"+{amount}", (70, 140, 80)))
|
||||
|
||||
def add_attack_line(self, x1, y1, x2, y2):
|
||||
self.lines.append(AttackLine(x1, y1, x2, y2))
|
||||
|
||||
def add_ink_splash(self, x, y):
|
||||
self.splashes.append(InkSplash(x, y))
|
||||
|
||||
def update(self):
|
||||
self.texts = [t for t in self.texts if t.update()]
|
||||
self.lines = [l for l in self.lines if l.update()]
|
||||
self.splashes = [s for s in self.splashes if s.update()]
|
||||
|
||||
def draw(self, surface, font):
|
||||
for line in self.lines:
|
||||
line.draw(surface)
|
||||
for splash in self.splashes:
|
||||
splash.draw(surface)
|
||||
for text in self.texts:
|
||||
text.draw(surface, font)
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Faction ability system: applies passive bonuses."""
|
||||
|
||||
|
||||
def get_passive_bonus(faction_id, bonus_type):
|
||||
"""Get passive bonus value for a faction."""
|
||||
bonuses = {
|
||||
"zhao": {"cavalry_attack": 1},
|
||||
"wei": {"infantry_defense": 1},
|
||||
}
|
||||
return bonuses.get(faction_id, {}).get(bonus_type, 0)
|
||||
|
||||
|
||||
def apply_faction_passive(unit, faction_id):
|
||||
"""Apply faction passive to a unit at deploy time."""
|
||||
if faction_id == "zhao" and unit.unit_type == "cavalry":
|
||||
unit.attack += 1
|
||||
elif faction_id == "wei" and unit.unit_type == "infantry":
|
||||
unit.defense += 1
|
||||
unit.max_hp += 1
|
||||
unit.current_hp += 1
|
||||
@@ -0,0 +1,340 @@
|
||||
"""Ink painting style rendering primitives for Chinese aesthetic."""
|
||||
|
||||
import random
|
||||
import math
|
||||
import pygame
|
||||
|
||||
from card_game.config import (
|
||||
WINDOW_WIDTH, WINDOW_HEIGHT,
|
||||
INK_BLACK, PAPER_WHITE, ZHU_HONG, SONGHUA_GREEN, TENG_HUANG, GOLD,
|
||||
BG_COLOR, INK_WASH_1, INK_WASH_2, INK_WASH_3, INK_WASH_4, INK_WASH_5,
|
||||
)
|
||||
|
||||
# Cached surfaces
|
||||
_paper_texture = None
|
||||
_mountain_layers = None
|
||||
|
||||
|
||||
def init_cache():
|
||||
"""Pre-generate cached surfaces. Call once after pygame.init()."""
|
||||
global _paper_texture, _mountain_layers
|
||||
_paper_texture = _generate_paper_texture(WINDOW_WIDTH, WINDOW_HEIGHT)
|
||||
_mountain_layers = _generate_mountain_layers(WINDOW_WIDTH, WINDOW_HEIGHT)
|
||||
|
||||
|
||||
def get_paper_texture():
|
||||
return _paper_texture
|
||||
|
||||
|
||||
def get_mountain_layers():
|
||||
return _mountain_layers
|
||||
|
||||
|
||||
def _generate_paper_texture(w, h):
|
||||
"""Generate a rice paper (宣纸) texture surface."""
|
||||
surf = pygame.Surface((w, h))
|
||||
surf.fill(BG_COLOR)
|
||||
|
||||
# Add subtle noise
|
||||
rng = random.Random(42)
|
||||
for y in range(0, h, 2):
|
||||
for x in range(0, w, 2):
|
||||
noise = rng.gauss(0, 6)
|
||||
r = min(255, max(0, int(BG_COLOR[0] + noise)))
|
||||
g = min(255, max(0, int(BG_COLOR[1] + noise)))
|
||||
b = min(255, max(0, int(BG_COLOR[2] + noise)))
|
||||
surf.set_at((x, y), (r, g, b))
|
||||
if x + 1 < w:
|
||||
surf.set_at((x + 1, y), (r, g, b))
|
||||
if y + 1 < h:
|
||||
surf.set_at((x, y + 1), (r, g, b))
|
||||
if x + 1 < w and y + 1 < h:
|
||||
surf.set_at((x + 1, y + 1), (r, g, b))
|
||||
|
||||
# Add fiber lines
|
||||
for _ in range(40):
|
||||
x1 = rng.randint(0, w)
|
||||
y1 = rng.randint(0, h)
|
||||
length = rng.randint(30, 150)
|
||||
angle = rng.uniform(0, math.pi)
|
||||
color = (rng.randint(200, 220), rng.randint(185, 205), rng.randint(160, 180))
|
||||
points = []
|
||||
for i in range(10):
|
||||
t = i / 9
|
||||
px = int(x1 + length * t * math.cos(angle) + rng.gauss(0, 2))
|
||||
py = int(y1 + length * t * math.sin(angle) + rng.gauss(0, 2))
|
||||
points.append((px, py))
|
||||
if len(points) >= 2:
|
||||
pygame.draw.lines(surf, color, False, points, 1)
|
||||
|
||||
return surf
|
||||
|
||||
|
||||
def _generate_mountain_layers(w, h):
|
||||
"""Generate 3 layers of misty mountain silhouettes."""
|
||||
rng = random.Random(123)
|
||||
layers = []
|
||||
layer_colors = [
|
||||
(INK_WASH_1[0], INK_WASH_1[1], INK_WASH_1[2], 60),
|
||||
(INK_WASH_2[0], INK_WASH_2[1], INK_WASH_2[2], 45),
|
||||
(INK_WASH_3[0], INK_WASH_3[1], INK_WASH_3[2], 30),
|
||||
]
|
||||
base_heights = [h * 0.55, h * 0.62, h * 0.70]
|
||||
|
||||
for i, (color, base_y) in enumerate(zip(layer_colors, base_heights)):
|
||||
surf = pygame.Surface((w, h), pygame.SRCALPHA)
|
||||
points = [(0, h)]
|
||||
x = 0
|
||||
while x <= w:
|
||||
freq1 = rng.uniform(0.003, 0.008)
|
||||
freq2 = rng.uniform(0.01, 0.02)
|
||||
amp1 = rng.uniform(30, 60)
|
||||
amp2 = rng.uniform(10, 25)
|
||||
y = base_y + amp1 * math.sin(x * freq1 + i) + amp2 * math.sin(x * freq2 + i * 2)
|
||||
points.append((x, int(y)))
|
||||
x += rng.randint(8, 20)
|
||||
points.append((w, h))
|
||||
|
||||
if len(points) >= 3:
|
||||
pygame.draw.polygon(surf, color, points)
|
||||
layers.append(surf)
|
||||
|
||||
return layers
|
||||
|
||||
|
||||
def blit_paper_background(surface):
|
||||
"""Blit the cached paper texture onto the surface."""
|
||||
if _paper_texture:
|
||||
surface.blit(_paper_texture, (0, 0))
|
||||
else:
|
||||
surface.fill(BG_COLOR)
|
||||
|
||||
|
||||
def blit_mountains(surface):
|
||||
"""Blit mountain layers onto the surface."""
|
||||
if _mountain_layers:
|
||||
for layer in _mountain_layers:
|
||||
surface.blit(layer, (0, 0))
|
||||
|
||||
|
||||
def draw_ink_rect(surface, rect, color, alpha=255, border_radius=0):
|
||||
"""Draw a rectangle with slightly wobbly brush-stroke edges."""
|
||||
x, y, w, h = rect if isinstance(rect, (list, tuple)) else (rect.x, rect.y, rect.w, rect.h)
|
||||
|
||||
s = pygame.Surface((w, h), pygame.SRCALPHA)
|
||||
base_color = (*color[:3], alpha)
|
||||
|
||||
# Draw base rect with slight gradient
|
||||
for col in range(w):
|
||||
gradient = 1.0 - 0.1 * (col / max(w, 1))
|
||||
r = min(255, max(0, int(color[0] * gradient)))
|
||||
g = min(255, max(0, int(color[1] * gradient)))
|
||||
b = min(255, max(0, int(color[2] * gradient)))
|
||||
pygame.draw.line(s, (r, g, b, alpha), (col, 0), (col, h - 1))
|
||||
|
||||
# Feather edges
|
||||
rng = random.Random(x * 1000 + y)
|
||||
for edge_x in range(min(3, w)):
|
||||
for edge_y in range(h):
|
||||
if rng.random() < 0.3:
|
||||
s.set_at((edge_x, edge_y), (0, 0, 0, 0))
|
||||
if rng.random() < 0.3:
|
||||
s.set_at((w - 1 - edge_x, edge_y), (0, 0, 0, 0))
|
||||
for edge_y in range(min(3, h)):
|
||||
for edge_x in range(w):
|
||||
if rng.random() < 0.3:
|
||||
s.set_at((edge_x, edge_y), (0, 0, 0, 0))
|
||||
if rng.random() < 0.3:
|
||||
s.set_at((edge_x, h - 1 - edge_y), (0, 0, 0, 0))
|
||||
|
||||
surface.blit(s, (x, y))
|
||||
|
||||
|
||||
def draw_ink_circle(surface, center, radius, color, alpha=200):
|
||||
"""Draw a circle with irregular ink-wash edges."""
|
||||
cx, cy = center
|
||||
size = radius * 2 + 4
|
||||
s = pygame.Surface((size, size), pygame.SRCALPHA)
|
||||
scx, scy = size // 2, size // 2
|
||||
|
||||
rng = random.Random(cx * 100 + cy)
|
||||
n = max(12, radius)
|
||||
for i in range(n):
|
||||
angle = 2 * math.pi * i / n
|
||||
r = radius + rng.gauss(0, max(1, radius * 0.08))
|
||||
px = int(scx + r * math.cos(angle))
|
||||
py = int(scy + r * math.sin(angle))
|
||||
pygame.draw.circle(s, (*color[:3], alpha), (px, py), max(2, int(radius * 0.4)))
|
||||
|
||||
# Fill center
|
||||
pygame.draw.circle(s, (*color[:3], alpha), (scx, scy), max(1, radius - 2))
|
||||
|
||||
surface.blit(s, (cx - size // 2, cy - size // 2))
|
||||
|
||||
|
||||
def draw_brush_stroke(surface, start, end, width, color, alpha=180):
|
||||
"""Draw a thick-to-thin brush stroke line."""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
dx, dy = x2 - x1, y2 - y1
|
||||
length = math.sqrt(dx * dx + dy * dy)
|
||||
if length < 1:
|
||||
return
|
||||
|
||||
steps = max(int(length / 2), 4)
|
||||
rng = random.Random(int(x1 * 100 + y1))
|
||||
|
||||
# Perpendicular direction for jitter
|
||||
nx, ny = -dy / length, dx / length
|
||||
|
||||
for i in range(steps):
|
||||
t = i / (steps - 1)
|
||||
# Width tapers: starts thin, peaks middle, ends thin
|
||||
w = width * (1 - abs(2 * t - 1) * 0.6) * (0.8 + 0.2 * rng.random())
|
||||
px = x1 + dx * t + nx * rng.gauss(0, 1.5)
|
||||
py = y1 + dy * t + ny * rng.gauss(0, 1.5)
|
||||
a = max(50, int(alpha * (0.7 + 0.3 * rng.random())))
|
||||
circle_s = pygame.Surface((int(w * 2 + 4), int(w * 2 + 4)), pygame.SRCALPHA)
|
||||
pygame.draw.circle(circle_s, (*color[:3], a),
|
||||
(int(w + 2), int(w + 2)), max(1, int(w)))
|
||||
surface.blit(circle_s, (int(px - w - 2), int(py - w - 2)))
|
||||
|
||||
|
||||
def draw_seal_stamp(surface, rect, text, font, color=None):
|
||||
"""Draw a traditional Chinese seal (印章): red square with white text."""
|
||||
if color is None:
|
||||
color = ZHU_HONG
|
||||
x, y, w, h = rect if isinstance(rect, (list, tuple)) else (rect.x, rect.y, rect.w, rect.h)
|
||||
|
||||
s = pygame.Surface((w, h), pygame.SRCALPHA)
|
||||
|
||||
# Red background with slight irregularity
|
||||
rng = random.Random(x * 100 + y + w)
|
||||
pygame.draw.rect(s, color, (0, 0, w, h))
|
||||
# Feather edges slightly
|
||||
for edge in range(2):
|
||||
for i in range(w):
|
||||
if rng.random() < 0.25:
|
||||
s.set_at((i, edge), (0, 0, 0, 0))
|
||||
s.set_at((i, h - 1 - edge), (0, 0, 0, 0))
|
||||
for i in range(h):
|
||||
if rng.random() < 0.25:
|
||||
s.set_at((edge, i), (0, 0, 0, 0))
|
||||
s.set_at((w - 1 - edge, i), (0, 0, 0, 0))
|
||||
|
||||
# White border
|
||||
pygame.draw.rect(s, (255, 255, 255), (0, 0, w, h), 2)
|
||||
|
||||
# Text in white
|
||||
text_surf = font.render(text, True, (255, 255, 255))
|
||||
tx = (w - text_surf.get_width()) // 2
|
||||
ty = (h - text_surf.get_height()) // 2
|
||||
s.blit(text_surf, (tx, ty))
|
||||
|
||||
surface.blit(s, (x, y))
|
||||
|
||||
|
||||
def draw_scroll(surface, rect, scroll_color=None):
|
||||
"""Draw a scroll/卷轴 shape with wooden rollers."""
|
||||
if scroll_color is None:
|
||||
scroll_color = (210, 195, 170)
|
||||
x, y, w, h = rect if isinstance(rect, (list, tuple)) else (rect.x, rect.y, rect.w, rect.h)
|
||||
|
||||
roller_h = 6
|
||||
roller_color = (140, 90, 50)
|
||||
|
||||
# Top roller
|
||||
pygame.draw.rect(surface, roller_color, (x - 4, y, w + 8, roller_h), border_radius=3)
|
||||
pygame.draw.rect(surface, (100, 65, 35), (x - 4, y, w + 8, roller_h), 1, border_radius=3)
|
||||
|
||||
# Paper body
|
||||
body_rect = pygame.Rect(x, y + roller_h, w, h - roller_h * 2)
|
||||
draw_ink_rect(surface, body_rect, scroll_color, alpha=240)
|
||||
|
||||
# Bottom roller
|
||||
by = y + h - roller_h
|
||||
pygame.draw.rect(surface, roller_color, (x - 4, by, w + 8, roller_h), border_radius=3)
|
||||
pygame.draw.rect(surface, (100, 65, 35), (x - 4, by, w + 8, roller_h), 1, border_radius=3)
|
||||
|
||||
|
||||
def draw_ink_text(surface, text, pos, font, color, shadow=True):
|
||||
"""Render text with a subtle ink bleed shadow."""
|
||||
if shadow:
|
||||
shadow_color = (max(0, color[0] - 40), max(0, color[1] - 40), max(0, color[2] - 40))
|
||||
shadow_surf = font.render(text, True, shadow_color)
|
||||
surface.blit(shadow_surf, (pos[0] + 1, pos[1] + 1))
|
||||
text_surf = font.render(text, True, color)
|
||||
surface.blit(text_surf, pos)
|
||||
return text_surf
|
||||
|
||||
|
||||
def draw_cloud_pattern(surface, rect):
|
||||
"""Draw traditional Chinese cloud motifs (祥云) as decoration."""
|
||||
x, y, w, h = rect if isinstance(rect, (list, tuple)) else (rect.x, rect.y, rect.w, rect.h)
|
||||
color = (*INK_WASH_2[:3], 80)
|
||||
|
||||
s = pygame.Surface((w, h), pygame.SRCALPHA)
|
||||
rng = random.Random(x * 7 + y)
|
||||
n_clouds = max(1, w // 80)
|
||||
|
||||
for i in range(n_clouds):
|
||||
cx = rng.randint(10, w - 10)
|
||||
cy = rng.randint(5, h - 5)
|
||||
# Simple cloud: overlapping arcs
|
||||
for j in range(3):
|
||||
r = rng.randint(6, 12)
|
||||
ox = j * 8 - 8
|
||||
oy = rng.randint(-3, 3)
|
||||
pygame.draw.circle(s, color, (cx + ox, cy + oy), r)
|
||||
|
||||
surface.blit(s, (x, y))
|
||||
|
||||
|
||||
def draw_zone_bg(surface, rect, base_color, accent_color=None):
|
||||
"""Draw a battlefield zone background with ink wash effect."""
|
||||
x, y, w, h = rect if isinstance(rect, (list, tuple)) else (rect.x, rect.y, rect.w, rect.h)
|
||||
|
||||
s = pygame.Surface((w, h), pygame.SRCALPHA)
|
||||
|
||||
# Base fill
|
||||
s.fill((*base_color[:3], 120))
|
||||
|
||||
# Subtle horizontal brush strokes
|
||||
rng = random.Random(y)
|
||||
for _ in range(5):
|
||||
by = rng.randint(0, h)
|
||||
bx = rng.randint(0, w // 4)
|
||||
bw = rng.randint(w // 3, w)
|
||||
bh = rng.randint(2, 6)
|
||||
bc = (*INK_WASH_2[:3], rng.randint(15, 35))
|
||||
pygame.draw.rect(s, bc, (bx, by, bw, bh))
|
||||
|
||||
surface.blit(s, (x, y))
|
||||
|
||||
# Border line
|
||||
if accent_color:
|
||||
pygame.draw.line(surface, accent_color, (x, y + h - 1), (x + w, y + h - 1), 1)
|
||||
|
||||
|
||||
def draw_ink_hp_bar(surface, x, y, w, h, ratio, bg_color=None):
|
||||
"""Draw an HP bar with ink brush style."""
|
||||
if bg_color is None:
|
||||
bg_color = INK_WASH_4
|
||||
pygame.draw.rect(surface, bg_color, (x, y, w, h))
|
||||
|
||||
if ratio > 0:
|
||||
bar_w = max(1, int(w * ratio))
|
||||
if ratio > 0.5:
|
||||
bar_color = SONGHUA_GREEN
|
||||
elif ratio > 0.25:
|
||||
bar_color = TENG_HUANG
|
||||
else:
|
||||
bar_color = ZHU_HONG
|
||||
|
||||
# Brush-stroke bar
|
||||
rng = random.Random(x * 100 + y)
|
||||
for px in range(bar_w):
|
||||
thickness = h - rng.randint(0, 1)
|
||||
pygame.draw.line(surface, bar_color,
|
||||
(x + px, y + (h - thickness) // 2),
|
||||
(x + px, y + (h + thickness) // 2))
|
||||
@@ -0,0 +1,549 @@
|
||||
"""Game class: main loop, state machine, event handling."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import random
|
||||
import pygame
|
||||
from card_game.config import WINDOW_WIDTH, WINDOW_HEIGHT, FPS, TENG_HUANG, INK_WASH_3
|
||||
from card_game.battlefield import Battlefield
|
||||
from card_game.ui import UI
|
||||
from card_game.ai import AIPlayer
|
||||
from card_game.factions import apply_faction_passive
|
||||
from card_game import ink_style
|
||||
|
||||
SAVED_DECKS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "saved_decks")
|
||||
|
||||
|
||||
class Game:
|
||||
def __init__(self):
|
||||
pygame.init()
|
||||
self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
|
||||
pygame.display.set_caption("战国卡牌 - 水墨风云")
|
||||
self.clock = pygame.time.Clock()
|
||||
|
||||
# Initialize ink style cache
|
||||
ink_style.init_cache()
|
||||
|
||||
self.ui = UI(self.screen)
|
||||
self.ai_player = None
|
||||
|
||||
self.state = "menu"
|
||||
self.battlefield = None
|
||||
self.player_faction = None
|
||||
self.ai_faction = None
|
||||
self.custom_deck = None
|
||||
self._load_custom_deck()
|
||||
self.custom_deck = None
|
||||
|
||||
# AI turn timing
|
||||
self.ai_timer = 0
|
||||
self.ai_step = 0
|
||||
|
||||
def _load_custom_deck(self):
|
||||
if not self.player_faction:
|
||||
return
|
||||
path = os.path.join(SAVED_DECKS_DIR, f"{self.player_faction}.json")
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if len(data.get("cards", [])) == 30:
|
||||
self.custom_deck = data["cards"]
|
||||
except Exception:
|
||||
self.custom_deck = None
|
||||
|
||||
def _save_custom_deck(self, faction_id, cards):
|
||||
os.makedirs(SAVED_DECKS_DIR, exist_ok=True)
|
||||
path = os.path.join(SAVED_DECKS_DIR, f"{faction_id}.json")
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump({"faction": faction_id, "cards": cards}, f, ensure_ascii=False, indent=2)
|
||||
self.custom_deck = list(cards)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
try:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
pygame.quit()
|
||||
sys.exit()
|
||||
elif event.type == pygame.MOUSEBUTTONDOWN:
|
||||
if event.button == 1:
|
||||
self._handle_click(event.pos)
|
||||
elif event.button == 3:
|
||||
self._handle_right_click(event.pos)
|
||||
elif event.type == pygame.MOUSEMOTION:
|
||||
self._handle_hover(event.pos)
|
||||
elif event.type == pygame.KEYDOWN:
|
||||
if event.key == pygame.K_ESCAPE:
|
||||
self._handle_escape()
|
||||
|
||||
self._update()
|
||||
self._draw()
|
||||
pygame.display.flip()
|
||||
self.clock.tick(FPS)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self.screen.fill((0, 0, 0))
|
||||
font = pygame.font.SysFont("microsoftyahei", 16)
|
||||
lines = traceback.format_exc().split('\n')
|
||||
for i, line in enumerate(lines[:15]):
|
||||
surf = font.render(line[:80], True, (255, 80, 80))
|
||||
self.screen.blit(surf, (10, 10 + i * 20))
|
||||
pygame.display.flip()
|
||||
waiting = True
|
||||
while waiting:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
pygame.quit()
|
||||
sys.exit()
|
||||
if event.type == pygame.KEYDOWN:
|
||||
waiting = False
|
||||
|
||||
# --- State: Menu ---
|
||||
|
||||
def _handle_click_menu(self, pos):
|
||||
fid = self.ui.get_faction_at(pos)
|
||||
if fid:
|
||||
self.player_faction = fid
|
||||
self.custom_deck = None
|
||||
self._load_custom_deck()
|
||||
self.state = "deck_select"
|
||||
|
||||
# --- State: Deck Select ---
|
||||
|
||||
def _handle_click_deck_select(self, pos):
|
||||
if "back" in self.ui.menu_buttons and self.ui.menu_buttons["back"].collidepoint(pos):
|
||||
self.state = "menu"
|
||||
return
|
||||
if "deck_build" in self.ui.menu_buttons and self.ui.menu_buttons["deck_build"].collidepoint(pos):
|
||||
self.ui.deck_builder_cards = []
|
||||
self.ui.deck_builder_faction = self.player_faction
|
||||
self.state = "deck_build"
|
||||
return
|
||||
fid = self.ui.get_faction_at(pos)
|
||||
if fid and fid != self.player_faction:
|
||||
self.ai_faction = fid
|
||||
self._start_game()
|
||||
|
||||
# --- State: Deck Build ---
|
||||
|
||||
def _handle_click_deck_build(self, pos):
|
||||
from card_game.config import DECK_SIZE, RARITY_LIMITS, CARD_DATABASE
|
||||
|
||||
if "back" in self.ui.menu_buttons and self.ui.menu_buttons["back"].collidepoint(pos):
|
||||
self.state = "deck_select"
|
||||
return
|
||||
if "clear" in self.ui.menu_buttons and self.ui.menu_buttons["clear"].collidepoint(pos):
|
||||
self.ui.deck_builder_cards = []
|
||||
return
|
||||
if "preset" in self.ui.menu_buttons and self.ui.menu_buttons["preset"].collidepoint(pos):
|
||||
from card_game.config import DECK_PRESETS
|
||||
self.ui.deck_builder_cards = list(DECK_PRESETS[self.player_faction]["cards"])
|
||||
return
|
||||
if "confirm" in self.ui.menu_buttons and self.ui.menu_buttons["confirm"].collidepoint(pos):
|
||||
if len(self.ui.deck_builder_cards) == DECK_SIZE:
|
||||
self._save_custom_deck(self.player_faction, self.ui.deck_builder_cards)
|
||||
self.state = "deck_select"
|
||||
return
|
||||
cid = self.ui.get_deck_card_at(pos)
|
||||
if cid:
|
||||
card_data = CARD_DATABASE[cid]
|
||||
in_deck = self.ui.deck_builder_cards.count(cid)
|
||||
max_copies = RARITY_LIMITS.get(card_data["rarity"], 3)
|
||||
if in_deck < max_copies and len(self.ui.deck_builder_cards) < DECK_SIZE:
|
||||
self.ui.deck_builder_cards.append(cid)
|
||||
return
|
||||
cid = self.ui.get_deck_build_card_at(pos)
|
||||
if cid:
|
||||
card_data = CARD_DATABASE[cid]
|
||||
in_deck = self.ui.deck_builder_cards.count(cid)
|
||||
max_copies = RARITY_LIMITS.get(card_data["rarity"], 3)
|
||||
if in_deck < max_copies and len(self.ui.deck_builder_cards) < DECK_SIZE:
|
||||
self.ui.deck_builder_cards.append(cid)
|
||||
|
||||
def _handle_right_click(self, pos):
|
||||
if self.state != "deck_build":
|
||||
return
|
||||
cid = self.ui.get_deck_card_at(pos)
|
||||
if cid and cid in self.ui.deck_builder_cards:
|
||||
self.ui.deck_builder_cards.remove(cid)
|
||||
return
|
||||
cid = self.ui.get_deck_build_card_at(pos)
|
||||
if cid and cid in self.ui.deck_builder_cards:
|
||||
self.ui.deck_builder_cards.remove(cid)
|
||||
|
||||
def _start_game(self):
|
||||
self.battlefield = Battlefield(self.player_faction, self.ai_faction)
|
||||
if self.custom_deck:
|
||||
self.battlefield.player.deck.build(self.custom_deck)
|
||||
self.battlefield.player.hand = []
|
||||
for _ in range(4):
|
||||
self.battlefield.player.draw_card()
|
||||
self.battlefield.start_game()
|
||||
self.ai_player = AIPlayer(self.battlefield)
|
||||
self.ui.clear_selection()
|
||||
self.state = "playing"
|
||||
|
||||
# --- State: Playing ---
|
||||
|
||||
def _handle_click_playing(self, pos):
|
||||
bf = self.battlefield
|
||||
player = bf.player
|
||||
|
||||
if self.ui.end_turn_btn.collidepoint(pos):
|
||||
self.ui.clear_selection()
|
||||
self._start_ai_turn()
|
||||
return
|
||||
|
||||
if self.ui.target_mode == "order_target" and self.ui.selected_card:
|
||||
self._handle_order_target_click(pos)
|
||||
return
|
||||
|
||||
if self.ui.target_mode == "deploy" and self.ui.selected_card:
|
||||
if self._handle_deploy_click(pos):
|
||||
return
|
||||
self.ui.clear_selection()
|
||||
return
|
||||
|
||||
if self.ui.target_mode == "move" and self.ui.selected_unit:
|
||||
self._handle_move_click(pos)
|
||||
return
|
||||
|
||||
if self.ui.selected_unit and self.ui.target_mode == "attack":
|
||||
self._handle_attack_click(pos)
|
||||
return
|
||||
|
||||
card = self.ui.get_hand_card_at(pos, player)
|
||||
if card:
|
||||
self._select_hand_card(card)
|
||||
return
|
||||
|
||||
unit, owner = self.ui.get_field_unit_at(pos, bf)
|
||||
if unit and owner == player:
|
||||
self._select_own_unit(unit)
|
||||
return
|
||||
|
||||
self.ui.clear_selection()
|
||||
|
||||
def _select_hand_card(self, card):
|
||||
player = self.battlefield.player
|
||||
self.ui.clear_selection()
|
||||
|
||||
if not player.can_play_card(card):
|
||||
return
|
||||
|
||||
if card.card_type == "unit":
|
||||
self.ui.selected_card = card
|
||||
self.ui.target_mode = "deploy"
|
||||
self.ui.valid_targets = []
|
||||
elif card.card_type == "order":
|
||||
if self.battlefield.needs_target(card):
|
||||
self.ui.selected_card = card
|
||||
self.ui.target_mode = "order_target"
|
||||
self.ui.valid_targets = self.battlefield.get_valid_targets(card, player)
|
||||
else:
|
||||
self.battlefield.apply_order_effect(card, player)
|
||||
player.play_order(card)
|
||||
self.ui.clear_selection()
|
||||
|
||||
def _select_own_unit(self, unit):
|
||||
self.ui.clear_selection()
|
||||
player = self.battlefield.player
|
||||
owner = self.battlefield._get_unit_owner(unit)
|
||||
if owner != player:
|
||||
return
|
||||
|
||||
opponent = self.battlefield.get_opponent(player)
|
||||
|
||||
if unit.zone == "support":
|
||||
if unit.can_attack and not unit.has_attacked and unit.is_ranged():
|
||||
self.ui.selected_unit = unit
|
||||
self.ui.target_mode = "attack"
|
||||
targets = list(opponent.get_frontline_units())
|
||||
targets.append(("capital", opponent))
|
||||
self.ui.valid_targets = targets
|
||||
return
|
||||
if self.battlefield.can_move_to_frontline(player):
|
||||
op_cost = player._get_op_cost(unit)
|
||||
if player.provisions >= op_cost:
|
||||
self.ui.selected_unit = unit
|
||||
self.ui.target_mode = "move"
|
||||
elif unit.zone == "frontline":
|
||||
if unit.can_attack and not unit.has_attacked:
|
||||
self.ui.selected_unit = unit
|
||||
self.ui.target_mode = "attack"
|
||||
targets = list(opponent.get_frontline_units())
|
||||
targets.append(("capital", opponent))
|
||||
self.ui.valid_targets = targets
|
||||
|
||||
def _handle_deploy_click(self, pos):
|
||||
from card_game.config import PLAYER_SUPPORT_Y, ZONE_HEIGHT
|
||||
player = self.battlefield.player
|
||||
card = self.ui.selected_card
|
||||
if not card:
|
||||
return False
|
||||
|
||||
mx, my = pos
|
||||
in_support_zone = (PLAYER_SUPPORT_Y <= my <= PLAYER_SUPPORT_Y + ZONE_HEIGHT)
|
||||
|
||||
if in_support_zone:
|
||||
slot = self.ui.get_support_slot_at(pos, player)
|
||||
if slot < 0:
|
||||
for i, s in enumerate(player.support_line):
|
||||
if s is None:
|
||||
slot = i
|
||||
break
|
||||
if slot >= 0:
|
||||
apply_faction_passive(card, player.faction_id)
|
||||
player.deploy_unit(card, slot)
|
||||
self._handle_deploy_abilities(card, player)
|
||||
self.ui.clear_selection()
|
||||
return True
|
||||
return False
|
||||
|
||||
def _handle_order_target_click(self, pos):
|
||||
card = self.ui.selected_card
|
||||
player = self.battlefield.player
|
||||
if not card:
|
||||
self.ui.clear_selection()
|
||||
return
|
||||
|
||||
unit, owner = self.ui.get_field_unit_at(pos, self.battlefield)
|
||||
if unit and unit in self.ui.valid_targets:
|
||||
self.battlefield.apply_order_effect(card, player, unit)
|
||||
player.play_order(card)
|
||||
self.ui.clear_selection()
|
||||
return
|
||||
|
||||
if self.ui.get_enemy_capital_at(pos, self.battlefield.ai):
|
||||
self.battlefield.apply_order_effect(card, player, "capital")
|
||||
player.play_order(card)
|
||||
self.ui.clear_selection()
|
||||
|
||||
def _handle_move_click(self, pos):
|
||||
player = self.battlefield.player
|
||||
unit = self.ui.selected_unit
|
||||
if not unit:
|
||||
self.ui.clear_selection()
|
||||
return
|
||||
|
||||
if not self.battlefield.can_move_to_frontline(player):
|
||||
self.ui.clear_selection()
|
||||
return
|
||||
|
||||
slot = self.ui.get_frontline_slot_at(pos, player)
|
||||
if slot >= 0:
|
||||
result_slot = player.move_to_frontline(unit)
|
||||
if result_slot >= 0:
|
||||
if self.battlefield.frontline_controller is None:
|
||||
self.battlefield.claim_frontline(player)
|
||||
self.ui.clear_selection()
|
||||
else:
|
||||
self.ui.clear_selection()
|
||||
|
||||
def _handle_attack_click(self, pos):
|
||||
unit = self.ui.selected_unit
|
||||
if not unit:
|
||||
self.ui.clear_selection()
|
||||
return
|
||||
|
||||
bf = self.battlefield
|
||||
player = bf.player
|
||||
opponent = bf.ai
|
||||
valid = self.ui.valid_targets
|
||||
|
||||
target_unit, target_owner = self.ui.get_field_unit_at(pos, bf)
|
||||
if target_unit and target_owner == opponent and target_unit in valid:
|
||||
dead = bf.resolve_attack(unit, target_unit)
|
||||
self._add_attack_effect(unit, target_unit)
|
||||
self.ui.clear_selection()
|
||||
return
|
||||
|
||||
if self.ui.get_enemy_capital_at(pos, opponent):
|
||||
cap_target = ("capital", opponent)
|
||||
if cap_target in valid:
|
||||
bf.attack_capital(unit)
|
||||
self.ui.clear_selection()
|
||||
return
|
||||
|
||||
self.ui.clear_selection()
|
||||
|
||||
def _handle_deploy_abilities(self, card, player):
|
||||
for ability in card.abilities:
|
||||
if ability.startswith("draw_on_deploy:"):
|
||||
count = int(ability.split(":")[1])
|
||||
for _ in range(count):
|
||||
player.draw_card()
|
||||
elif ability.startswith("damage_on_deploy:"):
|
||||
dmg = int(ability.split(":")[1])
|
||||
import random
|
||||
opponent = self.battlefield.get_opponent(player)
|
||||
targets = opponent.get_all_units()
|
||||
if targets:
|
||||
target = random.choice(targets)
|
||||
target.take_damage(dmg)
|
||||
elif ability.startswith("gain_on_deploy:"):
|
||||
amount = int(ability.split(":")[1])
|
||||
player.provisions += amount
|
||||
|
||||
def _add_attack_effect(self, attacker, target):
|
||||
from card_game.ui import _support_slot_x, _frontline_slot_x
|
||||
from card_game.config import (FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT, ZONE_HEIGHT,
|
||||
ENEMY_SUPPORT_Y, FRONTLINE_Y, PLAYER_SUPPORT_Y, WINDOW_WIDTH,
|
||||
MAX_FRONTLINE_SLOTS, CAPITAL_WIDTH)
|
||||
half = ZONE_HEIGHT // 2
|
||||
n_fl = MAX_FRONTLINE_SLOTS
|
||||
|
||||
def _unit_pos(unit):
|
||||
owner = self.battlefield._get_unit_owner(unit)
|
||||
is_ai = (owner == self.battlefield.ai)
|
||||
if unit.zone == "support":
|
||||
x = _support_slot_x(unit.slot) + FIELD_CARD_WIDTH // 2
|
||||
zone_y = ENEMY_SUPPORT_Y if is_ai else PLAYER_SUPPORT_Y
|
||||
y = zone_y + ZONE_HEIGHT // 2
|
||||
else:
|
||||
x = _frontline_slot_x(unit.slot, n_fl) + FIELD_CARD_WIDTH // 2
|
||||
zone_y = FRONTLINE_Y if is_ai else (FRONTLINE_Y + half)
|
||||
y = zone_y + half // 2
|
||||
return x, y
|
||||
|
||||
try:
|
||||
if hasattr(attacker, 'zone'):
|
||||
ax, ay = _unit_pos(attacker)
|
||||
else:
|
||||
ax, ay = WINDOW_WIDTH // 2, ENEMY_SUPPORT_Y + ZONE_HEIGHT // 2
|
||||
|
||||
if hasattr(target, 'zone'):
|
||||
tx, ty = _unit_pos(target)
|
||||
else:
|
||||
tx = WINDOW_WIDTH // 2
|
||||
ty = ENEMY_SUPPORT_Y + ZONE_HEIGHT // 2
|
||||
|
||||
self.ui.effects.add_attack_line(ax, ay, tx, ty)
|
||||
self.ui.effects.add_damage(tx, ty - 20, attacker.get_effective_attack())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# --- State: AI Turn ---
|
||||
|
||||
def _start_ai_turn(self):
|
||||
self.state = "ai_turn"
|
||||
try:
|
||||
self.battlefield.start_ai_turn()
|
||||
self.ai_actions = self.ai_player.execute_turn()
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self.ai_actions = []
|
||||
self.ai_step = 0
|
||||
self.ai_timer = 20
|
||||
|
||||
def _update_ai_turn(self):
|
||||
if self.ai_timer > 0:
|
||||
self.ai_timer -= 1
|
||||
return
|
||||
|
||||
if self.ai_step >= len(self.ai_actions):
|
||||
try:
|
||||
self.battlefield.end_ai_turn()
|
||||
except Exception:
|
||||
pass
|
||||
if self.battlefield.game_over:
|
||||
self.state = "game_over"
|
||||
else:
|
||||
self.state = "playing"
|
||||
return
|
||||
|
||||
action = self.ai_actions[self.ai_step]
|
||||
self.ai_step += 1
|
||||
self.ai_timer = 15
|
||||
|
||||
if action[0] in ("attack_unit", "attack_capital"):
|
||||
attacker = action[1]
|
||||
try:
|
||||
if len(action) > 2:
|
||||
self._add_attack_effect(attacker, action[2])
|
||||
else:
|
||||
self._add_attack_effect(attacker, "capital")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# --- Hover ---
|
||||
|
||||
def _handle_hover(self, pos):
|
||||
if self.state not in ("playing", "ai_turn"):
|
||||
return
|
||||
if not self.battlefield:
|
||||
return
|
||||
player = self.battlefield.player
|
||||
card = self.ui.get_hand_card_at(pos, player)
|
||||
self.ui.hover_card = card
|
||||
unit, owner = self.ui.get_field_unit_at(pos, self.battlefield)
|
||||
self.ui.hover_field_unit = unit
|
||||
self.ui.hover_pos = pos
|
||||
|
||||
# --- Escape ---
|
||||
|
||||
def _handle_escape(self):
|
||||
self.ui.clear_selection()
|
||||
if self.state in ("playing", "ai_turn"):
|
||||
self.state = "menu"
|
||||
self.battlefield = None
|
||||
|
||||
# --- State: Game Over ---
|
||||
|
||||
def _handle_click_game_over(self, pos):
|
||||
if "restart" in self.ui.menu_buttons and self.ui.menu_buttons["restart"].collidepoint(pos):
|
||||
self._start_game()
|
||||
elif "menu" in self.ui.menu_buttons and self.ui.menu_buttons["menu"].collidepoint(pos):
|
||||
self.state = "menu"
|
||||
self.battlefield = None
|
||||
|
||||
# --- Unified handlers ---
|
||||
|
||||
def _handle_click(self, pos):
|
||||
if self.state == "menu":
|
||||
self._handle_click_menu(pos)
|
||||
elif self.state == "deck_select":
|
||||
self._handle_click_deck_select(pos)
|
||||
elif self.state == "deck_build":
|
||||
self._handle_click_deck_build(pos)
|
||||
elif self.state == "playing":
|
||||
self._handle_click_playing(pos)
|
||||
elif self.state == "game_over":
|
||||
self._handle_click_game_over(pos)
|
||||
|
||||
def _update(self):
|
||||
if self.state == "ai_turn":
|
||||
self._update_ai_turn()
|
||||
if self.battlefield:
|
||||
self.battlefield.update_effects()
|
||||
self.ui.effects.update()
|
||||
|
||||
def _draw(self):
|
||||
if self.state == "menu":
|
||||
self.ui.draw_menu()
|
||||
elif self.state == "deck_select":
|
||||
self.ui.draw_deck_select(self.player_faction)
|
||||
elif self.state == "deck_build":
|
||||
self.ui.draw_deck_builder(self.player_faction)
|
||||
elif self.state in ("playing", "ai_turn"):
|
||||
self.ui.draw_game(self.battlefield)
|
||||
if self.state == "ai_turn":
|
||||
from card_game.config import WINDOW_WIDTH, ENEMY_INFO_HEIGHT
|
||||
text = self.ui.font_md.render("对手回合...", True, INK_WASH_3)
|
||||
self.screen.blit(text, (WINDOW_WIDTH // 2 - text.get_width() // 2,
|
||||
ENEMY_INFO_HEIGHT + 55))
|
||||
elif self.state == "game_over":
|
||||
self.ui.draw_game(self.battlefield)
|
||||
self.ui.draw_game_over(self.battlefield.winner, self.battlefield)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
game = Game()
|
||||
game.run()
|
||||
@@ -0,0 +1,187 @@
|
||||
"""Player class: manages hand, support line, frontline, HP, provisions."""
|
||||
|
||||
from card_game.config import (
|
||||
STARTING_CAPITAL_HP, MAX_PROVISIONS, MAX_HAND_SIZE,
|
||||
MAX_SUPPORT_SLOTS, MAX_FRONTLINE_SLOTS, DECK_SIZE, STARTING_HAND_SIZE,
|
||||
FATIGUE_START_DAMAGE, FACTIONS, DECK_PRESETS,
|
||||
)
|
||||
from card_game.deck import Deck
|
||||
from card_game.card import Card
|
||||
|
||||
|
||||
class Player:
|
||||
def __init__(self, faction_id, is_ai=False):
|
||||
faction = FACTIONS[faction_id]
|
||||
self.faction_id = faction_id
|
||||
self.faction = faction
|
||||
self.is_ai = is_ai
|
||||
|
||||
self.capital_hp = faction["capital_hp"]
|
||||
self.max_capital_hp = faction["capital_hp"]
|
||||
self.provisions = 0
|
||||
self.max_provisions = 0
|
||||
|
||||
self.hand = []
|
||||
self.support_line = [None] * MAX_SUPPORT_SLOTS
|
||||
self.frontline = [None] * MAX_FRONTLINE_SLOTS
|
||||
|
||||
self.deck = Deck()
|
||||
self.fatigue_damage = 0
|
||||
self.turn_number = 0
|
||||
|
||||
def build_deck(self):
|
||||
preset = DECK_PRESETS[self.faction_id]
|
||||
self.deck.build(preset["cards"])
|
||||
|
||||
def start_game(self):
|
||||
self.build_deck()
|
||||
for _ in range(STARTING_HAND_SIZE):
|
||||
self.draw_card()
|
||||
|
||||
def start_turn(self, turn_number):
|
||||
self.turn_number = turn_number
|
||||
gained = min(turn_number + 1, MAX_PROVISIONS)
|
||||
self.provisions = gained
|
||||
self.max_provisions = gained
|
||||
|
||||
# Faction passive: Qi extra provision
|
||||
if self.faction_id == "qi":
|
||||
self.provisions += 1
|
||||
|
||||
self.draw_card()
|
||||
|
||||
# Reset unit flags
|
||||
for unit in self.get_all_units():
|
||||
unit.reset_turn_flags()
|
||||
if unit.zone == "frontline" and unit.turn_played < turn_number:
|
||||
unit.can_attack = True
|
||||
if unit.zone == "support" and unit.is_ranged() and unit.turn_played < turn_number:
|
||||
unit.can_attack = True
|
||||
|
||||
# Chu healer
|
||||
if self.faction_id == "chu":
|
||||
self._apply_heal_ability()
|
||||
|
||||
def draw_card(self):
|
||||
if len(self.hand) >= MAX_HAND_SIZE:
|
||||
return
|
||||
card = self.deck.draw()
|
||||
if card:
|
||||
card.zone = "hand"
|
||||
self.hand.append(card)
|
||||
else:
|
||||
self.fatigue_damage += 1
|
||||
self.capital_hp -= self.fatigue_damage
|
||||
|
||||
def can_play_card(self, card):
|
||||
if card.cost > self.provisions:
|
||||
return False
|
||||
if card.card_type == "unit":
|
||||
return any(s is None for s in self.support_line)
|
||||
return True
|
||||
|
||||
def deploy_unit(self, card, slot=-1):
|
||||
if slot < 0:
|
||||
for i, s in enumerate(self.support_line):
|
||||
if s is None:
|
||||
slot = i
|
||||
break
|
||||
if slot < 0 or slot >= len(self.support_line):
|
||||
return None
|
||||
self.support_line[slot] = card
|
||||
card.zone = "support"
|
||||
card.slot = slot
|
||||
card.turn_played = self.turn_number
|
||||
self.provisions -= card.cost
|
||||
self.hand.remove(card)
|
||||
return slot
|
||||
|
||||
def play_order(self, card):
|
||||
self.provisions -= card.cost
|
||||
self.hand.remove(card)
|
||||
|
||||
def move_to_frontline(self, unit):
|
||||
op_cost = self._get_op_cost(unit)
|
||||
if self.provisions < op_cost:
|
||||
return -1
|
||||
slot = -1
|
||||
for i, s in enumerate(self.frontline):
|
||||
if s is None:
|
||||
slot = i
|
||||
break
|
||||
if slot < 0:
|
||||
return -1
|
||||
self.support_line[unit.slot] = None
|
||||
self.frontline[slot] = unit
|
||||
unit.zone = "frontline"
|
||||
unit.slot = slot
|
||||
unit.has_moved = True
|
||||
self.provisions -= op_cost
|
||||
if unit.turn_played < self.turn_number:
|
||||
if unit.can_move_and_attack():
|
||||
unit.can_attack = True
|
||||
return slot
|
||||
|
||||
def move_to_frontline_free(self, unit):
|
||||
slot = -1
|
||||
for i, s in enumerate(self.frontline):
|
||||
if s is None:
|
||||
slot = i
|
||||
break
|
||||
if slot < 0:
|
||||
return -1
|
||||
self.support_line[unit.slot] = None
|
||||
self.frontline[slot] = unit
|
||||
unit.zone = "frontline"
|
||||
unit.slot = slot
|
||||
unit.has_moved = True
|
||||
if unit.turn_played < self.turn_number:
|
||||
unit.can_attack = True
|
||||
return slot
|
||||
|
||||
def remove_unit(self, unit):
|
||||
if unit.zone == "support":
|
||||
if 0 <= unit.slot < len(self.support_line):
|
||||
self.support_line[unit.slot] = None
|
||||
elif unit.zone == "frontline":
|
||||
if 0 <= unit.slot < len(self.frontline):
|
||||
self.frontline[unit.slot] = None
|
||||
|
||||
def get_all_units(self):
|
||||
units = []
|
||||
for u in self.support_line:
|
||||
if u is not None:
|
||||
units.append(u)
|
||||
for u in self.frontline:
|
||||
if u is not None:
|
||||
units.append(u)
|
||||
return units
|
||||
|
||||
def get_frontline_units(self):
|
||||
return [u for u in self.frontline if u is not None]
|
||||
|
||||
def get_support_units(self):
|
||||
return [u for u in self.support_line if u is not None]
|
||||
|
||||
def _get_op_cost(self, unit):
|
||||
cost = unit.op_cost
|
||||
if self.faction_id == "yan" and unit.unit_type == "cavalry":
|
||||
cost = max(0, cost - 1)
|
||||
return cost
|
||||
|
||||
def _apply_heal_ability(self):
|
||||
healers = [u for u in self.get_all_units() if "heal_all:1" in u.abilities]
|
||||
if healers:
|
||||
for unit in self.get_all_units():
|
||||
if unit.current_hp < unit.max_hp:
|
||||
unit.current_hp = min(unit.max_hp, unit.current_hp + 1)
|
||||
|
||||
def cleanup_dead(self):
|
||||
for i in range(len(self.support_line)):
|
||||
u = self.support_line[i]
|
||||
if u is not None and not u.is_alive():
|
||||
self.support_line[i] = None
|
||||
for i in range(len(self.frontline)):
|
||||
u = self.frontline[i]
|
||||
if u is not None and not u.is_alive():
|
||||
self.frontline[i] = None
|
||||
+776
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Utility functions."""
|
||||
|
||||
import math
|
||||
|
||||
|
||||
def distance(x1, y1, x2, y2):
|
||||
return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
|
||||
|
||||
|
||||
def lerp(a, b, t):
|
||||
return a + (b - a) * t
|
||||
@@ -0,0 +1 @@
|
||||
pygame>=2.5
|
||||
Reference in New Issue
Block a user