refactor economy prices
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 .sold import SellItems
|
||||
from .sell import SellItems
|
||||
from .attack import Attack
|
||||
from .plunder_mortals import PlunderMortals
|
||||
from .help_mortals import HelpMortals
|
||||
|
||||
@@ -4,14 +4,13 @@ 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.prices import prices
|
||||
from src.classes.normalize import normalize_item_name
|
||||
|
||||
|
||||
class SellItems(InstantAction):
|
||||
"""
|
||||
在城镇出售指定名称的物品,一次性卖出持有的全部数量。
|
||||
收益为 item_price * item_num,动作耗时1个月。
|
||||
收益通过 avatar.sell_item() 结算。
|
||||
"""
|
||||
|
||||
ACTION_NAME = "出售物品"
|
||||
@@ -37,21 +36,8 @@ class SellItems(InstantAction):
|
||||
if quantity <= 0:
|
||||
return
|
||||
|
||||
# 计算价格并结算
|
||||
price_per = prices.get_price(item)
|
||||
base_total_gain = price_per * quantity
|
||||
|
||||
# 应用出售价格倍率加成
|
||||
price_multiplier_raw = self.avatar.effects.get("extra_item_sell_price_multiplier", 0.0)
|
||||
price_multiplier = 1.0 + float(price_multiplier_raw or 0.0)
|
||||
total_gain = int(base_total_gain * price_multiplier)
|
||||
|
||||
# 扣除物品并增加灵石
|
||||
removed = self.avatar.remove_item(item, quantity)
|
||||
if not removed:
|
||||
return
|
||||
|
||||
self.avatar.magic_stone = self.avatar.magic_stone + total_gain
|
||||
# 通过统一接口出售
|
||||
self.avatar.sell_item(item, quantity)
|
||||
|
||||
def can_start(self, item_name: str | None = None) -> tuple[bool, str]:
|
||||
region = self.avatar.tile.region
|
||||
@@ -78,9 +64,6 @@ class SellItems(InstantAction):
|
||||
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])
|
||||
|
||||
# InstantAction 已实现 step 完成
|
||||
|
||||
async def finish(self, item_name: str) -> list[Event]:
|
||||
return []
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ def get_avatar_info(avatar: "Avatar", detailed: bool = False) -> dict:
|
||||
from src.classes.sect import get_sect_info_with_rank
|
||||
|
||||
if detailed:
|
||||
weapon_info = f"{avatar.weapon.get_detailed_info()},熟练度:{avatar.weapon_proficiency:.1f}%"
|
||||
auxiliary_info = avatar.auxiliary.get_detailed_info() if avatar.auxiliary is not None else "无"
|
||||
weapon_info = f"{avatar.weapon.get_detailed_info()},熟练度:{avatar.weapon_proficiency:.1f}%" if avatar.weapon else "无"
|
||||
auxiliary_info = avatar.auxiliary.get_detailed_info() if avatar.auxiliary else "无"
|
||||
sect_info = get_sect_info_with_rank(avatar, detailed=True)
|
||||
alignment_info = avatar.alignment.get_detailed_info() if avatar.alignment is not None else "未知"
|
||||
region_info = region.get_detailed_info() if region is not None else "无"
|
||||
|
||||
@@ -104,3 +104,50 @@ class InventoryMixin:
|
||||
actual_amount = amount * gain_multiplier
|
||||
self.weapon_proficiency = min(100.0, self.weapon_proficiency + actual_amount)
|
||||
|
||||
# ==================== 出售接口 ====================
|
||||
|
||||
def _get_sell_multiplier(self: "Avatar") -> float:
|
||||
"""获取出售价格倍率(包含效果加成)"""
|
||||
raw = self.effects.get("extra_item_sell_price_multiplier", 0.0)
|
||||
return 1.0 + float(raw or 0.0)
|
||||
|
||||
def sell_item(self: "Avatar", item: "Item", 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:
|
||||
return 0
|
||||
|
||||
self.remove_item(item, quantity)
|
||||
|
||||
base_price = prices.get_item_price(item) * quantity
|
||||
total = int(base_price * self._get_sell_multiplier())
|
||||
|
||||
self.magic_stone = self.magic_stone + total
|
||||
return total
|
||||
|
||||
def sell_weapon(self: "Avatar", weapon: "Weapon") -> int:
|
||||
"""
|
||||
出售兵器,返回获得的灵石数量。
|
||||
注意:这是辅助方法,不会自动卸下当前装备。
|
||||
"""
|
||||
from src.classes.prices import prices
|
||||
|
||||
total = int(prices.get_weapon_price(weapon) * self._get_sell_multiplier())
|
||||
self.magic_stone = self.magic_stone + total
|
||||
return total
|
||||
|
||||
def sell_auxiliary(self: "Avatar", auxiliary: "Auxiliary") -> int:
|
||||
"""
|
||||
出售辅助装备,返回获得的灵石数量。
|
||||
注意:这是辅助方法,不会自动卸下当前装备。
|
||||
"""
|
||||
from src.classes.prices import prices
|
||||
|
||||
total = int(prices.get_auxiliary_price(auxiliary) * self._get_sell_multiplier())
|
||||
self.magic_stone = self.magic_stone + total
|
||||
return total
|
||||
|
||||
|
||||
@@ -423,7 +423,7 @@ async def try_trigger_fortune(avatar: Avatar) -> list[Event]:
|
||||
},
|
||||
{
|
||||
"key": "B",
|
||||
"desc": f"放弃原{type_label},接受新{type_label}『{new_name}』({new_grade_val})。"
|
||||
"desc": f"卖掉原{type_label}『{old_name}』换取灵石,接受新{type_label}『{new_name}』({new_grade_val})。"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -435,7 +435,7 @@ async def try_trigger_fortune(avatar: Avatar) -> list[Event]:
|
||||
if choice == "A":
|
||||
return False, f"{avatar.name} 放弃了{new_grade_val}{type_label}『{new_name}』,保留了『{old_name}』"
|
||||
else:
|
||||
return True, f"{avatar.name} 获得了{new_grade_val}{type_label}『{new_name}』,替换了『{old_name}』"
|
||||
return True, f"{avatar.name} 获得了{new_grade_val}{type_label}『{new_name}』,卖掉了『{old_name}』"
|
||||
|
||||
if kind == FortuneKind.WEAPON:
|
||||
weapon = _get_weapon_for_avatar(avatar)
|
||||
@@ -448,6 +448,9 @@ async def try_trigger_fortune(avatar: Avatar) -> list[Event]:
|
||||
weapon, avatar.weapon, "兵器"
|
||||
)
|
||||
if should_equip:
|
||||
# 自动卖掉旧武器
|
||||
if avatar.weapon is not None:
|
||||
avatar.sell_weapon(avatar.weapon)
|
||||
avatar.change_weapon(weapon)
|
||||
|
||||
if kind == FortuneKind.AUXILIARY:
|
||||
@@ -461,6 +464,9 @@ async def try_trigger_fortune(avatar: Avatar) -> list[Event]:
|
||||
auxiliary, avatar.auxiliary, "辅助装备"
|
||||
)
|
||||
if should_equip:
|
||||
# 自动卖掉旧辅助装备
|
||||
if avatar.auxiliary is not None:
|
||||
avatar.sell_auxiliary(avatar.auxiliary)
|
||||
avatar.change_auxiliary(auxiliary)
|
||||
|
||||
if kind == FortuneKind.TECHNIQUE:
|
||||
|
||||
@@ -56,7 +56,7 @@ async def kill_and_grab(winner: Avatar, loser: Avatar) -> str:
|
||||
options = [
|
||||
{
|
||||
"key": "A",
|
||||
"desc": f"夺取『{loot_item.name}』,替换掉身上的『{winner_current.name}』。\n - 新装备:{item_desc}\n - 原装备:{current_desc}"
|
||||
"desc": f"夺取『{loot_item.name}』,卖掉身上的『{winner_current.name}』换取灵石。\n - 新装备:{item_desc}\n - 原装备:{current_desc}"
|
||||
},
|
||||
{
|
||||
"key": "B",
|
||||
@@ -69,9 +69,15 @@ async def kill_and_grab(winner: Avatar, loser: Avatar) -> str:
|
||||
|
||||
if should_loot:
|
||||
if loot_type == "weapon":
|
||||
# 自动卖掉旧武器
|
||||
if winner.weapon is not None:
|
||||
winner.sell_weapon(winner.weapon)
|
||||
winner.change_weapon(loot_item)
|
||||
loser.change_weapon(None)
|
||||
else:
|
||||
# 自动卖掉旧辅助装备
|
||||
if winner.auxiliary is not None:
|
||||
winner.sell_auxiliary(winner.auxiliary)
|
||||
winner.change_auxiliary(loot_item)
|
||||
loser.change_auxiliary(None)
|
||||
|
||||
|
||||
@@ -1,23 +1,89 @@
|
||||
"""
|
||||
统一价格系统
|
||||
============
|
||||
|
||||
所有物品/装备的价格通过这个模块获取。
|
||||
价格只和对应的 realm 绑定,全局统一。
|
||||
|
||||
价格设计参考(以练气期年收入约 20-30 灵石为基准):
|
||||
- 材料(Item): 采集物等消耗品
|
||||
- 兵器(Weapon): 稀有装备,价值较高
|
||||
- 辅助装备(Auxiliary): 法宝等,价值次于兵器
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union, TYPE_CHECKING
|
||||
|
||||
from src.classes.cultivation import Realm
|
||||
from src.classes.item import Item
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.item import Item
|
||||
from src.classes.weapon import Weapon
|
||||
from src.classes.auxiliary import Auxiliary
|
||||
|
||||
# 类型别名
|
||||
Sellable = Union["Item", "Weapon", "Auxiliary"]
|
||||
|
||||
|
||||
class Prices:
|
||||
"""
|
||||
价格体系。
|
||||
刚开始我只准备做一个比较简单的价格体系,之后可能复杂化。
|
||||
目前是所有的城镇都可以出售材料,同时这些材料的价格是固定的,并且全局公开。
|
||||
价格只和对应的realm绑定。
|
||||
所有城镇可交易物品/装备的价格在此统一管理。
|
||||
"""
|
||||
def __init__(self):
|
||||
self.realm_to_prices = {
|
||||
Realm.Qi_Refinement: 10,
|
||||
Realm.Foundation_Establishment: 50,
|
||||
Realm.Core_Formation: 100,
|
||||
Realm.Nascent_Soul: 200,
|
||||
}
|
||||
|
||||
# 材料价格表(采集物等)
|
||||
ITEM_PRICES = {
|
||||
Realm.Qi_Refinement: 10,
|
||||
Realm.Foundation_Establishment: 30,
|
||||
Realm.Core_Formation: 60,
|
||||
Realm.Nascent_Soul: 100,
|
||||
}
|
||||
|
||||
# 兵器价格表(稀有,价值高)
|
||||
WEAPON_PRICES = {
|
||||
Realm.Qi_Refinement: 10,
|
||||
Realm.Foundation_Establishment: 300,
|
||||
Realm.Core_Formation: 1000,
|
||||
Realm.Nascent_Soul: 2000,
|
||||
}
|
||||
|
||||
# 辅助装备价格表
|
||||
AUXILIARY_PRICES = {
|
||||
Realm.Qi_Refinement: 10,
|
||||
Realm.Foundation_Establishment: 250,
|
||||
Realm.Core_Formation: 800,
|
||||
Realm.Nascent_Soul: 1600,
|
||||
}
|
||||
|
||||
def get_item_price(self, item: "Item") -> int:
|
||||
"""获取材料价格"""
|
||||
return self.ITEM_PRICES.get(item.realm, 10)
|
||||
|
||||
def get_weapon_price(self, weapon: "Weapon") -> int:
|
||||
"""获取兵器价格"""
|
||||
return self.WEAPON_PRICES.get(weapon.realm, 100)
|
||||
|
||||
def get_auxiliary_price(self, auxiliary: "Auxiliary") -> int:
|
||||
"""获取辅助装备价格"""
|
||||
return self.AUXILIARY_PRICES.get(auxiliary.realm, 80)
|
||||
|
||||
def get_price(self, obj: Sellable) -> int:
|
||||
"""
|
||||
统一价格查询接口。
|
||||
根据对象类型自动分发到对应的价格查询方法。
|
||||
"""
|
||||
from src.classes.item import Item
|
||||
from src.classes.weapon import Weapon
|
||||
from src.classes.auxiliary import Auxiliary
|
||||
|
||||
if isinstance(obj, Item):
|
||||
return self.get_item_price(obj)
|
||||
elif isinstance(obj, Weapon):
|
||||
return self.get_weapon_price(obj)
|
||||
elif isinstance(obj, Auxiliary):
|
||||
return self.get_auxiliary_price(obj)
|
||||
return 0
|
||||
|
||||
def get_price(self, item: Item) -> int:
|
||||
return self.realm_to_prices[item.realm]
|
||||
|
||||
# 预先创建全局价格实例,供全局使用
|
||||
prices = Prices()
|
||||
# 全局单例
|
||||
prices = Prices()
|
||||
|
||||
178
tests/test_prices.py
Normal file
178
tests/test_prices.py
Normal file
@@ -0,0 +1,178 @@
|
||||
import pytest
|
||||
import copy
|
||||
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.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
|
||||
|
||||
|
||||
class TestPrices:
|
||||
"""价格系统测试"""
|
||||
|
||||
def test_item_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]
|
||||
|
||||
def test_weapon_prices_by_realm(self):
|
||||
"""测试兵器价格按境界递增"""
|
||||
assert prices.WEAPON_PRICES[Realm.Qi_Refinement] < prices.WEAPON_PRICES[Realm.Foundation_Establishment]
|
||||
assert prices.WEAPON_PRICES[Realm.Foundation_Establishment] < prices.WEAPON_PRICES[Realm.Core_Formation]
|
||||
assert prices.WEAPON_PRICES[Realm.Core_Formation] < prices.WEAPON_PRICES[Realm.Nascent_Soul]
|
||||
|
||||
def test_auxiliary_prices_by_realm(self):
|
||||
"""测试辅助装备价格按境界递增"""
|
||||
assert prices.AUXILIARY_PRICES[Realm.Qi_Refinement] < prices.AUXILIARY_PRICES[Realm.Foundation_Establishment]
|
||||
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:
|
||||
pytest.skip("No items available in config")
|
||||
|
||||
item = next(iter(items_by_id.values()))
|
||||
price = prices.get_price(item)
|
||||
expected = prices.get_item_price(item)
|
||||
assert price == expected
|
||||
assert price == prices.ITEM_PRICES[item.realm]
|
||||
|
||||
def test_get_price_for_weapon(self):
|
||||
"""测试 get_price 对 Weapon 类型的分发"""
|
||||
if not weapons_by_id:
|
||||
pytest.skip("No weapons available in config")
|
||||
|
||||
weapon = next(iter(weapons_by_id.values()))
|
||||
price = prices.get_price(weapon)
|
||||
expected = prices.get_weapon_price(weapon)
|
||||
assert price == expected
|
||||
assert price == prices.WEAPON_PRICES[weapon.realm]
|
||||
|
||||
def test_get_price_for_auxiliary(self):
|
||||
"""测试 get_price 对 Auxiliary 类型的分发"""
|
||||
if not auxiliaries_by_id:
|
||||
pytest.skip("No auxiliaries available in config")
|
||||
|
||||
aux = next(iter(auxiliaries_by_id.values()))
|
||||
price = prices.get_price(aux)
|
||||
expected = prices.get_auxiliary_price(aux)
|
||||
assert price == expected
|
||||
assert price == prices.AUXILIARY_PRICES[aux.realm]
|
||||
|
||||
def test_weapon_more_expensive_than_item(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]
|
||||
|
||||
|
||||
class TestAvatarSell:
|
||||
"""Avatar 出售接口测试"""
|
||||
|
||||
def test_sell_item_basic(self, dummy_avatar):
|
||||
"""测试基础材料出售"""
|
||||
if not items_by_id:
|
||||
pytest.skip("No items available in config")
|
||||
|
||||
item = next(iter(items_by_id.values()))
|
||||
dummy_avatar.items = {} # 清空背包
|
||||
dummy_avatar.magic_stone.value = 0
|
||||
|
||||
# 添加物品
|
||||
dummy_avatar.add_item(item, 5)
|
||||
assert dummy_avatar.get_item_quantity(item) == 5
|
||||
|
||||
# 出售3个
|
||||
gained = dummy_avatar.sell_item(item, 3)
|
||||
|
||||
expected_price = prices.get_item_price(item) * 3
|
||||
assert gained == expected_price
|
||||
assert dummy_avatar.magic_stone.value == expected_price
|
||||
assert dummy_avatar.get_item_quantity(item) == 2
|
||||
|
||||
def test_sell_item_insufficient(self, dummy_avatar):
|
||||
"""测试出售物品数量不足"""
|
||||
if not items_by_id:
|
||||
pytest.skip("No items available in config")
|
||||
|
||||
item = next(iter(items_by_id.values()))
|
||||
dummy_avatar.items = {}
|
||||
dummy_avatar.magic_stone.value = 100
|
||||
|
||||
dummy_avatar.add_item(item, 2)
|
||||
|
||||
# 尝试出售5个(只有2个)
|
||||
gained = dummy_avatar.sell_item(item, 5)
|
||||
|
||||
assert gained == 0
|
||||
assert dummy_avatar.magic_stone.value == 100 # 没有变化
|
||||
assert dummy_avatar.get_item_quantity(item) == 2 # 物品未减少
|
||||
|
||||
def test_sell_weapon(self, dummy_avatar):
|
||||
"""测试出售兵器"""
|
||||
weapon = get_random_weapon_by_realm(Realm.Foundation_Establishment)
|
||||
if not weapon:
|
||||
pytest.skip("No Foundation Establishment weapons available")
|
||||
|
||||
dummy_avatar.magic_stone.value = 0
|
||||
|
||||
gained = dummy_avatar.sell_weapon(weapon)
|
||||
|
||||
expected = prices.get_weapon_price(weapon)
|
||||
assert gained == expected
|
||||
assert dummy_avatar.magic_stone.value == expected
|
||||
|
||||
def test_sell_auxiliary(self, dummy_avatar):
|
||||
"""测试出售辅助装备"""
|
||||
aux = get_random_auxiliary_by_realm(Realm.Core_Formation)
|
||||
if not aux:
|
||||
pytest.skip("No Core Formation auxiliaries available")
|
||||
|
||||
dummy_avatar.magic_stone.value = 0
|
||||
|
||||
gained = dummy_avatar.sell_auxiliary(aux)
|
||||
|
||||
expected = prices.get_auxiliary_price(aux)
|
||||
assert gained == expected
|
||||
assert dummy_avatar.magic_stone.value == expected
|
||||
|
||||
def test_sell_with_price_multiplier(self, dummy_avatar):
|
||||
"""测试出售价格倍率效果"""
|
||||
if not items_by_id:
|
||||
pytest.skip("No items available in config")
|
||||
|
||||
item = next(iter(items_by_id.values()))
|
||||
dummy_avatar.items = {}
|
||||
dummy_avatar.magic_stone.value = 0
|
||||
dummy_avatar.add_item(item, 1)
|
||||
|
||||
base_price = prices.get_item_price(item)
|
||||
|
||||
# 设置 20% 加成 - patch 内部方法
|
||||
with patch.object(dummy_avatar, '_get_sell_multiplier', return_value=1.2):
|
||||
gained = dummy_avatar.sell_item(item, 1)
|
||||
|
||||
expected = int(base_price * 1.2)
|
||||
assert gained == expected
|
||||
assert dummy_avatar.magic_stone.value == expected
|
||||
|
||||
def test_sell_weapon_with_multiplier(self, dummy_avatar):
|
||||
"""测试出售兵器时价格倍率生效"""
|
||||
weapon = get_random_weapon_by_realm(Realm.Qi_Refinement)
|
||||
if not weapon:
|
||||
pytest.skip("No Qi Refinement weapons available")
|
||||
|
||||
dummy_avatar.magic_stone.value = 0
|
||||
base_price = prices.get_weapon_price(weapon)
|
||||
|
||||
# 设置 50% 加成 - patch 内部方法
|
||||
with patch.object(dummy_avatar, '_get_sell_multiplier', return_value=1.5):
|
||||
gained = dummy_avatar.sell_weapon(weapon)
|
||||
|
||||
expected = int(base_price * 1.5)
|
||||
assert gained == expected
|
||||
|
||||
Reference in New Issue
Block a user