mirror of
https://github.com/Shu-Ying/main_plugin_farm.git
synced 2026-11-02 18:50:08 +08:00
add # 新增经验获取方式
This commit is contained in:
@@ -74,7 +74,8 @@ jobs:
|
|||||||
|
|
||||||
sync-preview:
|
sync-preview:
|
||||||
if: >-
|
if: >-
|
||||||
github.event_name == 'workflow_dispatch' && inputs.publish &&
|
(github.event_name == 'push' ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && inputs.publish)) &&
|
||||||
github.ref == 'refs/heads/main' &&
|
github.ref == 'refs/heads/main' &&
|
||||||
github.repository == 'Shu-Ying/main_plugin_farm'
|
github.repository == 'Shu-Ying/main_plugin_farm'
|
||||||
needs: [test, build]
|
needs: [test, build]
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ v1→v2→v3、v2→v3 都在初始化事务内迁移,保留已有资产、流
|
|||||||
## QQ 指令与绘制
|
## QQ 指令与绘制
|
||||||
|
|
||||||
发送 `农场除虫` 或 `除虫`,一次清除自己所有地块现存虫子,
|
发送 `农场除虫` 或 `除虫`,一次清除自己所有地块现存虫子,
|
||||||
不收费、不奖励经验;无虫时返回明确提示。重复事件返回原结果,
|
不收费;每实际清除 1 只虫奖励 1 经验,无虫时不奖励并返回明确提示。重复事件返回原结果,
|
||||||
事务失败全部回滚。`农场详述` 显示虫数、当前减速及已冻结减产比例,
|
事务失败全部回滚。`农场详述` 显示虫数、当前减速及已冻结减产比例,
|
||||||
收获结果说明发生减产的地块。
|
收获结果说明发生减产的地块。
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# 当前经验规则
|
||||||
|
|
||||||
|
当前正式实现有四种经验来源:
|
||||||
|
|
||||||
|
| 来源 | 结算规则 |
|
||||||
|
| --- | --- |
|
||||||
|
| 成功播种 | 每成功播种 1 块地获得 2 经验;一次播种 3 块地获得 6 经验 |
|
||||||
|
| 收获成熟作物 | 每块地每季按播种时冻结的作物经验与该地块经验加成结算,然后累加 |
|
||||||
|
| 铲除枯萎作物 | 每实际清理 1 块枯萎地块获得 2 经验 |
|
||||||
|
| 除虫 | 每实际清除 1 只虫获得 1 经验;跨地块累加 |
|
||||||
|
|
||||||
|
收获单地块经验为 `基础经验 × (100 + 地块经验加成百分比) // 100`。
|
||||||
|
多季作物每季收获都会结算;虫害减产不减少收获经验。
|
||||||
|
购买、出售、查看农场或商店均不增加经验。
|
||||||
|
开通农场初始经验为 0。未实现的签到等玩法不属于当前经验来源。
|
||||||
|
|
||||||
|
播种、铲除和除虫奖励与对应地块更新及流水在同一数据库事务中提交。
|
||||||
|
失败不奖励,同一消息重试不重复奖励;已有历史操作不补发。
|
||||||
|
没有枯萎作物或虫子时不奖励。成熟后除虫仍按清除数量奖励,
|
||||||
|
但不会恢复已确定的本季虫害减产。铲除仍仅支持枯萎作物。
|
||||||
|
|
||||||
|
等级 k 的累计经验门槛为 `200 × k × (k + 1) // 2`。
|
||||||
|
经验条显示本级已获经验与本级所需经验:
|
||||||
|
`累计经验 - 当前等级门槛` / `200 × (k + 1)`。
|
||||||
|
例如累计经验 599、等级 1 时显示 399/400。
|
||||||
@@ -31,6 +31,8 @@ HELP = (
|
|||||||
"收获|铲除|农场除虫|我的种子|我的作物\n"
|
"收获|铲除|农场除虫|我的种子|我的作物\n"
|
||||||
"作物加锁 作物|作物解锁 作物|出售作物 [作物] [数量]\n"
|
"作物加锁 作物|作物解锁 作物|出售作物 [作物] [数量]\n"
|
||||||
"作物可填写名称或 #编号;同名作物请使用商店或仓库中的编号。\n"
|
"作物可填写名称或 #编号;同名作物请使用商店或仓库中的编号。\n"
|
||||||
|
"每成功播种1块地获得2经验;收获经验按作物结算。\n"
|
||||||
|
"每铲除1块枯萎地块获得2经验;每清除1只虫获得1经验。\n"
|
||||||
"每只虫使生长速度降低5%;成熟时每只虫使本季减产5%。\n"
|
"每只虫使生长速度降低5%;成熟时每只虫使本季减产5%。\n"
|
||||||
"成熟后除虫不能恢复本季减产;生长期间可发送:农场除虫。\n"
|
"成熟后除虫不能恢复本季减产;生长期间可发送:农场除虫。\n"
|
||||||
"我的农场按北京时间自动切换春夏秋冬背景;高清使用相同季节。\n"
|
"我的农场按北京时间自动切换春夏秋冬背景;高清使用相同季节。\n"
|
||||||
|
|||||||
@@ -30,9 +30,16 @@ def main():
|
|||||||
if inputs["publish"]["default"] != "false":
|
if inputs["publish"]["default"] != "false":
|
||||||
raise ValueError("Publish must be opt-in")
|
raise ValueError("Publish must be opt-in")
|
||||||
sync = source["jobs"]["sync-preview"]
|
sync = source["jobs"]["sync-preview"]
|
||||||
|
expected_condition = (
|
||||||
|
"(github.event_name == 'push' || "
|
||||||
|
"(github.event_name == 'workflow_dispatch' && inputs.publish)) && "
|
||||||
|
"github.ref == 'refs/heads/main' && "
|
||||||
|
"github.repository == 'Shu-Ying/main_plugin_farm'"
|
||||||
|
)
|
||||||
if (sync["environment"] != "zhenxun-preview"
|
if (sync["environment"] != "zhenxun-preview"
|
||||||
or "refs/heads/main" not in sync["if"]
|
or " ".join(sync["if"].split()) != expected_condition
|
||||||
or "workflow_dispatch" not in sync["if"]
|
or trigger["push"]["branches"] != ["main"]
|
||||||
|
or source["jobs"]["build"]["needs"] != "test"
|
||||||
or sync["needs"] != ["test", "build"]):
|
or sync["needs"] != ["test", "build"]):
|
||||||
raise ValueError("Publish boundary differs")
|
raise ValueError("Publish boundary differs")
|
||||||
if generated["permissions"] != {"contents":"read"}:
|
if generated["permissions"] != {"contents":"read"}:
|
||||||
|
|||||||
@@ -120,11 +120,14 @@ class FarmService:
|
|||||||
repository.save_plot(start_growth(replace(
|
repository.save_plot(start_growth(replace(
|
||||||
plot, plant=plant, mature_at=mature_at, harvest_count=0,
|
plot, plant=plant, mature_at=mature_at, harvest_count=0,
|
||||||
), now))
|
), now))
|
||||||
|
experience = integer(amount * 2)
|
||||||
|
repository.asset(Asset.EXPERIENCE, experience)
|
||||||
return self._result(
|
return self._result(
|
||||||
repository, name=plant.name, plant_id=plant.plant_id,
|
repository, name=plant.name, plant_id=plant.plant_id,
|
||||||
quantity=amount,
|
quantity=amount,
|
||||||
slots=[p.slot for p in empty[:amount]],
|
slots=[p.slot for p in empty[:amount]],
|
||||||
seeds=repository.item(plant).seeds,
|
seeds=repository.item(plant).seeds,
|
||||||
|
experience=experience,
|
||||||
)
|
)
|
||||||
|
|
||||||
return await self._execute(
|
return await self._execute(
|
||||||
@@ -200,8 +203,11 @@ class FarmService:
|
|||||||
growth_updated_at=None, next_pest_at=None,
|
growth_updated_at=None, next_pest_at=None,
|
||||||
maturity_pests=0,
|
maturity_pests=0,
|
||||||
))
|
))
|
||||||
|
experience = integer(len(withered) * 2)
|
||||||
|
repository.asset(Asset.EXPERIENCE, experience)
|
||||||
return self._result(
|
return self._result(
|
||||||
repository, slots=[p.slot for p in withered],
|
repository, slots=[p.slot for p in withered],
|
||||||
|
experience=experience,
|
||||||
)
|
)
|
||||||
|
|
||||||
return await self._execute(
|
return await self._execute(
|
||||||
@@ -221,9 +227,11 @@ class FarmService:
|
|||||||
repository.save_plot(advance_plot(updated, now))
|
repository.save_plot(advance_plot(updated, now))
|
||||||
if not cleared:
|
if not cleared:
|
||||||
raise FarmError("nothing_to_clear")
|
raise FarmError("nothing_to_clear")
|
||||||
|
quantity = integer(sum(item['pests'] for item in cleared))
|
||||||
|
repository.asset(Asset.EXPERIENCE, quantity)
|
||||||
return self._result(
|
return self._result(
|
||||||
repository, cleared=cleared,
|
repository, cleared=cleared,
|
||||||
quantity=sum(item["pests"] for item in cleared),
|
quantity=quantity, experience=quantity,
|
||||||
)
|
)
|
||||||
|
|
||||||
return await self._execute(
|
return await self._execute(
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ def receipt_text(receipt: Receipt) -> str:
|
|||||||
lines.append(f"实际数量:{data['quantity']}")
|
lines.append(f"实际数量:{data['quantity']}")
|
||||||
if "proceeds" in data:
|
if "proceeds" in data:
|
||||||
lines.append(f"本次收入:{data['proceeds']}")
|
lines.append(f"本次收入:{data['proceeds']}")
|
||||||
|
if "experience" in data:
|
||||||
|
lines.append(f"本次经验:+{data['experience']}")
|
||||||
for item in data.get("harvested", ()):
|
for item in data.get("harvested", ()):
|
||||||
if item.get("yield_loss_percent", 0):
|
if item.get("yield_loss_percent", 0):
|
||||||
lines.append(
|
lines.append(
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ class FarmRenderer:
|
|||||||
account = scene.account
|
account = scene.account
|
||||||
level = account.level
|
level = account.level
|
||||||
current = account.experience - 200 * level * (level + 1) // 2
|
current = account.experience - 200 * level * (level + 1) // 2
|
||||||
next_total = 200 * (level + 1) * (level + 2) // 2
|
next_total = 200 * (level + 1)
|
||||||
for entry in layout['sprites']:
|
for entry in layout['sprites']:
|
||||||
x, y, w, h = entry['rect']
|
x, y, w, h = entry['rect']
|
||||||
image = self.assets.image(
|
image = self.assets.image(
|
||||||
|
|||||||
+29
-2
@@ -118,14 +118,41 @@ class PestTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
await self.service.clear_pests(self.who, "failed-clear")
|
await self.service.clear_pests(self.who, "failed-clear")
|
||||||
self.assertEqual(await self.store.snapshot(self.who), before)
|
self.assertEqual(await self.store.snapshot(self.who), before)
|
||||||
|
|
||||||
async def test_empty_clear_and_ripe_clear_reward_no_assets(self):
|
async def test_empty_clear_has_no_reward_and_clear_awards_per_pest(self):
|
||||||
with self.assertRaisesRegex(FarmError, "nothing_to_clear"):
|
with self.assertRaisesRegex(FarmError, "nothing_to_clear"):
|
||||||
await self.service.clear_pests(self.who, "empty-clear")
|
await self.service.clear_pests(self.who, "empty-clear")
|
||||||
self.now = 4600
|
self.now = 4600
|
||||||
before = await self.service.snapshot(self.who)
|
before = await self.service.snapshot(self.who)
|
||||||
await self.service.clear_pests(self.who, "clear")
|
await self.service.clear_pests(self.who, "clear")
|
||||||
after = await self.service.snapshot(self.who)
|
after = await self.service.snapshot(self.who)
|
||||||
self.assertEqual(before.account, after.account)
|
self.assertEqual(after.account.experience, before.account.experience+1)
|
||||||
|
self.assertEqual(after.account.point, before.account.point)
|
||||||
|
self.assertEqual(after.account.vip_point, before.account.vip_point)
|
||||||
|
|
||||||
|
async def test_multi_plot_pest_rewards_replay_and_transaction_rollback(self):
|
||||||
|
await self.service.buy(self.who, '虫害测试菜', 1, 'buy-second')
|
||||||
|
await self.service.sow(self.who, '虫害测试菜', 1, 'sow-second')
|
||||||
|
self.now = 11800
|
||||||
|
before = await self.store.snapshot(self.who)
|
||||||
|
ledger = await self.store.ledger(self.who)
|
||||||
|
with patch.object(SQLiteRepository, 'asset',
|
||||||
|
side_effect=RuntimeError('reward failure')):
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
await self.service.clear_pests(self.who, 'clear-six')
|
||||||
|
self.assertEqual(await self.store.snapshot(self.who), before)
|
||||||
|
self.assertEqual(await self.store.ledger(self.who), ledger)
|
||||||
|
receipts = await asyncio.gather(*(
|
||||||
|
self.service.clear_pests(self.who, 'clear-six') for _ in range(3)
|
||||||
|
))
|
||||||
|
self.assertTrue(all(r == receipts[0] for r in receipts))
|
||||||
|
self.assertEqual(receipts[0].data['quantity'], 6)
|
||||||
|
self.assertEqual(receipts[0].data['experience'], 6)
|
||||||
|
after = await self.service.snapshot(self.who)
|
||||||
|
self.assertEqual(after.account.experience, before.account.experience+6)
|
||||||
|
with self.assertRaisesRegex(FarmError, 'nothing_to_clear'):
|
||||||
|
await self.service.clear_pests(self.who, 'empty-after')
|
||||||
|
self.assertEqual((await self.service.snapshot(self.who)).account,
|
||||||
|
after.account)
|
||||||
|
|
||||||
async def test_projection_is_independent_of_query_frequency(self):
|
async def test_projection_is_independent_of_query_frequency(self):
|
||||||
initial = (await self.store.snapshot(self.who)).plots[0]
|
initial = (await self.store.snapshot(self.who)).plots[0]
|
||||||
|
|||||||
@@ -137,8 +137,8 @@ class RenderTests(unittest.TestCase):
|
|||||||
def test_web_panel_fields_fit_their_prefab_rectangles(self):
|
def test_web_panel_fields_fit_their_prefab_rectangles(self):
|
||||||
options = RenderOptions(width=3072)
|
options = RenderOptions(width=3072)
|
||||||
for exp, level, current, threshold in (
|
for exp, level, current, threshold in (
|
||||||
(0, 0, 0, 200), (199, 0, 199, 200), (200, 1, 0, 600),
|
(0, 0, 0, 200), (199, 0, 199, 200), (200, 1, 0, 400),
|
||||||
(599, 1, 399, 600), (600, 2, 0, 1200), (3420, 5, 420, 4200),
|
(599, 1, 399, 400), (600, 2, 0, 600), (3420, 5, 420, 1200),
|
||||||
):
|
):
|
||||||
data = snapshot(name="农场", point=12580, vip_point=36,
|
data = snapshot(name="农场", point=12580, vip_point=36,
|
||||||
experience=exp)
|
experience=exp)
|
||||||
@@ -173,7 +173,10 @@ class RenderTests(unittest.TestCase):
|
|||||||
original = self.renderer.assets.image(
|
original = self.renderer.assets.image(
|
||||||
'webgl/panel/ExpProgress.png', (172, 18),
|
'webgl/panel/ExpProgress.png', (172, 18),
|
||||||
)
|
)
|
||||||
for exp in (0, 100, 199):
|
for exp, current, required in (
|
||||||
|
(0, 0, 200), (100, 100, 200), (199, 199, 200),
|
||||||
|
(599, 399, 400), (1199, 599, 600),
|
||||||
|
):
|
||||||
scene = self.renderer.scene(
|
scene = self.renderer.scene(
|
||||||
self.who, snapshot(experience=exp), 0,
|
self.who, snapshot(experience=exp), 0,
|
||||||
)
|
)
|
||||||
@@ -185,7 +188,7 @@ class RenderTests(unittest.TestCase):
|
|||||||
self.assertEqual(len(calls), 6)
|
self.assertEqual(len(calls), 6)
|
||||||
else:
|
else:
|
||||||
self.assertEqual(len(calls), 7)
|
self.assertEqual(len(calls), 7)
|
||||||
filled = exp * 172 // 200
|
filled = current * 172 // required
|
||||||
self.assertEqual(calls[4].args[0].tobytes(),
|
self.assertEqual(calls[4].args[0].tobytes(),
|
||||||
original.crop((0, 0, filled, 18)).tobytes())
|
original.crop((0, 0, filled, 18)).tobytes())
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class TransactionTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
sale = await self.service.sell(self.identity, None, None, "sell")
|
sale = await self.service.sell(self.identity, None, None, "sell")
|
||||||
self.assertEqual(sale.data["proceeds"], 20)
|
self.assertEqual(sale.data["proceeds"], 20)
|
||||||
self.assertEqual(sale.data["account"]["point"], 510)
|
self.assertEqual(sale.data["account"]["point"], 510)
|
||||||
self.assertEqual(sale.data["account"]["experience"], 20)
|
self.assertEqual(sale.data["account"]["experience"], 22)
|
||||||
snapshot = await self.service.snapshot(self.identity)
|
snapshot = await self.service.snapshot(self.identity)
|
||||||
self.assertEqual(snapshot.inventory[0].seeds, 0)
|
self.assertEqual(snapshot.inventory[0].seeds, 0)
|
||||||
self.assertEqual(snapshot.inventory[0].crops, 0)
|
self.assertEqual(snapshot.inventory[0].crops, 0)
|
||||||
@@ -106,7 +106,7 @@ class TransactionTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
await self.mature_radish()
|
await self.mature_radish()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
(await self.service.snapshot(self.identity)).account.experience,
|
(await self.service.snapshot(self.identity)).account.experience,
|
||||||
120,
|
122,
|
||||||
)
|
)
|
||||||
await self.seed("balance-100", lambda repo: (
|
await self.seed("balance-100", lambda repo: (
|
||||||
repo.asset(Asset.POINT, -390) or {}
|
repo.asset(Asset.POINT, -390) or {}
|
||||||
@@ -185,7 +185,7 @@ class TransactionTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
snapshot = await self.service.snapshot(self.identity)
|
snapshot = await self.service.snapshot(self.identity)
|
||||||
self.assertTrue(snapshot.plots[0].withered)
|
self.assertTrue(snapshot.plots[0].withered)
|
||||||
self.assertEqual(snapshot.inventory[0].crops, 6)
|
self.assertEqual(snapshot.inventory[0].crops, 6)
|
||||||
self.assertEqual(snapshot.account.experience, 60)
|
self.assertEqual(snapshot.account.experience, 62)
|
||||||
with self.assertRaisesRegex(FarmError, "nothing_to_harvest"):
|
with self.assertRaisesRegex(FarmError, "nothing_to_harvest"):
|
||||||
await self.service.harvest(self.identity, "third")
|
await self.service.harvest(self.identity, "third")
|
||||||
|
|
||||||
@@ -338,7 +338,62 @@ class TransactionTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertEqual(sum(not isinstance(r, Exception) for r in results), 1)
|
self.assertEqual(sum(not isinstance(r, Exception) for r in results), 1)
|
||||||
snapshot = await self.service.snapshot(self.identity)
|
snapshot = await self.service.snapshot(self.identity)
|
||||||
self.assertEqual(snapshot.inventory[0].crops, 2)
|
self.assertEqual(snapshot.inventory[0].crops, 2)
|
||||||
self.assertEqual(snapshot.account.experience, 20)
|
self.assertEqual(snapshot.account.experience, 22)
|
||||||
|
|
||||||
|
async def test_sowing_awards_two_per_plot_and_replay_once(self):
|
||||||
|
await self.service.buy(self.identity, '萝卜', 3, 'buy')
|
||||||
|
first = await self.service.sow(self.identity, '萝卜', 2, 'sow-two')
|
||||||
|
self.assertEqual(first.data['experience'], 4)
|
||||||
|
self.assertEqual(first.data['account']['experience'], 4)
|
||||||
|
self.assertIn('本次经验:+4', receipt_text(first))
|
||||||
|
repeated = await asyncio.gather(*(
|
||||||
|
self.service.sow(self.identity, '萝卜', 2, 'sow-two')
|
||||||
|
for _ in range(4)
|
||||||
|
))
|
||||||
|
self.assertTrue(all(result == first for result in repeated))
|
||||||
|
await self.service.sow(self.identity, '萝卜', None, 'sow-rest')
|
||||||
|
self.assertEqual(
|
||||||
|
(await self.service.snapshot(self.identity)).account.experience, 6,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_eradication_rewards_each_plot_once_and_rolls_back(self):
|
||||||
|
await self.mature_radish(count=2)
|
||||||
|
before = await self.service.snapshot(self.identity)
|
||||||
|
ledger = await self.store.ledger(self.identity)
|
||||||
|
with patch.object(SQLiteRepository, 'asset',
|
||||||
|
side_effect=sqlite3.OperationalError('injected')):
|
||||||
|
with self.assertRaises(StorageError):
|
||||||
|
await self.service.eradicate(self.identity, 'clear')
|
||||||
|
self.assertEqual(await self.service.snapshot(self.identity), before)
|
||||||
|
self.assertEqual(await self.store.ledger(self.identity), ledger)
|
||||||
|
receipts = await asyncio.gather(*(
|
||||||
|
self.service.eradicate(self.identity, 'clear') for _ in range(3)
|
||||||
|
))
|
||||||
|
self.assertTrue(all(r == receipts[0] for r in receipts))
|
||||||
|
self.assertEqual(receipts[0].data['experience'], 4)
|
||||||
|
after = await self.service.snapshot(self.identity)
|
||||||
|
self.assertEqual(after.account.experience, before.account.experience+4)
|
||||||
|
self.assertTrue(all(p.plant is None for p in after.plots))
|
||||||
|
with self.assertRaisesRegex(FarmError, 'nothing_to_eradicate'):
|
||||||
|
await self.service.eradicate(self.identity, 'empty')
|
||||||
|
self.assertEqual(await self.service.snapshot(self.identity), after)
|
||||||
|
|
||||||
|
async def test_failed_sowing_rolls_back_experience_seeds_and_plots(self):
|
||||||
|
await self.service.buy(self.identity, '萝卜', 2, 'buy')
|
||||||
|
before = await self.service.snapshot(self.identity)
|
||||||
|
ledger = await self.store.ledger(self.identity)
|
||||||
|
with self.assertRaisesRegex(FarmError, 'insufficient_stock'):
|
||||||
|
await self.service.sow(self.identity, '萝卜', 3, 'too-many')
|
||||||
|
with patch.object(SQLiteRepository, 'asset',
|
||||||
|
side_effect=sqlite3.OperationalError('injected')):
|
||||||
|
with self.assertRaises(StorageError):
|
||||||
|
await self.service.sow(self.identity, '萝卜', 2, 'sow-fail')
|
||||||
|
self.assertEqual(await self.service.snapshot(self.identity), before)
|
||||||
|
self.assertEqual(await self.store.ledger(self.identity), ledger)
|
||||||
|
receipt = await self.service.sow(
|
||||||
|
self.identity, '萝卜', 2, 'sow-fail',
|
||||||
|
)
|
||||||
|
self.assertEqual(receipt.data['account']['experience'], 4)
|
||||||
|
|
||||||
async def test_restart_and_cancelled_request_can_replay_result(self):
|
async def test_restart_and_cancelled_request_can_replay_result(self):
|
||||||
entered = threading.Event()
|
entered = threading.Event()
|
||||||
|
|||||||
Reference in New Issue
Block a user