Files
cultivation-world-simulator/src/classes/avatar/core.py

354 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Avatar 核心类
精简后的 Avatar 类,通过 Mixin 组合完整功能。
"""
import random
from collections import defaultdict
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional, List, TYPE_CHECKING
if TYPE_CHECKING:
from src.classes.sect_ranks import SectRank
from src.classes.calendar import MonthStamp
from src.classes.world import World
from src.sim.save.avatar_save_mixin import AvatarSaveMixin
from src.sim.load.avatar_load_mixin import AvatarLoadMixin
from src.classes.tile import Tile
from src.classes.region import Region
from src.classes.cultivation import CultivationProgress
from src.classes.root import Root
from src.classes.technique import Technique, get_technique_by_sect
from src.classes.age import Age
from src.classes.event import Event
from src.classes.action_runtime import ActionPlan, ActionInstance
from src.classes.alignment import Alignment
from src.classes.persona import Persona, get_random_compatible_personas
from src.classes.material import Material
from src.classes.weapon import Weapon
from src.classes.auxiliary import Auxiliary
from src.classes.magic_stone import MagicStone
from src.classes.hp import HP, HP_MAX_BY_REALM
from src.classes.relation import Relation
from src.classes.sect import Sect
from src.classes.appearance import Appearance, get_random_appearance
from src.classes.spirit_animal import SpiritAnimal
from src.classes.long_term_objective import LongTermObjective
from src.classes.nickname_data import Nickname
from src.classes.emotions import EmotionType
from src.utils.config import CONFIG
from src.classes.elixir import ConsumedElixir, Elixir
# Mixin 导入
from src.classes.effect import EffectsMixin
from src.classes.avatar.inventory_mixin import InventoryMixin
from src.classes.avatar.action_mixin import ActionMixin
persona_num = CONFIG.avatar.persona_num
class Gender(Enum):
MALE = "male"
FEMALE = "female"
def __str__(self) -> str:
return gender_strs.get(self, self.value)
gender_strs = {
Gender.MALE: "",
Gender.FEMALE: "",
}
@dataclass
class Avatar(
AvatarSaveMixin,
AvatarLoadMixin,
EffectsMixin,
InventoryMixin,
ActionMixin,
):
"""
NPC的类。
包含了这个角色的一切信息。
"""
world: World
name: str
id: str
birth_month_stamp: MonthStamp
age: Age
gender: Gender
cultivation_progress: CultivationProgress = field(default_factory=lambda: CultivationProgress(0))
pos_x: int = 0
pos_y: int = 0
tile: Optional[Tile] = None
root: Root = field(default_factory=lambda: random.choice(list(Root)))
personas: List[Persona] = field(default_factory=list)
technique: Technique | None = None
_pending_events: List[Event] = field(default_factory=list)
current_action: Optional[ActionInstance] = None
planned_actions: List[ActionPlan] = field(default_factory=list)
thinking: str = ""
short_term_objective: str = ""
long_term_objective: Optional[LongTermObjective] = None
magic_stone: MagicStone = field(default_factory=lambda: MagicStone(0))
materials: dict[Material, int] = field(default_factory=dict)
hp: HP = field(default_factory=lambda: HP(0, 0))
relations: dict["Avatar", Relation] = field(default_factory=dict)
alignment: Alignment | None = None
sect: Sect | None = None
sect_rank: "SectRank | None" = None
appearance: Appearance = field(default_factory=get_random_appearance)
weapon: Optional[Weapon] = None
weapon_proficiency: float = 0.0
auxiliary: Optional[Auxiliary] = None
spirit_animal: Optional[SpiritAnimal] = None
nickname: Optional[Nickname] = None
emotion: EmotionType = EmotionType.CALM
custom_pic_id: Optional[int] = None
elixirs: List[ConsumedElixir] = field(default_factory=list)
is_dead: bool = False
death_info: Optional[dict] = None
_new_action_set_this_step: bool = False
_action_cd_last_months: dict[str, int] = field(default_factory=dict)
known_regions: set[int] = field(default_factory=set)
# 关系交互计数器: key=target_id, value={"count": 0, "checked_times": 0}
relation_interaction_states: dict[str, dict[str, int]] = field(default_factory=lambda: defaultdict(lambda: {"count": 0, "checked_times": 0}))
# ========== 宗门相关 ==========
def consume_elixir(self, elixir: Elixir) -> bool:
"""
服用丹药
:return: 是否成功服用
"""
# 1. 境界校验:只能服用境界等于或者小于当前境界的丹药
if elixir.realm > self.cultivation_progress.realm:
return False
# 2. 重复服用校验:若已服用过同种且未失效的丹药,则无效
# 因为延寿丹药都是无限持久的,所以所有延寿丹药都只能服用一次。
for consumed in self.elixirs:
if consumed.elixir.id == elixir.id:
if not consumed.is_completely_expired(int(self.world.month_stamp)):
return False
# 3. 记录服用状态
self.elixirs.append(ConsumedElixir(elixir, int(self.world.month_stamp)))
# 4. 立即触发属性重算因为可能有立即生效的数值变化或者MaxHP/Lifespan改变
self.recalc_effects()
return True
def process_elixir_expiration(self, current_month: int) -> None:
"""
处理丹药过期:
1. 移除已完全过期的丹药
2. 如果有移除,触发属性重算
"""
if not self.elixirs:
return
original_count = len(self.elixirs)
# 过滤掉完全过期的
self.elixirs = [
e for e in self.elixirs
if not e.is_completely_expired(current_month)
]
# 如果数量减少说明有过期重算属性主要是寿命、MaxHP
if len(self.elixirs) < original_count:
self.recalc_effects()
def join_sect(self, sect: Sect, rank: "SectRank") -> None:
"""加入宗门"""
if self.is_dead:
return
if self.sect:
self.leave_sect()
self.sect = sect
self.sect_rank = rank
sect.add_member(self)
def leave_sect(self) -> None:
"""退出宗门"""
if self.sect:
self.sect.remove_member(self)
self.sect = None
self.sect_rank = None
def get_sect_str(self) -> str:
"""获取宗门显示名:有宗门则返回"宗门名+职位",否则返回"散修""""
if self.sect is None:
return "散修"
if self.sect_rank is None:
return self.sect.name
from src.classes.sect_ranks import get_rank_display_name
rank_name = get_rank_display_name(self.sect_rank, self.sect)
return f"{self.sect.name}{rank_name}"
def get_sect_rank_name(self) -> str:
"""获取宗门职位的显示名称"""
if self.sect is None or self.sect_rank is None:
return "散修"
from src.classes.sect_ranks import get_rank_display_name
return get_rank_display_name(self.sect_rank, self.sect)
# ========== 死亡相关 ==========
def set_dead(self, reason: str, time: MonthStamp) -> None:
"""设置角色死亡状态。"""
if self.is_dead:
return
self.is_dead = True
self.death_info = {
"time": int(time),
"reason": reason,
"location": (self.pos_x, self.pos_y)
}
self.planned_actions.clear()
self.current_action = None
self._pending_events.clear()
self.thinking = ""
self.short_term_objective = ""
if self.sect:
self.sect.remove_member(self)
def death_by_old_age(self) -> bool:
"""检查是否老死"""
return self.age.death_by_old_age(self.cultivation_progress.realm)
# ========== 年龄与修为 ==========
def update_age(self, current_month_stamp: MonthStamp):
"""更新年龄"""
self.age.update_age(current_month_stamp, self.birth_month_stamp)
def update_cultivation(self, new_level: int):
"""更新修仙进度,并在境界提升时更新寿命和宗门职位"""
old_realm = self.cultivation_progress.realm
self.cultivation_progress.level = new_level
self.cultivation_progress.realm = self.cultivation_progress.get_realm(new_level)
if self.cultivation_progress.realm != old_realm:
self.age.update_realm(self.cultivation_progress.realm)
self.recalc_effects()
from src.classes.sect_ranks import check_and_promote_sect_rank
check_and_promote_sect_rank(self, old_realm, self.cultivation_progress.realm)
# ========== 区域与位置 ==========
def _init_known_regions(self):
"""初始化已知区域:当前位置 + 宗门驻地"""
if self.tile and self.tile.region:
self.known_regions.add(self.tile.region.id)
if self.sect:
for r in self.world.map.sect_regions.values():
if r.sect_id == self.sect.id:
self.known_regions.add(r.id)
break
# ========== 关系相关 ==========
def set_relation(self, other: "Avatar", relation: Relation) -> None:
"""设置与另一个角色的关系。"""
from src.classes.relations import set_relation
set_relation(self, other, relation)
def get_relation(self, other: "Avatar") -> Optional[Relation]:
"""获取与另一个角色的关系。"""
from src.classes.relations import get_relation
return get_relation(self, other)
def clear_relation(self, other: "Avatar") -> None:
"""清除与另一个角色的关系。"""
from src.classes.relations import clear_relation
clear_relation(self, other)
# ========== 信息展示(委托) ==========
def get_info(self, detailed: bool = False) -> dict:
from src.classes.avatar.info_presenter import get_avatar_info
return get_avatar_info(self, detailed)
def get_structured_info(self) -> dict:
from src.classes.avatar.info_presenter import get_avatar_structured_info
return get_avatar_structured_info(self)
def get_expanded_info(
self,
co_region_avatars: Optional[List["Avatar"]] = None,
other_avatar: Optional["Avatar"] = None,
detailed: bool = False
) -> dict:
from src.classes.avatar.info_presenter import get_avatar_expanded_info
return get_avatar_expanded_info(self, co_region_avatars, other_avatar, detailed)
def get_other_avatar_info(self, other_avatar: "Avatar") -> str:
from src.classes.avatar.info_presenter import get_other_avatar_info
return get_other_avatar_info(self, other_avatar)
def get_desc(self, detailed: bool = False) -> str:
"""获取角色的文本描述(包含效果明细)"""
from src.classes.avatar.info_presenter import get_avatar_desc
return get_avatar_desc(self, detailed=detailed)
# ========== 魔法方法 ==========
@property
def current_action_name(self) -> str:
"""获取当前动作名称,默认返回'思考'"""
if self.current_action and self.current_action.action:
action = self.current_action.action
# 优先取 ACTION_NAME (中文名),如果没有则使用类名
return getattr(action, "ACTION_NAME", getattr(action, "name", "思考"))
return "思考"
def __post_init__(self):
"""在Avatar创建后自动初始化tile和HP"""
self.tile = self.world.map.get_tile(self.pos_x, self.pos_y)
max_hp = HP_MAX_BY_REALM.get(self.cultivation_progress.realm, 100)
self.hp = HP(max_hp, max_hp)
if not self.personas:
self.personas = get_random_compatible_personas(persona_num, avatar=self)
if self.technique is None:
self.technique = get_technique_by_sect(self.sect)
if self.sect:
self.sect.add_member(self)
if self.alignment is None:
if self.sect is not None:
self.alignment = self.sect.alignment
else:
self.alignment = random.choice(list(Alignment))
self.recalc_effects()
self._init_known_regions()
def __hash__(self) -> int:
if not hasattr(self, 'id'):
# 防御性编程如果id尚未初始化例如deepcopy过程中使用对象内存地址
return super().__hash__()
return hash(self.id)
def __str__(self) -> str:
return str(self.get_info(detailed=False))