refactor economy prices

This commit is contained in:
bridge
2025-12-31 23:41:45 +08:00
parent 3a47d48fb8
commit b43530ee99
8 changed files with 327 additions and 41 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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