rename item -> material & refactor buying action
This commit is contained in:
@@ -133,7 +133,7 @@ You can also join the QQ group for discussion: 1071821688. Verification answer i
|
||||
- [ ] Character compatibility
|
||||
- [ ] Life Skills
|
||||
- ✅ Forging
|
||||
- [ ] Alchemy
|
||||
- ✅ Refine
|
||||
- [ ] Planting
|
||||
- [ ] Taming
|
||||
- [ ] Evolving skills
|
||||
@@ -271,7 +271,7 @@ You can also join the QQ group for discussion: 1071821688. Verification answer i
|
||||
|
||||
## Contributors
|
||||
- Aku, for world design & discussion
|
||||
- [@xzhseh](https://github.com/xzhseh), contributed part of the frontend code
|
||||
- [@xzhseh](https://github.com/xzhseh), contributed code
|
||||
|
||||
## Acknowledgments
|
||||
- Referenced some UI elements from ailifeengine
|
||||
@@ -133,7 +133,7 @@
|
||||
- [ ] 角色间相性
|
||||
- [ ] 生活技能
|
||||
- ✅ 铸造
|
||||
- [ ] 丹药
|
||||
- ✅ 炼丹
|
||||
- [ ] 种植
|
||||
- [ ] 饲养
|
||||
- [ ] 技能可进化
|
||||
@@ -270,7 +270,7 @@
|
||||
|
||||
## 贡献者
|
||||
* Aku, 世界观\玩法设计与讨论
|
||||
* [@xzhseh](https://github.com/xzhseh), 贡献部分前端代码
|
||||
* [@xzhseh](https://github.com/xzhseh), 贡献代码
|
||||
|
||||
## 致谢
|
||||
- 参考了ai life engine部分ui
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from typing import TYPE_CHECKING, Tuple, Any
|
||||
|
||||
from src.classes.action import InstantAction
|
||||
@@ -11,7 +10,7 @@ from src.classes.prices import prices
|
||||
from src.classes.cultivation import Realm
|
||||
from src.classes.weapon import Weapon
|
||||
from src.classes.auxiliary import Auxiliary
|
||||
from src.classes.item import Item
|
||||
from src.classes.material import Material
|
||||
from src.utils.resolution import resolve_query
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -24,7 +23,7 @@ class Buy(InstantAction):
|
||||
|
||||
如果是丹药:购买后强制立即服用。
|
||||
如果是其他物品:购买后放入背包。
|
||||
如果是装备(兵器/法宝):购买后直接装备(替换原有装备)。
|
||||
如果是装备(兵器/法宝):购买后直接装备(替换原有装备,旧装备折价售出)。
|
||||
"""
|
||||
|
||||
ACTION_NAME = "购买"
|
||||
@@ -39,77 +38,46 @@ class Buy(InstantAction):
|
||||
if not isinstance(region, CityRegion):
|
||||
return False, "仅能在城市区域执行"
|
||||
|
||||
res = resolve_query(target_name, expected_types=[Elixir, Weapon, Auxiliary, Item])
|
||||
res = resolve_query(target_name, expected_types=[Elixir, Weapon, Auxiliary, Material])
|
||||
if not res.is_valid:
|
||||
return False, f"未知物品: {target_name}"
|
||||
|
||||
obj = res.obj
|
||||
|
||||
# 检查价格
|
||||
price = prices.get_buying_price(obj, self.avatar)
|
||||
if self.avatar.magic_stone < price:
|
||||
return False, f"灵石不足 (需要 {price})"
|
||||
|
||||
# 丹药特殊限制
|
||||
if isinstance(obj, Elixir):
|
||||
elixir: Elixir = obj
|
||||
|
||||
# 必须是练气期丹药
|
||||
if elixir.realm != Realm.Qi_Refinement:
|
||||
return False, "当前仅开放练气期丹药购买"
|
||||
|
||||
# 境界限制
|
||||
if elixir.realm > self.avatar.cultivation_progress.realm:
|
||||
return False, f"境界不足,无法承受药力 ({elixir.realm.value})"
|
||||
|
||||
# 耐药性/生效中检查
|
||||
for consumed in self.avatar.elixirs:
|
||||
if consumed.elixir.id == elixir.id:
|
||||
if not consumed.is_completely_expired(int(self.world.month_stamp)):
|
||||
return False, "药效尚存,无法重复服用"
|
||||
|
||||
return True, ""
|
||||
# 核心逻辑委托给 Avatar
|
||||
return self.avatar.can_buy_item(res.obj)
|
||||
|
||||
def _execute(self, target_name: str) -> None:
|
||||
res = resolve_query(target_name, expected_types=[Elixir, Weapon, Auxiliary, Item])
|
||||
res = resolve_query(target_name, expected_types=[Elixir, Weapon, Auxiliary, Material])
|
||||
if not res.is_valid:
|
||||
return
|
||||
|
||||
obj = res.obj
|
||||
price = prices.get_buying_price(obj, self.avatar)
|
||||
self.avatar.magic_stone -= price
|
||||
|
||||
# 交付
|
||||
if isinstance(obj, Elixir):
|
||||
self.avatar.consume_elixir(obj)
|
||||
elif isinstance(obj, Item):
|
||||
self.avatar.add_item(obj)
|
||||
elif isinstance(obj, Weapon):
|
||||
# 购买装备需要深拷贝,因为装备有独立状态
|
||||
new_weapon = copy.deepcopy(obj)
|
||||
self.avatar.change_weapon(new_weapon)
|
||||
elif isinstance(obj, Auxiliary):
|
||||
# 购买装备需要深拷贝
|
||||
new_auxiliary = copy.deepcopy(obj)
|
||||
self.avatar.change_auxiliary(new_auxiliary)
|
||||
# 真正执行购买 (含扣款、服用/装备/卖旧)
|
||||
self.avatar.buy_item(res.obj)
|
||||
|
||||
def start(self, target_name: str) -> Event:
|
||||
res = resolve_query(target_name, expected_types=[Elixir, Weapon, Auxiliary, Item])
|
||||
res = resolve_query(target_name, expected_types=[Elixir, Weapon, Auxiliary, Material])
|
||||
obj = res.obj
|
||||
display_name = res.name
|
||||
|
||||
# 预先获取一些信息用于生成文本 (不修改状态)
|
||||
price = prices.get_buying_price(obj, self.avatar)
|
||||
|
||||
# 构造描述
|
||||
action_desc = "购买了"
|
||||
suffix = ""
|
||||
|
||||
if isinstance(obj, Elixir):
|
||||
action_desc = "购买并服用了"
|
||||
elif isinstance(obj, (Weapon, Auxiliary)):
|
||||
action_desc = "购买并装备了"
|
||||
else:
|
||||
action_desc = "购买了"
|
||||
|
||||
price = prices.get_buying_price(obj, self.avatar) if obj else 0
|
||||
|
||||
# 预测是否会有卖旧行为,生成对应描述
|
||||
if isinstance(obj, Weapon) and self.avatar.weapon:
|
||||
suffix = f" (并将原有的{self.avatar.weapon.name}折价售出)"
|
||||
elif isinstance(obj, Auxiliary) and self.avatar.auxiliary:
|
||||
suffix = f" (并将原有的{self.avatar.auxiliary.name}折价售出)"
|
||||
|
||||
return Event(
|
||||
self.world.month_stamp,
|
||||
f"{self.avatar.name} 在城镇花费 {price} 灵石{action_desc} {display_name}",
|
||||
f"{self.avatar.name} 在城镇花费 {price} 灵石{action_desc} {display_name}{suffix}",
|
||||
related_avatars=[self.avatar.id]
|
||||
)
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ from typing import Optional, TYPE_CHECKING, List
|
||||
from src.classes.action import TimedAction
|
||||
from src.classes.cultivation import Realm
|
||||
from src.classes.event import Event
|
||||
from src.classes.item import Item
|
||||
from src.classes.lode import ORE_ITEM_IDS
|
||||
from src.classes.material import Material
|
||||
from src.classes.weapon import get_random_weapon_by_realm
|
||||
from src.classes.auxiliary import get_random_auxiliary_by_realm
|
||||
from src.classes.single_choice import handle_item_exchange
|
||||
@@ -49,12 +48,10 @@ class Cast(TimedAction):
|
||||
def _count_materials(self, realm: Realm) -> int:
|
||||
"""
|
||||
统计符合条件的材料数量。
|
||||
注意:统计所有 Item 类的直接实例,不限于矿石。
|
||||
"""
|
||||
count = 0
|
||||
for item, qty in self.avatar.items.items():
|
||||
# 只要是 Item 实例且境界符合即可
|
||||
if type(item).__name__ == "Item" and item.realm == realm:
|
||||
for material, qty in self.avatar.materials.items():
|
||||
if material.realm == realm:
|
||||
count += qty
|
||||
return count
|
||||
|
||||
@@ -85,19 +82,19 @@ class Cast(TimedAction):
|
||||
|
||||
# 扣除材料逻辑
|
||||
to_deduct = cost
|
||||
items_to_modify = []
|
||||
materials_to_modify = []
|
||||
|
||||
# 再次遍历寻找材料进行扣除
|
||||
for item, qty in self.avatar.items.items():
|
||||
for material, qty in self.avatar.materials.items():
|
||||
if to_deduct <= 0:
|
||||
break
|
||||
if type(item).__name__ == "Item" and item.realm == self.target_realm:
|
||||
if material.realm == self.target_realm:
|
||||
take = min(qty, to_deduct)
|
||||
items_to_modify.append((item, take))
|
||||
materials_to_modify.append((material, take))
|
||||
to_deduct -= take
|
||||
|
||||
for item, take in items_to_modify:
|
||||
self.avatar.remove_item(item, take)
|
||||
for material, take in materials_to_modify:
|
||||
self.avatar.remove_material(material, take)
|
||||
|
||||
realm_val = self.target_realm.value if self.target_realm else target_realm
|
||||
return Event(
|
||||
|
||||
@@ -8,7 +8,7 @@ from src.utils.gather import execute_gather, check_can_start_gather
|
||||
class Harvest(TimedAction):
|
||||
"""
|
||||
采集动作,在有植物的区域进行采集,持续6个月
|
||||
可以获得植物对应的物品
|
||||
可以获得植物对应的材料
|
||||
"""
|
||||
|
||||
ACTION_NAME = "采集"
|
||||
@@ -21,15 +21,15 @@ class Harvest(TimedAction):
|
||||
|
||||
def __init__(self, avatar, world):
|
||||
super().__init__(avatar, world)
|
||||
self.gained_items: dict[str, int] = {}
|
||||
self.gained_materials: dict[str, int] = {}
|
||||
|
||||
def _execute(self) -> None:
|
||||
"""
|
||||
执行采集动作
|
||||
"""
|
||||
gained = execute_gather(self.avatar, "plants", "extra_harvest_items")
|
||||
gained = execute_gather(self.avatar, "plants", "extra_harvest_materials")
|
||||
for name, count in gained.items():
|
||||
self.gained_items[name] = self.gained_items.get(name, 0) + count
|
||||
self.gained_materials[name] = self.gained_materials.get(name, 0) + count
|
||||
|
||||
def can_start(self) -> tuple[bool, str]:
|
||||
return check_can_start_gather(self.avatar, "plants", "植物")
|
||||
@@ -41,9 +41,9 @@ class Harvest(TimedAction):
|
||||
|
||||
async def finish(self) -> list[Event]:
|
||||
# 必定有产出
|
||||
items_desc = "、".join([f"{k}x{v}" for k, v in self.gained_items.items()])
|
||||
materials_desc = "、".join([f"{k}x{v}" for k, v in self.gained_materials.items()])
|
||||
return [Event(
|
||||
self.world.month_stamp,
|
||||
f"{self.avatar.name} 结束了采集,获得了:{items_desc}",
|
||||
f"{self.avatar.name} 结束了采集,获得了:{materials_desc}",
|
||||
related_avatars=[self.avatar.id]
|
||||
)]
|
||||
|
||||
@@ -8,7 +8,7 @@ from src.utils.gather import execute_gather, check_can_start_gather
|
||||
class Hunt(TimedAction):
|
||||
"""
|
||||
狩猎动作,在有动物的区域进行狩猎,持续6个月
|
||||
可以获得动物对应的物品
|
||||
可以获得动物对应的材料
|
||||
"""
|
||||
|
||||
ACTION_NAME = "狩猎"
|
||||
@@ -21,15 +21,15 @@ class Hunt(TimedAction):
|
||||
|
||||
def __init__(self, avatar, world):
|
||||
super().__init__(avatar, world)
|
||||
self.gained_items: dict[str, int] = {}
|
||||
self.gained_materials: dict[str, int] = {}
|
||||
|
||||
def _execute(self) -> None:
|
||||
"""
|
||||
执行狩猎动作
|
||||
"""
|
||||
gained = execute_gather(self.avatar, "animals", "extra_hunt_items")
|
||||
gained = execute_gather(self.avatar, "animals", "extra_hunt_materials")
|
||||
for name, count in gained.items():
|
||||
self.gained_items[name] = self.gained_items.get(name, 0) + count
|
||||
self.gained_materials[name] = self.gained_materials.get(name, 0) + count
|
||||
|
||||
def can_start(self) -> tuple[bool, str]:
|
||||
return check_can_start_gather(self.avatar, "animals", "动物")
|
||||
@@ -41,9 +41,9 @@ class Hunt(TimedAction):
|
||||
|
||||
async def finish(self) -> list[Event]:
|
||||
# 必定有产出
|
||||
items_desc = "、".join([f"{k}x{v}" for k, v in self.gained_items.items()])
|
||||
materials_desc = "、".join([f"{k}x{v}" for k, v in self.gained_materials.items()])
|
||||
return [Event(
|
||||
self.world.month_stamp,
|
||||
f"{self.avatar.name} 结束了狩猎,获得了:{items_desc}",
|
||||
f"{self.avatar.name} 结束了狩猎,获得了:{materials_desc}",
|
||||
related_avatars=[self.avatar.id]
|
||||
)]
|
||||
|
||||
@@ -21,15 +21,15 @@ class Mine(TimedAction):
|
||||
|
||||
def __init__(self, avatar, world):
|
||||
super().__init__(avatar, world)
|
||||
self.gained_items: dict[str, int] = {}
|
||||
self.gained_materials: dict[str, int] = {}
|
||||
|
||||
def _execute(self) -> None:
|
||||
"""
|
||||
执行挖矿动作
|
||||
"""
|
||||
gained = execute_gather(self.avatar, "lodes", "extra_mine_items")
|
||||
gained = execute_gather(self.avatar, "lodes", "extra_mine_materials")
|
||||
for name, count in gained.items():
|
||||
self.gained_items[name] = self.gained_items.get(name, 0) + count
|
||||
self.gained_materials[name] = self.gained_materials.get(name, 0) + count
|
||||
|
||||
def can_start(self) -> tuple[bool, str]:
|
||||
return check_can_start_gather(self.avatar, "lodes", "矿脉")
|
||||
@@ -40,9 +40,9 @@ class Mine(TimedAction):
|
||||
# TimedAction 已统一 step 逻辑
|
||||
|
||||
async def finish(self) -> list[Event]:
|
||||
items_desc = "、".join([f"{k}x{v}" for k, v in self.gained_items.items()])
|
||||
materials_desc = "、".join([f"{k}x{v}" for k, v in self.gained_materials.items()])
|
||||
return [Event(
|
||||
self.world.month_stamp,
|
||||
f"{self.avatar.name} 结束了挖矿,获得了:{items_desc}",
|
||||
f"{self.avatar.name} 结束了挖矿,获得了:{materials_desc}",
|
||||
related_avatars=[self.avatar.id]
|
||||
)]
|
||||
|
||||
@@ -46,12 +46,11 @@ class Refine(TimedAction):
|
||||
def _count_materials(self, realm: Realm) -> int:
|
||||
"""
|
||||
统计符合条件的材料数量。
|
||||
注意:统计所有 Item 类的直接实例,不限于矿石。
|
||||
注意:统计所有材料,不限于矿石。
|
||||
"""
|
||||
count = 0
|
||||
for item, qty in self.avatar.items.items():
|
||||
# 只要是 Item 实例且境界符合即可
|
||||
if type(item).__name__ == "Item" and item.realm == realm:
|
||||
for material, qty in self.avatar.materials.items():
|
||||
if material.realm == realm:
|
||||
count += qty
|
||||
return count
|
||||
|
||||
@@ -62,7 +61,7 @@ class Refine(TimedAction):
|
||||
res = resolve_query(target_realm, expected_types=[Realm])
|
||||
if not res.is_valid:
|
||||
return False, f"无效的境界: {target_realm}"
|
||||
|
||||
|
||||
realm = res.obj
|
||||
|
||||
cost = self._get_cost()
|
||||
@@ -82,19 +81,19 @@ class Refine(TimedAction):
|
||||
|
||||
# 扣除材料逻辑
|
||||
to_deduct = cost
|
||||
items_to_modify = []
|
||||
materials_to_modify = []
|
||||
|
||||
# 再次遍历寻找材料进行扣除
|
||||
for item, qty in self.avatar.items.items():
|
||||
for material, qty in self.avatar.materials.items():
|
||||
if to_deduct <= 0:
|
||||
break
|
||||
if type(item).__name__ == "Item" and item.realm == self.target_realm:
|
||||
if material.realm == self.target_realm:
|
||||
take = min(qty, to_deduct)
|
||||
items_to_modify.append((item, take))
|
||||
materials_to_modify.append((material, take))
|
||||
to_deduct -= take
|
||||
|
||||
for item, take in items_to_modify:
|
||||
self.avatar.remove_item(item, take)
|
||||
for material, take in materials_to_modify:
|
||||
self.avatar.remove_material(material, take)
|
||||
|
||||
realm_val = self.target_realm.value if self.target_realm else target_realm
|
||||
return Event(
|
||||
@@ -133,18 +132,8 @@ class Refine(TimedAction):
|
||||
|
||||
# 3. 成功:生成物品
|
||||
new_item = get_random_elixir_by_realm(self.target_realm)
|
||||
if new_item is None:
|
||||
# 理论上不应该发生,除非该境界没有配置丹药
|
||||
fail_event = Event(
|
||||
self.world.month_stamp,
|
||||
f"{self.avatar.name} 炼制成功,但似乎没有产生任何已知的丹药。",
|
||||
related_avatars=[self.avatar.id],
|
||||
is_major=False
|
||||
)
|
||||
events.append(fail_event)
|
||||
return events
|
||||
|
||||
# 4. 决策:保留还是卖出
|
||||
# 4. 决策:保留(服用)还是卖出
|
||||
base_desc = f"炼丹成功!获得了{self.target_realm.value}丹药『{new_item.name}』。"
|
||||
|
||||
# 事件1:炼丹成功
|
||||
|
||||
@@ -7,7 +7,7 @@ from src.classes.event import Event
|
||||
from src.classes.region import CityRegion
|
||||
from src.classes.normalize import normalize_goods_name
|
||||
from src.utils.resolution import resolve_query
|
||||
from src.classes.item import Item
|
||||
from src.classes.material import Material
|
||||
from src.classes.weapon import Weapon
|
||||
from src.classes.auxiliary import Auxiliary
|
||||
|
||||
@@ -17,7 +17,7 @@ class Sell(InstantAction):
|
||||
在城镇出售指定名称的物品/装备。
|
||||
如果是材料:一次性卖出持有的全部数量。
|
||||
如果是装备:卖出当前装备的(如果是当前装备)。
|
||||
收益通过 avatar.sell_item() / sell_weapon() / sell_auxiliary() 结算。
|
||||
收益通过 avatar.sell_material() / sell_weapon() / sell_auxiliary() 结算。
|
||||
"""
|
||||
|
||||
ACTION_NAME = "出售"
|
||||
@@ -32,19 +32,19 @@ class Sell(InstantAction):
|
||||
return False, "仅能在城市区域执行"
|
||||
|
||||
# 使用通用解析逻辑获取物品原型和类型
|
||||
res = resolve_query(target_name, expected_types=[Item, Weapon, Auxiliary])
|
||||
res = resolve_query(target_name, expected_types=[Material, Weapon, Auxiliary])
|
||||
if not res.is_valid:
|
||||
return False, f"未持有物品/装备: {target_name}"
|
||||
|
||||
obj = res.obj
|
||||
normalized_name = normalize_goods_name(target_name)
|
||||
|
||||
# 1. 如果是物品,检查背包
|
||||
if isinstance(obj, Item):
|
||||
if self.avatar.get_item_quantity(obj) > 0:
|
||||
# 1. 如果是材料,检查背包
|
||||
if isinstance(obj, Material):
|
||||
if self.avatar.get_material_quantity(obj) > 0:
|
||||
pass # 检查通过
|
||||
else:
|
||||
return False, f"未持有物品: {target_name}"
|
||||
return False, f"未持有材料: {target_name}"
|
||||
|
||||
# 2. 如果是兵器,检查当前装备
|
||||
elif isinstance(obj, Weapon):
|
||||
@@ -70,16 +70,16 @@ class Sell(InstantAction):
|
||||
if not isinstance(region, CityRegion):
|
||||
return
|
||||
|
||||
res = resolve_query(target_name, expected_types=[Item, Weapon, Auxiliary])
|
||||
res = resolve_query(target_name, expected_types=[Material, Weapon, Auxiliary])
|
||||
if not res.is_valid:
|
||||
return
|
||||
|
||||
obj = res.obj
|
||||
normalized_name = normalize_goods_name(target_name)
|
||||
|
||||
if isinstance(obj, Item):
|
||||
quantity = self.avatar.get_item_quantity(obj)
|
||||
self.avatar.sell_item(obj, quantity)
|
||||
if isinstance(obj, Material):
|
||||
quantity = self.avatar.get_material_quantity(obj)
|
||||
self.avatar.sell_material(obj, quantity)
|
||||
elif isinstance(obj, Weapon):
|
||||
# 需要再确认一次是否是当前装备
|
||||
if self.avatar.weapon and normalize_goods_name(self.avatar.weapon.name) == normalized_name:
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Optional
|
||||
|
||||
from src.utils.df import game_configs, get_str, get_int, get_list_int
|
||||
from src.utils.config import CONFIG
|
||||
from src.classes.item import Item, items_by_id
|
||||
from src.classes.material import Material, materials_by_id
|
||||
from src.classes.cultivation import Realm
|
||||
|
||||
@dataclass
|
||||
@@ -15,16 +15,16 @@ class Animal:
|
||||
name: str
|
||||
desc: str
|
||||
realm: Realm
|
||||
item_ids: list[int] = field(default_factory=list) # 该动物对应的物品IDs
|
||||
material_ids: list[int] = field(default_factory=list) # 该动物对应的物品IDs
|
||||
|
||||
# 这些字段将在__post_init__中设置
|
||||
items: list[Item] = field(init=False, default_factory=list) # 该动物对应的物品实例
|
||||
materials: list[Material] = field(init=False, default_factory=list) # 该动物对应的物品实例
|
||||
|
||||
def __post_init__(self):
|
||||
"""初始化物品实例"""
|
||||
for item_id in self.item_ids:
|
||||
if item_id in items_by_id:
|
||||
self.items.append(items_by_id[item_id])
|
||||
for material_id in self.material_ids:
|
||||
if material_id in materials_by_id:
|
||||
self.materials.append(materials_by_id[material_id])
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.id)
|
||||
@@ -38,20 +38,20 @@ class Animal:
|
||||
"""
|
||||
info_parts = [f"【{self.name}】({self.realm.value})", self.desc]
|
||||
|
||||
if self.items:
|
||||
item_names = [item.name for item in self.items]
|
||||
info_parts.append(f"可获得材料:{', '.join(item_names)}")
|
||||
if self.materials:
|
||||
material_names = [material.name for material in self.materials]
|
||||
info_parts.append(f"可获得材料:{', '.join(material_names)}")
|
||||
|
||||
return " - ".join(info_parts)
|
||||
|
||||
def get_structured_info(self) -> dict:
|
||||
items_info = [item.get_structured_info() for item in self.items]
|
||||
materials_info = [material.get_structured_info() for material in self.materials]
|
||||
return {
|
||||
"id": str(self.id),
|
||||
"name": self.name,
|
||||
"desc": self.desc,
|
||||
"grade": self.realm.value,
|
||||
"drops": items_info,
|
||||
"drops": materials_info,
|
||||
"type": "animal"
|
||||
}
|
||||
|
||||
@@ -62,14 +62,14 @@ def _load_animals() -> tuple[dict[int, Animal], dict[str, Animal]]:
|
||||
|
||||
animal_df = game_configs["animal"]
|
||||
for row in animal_df:
|
||||
item_ids_list = get_list_int(row, "item_ids")
|
||||
material_ids_list = get_list_int(row, "material_ids")
|
||||
|
||||
animal = Animal(
|
||||
id=get_int(row, "id"),
|
||||
name=get_str(row, "name"),
|
||||
desc=get_str(row, "desc"),
|
||||
realm=Realm.from_id(get_int(row, "stage_id")),
|
||||
item_ids=item_ids_list
|
||||
material_ids=material_ids_list
|
||||
)
|
||||
animals_by_id[animal.id] = animal
|
||||
animals_by_name[animal.name] = animal
|
||||
|
||||
@@ -26,7 +26,7 @@ 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.item import Item
|
||||
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
|
||||
@@ -96,7 +96,7 @@ class Avatar(
|
||||
short_term_objective: str = ""
|
||||
long_term_objective: Optional[LongTermObjective] = None
|
||||
magic_stone: MagicStone = field(default_factory=lambda: MagicStone(0))
|
||||
items: dict[Item, int] = field(default_factory=dict)
|
||||
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
|
||||
|
||||
@@ -50,7 +50,7 @@ def get_avatar_info(avatar: "Avatar", detailed: bool = False) -> dict:
|
||||
technique_info = avatar.technique.get_detailed_info() if avatar.technique is not None else "无"
|
||||
cultivation_info = avatar.cultivation_progress.get_detailed_info()
|
||||
personas_info = ", ".join([p.get_detailed_info() for p in avatar.personas]) if avatar.personas else "无"
|
||||
items_info = ",".join([f"{item.get_detailed_info()}x{quantity}" for item, quantity in avatar.items.items()]) if avatar.items else "无"
|
||||
materials_info = ",".join([f"{mat.get_detailed_info()}x{quantity}" for mat, quantity in avatar.materials.items()]) if avatar.materials else "无"
|
||||
appearance_info = avatar.appearance.get_detailed_info(avatar.gender)
|
||||
spirit_animal_info = avatar.spirit_animal.get_info() if avatar.spirit_animal is not None else "无"
|
||||
else:
|
||||
@@ -63,7 +63,7 @@ def get_avatar_info(avatar: "Avatar", detailed: bool = False) -> dict:
|
||||
technique_info = avatar.technique.get_info() if avatar.technique is not None else "无"
|
||||
cultivation_info = avatar.cultivation_progress.get_info()
|
||||
personas_info = ", ".join([p.get_detailed_info() for p in avatar.personas]) if avatar.personas else "无"
|
||||
items_info = ",".join([f"{item.get_info()}x{quantity}" for item, quantity in avatar.items.items()]) if avatar.items else "无"
|
||||
materials_info = ",".join([f"{mat.get_info()}x{quantity}" for mat, quantity in avatar.materials.items()]) if avatar.materials else "无"
|
||||
appearance_info = avatar.appearance.get_info()
|
||||
spirit_animal_info = avatar.spirit_animal.get_info() if avatar.spirit_animal is not None else "无"
|
||||
|
||||
@@ -81,7 +81,7 @@ def get_avatar_info(avatar: "Avatar", detailed: bool = False) -> dict:
|
||||
"功法": technique_info,
|
||||
"境界": cultivation_info,
|
||||
"特质": personas_info,
|
||||
"物品": items_info,
|
||||
"材料": materials_info,
|
||||
"外貌": appearance_info,
|
||||
"兵器": weapon_info,
|
||||
"辅助装备": auxiliary_info,
|
||||
@@ -185,13 +185,13 @@ def get_avatar_structured_info(avatar: "Avatar") -> dict:
|
||||
else:
|
||||
info["auxiliary"] = None
|
||||
|
||||
# 5. 物品 (Items)
|
||||
items_list = []
|
||||
for item, count in avatar.items.items():
|
||||
i_info = item.get_structured_info()
|
||||
i_info["count"] = count
|
||||
items_list.append(i_info)
|
||||
info["items"] = items_list
|
||||
# 5. 材料 (Materials)
|
||||
materials_list = []
|
||||
for material, count in avatar.materials.items():
|
||||
m_info = material.get_structured_info()
|
||||
m_info["count"] = count
|
||||
materials_list.append(m_info)
|
||||
info["materials"] = materials_list
|
||||
|
||||
# 6. 关系 (Relations)
|
||||
relations_list = []
|
||||
|
||||
@@ -3,73 +3,74 @@ Avatar 物品与装备管理 Mixin
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Optional, Any, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.avatar.core import Avatar
|
||||
from src.classes.item import Item
|
||||
from src.classes.material import Material
|
||||
from src.classes.weapon import Weapon
|
||||
from src.classes.auxiliary import Auxiliary
|
||||
from src.classes.elixir import Elixir
|
||||
|
||||
|
||||
class InventoryMixin:
|
||||
"""物品与装备管理相关方法"""
|
||||
|
||||
def add_item(self: "Avatar", item: "Item", quantity: int = 1) -> None:
|
||||
def add_material(self: "Avatar", material: "Material", quantity: int = 1) -> None:
|
||||
"""
|
||||
添加物品到背包
|
||||
|
||||
Args:
|
||||
item: 要添加的物品
|
||||
material: 要添加的物品
|
||||
quantity: 添加数量,默认为1
|
||||
"""
|
||||
if quantity <= 0:
|
||||
return
|
||||
|
||||
if item in self.items:
|
||||
self.items[item] += quantity
|
||||
if material in self.materials:
|
||||
self.materials[material] += quantity
|
||||
else:
|
||||
self.items[item] = quantity
|
||||
self.materials[material] = quantity
|
||||
|
||||
def remove_item(self: "Avatar", item: "Item", quantity: int = 1) -> bool:
|
||||
def remove_material(self: "Avatar", material: "Material", quantity: int = 1) -> bool:
|
||||
"""
|
||||
从背包移除物品
|
||||
从背包移除材料
|
||||
|
||||
Args:
|
||||
item: 要移除的物品
|
||||
material: 要移除的材料
|
||||
quantity: 移除数量,默认为1
|
||||
|
||||
Returns:
|
||||
bool: 是否成功移除(如果物品不足则返回False)
|
||||
bool: 是否成功移除(如果材料不足则返回False)
|
||||
"""
|
||||
if quantity <= 0:
|
||||
return True
|
||||
|
||||
if item not in self.items:
|
||||
if material not in self.materials:
|
||||
return False
|
||||
|
||||
if self.items[item] < quantity:
|
||||
if self.materials[material] < quantity:
|
||||
return False
|
||||
|
||||
self.items[item] -= quantity
|
||||
self.materials[material] -= quantity
|
||||
|
||||
# 如果数量为0,从字典中移除该物品
|
||||
if self.items[item] == 0:
|
||||
del self.items[item]
|
||||
if self.materials[material] == 0:
|
||||
del self.materials[material]
|
||||
|
||||
return True
|
||||
|
||||
def get_item_quantity(self: "Avatar", item: "Item") -> int:
|
||||
def get_material_quantity(self: "Avatar", material: "Material") -> int:
|
||||
"""
|
||||
获取指定物品的数量
|
||||
获取指定材料的数量
|
||||
|
||||
Args:
|
||||
item: 要查询的物品
|
||||
material: 要查询的材料
|
||||
|
||||
Returns:
|
||||
int: 物品数量,如果没有该物品则返回0
|
||||
int: 材料数量,如果没有该材料则返回0
|
||||
"""
|
||||
return self.items.get(item, 0)
|
||||
return self.materials.get(material, 0)
|
||||
|
||||
def change_weapon(self: "Avatar", new_weapon: "Weapon") -> None:
|
||||
"""
|
||||
@@ -106,20 +107,20 @@ class InventoryMixin:
|
||||
|
||||
# ==================== 出售接口 ====================
|
||||
|
||||
def sell_item(self: "Avatar", item: "Item", quantity: int = 1) -> int:
|
||||
def sell_material(self: "Avatar", material: "Material", quantity: int = 1) -> int:
|
||||
"""
|
||||
出售材料物品,返回获得的灵石数量。
|
||||
应用 extra_item_sell_price_multiplier 效果。
|
||||
"""
|
||||
from src.classes.prices import prices
|
||||
|
||||
if quantity <= 0 or self.get_item_quantity(item) < quantity:
|
||||
if quantity <= 0 or self.get_material_quantity(material) < quantity:
|
||||
return 0
|
||||
|
||||
self.remove_item(item, quantity)
|
||||
self.remove_material(material, quantity)
|
||||
|
||||
# 使用统一的卖出价格接口(包含所有加成逻辑)
|
||||
unit_price = prices.get_selling_price(item, self)
|
||||
unit_price = prices.get_selling_price(material, self)
|
||||
total = unit_price * quantity
|
||||
|
||||
self.magic_stone = self.magic_stone + total
|
||||
@@ -155,3 +156,132 @@ class InventoryMixin:
|
||||
self.magic_stone = self.magic_stone + total
|
||||
return total
|
||||
|
||||
def sell_elixir(self: "Avatar", elixir: "Elixir") -> int:
|
||||
"""
|
||||
出售丹药,返回获得的灵石数量。
|
||||
"""
|
||||
from src.classes.prices import prices
|
||||
|
||||
# 使用统一的卖出价格接口
|
||||
total = prices.get_selling_price(elixir, self)
|
||||
self.magic_stone = self.magic_stone + total
|
||||
return total
|
||||
|
||||
# ==================== 购买接口 ====================
|
||||
|
||||
def can_buy_item(self: "Avatar", obj: Any) -> tuple[bool, str]:
|
||||
"""
|
||||
检查是否可以购买指定物品。
|
||||
涵盖价格检查、境界限制、耐药性等。
|
||||
"""
|
||||
from src.classes.elixir import Elixir
|
||||
from src.classes.prices import prices
|
||||
from src.classes.cultivation import Realm
|
||||
|
||||
# 1. 检查价格
|
||||
price = prices.get_buying_price(obj, self)
|
||||
if self.magic_stone < price:
|
||||
return False, f"灵石不足 (需要 {price})"
|
||||
|
||||
# 2. 丹药特殊检查
|
||||
if isinstance(obj, Elixir):
|
||||
# 商店业务规则:当前仅开放练气期丹药购买
|
||||
if obj.realm != Realm.Qi_Refinement:
|
||||
return False, "当前仅开放练气期丹药购买"
|
||||
|
||||
# 境界限制
|
||||
if obj.realm > self.cultivation_progress.realm:
|
||||
return False, f"境界不足,无法承受药力 ({obj.realm.value})"
|
||||
|
||||
# 耐药性/生效中检查
|
||||
for consumed in self.elixirs:
|
||||
if consumed.elixir.id == obj.id:
|
||||
if not consumed.is_completely_expired(int(self.world.month_stamp)):
|
||||
return False, "药效尚存,无法重复服用"
|
||||
|
||||
return True, ""
|
||||
|
||||
def buy_item(self: "Avatar", obj: Any) -> dict:
|
||||
"""
|
||||
执行购买逻辑。
|
||||
包括扣款、获得物品(服用/入包/装备)、以旧换新。
|
||||
返回交易报告 dict。
|
||||
"""
|
||||
import copy
|
||||
from src.classes.elixir import Elixir
|
||||
from src.classes.weapon import Weapon
|
||||
from src.classes.auxiliary import Auxiliary
|
||||
from src.classes.material import Material
|
||||
from src.classes.prices import prices
|
||||
|
||||
report = {
|
||||
"success": True,
|
||||
"cost": 0,
|
||||
"action_type": "store", # store, consume, equip
|
||||
"sold_item_name": None,
|
||||
"sold_item_refund": 0
|
||||
}
|
||||
|
||||
# 1. 扣款
|
||||
price = prices.get_buying_price(obj, self)
|
||||
self.magic_stone -= price
|
||||
report["cost"] = price
|
||||
|
||||
# 2. 交付
|
||||
if isinstance(obj, Elixir):
|
||||
# 购买即服用
|
||||
self.consume_elixir(obj)
|
||||
report["action_type"] = "consume"
|
||||
|
||||
elif isinstance(obj, Material):
|
||||
# 放入背包
|
||||
self.add_material(obj)
|
||||
report["action_type"] = "store"
|
||||
|
||||
elif isinstance(obj, (Weapon, Auxiliary)):
|
||||
# 装备需要深拷贝
|
||||
new_equip = copy.deepcopy(obj)
|
||||
|
||||
# 尝试卖出旧装备并换上新装备
|
||||
sold_name, refund = self._equip_and_trade_in(new_equip)
|
||||
|
||||
report["action_type"] = "equip"
|
||||
if sold_name:
|
||||
report["sold_item_name"] = sold_name
|
||||
report["sold_item_refund"] = refund
|
||||
|
||||
return report
|
||||
|
||||
def _equip_and_trade_in(self: "Avatar", new_equip: Union["Weapon", "Auxiliary"]) -> tuple[str | None, int]:
|
||||
"""
|
||||
内部方法:装备新物品,并尝试卖出旧物品(如果有)。
|
||||
返回: (旧物品名称, 卖出金额)
|
||||
"""
|
||||
from src.classes.weapon import Weapon
|
||||
from src.classes.auxiliary import Auxiliary
|
||||
|
||||
sold_name = None
|
||||
refund = 0
|
||||
|
||||
if isinstance(new_equip, Weapon):
|
||||
# 检查是否有旧兵器
|
||||
if self.weapon:
|
||||
sold_name = self.weapon.name
|
||||
# sell_weapon 会把旧兵器加到 circulation 并加钱给 avatar
|
||||
# 注意:sell_weapon 不会 clear self.weapon,也不会 deepcopy(因为是直接把引用给 circulation)
|
||||
# 这是正确的,旧对象给系统,新对象上身
|
||||
refund = self.sell_weapon(self.weapon)
|
||||
|
||||
# 换上新兵器 (覆盖 self.weapon)
|
||||
self.change_weapon(new_equip)
|
||||
|
||||
elif isinstance(new_equip, Auxiliary):
|
||||
# 检查是否有旧法宝
|
||||
if self.auxiliary:
|
||||
sold_name = self.auxiliary.name
|
||||
refund = self.sell_auxiliary(self.auxiliary)
|
||||
|
||||
# 换上新法宝
|
||||
self.change_auxiliary(new_equip)
|
||||
|
||||
return sold_name, refund
|
||||
|
||||
@@ -7,8 +7,9 @@ from .consts import (
|
||||
CULTIVATE_DURATION_REDUCTION,
|
||||
EXTRA_BREAKTHROUGH_SUCCESS_RATE,
|
||||
EXTRA_DUAL_CULTIVATION_EXP,
|
||||
EXTRA_HARVEST_ITEMS,
|
||||
EXTRA_HUNT_ITEMS,
|
||||
EXTRA_HARVEST_MATERIALS,
|
||||
EXTRA_HUNT_MATERIALS,
|
||||
EXTRA_MINE_MATERIALS,
|
||||
EXTRA_MOVE_STEP,
|
||||
EXTRA_CATCH_SUCCESS_RATE,
|
||||
EXTRA_ESCAPE_SUCCESS_RATE,
|
||||
|
||||
@@ -105,30 +105,38 @@ EXTRA_DUAL_CULTIVATION_EXP = "extra_dual_cultivation_exp"
|
||||
"""
|
||||
|
||||
# --- 采集相关 ---
|
||||
EXTRA_HARVEST_ITEMS = "extra_harvest_items"
|
||||
EXTRA_HARVEST_MATERIALS = "extra_harvest_materials"
|
||||
"""
|
||||
额外采集物品数量
|
||||
额外采集材料数量
|
||||
类型: int
|
||||
结算: src/classes/action/harvest.py
|
||||
说明: 采集植物时额外获得的物品数量。
|
||||
说明: 采集植物时额外获得的材料数量。
|
||||
数值参考:
|
||||
- 微量: 1
|
||||
- 中量: 2
|
||||
- 大量: 3
|
||||
"""
|
||||
|
||||
EXTRA_HUNT_ITEMS = "extra_hunt_items"
|
||||
EXTRA_HUNT_MATERIALS = "extra_hunt_materials"
|
||||
"""
|
||||
额外狩猎物品数量
|
||||
额外狩猎材料数量
|
||||
类型: int
|
||||
结算: src/classes/action/hunt.py
|
||||
说明: 狩猎动物时额外获得的物品数量。
|
||||
说明: 狩猎动物时额外获得的材料数量。
|
||||
数值参考:
|
||||
- 微量: 1
|
||||
- 中量: 2
|
||||
- 大量: 3
|
||||
"""
|
||||
|
||||
EXTRA_MINE_MATERIALS = "extra_mine_materials"
|
||||
"""
|
||||
额外挖矿材料数量
|
||||
类型: int
|
||||
结算: src/classes/action/mine.py
|
||||
说明: 挖矿时额外获得的材料数量。
|
||||
"""
|
||||
|
||||
# --- 移动相关 ---
|
||||
EXTRA_MOVE_STEP = "extra_move_step"
|
||||
"""
|
||||
@@ -421,8 +429,9 @@ ALL_EFFECTS = [
|
||||
"extra_dual_cultivation_exp", # int - 额外双修经验
|
||||
|
||||
# 采集相关
|
||||
"extra_harvest_items", # int - 额外采集物品数量
|
||||
"extra_hunt_items", # int - 额外狩猎物品数量
|
||||
"extra_harvest_materials", # int - 额外采集材料数量
|
||||
"extra_hunt_materials", # int - 额外狩猎材料数量
|
||||
"extra_mine_materials", # int - 额外挖矿材料数量
|
||||
|
||||
# 移动相关
|
||||
"extra_move_step", # int - 额外移动步数
|
||||
|
||||
@@ -10,8 +10,9 @@ EFFECT_DESC_MAP = {
|
||||
"extra_breakthrough_success_rate": "突破成功率",
|
||||
"extra_fortune_probability": "奇遇概率",
|
||||
"extra_misfortune_probability": "霉运概率",
|
||||
"extra_harvest_items": "采集获取物品",
|
||||
"extra_hunt_items": "狩猎获取物品",
|
||||
"extra_harvest_materials": "采集获取材料",
|
||||
"extra_hunt_materials": "狩猎获取材料",
|
||||
"extra_mine_materials": "挖矿获取材料",
|
||||
"extra_item_sell_price_multiplier": "物品出售价格",
|
||||
"shop_buy_price_reduction": "购买折扣",
|
||||
"extra_weapon_upgrade_chance": "兵器升级概率",
|
||||
@@ -163,7 +164,7 @@ def format_effects_to_text(effects: dict[str, Any] | list[dict[str, Any]]) -> st
|
||||
|
||||
desc_list = []
|
||||
for k, v in effects.items():
|
||||
if k == "when":
|
||||
if k in ["when", "duration_month"]:
|
||||
continue
|
||||
|
||||
# 跳过 eval 表达式或者无法解析的 key,或者直接显示 key
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from src.utils.df import game_configs, get_str, get_int
|
||||
from src.classes.cultivation import Realm
|
||||
|
||||
@dataclass
|
||||
class Item:
|
||||
"""
|
||||
物品
|
||||
"""
|
||||
id: int
|
||||
name: str
|
||||
desc: str
|
||||
realm: Realm
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.id)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def get_info(self) -> str:
|
||||
return f"{self.name} -({self.realm.value})"
|
||||
|
||||
def get_detailed_info(self) -> str:
|
||||
return f"{self.name} - {self.desc}({self.realm.value})"
|
||||
|
||||
def get_structured_info(self) -> dict:
|
||||
return {
|
||||
"id": str(self.id),
|
||||
"name": self.name,
|
||||
"desc": self.desc,
|
||||
"grade": self.realm.value,
|
||||
"effect_desc": "" # 物品暂时没有效果字段
|
||||
}
|
||||
|
||||
def _load_items() -> tuple[dict[int, Item], dict[str, Item]]:
|
||||
"""从配表加载item数据"""
|
||||
items_by_id: dict[int, Item] = {}
|
||||
items_by_name: dict[str, Item] = {}
|
||||
|
||||
item_df = game_configs["item"]
|
||||
for row in item_df:
|
||||
item = Item(
|
||||
id=get_int(row, "id"),
|
||||
name=get_str(row, "name"),
|
||||
desc=get_str(row, "desc"),
|
||||
realm=Realm.from_id(get_int(row, "stage_id"))
|
||||
)
|
||||
items_by_id[item.id] = item
|
||||
items_by_name[item.name] = item
|
||||
|
||||
return items_by_id, items_by_name
|
||||
|
||||
# 从配表加载item数据
|
||||
items_by_id, items_by_name = _load_items()
|
||||
@@ -2,7 +2,7 @@ from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
from src.utils.df import game_configs, get_str, get_int, get_list_int
|
||||
from src.classes.item import Item, items_by_id
|
||||
from src.classes.material import Material, materials_by_id
|
||||
from src.classes.cultivation import Realm
|
||||
|
||||
@dataclass
|
||||
@@ -14,16 +14,16 @@ class Lode:
|
||||
name: str
|
||||
desc: str
|
||||
realm: Realm
|
||||
item_ids: list[int] = field(default_factory=list) # 该矿脉对应的物品IDs
|
||||
material_ids: list[int] = field(default_factory=list) # 该矿脉对应的物品IDs
|
||||
|
||||
# 这些字段将在__post_init__中设置
|
||||
items: list[Item] = field(init=False, default_factory=list) # 该矿脉对应的物品实例
|
||||
materials: list[Material] = field(init=False, default_factory=list) # 该矿脉对应的物品实例
|
||||
|
||||
def __post_init__(self):
|
||||
"""初始化物品实例"""
|
||||
for item_id in self.item_ids:
|
||||
if item_id in items_by_id:
|
||||
self.items.append(items_by_id[item_id])
|
||||
for material_id in self.material_ids:
|
||||
if material_id in materials_by_id:
|
||||
self.materials.append(materials_by_id[material_id])
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.id)
|
||||
@@ -37,20 +37,20 @@ class Lode:
|
||||
"""
|
||||
info_parts = [f"【{self.name}】({self.realm.value})", self.desc]
|
||||
|
||||
if self.items:
|
||||
item_names = [item.name for item in self.items]
|
||||
info_parts.append(f"可获得矿石:{', '.join(item_names)}")
|
||||
if self.materials:
|
||||
material_names = [material.name for material in self.materials]
|
||||
info_parts.append(f"可获得矿石:{', '.join(material_names)}")
|
||||
|
||||
return " - ".join(info_parts)
|
||||
|
||||
def get_structured_info(self) -> dict:
|
||||
items_info = [item.get_structured_info() for item in self.items]
|
||||
materials_info = [material.get_structured_info() for material in self.materials]
|
||||
return {
|
||||
"id": str(self.id),
|
||||
"name": self.name,
|
||||
"desc": self.desc,
|
||||
"grade": self.realm.value,
|
||||
"drops": items_info,
|
||||
"drops": materials_info,
|
||||
"type": "lode"
|
||||
}
|
||||
|
||||
@@ -65,14 +65,14 @@ def _load_lodes() -> tuple[dict[int, Lode], dict[str, Lode]]:
|
||||
|
||||
lode_df = game_configs["lode"]
|
||||
for row in lode_df:
|
||||
item_ids_list = get_list_int(row, "item_ids")
|
||||
material_ids_list = get_list_int(row, "material_ids")
|
||||
|
||||
lode = Lode(
|
||||
id=get_int(row, "id"),
|
||||
name=get_str(row, "name"),
|
||||
desc=get_str(row, "desc"),
|
||||
realm=Realm.from_id(get_int(row, "stage_id")),
|
||||
item_ids=item_ids_list
|
||||
material_ids=material_ids_list
|
||||
)
|
||||
lodes_by_id[lode.id] = lode
|
||||
lodes_by_name[lode.name] = lode
|
||||
@@ -83,5 +83,4 @@ def _load_lodes() -> tuple[dict[int, Lode], dict[str, Lode]]:
|
||||
lodes_by_id, lodes_by_name = _load_lodes()
|
||||
|
||||
# 导出所有属于矿石的物品ID,供铸造逻辑判断
|
||||
ORE_ITEM_IDS = {item_id for lode in lodes_by_id.values() for item_id in lode.item_ids}
|
||||
|
||||
ORE_MATERIAL_IDS = {material_id for lode in lodes_by_id.values() for material_id in lode.material_ids}
|
||||
|
||||
57
src/classes/material.py
Normal file
57
src/classes/material.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from src.utils.df import game_configs, get_str, get_int
|
||||
from src.classes.cultivation import Realm
|
||||
|
||||
@dataclass
|
||||
class Material:
|
||||
"""
|
||||
材料
|
||||
"""
|
||||
id: int
|
||||
name: str
|
||||
desc: str
|
||||
realm: Realm
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.id)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def get_info(self) -> str:
|
||||
return f"{self.name} -({self.realm.value})"
|
||||
|
||||
def get_detailed_info(self) -> str:
|
||||
return f"{self.name} - {self.desc}({self.realm.value})"
|
||||
|
||||
def get_structured_info(self) -> dict:
|
||||
return {
|
||||
"id": str(self.id),
|
||||
"name": self.name,
|
||||
"desc": self.desc,
|
||||
"grade": self.realm.value,
|
||||
"effect_desc": "" # 材料暂时没有效果字段
|
||||
}
|
||||
|
||||
def _load_materials() -> tuple[dict[int, Material], dict[str, Material]]:
|
||||
"""从配表加载 material 数据"""
|
||||
materials_by_id: dict[int, Material] = {}
|
||||
materials_by_name: dict[str, Material] = {}
|
||||
|
||||
if "material" in game_configs:
|
||||
material_df = game_configs["material"]
|
||||
for row in material_df:
|
||||
material = Material(
|
||||
id=get_int(row, "id"),
|
||||
name=get_str(row, "name"),
|
||||
desc=get_str(row, "desc"),
|
||||
realm=Realm.from_id(get_int(row, "stage_id"))
|
||||
)
|
||||
materials_by_id[material.id] = material
|
||||
materials_by_name[material.name] = material
|
||||
|
||||
return materials_by_id, materials_by_name
|
||||
|
||||
# 从配表加载 material 数据
|
||||
materials_by_id, materials_by_name = _load_materials()
|
||||
@@ -3,7 +3,7 @@ from typing import Optional
|
||||
|
||||
from src.utils.df import game_configs, get_str, get_int, get_list_int
|
||||
from src.utils.config import CONFIG
|
||||
from src.classes.item import Item, items_by_id
|
||||
from src.classes.material import Material, materials_by_id
|
||||
from src.classes.cultivation import Realm
|
||||
|
||||
@dataclass
|
||||
@@ -15,16 +15,16 @@ class Plant:
|
||||
name: str
|
||||
desc: str
|
||||
realm: Realm
|
||||
item_ids: list[int] = field(default_factory=list) # 该植物对应的物品IDs
|
||||
material_ids: list[int] = field(default_factory=list) # 该植物对应的物品IDs
|
||||
|
||||
# 这些字段将在__post_init__中设置
|
||||
items: list[Item] = field(init=False, default_factory=list) # 该植物对应的物品实例
|
||||
materials: list[Material] = field(init=False, default_factory=list) # 该植物对应的物品实例
|
||||
|
||||
def __post_init__(self):
|
||||
"""初始化物品实例"""
|
||||
for item_id in self.item_ids:
|
||||
if item_id in items_by_id:
|
||||
self.items.append(items_by_id[item_id])
|
||||
for material_id in self.material_ids:
|
||||
if material_id in materials_by_id:
|
||||
self.materials.append(materials_by_id[material_id])
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.id)
|
||||
@@ -38,20 +38,20 @@ class Plant:
|
||||
"""
|
||||
info_parts = [f"【{self.name}】({self.realm.value})", self.desc]
|
||||
|
||||
if self.items:
|
||||
item_names = [item.name for item in self.items]
|
||||
info_parts.append(f"可获得材料:{', '.join(item_names)}")
|
||||
if self.materials:
|
||||
material_names = [material.name for material in self.materials]
|
||||
info_parts.append(f"可获得材料:{', '.join(material_names)}")
|
||||
|
||||
return " - ".join(info_parts)
|
||||
|
||||
def get_structured_info(self) -> dict:
|
||||
items_info = [item.get_structured_info() for item in self.items]
|
||||
materials_info = [material.get_structured_info() for material in self.materials]
|
||||
return {
|
||||
"id": str(self.id),
|
||||
"name": self.name,
|
||||
"desc": self.desc,
|
||||
"grade": self.realm.value,
|
||||
"drops": items_info,
|
||||
"drops": materials_info,
|
||||
"type": "plant"
|
||||
}
|
||||
|
||||
@@ -62,14 +62,14 @@ def _load_plants() -> tuple[dict[int, Plant], dict[str, Plant]]:
|
||||
|
||||
plant_df = game_configs["plant"]
|
||||
for row in plant_df:
|
||||
item_ids_list = get_list_int(row, "item_ids")
|
||||
material_ids_list = get_list_int(row, "material_ids")
|
||||
|
||||
plant = Plant(
|
||||
id=get_int(row, "id"),
|
||||
name=get_str(row, "name"),
|
||||
desc=get_str(row, "desc"),
|
||||
realm=Realm.from_id(get_int(row, "stage_id")),
|
||||
item_ids=item_ids_list
|
||||
material_ids=material_ids_list
|
||||
)
|
||||
plants_by_id[plant.id] = plant
|
||||
plants_by_name[plant.name] = plant
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
价格只和对应的 realm 绑定,全局统一。
|
||||
|
||||
价格设计参考(以练气期年收入约 20-30 灵石为基准):
|
||||
- 材料(Item): 采集物等消耗品
|
||||
- 材料(Material): 采集物等消耗品
|
||||
- 兵器(Weapon): 稀有装备,价值较高
|
||||
- 辅助装备(Auxiliary): 法宝等,价值次于兵器
|
||||
"""
|
||||
@@ -17,13 +17,13 @@ from typing import Union, TYPE_CHECKING
|
||||
from src.classes.cultivation import Realm
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.item import Item
|
||||
from src.classes.material import Material
|
||||
from src.classes.weapon import Weapon
|
||||
from src.classes.auxiliary import Auxiliary
|
||||
from src.classes.avatar import Avatar
|
||||
|
||||
# 类型别名
|
||||
Sellable = Union["Item", "Weapon", "Auxiliary"]
|
||||
Sellable = Union["Material", "Weapon", "Auxiliary"]
|
||||
|
||||
|
||||
class Prices:
|
||||
@@ -36,11 +36,11 @@ class Prices:
|
||||
GLOBAL_BUY_MULTIPLIER = 1.5
|
||||
|
||||
# 材料价格表(采集物等)
|
||||
ITEM_PRICES = {
|
||||
MATERIAL_PRICES = {
|
||||
Realm.Qi_Refinement: 10,
|
||||
Realm.Foundation_Establishment: 30,
|
||||
Realm.Core_Formation: 60,
|
||||
Realm.Nascent_Soul: 100,
|
||||
Realm.Core_Formation: 50,
|
||||
Realm.Nascent_Soul: 70,
|
||||
}
|
||||
|
||||
# 兵器价格表(稀有,价值高)
|
||||
@@ -59,9 +59,9 @@ class Prices:
|
||||
Realm.Nascent_Soul: 1600,
|
||||
}
|
||||
|
||||
def get_item_price(self, item: "Item") -> int:
|
||||
def get_material_price(self, material: "Material") -> int:
|
||||
"""获取材料基础价格"""
|
||||
return self.ITEM_PRICES.get(item.realm, 10)
|
||||
return self.MATERIAL_PRICES.get(material.realm, 10)
|
||||
|
||||
def get_weapon_price(self, weapon: "Weapon") -> int:
|
||||
"""获取兵器基础价格"""
|
||||
@@ -77,13 +77,13 @@ class Prices:
|
||||
根据对象类型自动分发到对应的价格查询方法。
|
||||
注意:这是物品的【基准价值】,通常等于玩家【卖出给系统】的基础价格。
|
||||
"""
|
||||
from src.classes.item import Item
|
||||
from src.classes.material import Material
|
||||
from src.classes.weapon import Weapon
|
||||
from src.classes.auxiliary import Auxiliary
|
||||
from src.classes.elixir import Elixir
|
||||
|
||||
if isinstance(obj, Item):
|
||||
return self.get_item_price(obj)
|
||||
if isinstance(obj, Material):
|
||||
return self.get_material_price(obj)
|
||||
elif isinstance(obj, Weapon):
|
||||
return self.get_weapon_price(obj)
|
||||
elif isinstance(obj, Auxiliary):
|
||||
|
||||
@@ -76,22 +76,49 @@ def _get_item_ops(avatar: "Avatar", item_type: str) -> dict:
|
||||
return {
|
||||
"label": "兵器",
|
||||
"get_current": lambda: avatar.weapon,
|
||||
"equip": avatar.change_weapon,
|
||||
"sell": avatar.sell_weapon
|
||||
"use_func": avatar.change_weapon,
|
||||
"sell_func": avatar.sell_weapon,
|
||||
"verbs": {
|
||||
"action": "装备",
|
||||
"done": "换上了",
|
||||
"replace": "替换"
|
||||
}
|
||||
}
|
||||
elif item_type == "auxiliary":
|
||||
return {
|
||||
"label": "辅助装备",
|
||||
"get_current": lambda: avatar.auxiliary,
|
||||
"equip": avatar.change_auxiliary,
|
||||
"sell": avatar.sell_auxiliary
|
||||
"use_func": avatar.change_auxiliary,
|
||||
"sell_func": avatar.sell_auxiliary,
|
||||
"verbs": {
|
||||
"action": "装备",
|
||||
"done": "换上了",
|
||||
"replace": "替换"
|
||||
}
|
||||
}
|
||||
elif item_type == "technique":
|
||||
return {
|
||||
"label": "功法",
|
||||
"get_current": lambda: avatar.technique,
|
||||
"equip": lambda x: setattr(avatar, 'technique', x),
|
||||
"sell": None # 功法通常不能卖
|
||||
"use_func": lambda x: setattr(avatar, 'technique', x),
|
||||
"sell_func": None, # 功法通常不能卖
|
||||
"verbs": {
|
||||
"action": "修炼",
|
||||
"done": "改修了",
|
||||
"replace": "替换"
|
||||
}
|
||||
}
|
||||
elif item_type == "elixir":
|
||||
return {
|
||||
"label": "丹药",
|
||||
"get_current": lambda: None, # 丹药没有“当前装备”的概念,都是新获得的
|
||||
"use_func": avatar.consume_elixir,
|
||||
"sell_func": avatar.sell_elixir,
|
||||
"verbs": {
|
||||
"action": "服用",
|
||||
"done": "服用了",
|
||||
"replace": "替换" # 丹药其实没有 replace,但为了模板通用可以给个默认值
|
||||
}
|
||||
}
|
||||
else:
|
||||
raise ValueError(f"Unsupported item type: {item_type}")
|
||||
@@ -100,7 +127,7 @@ def _get_item_ops(avatar: "Avatar", item_type: str) -> dict:
|
||||
async def handle_item_exchange(
|
||||
avatar: "Avatar",
|
||||
new_item: Any,
|
||||
item_type: str, # "weapon", "auxiliary", "technique"
|
||||
item_type: str, # "weapon", "auxiliary", "technique", "elixir"
|
||||
context_intro: str,
|
||||
can_sell_new: bool = False
|
||||
) -> Tuple[bool, str]:
|
||||
@@ -119,6 +146,7 @@ async def handle_item_exchange(
|
||||
"""
|
||||
ops = _get_item_ops(avatar, item_type)
|
||||
label = ops["label"]
|
||||
verbs = ops["verbs"]
|
||||
current_item = ops["get_current"]()
|
||||
|
||||
new_name = new_item.name
|
||||
@@ -126,8 +154,8 @@ async def handle_item_exchange(
|
||||
|
||||
# 1. 自动装备:当前无装备且不强制考虑卖新
|
||||
if current_item is None and not can_sell_new:
|
||||
ops["equip"](new_item)
|
||||
return True, f"{avatar.name} 获得了{new_grade}{label}『{new_name}』并装备。"
|
||||
ops["use_func"](new_item)
|
||||
return True, f"{avatar.name} 获得了{new_grade}{label}『{new_name}』并{verbs['action']}。"
|
||||
|
||||
# 2. 需要决策:准备描述
|
||||
old_name = current_item.name if current_item else ""
|
||||
@@ -137,19 +165,19 @@ async def handle_item_exchange(
|
||||
if current_item:
|
||||
old_info = current_item.get_info(detailed=True)
|
||||
swap_desc = f"现有{label}:{old_info}\n{swap_desc}"
|
||||
if ops["sell"]:
|
||||
swap_desc += f"\n(选择替换将卖出旧{label})"
|
||||
if ops["sell_func"]:
|
||||
swap_desc += f"\n(选择{verbs['replace']}将卖出旧{label})"
|
||||
|
||||
# 3. 构建选项
|
||||
# Option A: 装备新物品
|
||||
opt_a_text = f"装备新{label}『{new_name}』"
|
||||
if current_item and ops["sell"]:
|
||||
# Option A: 装备/服用新物品
|
||||
opt_a_text = f"{verbs['action']}新{label}『{new_name}』"
|
||||
if current_item and ops["sell_func"]:
|
||||
opt_a_text += f",卖掉旧{label}『{old_name}』"
|
||||
elif current_item:
|
||||
opt_a_text += f",替换旧{label}『{old_name}』"
|
||||
opt_a_text += f",{verbs['replace']}旧{label}『{old_name}』"
|
||||
|
||||
# Option B: 拒绝新物品
|
||||
if can_sell_new and ops["sell"]:
|
||||
if can_sell_new and ops["sell_func"]:
|
||||
opt_b_text = f"卖掉新{label}『{new_name}』换取灵石,保留现状"
|
||||
else:
|
||||
opt_b_text = f"放弃『{new_name}』"
|
||||
@@ -167,15 +195,15 @@ async def handle_item_exchange(
|
||||
# 4. 执行决策
|
||||
if choice == "A":
|
||||
# 卖旧(如果有且能卖)
|
||||
if current_item and ops["sell"]:
|
||||
ops["sell"](current_item)
|
||||
# 装新
|
||||
ops["equip"](new_item)
|
||||
return True, f"{avatar.name} 换上了{new_grade}{label}『{new_name}』。"
|
||||
if current_item and ops["sell_func"]:
|
||||
ops["sell_func"](current_item)
|
||||
# 装新/服用
|
||||
ops["use_func"](new_item)
|
||||
return True, f"{avatar.name} {verbs['done']}{new_grade}{label}『{new_name}』。"
|
||||
else:
|
||||
# 卖新(如果被要求且能卖)
|
||||
if can_sell_new and ops["sell"]:
|
||||
sold_price = ops["sell"](new_item)
|
||||
if can_sell_new and ops["sell_func"]:
|
||||
sold_price = ops["sell_func"](new_item)
|
||||
return False, f"{avatar.name} 卖掉了新获得的{new_name},获利 {sold_price} 灵石。"
|
||||
else:
|
||||
return False, f"{avatar.name} 放弃了{new_name}。"
|
||||
|
||||
@@ -41,7 +41,7 @@ class AvatarLoadMixin:
|
||||
from src.classes.age import Age
|
||||
from src.classes.hp import HP
|
||||
from src.classes.technique import techniques_by_id
|
||||
from src.classes.item import items_by_id
|
||||
from src.classes.material import materials_by_id
|
||||
from src.classes.weapon import weapons_by_id
|
||||
from src.classes.auxiliary import auxiliaries_by_id
|
||||
from src.classes.sect import sects_by_id
|
||||
@@ -92,13 +92,13 @@ class AvatarLoadMixin:
|
||||
# 设置物品与资源
|
||||
avatar.magic_stone = MagicStone(data.get("magic_stone", 0))
|
||||
|
||||
# 重建items
|
||||
items_dict = data.get("items", {})
|
||||
avatar.items = {}
|
||||
for item_id_str, quantity in items_dict.items():
|
||||
item_id = int(item_id_str)
|
||||
if item_id in items_by_id:
|
||||
avatar.items[items_by_id[item_id]] = quantity
|
||||
# 重建materials (兼容旧档 items)
|
||||
materials_dict = data.get("materials") or data.get("items") or {}
|
||||
avatar.materials = {}
|
||||
for mat_id_str, quantity in materials_dict.items():
|
||||
mat_id = int(mat_id_str)
|
||||
if mat_id in materials_by_id:
|
||||
avatar.materials[materials_by_id[mat_id]] = quantity
|
||||
|
||||
# 重建weapon
|
||||
# 使用copy而非deepcopy,避免潜在的递归引用导致的崩溃,且性能更好
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
加载流程(两阶段):
|
||||
1. 第一阶段:加载所有Avatar对象(relations留空)
|
||||
- 通过AvatarLoadMixin.from_save_dict反序列化
|
||||
- 配表对象(Technique, Item等)通过id从全局字典获取
|
||||
- 配表对象(Technique, Material等)通过id从全局字典获取
|
||||
2. 第二阶段:重建Avatar之间的relations网络
|
||||
- 必须在所有Avatar加载完成后才能建立引用关系
|
||||
|
||||
@@ -123,12 +123,7 @@ def load_game(save_path: Optional[Path] = None) -> Tuple["World", "Simulator", L
|
||||
avatar.relations[other_avatar] = relation
|
||||
|
||||
# 将所有avatar添加到world
|
||||
# 根据生死状态分流
|
||||
for avatar in all_avatars.values():
|
||||
if avatar.is_dead:
|
||||
world.avatar_manager.dead_avatars[avatar.id] = avatar
|
||||
else:
|
||||
world.avatar_manager.avatars[avatar.id] = avatar
|
||||
world.avatar_manager.avatars = all_avatars
|
||||
|
||||
# 恢复洞府主人关系
|
||||
cultivate_regions_hosts = world_data.get("cultivate_regions_hosts", {})
|
||||
|
||||
@@ -4,9 +4,9 @@ Avatar存档序列化Mixin
|
||||
将Avatar的序列化逻辑从avatar.py分离出来,保持核心类的清晰性。
|
||||
|
||||
存档策略:
|
||||
- 引用对象(Technique, Item等):保存id,加载时从全局字典获取
|
||||
- 引用对象(Technique, Material等):保存id,加载时从全局字典获取
|
||||
- relations:转换为dict[str, str](avatar_id -> relation_value)
|
||||
- items:转换为dict[int, int](item_id -> quantity)
|
||||
- materials:转换为dict[int, int](material_id -> quantity)
|
||||
- current_action:保存动作类名和参数
|
||||
- weapon/auxiliary:需要深拷贝(因为special_data是实例特有的)
|
||||
"""
|
||||
@@ -30,10 +30,10 @@ class AvatarSaveMixin:
|
||||
for other, relation in self.relations.items()
|
||||
}
|
||||
|
||||
# 序列化items: dict[Item, int] -> dict[int, int]
|
||||
items_dict = {
|
||||
item.id: quantity
|
||||
for item, quantity in self.items.items()
|
||||
# 序列化materials: dict[Material, int] -> dict[int, int]
|
||||
materials_dict = {
|
||||
material.id: quantity
|
||||
for material, quantity in self.materials.items()
|
||||
}
|
||||
|
||||
# 序列化current_action
|
||||
@@ -75,7 +75,7 @@ class AvatarSaveMixin:
|
||||
|
||||
# 物品与资源
|
||||
"magic_stone": self.magic_stone.value,
|
||||
"items": items_dict,
|
||||
"materials": materials_dict,
|
||||
"weapon_id": self.weapon.id if self.weapon else None,
|
||||
"weapon_special_data": self.weapon.special_data if self.weapon else {},
|
||||
"weapon_proficiency": self.weapon_proficiency,
|
||||
|
||||
@@ -37,7 +37,7 @@ def execute_gather(
|
||||
) -> dict[str, int]:
|
||||
"""
|
||||
执行采集逻辑。
|
||||
返回: {item_name: count}
|
||||
返回: {material_name: count}
|
||||
"""
|
||||
from src.classes.region import NormalRegion
|
||||
region = avatar.tile.region
|
||||
@@ -61,16 +61,16 @@ def execute_gather(
|
||||
target = random.choice(available)
|
||||
|
||||
# 2. 随机选择产出物
|
||||
if not hasattr(target, "items") or not target.items:
|
||||
if not hasattr(target, "materials") or not target.materials:
|
||||
return {}
|
||||
|
||||
item = random.choice(target.items)
|
||||
material = random.choice(target.materials)
|
||||
|
||||
base_quantity = 1
|
||||
extra_items = int(avatar.effects.get(extra_effect_key, 0) or 0)
|
||||
total_quantity = base_quantity + extra_items
|
||||
extra_materials = int(avatar.effects.get(extra_effect_key, 0) or 0)
|
||||
total_quantity = base_quantity + extra_materials
|
||||
|
||||
avatar.add_item(item, total_quantity)
|
||||
avatar.add_material(material, total_quantity)
|
||||
|
||||
return {item.name: total_quantity}
|
||||
return {material.name: total_quantity}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from src.classes.normalize import normalize_goods_name, normalize_name, normaliz
|
||||
from src.classes.elixir import elixirs_by_name, Elixir
|
||||
from src.classes.weapon import weapons_by_name, Weapon
|
||||
from src.classes.auxiliary import auxiliaries_by_name, Auxiliary
|
||||
from src.classes.item import items_by_name, Item
|
||||
from src.classes.material import materials_by_name, Material
|
||||
from src.classes.cultivation import Realm
|
||||
|
||||
@dataclass
|
||||
@@ -36,7 +36,7 @@ def resolve_query(
|
||||
query: 待解析的对象(字符串名 或 直接的对象实例)
|
||||
world: 世界对象上下文(用于查找 Avatar, Region)
|
||||
expected_types: 期望的类型列表。如果提供,将优先或仅尝试这些类型。
|
||||
支持的类型: Item, Weapon, Elixir, Auxiliary, Region, Avatar, Realm 等类对象
|
||||
支持的类型: Material, Weapon, Elixir, Auxiliary, Region, Avatar, Realm 等类对象
|
||||
必须直接传入类对象,不再支持字符串名。
|
||||
"""
|
||||
if query is None:
|
||||
@@ -73,7 +73,7 @@ def resolve_query(
|
||||
|
||||
t_name = t.__name__
|
||||
|
||||
if t_name in ["Item", "Weapon", "Elixir", "Auxiliary"]:
|
||||
if t_name in ["Material", "Weapon", "Elixir", "Auxiliary"]:
|
||||
if "goods" not in checks: checks.append("goods")
|
||||
elif t_name == "Region" or t_name == "CityRegion" or t_name == "SectRegion" or t_name == "CultivateRegion":
|
||||
if "region" not in checks: checks.append("region")
|
||||
@@ -124,9 +124,9 @@ def _resolve_goods(name: str) -> Any | None:
|
||||
if norm in auxiliaries_by_name:
|
||||
return auxiliaries_by_name[norm]
|
||||
|
||||
# 4. 普通物品
|
||||
if norm in items_by_name:
|
||||
return items_by_name[norm]
|
||||
# 4. 材料
|
||||
if norm in materials_by_name:
|
||||
return materials_by_name[norm]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
id,name,desc,stage_id,item_ids
|
||||
id,name,desc,stage_id,material_ids
|
||||
,,,"该动物对应的物品ID"
|
||||
1,灵兔,天性机敏的灵性兔子,毛色雪白,蕴含微弱灵力,性情温和易驯服,1,1
|
||||
2,魔狼,凶猛的魔性狼族,体型巨大,拥有强大的魔力和锋利的爪牙,2,2
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
id,name,rarity,effects,desc,duration_years
|
||||
1,紫气东来,R,{extra_cultivate_exp: 15},天地灵气充沛,修士修行速度大增,修行欲望提高,5
|
||||
2,金煞之年,R,"{extra_battle_strength_points: 3}",金煞充盈天地肃杀,修士更大可能嗜血而相互攻伐,5
|
||||
3,木灵盛世,R,"{extra_harvest_items: 2, extra_hp_recovery_rate: 0.5}",木德滋养生机盎然,采集收获倍增且伤势恢复极快,宜四处搜罗天材地宝,5
|
||||
3,木灵盛世,R,"{extra_harvest_materials: 2, extra_hp_recovery_rate: 0.5}",木德滋养生机盎然,采集收获倍增且伤势恢复极快,宜四处搜罗天材地宝,5
|
||||
4,水德之纪,R,"{extra_cultivate_exp: 20, cultivate_duration_reduction: 0.2}",水行流转通达无碍,修炼效率与速度双重提升,正是闭关苦修的大好时机,5
|
||||
5,火劫时代,R,"{extra_battle_strength_points: 5, extra_max_lifespan: -20}",天火燃烧劫数降临,战力暴涨但寿元流逝,当速战速决以命搏天,5
|
||||
6,土厚之世,R,"{damage_reduction: 0.15, extra_max_hp: 150}",土德厚重载物无疆,身躯坚韧血气充盈,可无惧强敌正面争锋,5
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
id,name,desc,stage_id,item_ids
|
||||
id,name,desc,stage_id,material_ids
|
||||
,,,"该矿脉对应的物品ID"
|
||||
1,玄铁矿脉,蕴含玄铁的普通矿脉,常见于浅层地表,1,101
|
||||
2,赤铜矿脉,深埋地下的赤铜矿脉,周围往往伴生火热之气,2,102
|
||||
|
||||
|
@@ -6,8 +6,8 @@ id,name,exclusion_names,desc,rarity,condition,effects
|
||||
4,冒险,怠惰;惜命,你总是会冒险,喜欢刺激,总想放手一搏。,N,
|
||||
5,随性,理性;极端正义;极端邪恶,你总是会随机应变,性子到哪里了就是哪里,没有一定之规。,N,
|
||||
6,贪财,,你对灵石和财富有着强烈的渴望。,N,,{extra_item_sell_price_multiplier: 0.1}
|
||||
7,采集者,,喜欢在山林中寻找各种奇花异草和灵药,对植物有着敏锐的直觉和深厚的兴趣。你认为大自然的恩赐需要用心去发现和珍惜。,R,,{extra_harvest_items: 1}
|
||||
8,猎人,,享受在野外追踪猎物的刺激感,对各种动物的习性了如指掌,喜欢捕猎野兽。情况允许也会御兽。,R,,{extra_hunt_items: 1}
|
||||
7,采集者,,喜欢在山林中寻找各种奇花异草和灵药,对植物有着敏锐的直觉和深厚的兴趣。你认为大自然的恩赐需要用心去发现和珍惜。,R,,{extra_harvest_materials: 1}
|
||||
8,猎人,,享受在野外追踪猎物的刺激感,对各种动物的习性了如指掌,喜欢捕猎野兽。情况允许也会御兽。,R,,{extra_hunt_materials: 1}
|
||||
9,沉思,无常,你总是会深思熟虑,思考问题比较有哲理。,N,
|
||||
10,惜命,冒险;极端正义;极端邪恶,你总是会珍惜自己的生命,不会轻易冒险。,R,,{extra_escape_success_rate: 0.15}
|
||||
11,友爱,孤僻;淡漠;好斗;复仇;极端正义;极端邪恶,你重视同伴与和谐,乐于助人,倾向通过协作与沟通化解矛盾。,N,
|
||||
@@ -62,4 +62,4 @@ id,name,exclusion_names,desc,rarity,condition,effects
|
||||
60,大器晚成,,早年修行多舛,霉运连连;但若能坚持至金丹元婴,便可否极泰来,气运亨通。,SR,,"[{when: 'avatar.cultivation_progress.realm.value in [""练气"", ""筑基""]', extra_misfortune_probability: 0.005}, {when: 'avatar.cultivation_progress.realm.value in [""金丹"", ""元婴""]', extra_fortune_probability: 0.01}]"
|
||||
61,炼器师,好斗,精通炼器之道,对材料敏锐,擅长铸造法宝。你认为法宝是修行的关键,战斗并非你的专长。,R,,{extra_cast_success_rate: 0.15}
|
||||
62,情绪化,理性;淡漠,你的情绪波动很大,极易受外界事件影响而改变心情,做事也更随心所欲。,N,,
|
||||
63,矿工,怠惰,擅长勘探挖掘,对矿脉有着独特的直觉。你认为地下的宝藏才是最实在的财富。,R,,{extra_mine_items: 1}
|
||||
63,矿工,怠惰,擅长勘探挖掘,对矿脉有着独特的直觉。你认为地下的宝藏才是最实在的财富。,R,,{extra_mine_materials: 1}
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
id,name,desc,stage_id,item_ids
|
||||
id,name,desc,stage_id,material_ids
|
||||
,,,"该植物对应的物品ID"
|
||||
1,奇草,生长在灵气充沛之地的奇异草药,叶片呈淡蓝色,具有神奇的治愈效果,1,10
|
||||
2,灵木,千年古树吸收天地灵气而成,木质坚硬且蕴含浓郁灵力,2,11
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
id,name,desc,member_act_style,alignment,weight,preferred_weapon,effects,rank_names
|
||||
,,宗门名称与描述,宗门成员行事风格,阵营(正/中立/邪),权重(默认1),倾向兵器类型,"effects(JSON形式,支持宽松格式,见effects.py说明)",自定义职位(掌门;长老;内门;外门)
|
||||
1,明心剑宗,"通玄界东方第一宗,以无上剑道称雄于世。云纹禁制为不传心法。【剑道专精】作为剑修,你使用剑类兵器时战力惊人,且在剑道上的感悟速度远超常人。",清明克己,行止如一。重剑与心法并重,讲究明心见性。,正,1,剑,"{extra_battle_strength_points: 3, extra_weapon_proficiency_gain: 0.5}",
|
||||
2,百兽宗,"以驯养灵兽闻名,豢养各种妖兽灵怪为战力。【御兽大师】你拥有独特的御兽天赋,捕捉妖兽对你来说轻而易举,善于驱使兽群为你而战。",言语直接,重视力量与血性,崇尚狩猎与搏斗。,邪,1,鞭,"{extra_catch_success_rate: 0.25, extra_hunt_items: 1}",谷主;供奉;驭兽师;扈从
|
||||
2,百兽宗,"以驯养灵兽闻名,豢养各种妖兽灵怪为战力。【御兽大师】你拥有独特的御兽天赋,捕捉妖兽对你来说轻而易举,善于驱使兽群为你而战。",言语直接,重视力量与血性,崇尚狩猎与搏斗。,邪,1,鞭,"{extra_catch_success_rate: 0.25, extra_hunt_materials: 1}",谷主;供奉;驭兽师;扈从
|
||||
3,水镜宗,"正道十宗之一,实则严守中立。拥有仙界异宝""彻天水镜""可预知未来。【趋吉避凶】你拥有超乎常人的直觉,视野开阔,且极易在探索中发现奇遇。",处事冷静圆融,喜以柔克刚,擅借力与反制。,中立,1,扇,"{extra_observation_radius: 2, extra_fortune_probability: 0.002, extra_refine_success_rate: 0.05}",镜主;掌镜人;传人;侍镜
|
||||
4,冥王宗,"行走幽冥之道,术法阴冷狠厉。【通幽】你修行幽冥之法,心志坚定,突破瓶颈时心无杂念,成功率更高。",言辞冷厉少情,敬畏因果而不惧杀伐,偏向效率与结果。,邪,1,扇,"{extra_breakthrough_success_rate: 0.1}",殿主;判官;无常;鬼卒
|
||||
5,朱勾宗,"邪宗大派。以炼器、机关、暗杀闻名于世,素来阴毒冷僻。【暗杀专家】你精通潜伏与刺杀,对于强敌,避开正面交锋、伺机暗杀往往是你最佳的制胜之道。",直面欲望与代价,不惧黑暗,以攻伐见长。,邪,1,暗器,"{extra_assassinate_success_rate: 0.15, extra_battle_strength_points: 1, extra_cast_success_rate: 0.05}",楼主;掌刑使;影刺;探子
|
||||
|
||||
|
@@ -3,7 +3,9 @@ from unittest.mock import patch, MagicMock
|
||||
from src.classes.action.buy import Buy
|
||||
from src.classes.region import CityRegion, Region
|
||||
from src.classes.elixir import Elixir, ElixirType, ConsumedElixir
|
||||
from src.classes.item import Item
|
||||
from src.classes.material import Material
|
||||
from src.classes.weapon import Weapon
|
||||
from src.classes.weapon_type import WeaponType
|
||||
from src.classes.cultivation import Realm
|
||||
from src.classes.tile import Tile, TileType
|
||||
|
||||
@@ -21,9 +23,9 @@ def create_test_elixir(name, realm, price=100, elixir_id=1, effects=None):
|
||||
effects=effects
|
||||
)
|
||||
|
||||
def create_test_item(name, realm, item_id=101):
|
||||
return Item(
|
||||
id=item_id,
|
||||
def create_test_material(name, realm, material_id=101):
|
||||
return Material(
|
||||
id=material_id,
|
||||
name=name,
|
||||
desc="测试物品",
|
||||
realm=realm
|
||||
@@ -50,11 +52,11 @@ def avatar_in_city(dummy_avatar):
|
||||
@pytest.fixture
|
||||
def mock_objects():
|
||||
"""
|
||||
Mock elixirs_by_name 和 items_by_name
|
||||
Mock elixirs_by_name 和 materials_by_name
|
||||
"""
|
||||
test_elixir = create_test_elixir("聚气丹", Realm.Qi_Refinement, price=100)
|
||||
high_level_elixir = create_test_elixir("筑基丹", Realm.Foundation_Establishment, price=1000, elixir_id=2)
|
||||
test_item = create_test_item("铁矿石", Realm.Qi_Refinement)
|
||||
test_material = create_test_material("铁矿石", Realm.Qi_Refinement)
|
||||
|
||||
# elixirs_by_name 是 Dict[str, List[Elixir]]
|
||||
elixirs_mock = {
|
||||
@@ -62,19 +64,19 @@ def mock_objects():
|
||||
"筑基丹": [high_level_elixir]
|
||||
}
|
||||
|
||||
# items_by_name 是 Dict[str, Item]
|
||||
items_mock = {
|
||||
"铁矿石": test_item
|
||||
# materials_by_name 是 Dict[str, Material]
|
||||
materials_mock = {
|
||||
"铁矿石": test_material
|
||||
}
|
||||
|
||||
return elixirs_mock, items_mock, test_elixir, high_level_elixir, test_item
|
||||
return elixirs_mock, materials_mock, test_elixir, high_level_elixir, test_material
|
||||
|
||||
def test_buy_item_success(avatar_in_city, mock_objects):
|
||||
"""测试购买普通物品成功"""
|
||||
elixirs_mock, items_mock, _, _, test_item = mock_objects
|
||||
"""测试购买普通材料成功"""
|
||||
elixirs_mock, materials_mock, _, _, test_material = mock_objects
|
||||
|
||||
with patch("src.utils.resolution.elixirs_by_name", elixirs_mock), \
|
||||
patch("src.utils.resolution.items_by_name", items_mock):
|
||||
patch("src.utils.resolution.materials_by_name", materials_mock):
|
||||
|
||||
action = Buy(avatar_in_city, avatar_in_city.world)
|
||||
|
||||
@@ -84,21 +86,21 @@ def test_buy_item_success(avatar_in_city, mock_objects):
|
||||
|
||||
# 2. 执行购买
|
||||
initial_money = avatar_in_city.magic_stone
|
||||
# 练气期物品基础价格 10,倍率 1.5 -> 15
|
||||
# 练气期材料基础价格 10,倍率 1.5 -> 15
|
||||
expected_price = int(10 * 1.5)
|
||||
|
||||
action._execute("铁矿石")
|
||||
|
||||
# 3. 验证结果
|
||||
assert avatar_in_city.magic_stone == initial_money - expected_price
|
||||
assert avatar_in_city.get_item_quantity(test_item) == 1
|
||||
assert avatar_in_city.get_material_quantity(test_material) == 1
|
||||
|
||||
def test_buy_elixir_success(avatar_in_city, mock_objects):
|
||||
"""测试购买并服用丹药成功"""
|
||||
elixirs_mock, items_mock, test_elixir, _, _ = mock_objects
|
||||
elixirs_mock, materials_mock, test_elixir, _, _ = mock_objects
|
||||
|
||||
with patch("src.utils.resolution.elixirs_by_name", elixirs_mock), \
|
||||
patch("src.utils.resolution.items_by_name", items_mock):
|
||||
patch("src.utils.resolution.materials_by_name", materials_mock):
|
||||
|
||||
action = Buy(avatar_in_city, avatar_in_city.world)
|
||||
|
||||
@@ -108,28 +110,26 @@ def test_buy_elixir_success(avatar_in_city, mock_objects):
|
||||
initial_money = avatar_in_city.magic_stone
|
||||
expected_price = int(test_elixir.price * 1.5)
|
||||
|
||||
# 模拟服用丹药的行为(因为 consume_elixir 是 Avatar 的方法,我们可以信赖它,
|
||||
# 但为了单元测试的隔离性,或者确认它被调用了,可以验证副作用)
|
||||
# 这里直接验证副作用:elixirs 列表增加
|
||||
# 模拟服用丹药的行为
|
||||
|
||||
action._execute("聚气丹")
|
||||
|
||||
assert avatar_in_city.magic_stone == initial_money - expected_price
|
||||
# 背包里不应该有丹药
|
||||
assert len(avatar_in_city.items) == 0
|
||||
assert len(avatar_in_city.materials) == 0
|
||||
# 已服用列表应该有
|
||||
assert len(avatar_in_city.elixirs) == 1
|
||||
assert avatar_in_city.elixirs[0].elixir.name == "聚气丹"
|
||||
|
||||
def test_buy_fail_not_in_city(dummy_avatar, mock_objects):
|
||||
"""测试不在城市无法购买"""
|
||||
elixirs_mock, items_mock, _, _, _ = mock_objects
|
||||
elixirs_mock, materials_mock, _, _, _ = mock_objects
|
||||
|
||||
# 确保不在城市 (dummy_avatar 默认在 (0,0) PLAIN)
|
||||
assert not isinstance(dummy_avatar.tile.region, CityRegion)
|
||||
|
||||
with patch("src.utils.resolution.elixirs_by_name", elixirs_mock), \
|
||||
patch("src.utils.resolution.items_by_name", items_mock):
|
||||
patch("src.utils.resolution.materials_by_name", materials_mock):
|
||||
|
||||
action = Buy(dummy_avatar, dummy_avatar.world)
|
||||
can_start, reason = action.can_start("铁矿石")
|
||||
@@ -139,12 +139,12 @@ def test_buy_fail_not_in_city(dummy_avatar, mock_objects):
|
||||
|
||||
def test_buy_fail_no_money(avatar_in_city, mock_objects):
|
||||
"""测试没钱无法购买"""
|
||||
elixirs_mock, items_mock, _, _, test_item = mock_objects
|
||||
elixirs_mock, materials_mock, _, _, test_material = mock_objects
|
||||
|
||||
avatar_in_city.magic_stone = 0 # 没钱
|
||||
|
||||
with patch("src.utils.resolution.elixirs_by_name", elixirs_mock), \
|
||||
patch("src.utils.resolution.items_by_name", items_mock):
|
||||
patch("src.utils.resolution.materials_by_name", materials_mock):
|
||||
|
||||
action = Buy(avatar_in_city, avatar_in_city.world)
|
||||
can_start, reason = action.can_start("铁矿石")
|
||||
@@ -154,10 +154,10 @@ def test_buy_fail_no_money(avatar_in_city, mock_objects):
|
||||
|
||||
def test_buy_fail_unknown_item(avatar_in_city, mock_objects):
|
||||
"""测试未知物品"""
|
||||
elixirs_mock, items_mock, _, _, _ = mock_objects
|
||||
elixirs_mock, materials_mock, _, _, _ = mock_objects
|
||||
|
||||
with patch("src.utils.resolution.elixirs_by_name", elixirs_mock), \
|
||||
patch("src.utils.resolution.items_by_name", items_mock):
|
||||
patch("src.utils.resolution.materials_by_name", materials_mock):
|
||||
|
||||
action = Buy(avatar_in_city, avatar_in_city.world)
|
||||
can_start, reason = action.can_start("不存在的东西")
|
||||
@@ -165,43 +165,38 @@ def test_buy_fail_unknown_item(avatar_in_city, mock_objects):
|
||||
assert can_start is False
|
||||
assert "未知物品" in reason
|
||||
|
||||
def test_buy_elixir_fail_high_level_restricted(avatar_in_city, mock_objects):
|
||||
"""测试购买高阶丹药被限制"""
|
||||
elixirs_mock, items_mock, _, high_level_elixir, _ = mock_objects
|
||||
def test_buy_elixir_fail_high_level_restricted(avatar_in_city, mock_objects):
|
||||
"""测试购买高阶丹药被限制"""
|
||||
elixirs_mock, materials_mock, _, high_level_elixir, _ = mock_objects
|
||||
|
||||
# 给予足够金钱,避免因为钱不够而先报错
|
||||
avatar_in_city.magic_stone = 10000
|
||||
|
||||
# 角色是练气期,尝试买筑基期丹药
|
||||
assert avatar_in_city.cultivation_progress.realm == Realm.Qi_Refinement
|
||||
assert high_level_elixir.realm == Realm.Foundation_Establishment
|
||||
|
||||
with patch("src.utils.resolution.elixirs_by_name", elixirs_mock), \
|
||||
patch("src.utils.resolution.materials_by_name", materials_mock):
|
||||
|
||||
# 给予足够金钱,避免因为钱不够而先报错
|
||||
avatar_in_city.magic_stone = 10000
|
||||
action = Buy(avatar_in_city, avatar_in_city.world)
|
||||
can_start, reason = action.can_start("筑基丹")
|
||||
|
||||
# 角色是练气期,尝试买筑基期丹药
|
||||
assert avatar_in_city.cultivation_progress.realm == Realm.Qi_Refinement
|
||||
assert high_level_elixir.realm == Realm.Foundation_Establishment
|
||||
|
||||
with patch("src.utils.resolution.elixirs_by_name", elixirs_mock), \
|
||||
patch("src.utils.resolution.items_by_name", items_mock):
|
||||
|
||||
action = Buy(avatar_in_city, avatar_in_city.world)
|
||||
can_start, reason = action.can_start("筑基丹")
|
||||
|
||||
assert can_start is False
|
||||
# 当前版本限制仅开放练气期丹药
|
||||
assert "当前仅开放练气期丹药购买" in reason
|
||||
assert can_start is False
|
||||
# 当前版本限制仅开放练气期丹药
|
||||
assert "当前仅开放练气期丹药购买" in reason
|
||||
|
||||
def test_buy_elixir_fail_duplicate_active(avatar_in_city, mock_objects):
|
||||
"""测试药效尚存无法重复购买"""
|
||||
elixirs_mock, items_mock, test_elixir, _, _ = mock_objects
|
||||
elixirs_mock, materials_mock, test_elixir, _, _ = mock_objects
|
||||
|
||||
# 先服用一个
|
||||
consumed = ConsumedElixir(test_elixir, int(avatar_in_city.world.month_stamp))
|
||||
# 假设它是持久效果或未过期
|
||||
# ConsumedElixir 计算过期时间依赖 effects,我们在 create_test_elixir 里如果不给 duration_month,默认是 inf 或者是 0 (Action里的逻辑是看 is_completely_expired)
|
||||
# 这里的 mock elixir 默认 effects 是 {"max_hp": 10},没有 duration_month,所以是永久效果?
|
||||
# 查阅 ConsumedElixir._get_max_duration: 如果没有 duration_month, return inf (永久)。
|
||||
# 所以这应该是永久生效的。
|
||||
|
||||
avatar_in_city.elixirs.append(consumed)
|
||||
|
||||
with patch("src.utils.resolution.elixirs_by_name", elixirs_mock), \
|
||||
patch("src.utils.resolution.items_by_name", items_mock):
|
||||
patch("src.utils.resolution.materials_by_name", materials_mock):
|
||||
|
||||
action = Buy(avatar_in_city, avatar_in_city.world)
|
||||
can_start, reason = action.can_start("聚气丹")
|
||||
@@ -209,4 +204,45 @@ def test_buy_elixir_fail_duplicate_active(avatar_in_city, mock_objects):
|
||||
assert can_start is False
|
||||
assert "药效尚存" in reason
|
||||
|
||||
|
||||
def test_buy_weapon_trade_in(avatar_in_city, mock_objects):
|
||||
"""测试购买新武器时自动卖出旧武器"""
|
||||
elixirs_mock, materials_mock, _, _, _ = mock_objects
|
||||
|
||||
# 构造旧武器和新武器
|
||||
old_weapon = Weapon(id=201, name="铁剑", weapon_type=WeaponType.SWORD, realm=Realm.Qi_Refinement, desc="...", effects={'atk': 1})
|
||||
new_weapon = Weapon(id=202, name="青云剑", weapon_type=WeaponType.SWORD, realm=Realm.Qi_Refinement, desc="...", effects={'atk': 10})
|
||||
|
||||
# 装备旧武器
|
||||
avatar_in_city.change_weapon(old_weapon)
|
||||
assert avatar_in_city.weapon == old_weapon
|
||||
|
||||
materials_mock["青云剑"] = new_weapon
|
||||
|
||||
initial_money = avatar_in_city.magic_stone
|
||||
|
||||
# 价格计算
|
||||
# 练气期 Weapon Base Price = 10
|
||||
# 买入: 10 * 1.5 = 15
|
||||
buy_cost = 15
|
||||
# 卖出: 10 * 1.0 = 10
|
||||
sell_refund = 10
|
||||
|
||||
expected_money = initial_money - buy_cost + sell_refund
|
||||
|
||||
with patch("src.utils.resolution.elixirs_by_name", elixirs_mock), \
|
||||
patch("src.utils.resolution.materials_by_name", materials_mock):
|
||||
|
||||
action = Buy(avatar_in_city, avatar_in_city.world)
|
||||
|
||||
# 验证 Event 描述
|
||||
event = action.start("青云剑")
|
||||
assert "青云剑" in event.content
|
||||
assert "铁剑" in event.content
|
||||
assert "折价售出" in event.content
|
||||
|
||||
# 执行购买
|
||||
action._execute("青云剑")
|
||||
|
||||
assert avatar_in_city.weapon.name == "青云剑"
|
||||
assert avatar_in_city.weapon != old_weapon # 应该是新对象
|
||||
assert avatar_in_city.magic_stone == expected_money
|
||||
|
||||
@@ -179,7 +179,7 @@ def test_avatar_sell_integration(empty_world):
|
||||
weapon.name = "TestBlade"
|
||||
weapon.realm = Realm.Qi_Refinement
|
||||
|
||||
# The mixin usually requires self.items to have the item for sell_item,
|
||||
# The mixin usually requires self.materials to have the material for sell_material,
|
||||
# but sell_weapon/sell_auxiliary are for equipped items or passed items.
|
||||
# Looking at inventory_mixin.py: sell_weapon(self, weapon) just calculates price and adds stones.
|
||||
# It calls _get_sell_multiplier()
|
||||
|
||||
@@ -17,18 +17,18 @@ def mock_region(dummy_avatar):
|
||||
return real_region
|
||||
|
||||
@pytest.fixture
|
||||
def mock_resource_item():
|
||||
item = MagicMock()
|
||||
item.name = "TestItem"
|
||||
item.realm = Realm.Qi_Refinement
|
||||
return item
|
||||
def mock_resource_material():
|
||||
material = MagicMock()
|
||||
material.name = "TestMaterial"
|
||||
material.realm = Realm.Qi_Refinement
|
||||
return material
|
||||
|
||||
@pytest.fixture
|
||||
def mock_resource(mock_resource_item):
|
||||
def mock_resource(mock_resource_material):
|
||||
"""创建一个通用的资源对象 (Lode/Animal/Plant)"""
|
||||
res = MagicMock()
|
||||
res.realm = Realm.Qi_Refinement
|
||||
res.items = [mock_resource_item]
|
||||
res.materials = [mock_resource_material]
|
||||
return res
|
||||
|
||||
def test_check_can_start_gather_success(dummy_avatar, mock_region, mock_resource):
|
||||
@@ -66,22 +66,22 @@ def test_check_can_start_gather_realm_too_low(dummy_avatar, mock_region, mock_re
|
||||
assert can is False
|
||||
assert "当前区域的矿脉境界过高" in msg
|
||||
|
||||
def test_execute_gather_success(dummy_avatar, mock_region, mock_resource, mock_resource_item):
|
||||
def test_execute_gather_success(dummy_avatar, mock_region, mock_resource, mock_resource_material):
|
||||
"""测试执行采集逻辑成功"""
|
||||
mock_region.lodes = [mock_resource]
|
||||
|
||||
# 模拟 add_item
|
||||
dummy_avatar.add_item = MagicMock()
|
||||
# 模拟 add_material
|
||||
dummy_avatar.add_material = MagicMock()
|
||||
|
||||
result = execute_gather(dummy_avatar, "lodes", "extra_mine_items")
|
||||
result = execute_gather(dummy_avatar, "lodes", "extra_mine_materials")
|
||||
|
||||
assert "TestItem" in result
|
||||
assert result["TestItem"] >= 1
|
||||
dummy_avatar.add_item.assert_called_once()
|
||||
assert "TestMaterial" in result
|
||||
assert result["TestMaterial"] >= 1
|
||||
dummy_avatar.add_material.assert_called_once()
|
||||
|
||||
# 验证获得的物品是正确的
|
||||
args, _ = dummy_avatar.add_item.call_args
|
||||
assert args[0] == mock_resource_item
|
||||
# 验证获得的材料是正确的
|
||||
args, _ = dummy_avatar.add_material.call_args
|
||||
assert args[0] == mock_resource_material
|
||||
assert args[1] >= 1
|
||||
|
||||
def test_execute_gather_with_extra_effect(dummy_avatar, mock_region, mock_resource):
|
||||
@@ -90,28 +90,28 @@ def test_execute_gather_with_extra_effect(dummy_avatar, mock_region, mock_resour
|
||||
|
||||
# effects 是只读属性,它通过合并各个组件的 effects 来计算。
|
||||
# 为了测试,我们 Mock 掉 effects 属性。
|
||||
with patch.object(type(dummy_avatar), 'effects', new_callable=lambda: {"extra_mine_items": 2}):
|
||||
dummy_avatar.add_item = MagicMock()
|
||||
with patch.object(type(dummy_avatar), 'effects', new_callable=lambda: {"extra_mine_materials": 2}):
|
||||
dummy_avatar.add_material = MagicMock()
|
||||
|
||||
result = execute_gather(dummy_avatar, "lodes", "extra_mine_items")
|
||||
result = execute_gather(dummy_avatar, "lodes", "extra_mine_materials")
|
||||
|
||||
# 基础1 + 加成2 = 3
|
||||
assert result["TestItem"] == 3
|
||||
assert result["TestMaterial"] == 3
|
||||
|
||||
def test_execute_gather_random_selection(dummy_avatar, mock_region):
|
||||
"""测试从多个资源中随机选择"""
|
||||
res1 = MagicMock()
|
||||
res1.realm = Realm.Qi_Refinement
|
||||
res1.items = [MagicMock(name="Item1")]
|
||||
res1.items[0].name = "Item1"
|
||||
res1.materials = [MagicMock(name="Material1")]
|
||||
res1.materials[0].name = "Material1"
|
||||
|
||||
res2 = MagicMock()
|
||||
res2.realm = Realm.Qi_Refinement
|
||||
res2.items = [MagicMock(name="Item2")]
|
||||
res2.items[0].name = "Item2"
|
||||
res2.materials = [MagicMock(name="Material2")]
|
||||
res2.materials[0].name = "Material2"
|
||||
|
||||
mock_region.lodes = [res1, res2]
|
||||
dummy_avatar.add_item = MagicMock()
|
||||
dummy_avatar.add_material = MagicMock()
|
||||
|
||||
execute_gather(dummy_avatar, "lodes", "extra_mine_items")
|
||||
dummy_avatar.add_item.assert_called_once()
|
||||
execute_gather(dummy_avatar, "lodes", "extra_mine_materials")
|
||||
dummy_avatar.add_material.assert_called_once()
|
||||
|
||||
@@ -10,7 +10,7 @@ from src.utils.resolution import (
|
||||
resolve_query,
|
||||
ResolutionResult
|
||||
)
|
||||
from src.classes.item import Item
|
||||
from src.classes.material import Material
|
||||
from src.classes.cultivation import Realm
|
||||
|
||||
# ==================== Normalize Tests ====================
|
||||
@@ -75,14 +75,14 @@ def test_resolve_query_empty():
|
||||
def test_resolve_query_direct_object():
|
||||
"""测试直接传递对象"""
|
||||
# 1. 匹配类型
|
||||
item = Item(id=999, name="测试物品", desc="测试描述", realm=Realm.Qi_Refinement)
|
||||
res = resolve_query(item, expected_types=[Item])
|
||||
material = Material(id=999, name="测试材料", desc="测试描述", realm=Realm.Qi_Refinement)
|
||||
res = resolve_query(material, expected_types=[Material])
|
||||
assert res.is_valid
|
||||
assert res.obj is item
|
||||
assert res.resolved_type == Item
|
||||
assert res.obj is material
|
||||
assert res.resolved_type == Material
|
||||
|
||||
# 2. 不匹配类型但作为对象传入
|
||||
res = resolve_query(item, expected_types=[Realm])
|
||||
res = resolve_query(material, expected_types=[Realm])
|
||||
assert not res.is_valid
|
||||
|
||||
def test_resolve_query_realm():
|
||||
@@ -101,7 +101,7 @@ def test_resolve_query_realm():
|
||||
|
||||
def test_resolve_query_unsupported_type():
|
||||
"""测试不支持的类型输入"""
|
||||
res = resolve_query(123, expected_types=[Item])
|
||||
res = resolve_query(123, expected_types=[Material])
|
||||
assert not res.is_valid
|
||||
assert "非字符串" in res.error_msg
|
||||
|
||||
@@ -121,7 +121,7 @@ def test_resolve_region_mock(mock_world):
|
||||
# 或者我们只测试逻辑分支
|
||||
pass
|
||||
|
||||
# 由于 resolution.py 内部强依赖了实际的类 (Item, Region 等),
|
||||
# 由于 resolution.py 内部强依赖了实际的类 (Material, Region 等),
|
||||
# 且使用了 isinstance(t, type) 和 t.__name__ 判断,
|
||||
# 纯单元测试建议主要覆盖逻辑分支。集成测试覆盖实际类。
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
from src.classes.prices import prices, Prices
|
||||
from src.classes.cultivation import Realm
|
||||
from src.classes.item import items_by_id
|
||||
from src.classes.material import materials_by_id
|
||||
from src.classes.weapon import weapons_by_id, Weapon, get_random_weapon_by_realm
|
||||
from src.classes.auxiliary import auxiliaries_by_id, Auxiliary, get_random_auxiliary_by_realm
|
||||
|
||||
@@ -12,11 +12,11 @@ from src.classes.auxiliary import auxiliaries_by_id, Auxiliary, get_random_auxil
|
||||
class TestPrices:
|
||||
"""价格系统测试"""
|
||||
|
||||
def test_item_prices_by_realm(self):
|
||||
def test_material_prices_by_realm(self):
|
||||
"""测试材料价格按境界递增"""
|
||||
assert prices.ITEM_PRICES[Realm.Qi_Refinement] < prices.ITEM_PRICES[Realm.Foundation_Establishment]
|
||||
assert prices.ITEM_PRICES[Realm.Foundation_Establishment] < prices.ITEM_PRICES[Realm.Core_Formation]
|
||||
assert prices.ITEM_PRICES[Realm.Core_Formation] < prices.ITEM_PRICES[Realm.Nascent_Soul]
|
||||
assert prices.MATERIAL_PRICES[Realm.Qi_Refinement] < prices.MATERIAL_PRICES[Realm.Foundation_Establishment]
|
||||
assert prices.MATERIAL_PRICES[Realm.Foundation_Establishment] < prices.MATERIAL_PRICES[Realm.Core_Formation]
|
||||
assert prices.MATERIAL_PRICES[Realm.Core_Formation] < prices.MATERIAL_PRICES[Realm.Nascent_Soul]
|
||||
|
||||
def test_weapon_prices_by_realm(self):
|
||||
"""测试兵器价格按境界递增"""
|
||||
@@ -30,16 +30,16 @@ class TestPrices:
|
||||
assert prices.AUXILIARY_PRICES[Realm.Foundation_Establishment] < prices.AUXILIARY_PRICES[Realm.Core_Formation]
|
||||
assert prices.AUXILIARY_PRICES[Realm.Core_Formation] < prices.AUXILIARY_PRICES[Realm.Nascent_Soul]
|
||||
|
||||
def test_get_price_for_item(self):
|
||||
"""测试 get_price 对 Item 类型的分发"""
|
||||
if not items_by_id:
|
||||
def test_get_price_for_material(self):
|
||||
"""测试 get_price 对 Material 类型的分发"""
|
||||
if not materials_by_id:
|
||||
pytest.skip("No items available in config")
|
||||
|
||||
item = next(iter(items_by_id.values()))
|
||||
item = next(iter(materials_by_id.values()))
|
||||
price = prices.get_price(item)
|
||||
expected = prices.get_item_price(item)
|
||||
expected = prices.get_material_price(item)
|
||||
assert price == expected
|
||||
assert price == prices.ITEM_PRICES[item.realm]
|
||||
assert price == prices.MATERIAL_PRICES[item.realm]
|
||||
|
||||
def test_get_price_for_weapon(self):
|
||||
"""测试 get_price 对 Weapon 类型的分发"""
|
||||
@@ -63,54 +63,54 @@ class TestPrices:
|
||||
assert price == expected
|
||||
assert price == prices.AUXILIARY_PRICES[aux.realm]
|
||||
|
||||
def test_weapon_more_expensive_than_item(self):
|
||||
def test_weapon_more_expensive_than_material(self):
|
||||
"""测试同境界下兵器比材料贵"""
|
||||
for realm in Realm:
|
||||
if realm in prices.ITEM_PRICES and realm in prices.WEAPON_PRICES:
|
||||
assert prices.WEAPON_PRICES[realm] >= prices.ITEM_PRICES[realm]
|
||||
if realm in prices.MATERIAL_PRICES and realm in prices.WEAPON_PRICES:
|
||||
assert prices.WEAPON_PRICES[realm] >= prices.MATERIAL_PRICES[realm]
|
||||
|
||||
|
||||
class TestAvatarSell:
|
||||
"""Avatar 出售接口测试"""
|
||||
|
||||
def test_sell_item_basic(self, dummy_avatar):
|
||||
def test_sell_material_basic(self, dummy_avatar):
|
||||
"""测试基础材料出售"""
|
||||
if not items_by_id:
|
||||
if not materials_by_id:
|
||||
pytest.skip("No items available in config")
|
||||
|
||||
item = next(iter(items_by_id.values()))
|
||||
dummy_avatar.items = {} # 清空背包
|
||||
item = next(iter(materials_by_id.values()))
|
||||
dummy_avatar.materials = {} # 清空背包
|
||||
dummy_avatar.magic_stone.value = 0
|
||||
|
||||
# 添加物品
|
||||
dummy_avatar.add_item(item, 5)
|
||||
assert dummy_avatar.get_item_quantity(item) == 5
|
||||
dummy_avatar.add_material(item, 5)
|
||||
assert dummy_avatar.get_material_quantity(item) == 5
|
||||
|
||||
# 出售3个
|
||||
gained = dummy_avatar.sell_item(item, 3)
|
||||
gained = dummy_avatar.sell_material(item, 3)
|
||||
|
||||
expected_price = prices.get_item_price(item) * 3
|
||||
expected_price = prices.get_material_price(item) * 3
|
||||
assert gained == expected_price
|
||||
assert dummy_avatar.magic_stone.value == expected_price
|
||||
assert dummy_avatar.get_item_quantity(item) == 2
|
||||
assert dummy_avatar.get_material_quantity(item) == 2
|
||||
|
||||
def test_sell_item_insufficient(self, dummy_avatar):
|
||||
def test_sell_material_insufficient(self, dummy_avatar):
|
||||
"""测试出售物品数量不足"""
|
||||
if not items_by_id:
|
||||
if not materials_by_id:
|
||||
pytest.skip("No items available in config")
|
||||
|
||||
item = next(iter(items_by_id.values()))
|
||||
dummy_avatar.items = {}
|
||||
item = next(iter(materials_by_id.values()))
|
||||
dummy_avatar.materials = {}
|
||||
dummy_avatar.magic_stone.value = 100
|
||||
|
||||
dummy_avatar.add_item(item, 2)
|
||||
dummy_avatar.add_material(item, 2)
|
||||
|
||||
# 尝试出售5个(只有2个)
|
||||
gained = dummy_avatar.sell_item(item, 5)
|
||||
gained = dummy_avatar.sell_material(item, 5)
|
||||
|
||||
assert gained == 0
|
||||
assert dummy_avatar.magic_stone.value == 100 # 没有变化
|
||||
assert dummy_avatar.get_item_quantity(item) == 2 # 物品未减少
|
||||
assert dummy_avatar.get_material_quantity(item) == 2 # 物品未减少
|
||||
|
||||
def test_sell_weapon(self, dummy_avatar):
|
||||
"""测试出售兵器"""
|
||||
@@ -142,15 +142,15 @@ class TestAvatarSell:
|
||||
|
||||
def test_sell_with_price_multiplier(self, dummy_avatar):
|
||||
"""测试出售价格倍率效果"""
|
||||
if not items_by_id:
|
||||
if not materials_by_id:
|
||||
pytest.skip("No items available in config")
|
||||
|
||||
item = next(iter(items_by_id.values()))
|
||||
dummy_avatar.items = {}
|
||||
item = next(iter(materials_by_id.values()))
|
||||
dummy_avatar.materials = {}
|
||||
dummy_avatar.magic_stone.value = 0
|
||||
dummy_avatar.add_item(item, 1)
|
||||
dummy_avatar.add_material(item, 1)
|
||||
|
||||
base_price = prices.get_item_price(item)
|
||||
base_price = prices.get_material_price(item)
|
||||
|
||||
# 模拟 20% 加成 (0.2)
|
||||
# 这里的 effects 是 property,需要用 PropertyMock
|
||||
@@ -158,7 +158,7 @@ class TestAvatarSell:
|
||||
# 注意:由于 Avatar 分布在多个 mixin 中,patch 的位置取决于 effects 定义的位置
|
||||
# effects 定义在 EffectsMixin 中,但混入后是在 Avatar 类上
|
||||
# 如果 patch 比较麻烦,我们可以利用 Prices.get_selling_price 的逻辑
|
||||
# 这里我们其实也可以直接 patch get_selling_price 来验证 sell_item 是否使用了它
|
||||
# 这里我们其实也可以直接 patch get_selling_price 来验证 sell_material 是否使用了它
|
||||
# 但为了验证集成逻辑,我们尝试 patch effects
|
||||
pass
|
||||
|
||||
@@ -169,7 +169,7 @@ class TestAvatarSell:
|
||||
|
||||
expected_total = int(base_price * 1.2)
|
||||
with patch("src.classes.prices.prices.get_selling_price", return_value=expected_total) as mock_get_price:
|
||||
gained = dummy_avatar.sell_item(item, 1)
|
||||
gained = dummy_avatar.sell_material(item, 1)
|
||||
|
||||
# 验证调用参数
|
||||
mock_get_price.assert_called_with(item, dummy_avatar)
|
||||
|
||||
@@ -2,7 +2,7 @@ import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from src.classes.action.sell import Sell
|
||||
from src.classes.region import CityRegion
|
||||
from src.classes.item import Item
|
||||
from src.classes.material import Material
|
||||
from src.classes.weapon import Weapon
|
||||
from src.classes.auxiliary import Auxiliary
|
||||
from src.classes.cultivation import Realm
|
||||
@@ -10,11 +10,11 @@ from src.classes.tile import Tile, TileType
|
||||
from src.classes.weapon_type import WeaponType
|
||||
|
||||
# 创建测试用的对象 helper
|
||||
def create_test_item(name, realm, item_id=101):
|
||||
return Item(
|
||||
id=item_id,
|
||||
def create_test_material(name, realm, material_id=101):
|
||||
return Material(
|
||||
id=material_id,
|
||||
name=name,
|
||||
desc="测试物品",
|
||||
desc="测试材料",
|
||||
realm=realm
|
||||
)
|
||||
|
||||
@@ -50,7 +50,7 @@ def avatar_in_city(dummy_avatar):
|
||||
|
||||
dummy_avatar.tile = tile
|
||||
dummy_avatar.magic_stone = 0
|
||||
dummy_avatar.items = {}
|
||||
dummy_avatar.materials = {}
|
||||
dummy_avatar.weapon = None
|
||||
dummy_avatar.auxiliary = None
|
||||
|
||||
@@ -59,26 +59,26 @@ def avatar_in_city(dummy_avatar):
|
||||
@pytest.fixture
|
||||
def mock_sell_objects():
|
||||
"""
|
||||
Mock items_by_name/weapons/auxiliaries 并提供测试对象
|
||||
Mock materials_by_name/weapons/auxiliaries 并提供测试对象
|
||||
"""
|
||||
test_item = create_test_item("铁矿石", Realm.Qi_Refinement)
|
||||
test_material = create_test_material("铁矿石", Realm.Qi_Refinement)
|
||||
test_weapon = create_test_weapon("青云剑", Realm.Qi_Refinement)
|
||||
test_auxiliary = create_test_auxiliary("聚灵珠", Realm.Qi_Refinement)
|
||||
|
||||
items_mock = {"铁矿石": test_item}
|
||||
materials_mock = {"铁矿石": test_material}
|
||||
weapons_mock = {"青云剑": test_weapon}
|
||||
auxiliaries_mock = {"聚灵珠": test_auxiliary}
|
||||
|
||||
return items_mock, weapons_mock, auxiliaries_mock, test_item, test_weapon, test_auxiliary
|
||||
return materials_mock, weapons_mock, auxiliaries_mock, test_material, test_weapon, test_auxiliary
|
||||
|
||||
def test_sell_item_success(avatar_in_city, mock_sell_objects):
|
||||
"""测试出售普通物品成功"""
|
||||
items_mock, weapons_mock, auxiliaries_mock, test_item, _, _ = mock_sell_objects
|
||||
def test_sell_material_success(avatar_in_city, mock_sell_objects):
|
||||
"""测试出售普通材料成功"""
|
||||
materials_mock, weapons_mock, auxiliaries_mock, test_material, _, _ = mock_sell_objects
|
||||
|
||||
# 给角色添加物品
|
||||
avatar_in_city.add_item(test_item, quantity=5)
|
||||
# 给角色添加材料
|
||||
avatar_in_city.add_material(test_material, quantity=5)
|
||||
|
||||
with patch("src.utils.resolution.items_by_name", items_mock), \
|
||||
with patch("src.utils.resolution.materials_by_name", materials_mock), \
|
||||
patch("src.utils.resolution.weapons_by_name", weapons_mock), \
|
||||
patch("src.utils.resolution.auxiliaries_by_name", auxiliaries_mock):
|
||||
|
||||
@@ -89,7 +89,7 @@ def test_sell_item_success(avatar_in_city, mock_sell_objects):
|
||||
assert can_start is True
|
||||
|
||||
# 2. 执行出售
|
||||
# 练气期物品基础价格 10,卖出倍率默认为 1.0 -> 单价 10
|
||||
# 练气期材料基础价格 10,卖出倍率默认为 1.0 -> 单价 10
|
||||
# 卖出全部 5 个 -> 总价 50
|
||||
initial_money = avatar_in_city.magic_stone
|
||||
expected_income = 50
|
||||
@@ -98,16 +98,16 @@ def test_sell_item_success(avatar_in_city, mock_sell_objects):
|
||||
|
||||
# 3. 验证结果
|
||||
assert avatar_in_city.magic_stone == initial_money + expected_income
|
||||
assert avatar_in_city.get_item_quantity(test_item) == 0
|
||||
assert avatar_in_city.get_material_quantity(test_material) == 0
|
||||
|
||||
def test_sell_weapon_success(avatar_in_city, mock_sell_objects):
|
||||
"""测试出售当前兵器成功"""
|
||||
items_mock, weapons_mock, auxiliaries_mock, _, test_weapon, _ = mock_sell_objects
|
||||
materials_mock, weapons_mock, auxiliaries_mock, _, test_weapon, _ = mock_sell_objects
|
||||
|
||||
# 装备兵器
|
||||
avatar_in_city.weapon = test_weapon
|
||||
|
||||
with patch("src.utils.resolution.items_by_name", items_mock), \
|
||||
with patch("src.utils.resolution.materials_by_name", materials_mock), \
|
||||
patch("src.utils.resolution.weapons_by_name", weapons_mock), \
|
||||
patch("src.utils.resolution.auxiliaries_by_name", auxiliaries_mock):
|
||||
|
||||
@@ -134,12 +134,12 @@ def test_sell_weapon_success(avatar_in_city, mock_sell_objects):
|
||||
|
||||
def test_sell_auxiliary_success(avatar_in_city, mock_sell_objects):
|
||||
"""测试出售当前法宝成功"""
|
||||
items_mock, weapons_mock, auxiliaries_mock, _, _, test_auxiliary = mock_sell_objects
|
||||
materials_mock, weapons_mock, auxiliaries_mock, _, _, test_auxiliary = mock_sell_objects
|
||||
|
||||
# 装备法宝
|
||||
avatar_in_city.auxiliary = test_auxiliary
|
||||
|
||||
with patch("src.utils.resolution.items_by_name", items_mock), \
|
||||
with patch("src.utils.resolution.materials_by_name", materials_mock), \
|
||||
patch("src.utils.resolution.weapons_by_name", weapons_mock), \
|
||||
patch("src.utils.resolution.auxiliaries_by_name", auxiliaries_mock):
|
||||
|
||||
@@ -157,13 +157,13 @@ def test_sell_auxiliary_success(avatar_in_city, mock_sell_objects):
|
||||
|
||||
def test_sell_fail_not_in_city(dummy_avatar, mock_sell_objects):
|
||||
"""测试不在城市无法出售"""
|
||||
items_mock, weapons_mock, auxiliaries_mock, test_item, _, _ = mock_sell_objects
|
||||
materials_mock, weapons_mock, auxiliaries_mock, test_material, _, _ = mock_sell_objects
|
||||
|
||||
# 确保不在城市
|
||||
assert not isinstance(dummy_avatar.tile.region, CityRegion)
|
||||
dummy_avatar.add_item(test_item, 1)
|
||||
dummy_avatar.add_material(test_material, 1)
|
||||
|
||||
with patch("src.utils.resolution.items_by_name", items_mock), \
|
||||
with patch("src.utils.resolution.materials_by_name", materials_mock), \
|
||||
patch("src.utils.resolution.weapons_by_name", weapons_mock), \
|
||||
patch("src.utils.resolution.auxiliaries_by_name", auxiliaries_mock):
|
||||
|
||||
@@ -174,12 +174,12 @@ def test_sell_fail_not_in_city(dummy_avatar, mock_sell_objects):
|
||||
assert "仅能在城市" in reason
|
||||
|
||||
def test_sell_fail_no_item(avatar_in_city, mock_sell_objects):
|
||||
"""测试未持有该物品"""
|
||||
items_mock, weapons_mock, auxiliaries_mock, _, _, _ = mock_sell_objects
|
||||
"""测试未持有该材料"""
|
||||
materials_mock, weapons_mock, auxiliaries_mock, _, _, _ = mock_sell_objects
|
||||
|
||||
# 背包为空,无装备
|
||||
|
||||
with patch("src.utils.resolution.items_by_name", items_mock), \
|
||||
with patch("src.utils.resolution.materials_by_name", materials_mock), \
|
||||
patch("src.utils.resolution.weapons_by_name", weapons_mock), \
|
||||
patch("src.utils.resolution.auxiliaries_by_name", auxiliaries_mock):
|
||||
|
||||
@@ -187,13 +187,13 @@ def test_sell_fail_no_item(avatar_in_city, mock_sell_objects):
|
||||
can_start, reason = action.can_start("铁矿石")
|
||||
|
||||
assert can_start is False
|
||||
assert "未持有物品" in reason
|
||||
assert "未持有材料" in reason
|
||||
|
||||
def test_sell_fail_unknown_name(avatar_in_city, mock_sell_objects):
|
||||
"""测试未知物品名称"""
|
||||
items_mock, weapons_mock, auxiliaries_mock, _, _, _ = mock_sell_objects
|
||||
materials_mock, weapons_mock, auxiliaries_mock, _, _, _ = mock_sell_objects
|
||||
|
||||
with patch("src.utils.resolution.items_by_name", items_mock), \
|
||||
with patch("src.utils.resolution.materials_by_name", materials_mock), \
|
||||
patch("src.utils.resolution.weapons_by_name", weapons_mock), \
|
||||
patch("src.utils.resolution.auxiliaries_by_name", auxiliaries_mock):
|
||||
|
||||
@@ -204,18 +204,18 @@ def test_sell_fail_unknown_name(avatar_in_city, mock_sell_objects):
|
||||
assert "未持有物品/装备" in reason
|
||||
|
||||
def test_sell_priority(avatar_in_city, mock_sell_objects):
|
||||
"""测试物品优先级:同名时优先卖背包里的材料"""
|
||||
items_mock, weapons_mock, auxiliaries_mock, test_item, test_weapon, _ = mock_sell_objects
|
||||
"""测试优先级:同名时优先卖身上装备(根据 resolution 优先级)"""
|
||||
materials_mock, weapons_mock, auxiliaries_mock, test_material, test_weapon, _ = mock_sell_objects
|
||||
|
||||
# 构造一个同名的兵器和材料
|
||||
fake_sword_item = create_test_item("青云剑", Realm.Qi_Refinement)
|
||||
items_mock["青云剑"] = fake_sword_item
|
||||
fake_sword_material = create_test_material("青云剑", Realm.Qi_Refinement)
|
||||
materials_mock["青云剑"] = fake_sword_material
|
||||
|
||||
# 角色同时拥有该材料和该兵器
|
||||
avatar_in_city.add_item(fake_sword_item, 1)
|
||||
avatar_in_city.add_material(fake_sword_material, 1)
|
||||
avatar_in_city.weapon = test_weapon # name也是 "青云剑"
|
||||
|
||||
with patch("src.utils.resolution.items_by_name", items_mock), \
|
||||
with patch("src.utils.resolution.materials_by_name", materials_mock), \
|
||||
patch("src.utils.resolution.weapons_by_name", weapons_mock), \
|
||||
patch("src.utils.resolution.auxiliaries_by_name", auxiliaries_mock):
|
||||
|
||||
@@ -224,69 +224,6 @@ def test_sell_priority(avatar_in_city, mock_sell_objects):
|
||||
# 执行出售
|
||||
action._execute("青云剑")
|
||||
|
||||
# 应该优先卖掉了材料
|
||||
# 注意:在新的 resolution 逻辑中,resolve_goods_by_name 的查找顺序是:
|
||||
# 1. Elixir
|
||||
# 2. Weapon
|
||||
# 3. Auxiliary
|
||||
# 4. Item
|
||||
# 所以如果在 mock 中都有 "青云剑",它会先被解析为 Weapon 类型。
|
||||
#
|
||||
# 然后 Sell._resolve_obj (现在内联在方法里) 逻辑:
|
||||
# obj, obj_type, _ = resolve_goods_by_name(target_name)
|
||||
#
|
||||
# 如果解析出来是 Weapon:
|
||||
# Sell logic: if obj_type == "weapon": check if self.avatar.weapon == normalized_name
|
||||
#
|
||||
# 所以如果名字相同,且 resolve 优先判定为 Weapon,那么代码会认为你想卖 Weapon。
|
||||
# 之前的逻辑:
|
||||
# 1. 检查背包材料 -> 有就卖
|
||||
# 2. 检查兵器
|
||||
#
|
||||
# 新的逻辑:
|
||||
# 1. resolve_goods_by_name -> 返回类型
|
||||
# 2. 根据类型检查
|
||||
#
|
||||
# 由于 resolution 中 Weapon 优先于 Item,所以 "青云剑" 会被解析为 Weapon。
|
||||
# 于是 Sell 动作会尝试卖身上的兵器。
|
||||
# 如果此时也正好装备了青云剑,就会卖掉兵器。
|
||||
#
|
||||
# 这意味着:新逻辑改变了优先级!
|
||||
# 之前是优先卖背包里的 Item(即使有同名的 Weapon 定义)。
|
||||
# 现在是看 resolution 认为它是什么。
|
||||
#
|
||||
# 如果我想保留"优先卖背包"的逻辑,我需要在 Sell 里特殊处理吗?
|
||||
# 或者接受这个变更。
|
||||
#
|
||||
# 假设"青云剑"既是 Weapon 又是 Item。
|
||||
# resolve_goods_by_name 会返回 Weapon。
|
||||
# Sell 拿到 Weapon 类型,检查 self.avatar.weapon。
|
||||
# -> 卖掉兵器。
|
||||
#
|
||||
# 如果我想测试"优先卖背包",这在当前新逻辑下可能不再成立,除非 Item 的查找优先级高于 Weapon。
|
||||
# 但通常 Item 优先级最低。
|
||||
#
|
||||
# 考虑到“青云剑”作为材料这种名字冲突本身就很罕见。
|
||||
# 我将修改测试预期:现在应该优先卖掉兵器(或者说,被识别为兵器)。
|
||||
|
||||
# 但是,如果我没有装备青云剑呢?
|
||||
# resolve 还是返回 Weapon。
|
||||
# Sell 检查 weapon -> 没装备 -> 报错 "未持有装备"。
|
||||
# 而背包里其实有 "青云剑" (Item)。
|
||||
# 这就是一个潜在的 Bug/Feature change。
|
||||
#
|
||||
# 如果用户输入 "青云剑",系统认为这是个 Weapon。用户没装备,系统提示"你没装备这个"。
|
||||
# 用户困惑:"但我背包里有一堆青云剑材料啊!"
|
||||
#
|
||||
# 为了解决这个问题,resolve_goods_by_name 可能需要更智能,或者 Sell 需要尝试多种可能。
|
||||
# 但目前的 resolve 是确定的。
|
||||
#
|
||||
# 也许我应该让 Item 的优先级高于 Weapon?
|
||||
# 不,通常名字是唯一的。
|
||||
#
|
||||
# 让我们先按新逻辑修正测试预期。
|
||||
# 如果 resolve 返回 Weapon,且角色装备了,就会卖掉装备。
|
||||
# 所以这里断言:兵器没了,材料还在。
|
||||
|
||||
# 断言:兵器没了,材料还在。
|
||||
assert avatar_in_city.weapon is None
|
||||
assert avatar_in_city.get_item_quantity(fake_sword_item) == 1
|
||||
assert avatar_in_city.get_material_quantity(fake_sword_material) == 1
|
||||
|
||||
124
tests/test_single_choice.py
Normal file
124
tests/test_single_choice.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
from src.classes.single_choice import handle_item_exchange
|
||||
|
||||
# Mocks for types
|
||||
class MockAvatar:
|
||||
def __init__(self):
|
||||
self.name = "TestAvatar"
|
||||
self.weapon = None
|
||||
self.auxiliary = None
|
||||
self.technique = None
|
||||
self.world = Mock()
|
||||
self.world.static_info = {}
|
||||
self.change_weapon = Mock()
|
||||
self.sell_weapon = Mock(return_value=100)
|
||||
self.consume_elixir = Mock()
|
||||
self.sell_elixir = Mock(return_value=50)
|
||||
|
||||
def get_info(self, detailed=False):
|
||||
return {"name": self.name}
|
||||
|
||||
class MockItem:
|
||||
def __init__(self, name, item_type="weapon"):
|
||||
self.name = name
|
||||
self.item_type = item_type
|
||||
# Weapon/Auxiliary/Elixir usually have realm or grade
|
||||
self.realm = Mock()
|
||||
self.realm.value = "TestRealm"
|
||||
|
||||
def get_info(self, detailed=False):
|
||||
return f"Info({self.name})"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_weapon_auto_equip_no_sell_new():
|
||||
"""测试:自动装备兵器(无旧兵器,不可卖新)"""
|
||||
avatar = MockAvatar()
|
||||
new_weapon = MockItem("NewSword", "weapon")
|
||||
|
||||
# 模拟无旧兵器
|
||||
avatar.weapon = None
|
||||
|
||||
swapped, msg = await handle_item_exchange(
|
||||
avatar, new_weapon, "weapon", "Context", can_sell_new=False
|
||||
)
|
||||
|
||||
assert swapped is True
|
||||
assert "获得了TestRealm兵器『NewSword』并装备" in msg
|
||||
avatar.change_weapon.assert_called_once_with(new_weapon)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_weapon_swap_choice_A():
|
||||
"""测试:替换兵器,选择 A(装备新,卖旧)"""
|
||||
avatar = MockAvatar()
|
||||
old_weapon = MockItem("OldSword", "weapon")
|
||||
new_weapon = MockItem("NewSword", "weapon")
|
||||
avatar.weapon = old_weapon
|
||||
|
||||
# Mock decision to return 'A'
|
||||
with patch("src.classes.single_choice.make_decision", new_callable=AsyncMock) as mock_decision:
|
||||
mock_decision.return_value = "A"
|
||||
|
||||
swapped, msg = await handle_item_exchange(
|
||||
avatar, new_weapon, "weapon", "Context", can_sell_new=True
|
||||
)
|
||||
|
||||
# 验证文案包含动词
|
||||
# call_args[0][1] is context string, check options description
|
||||
call_args = mock_decision.call_args
|
||||
options = call_args[0][2] # options list
|
||||
opt_a_desc = options[0]["desc"]
|
||||
|
||||
# 验证选项文案使用了 "装备" 和 "卖掉"
|
||||
assert "装备新兵器『NewSword』" in opt_a_desc
|
||||
assert "卖掉旧兵器『OldSword』" in opt_a_desc
|
||||
|
||||
assert swapped is True
|
||||
assert "换上了TestRealm兵器『NewSword』" in msg
|
||||
avatar.sell_weapon.assert_called_once_with(old_weapon)
|
||||
avatar.change_weapon.assert_called_once_with(new_weapon)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_elixir_consume_choice_A():
|
||||
"""测试:获得丹药,选择 A(服用)"""
|
||||
avatar = MockAvatar()
|
||||
new_elixir = MockItem("PowerPill", "elixir")
|
||||
|
||||
# Mock decision to return 'A'
|
||||
with patch("src.classes.single_choice.make_decision", new_callable=AsyncMock) as mock_decision:
|
||||
mock_decision.return_value = "A"
|
||||
|
||||
swapped, msg = await handle_item_exchange(
|
||||
avatar, new_elixir, "elixir", "Context", can_sell_new=True
|
||||
)
|
||||
|
||||
# 验证选项文案
|
||||
options = mock_decision.call_args[0][2]
|
||||
opt_a_desc = options[0]["desc"]
|
||||
# 验证使用了 "服用"
|
||||
assert "服用新丹药『PowerPill』" in opt_a_desc
|
||||
|
||||
assert swapped is True
|
||||
# 验证结果使用了 "服用了"
|
||||
assert "服用了TestRealm丹药『PowerPill』" in msg
|
||||
avatar.consume_elixir.assert_called_once_with(new_elixir)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_elixir_sell_choice_B():
|
||||
"""测试:获得丹药,选择 B(卖出)"""
|
||||
avatar = MockAvatar()
|
||||
new_elixir = MockItem("PowerPill", "elixir")
|
||||
|
||||
# Mock decision to return 'B'
|
||||
with patch("src.classes.single_choice.make_decision", new_callable=AsyncMock) as mock_decision:
|
||||
mock_decision.return_value = "B"
|
||||
|
||||
swapped, msg = await handle_item_exchange(
|
||||
avatar, new_elixir, "elixir", "Context", can_sell_new=True
|
||||
)
|
||||
|
||||
assert swapped is False
|
||||
assert "卖掉了新获得的PowerPill" in msg
|
||||
avatar.sell_elixir.assert_called_once_with(new_elixir)
|
||||
avatar.consume_elixir.assert_not_called()
|
||||
|
||||
@@ -154,12 +154,12 @@ async function handleClearObjective() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Items -->
|
||||
<div class="section" v-if="data.items?.length">
|
||||
<div class="section-title">物品</div>
|
||||
<!-- Materials -->
|
||||
<div class="section" v-if="data.materials?.length">
|
||||
<div class="section-title">材料</div>
|
||||
<div class="list-container">
|
||||
<EntityRow
|
||||
v-for="item in data.items"
|
||||
v-for="item in data.materials"
|
||||
:key="item.name"
|
||||
:item="item"
|
||||
:meta="`x${item.count}`"
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface EffectEntity extends EntityBase {
|
||||
hq_desc?: string;
|
||||
}
|
||||
|
||||
export interface Item extends EffectEntity {
|
||||
export interface Material extends EffectEntity {
|
||||
count: number;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export interface AvatarDetail extends EntityBase {
|
||||
spirit_animal?: EffectEntity;
|
||||
|
||||
// 列表数据
|
||||
items: Item[];
|
||||
materials: Material[];
|
||||
relations: RelationInfo[];
|
||||
|
||||
// 附加信息
|
||||
|
||||
Reference in New Issue
Block a user