add treasures

This commit is contained in:
bridge
2025-10-19 01:33:22 +08:00
parent a0abf1cc4b
commit a002d1bd70
10 changed files with 265 additions and 115 deletions

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

78
src/classes/treasure.py Normal file
View File

@@ -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)

View File

@@ -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():

137
src/sim/new_avatar.py Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 99.