refactor buy and sell
This commit is contained in:
@@ -25,7 +25,7 @@ from .breakthrough import Breakthrough
|
||||
from .play import Play
|
||||
from .hunt import Hunt
|
||||
from .harvest import Harvest
|
||||
from .sell import SellItems
|
||||
from .sell import Sell
|
||||
from .attack import Attack
|
||||
from .plunder_mortals import PlunderMortals
|
||||
from .help_mortals import HelpMortals
|
||||
@@ -37,7 +37,7 @@ from .switch_weapon import SwitchWeapon
|
||||
from .assassinate import Assassinate
|
||||
from .move_to_direction import MoveToDirection
|
||||
from .cast import Cast
|
||||
from .buy import BuyItem
|
||||
from .buy import Buy
|
||||
|
||||
# 注册到 ActionRegistry(标注是否为实际可执行动作)
|
||||
register_action(actual=False)(Action)
|
||||
@@ -59,7 +59,7 @@ register_action(actual=True)(Breakthrough)
|
||||
register_action(actual=True)(Play)
|
||||
register_action(actual=True)(Hunt)
|
||||
register_action(actual=True)(Harvest)
|
||||
register_action(actual=True)(SellItems)
|
||||
register_action(actual=True)(Sell)
|
||||
register_action(actual=False)(Attack)
|
||||
register_action(actual=True)(PlunderMortals)
|
||||
register_action(actual=True)(HelpMortals)
|
||||
@@ -71,7 +71,7 @@ register_action(actual=True)(SwitchWeapon)
|
||||
register_action(actual=True)(Assassinate)
|
||||
register_action(actual=True)(MoveToDirection)
|
||||
register_action(actual=True)(Cast)
|
||||
register_action(actual=True)(BuyItem)
|
||||
register_action(actual=True)(Buy)
|
||||
# Talk 已移动到 mutual_action 模块,在那里注册
|
||||
|
||||
__all__ = [
|
||||
@@ -96,7 +96,7 @@ __all__ = [
|
||||
"Play",
|
||||
"Hunt",
|
||||
"Harvest",
|
||||
"SellItems",
|
||||
"Sell",
|
||||
"Attack",
|
||||
"PlunderMortals",
|
||||
"HelpMortals",
|
||||
@@ -108,7 +108,7 @@ __all__ = [
|
||||
"Assassinate",
|
||||
"MoveToDirection",
|
||||
"Cast",
|
||||
"BuyItem",
|
||||
"Buy",
|
||||
# Talk 已移动到 mutual_action 模块
|
||||
# Occupy 已移动到 mutual_action 模块
|
||||
]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from typing import TYPE_CHECKING, Tuple, Any
|
||||
|
||||
from src.classes.action import InstantAction
|
||||
@@ -7,62 +8,75 @@ from src.classes.event import Event
|
||||
from src.classes.region import CityRegion
|
||||
from src.classes.elixir import elixirs_by_name, Elixir
|
||||
from src.classes.item import items_by_name, Item
|
||||
from src.classes.weapon import weapons_by_name, Weapon
|
||||
from src.classes.auxiliary import auxiliaries_by_name, Auxiliary
|
||||
from src.classes.prices import prices
|
||||
from src.classes.normalize import normalize_item_name
|
||||
from src.classes.normalize import normalize_goods_name
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.avatar import Avatar
|
||||
|
||||
|
||||
class BuyItem(InstantAction):
|
||||
class Buy(InstantAction):
|
||||
"""
|
||||
在城镇购买物品。
|
||||
|
||||
如果是丹药:购买后强制立即服用。
|
||||
如果是其他物品:购买后放入背包。
|
||||
如果是装备(兵器/法宝):购买后直接装备(替换原有装备)。
|
||||
"""
|
||||
|
||||
ACTION_NAME = "购买物品"
|
||||
ACTION_NAME = "购买"
|
||||
EMOJI = "💸"
|
||||
DESC = "在城镇购买物品(丹药购买后将立即服用)"
|
||||
elixir_names_str = ", ".join(elixirs_by_name.keys())
|
||||
DESC = f"在城镇购买物品/装备(丹药购买后将立即服用)。可选丹药:{elixir_names_str}"
|
||||
DOABLES_REQUIREMENTS = "在城镇且金钱足够"
|
||||
PARAMS = {"item_name": "str"}
|
||||
PARAMS = {"target_name": "str"}
|
||||
|
||||
def _resolve_obj(self, item_name: str) -> Tuple[Any, str, str]:
|
||||
def _resolve_obj(self, target_name: str) -> Tuple[Any, str, str]:
|
||||
"""
|
||||
解析物品名称,返回 (对象, 类型, 显示名称)。
|
||||
类型字符串: "elixir", "item", "unknown"
|
||||
类型字符串: "elixir", "item", "weapon", "auxiliary", "unknown"
|
||||
"""
|
||||
normalized_name = normalize_item_name(item_name)
|
||||
normalized_name = normalize_goods_name(target_name)
|
||||
|
||||
# 1. 尝试作为丹药查找
|
||||
if normalized_name in elixirs_by_name:
|
||||
# 这里的 elixirs_by_name 返回的是 list,我们取第一个作为购买对象
|
||||
# TODO: 如果未来有同名不同级的丹药,这里可能需要更精确的逻辑
|
||||
elixir = elixirs_by_name[normalized_name][0]
|
||||
return elixir, "elixir", elixir.name
|
||||
|
||||
# 2. 尝试作为普通物品查找
|
||||
# 2. 尝试作为兵器查找
|
||||
weapon = weapons_by_name.get(normalized_name)
|
||||
if weapon:
|
||||
return weapon, "weapon", weapon.name
|
||||
|
||||
# 3. 尝试作为辅助装备查找
|
||||
auxiliary = auxiliaries_by_name.get(normalized_name)
|
||||
if auxiliary:
|
||||
return auxiliary, "auxiliary", auxiliary.name
|
||||
|
||||
# 4. 尝试作为普通物品查找
|
||||
item = items_by_name.get(normalized_name)
|
||||
if item:
|
||||
return item, "item", item.name
|
||||
|
||||
return None, "unknown", normalized_name
|
||||
|
||||
def can_start(self, item_name: str | None = None) -> tuple[bool, str]:
|
||||
def can_start(self, target_name: str | None = None) -> tuple[bool, str]:
|
||||
region = self.avatar.tile.region
|
||||
if not isinstance(region, CityRegion):
|
||||
return False, "仅能在城市区域执行"
|
||||
|
||||
if item_name is None:
|
||||
if target_name is None:
|
||||
# 用于动作空间检查
|
||||
# 理论上只要有钱就可以买东西,这里简单判定金钱>0
|
||||
ok = self.avatar.magic_stone > 0
|
||||
return (ok, "" if ok else "身无分文")
|
||||
|
||||
obj, obj_type, display_name = self._resolve_obj(item_name)
|
||||
obj, obj_type, display_name = self._resolve_obj(target_name)
|
||||
if obj_type == "unknown":
|
||||
return False, f"未知物品: {item_name}"
|
||||
return False, f"未知物品: {target_name}"
|
||||
|
||||
# 检查价格
|
||||
price = prices.get_buying_price(obj, self.avatar)
|
||||
@@ -85,8 +99,8 @@ class BuyItem(InstantAction):
|
||||
|
||||
return True, ""
|
||||
|
||||
def _execute(self, item_name: str) -> None:
|
||||
obj, obj_type, display_name = self._resolve_obj(item_name)
|
||||
def _execute(self, target_name: str) -> None:
|
||||
obj, obj_type, display_name = self._resolve_obj(target_name)
|
||||
if obj_type == "unknown":
|
||||
return
|
||||
|
||||
@@ -98,11 +112,25 @@ class BuyItem(InstantAction):
|
||||
self.avatar.consume_elixir(obj)
|
||||
elif obj_type == "item":
|
||||
self.avatar.add_item(obj)
|
||||
elif obj_type == "weapon":
|
||||
# 购买装备需要深拷贝,因为装备有独立状态
|
||||
new_weapon = copy.deepcopy(obj)
|
||||
self.avatar.change_weapon(new_weapon)
|
||||
elif obj_type == "auxiliary":
|
||||
# 购买装备需要深拷贝
|
||||
new_auxiliary = copy.deepcopy(obj)
|
||||
self.avatar.change_auxiliary(new_auxiliary)
|
||||
|
||||
def start(self, item_name: str) -> Event:
|
||||
obj, obj_type, display_name = self._resolve_obj(item_name)
|
||||
def start(self, target_name: str) -> Event:
|
||||
obj, obj_type, display_name = self._resolve_obj(target_name)
|
||||
|
||||
action_desc = "购买并服用了" if obj_type == "elixir" else "购买了"
|
||||
if obj_type == "elixir":
|
||||
action_desc = "购买并服用了"
|
||||
elif obj_type in ["weapon", "auxiliary"]:
|
||||
action_desc = "购买并装备了"
|
||||
else:
|
||||
action_desc = "购买了"
|
||||
|
||||
price = prices.get_buying_price(obj, self.avatar) if obj else 0
|
||||
|
||||
return Event(
|
||||
@@ -111,6 +139,5 @@ class BuyItem(InstantAction):
|
||||
related_avatars=[self.avatar.id]
|
||||
)
|
||||
|
||||
async def finish(self, item_name: str) -> list[Event]:
|
||||
async def finish(self, target_name: str) -> list[Event]:
|
||||
return []
|
||||
|
||||
|
||||
@@ -1,70 +1,94 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Tuple, Any
|
||||
|
||||
from src.classes.action import InstantAction
|
||||
from src.classes.event import Event
|
||||
from src.classes.region import CityRegion
|
||||
from src.classes.item import items_by_name
|
||||
from src.classes.normalize import normalize_item_name
|
||||
from src.classes.normalize import normalize_goods_name
|
||||
|
||||
|
||||
class SellItems(InstantAction):
|
||||
class Sell(InstantAction):
|
||||
"""
|
||||
在城镇出售指定名称的物品,一次性卖出持有的全部数量。
|
||||
收益通过 avatar.sell_item() 结算。
|
||||
在城镇出售指定名称的物品/装备。
|
||||
如果是材料:一次性卖出持有的全部数量。
|
||||
如果是装备:卖出当前装备的(如果是当前装备)。
|
||||
收益通过 avatar.sell_item() / sell_weapon() / sell_auxiliary() 结算。
|
||||
"""
|
||||
|
||||
ACTION_NAME = "出售物品"
|
||||
ACTION_NAME = "出售"
|
||||
EMOJI = "💰"
|
||||
DESC = "在城镇出售持有的某类物品的全部"
|
||||
DOABLES_REQUIREMENTS = "在城镇且背包非空"
|
||||
PARAMS = {"item_name": "str"}
|
||||
DESC = "在城镇出售持有的某类物品的全部,或当前装备"
|
||||
DOABLES_REQUIREMENTS = "在城镇且持有可出售物品/装备"
|
||||
PARAMS = {"target_name": "str"}
|
||||
|
||||
def _execute(self, item_name: str) -> None:
|
||||
def _resolve_obj(self, target_name: str) -> Tuple[Any, str, str]:
|
||||
"""
|
||||
解析出售对象
|
||||
返回: (对象, 类型, 显示名称)
|
||||
类型: "item", "weapon", "auxiliary", "none"
|
||||
"""
|
||||
normalized_name = normalize_goods_name(target_name)
|
||||
|
||||
# 1. 检查背包材料
|
||||
item = items_by_name.get(normalized_name)
|
||||
if item and self.avatar.get_item_quantity(item) > 0:
|
||||
return item, "item", item.name
|
||||
|
||||
# 2. 检查当前兵器
|
||||
if self.avatar.weapon and normalize_goods_name(self.avatar.weapon.name) == normalized_name:
|
||||
return self.avatar.weapon, "weapon", self.avatar.weapon.name
|
||||
|
||||
# 3. 检查当前辅助装备
|
||||
if self.avatar.auxiliary and normalize_goods_name(self.avatar.auxiliary.name) == normalized_name:
|
||||
return self.avatar.auxiliary, "auxiliary", self.avatar.auxiliary.name
|
||||
|
||||
return None, "none", normalized_name
|
||||
|
||||
def _execute(self, target_name: str) -> None:
|
||||
region = self.avatar.tile.region
|
||||
if not isinstance(region, CityRegion):
|
||||
return
|
||||
|
||||
# 规范化物品名称(去除境界等附加信息)
|
||||
normalized_name = normalize_item_name(item_name)
|
||||
obj, obj_type, _ = self._resolve_obj(target_name)
|
||||
|
||||
# 找到物品
|
||||
item = items_by_name.get(normalized_name)
|
||||
if item is None:
|
||||
return
|
||||
if obj_type == "item":
|
||||
quantity = self.avatar.get_item_quantity(obj)
|
||||
self.avatar.sell_item(obj, quantity)
|
||||
elif obj_type == "weapon":
|
||||
self.avatar.sell_weapon(obj)
|
||||
self.avatar.change_weapon(None) # 卖出后卸下
|
||||
elif obj_type == "auxiliary":
|
||||
self.avatar.sell_auxiliary(obj)
|
||||
self.avatar.change_auxiliary(None) # 卖出后卸下
|
||||
|
||||
# 检查持有数量
|
||||
quantity = self.avatar.get_item_quantity(item)
|
||||
if quantity <= 0:
|
||||
return
|
||||
|
||||
# 通过统一接口出售
|
||||
self.avatar.sell_item(item, quantity)
|
||||
|
||||
def can_start(self, item_name: str | None = None) -> tuple[bool, str]:
|
||||
def can_start(self, target_name: str | None = None) -> tuple[bool, str]:
|
||||
region = self.avatar.tile.region
|
||||
if not isinstance(region, CityRegion):
|
||||
return False, "仅能在城市区域执行"
|
||||
if item_name is None:
|
||||
# 用于动作空间:只要背包非空即可
|
||||
ok = bool(self.avatar.items)
|
||||
return (ok, "" if ok else "背包为空,无可出售物品")
|
||||
|
||||
if target_name is None:
|
||||
# 用于动作空间:只要有任何可卖东西即可
|
||||
has_items = bool(self.avatar.items)
|
||||
has_weapon = self.avatar.weapon is not None
|
||||
has_auxiliary = self.avatar.auxiliary is not None
|
||||
ok = has_items or has_weapon or has_auxiliary
|
||||
return (ok, "" if ok else "背包为空且无装备,无可出售物品")
|
||||
|
||||
# 规范化物品名称
|
||||
normalized_name = normalize_item_name(item_name)
|
||||
item = items_by_name.get(normalized_name)
|
||||
if item is None:
|
||||
return False, f"未知物品: {item_name}"
|
||||
ok = self.avatar.get_item_quantity(item) > 0
|
||||
return (ok, "" if ok else "该物品数量为0")
|
||||
obj, obj_type, _ = self._resolve_obj(target_name)
|
||||
if obj_type == "none":
|
||||
return False, f"未持有物品/装备: {target_name}"
|
||||
|
||||
return True, ""
|
||||
|
||||
def start(self, item_name: str) -> Event:
|
||||
# 规范化物品名称用于显示(与执行逻辑一致)
|
||||
normalized_name = normalize_item_name(item_name)
|
||||
# 尝试获取标准物品名(如果存在)
|
||||
item = items_by_name.get(normalized_name)
|
||||
display_name = item.name if item is not None else normalized_name
|
||||
return Event(self.world.month_stamp, f"{self.avatar.name} 在城镇出售 {display_name}", related_avatars=[self.avatar.id])
|
||||
def start(self, target_name: str) -> Event:
|
||||
obj, obj_type, display_name = self._resolve_obj(target_name)
|
||||
return Event(
|
||||
self.world.month_stamp,
|
||||
f"{self.avatar.name} 在城镇出售了 {display_name}",
|
||||
related_avatars=[self.avatar.id]
|
||||
)
|
||||
|
||||
async def finish(self, item_name: str) -> list[Event]:
|
||||
async def finish(self, target_name: str) -> list[Event]:
|
||||
return []
|
||||
|
||||
|
||||
@@ -105,28 +105,30 @@ def normalize_region_name(name: str) -> str:
|
||||
return s
|
||||
|
||||
|
||||
def normalize_item_name(name: str) -> str:
|
||||
def normalize_goods_name(name: str) -> str:
|
||||
"""
|
||||
规范化物品名称:去除境界标识等附加信息。
|
||||
规范化商品名称(包括物品、兵器、法宝、丹药)。
|
||||
|
||||
处理格式:
|
||||
- "青云鹿角 -(练气)" -> "青云鹿角"
|
||||
- "风速马皮(筑基)" -> "风速马皮"
|
||||
统一逻辑:
|
||||
1. 移除括号及内容(如境界、类型说明)
|
||||
2. 移除尾部的 " -" 标记(常见于材料生成名)
|
||||
3. 移除首尾空格
|
||||
|
||||
Args:
|
||||
name: 原始物品名称,可能包含境界等附加信息
|
||||
name: 原始商品名称
|
||||
|
||||
Returns:
|
||||
规范化后的物品名称
|
||||
规范化后的商品名称
|
||||
|
||||
Examples:
|
||||
>>> normalize_item_name("青云鹿角 -(练气)")
|
||||
>>> normalize_goods_name("青云鹿角 -(练气)") # item
|
||||
'青云鹿角'
|
||||
>>> normalize_item_name("风速马皮(筑基)")
|
||||
'风速马皮'
|
||||
>>> normalize_goods_name("精铁剑(练气)") # weapon
|
||||
'精铁剑'
|
||||
>>> normalize_goods_name("聚气丹(练气)") # elixir
|
||||
'聚气丹'
|
||||
"""
|
||||
s = _remove_parentheses(name)
|
||||
# 额外处理:去除尾部的 " -" 标记
|
||||
s = s.rstrip(" -").strip()
|
||||
return s
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from src.classes.action.buy import BuyItem
|
||||
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
|
||||
@@ -76,7 +76,7 @@ def test_buy_item_success(avatar_in_city, mock_objects):
|
||||
with patch("src.classes.action.buy.elixirs_by_name", elixirs_mock), \
|
||||
patch("src.classes.action.buy.items_by_name", items_mock):
|
||||
|
||||
action = BuyItem(avatar_in_city, avatar_in_city.world)
|
||||
action = Buy(avatar_in_city, avatar_in_city.world)
|
||||
|
||||
# 1. 检查是否可购买
|
||||
can_start, reason = action.can_start("铁矿石")
|
||||
@@ -100,7 +100,7 @@ def test_buy_elixir_success(avatar_in_city, mock_objects):
|
||||
with patch("src.classes.action.buy.elixirs_by_name", elixirs_mock), \
|
||||
patch("src.classes.action.buy.items_by_name", items_mock):
|
||||
|
||||
action = BuyItem(avatar_in_city, avatar_in_city.world)
|
||||
action = Buy(avatar_in_city, avatar_in_city.world)
|
||||
|
||||
can_start, reason = action.can_start("聚气丹")
|
||||
assert can_start is True
|
||||
@@ -131,7 +131,7 @@ def test_buy_fail_not_in_city(dummy_avatar, mock_objects):
|
||||
with patch("src.classes.action.buy.elixirs_by_name", elixirs_mock), \
|
||||
patch("src.classes.action.buy.items_by_name", items_mock):
|
||||
|
||||
action = BuyItem(dummy_avatar, dummy_avatar.world)
|
||||
action = Buy(dummy_avatar, dummy_avatar.world)
|
||||
can_start, reason = action.can_start("铁矿石")
|
||||
|
||||
assert can_start is False
|
||||
@@ -146,7 +146,7 @@ def test_buy_fail_no_money(avatar_in_city, mock_objects):
|
||||
with patch("src.classes.action.buy.elixirs_by_name", elixirs_mock), \
|
||||
patch("src.classes.action.buy.items_by_name", items_mock):
|
||||
|
||||
action = BuyItem(avatar_in_city, avatar_in_city.world)
|
||||
action = Buy(avatar_in_city, avatar_in_city.world)
|
||||
can_start, reason = action.can_start("铁矿石")
|
||||
|
||||
assert can_start is False
|
||||
@@ -159,7 +159,7 @@ def test_buy_fail_unknown_item(avatar_in_city, mock_objects):
|
||||
with patch("src.classes.action.buy.elixirs_by_name", elixirs_mock), \
|
||||
patch("src.classes.action.buy.items_by_name", items_mock):
|
||||
|
||||
action = BuyItem(avatar_in_city, avatar_in_city.world)
|
||||
action = Buy(avatar_in_city, avatar_in_city.world)
|
||||
can_start, reason = action.can_start("不存在的东西")
|
||||
|
||||
assert can_start is False
|
||||
@@ -179,7 +179,7 @@ def test_buy_elixir_fail_realm_too_low(avatar_in_city, mock_objects):
|
||||
with patch("src.classes.action.buy.elixirs_by_name", elixirs_mock), \
|
||||
patch("src.classes.action.buy.items_by_name", items_mock):
|
||||
|
||||
action = BuyItem(avatar_in_city, avatar_in_city.world)
|
||||
action = Buy(avatar_in_city, avatar_in_city.world)
|
||||
can_start, reason = action.can_start("筑基丹")
|
||||
|
||||
assert can_start is False
|
||||
@@ -202,7 +202,7 @@ def test_buy_elixir_fail_duplicate_active(avatar_in_city, mock_objects):
|
||||
with patch("src.classes.action.buy.elixirs_by_name", elixirs_mock), \
|
||||
patch("src.classes.action.buy.items_by_name", items_mock):
|
||||
|
||||
action = BuyItem(avatar_in_city, avatar_in_city.world)
|
||||
action = Buy(avatar_in_city, avatar_in_city.world)
|
||||
can_start, reason = action.can_start("聚气丹")
|
||||
|
||||
assert can_start is False
|
||||
|
||||
215
tests/test_sell_action.py
Normal file
215
tests/test_sell_action.py
Normal file
@@ -0,0 +1,215 @@
|
||||
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.weapon import Weapon
|
||||
from src.classes.auxiliary import Auxiliary
|
||||
from src.classes.cultivation import Realm
|
||||
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,
|
||||
name=name,
|
||||
desc="测试物品",
|
||||
realm=realm
|
||||
)
|
||||
|
||||
def create_test_weapon(name, realm, weapon_id=201):
|
||||
return Weapon(
|
||||
id=weapon_id,
|
||||
name=name,
|
||||
weapon_type=WeaponType.SWORD,
|
||||
realm=realm,
|
||||
desc="测试兵器",
|
||||
effects={},
|
||||
effect_desc=""
|
||||
)
|
||||
|
||||
def create_test_auxiliary(name, realm, aux_id=301):
|
||||
return Auxiliary(
|
||||
id=aux_id,
|
||||
name=name,
|
||||
realm=realm,
|
||||
desc="测试法宝",
|
||||
effects={},
|
||||
effect_desc=""
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def avatar_in_city(dummy_avatar):
|
||||
"""
|
||||
修改 dummy_avatar,使其位于城市中,并给予初始状态
|
||||
"""
|
||||
city_region = CityRegion(id=1, name="TestCity", desc="测试城市")
|
||||
tile = Tile(0, 0, TileType.CITY)
|
||||
tile.region = city_region
|
||||
|
||||
dummy_avatar.tile = tile
|
||||
dummy_avatar.magic_stone = 0
|
||||
dummy_avatar.items = {}
|
||||
dummy_avatar.weapon = None
|
||||
dummy_avatar.auxiliary = None
|
||||
|
||||
return dummy_avatar
|
||||
|
||||
@pytest.fixture
|
||||
def mock_sell_objects():
|
||||
"""
|
||||
Mock items_by_name 并提供测试对象
|
||||
"""
|
||||
test_item = create_test_item("铁矿石", Realm.Qi_Refinement)
|
||||
test_weapon = create_test_weapon("青云剑", Realm.Qi_Refinement)
|
||||
test_auxiliary = create_test_auxiliary("聚灵珠", Realm.Qi_Refinement)
|
||||
|
||||
items_mock = {
|
||||
"铁矿石": test_item
|
||||
}
|
||||
|
||||
return items_mock, test_item, test_weapon, test_auxiliary
|
||||
|
||||
def test_sell_item_success(avatar_in_city, mock_sell_objects):
|
||||
"""测试出售普通物品成功"""
|
||||
items_mock, test_item, _, _ = mock_sell_objects
|
||||
|
||||
# 给角色添加物品
|
||||
avatar_in_city.add_item(test_item, quantity=5)
|
||||
|
||||
with patch("src.classes.action.sell.items_by_name", items_mock):
|
||||
action = Sell(avatar_in_city, avatar_in_city.world)
|
||||
|
||||
# 1. 检查是否可出售
|
||||
can_start, reason = action.can_start("铁矿石")
|
||||
assert can_start is True
|
||||
|
||||
# 2. 执行出售
|
||||
# 练气期物品基础价格 10,卖出倍率默认为 1.0 -> 单价 10
|
||||
# 卖出全部 5 个 -> 总价 50
|
||||
initial_money = avatar_in_city.magic_stone
|
||||
expected_income = 50
|
||||
|
||||
action._execute("铁矿石")
|
||||
|
||||
# 3. 验证结果
|
||||
assert avatar_in_city.magic_stone == initial_money + expected_income
|
||||
assert avatar_in_city.get_item_quantity(test_item) == 0
|
||||
|
||||
def test_sell_weapon_success(avatar_in_city, mock_sell_objects):
|
||||
"""测试出售当前兵器成功"""
|
||||
items_mock, _, test_weapon, _ = mock_sell_objects
|
||||
|
||||
# 装备兵器
|
||||
avatar_in_city.weapon = test_weapon
|
||||
|
||||
with patch("src.classes.action.sell.items_by_name", items_mock):
|
||||
action = Sell(avatar_in_city, avatar_in_city.world)
|
||||
|
||||
# 1. 检查是否可出售
|
||||
can_start, reason = action.can_start("青云剑")
|
||||
assert can_start is True
|
||||
|
||||
# 2. 执行出售
|
||||
# 练气期兵器基础价格 100,卖出倍率 1.0 -> 100
|
||||
# 注意:Prices.WEAPON_PRICES[Realm.Qi_Refinement] 实际值需确认,假设是 default 100 或 mock
|
||||
# 根据 prices.py: WEAPON_PRICES = {Realm.Qi_Refinement: 10...}
|
||||
# 等等,prices.py 里 Qi_Refinement 兵器是 10 吗?
|
||||
# 让我们 check prices.py 的内容:
|
||||
# Realm.Qi_Refinement: 10 (ITEM_PRICES)
|
||||
# Realm.Qi_Refinement: 10 (WEAPON_PRICES)
|
||||
# Realm.Qi_Refinement: 10 (AUXILIARY_PRICES)
|
||||
# 看来练气期都是 10。
|
||||
|
||||
expected_income = 10
|
||||
|
||||
action._execute("青云剑")
|
||||
|
||||
# 3. 验证结果
|
||||
assert avatar_in_city.magic_stone == expected_income
|
||||
assert avatar_in_city.weapon is None
|
||||
|
||||
def test_sell_auxiliary_success(avatar_in_city, mock_sell_objects):
|
||||
"""测试出售当前法宝成功"""
|
||||
items_mock, _, _, test_auxiliary = mock_sell_objects
|
||||
|
||||
# 装备法宝
|
||||
avatar_in_city.auxiliary = test_auxiliary
|
||||
|
||||
with patch("src.classes.action.sell.items_by_name", items_mock):
|
||||
action = Sell(avatar_in_city, avatar_in_city.world)
|
||||
|
||||
can_start, reason = action.can_start("聚灵珠")
|
||||
assert can_start is True
|
||||
|
||||
# 练气期辅助装备也是 10
|
||||
expected_income = 10
|
||||
|
||||
action._execute("聚灵珠")
|
||||
|
||||
assert avatar_in_city.magic_stone == expected_income
|
||||
assert avatar_in_city.auxiliary is None
|
||||
|
||||
def test_sell_fail_not_in_city(dummy_avatar, mock_sell_objects):
|
||||
"""测试不在城市无法出售"""
|
||||
items_mock, test_item, _, _ = mock_sell_objects
|
||||
|
||||
# 确保不在城市
|
||||
assert not isinstance(dummy_avatar.tile.region, CityRegion)
|
||||
dummy_avatar.add_item(test_item, 1)
|
||||
|
||||
with patch("src.classes.action.sell.items_by_name", items_mock):
|
||||
action = Sell(dummy_avatar, dummy_avatar.world)
|
||||
can_start, reason = action.can_start("铁矿石")
|
||||
|
||||
assert can_start is False
|
||||
assert "仅能在城市" in reason
|
||||
|
||||
def test_sell_fail_no_item(avatar_in_city, mock_sell_objects):
|
||||
"""测试未持有该物品"""
|
||||
items_mock, _, _, _ = mock_sell_objects
|
||||
|
||||
# 背包为空,无装备
|
||||
|
||||
with patch("src.classes.action.sell.items_by_name", items_mock):
|
||||
action = Sell(avatar_in_city, avatar_in_city.world)
|
||||
can_start, reason = action.can_start("铁矿石")
|
||||
|
||||
assert can_start is False
|
||||
assert "未持有物品/装备" in reason
|
||||
|
||||
def test_sell_fail_unknown_name(avatar_in_city, mock_sell_objects):
|
||||
"""测试未知物品名称"""
|
||||
items_mock, _, _, _ = mock_sell_objects
|
||||
|
||||
with patch("src.classes.action.sell.items_by_name", items_mock):
|
||||
action = Sell(avatar_in_city, avatar_in_city.world)
|
||||
can_start, reason = action.can_start("不存在的神器")
|
||||
|
||||
assert can_start is False
|
||||
assert "未持有物品/装备" in reason
|
||||
|
||||
def test_sell_priority(avatar_in_city, mock_sell_objects):
|
||||
"""测试物品优先级:同名时优先卖背包里的材料"""
|
||||
items_mock, test_item, test_weapon, _ = mock_sell_objects
|
||||
|
||||
# 构造一个同名的兵器和材料(虽然逻辑上不太可能,但测试代码健壮性)
|
||||
# 假设 items_mock 里有一个 "青云剑" 的材料
|
||||
fake_sword_item = create_test_item("青云剑", Realm.Qi_Refinement)
|
||||
items_mock["青云剑"] = fake_sword_item
|
||||
|
||||
# 角色同时拥有该材料和该兵器
|
||||
avatar_in_city.add_item(fake_sword_item, 1)
|
||||
avatar_in_city.weapon = test_weapon # name也是 "青云剑"
|
||||
|
||||
with patch("src.classes.action.sell.items_by_name", items_mock):
|
||||
action = Sell(avatar_in_city, avatar_in_city.world)
|
||||
|
||||
# 执行出售
|
||||
action._execute("青云剑")
|
||||
|
||||
# 应该优先卖掉了材料
|
||||
assert avatar_in_city.get_item_quantity(fake_sword_item) == 0
|
||||
assert avatar_in_city.weapon is not None # 兵器还在
|
||||
|
||||
Reference in New Issue
Block a user