搬运了原版食材 死亡生物 手办 功能实现 小bug 1

This commit is contained in:
wenhe committed 2026-09-02 19:24:00 +08:00
1 parent 4f032234c0
commit 213c74cb55
12 files changed
+1304

No files matched your search

@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
"""
================================================================================
手办分类转换规则数据
================================================================================
只处理"手办"(xy_shouban) 分类下的转换:
1) 盲盒开盒: 空手右键 盲盒(未开, jewelrybox_0) → 盲盒(开过, jewelrybox_1)
+ 随机开出 1 个手办方块(掉落物形式)
2) 猫/狐狸手办循环: 空手右键 逐级切换
cat_0 → cat_1 → cat_2 → cat_3 → cat_0, fox 同理
其余赞助者手办(figurines ~ figurines_5)为静态手办, 无转换。
【盲盒开出范围】
只开"转换链第一个"的猫/狐狸手办(cat_0 / fox_0) + 赞助者手办, 共 8 种均等概率。
cat_1~3、fox_1~3 无法从盲盒开出, 只能通过循环转换逐级获得。
================================================================================
"""
# ============================================================
# 一、盲盒
# ============================================================
BLIND_BOX_SOURCE = "rtdd:jewelrybox_0" # 盲盒(未开)
BLIND_BOX_TARGET = "rtdd:jewelrybox_1" # 盲盒(开过)
# 盲盒开出内容: 只开链首的猫/狐狸 + 赞助者手办(均等概率)
BLIND_BOX_POOL = [
"rtdd:cat_0",
"rtdd:fox_0",
"rtdd:figurines",
"rtdd:figurines_1",
"rtdd:figurines_2",
"rtdd:figurines_3",
"rtdd:figurines_4",
"rtdd:figurines_5",
]
# 开出内容的显示名(用于开盒提示)
FIGURINE_NAMES = {
"rtdd:cat_0": "猫手办",
"rtdd:fox_0": "狐狸手办",
"rtdd:figurines": "赞助者手办_1",
"rtdd:figurines_1": "赞助者手办_2",
"rtdd:figurines_2": "赞助者手办_3",
"rtdd:figurines_3": "赞助者手办_4",
"rtdd:figurines_4": "赞助者手办_5",
"rtdd:figurines_5": "赞助者手办_6",
}
# ============================================================
# 二、猫/狐狸手办循环转换链(空手右键逐级切换, 末尾回到起点)
# ============================================================
CYCLE_CHAINS = {
"rtdd:cat_0": "rtdd:cat_1",
"rtdd:cat_1": "rtdd:cat_2",
"rtdd:cat_2": "rtdd:cat_3",
"rtdd:cat_3": "rtdd:cat_0",
"rtdd:fox_0": "rtdd:fox_1",
"rtdd:fox_1": "rtdd:fox_2",
"rtdd:fox_2": "rtdd:fox_3",
"rtdd:fox_3": "rtdd:fox_0",
}
@@ -0,0 +1,203 @@
# -*- coding: utf-8 -*-
"""
================================================================================
手办·右键转换引擎(服务端)
================================================================================
只处理"手办"(xy_shouban) 分类下的【右键切换型】转换:
盲盒开盒、猫/狐狸手办循环切换。
全部为即时右键转换, 无计时, 因此只有一个服务类, 不需要拆分 TimerService。
【事件监听在哪?】
本类【不】监听任何事件!
所有事件由 FeatureDispatcher(xiangyemod/service/FeatureDispatcher.py)统一监听:
- ServerBlockUseEvent → OnBlockUse → 按 blockOwner 路由到本类 handleBlockUse
本类只实现被分发器调用的接口方法, 不关心事件来源。
【本类怎么做(自上而下的方法系列)】
handleBlockUse ← 入口:分发器转发右键方块事件
├─ 空手确认 ← 盲盒/循环都要求空手
├─ checkCooldown← 连点冷却(防刷手办)
├─ openBlindBox ← 盲盒: 换为"开过"盒 + 随机掉落 1 个手办 + 提示
└─ replaceBlock ← 猫/狐狸: 逐级切到链上下一个形态
================================================================================
"""
import time
import random
import mod.server.extraServerApi as serverApi
# ---- 网易API基础对象 ----
ServerSystem = serverApi.GetServerSystemCls() # 服务端系统基类
comp = serverApi.GetEngineCompFactory() # 组件工厂
# ---- 继承 BlueFox 框架的系统基类(提供 CreateEngineItemEntity 等方法) ----
from ....BlueFoxTools.SpecialFunciton.BlueFoxSystem import BlueFoxServerSystem
from ....BlueFoxTools.ToolsConfig import LevelId
# ---- 只导入手办自己的数据库 ----
from ..ShoubanData import (
BLIND_BOX_SOURCE, BLIND_BOX_TARGET,
BLIND_BOX_POOL, FIGURINE_NAMES, CYCLE_CHAINS,
)
# ---- 统一分发器(本类把自己注册进去, 让分发器把右键事件转发过来) ----
from ...service.FeatureDispatcher import FeatureDispatcher
class ShoubanService(BlueFoxServerSystem):
"""
手办·右键转换引擎
【核心数据结构】
shoubanBlockSet = {方块名, ...} # 管辖方块(盲盒 + 猫/狐狸链上所有方块)
cooldown = {key: 时间戳} # 连点冷却表
"""
def __init__(self, namespace, systemName):
"""
初始化:构建管辖方块集合, 并注册到统一分发器。
"""
super(ShoubanService, self).__init__(namespace, systemName)
print "===== ShoubanService init ====="
# ---- 管辖方块: 盲盒(未开) + 猫/狐狸循环链上所有方块 ----
self.shoubanBlockSet = set([BLIND_BOX_SOURCE]) | set(CYCLE_CHAINS.keys())
# ---- 右键冷却表 ----
self.cooldown = {}
# ---- 注册到统一分发器 ----
FeatureDispatcher.register(self)
# ============================================================================
# 一、给 FeatureDispatcher 的接口
# ============================================================================
def getFeatureBlocks(self):
"""返回本功能管辖的所有方块名集合(右键路由用)"""
return self.shoubanBlockSet
def getItemUseOnRules(self):
"""返回"手持物品右键方块表面"的规则(手办分类没有, 返回空)"""
return {}
def handleBlockUse(self, args):
"""
由 FeatureDispatcher 转发的右键方块入口。
只有 blockName 在本功能的方块集合里才会被调用。
盲盒开盒 / 手办循环均要求【空手】右键:
- 盲盒: 空手右键 → 换为"开过"盒 + 随机掉落 1 个手办
- 猫/狐狸: 空手右键 → 逐级切到链上下一个形态
返回 True = 本功能处理了这个右键(分发器取消默认行为)
返回 False = 当前组合没有匹配规则(分发器不拦截)
"""
blockName = args.get("blockName")
playerId = args.get("playerId")
x, y, z = args.get("x"), args.get("y"), args.get("z")
pos = (x, y, z)
dim = comp.CreateDimension(playerId).GetEntityDimensionId()
key = (dim, x, y, z)
# ---- 空手确认(盲盒/循环都要求空手) ----
itemDict = self.getCarriedItem(playerId)
itemName = itemDict.get("newItemName") if itemDict else None
if itemName and itemName != "minecraft:air":
return False
# ---- 连点冷却 ----
if not self.checkCooldown(key, 0.5):
return True
# ---- 盲盒开盒 ----
if blockName == BLIND_BOX_SOURCE:
self.openBlindBox(playerId, pos, dim)
return True
# ---- 猫/狐狸手办循环切换 ----
if blockName in CYCLE_CHAINS:
self.replaceBlock(pos, dim, CYCLE_CHAINS[blockName], playerId)
return True
return False
# ============================================================================
# 二、盲盒开盒
# ============================================================================
def openBlindBox(self, playerId, pos, dim):
"""
盲盒开盒:换为"开过"盒, 随机掉落 1 个手办方块, 提示开出内容。
"""
# 随机开出一个手办
fig = random.choice(BLIND_BOX_POOL)
# 盒子变为"开过"
self.replaceBlock(pos, dim, BLIND_BOX_TARGET, playerId)
# 掉落手办
self.dropItems(dim, pos, [{"name": fig, "count": 1}])
# 提示开出内容
name = FIGURINE_NAMES.get(fig, "手办")
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a盲盒开出了" + name + "!")
# ============================================================================
# 三、工具方法:获取玩家主手物品
# ============================================================================
def getCarriedItem(self, playerId):
itemComp = comp.CreateItem(playerId)
return itemComp.GetPlayerItem(
serverApi.GetMinecraftEnum().ItemPosType.CARRIED, 0)
# ============================================================================
# 四、工具方法:替换方块
# ============================================================================
def replaceBlock(self, pos, dim, newBlockName, playerId=None):
"""
替换指定位置的方块。
playerId 可为 None, 用 LevelId 创建组件。
"""
creator = playerId if playerId is not None else LevelId
aux = 0
try:
oldBlock = comp.CreateBlockInfo(creator).GetBlockNew(pos, dim)
aux = oldBlock.get("aux", 0)
except Exception:
aux = 0
comp.CreateBlockInfo(creator).SetBlockNew(
pos,
blockDict={"name": newBlockName, "aux": aux},
oldBlockHandling=0,
dimensionId=dim)
# ============================================================================
# 五、工具方法:以掉落物形式生成物品(开出/取下等)
# ============================================================================
def dropItems(self, dim, pos, dropList):
"""
dropList 格式:[{"name": "xxx", "count": N}, ...]
在方块上方生成掉落物实体, 让它自然掉到地上。
"""
for d in dropList:
itemDict = {
"itemName": d["name"],
"count": d.get("count", 1),
"auxValue": d.get("aux", 0),
}
self.CreateEngineItemEntity(itemDict, dim, (pos[0], pos[1] + 0.5, pos[2]))
# ============================================================================
# 六、工具方法:冷却检查
# ============================================================================
def checkCooldown(self, key, cd=0.5):
"""
右键冷却:同一方块 cd 秒内只响应一次, 防止连点刷手办/刷提示。
"""
now = time.time()
last = self.cooldown.get(key, 0)
if now - last < cd:
return False
self.cooldown[key] = now
return True
@@ -0,0 +1,460 @@
# -*- coding: utf-8 -*-
"""
================================================================================
死亡生物方块转换规则数据
================================================================================
所有"死亡生物"(xy_deadmob) 分类下"右键方块→消耗物品→替换方块/掉落物品"的转换规则
都在这里。只被 DeadmobService / DeadmobTimerService 读取,FeatureDispatcher 通过
方块名集合路由到对应服务,不会去查其他功能的数据库。
【流程】
1) 挂肉钩挂放: 手持 死牛/死猪/死羊/原版鲑鱼/原版鳕鱼 右键 空钩(meathook)
→ 挂上对应尸体(消耗1个)。鱼可挂两条。
2) 取下退回: 切肉开始前, 空手右键 整只/整头 → 空钩 + 掉落回尸体(不可逆前可退)
3) 挂肉钩切肉: 手持 菜刀(cleaver) 右键 逐级切割(不可逆), 以掉落物形式获得对应部位肉
- 切肉每刀掉落数量: 常规肉 2~3 个; 四肢固定 4 个; 牛头骨固定 1 个
- 牛: 去头去皮掉牛头骨1+原版牛肉4~5 → 去四肢掉牛腿肉4 → 去肉掉牛肋2~3+牛里脊2~3+原版牛肉4~5 → 空钩掉碎肉2~3+原版牛肉4~5
- 猪: 去皮掉五花肉2~3+原版生猪排4~5 → 去四肢掉猪蹄4 → 去肉掉猪肋排2~3+原版生猪排4~5 → 空钩掉碎肉2~3+原版生猪排4~5
- 羊: 去皮掉羊小排2~3+原版生羊肉4~5 → 去四肢掉羊腿4 → 去肉掉羊里脊2~3+原版生羊肉4~5 → 空钩掉碎肉2~3+原版生羊肉4~5
4) 鱼晾干(计时): 鲑鱼/鳕鱼挂上后自动计时约300秒晾干(挂肉钩自动开始, autoStart=True)
- 鲑鱼: 挂一条→干一条; 挂两条→干两条
- 鳕鱼: 挂两条→干两条
5) 取鱼干: 空手右键 干鱼钩 → 以掉落物形式获得 鱼干(thedriedfish)
6) 死鱼/死鱿鱼切块(手持菜刀, 掉落物):
- 死鲑鱼: 共四刀, 每刀掉1份生鱼片(sashimi), 共4份 → 方块消失
- 死鳕鱼: 共三刀, 每刀掉1份生鱼片(sashimi), 共3份 → 方块消失
- 死鱿鱼: 一刀切须, 掉5~6个鱿鱼须(rawsquidwhiskersr) → 变为 死鱿鱼(切掉鱿鱼须)
【尸体生成机制】(玩家杀死对应生物生成尸体) 已在txt标注"暂时不搞", 本次不实现。
字段说明(与 AnimalData/BarbecueData 一致):
source: 被右键的方块ID(必填)
item: 玩家手里必须拿的物品, None=空手
target: 替换成什么方块, "minecraft:air"=消失
consume: 消耗物品数量, 0=不消耗(默认1)
dropItem: 转换时以掉落物形式生成的物品列表(优先于 giveItem)
格式: [{"name": "xxx", "count": N}, ...]
count 支持范围写法 (min,max), 服务端随机取整(如 (2,3)=2~3)
type: "switch"=右键切换, "timer"=计时自动转换
time: 计时型转换需要多少秒
tip: 转换成功时给玩家的提示(绿色§a开头)
doneTip: 计时到点完成时给启动者的提示
autoStart: 放置时是否自动开始计时, 默认True(鱼挂上自动晾干)
================================================================================
"""
DEADMOB_SWITCHES = [
# ============================================================
# 一、挂肉钩 - 挂放尸体(手持尸体右键空钩, 消耗1个)
# ============================================================
{
"source": "rtdd:meathook",
"item": "rtdd:dead_cow",
"target": "rtdd:cow_0",
"consume": 1,
"type": "switch",
"tip": "§a已把整只牛挂上肉钩",
"giveItem": None,
},
{
"source": "rtdd:meathook",
"item": "rtdd:dead_pig",
"target": "rtdd:pig_0",
"consume": 1,
"type": "switch",
"tip": "§a已把整只猪挂上肉钩",
"giveItem": None,
},
{
"source": "rtdd:meathook",
"item": "rtdd:dead_sheep",
"target": "rtdd:sheep",
"consume": 1,
"type": "switch",
"tip": "§a已把整只羊挂上肉钩",
"giveItem": None,
},
# 挂鲑鱼(原版salmon): 空钩→一条→两条
{
"source": "rtdd:meathook",
"item": "minecraft:salmon",
"target": "rtdd:hang_meat_hooks_1",
"consume": 1,
"type": "switch",
"tip": "§a已挂上一条鲑鱼,开始晾晒",
"giveItem": None,
},
{
"source": "rtdd:hang_meat_hooks_1",
"item": "minecraft:salmon",
"target": "rtdd:hang_meat_hooks_2",
"consume": 1,
"type": "switch",
"tip": "§a已再挂上一条鲑鱼",
"giveItem": None,
},
# 挂鳕鱼(原版cod): 空钩→一条→两条
{
"source": "rtdd:meathook",
"item": "minecraft:cod",
"target": "rtdd:meathookcod_1",
"consume": 1,
"type": "switch",
"tip": "§a已挂上一条鳕鱼,开始晾晒",
"giveItem": None,
},
{
"source": "rtdd:meathookcod_1",
"item": "minecraft:cod",
"target": "rtdd:meathookcod_2",
"consume": 1,
"type": "switch",
"tip": "§a已再挂上一条鳕鱼",
"giveItem": None,
},
# ============================================================
# 二、取下退回(未切割前, 空手右键 整只/整头 → 空钩 + 掉落回尸体)
# ============================================================
{
"source": "rtdd:cow_0",
"item": None,
"target": "rtdd:meathook",
"consume": 0,
"type": "switch",
"dropItem": [{"name": "rtdd:dead_cow", "count": 1}],
"tip": "§a取下了整只牛(尚未切割)",
"giveItem": None,
},
{
"source": "rtdd:pig_0",
"item": None,
"target": "rtdd:meathook",
"consume": 0,
"type": "switch",
"dropItem": [{"name": "rtdd:dead_pig", "count": 1}],
"tip": "§a取下了整只猪(尚未切割)",
"giveItem": None,
},
{
"source": "rtdd:sheep",
"item": None,
"target": "rtdd:meathook",
"consume": 0,
"type": "switch",
"dropItem": [{"name": "rtdd:dead_sheep", "count": 1}],
"tip": "§a取下了整只羊(尚未切割)",
"giveItem": None,
},
# ============================================================
# 三、挂肉钩切肉(手持 菜刀cleaver, 掉落物, 数量随机 2~3 / 四肢固定4)
# ============================================================
# ---- 牛(不可逆) ----
{
"source": "rtdd:cow_0",
"item": "rtdd:cleaver",
"target": "rtdd:cow_1",
"consume": 0,
"type": "switch",
"dropItem": [
{"name": "rtdd:cow_skull", "count": 1},
{"name": "minecraft:beef", "count": (4, 5)},
],
"tip": "§a砍下了牛头",
"giveItem": None,
},
{
"source": "rtdd:cow_1",
"item": "rtdd:cleaver",
"target": "rtdd:cow_2",
"consume": 0,
"type": "switch",
"dropItem": [{"name": "rtdd:legcow", "count": 4}],
"tip": "§a卸下了四肢牛腿肉",
"giveItem": None,
},
{
"source": "rtdd:cow_2",
"item": "rtdd:cleaver",
"target": "rtdd:cow_3",
"consume": 0,
"type": "switch",
"dropItem": [
{"name": "rtdd:costillavacaes", "count": (2, 3)},
{"name": "rtdd:beef_tenderloin", "count": (2, 3)},
{"name": "minecraft:beef", "count": (4, 5)},
],
"tip": "§a剔出了牛肋和牛里脊",
"giveItem": None,
},
{
"source": "rtdd:cow_3",
"item": "rtdd:cleaver",
"target": "rtdd:meathook",
"consume": 0,
"type": "switch",
"dropItem": [
{"name": "rtdd:crushedmeat", "count": (2, 3)},
{"name": "minecraft:beef", "count": (4, 5)},
],
"tip": "§a刮下碎肉,肉钩已清空",
"giveItem": None,
},
# ---- 猪(不可逆) ----
{
"source": "rtdd:pig_0",
"item": "rtdd:cleaver",
"target": "rtdd:pig_1",
"consume": 0,
"type": "switch",
"dropItem": [
{"name": "rtdd:porkloins", "count": (2, 3)},
{"name": "minecraft:porkchop", "count": (4, 5)},
],
"tip": "§a已给猪去皮",
"giveItem": None,
},
{
"source": "rtdd:pig_1",
"item": "rtdd:cleaver",
"target": "rtdd:pig_2",
"consume": 0,
"type": "switch",
"dropItem": [{"name": "rtdd:ham", "count": 4}],
"tip": "§a卸下了四肢猪蹄",
"giveItem": None,
},
{
"source": "rtdd:pig_2",
"item": "rtdd:cleaver",
"target": "rtdd:pig_3",
"consume": 0,
"type": "switch",
"dropItem": [
{"name": "rtdd:porkrib", "count": (2, 3)},
{"name": "minecraft:porkchop", "count": (4, 5)},
],
"tip": "§a剔出了猪肋排",
"giveItem": None,
},
{
"source": "rtdd:pig_3",
"item": "rtdd:cleaver",
"target": "rtdd:meathook",
"consume": 0,
"type": "switch",
"dropItem": [
{"name": "rtdd:crushedmeat", "count": (2, 3)},
{"name": "minecraft:porkchop", "count": (4, 5)},
],
"tip": "§a刮下碎肉,肉钩已清空",
"giveItem": None,
},
# ---- 羊(不可逆) ----
{
"source": "rtdd:sheep",
"item": "rtdd:cleaver",
"target": "rtdd:sheep_1",
"consume": 0,
"type": "switch",
"dropItem": [
{"name": "rtdd:sheepracks", "count": (2, 3)},
{"name": "minecraft:mutton", "count": (4, 5)},
],
"tip": "§a已给羊去皮",
"giveItem": None,
},
{
"source": "rtdd:sheep_1",
"item": "rtdd:cleaver",
"target": "rtdd:sheep_2",
"consume": 0,
"type": "switch",
"dropItem": [{"name": "rtdd:legsleep", "count": 4}],
"tip": "§a卸下了四肢羊腿",
"giveItem": None,
},
{
"source": "rtdd:sheep_2",
"item": "rtdd:cleaver",
"target": "rtdd:sheep_3",
"consume": 0,
"type": "switch",
"dropItem": [
{"name": "rtdd:sheeploins", "count": (2, 3)},
{"name": "minecraft:mutton", "count": (4, 5)},
],
"tip": "§a剔出了羊里脊",
"giveItem": None,
},
{
"source": "rtdd:sheep_3",
"item": "rtdd:cleaver",
"target": "rtdd:meathook",
"consume": 0,
"type": "switch",
"dropItem": [
{"name": "rtdd:crushedmeat", "count": (2, 3)},
{"name": "minecraft:mutton", "count": (4, 5)},
],
"tip": "§a刮下碎肉,肉钩已清空",
"giveItem": None,
},
# ============================================================
# 四、取鱼干(空手右键 干鱼钩 → 掉落 鱼干)
# ============================================================
{
"source": "rtdd:hang_meat_hooks_3",
"item": None,
"target": "rtdd:meathook",
"consume": 0,
"type": "switch",
"dropItem": [{"name": "rtdd:thedriedfish", "count": 1}],
"tip": "§a取下了1份鱼干",
"giveItem": None,
},
{
"source": "rtdd:hang_meat_hooks_4",
"item": None,
"target": "rtdd:hang_meat_hooks_3",
"consume": 0,
"type": "switch",
"dropItem": [{"name": "rtdd:thedriedfish", "count": 1}],
"tip": "§a取下了1份鱼干,还剩一条",
"giveItem": None,
},
{
"source": "rtdd:meathookcoddry",
"item": None,
"target": "rtdd:meathook",
"consume": 0,
"type": "switch",
"dropItem": [{"name": "rtdd:thedriedfish", "count": 1}],
"tip": "§a取下了1份鱼干",
"giveItem": None,
},
# ============================================================
# 五、死鱼/死鱿鱼切块(手持 菜刀cleaver, 掉落物)
# ============================================================
# 死鲑鱼: 四刀, 每刀1份生鱼片
{
"source": "rtdd:killfish_0",
"item": "rtdd:cleaver",
"target": "rtdd:killfish_1",
"consume": 0,
"type": "switch",
"dropItem": [{"name": "rtdd:sashimi", "count": 1}],
"tip": "§a切下1份生鱼片",
"giveItem": None,
},
{
"source": "rtdd:killfish_1",
"item": "rtdd:cleaver",
"target": "rtdd:killfish_2",
"consume": 0,
"type": "switch",
"dropItem": [{"name": "rtdd:sashimi", "count": 1}],
"tip": "§a切下1份生鱼片",
"giveItem": None,
},
{
"source": "rtdd:killfish_2",
"item": "rtdd:cleaver",
"target": "rtdd:killfish_3",
"consume": 0,
"type": "switch",
"dropItem": [{"name": "rtdd:sashimi", "count": 1}],
"tip": "§a切下1份生鱼片",
"giveItem": None,
},
{
"source": "rtdd:killfish_3",
"item": "rtdd:cleaver",
"target": "minecraft:air",
"consume": 0,
"type": "switch",
"dropItem": [{"name": "rtdd:sashimi", "count": 1}],
"tip": "§a切完最后一份生鱼片",
"giveItem": None,
},
# 死鳕鱼: 三刀, 每刀1份生鱼片
{
"source": "rtdd:killcod_0",
"item": "rtdd:cleaver",
"target": "rtdd:killcod_1",
"consume": 0,
"type": "switch",
"dropItem": [{"name": "rtdd:sashimi", "count": 1}],
"tip": "§a切下1份生鱼片",
"giveItem": None,
},
{
"source": "rtdd:killcod_1",
"item": "rtdd:cleaver",
"target": "rtdd:killcod_2",
"consume": 0,
"type": "switch",
"dropItem": [{"name": "rtdd:sashimi", "count": 1}],
"tip": "§a切下1份生鱼片",
"giveItem": None,
},
{
"source": "rtdd:killcod_2",
"item": "rtdd:cleaver",
"target": "minecraft:air",
"consume": 0,
"type": "switch",
"dropItem": [{"name": "rtdd:sashimi", "count": 1}],
"tip": "§a切完最后一份生鱼片",
"giveItem": None,
},
# 死鱿鱼: 一刀切须
{
"source": "rtdd:deadsquida",
"item": "rtdd:cleaver",
"target": "rtdd:dead_squid_1",
"consume": 0,
"type": "switch",
"dropItem": [{"name": "rtdd:rawsquidwhiskersr", "count": (5, 6)}],
"tip": "§a切下了鱿鱼须",
"giveItem": None,
},
# ============================================================
# 六、鱼晾干(计时自动转换, 挂上即自动开始 autoStart=True)
# ============================================================
{
"source": "rtdd:hang_meat_hooks_1",
"item": None,
"target": "rtdd:hang_meat_hooks_3",
"consume": 0,
"type": "timer",
"time": 300,
"doneTip": "§e鲑鱼晾干了!",
"giveItem": None,
},
{
"source": "rtdd:hang_meat_hooks_2",
"item": None,
"target": "rtdd:hang_meat_hooks_4",
"consume": 0,
"type": "timer",
"time": 300,
"doneTip": "§e鲑鱼晾干了!",
"giveItem": None,
},
{
"source": "rtdd:meathookcod_2",
"item": None,
"target": "rtdd:meathookcoddry",
"consume": 0,
"type": "timer",
"time": 300,
"doneTip": "§e鳕鱼晾干了!",
"giveItem": None,
},
]
@@ -0,0 +1,270 @@
# -*- coding: utf-8 -*-
"""
================================================================================
死亡生物·右键转换引擎(服务端)
================================================================================
只处理"死亡生物"(xy_deadmob) 分类下的【右键切换型】转换:
挂肉钩挂放尸体、取下退回、菜刀切肉(牛/猪/羊)、取鱼干、死鱼/死鱿鱼切块。
【与计时转换解耦】
同分类的"鱼晾干"(计时自动转换)已拆到独立类:DeadmobTimerService。
本类只负责右键事件,两者通过 FeatureDispatcher 协作:
- 挂上鱼后(变成计时源方块)由 DeadmobTimerService 的 tick 自动开始晾干
(autoStart=True,无需右键激活,因此本类不需要转发 startTimer)
- 计时到点转换完全由 DeadmobTimerService 负责
【事件监听在哪?】
本类【不】监听任何事件!
所有事件由 FeatureDispatcher(xiangyemod/service/FeatureDispatcher.py)统一监听:
- ServerBlockUseEvent → OnBlockUse → 按 blockOwner 路由到本类 handleBlockUse
本类只实现被分发器调用的接口方法,不关心事件来源。
【本类怎么做(自上而下的方法系列)】
handleBlockUse ← 入口:分发器转发右键方块事件
├─ findRecipe ← 匹配 (方块名, 物品名) 规则;空手规则需确认真正空手
├─ checkCooldown ← 连点冷却(防刷提示)
└─ handleSwitch ← 切换型规则具体执行(挂放/取回/切肉/取鱼干/切块)
├─ consumeItem ← 消耗物品(挂放尸体消耗1个)
├─ replaceBlock ← 替换方块
└─ dropItems ← 掉落物形式给物品(切肉/取回/取鱼干),支持随机数量
================================================================================
"""
import time
import random
import mod.server.extraServerApi as serverApi
# ---- 网易API基础对象 ----
ServerSystem = serverApi.GetServerSystemCls() # 服务端系统基类
comp = serverApi.GetEngineCompFactory() # 组件工厂
# ---- 继承 BlueFox 框架的系统基类(提供 CreateEngineItemEntity 等方法) ----
from ....BlueFoxTools.SpecialFunciton.BlueFoxSystem import BlueFoxServerSystem
from ....BlueFoxTools.ToolsConfig import LevelId
# ---- 只导入死亡生物自己的数据库 ----
from ..DeadmobData import DEADMOB_SWITCHES
# ---- 统一分发器 ----
from ...service.FeatureDispatcher import FeatureDispatcher
class DeadmobService(BlueFoxServerSystem):
"""
死亡生物·右键转换引擎
【核心数据结构】
deadmobRecipes = {(方块名, 物品名): 规则dict} # O(1) 查找,只收 switch 规则
deadmobBlockSet = {方块名, ...} # O(1) 注册用(所有右键源方块)
cooldown = {key: 时间戳} # 连点冷却表
"""
def __init__(self, namespace, systemName):
"""
初始化:构建规则字典,并注册到统一分发器(让分发器把右键事件转发过来)。
"""
super(DeadmobService, self).__init__(namespace, systemName)
print "===== DeadmobService init ====="
# ---- 构建右键规则字典(O(1) 查找),只收 switch 规则 ----
# 计时规则(鱼晾干)由 DeadmobTimerService 处理,本类不接管
self.deadmobRecipes = {}
self.deadmobBlockSet = set()
for sw in DEADMOB_SWITCHES:
if sw.get("type") == "timer":
continue
key = (sw["source"], sw.get("item"))
self.deadmobRecipes[key] = sw
self.deadmobBlockSet.add(sw["source"])
# ---- 右键冷却表 ----
self.cooldown = {}
# ---- 注册到统一分发器 ----
FeatureDispatcher.register(self)
# ============================================================================
# 一、给 FeatureDispatcher 的接口
# ============================================================================
def getFeatureBlocks(self):
"""返回本功能管辖的所有方块名集合(右键路由用)"""
return self.deadmobBlockSet
def getItemUseOnRules(self):
"""
返回"手持物品右键方块表面"的规则(当前返回空)。
挂鱼走 ServerBlockUseEvent(右键方块本身)即可,不需要 ItemUseOn:
日志验证手持原版鲑鱼/鳕鱼右键肉钩会正常触发 ServerBlockUseEvent 并转换,
若同时注册 ItemUseOn 会重复转换(多消耗一条鱼)且 ServerItemUseOnEvent 的
玩家键是 entityId 不是 playerId,易出错,故不再使用该路径。
"""
return {}
def handleBlockUse(self, args):
"""
由 FeatureDispatcher 转发的右键方块入口。
只有 blockName 在本功能的方块集合里才会被调用。
返回 True = 本功能处理了这个右键(分发器会取消默认行为)
返回 False = 当前组合没有匹配规则(分发器不拦截,交还默认行为)
"""
blockName = args.get("blockName")
playerId = args.get("playerId")
x, y, z = args.get("x"), args.get("y"), args.get("z")
pos = (x, y, z)
dim = comp.CreateDimension(playerId).GetEntityDimensionId()
key = (dim, x, y, z)
itemDict = self.getCarriedItem(playerId)
recipe = self.findRecipe(blockName, itemDict)
print "[Deadmob-Use]", blockName, "hand:", (itemDict.get("newItemName") if itemDict else None), "hit:", (recipe.get("source") if recipe else "NONE")
if not recipe:
return False
# 冷却检查(防止连点刷掉落物/刷提示)
if not self.checkCooldown(key, recipe.get("switchCd", 0.5)):
return True
# 切换型规则:就地执行
self.handleSwitch(recipe, playerId, pos, dim, itemDict)
return True
# ============================================================================
# 二、规则匹配(O(1) 字典查找)
# ============================================================================
def findRecipe(self, blockName, itemDict):
"""
在死亡生物字典中查找匹配规则:
1. 先精确匹配 (方块名, 物品名) —— 手持菜刀切肉、手持尸体挂放、手持原版鱼挂放
2. 没找到再匹配 (方块名, None) —— 空手触发的规则(取下退回/取鱼干)
注意:同一个源方块可能同时有"手持菜刀"和"空手"两类规则(如 cow_0 切肉/取回),
因此 fallback 到空手规则前必须确认玩家【确实空手】,避免手持其他物品误触发。
"""
itemName = itemDict.get("newItemName") if itemDict else None
recipe = self.deadmobRecipes.get((blockName, itemName))
if recipe:
return recipe
# 空手规则:只有确认空手(无物品或手持空气)才 fallback
if not itemName or itemName == "minecraft:air":
return self.deadmobRecipes.get((blockName, None))
return None
# ============================================================================
# 三、切换型规则处理
# ============================================================================
def handleSwitch(self, recipe, playerId, pos, dim, itemDict):
"""
处理"右键直接转换"类规则(统一流程:掉落物 → 消耗 → 替换 → 提示)。
- 切肉(手持菜刀, consume=0): 掉落物 + 替换, 不消耗
- 挂放(手持尸体/原版鱼, consume=1): 无掉落物, 消耗 + 替换
- 取回/取鱼干(空手, consume=0): 掉落物 + 替换
"""
# ---- 掉落物优先(切肉/取回/取鱼干都生成掉落物) ----
if recipe.get("dropItem"):
self.dropItems(dim, pos, recipe["dropItem"])
# ---- 消耗物品(挂放尸体/挂鱼) ----
if recipe.get("item") is not None and recipe.get("consume", 1) > 0:
self.consumeItem(playerId, itemDict, recipe.get("consume", 1))
# ---- 替换方块 ----
if recipe.get("target"):
self.replaceBlock(pos, dim, recipe["target"], playerId)
# ---- 转换成功提示 ----
if recipe.get("tip"):
comp.CreateMsg(playerId).NotifyOneMessage(playerId, recipe["tip"])
# ============================================================================
# 四、工具方法:获取玩家主手物品
# ============================================================================
def getCarriedItem(self, playerId):
itemComp = comp.CreateItem(playerId)
return itemComp.GetPlayerItem(
serverApi.GetMinecraftEnum().ItemPosType.CARRIED, 0)
# ============================================================================
# 五、工具方法:消耗物品
# ============================================================================
def consumeItem(self, playerId, itemDict, amount=1):
if itemDict is None:
return
count = itemDict.get("count", 0)
if count <= amount:
comp.CreateItem(playerId).SetEntityItem(
serverApi.GetMinecraftEnum().ItemPosType.CARRIED,
{"itemName": "minecraft:air", "count": 0, "auxValue": 0})
else:
itemDict["count"] = count - amount
comp.CreateItem(playerId).SetEntityItem(
serverApi.GetMinecraftEnum().ItemPosType.CARRIED,
{"itemName": itemDict["newItemName"],
"count": itemDict["count"],
"auxValue": itemDict.get("newAuxValue", 0)})
# ============================================================================
# 六、工具方法:替换方块
# ============================================================================
def replaceBlock(self, pos, dim, newBlockName, playerId=None):
"""
替换指定位置的方块。
newBlockName="minecraft:air" 表示清除该方块。
playerId 可为 None,用 LevelId 创建组件。
"""
creator = playerId if playerId is not None else LevelId
# 清空方块时必须用 aux=0:空气没有朝向变体,
# 若继承死鱼(face_directional)的朝向 aux,SetBlockNew 无法把方块清掉
aux = 0
if newBlockName != "minecraft:air":
try:
oldBlock = comp.CreateBlockInfo(creator).GetBlockNew(pos, dim)
aux = oldBlock.get("aux", 0)
except Exception:
aux = 0
comp.CreateBlockInfo(creator).SetBlockNew(
pos,
blockDict={"name": newBlockName, "aux": aux},
oldBlockHandling=0,
dimensionId=dim)
# ============================================================================
# 七、工具方法:以掉落物形式生成物品(切肉/取下/取鱼干)
# ============================================================================
def dropItems(self, dim, pos, dropList):
"""
dropList 格式:[{"name": "xxx", "count": N 或 (min,max)}, ...]
count 为 tuple/list 时表示范围,随机取整(如 (4,5)=4~5、(2,3)=2~3)。
在方块上方生成掉落物实体,让它自然掉到地上。
"""
for d in dropList:
cnt = d.get("count", 1)
if isinstance(cnt, (tuple, list)):
cnt = random.randint(cnt[0], cnt[1])
itemDict = {
"itemName": d["name"],
"count": cnt,
"auxValue": d.get("aux", 0),
}
self.CreateEngineItemEntity(itemDict, dim, (pos[0], pos[1] + 0.5, pos[2]))
# ============================================================================
# 八、工具方法:冷却检查
# ============================================================================
def checkCooldown(self, key, cd=0.5):
"""
右键冷却:同一方块 cd 秒内只响应一次,防止连点刷掉落物/刷提示。
"""
now = time.time()
last = self.cooldown.get(key, 0)
if now - last < cd:
return False
self.cooldown[key] = now
return True
@@ -0,0 +1,256 @@
# -*- coding: utf-8 -*-
"""
================================================================================
死亡生物·计时转换引擎(服务端)
================================================================================
只处理"死亡生物"(xy_deadmob) 分类下的【计时自动转换】:
鲑鱼/鳕鱼挂上肉钩后自动晾干(约300秒)→ 变为干鱼钩。
【与右键转换解耦】
同分类的"右键切换"(挂放/切肉/取回/取鱼干/死鱼切块)在 DeadmobService。
本类只负责计时,两者通过 FeatureDispatcher 协作:
- 挂上鱼后,方块变成计时源(如 hang_meat_hooks_1),它的 tick 由 FeatureDispatcher
按 timerOwner 路由到本类的 handleBlockEntityTick
- autoStart=True:挂上即自动开始晾干,无需右键激活,因此 DeadmobService
不需要转发 startTimer(本类 startTimer 仅为接口完整性保留,不会被调用)
【晾干规则】
- autoStart=True:计时源方块放置/替换后,第一次 tick 自动补写计时数据
- 晾干不需要任何环境条件(无 below 检查)
- 完成后清除计时数据,若启动者在线则发完成提示(自动晾干通常无启动者)
【计时怎么做到"退出游戏继续计时"】
不做倒计时,只记"开始时刻":
- 首次 tick 把 开始时间戳(rtdd_tstart)+总时长(rtdd_tdur) 写进方块实体数据
- 每帧 tick 判断 当前时间-开始时刻 >= 总时长 → 到点转换
- 时间戳是绝对时间(time.time()),服务器重启/玩家离线真实时间仍在流逝,
回来后 tick 直接算差值,剩余时间自然准确
【事件监听在哪?】
本类【不】监听任何事件!
ServerBlockEntityTickEvent 由 FeatureDispatcher 统一监听,再按 timerOwner
路由到本类的 handleBlockEntityTick。
【本类怎么做(自上而下的方法系列)】
handleBlockEntityTick ← 入口:分发器转发 tick 事件
└─ _tickProcess ← 判断是否到点 / autoStart=True 自动补写计时
└─ _finishTimer ← 到点执行转换(确认方块仍是计时源,否则清数据)
└─ _notifyIfOnline ← 完成提示(启动者在线才发)
_writeTimerData ← 写 开始时刻+时长 到方块实体
_clearTimerData ← 清理方块上的计时数据
================================================================================
"""
import time
import mod.server.extraServerApi as serverApi
# ---- 网易API基础对象 ----
ServerSystem = serverApi.GetServerSystemCls() # 服务端系统基类
comp = serverApi.GetEngineCompFactory() # 组件工厂
# ---- 继承 BlueFox 框架的系统基类 ----
from ....BlueFoxTools.SpecialFunciton.BlueFoxSystem import BlueFoxServerSystem
from ....BlueFoxTools.ToolsConfig import LevelId
# ---- 方块实体数据组件(计时持久化:把开始时间戳+时长写进方块,重启后继续计时) ----
blockEntityDataComp = comp.CreateBlockEntityData(LevelId)
# ---- 只导入死亡生物自己的数据库 ----
from ..DeadmobData import DEADMOB_SWITCHES
# ---- 统一分发器(本类把自己注册进计时路由表,让分发器把 tick 事件转发过来) ----
from ...service.FeatureDispatcher import FeatureDispatcher
class DeadmobTimerService(BlueFoxServerSystem):
"""
死亡生物·计时转换引擎(鱼晾干)
【核心数据结构】
deadmobTimers = {计时源方块名: 规则dict} # 只收 type=="timer" 的规则
"""
def __init__(self, namespace, systemName):
"""
初始化:收集计时型规则,并注册到统一分发器的计时路由表。
"""
super(DeadmobTimerService, self).__init__(namespace, systemName)
print "===== DeadmobTimerService init ====="
# ---- 只收集计时型规则(source → 规则) ----
self.deadmobTimers = {}
for sw in DEADMOB_SWITCHES:
if sw.get("type") == "timer":
self.deadmobTimers[sw["source"]] = sw
# ---- 注册到分发器的计时路由表(tick 事件按方块名路由到这里) ----
FeatureDispatcher.registerTimer(self)
# ============================================================================
# 一、给 FeatureDispatcher 的接口
# ============================================================================
def getTimerBlocks(self):
"""
返回本功能的所有"计时源方块"集合。
FeatureDispatcher 靠它构建 方块名→本实例 的计时路由表(timerOwner)。
"""
return set(self.deadmobTimers.keys())
def handleBlockEntityTick(self, args):
"""
由 FeatureDispatcher 转发的计时方块 tick 入口。
只有 blockName 在计时路由表里才会被调用。
"""
dim = args.get("dimension")
pos = (args.get("posX"), args.get("posY"), args.get("posZ"))
blockName = args.get("blockName", "")
self._tickProcess(dim, pos, blockName)
def startTimer(self, dim, pos, blockName, playerId=None):
"""
接口完整性保留。鱼晾干为 autoStart=True(挂上自动开始),
本功能右键服务不会转发 startTimer,此方法通常不会被调用。
"""
recipe = self.deadmobTimers.get(blockName)
if not recipe:
return
self._writeTimerData(dim, pos, recipe, playerId)
# ============================================================================
# 二、tick 检查与到点转换
# ============================================================================
def _tickProcess(self, dim, pos, blockName):
"""
tick 主逻辑:
1) 按事件自带的当前方块名判断是否计时源
2) 是计时源但没有计时数据 → autoStart=True 自动补写计时(挂上即自动晾干)
3) 有计时数据 → 到点执行转换(重启后时间戳照常生效)
方块被拆掉后 tick 停止、数据随方块消失,不会在原位置继续计时转换。
注意:GetBlockEntityData 返回的对象不支持 .get(),这里统一用 [] 读取。
"""
try:
recipe = self.deadmobTimers.get(blockName)
if not recipe:
return # 当前方块不是计时源(tick 只对计时源方块开启)
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
if blockData is None:
return
# 读取计时标识;没有或与当前方块不符
try:
tid = blockData["rtdd_tid"]
except Exception:
tid = ""
if tid != blockName:
# 鱼晾干 autoStart=True:挂上后自动补写计时
if recipe.get("autoStart", True):
self._writeTimerData(dim, pos, recipe, None)
print "[Deadmob-Tick-Start]", blockName, pos
return
tstart = blockData["rtdd_tstart"]
tdur = blockData["rtdd_tdur"]
if time.time() - tstart >= tdur:
self._finishTimer(dim, pos, recipe, blockData)
except Exception:
pass
def _finishTimer(self, dim, pos, recipe, data):
"""
计时到点:确认方块仍是计时源,才执行转换(晾干无环境条件检查)。
- 方块已被玩家改变 → 清掉过期计时
- 转换后清除计时数据,若启动者在线发完成提示
"""
try:
cur = comp.CreateBlockInfo(LevelId).GetBlockNew(pos, dim)
if cur and cur.get("name") != recipe["source"]:
# 方块已被玩家改变 → 清掉过期计时
self._clearTimerData(dim, pos)
return
# 替换方块
self.replaceBlock(pos, dim, recipe["target"], None)
print "[Deadmob-Tick-Finish]", recipe["source"], "->", recipe["target"], pos
nextRecipe = self.deadmobTimers.get(recipe["target"])
if nextRecipe:
# 下一级计时(换新的开始时间戳)
self._writeTimerData(dim, pos, nextRecipe, data["rtdd_tp"])
else:
# 整条链结束:清数据 + 完成提示
self._clearTimerData(dim, pos)
doneTip = recipe.get("doneTip")
if doneTip:
self._notifyIfOnline(data["rtdd_tp"], doneTip)
except Exception:
pass
# ============================================================================
# 三、计时数据读写(存在方块实体数据里,重启不丢)
# ============================================================================
def _writeTimerData(self, dim, pos, recipe, playerId=None):
"""把 计时标识+开始时刻+时长+启动者 写进方块实体数据"""
try:
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
if blockData is None:
return
now = time.time()
blockData["rtdd_tid"] = recipe["source"]
blockData["rtdd_tstart"] = now
blockData["rtdd_tdur"] = recipe.get("time", 300)
blockData["rtdd_tp"] = playerId if playerId else ""
except Exception:
pass
def _clearTimerData(self, dim, pos):
"""清除方块上的计时数据"""
try:
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
if blockData is None:
return
blockData["rtdd_tid"] = ""
blockData["rtdd_tstart"] = 0
blockData["rtdd_tdur"] = 0
blockData["rtdd_tp"] = ""
except Exception:
pass
# ============================================================================
# 四、工具方法:完成提示
# ============================================================================
def _notifyIfOnline(self, playerId, tip):
"""若启动者在线,发送完成提示(离线则跳过)"""
if not tip or not playerId:
return
try:
ids = comp.CreateGame(LevelId).GetAllPlayerIds()
if playerId in ids:
comp.CreateMsg(playerId).NotifyOneMessage(playerId, tip)
except Exception:
pass
# ============================================================================
# 五、工具方法:替换方块
# ============================================================================
def replaceBlock(self, pos, dim, newBlockName, playerId=None):
"""
替换指定位置的方块。
newBlockName="minecraft:air" 表示清除该方块。
playerId 可为 None(计时回调时没有玩家),用 LevelId 创建组件。
"""
creator = playerId if playerId is not None else LevelId
aux = 0
try:
oldBlock = comp.CreateBlockInfo(creator).GetBlockNew(pos, dim)
aux = oldBlock.get("aux", 0)
except Exception:
aux = 0
comp.CreateBlockInfo(creator).SetBlockNew(
pos,
blockDict={"name": newBlockName, "aux": aux},
oldBlockHandling=0,
dimensionId=dim)
@@ -0,0 +1,18 @@
{
"format_version": "1.10",
"minecraft:item": {
"description": {
"identifier": "rtdd:grape",
"category": "xy_ingredients"
},
"components": {
"minecraft:max_stack_size": 64,
"minecraft:use_animation": "eat",
"minecraft:use_duration": 1.6,
"minecraft:food": {
"nutrition": 2,
"saturation_modifier": "low"
}
}
}
}
@@ -0,0 +1,18 @@
{
"format_version": "1.10",
"minecraft:item": {
"description": {
"identifier": "rtdd:orange",
"category": "xy_ingredients"
},
"components": {
"minecraft:max_stack_size": 64,
"minecraft:use_animation": "eat",
"minecraft:use_duration": 1.6,
"minecraft:food": {
"nutrition": 2,
"saturation_modifier": "low"
}
}
}
}
@@ -0,0 +1,18 @@
{
"format_version": "1.10",
"minecraft:item": {
"description": {
"identifier": "rtdd:peach",
"category": "xy_ingredients"
},
"components": {
"minecraft:max_stack_size": 64,
"minecraft:use_animation": "eat",
"minecraft:use_duration": 1.6,
"minecraft:food": {
"nutrition": 2,
"saturation_modifier": "low"
}
}
}
}