diff --git a/src/classes/action/move.py b/src/classes/action/move.py index 14144e2..d1d75c6 100644 --- a/src/classes/action/move.py +++ b/src/classes/action/move.py @@ -19,6 +19,9 @@ class Move(DefineAction, ChunkActionMixin): world = self.world # 基于境界的移动步长:曼哈顿限制,优先斜向 step = getattr(self.avatar, "move_step_length", 1) + # 附加移动步长加成 + extra_raw = self.avatar.effects.get("extra_move_step", 0) + step += int(extra_raw or 0) clamped_dx, clamped_dy = clamp_manhattan_with_diagonal_priority(delta_x, delta_y, step) new_x = self.avatar.pos_x + clamped_dx diff --git a/src/classes/avatar.py b/src/classes/avatar.py index 6ace542..b7dc271 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -23,6 +23,7 @@ from src.classes.effect import _merge_effects from src.classes.alignment import Alignment from src.classes.persona import Persona, personas_by_id, get_random_compatible_personas from src.classes.item import Item +from src.classes.treasure import Treasure from src.classes.magic_stone import MagicStone from src.classes.hp_and_mp import HP, MP, HP_MAX_BY_REALM, MP_MAX_BY_REALM from src.utils.id_generator import get_avatar_id @@ -88,6 +89,8 @@ class Avatar: sect: Sect | None = None # 外貌(1~10级),创建时随机生成 appearance: Appearance = field(default_factory=get_random_appearance) + # 装备的法宝(仅一个) + treasure: Optional[Treasure] = None # 当月/当步新设动作标记:在 commit_next_plan 设为 True,首次 tick_action 后清为 False _new_action_set_this_step: bool = False # 不缓存 effects;实时从宗门与功法合并 @@ -136,6 +139,9 @@ class Avatar: merged = _merge_effects(merged, self.technique.effects) # 来自灵根 merged = _merge_effects(merged, self.root.effects) + # 来自法宝 + if self.treasure is not None: + merged = _merge_effects(merged, self.treasure.effects) return merged @@ -172,6 +178,12 @@ class Avatar: items_info = ",".join([f"{item.get_info()}x{quantity}" for item, quantity in self.items.items()]) if self.items else "无" appearance_info = self.appearance.get_info() + # 法宝信息:detailed 使用 get_detailed_info;简略使用 get_info + if self.treasure is not None: + treasures_info = self.treasure.get_detailed_info() if detailed else self.treasure.get_info() + else: + treasures_info = "无" + return { "id": self.id, "名字": self.name, @@ -190,6 +202,7 @@ class Avatar: "个性": personas_info, "物品": items_info, "外貌": appearance_info, + "法宝": treasures_info, } def __str__(self) -> str: @@ -579,7 +592,8 @@ class Avatar: relation = self.get_relation(other_avatar) relation_str = str(relation) sect_str = other_avatar.sect.name if other_avatar.sect is not None else "散修" - return f"{other_avatar.name},境界:{other_avatar.cultivation_progress.get_info()},关系:{relation_str},阵营:{other_avatar.alignment},宗门:{sect_str},外貌:{other_avatar.appearance.get_info()}" + tr_str = other_avatar.treasure.get_info() if other_avatar.treasure is not None else "无" + return f"{other_avatar.name},境界:{other_avatar.cultivation_progress.get_info()},关系:{relation_str},阵营:{other_avatar.alignment},宗门:{sect_str},法宝:{tr_str},外貌:{other_avatar.appearance.get_info()}" def update_time_effect(self) -> None: """ @@ -597,28 +611,4 @@ class Avatar: """ return self.cultivation_progress.get_move_step() -def get_new_avatar_from_ordinary(world: World, current_month_stamp: MonthStamp, name: str, age: Age): - """ - 从凡人中来的新修士 - 这代表其境界为最低 - """ - # 生成短ID,替代UUID4 - avatar_id = get_avatar_id() - - birth_month_stamp = current_month_stamp - age.age * 12 + random.randint(0, 11) # 在出生年内随机选择月份 - cultivation_progress = CultivationProgress(0) - pos_x = random.randint(0, world.map.width - 1) - pos_y = random.randint(0, world.map.height - 1) - gender = random.choice(list(Gender)) - - return Avatar( - world=world, - name=name, - id=avatar_id, - birth_month_stamp=MonthStamp(birth_month_stamp), - age=age, - gender=gender, - cultivation_progress=cultivation_progress, - pos_x=pos_x, - pos_y=pos_y, - ) + diff --git a/src/classes/battle.py b/src/classes/battle.py index 2a4d7d8..ee4d43b 100644 --- a/src/classes/battle.py +++ b/src/classes/battle.py @@ -38,7 +38,10 @@ def get_base_strength(self_avatar: "Avatar") -> float: grade_points = 0.0 if self_avatar.technique is not None: grade_points = _GRADE_POINTS.get(self_avatar.technique.grade, 0.0) - return strength_from_level + grade_points + # 来自效果的额外战斗力点数(例如法宝带来的被动加成) + extra_raw = self_avatar.effects.get("extra_battle_strength_points", 0) + extra_points = float(extra_raw or 0.0) + return strength_from_level + grade_points + extra_points def _combat_strength_vs(opponent: "Avatar", self_avatar: "Avatar") -> float: diff --git a/src/classes/mutual_action/dual_cultivation.py b/src/classes/mutual_action/dual_cultivation.py index b44e66c..b901788 100644 --- a/src/classes/mutual_action/dual_cultivation.py +++ b/src/classes/mutual_action/dual_cultivation.py @@ -85,6 +85,10 @@ class DualCultivation(MutualAction): jitter = random.uniform(-0.2, 0.2) factor = max(3.0, min(5.0, factor + jitter)) exp_gain = int(base * factor) + # 附加“双修经验提升”效果(如法宝) + extra_raw = initiator.effects.get("extra_dual_cultivation_exp", 0) + extra = int(extra_raw or 0) + exp_gain += extra initiator.cultivation_progress.add_exp(exp_gain) self._dual_exp_gain = exp_gain diff --git a/src/classes/observe.py b/src/classes/observe.py index bcf3e1f..b6a2b87 100644 --- a/src/classes/observe.py +++ b/src/classes/observe.py @@ -28,7 +28,10 @@ def get_avatar_observation_radius(avatar: "Avatar") -> int: """ 获取角色的感知半径。 """ - return get_observation_radius_by_realm(avatar.cultivation_progress.realm) + base = get_observation_radius_by_realm(avatar.cultivation_progress.realm) + extra_raw = avatar.effects.get("extra_observation_radius", 0) + extra = int(extra_raw or 0) + return max(1, base + extra) def is_within_observation(initiator: "Avatar", other: "Avatar") -> bool: diff --git a/src/classes/treasure.py b/src/classes/treasure.py new file mode 100644 index 0000000..5d15776 --- /dev/null +++ b/src/classes/treasure.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Optional, Dict + +from src.utils.df import game_configs +from src.classes.effect import load_effect_from_str +from src.classes.sect import Sect, sects_by_id + + +@dataclass +class Treasure: + """ + 法宝:配置驱动,暂不挂接到 Avatar。 + 字段与 static/game_configs/treasure.csv 对应: + - sect_id:对应宗门ID(见 sect.csv);允许为空表示无特定宗门归属 + - effects:解析为 dict,用于未来与 Avatar.effects 合并 + """ + id: int + name: str + sect_id: Optional[int] + desc: str + effects: dict[str, object] = field(default_factory=dict) + sect: Optional[Sect] = None + + def get_info(self) -> str: + return self.name + + def get_detailed_info(self) -> str: + sect_name = self.sect.name if self.sect is not None else "散修可用" + return f"{self.name}(宗门:{sect_name}){self.desc}" + + +def _load_treasures() -> tuple[Dict[int, Treasure], Dict[str, Treasure], Dict[int, Treasure]]: + """从配表加载 treasure 数据。 + 返回:(按ID、按名称、按宗门ID 的映射)。 + 若同一宗门配置多个法宝,按首次出现保留(每门至多一个)。 + """ + treasures_by_id: Dict[int, Treasure] = {} + treasures_by_name: Dict[str, Treasure] = {} + treasures_by_sect_id: Dict[int, Treasure] = {} + + df = game_configs.get("treasure") + if df is None: + return treasures_by_id, treasures_by_name, treasures_by_sect_id + + for _, row in df.iterrows(): + raw_sect = row.get("sect_id") + sect_id: Optional[int] = None + if raw_sect is not None and str(raw_sect).strip() and str(raw_sect).strip() != "nan": + sect_id = int(float(raw_sect)) + + effects = load_effect_from_str(row.get("effects", "")) + + sect_obj: Optional[Sect] = sects_by_id.get(int(sect_id)) if sect_id is not None else None + + t = Treasure( + id=int(row["id"]), + name=str(row["name"]), + sect_id=sect_id, + desc=str(row.get("desc", "")), + effects=effects, + sect=sect_obj, + ) + + treasures_by_id[t.id] = t + treasures_by_name[t.name] = t + if t.sect_id is not None and t.sect_id not in treasures_by_sect_id: + treasures_by_sect_id[t.sect_id] = t + + return treasures_by_id, treasures_by_name, treasures_by_sect_id + + +treasures_by_id, treasures_by_name, treasures_by_sect_id = _load_treasures() + + +for name, treasure in treasures_by_name.items(): + print(name, treasure.sect.name) \ No newline at end of file diff --git a/src/run/run.py b/src/run/run.py index a41aa08..2e0a8cd 100644 --- a/src/run/run.py +++ b/src/run/run.py @@ -10,6 +10,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) # 依赖项目内部模块 from src.front.front import Front from src.sim.simulator import Simulator +from src.sim.new_avatar import make_avatars from src.classes.world import World from src.classes.map import Map from src.classes.tile import TileType @@ -68,94 +69,12 @@ def sample_existed_sects(all_sects: Sequence, needed_sects: int) -> list: return result def make_avatars(world: World, count: int = 12, current_month_stamp: MonthStamp = MonthStamp(100 * 12), existed_sects: Optional[List] = None) -> dict[str, Avatar]: - avatars: dict[str, Avatar] = {} - width, height = world.map.width, world.map.height - # 依据配置决定宗门人数占比:当 init_npc_num > sect_num 时启用宗门逻辑 - num_total = int(count) - use_sects = bool(existed_sects) - # 约 2/3 为宗门弟子,1/3 为散修 - sect_member_target = int(num_total * 2 / 3) if use_sects else 0 - # 本局启用的宗门(已在上方确定) - # 在地图上添加本局宗门总部 + # 迁移到 src/sim/new_avatar.py + from src.sim.new_avatar import make_avatars as _new_make + # 在地图上添加本局宗门总部(保持原行为) if existed_sects: add_sect_headquarters(world.map, existed_sects) - # 统计将要分配的宗门成员数量(用于均分) - sect_member_count = 0 - sect_member_counts_by_id: dict[int, int] = {s.id: 0 for s in existed_sects} if existed_sects else {} - - for i in range(count): - # 随机生成年龄,范围从16到60岁 - age_years = random.randint(16, 60) - # 根据当前时间戳和年龄计算出生时间戳 - birth_month_stamp = current_month_stamp - age_years * 12 + random.randint(0, 11) # 在出生年内随机选择月份 - gender = random_gender() - # 分配宗门或散修 - assigned_sect = None - if use_sects and sect_member_count < sect_member_target and existed_sects: - # 均分到各宗门:选择当前成员最少的宗门,若并列则随机 - min_count = min(sect_member_counts_by_id.values()) if sect_member_counts_by_id else 0 - candidates = [s for s in existed_sects if sect_member_counts_by_id.get(s.id, 0) == min_count] - assigned_sect = random.choice(candidates) - sect_member_counts_by_id[assigned_sect.id] = sect_member_counts_by_id.get(assigned_sect.id, 0) + 1 - sect_member_count += 1 - # 根据宗门生成姓名 - name = get_random_name_for_sect(gender, assigned_sect) - - # 随机生成level,范围从0到120(对应四个大境界) - level = random.randint(0, 120) - cultivation_progress = CultivationProgress(level) - - # 创建Age实例,传入年龄与当前境界 - age = Age(age_years, cultivation_progress.realm) - - # 找一个非海域的出生点 - for _ in range(200): - x = random.randint(0, width - 1) - y = random.randint(0, height - 1) - t = world.map.get_tile(x, y) - if t.type not in (TileType.WATER, TileType.SEA, TileType.MOUNTAIN, TileType.VOLCANO, TileType.SWAMP, TileType.CAVE, TileType.RUINS): - break - else: - x, y = random.randint(0, width - 1), random.randint(0, height - 1) - - avatar = Avatar( - world=world, - name=name, - id=get_avatar_id(), - birth_month_stamp=MonthStamp(birth_month_stamp), - age=age, - gender=gender, - cultivation_progress=cultivation_progress, - pos_x=x, - pos_y=y, - root=random.choice(list(Root)), # 随机选择灵根 - sect=assigned_sect, - ) - avatar.tile = world.map.get_tile(x, y) - # 依据宗门设定阵营(若有宗门则与宗门阵营一致,否则保留默认随机) - if assigned_sect is not None: - avatar.alignment = assigned_sect.alignment - # 宗门弟子:按宗门功法随机 - t = get_technique_by_sect(assigned_sect) - avatar.technique = t - # 将灵根改为功法对应灵根(邪功法不变) - mapped_root = attribute_to_root(avatar.technique.attribute) - if mapped_root is not None: - avatar.root = mapped_root - avatars[avatar.id] = avatar - # # —— 为演示添加少量示例关系 —— - avatar_list = list(avatars.values()) - if len(avatar_list) >= 2: - avatar_list[0].set_relation(avatar_list[1], Relation.ENEMY) - if len(avatar_list) >= 4: - avatar_list[2].set_relation(avatar_list[3], Relation.FRIEND) - if len(avatar_list) >= 6: - # 师徒(有向):第5位是师傅,第6位是徒弟 - avatar_list[4].set_relation(avatar_list[5], Relation.MASTER) - if len(avatar_list) >= 8: - # 道侣 - avatar_list[6].set_relation(avatar_list[7], Relation.LOVERS) - return avatars + return _new_make(world, count=count, current_month_stamp=current_month_stamp, existed_sects=existed_sects) async def main(): diff --git a/src/sim/new_avatar.py b/src/sim/new_avatar.py new file mode 100644 index 0000000..4fb9f8d --- /dev/null +++ b/src/sim/new_avatar.py @@ -0,0 +1,137 @@ +import random +from typing import List, Optional, Dict + +from src.classes.world import World +from src.classes.map import Map +from src.classes.tile import TileType +from src.classes.avatar import Avatar, Gender +from src.classes.calendar import MonthStamp +from src.classes.cultivation import CultivationProgress +from src.classes.root import Root +from src.classes.age import Age +from src.utils.names import get_random_name_for_sect +from src.utils.id_generator import get_avatar_id +from src.classes.sect import Sect +from src.classes.alignment import Alignment +from src.classes.relation import Relation +from src.classes.technique import get_technique_by_sect, attribute_to_root +from src.classes.treasure import treasures_by_sect_id + + +def random_gender() -> Gender: + return Gender.MALE if random.random() < 0.5 else Gender.FEMALE + + +def get_new_avatar_from_ordinary(world: World, current_month_stamp: MonthStamp, name: str, age: Age) -> Avatar: + """ + 从凡人中来的新修士:最低境界、随机位置,不分配宗门/法宝。 + """ + avatar_id = get_avatar_id() + birth_month_stamp = current_month_stamp - age.age * 12 + random.randint(0, 11) + cultivation_progress = CultivationProgress(0) + pos_x = random.randint(0, world.map.width - 1) + pos_y = random.randint(0, world.map.height - 1) + gender = random.choice(list(Gender)) + return Avatar( + world=world, + name=name, + id=avatar_id, + birth_month_stamp=MonthStamp(birth_month_stamp), + age=age, + gender=gender, + cultivation_progress=cultivation_progress, + pos_x=pos_x, + pos_y=pos_y, + ) + + +def make_avatars( + world: World, + count: int = 12, + current_month_stamp: MonthStamp = MonthStamp(100 * 12), + existed_sects: Optional[List[Sect]] = None, +) -> dict[str, Avatar]: + avatars: dict[str, Avatar] = {} + width, height = world.map.width, world.map.height + + num_total = int(count) + use_sects = bool(existed_sects) + # 约 2/3 为宗门弟子,1/3 为散修 + sect_member_target = int(num_total * 2 / 3) if use_sects else 0 + + # 统计将要分配的宗门成员数量(用于均分) + sect_member_count = 0 + sect_member_counts_by_id: dict[int, int] = {s.id: 0 for s in existed_sects} if existed_sects else {} + + # 本局中“已给出宗门法宝”的标记,确保每个宗门最多一件且仅首次分配 + sect_treasure_assigned: Dict[int, bool] = {} + + for i in range(count): + age_years = random.randint(16, 60) + birth_month_stamp = current_month_stamp - age_years * 12 + random.randint(0, 11) + gender = random_gender() + + # 分配宗门或散修 + assigned_sect: Optional[Sect] = None + if use_sects and sect_member_count < sect_member_target and existed_sects: + min_count = min(sect_member_counts_by_id.values()) if sect_member_counts_by_id else 0 + candidates = [s for s in existed_sects if sect_member_counts_by_id.get(s.id, 0) == min_count] + assigned_sect = random.choice(candidates) + sect_member_counts_by_id[assigned_sect.id] = sect_member_counts_by_id.get(assigned_sect.id, 0) + 1 + sect_member_count += 1 + + name = get_random_name_for_sect(gender, assigned_sect) + + level = random.randint(0, 120) + cultivation_progress = CultivationProgress(level) + age = Age(age_years, cultivation_progress.realm) + + # 出生点: + x, y = random.randint(0, width - 1), random.randint(0, height - 1) + + avatar = Avatar( + world=world, + name=name, + id=get_avatar_id(), + birth_month_stamp=MonthStamp(birth_month_stamp), + age=age, + gender=gender, + cultivation_progress=cultivation_progress, + pos_x=x, + pos_y=y, + root=random.choice(list(Root)), + sect=assigned_sect, + ) + + avatar.tile = world.map.get_tile(x, y) + + if assigned_sect is not None: + avatar.alignment = assigned_sect.alignment + avatar.technique = get_technique_by_sect(assigned_sect) + + # 若该宗门有法宝,且本局尚未分配过,则给该宗门第一个生成的弟子分配法宝 + treasure = treasures_by_sect_id.get(assigned_sect.id) + if treasure is not None and not sect_treasure_assigned.get(assigned_sect.id, False): + avatar.treasure = treasure + sect_treasure_assigned[assigned_sect.id] = True + + mapped_root = attribute_to_root(avatar.technique.attribute) + if mapped_root is not None: + avatar.root = mapped_root + + avatars[avatar.id] = avatar + + # 简单关系样例 + avatar_list = list(avatars.values()) + if len(avatar_list) >= 2: + avatar_list[0].set_relation(avatar_list[1], Relation.ENEMY) + if len(avatar_list) >= 4: + avatar_list[2].set_relation(avatar_list[3], Relation.FRIEND) + if len(avatar_list) >= 6: + avatar_list[4].set_relation(avatar_list[5], Relation.MASTER) + if len(avatar_list) >= 8: + avatar_list[6].set_relation(avatar_list[7], Relation.LOVERS) + + return avatars + + diff --git a/src/sim/simulator.py b/src/sim/simulator.py index 8ff47d1..00867dc 100644 --- a/src/sim/simulator.py +++ b/src/sim/simulator.py @@ -1,7 +1,8 @@ import random from src.classes.calendar import Month, Year, MonthStamp -from src.classes.avatar import Avatar, get_new_avatar_from_ordinary, Gender +from src.classes.avatar import Avatar, Gender +from src.sim.new_avatar import get_new_avatar_from_ordinary from src.classes.age import Age from src.classes.cultivation import Realm from src.classes.world import World diff --git a/static/game_configs/treasure.csv b/static/game_configs/treasure.csv new file mode 100644 index 0000000..18d8ae7 --- /dev/null +++ b/static/game_configs/treasure.csv @@ -0,0 +1,12 @@ +id,name,sect_id,desc,effects +,名称,所属宗门ID(见sect.csv),描述/提示词,效果键值对(不含花括号),例:"extra_move_step":1 +1,本命剑匣,1,以心御剑,匣启如霆,剑意随心破万法.,"extra_battle_strength_points": 3 +2,灵舟,9,小舟承灵气,御风渡海,千里一瞬.,"extra_move_step": 1 +3,千里镜,3,澄澈如镜,观千里之外,洞察先机.,"extra_observation_radius": 2 +4,镇魂钟,7,钟鸣摄魄,定魂镇邪,护心安魂.,"extra_battle_strength_points": 3; "extra_observation_radius": 1 +5,聚灵阵盘,5,刻阵成盘,聚纳灵机,修行事半功倍.,"extra_cultivate_exp": 50 +6,万魂幡,4,幡起万魂啾啾,阴风过境。(效果留空,后续配置), +7,万欲同心结,6,情意同心,双修之道相互映照,修为更精进.,"extra_dual_cultivation_exp": 100 +8,影遁披风,8,融身影界,来去无踪,伏击出其不意.,"extra_move_step": 1; "extra_observation_radius": 1 + +