黄豆 饮料 冰棍 实现

This commit is contained in:
wenhe committed 2026-09-04 11:59:53 +08:00
1 parent 38f2ff2b33
commit f89b95fb99
23 files changed
+1772 -87

No files matched your search

@@ -10,6 +10,8 @@ from xiangyemod.shouban.service.ShoubanService import ShoubanService
from xiangyemod.shichangmaoyi.service.MarketService import MarketService from xiangyemod.shichangmaoyi.service.MarketService import MarketService
from xiangyemod.yuanbanfangzhi.service.YuanbanService import YuanbanService from xiangyemod.yuanbanfangzhi.service.YuanbanService import YuanbanService
from xiangyemod.jiaju.service.FurnitureService import FurnitureService from xiangyemod.jiaju.service.FurnitureService import FurnitureService
from xiangyemod.yinliao.service.YinliaoService import YinliaoService
from xiangyemod.yinliao.service.YinliaoTimerService import YinliaoTimerService
if not serverApi.IsInServer(): if not serverApi.IsInServer():
pass pass
@@ -111,21 +111,23 @@ class AnimalTimerService(BlueFoxServerSystem):
启动计时:把 计时标识+开始时间戳+时长 写入方块实体数据。 启动计时:把 计时标识+开始时间戳+时长 写入方块实体数据。
- 已处于本规则计时中的方块不会重复重置时间(保持原剩余时间) - 已处于本规则计时中的方块不会重复重置时间(保持原剩余时间)
- 动物计时 autoStart 默认 True:放置/填充后自动开始(tick 也会自动补写) - 动物计时 autoStart 默认 True:放置/填充后自动开始(tick 也会自动补写)
返回 True=找到规则并处理(含已计时中);False=不属于本计时功能
""" """
recipe = self.animalTimers.get(blockName) recipe = self.animalTimers.get(blockName)
if not recipe: if not recipe:
return return False
# 去重:如果该方块已经在本规则计时中,不重置(保持原剩余时间) # 去重:如果该方块已经在本规则计时中,不重置(保持原剩余时间)
try: try:
data = blockEntityDataComp.GetBlockEntityData(dim, pos) data = blockEntityDataComp.GetBlockEntityData(dim, pos)
if data and data["rtdd_tid"] == blockName: if data and data["rtdd_tid"] == blockName:
return return True
except Exception: except Exception:
pass pass
self._writeTimerData(dim, pos, recipe, playerId) self._writeTimerData(dim, pos, recipe, playerId)
# 计时开始提示 # 计时开始提示
if playerId and recipe.get("startTip"): if playerId and recipe.get("startTip"):
comp.CreateMsg(playerId).NotifyOneMessage(playerId, recipe["startTip"]) comp.CreateMsg(playerId).NotifyOneMessage(playerId, recipe["startTip"])
return True
# ============================================================================ # ============================================================================
# 三、tick 检查与到点转换 # 三、tick 检查与到点转换
@@ -0,0 +1,764 @@
# -*- coding: utf-8 -*-
"""
================================================================================
工具方块切换引擎(服务端)
================================================================================
这个文件负责所有"普通工具方块"的右键转换逻辑,和做菜(CookingService)彻底分开。
【一句话概括】
玩家右键工具方块(箭桶、挂肉钩、水槽、盘子架、药水架、书柜……) →
查 BlockSwitchData 有没有匹配的规则 → 消耗物品 → 替换方块 → 保存记录
【为什么单独拆一个类】
原来做菜和工具切换都在 CookingService 里,右键时每次都要同时碰到两套数据,
内存和逻辑都搅在一起。现在拆成两个系统:
- CookingService → 只处理做菜配方(RECIPES)
- BlockSwitchService → 只处理工具方块切换(BLOCK_SWITCHES)
两个系统都监听同一个 ServerBlockUseEvent,
但各自在事件第一行用 switchBlockSet/cookBlockSet(O(1) 的 set)按方块名拦截:
不是自己的方块直接 return,互不干扰,各自只查自己那一份数据。
【事件监听】
- ServerBlockUseEvent → 玩家右键方块(工具切换主入口)
- ServerItemTryUseEvent → 玩家尝试使用物品(拦截喝药水等默认行为)
【保存/持久化】
箭桶、药水架、书柜这类有 record 记录的方块,把放进来的物品记录在
方块实体数据里(字段 rtdd_swt/rtdd_swc/rtdd_swi/rtdd_sws),
这样玩家退出游戏再重进,loadSwitchRecord 还能从方块里把物品读回来。
================================================================================
"""
import time
import mod.server.extraServerApi as serverApi
# ---- 网易API基础对象 ----
ServerSystem = serverApi.GetServerSystemCls() # 服务端系统基类
comp = serverApi.GetEngineCompFactory() # 组件工厂,用来创建各种功能组件
# ---- 继承 BlueFox 框架的系统基类 ----
# BlueFoxServerSystem 会自动处理 LoadMod / UnloadMod 等生命周期,
# 不需要手动写,继承后框架自动发现并注册(ModLoader 的 GetLastSubclass)
from ...BlueFoxTools.SpecialFunciton.BlueFoxSystem import BlueFoxServerSystem
from ...BlueFoxTools.ToolsConfig import LevelId
# ---- 方块实体数据组件 ----
# GetBlockEntityData 返回的 BlockEntityData 对象支持 [] 操作,直接赋值即可持久化。
blockEntityDataComp = comp.CreateBlockEntityData(LevelId)
# ---- 游戏组件(用于 AddTimer 延迟写入 block entity) ----#
gameComp = comp.CreateGame(LevelId)
# ---- 导入工具方块切换规则 ----
# BLOCK_SWITCHES 是一个列表,每条规则是一个 dict,格式见 BlockSwitchData.py 顶部注释
from ..BlockSwitchData import BLOCK_SWITCHES
# ---- 已由 DeadmobService/DeadmobTimerService 接管的死亡生物方块 ----
# 这些方块属于"死亡生物"(xy_deadmob) 分类,由新系统处理(挂肉钩切肉/鱼晾干/死鱼切块),
# 工具切换系统必须跳过,否则右键会触发旧规则(含逆向退回),和新系统双重响应产生冲突。
_DEADMOB_OWNED = set([
"rtdd:meathook",
"rtdd:cow_0", "rtdd:cow_1", "rtdd:cow_2", "rtdd:cow_3",
"rtdd:pig_0", "rtdd:pig_1", "rtdd:pig_2", "rtdd:pig_3",
"rtdd:sheep", "rtdd:sheep_1", "rtdd:sheep_2", "rtdd:sheep_3",
"rtdd:hang_meat_hooks_1", "rtdd:hang_meat_hooks_2",
"rtdd:hang_meat_hooks_3", "rtdd:hang_meat_hooks_4",
"rtdd:meathookcod_1", "rtdd:meathookcod_2", "rtdd:meathookcoddry",
"rtdd:killfish_0", "rtdd:killfish_1", "rtdd:killfish_2", "rtdd:killfish_3",
"rtdd:killcod_0", "rtdd:killcod_1", "rtdd:killcod_2",
"rtdd:deadsquida", "rtdd:dead_squid_1",
# 旧版挂鱼方块(minecraft:fish 挂肉钩),已被新挂鱼(鲑鱼/鳕鱼)取代
"rtdd:fish_1", "rtdd:fish_2",
])
# ================================================================================
# 工具函数:读取物品 userData 里的嵌套字段
# ================================================================================
def getUserDataField(itemDict, *keys):
"""
从物品 dict 的 userData 里一层层往下读。
中间某一层不存在会自动创建空 dict,不会报错。
"""
itemDict['userData'] = itemDict.get('userData', {})
if itemDict.get('userData') is None:
itemDict['userData'] = {}
node = itemDict['userData']
for key in keys:
node[key] = node.get(key, {})
node = node[key]
return node
# ================================================================================
# 工具方块切换引擎主类
# ================================================================================
class BlockSwitchService(BlueFoxServerSystem):
"""
通用工具方块切换引擎
【核心数据结构】
switchRecord = {
(维度ID, x, y, z): { # 用方块坐标当 key,每个方块独立
"count": 整数, # recordType="count" 时记录数量(如箭桶里有多少箭)
"itemName": 字符串, # recordType="count" 时记录物品名(如"minecraft:arrow")
"stack": [物品信息列表], # recordType="stack" 时记录后进先出的物品堆
}
}
这个数据同时存在两处:
1. 内存(self.switchRecord)—— 快,关服就没了
2. 方块实体数据(blockEntityData)—— 慢一点,但关服重启后还在
每次操作都会同步更新两处,保证重启不丢记录
"""
def __init__(self, namespace, systemName):
"""
初始化:清空记录数据、注册事件监听、构建工具切换规则字典
"""
super(BlockSwitchService, self).__init__(namespace, systemName)
print "===== BlockSwitchService init ====="
self.switchRecord = {} # 工具方块的记录数据(箭桶数量、药水架顺序等)
self.switchCooldown = {} # 工具方块切换冷却时间(防止连点)
self._cancelItemUse = {} # 将要取消下一次物品使用的玩家ID集合(防止喝药水等)
self.ListenEvent() # 注册游戏事件监听
# ---- 构建工具切换规则字典(O(1) 查找) ----
# key = (方块名, 物品名),value = 规则dict
self.switchRecipes = {}
for switch in BLOCK_SWITCHES:
key = (switch["source"], switch.get("item"))
self.switchRecipes[key] = switch
# ---- 构建工具方块名集合(O(1) 快速拦截,第一道过滤) ----
# 事件进来先判断方块名在不在这个集合里,不在直接 return,
# 这样做菜方块的右键事件根本不会进到本系统的字典查找
self.switchBlockSet = set()
for switch in BLOCK_SWITCHES:
self.switchBlockSet.add(switch["source"])
def Destroy(self):
"""
系统销毁时取消所有事件监听,防止内存泄漏
"""
print "===== BlockSwitchService Destroy ====="
self.UnListenEvent()
# ============================================================================
# 事件注册与注销
# ============================================================================
def ListenEvent(self):
"""
注册两个游戏事件:
1. ServerBlockUseEvent —— 玩家右键方块(工具切换入口)
2. ServerItemTryUseEvent —— 玩家尝试使用物品(拦截喝药水等默认行为)
"""
# 事件1:玩家右键方块
self.ListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerBlockUseEvent", self, self.OnBlockUse)
# 事件2:玩家尝试使用物品(拦截喝药水)
self.ListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerItemTryUseEvent", self, self.OnItemTryUse)
def UnListenEvent(self):
"""
注销事件监听,和 ListenEvent 一一对应
"""
self.UnListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerBlockUseEvent", self, self.OnBlockUse)
self.UnListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerItemTryUseEvent", self, self.OnItemTryUse)
# ============================================================================
# 主入口:玩家右键方块
# ============================================================================
def OnBlockUse(self, args):
"""
玩家右键方块时触发,是工具方块切换的入口。
流程:
1. 先按方块名判断是不是本系统管辖的工具方块(O(1) set 拦截)
- 不是 → 直接 return(这是做菜方块的右键,交给 CookingService 处理)
2. 拿到被右键的方块名、玩家ID、方块坐标
3. 看玩家手里拿了什么物品
4. 拿着"方块名+手里物品名"去工具切换字典里 O(1) 查找
5. 命中规则 → 执行工具切换逻辑
"""
# ---- 第一步:提取事件参数 ----
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) # 完整的唯一标识:维度+坐标
# ---- 第二步:O(1) 方块名拦截 ----
# 只有工具方块的右键才归本系统处理;做菜方块直接放过,交给 CookingService
if blockName in _DEADMOB_OWNED:
# 死亡生物方块由新系统(DeadmobService)处理,本系统跳过
return
if blockName not in self.switchBlockSet:
return
# ---- 第三步:拿到玩家手里拿的物品 ----
itemDict = self.getCarriedItem(playerId)
# ---- 第四步:查工具切换规则(O(1) 字典查找) ----
recipe = self.findRecipe(blockName, itemDict)
if not recipe:
# 方块是工具方块,但当前组合没有匹配的规则(比如手里物品不对)→ 不拦截
return
# 找到规则后,取消默认行为(如喝药水、吃食物等),由本 Mod 全权接管
args['cancel'] = True
# 同时标记该玩家下一次物品使用事件也要取消,防止喝药水等默认行为
self._cancelItemUse[playerId] = True
# ---- 第五步:加载方块历史记录到内存 ----
# 这样 handleSwitch 里直接从 self.switchRecord 取就行
self._loadBlockDataToMemory(key, playerId, pos, dim)
# ---- 第六步:执行工具切换 ----
self.handleSwitch(recipe, playerId, pos, dim, itemDict, key, blockName)
# ============================================================================
# 拦截物品使用事件(防止喝药水等默认行为)
# ============================================================================
def OnItemTryUse(self, args):
playerId = args.get("playerId")
if self._cancelItemUse.pop(playerId, False):
args['cancel'] = True
# ============================================================================
# 规则匹配(O(1) 字典查找)
# ============================================================================
def findRecipe(self, blockName, itemDict):
"""
在工具切换字典中 O(1) 查找匹配的规则。
匹配条件:
1. 先精确匹配 (方块名, 物品名)
2. 如果没找到,尝试匹配 (方块名, None) —— 空手也能触发的规则
3. 如果匹配到的是 arrowMode 规则,额外检查手中物品是否为箭
返回:匹配到的规则 dict,找不到返回 None
"""
itemName = itemDict.get("newItemName") if itemDict else None
# 精确匹配:(方块名, 物品名)
recipe = self.switchRecipes.get((blockName, itemName))
if recipe:
return recipe
# 空手匹配:(方块名, None)
recipe = self.switchRecipes.get((blockName, None))
if recipe:
# 如果规则是 arrowMode 存箭模式,检查手中物品是否为箭
if recipe.get("arrowMode") == "store":
if itemDict and self._isArrow(itemDict):
return recipe
return None
return recipe
return None
def _isArrow(self, itemDict):
"""
判断手中物品是否为箭(普通箭或药水箭)。
返回 True/False。
"""
if not itemDict:
return False
itemName = itemDict.get("newItemName", "")
return itemName == "minecraft:arrow" or itemName == "minecraft:tipped_arrow"
# ============================================================================
# 工具方块切换处理
# ============================================================================
def handleSwitch(self, recipe, playerId, pos, dim, itemDict, key, blockName):
"""
处理工具方块的切换逻辑(箭桶、挂肉钩、水槽、盘子架、药水架、书柜等)。
支持三种模式:
1. recordType="count" → 记录一种物品+数量(如箭桶存箭)
2. recordType="stack" → 后进先出堆栈(如药水架、书柜)
3. 无 record → 纯切换,不记录(如挂肉钩直接换状态)
流程:
1. 如果是计时型(type="timer"),直接替换方块,不消耗物品
2. 如果是切换型(type="switch"),根据 recordType 决定记录方式
3. 消耗物品、替换方块、更新记录(记录写入方块实体数据,退出重进不丢)
"""
# ---- 如果规则指定了 tool,检查玩家手里是不是拿着指定工具 ----
if recipe.get("tool"):
if not itemDict:
return
carriedItem = itemDict.get("newItemName", "")
# 简单判断:物品名是否包含工具名
if recipe["tool"] not in carriedItem:
return
# ---- 冷却检查:防止连点右键瞬间切换多次 ----
now = time.time()
lastOp = self.switchCooldown.get(key, 0)
cd = recipe.get("switchCd", 0.5)
if now - lastOp < cd:
return
self.switchCooldown[key] = now
# ---- 箭桶模式(arrowMode):存箭 / 取箭 ----
# 独立于普通 count/stack 逻辑,因为箭桶需要批量操作
arrowMode = recipe.get("arrowMode")
if arrowMode:
self._handleArrowMode(arrowMode, recipe, playerId, pos, dim, itemDict, key, blockName)
return
# ---- 计时型:直接替换方块,不消耗物品,不记录 ----
if recipe.get("type") == "timer":
if recipe.get("target"):
self.replaceBlock(pos, dim, recipe["target"], playerId)
return
# ---- 切换型 ----
# 内存中已有数据(_loaBlockDataToMemory 已加载),直接用
record = self.switchRecord.get(key)
# ---- 正向操作:往方块里放东西(source == 被右键的方块) ----
if recipe["source"] == blockName and recipe.get("item") is not None:
# 玩家手里有物品,往方块里放
needSave = False # 标记是否需要保存记录到方块
if recipe.get("record"):
if recipe.get("recordType") == "count":
# 数量记录型:只记录一种物品的总数
if not record:
record = {
"count": 0,
"itemName": itemDict["newItemName"],
"auxValue": itemDict.get("newAuxValue", 0),
}
# 检查是否满了
maxCount = recipe.get("maxCount", 999)
if record["count"] >= maxCount:
return
# 记录物品+1
record["count"] += 1
needSave = True
elif recipe.get("recordType") == "stack":
# 堆栈记录型:后进先出,记录每种物品的完整信息
if not record:
record = {"stack": []}
maxCount = recipe.get("maxCount", 999)
if len(record["stack"]) >= maxCount:
return
# 把物品信息压入堆栈
record["stack"].append({
"itemName": itemDict["newItemName"],
"count": itemDict.get("count", 1),
"auxValue": itemDict.get("newAuxValue", 0),
"userData": itemDict.get("userData", {}),
})
needSave = True
# 消耗物品
if 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 needSave:
self._saveRecord(key, playerId, pos, dim, record)
# 如果规则指定了给物品(如烧烤架浇水给桶)
if recipe.get("giveItem"):
self.giveItem(playerId, recipe["giveItem"])
return
# ---- 逆向操作:从方块里取东西(空手右键或取回物品) ----
if recipe["source"] == blockName and recipe.get("item") is None:
needSaveRecord = False # 是否需要保存记录
needClearRecord = False # 是否需要清除记录
if recipe.get("record") and record:
if recipe.get("recordType") == "count":
# 数量记录型取回:给玩家对应的物品,数量-1
if record["count"] > 0:
self.giveItem(playerId, record["itemName"])
record["count"] -= 1
if record["count"] <= 0:
needClearRecord = True
else:
needSaveRecord = True
elif recipe.get("recordType") == "stack":
# 堆栈记录型取回:后进先出,取最后一个放的物品
if record["stack"]:
topItem = record["stack"].pop()
comp.CreateItem(playerId).SpawnItemToPlayerInv(
{"itemName": topItem["itemName"],
"count": topItem["count"],
"auxValue": topItem["auxValue"],
"userData": topItem.get("userData", {})},
playerId)
if not record["stack"]:
needClearRecord = True
else:
needSaveRecord = True
# 如果规则直接指定了给什么物品(水槽、盘子架取回、挂肉钩退回等)
if recipe.get("giveItem"):
self.giveItem(playerId, recipe["giveItem"])
# 替换方块(旧方块销毁,新方块出现)
if recipe.get("target"):
self.replaceBlock(pos, dim, recipe["target"], playerId)
# ★ 关键:替换完方块后再保存/清除记录,否则数据随旧方块一起销毁
if needClearRecord:
self.clearSwitchRecord(key, playerId, pos, dim)
elif needSaveRecord:
self._saveRecord(key, playerId, pos, dim, record)
return
# ============================================================================
# 通用持久化帮手(避免重复的 try/except/Get/Set 写法)
# ============================================================================
def _writeBlockData(self, dim, pos, **kwargs):
"""
通用方块实体数据写入。
GetBlockEntityData 返回的 BlockEntityData 对象支持 [] 操作,
直接赋值即可持久化。官方没有 SetBlockEntityData 方法。
"""
try:
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
if blockData is None:
return
for key, value in kwargs.items():
blockData[key] = value
except Exception as e:
pass
def _saveRecord(self, key, playerId, pos, dim, record):
"""
同时更新内存和方块实体数据中的工具方块记录。
替代原本重复出现的:
self.switchRecord[key] = record
self.saveSwitchRecord(playerId, pos, dim, record)
"""
self.switchRecord[key] = record
self.saveSwitchRecord(playerId, pos, dim, record)
# ============================================================================
# 统一数据加载入口(玩家右键工具方块时先把方块数据恢复到内存)
# ============================================================================
def _loadBlockDataToMemory(self, key, playerId, pos, dim):
"""
在确定玩家右键命中了一个工具切换规则后,立刻把方块里存的记录加载到内存。
这样 handleSwitch 里就不用自己加载了,直接从 self.switchRecord 取就行。
"""
if key not in self.switchRecord:
switchInfo = self.loadSwitchRecord(playerId, pos, dim)
if switchInfo:
self.switchRecord[key] = switchInfo
# ============================================================================
# 工具方块记录持久化(药水架、书柜、箭桶等)
# ============================================================================
def saveSwitchRecord(self, playerId, pos, dim, record):
"""
把工具方块的记录数据写入方块实体数据。
记录类型:count型(箭桶)→ rtdd_swt/swc/swi,stack型(书柜/药水架)→ rtdd_swt/sws
记录存在方块实体数据里,所以玩家退出游戏再重进,
loadSwitchRecord 还能从方块里把物品读回来。
"""
if "count" in record:
self._writeBlockData(dim, pos,
rtdd_swt="count",
rtdd_swc=record["count"],
rtdd_swi=record.get("itemName", ""),
rtdd_swa=record.get("auxValue", 0))
elif "stack" in record:
self._writeBlockData(dim, pos,
rtdd_swt="stack",
rtdd_sws=record["stack"])
def loadSwitchRecord(self, playerId, pos, dim):
"""
从方块实体数据里读取工具方块的记录数据。
返回:
有数据 → {"count": N, "itemName": "xxx"} 或 {"stack": [...]}
没有 → None
"""
try:
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
if blockData is None:
return None
swt = blockData["rtdd_swt"]
if swt == "count":
count = blockData["rtdd_swc"]
itemName = blockData["rtdd_swi"]
try:
auxValue = blockData["rtdd_swa"]
except (KeyError, Exception):
auxValue = 0
return {
"count": count,
"itemName": itemName,
"auxValue": auxValue,
}
elif swt == "stack":
stack = blockData["rtdd_sws"]
return {
"stack": stack,
}
else:
return None
except Exception as e:
return None
def clearSwitchRecord(self, key, playerId, pos, dim):
"""
清除工具方块的记录数据(内存+方块实体数据)。
"""
self.switchRecord.pop(key, None)
self._writeBlockData(dim, pos, rtdd_swt="", rtdd_swc=0, rtdd_swi="", rtdd_swa=0, rtdd_sws=[])
# ============================================================================
# 箭桶模式(arrowMode):存箭 / 取箭
# ============================================================================
def _handleArrowMode(self, arrowMode, recipe, playerId, pos, dim, itemDict, key, blockName):
"""
处理箭桶的存箭/取箭逻辑。
【存箭流程】
1. 检查手中是否为箭(_isArrow 已在 findRecipe 验证过)
2. 检查方块是否有记录且类型匹配:
a. 已有记录 → 检查手中箭类型(itemName+auxValue)是否匹配 → 不匹配则 return
b. 无记录 → 新建记录
3. 计算实际可存数量 = min(手中数量, maxCount - 已有数量)
4. 如果可存数量 <= 0 → return
5. 从手中扣减,更新记录,替换方块
【取箭流程】
1. 检查方块是否有记录且 count > 0
2. 给玩家手中/背包添加 count 只箭(itemName + auxValue 按记录)
3. 清空记录,替换方块
"""
record = self.switchRecord.get(key)
if arrowMode == "store":
# ---- 存箭 ----
handCount = itemDict.get("count", 0)
if handCount <= 0:
return
handItemName = itemDict.get("newItemName", "")
handAuxValue = itemDict.get("newAuxValue", 0)
# 检查方块是否有记录且类型匹配
if record and record["count"] > 0:
# 已有记录 → 检查箭类型是否一致
if record.get("itemName") != handItemName or record.get("auxValue", 0) != handAuxValue:
return # 类型不匹配,不能混存
else:
# 无记录 → 新建
record = {
"count": 0,
"itemName": handItemName,
"auxValue": handAuxValue,
}
# 计算实际可存数量
maxCount = recipe.get("maxCount", 32)
if record["count"] >= maxCount:
return # 已满
storeCount = min(handCount, maxCount - record["count"])
# 从玩家手中扣减
self._consumeItemCount(playerId, itemDict, storeCount)
# 更新记录
record["count"] += storeCount
# 替换方块
if recipe.get("target"):
self.replaceBlock(pos, dim, recipe["target"], playerId)
# ★ 保存记录到新方块
# 注意:这里不用延迟写入。虽然 SetBlockNew 替换方块后 block entity
# 可能尚未完全初始化,但 GetBlockEntityData 仍然能获取到对象并写入。
# 延迟写入反而可能导致退出游戏后数据不持久化。
self._saveRecord(key, playerId, pos, dim, record)
elif arrowMode == "retrieve":
# ---- 取箭 ----
if not record or record["count"] <= 0:
return
retCount = record["count"]
retItemName = record["itemName"]
retAuxValue = record.get("auxValue", 0)
# 给玩家箭
self.giveItem(playerId, retItemName, retCount, retAuxValue)
# 替换方块
if recipe.get("target"):
self.replaceBlock(pos, dim, recipe["target"], playerId)
# ★ 清空记录
self.clearSwitchRecord(key, playerId, pos, dim)
def _consumeItemCount(self, playerId, itemDict, amount):
"""
从玩家主手批量扣减指定数量的物品。
如果手中数量刚好等于 amount → 清空主手。
如果手中数量大于 amount → 减少 amount。
"""
if not itemDict or amount <= 0:
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:
# 数量够 → 减少 amount 个
itemDict["count"] = count - amount
comp.CreateItem(playerId).SetEntityItem(
serverApi.GetMinecraftEnum().ItemPosType.CARRIED,
{"itemName": itemDict["newItemName"],
"count": itemDict["count"],
"auxValue": itemDict.get("newAuxValue", 0)})
# ============================================================================
# 延迟写入帮手(解决 SetBlockNew 替换方块后 block entity 未初始化的问题)
# ============================================================================
def _saveRecordDelayed(self, key, playerId, pos, dim, record):
"""
延迟一帧保存记录到方块实体数据。
SetBlockNew 替换方块后,新方块的 block entity 需要引擎内部初始化时间,
立即 GetBlockEntityData 可能返回 None 导致写入静默失败。
先更新内存,再延迟写入方块实体。
"""
self.switchRecord[key] = record
gameComp.AddTimer(0.1, self.saveSwitchRecord, playerId, pos, dim, record)
def _clearRecordDelayed(self, key, playerId, pos, dim):
"""
延迟一帧清除方块实体数据。
"""
self.switchRecord.pop(key, None)
gameComp.AddTimer(0.1, self._clearRecordWrite, playerId, pos, dim)
def _clearRecordWrite(self, playerId, pos, dim):
"""
实际执行清除写入(被 AddTimer 调用)。
"""
self._writeBlockData(dim, pos, rtdd_swt="", rtdd_swc=0, rtdd_swi="", rtdd_swa=0, rtdd_sws=[])
# ============================================================================
# 工具方法:获取玩家手中物品
# ============================================================================
def getCarriedItem(self, playerId):
"""
获取玩家主手(右手)拿的物品。
空手返回 None。
"""
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:
# 数量够 → 减少 amount 个
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, aux=None):
"""
把指定位置的方块替换成新方块。
对照 BlockBlock.py 的标准写法:
- 使用命名参数
- oldBlockHandling=0 表示直接替换(不保留旧方块数据)
- aux 默认继承旧方块朝向,这样替换后不会转方向
"""
if aux is None:
try:
oldBlock = comp.CreateBlockInfo(playerId).GetBlockNew(pos, dim)
aux = oldBlock.get("aux", 0)
except:
aux = 0
comp.CreateBlockInfo(playerId).SetBlockNew(
pos,
blockDict={"name": newBlockName, "aux": aux},
oldBlockHandling=0,
dimensionId=dim)
# ============================================================================
# 工具方法:给玩家物品
# ============================================================================
def giveItem(self, playerId, itemName, count=1, auxValue=0):
"""
往玩家背包里塞一个物品。
如果背包满了,物品会掉在地上。
"""
comp.CreateItem(playerId).SpawnItemToPlayerInv(
{"itemName": itemName, "count": count, "auxValue": auxValue}, playerId)
@@ -106,15 +106,17 @@ class FeatureDispatcher(BlueFoxServerSystem):
def startTimerFor(cls, blockName, dim, pos, playerId=None): def startTimerFor(cls, blockName, dim, pos, playerId=None):
""" """
右键服务调用:把"计时源方块"的计时启动任务转发给对应的计时服务。 右键服务调用:把"计时源方块"的计时启动任务转发给对应的计时服务。
- 例如:烧烤右键"烤鸡(生)"开始烤制;动物装满饲料盆后自动开始变空计时 - 例如:烧烤右键"烤鸡(生)"开始烤制;动物装满饲料盆后自动开始变空计时;
- 内部通过 timerOwner 路由表 O(1) 找到计时服务,调它的 startTimer 饮料冰棍盒手持冰块右键未定型启动定型计时
返回 True=找到了计时服务并处理;False=该方块不属于任何计时服务 - 内部通过 timerOwner 路由表 O(1) 找到计时服务,调它的 startTimer,
并返回计时服务的实际返回值:
True = 本次真正启动了计时(如饮料冰棍盒据此消耗冰块)
False = 没有计时服务 / 该方块已在计时中(不重复启动)
""" """
service = cls.timerOwner.get(blockName) service = cls.timerOwner.get(blockName)
if not service: if not service:
return False return False
service.startTimer(dim, pos, blockName, playerId) return service.startTimer(dim, pos, blockName, playerId)
return True
def __init__(self, namespace, systemName): def __init__(self, namespace, systemName):
""" """
@@ -118,28 +118,30 @@ class BarbecueTimerService(BlueFoxServerSystem):
启动烤制计时:先检查下方篝火,通过后写 开始时间戳+时长 到方块实体数据。 启动烤制计时:先检查下方篝火,通过后写 开始时间戳+时长 到方块实体数据。
- 启动前检查:下方不是篝火 → 拒绝并提示(烧烤必须右键激活,不会自动烤) - 启动前检查:下方不是篝火 → 拒绝并提示(烧烤必须右键激活,不会自动烤)
- 已在本规则计时中的方块不会重复重置时间(保持原剩余时间) - 已在本规则计时中的方块不会重复重置时间(保持原剩余时间)
返回 True=找到规则并处理(含已计时中/篝火检查失败);False=不属于本计时功能
""" """
recipe = self.barbecueTimers.get(blockName) recipe = self.barbecueTimers.get(blockName)
if not recipe: if not recipe:
return return False
# 启动前检查下方篝火(烧烤必须下方是篝火才能开始烤制) # 启动前检查下方篝火(烧烤必须下方是篝火才能开始烤制)
if not self.checkBelow(recipe, dim, pos): if not self.checkBelow(recipe, dim, pos):
if playerId: if playerId:
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§c支杆下方需要放一个篝火(营火)才能烤制!") comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§c支杆下方需要放一个篝火(营火)才能烤制!")
return return True
# 去重:如果该方块已经在本规则计时中,不重置(保持原剩余时间) # 去重:如果该方块已经在本规则计时中,不重置(保持原剩余时间)
try: try:
data = blockEntityDataComp.GetBlockEntityData(dim, pos) data = blockEntityDataComp.GetBlockEntityData(dim, pos)
if data and data["rtdd_tid"] == blockName: if data and data["rtdd_tid"] == blockName:
return return True
except Exception: except Exception:
pass pass
self._writeTimerData(dim, pos, recipe, playerId) self._writeTimerData(dim, pos, recipe, playerId)
# 烤制开始提示 # 烤制开始提示
if playerId and recipe.get("startTip"): if playerId and recipe.get("startTip"):
comp.CreateMsg(playerId).NotifyOneMessage(playerId, recipe["startTip"]) comp.CreateMsg(playerId).NotifyOneMessage(playerId, recipe["startTip"])
return True
# ============================================================================ # ============================================================================
# 三、tick 检查与到点转换 # 三、tick 检查与到点转换
@@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
"""
================================================================================
乡野饮料(xy_drink)转化规则数据
================================================================================
所有"乡野饮料"(xy_drink) 分类下的转化规则常量都在这里。
只被 YinliaoService / YinliaoTimerService 读取,FeatureDispatcher 通过方块名/物品名
集合路由到对应服务,不会去查其他功能的数据库。
【三大分组】
1) 果汁·榨汁饮品(榨汁机系列 juicer / juicer_0~4) —— 全流程右键转换
果汁由第一步放入的水果决定并【持久化到方块实体数据】, 退出游戏重进后
榨汁完成仍取回正确果汁(详见 YinliaoService._readFruit / _writeFruit)。
2) 冰棍(冰棍盒 icelollybox_1/2 + 各水果盒 icelollyboxb~f)
未定型 → (手持冰块右键启动) 计时30秒 → 定型 → (手持水果x2) → 各水果盒
→ (空手右键) 取出2根冰棍, 还原未定型。
冰棍堆 popsiclepile_2/3: 组合式(苹果冰棍+堆 → 堆2, 空手取回 LIFO)。
3) 冰棍盒·炒冰盒(frytheicebox / frytheicebox_1 + 各水果炒冰盒)
空 → (手持冰块) 放冰 → (手持水果) 炒冰 → (空手右键) 取出冰沙, 还原空。
注: 西瓜炒冰盒可做, 但 西瓜冰沙(melonicecreamplace) 是禁用方块, 无取出规则。
字段说明(与 AnimalData/BarbecueData 一致):
水果物品名 → (成品方块, 名称) 的映射表, 服务端按需读取。
================================================================================
"""
# ============================================================================
# 一、冰块(广义的冰): 原版 冰块/冰砖/蓝冰/霜冰
# 冰棍盒定型、炒冰盒放冰都消耗这几种冰中的任意一种
# ============================================================================
ICE_ITEMS = [
"minecraft:ice",
"minecraft:packed_ice",
"minecraft:blue_ice",
"minecraft:frosted_ice",
]
# ============================================================================
# 二、榨汁机(果汁·榨汁饮品)
# ============================================================================
# 水果物品名 → (果汁方块, 果汁名)
# 西瓜=原版西瓜片 / 橙子=rtdd:orange / 苹果=原版苹果 / 桃子=rtdd:peach /
# 葡萄=rtdd:grape / 黄豆=rtdd:soybean(豆浆)
JUICE_FRUITS = {
"minecraft:melon_slice": ("rtdd:fruitjuice_1", "西瓜汁"),
"rtdd:orange": ("rtdd:fruitjuice_2", "橙汁"),
"minecraft:apple": ("rtdd:fruitjuice_3", "苹果汁"),
"rtdd:peach": ("rtdd:fruitjuice_4", "桃子汁"),
"rtdd:grape": ("rtdd:fruitjuice_6", "葡萄汁"),
"rtdd:soybean": ("rtdd:fruitjuice_7", "豆浆"),
}
# 榨汁机状态链(空→放入水果→榨汁中1→2→3→完成), 索引按步骤顺序
JUICER_CHAIN = [
"rtdd:juicer", # 0 榨汁机(空)
"rtdd:juicer_0", # 1 榨汁机(放入水果)
"rtdd:juicer_1", # 2 榨汁机(榨汁中1)
"rtdd:juicer_2", # 3 榨汁机(榨汁中2)
"rtdd:juicer_3", # 4 榨汁机(榨汁中3)
"rtdd:juicer_4", # 5 榨汁机(完成)
]
# 榨汁中逐级右键的提示(juicer_0→1→2→3→4 各阶段)
JUICER_ADVANCE_TIPS = {
"rtdd:juicer_0": "§a开始榨汁…",
"rtdd:juicer_1": "§a榨汁中…",
"rtdd:juicer_2": "§a榨汁中…",
"rtdd:juicer_3": "§e榨汁完成!",
}
# ============================================================================
# 三、冰棍盒(冰棍)
# ============================================================================
# 冰棍盒(未定型) icelollybox_1 → 冰棍盒(定型) icelollybox_2 的计时规则:
# 手持冰块右键启动(消耗1个冰块), 约30秒定型; autoStart=False 不自动开始
ICE_LOLLY_TIMER = {
"source": "rtdd:icelollybox_1",
"target": "rtdd:icelollybox_2",
"time": 30,
"autoStart": False,
"startTip": "§e已放入冰块,冰棍定型中…",
"doneTip": "§e冰棍盒里的冰棍定型了!",
}
# 水果物品名 → (各水果冰棍盒, 冰棍方块, 口味名)
# 苹果→icelollyboxc / 桃子→icelollyboxd / 橙子→icelollyboxb /
# 葡萄→icelollyboxe / 西瓜→icelollyboxf
POPSICLE_FRUITS = {
"minecraft:apple": ("rtdd:icelollyboxc", "rtdd:apple_popsicleplace", "苹果"),
"rtdd:peach": ("rtdd:icelollyboxd", "rtdd:peach_popsicleplace", "桃子"),
"rtdd:orange": ("rtdd:icelollyboxb", "rtdd:orange_popsicle_place", "橙子"),
"rtdd:grape": ("rtdd:icelollyboxe", "rtdd:grape_popsicleplace", "葡萄"),
"minecraft:melon_slice": ("rtdd:icelollyboxf", "rtdd:melon_popsicleplace", "西瓜"),
}
# 冰棍盒方块 → (水果物品名, 冰棍方块, 口味名) 反向表(取冰棍用)
POPSICLE_BOX_INFO = {v[0]: (k, v[1], v[2]) for k, v in POPSICLE_FRUITS.items()}
# 冰棍堆组合:
# popsiclepile_2(橙子+西瓜) + 苹果冰棍 → popsiclepile_3(橙子+西瓜+苹果)
# popsiclepile_3 空手取回 → 苹果冰棍, 退回 popsiclepile_2
# (其他口味组合暂无对应堆方块, 不支持堆叠)
POPSICLE_PILE_ADD = "rtdd:apple_popsicleplace" # 能插上堆的冰棍
POPSICLE_PILE_2 = "rtdd:popsiclepile_2" # 2根堆(基础)
POPSICLE_PILE_3 = "rtdd:popsiclepile_3" # 3根堆
# ============================================================================
# 四、炒冰盒(冰棍盒·炒冰盒 分组)
# ============================================================================
# 水果物品名 → (各水果炒冰盒, 冰沙方块, 口味名)
# 五种口味齐全: 西瓜冰沙(melonicecreamplace) 已从禁用移回乡野饮料, 可正常取出
FRYICE_FRUITS = {
"minecraft:apple": ("rtdd:appleicebox", "rtdd:appleicecreamplace", "苹果"),
"rtdd:grape": ("rtdd:grapeicebox", "rtdd:grapeicecreamplace", "葡萄"),
"minecraft:melon_slice": ("rtdd:melonicebox", "rtdd:melonicecreamplace", "西瓜"),
"rtdd:orange": ("rtdd:orangeicebox", "rtdd:orangeicecreamplace", "橘子"),
"rtdd:peach": ("rtdd:peachicebox", "rtdd:peachicecreamplace", "桃子"),
}
# 炒冰盒方块 → (水果物品名, 冰沙方块, 口味名) 反向表(取冰沙用)
FRYICE_BOX_INFO = {v[0]: (k, v[1], v[2]) for k, v in FRYICE_FRUITS.items()}
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
"""
乡野饮料客户端(预留)
=====================
目前乡野饮料功能的服务端逻辑已由 YinliaoService + YinliaoTimerService 处理,
客户端暂不需要额外的 UI 工作。
如果以后需要榨汁进度提示、冰棍定型倒计时等 UI,可以在这里扩展。
"""
import mod.client.extraClientApi as clientApi
ClientSystem = clientApi.GetClientSystemCls()
class YinliaoClient(ClientSystem):
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
print "===== YinliaoClient init ====="
def Destroy(self):
print "===== YinliaoClient Destroy ====="
@@ -0,0 +1,449 @@
# -*- coding: utf-8 -*-
"""
================================================================================
乡野饮料·右键转换引擎(服务端)
================================================================================
处理"乡野饮料"(xy_drink) 分类下的全部【右键切换型】转换:
1) 榨汁机(果汁·榨汁饮品): 全流程右键
空(juicer) + 水果 → 放入水果(juicer_0, 记录水果类型)
→ 榨汁中1/2/3(juicer_1/2/3) → 完成(juicer_4)
→ 空手右键取出对应果汁, 还原空
【水果类型持久化】放入水果时把水果写进方块实体数据(rtdd_jf), 之后每次
换态(SetBlockNew 会清空方块实体)都【先读旧→替换→写回新】, 因此玩家
在 榨汁2→榨汁3 过程中退出游戏, 重进后取出的果汁仍是第一步放入的水果。
2) 冰棍盒(冰棍):
未定型(icelollybox_1) + 冰块 → 启动30秒定型计时(转发给计时服务, 消耗1冰)
定型(icelollybox_2) + 水果x2 → 各水果冰棍盒(icelollyboxb~f)
各水果冰棍盒 空手右键 → 取出2根冰棍, 还原未定型
3) 炒冰盒(冰棍盒·炒冰盒):
空(frytheicebox) + 冰块 → 放冰(frytheicebox_1)
放冰 + 水果 → 各水果炒冰盒(appleicebox 等)
各水果炒冰盒(除西瓜) 空手右键 → 取出1个冰沙, 还原空
4) 冰棍堆(组合, 同厨具架逻辑):
popsiclepile_2(橙子+西瓜) + 苹果冰棍 → popsiclepile_3(橙子+西瓜+苹果)
popsiclepile_3 空手右键 → 取出苹果冰棍(LIFO), 退回 popsiclepile_2
【与计时转换解耦】
冰棍盒"未定型→定型"的 30 秒计时拆到独立类 YinliaoTimerService。
本类右键到计时源方块(icelollybox_1 + 冰块)时, 调 FeatureDispatcher.startTimerFor
转发给计时服务启动(冰块消耗由本类处理)。
【事件监听在哪?】
本类【不】监听任何事件!
所有事件由 FeatureDispatcher 统一监听, 按 blockOwner 路由到本类 handleBlockUse。
【本类怎么做(自上而下的方法系列)】
handleBlockUse ← 入口:分发器转发右键方块事件
├─ _handleJuicer ← 榨汁机: 放入水果/逐级榨汁/取出果汁
│ ├─ _readFruit / _writeFruit ← 水果类型 读/写(持久化)
├─ _handleLollyBox ← 冰棍盒: 加冰块(启动计时)/加水果/取冰棍
├─ _handleFryIceBox ← 炒冰盒: 加冰块/加水果/取冰沙
└─ _handlePopsiclePile ← 冰棍堆: 插苹果冰棍/取回
(通用工具) getCarriedItem / consumeItem / replaceBlock / dropItems / checkCooldown
================================================================================
"""
import time
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
# ---- 方块实体数据组件(记录榨汁机水果类型; SetBlockNew 后带延迟兜底) ----
blockEntityDataComp = comp.CreateBlockEntityData(LevelId)
# ---- 游戏组件(AddTimer 延迟写入 block entity 兜底) ----
gameComp = comp.CreateGame(LevelId)
# ---- 只导入乡野饮料自己的数据库 ----
from ..YinliaoData import (
ICE_ITEMS,
JUICE_FRUITS, JUICER_CHAIN, JUICER_ADVANCE_TIPS,
ICE_LOLLY_TIMER, POPSICLE_FRUITS, POPSICLE_BOX_INFO,
POPSICLE_PILE_ADD, POPSICLE_PILE_2, POPSICLE_PILE_3,
FRYICE_FRUITS, FRYICE_BOX_INFO,
)
# ---- 统一分发器 ----
from ...service.FeatureDispatcher import FeatureDispatcher
class YinliaoService(BlueFoxServerSystem):
"""
乡野饮料·右键转换引擎
【核心数据结构】
featureBlocks = {方块名, ...} # 本功能管辖的所有右键方块(注册路由用)
cooldown = {key: 时间戳} # 连点冷却表
"""
def __init__(self, namespace, systemName):
super(YinliaoService, self).__init__(namespace, systemName)
print "===== YinliaoService init ====="
# ---- 收集本功能管辖的所有右键方块 ----
self.featureBlocks = set()
for b in JUICER_CHAIN: # 榨汁机全态
self.featureBlocks.add(b)
self.featureBlocks.add("rtdd:icelollybox_1") # 冰棍盒(未定型/定型)
self.featureBlocks.add("rtdd:icelollybox_2")
for _f, info in POPSICLE_FRUITS.items(): # 各水果冰棍盒
self.featureBlocks.add(info[0])
self.featureBlocks.add("rtdd:frytheicebox") # 炒冰盒(空/放冰)
self.featureBlocks.add("rtdd:frytheicebox_1")
for _f, info in FRYICE_FRUITS.items(): # 各水果炒冰盒
self.featureBlocks.add(info[0])
self.featureBlocks.add(POPSICLE_PILE_2) # 冰棍堆
self.featureBlocks.add(POPSICLE_PILE_3)
# ---- 右键冷却表 ----
self.cooldown = {}
# ---- 注册到统一分发器 ----
FeatureDispatcher.register(self)
# ============================================================================
# 一、给 FeatureDispatcher 的接口
# ============================================================================
def getFeatureBlocks(self):
"""返回本功能管辖的所有方块名集合(右键路由用)"""
return self.featureBlocks
def getItemUseOnRules(self):
"""手持水果右键方块表面不需要单独路由, 返回空。
水果为可食用物品, 右键饮料方块时会走 ServerBlockUseEvent(与挂鱼一致),
由分发器取消后续"吃"行为。"""
return {}
def handleBlockUse(self, args):
"""
由 FeatureDispatcher 转发的右键方块入口。
返回 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 blockName in JUICER_CHAIN:
return self._handleJuicer(blockName, playerId, pos, dim, itemDict, itemName, key)
# ---- 冰棍盒 ----
if blockName == "rtdd:icelollybox_1" or blockName == "rtdd:icelollybox_2" \
or blockName in POPSICLE_BOX_INFO:
return self._handleLollyBox(blockName, playerId, pos, dim, itemDict, itemName, key)
# ---- 炒冰盒 ----
if blockName == "rtdd:frytheicebox" or blockName == "rtdd:frytheicebox_1" \
or blockName in FRYICE_BOX_INFO:
return self._handleFryIceBox(blockName, playerId, pos, dim, itemDict, itemName, key)
# ---- 冰棍堆 ----
if blockName == POPSICLE_PILE_2 or blockName == POPSICLE_PILE_3:
return self._handlePopsiclePile(blockName, playerId, pos, dim, itemDict, itemName, key)
return False
# ============================================================================
# 二、榨汁机(全流程右键, 水果类型持久化)
# ============================================================================
def _handleJuicer(self, blockName, playerId, pos, dim, itemDict, itemName, key):
"""榨汁机分四步: 放入水果 → 逐级榨汁 → 空手取出果汁"""
# ---- 空榨汁机 + 水果 → 放入水果(记录水果类型) ----
if blockName == "rtdd:juicer":
juiceInfo = JUICE_FRUITS.get(itemName)
if not juiceInfo:
return False
if not self.checkCooldown(key, 0.5):
return True
self.consumeItem(playerId, itemDict, 1)
self.replaceBlock(pos, dim, "rtdd:juicer_0", playerId)
# 替换后写水果类型(block entity 可能未初始化, 带延迟兜底)
self._writeFruit(dim, pos, itemName)
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a已放入水果,右键开始榨汁")
return True
# ---- 榨汁中逐级推进(juicer_0→1→2→3→4): 任意右键, 换态并保留水果类型 ----
if blockName in JUICER_ADVANCE_TIPS:
if not self.checkCooldown(key, 0.5):
return True
# 先读旧方块的水果类型(SetBlockNew 会清空方块实体)
fruit = self._readFruit(pos, dim)
idx = JUICER_CHAIN.index(blockName)
self.replaceBlock(pos, dim, JUICER_CHAIN[idx + 1], playerId)
if fruit:
self._writeFruit(dim, pos, fruit) # 替换后写回新方块
tip = JUICER_ADVANCE_TIPS.get(blockName)
if tip:
comp.CreateMsg(playerId).NotifyOneMessage(playerId, tip)
return True
# ---- 完成(juicer_4) 空手右键 → 取出果汁, 还原空 ----
if blockName == "rtdd:juicer_4":
if itemName and itemName != "minecraft:air":
return False # 必须空手取出, 手持其他物品不处理
if not self.checkCooldown(key, 0.5):
return True
fruit = self._readFruit(pos, dim)
juiceInfo = JUICE_FRUITS.get(fruit) if fruit else None
if not juiceInfo:
return False # 无水果记录(创造直接放置的完成态) → 不反应
juiceBlock, juiceName = juiceInfo
self.dropItems(dim, pos, [{"name": juiceBlock, "count": 1}])
self.replaceBlock(pos, dim, "rtdd:juicer", playerId) # 还原空, 记录随方块清除
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a取出了" + juiceName)
return True
return False
# ============================================================================
# 三、冰棍盒(加冰块启动定型计时 / 加水果 / 取冰棍)
# ============================================================================
def _handleLollyBox(self, blockName, playerId, pos, dim, itemDict, itemName, key):
# ---- 未定型 + 冰块 → 启动30秒定型计时(消耗1块冰) ----
if blockName == "rtdd:icelollybox_1":
if itemName not in ICE_ITEMS:
return False
if not self.checkCooldown(key, 0.5):
return True
# 转发给计时服务: True=本次启动, False=已在定型中(不重复消耗冰块)
started = FeatureDispatcher.startTimerFor(blockName, dim, pos, playerId)
if not started:
return True # 已在定型中, 拦截但不消耗
self.consumeItem(playerId, itemDict, 1)
if ICE_LOLLY_TIMER.get("startTip"):
comp.CreateMsg(playerId).NotifyOneMessage(playerId, ICE_LOLLY_TIMER["startTip"])
return True
# ---- 定型 + 水果x2 → 各水果冰棍盒 ----
if blockName == "rtdd:icelollybox_2":
info = POPSICLE_FRUITS.get(itemName)
if not info:
return False
if not self.checkCooldown(key, 0.5):
return True
# 数量检查: 需要2个水果, 不足则红字提示且不转换
if (itemDict.get("count") or 0) < 2:
comp.CreateMsg(playerId).NotifyOneMessage(
playerId, "§c需要2个" + info[2] + "才能做冰棍")
return True
boxBlock, _popsicle, flavor = info
self.consumeItem(playerId, itemDict, 2)
self.replaceBlock(pos, dim, boxBlock, playerId)
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a" + flavor + "冰棍做好了!")
return True
# ---- 各水果冰棍盒 空手右键 → 取出2根冰棍, 还原未定型 ----
info = POPSICLE_BOX_INFO.get(blockName)
if not info:
return False
if itemName and itemName != "minecraft:air":
return False
if not self.checkCooldown(key, 0.5):
return True
_fruit, popsicle, flavor = info
self.dropItems(dim, pos, [{"name": popsicle, "count": 2}])
self.replaceBlock(pos, dim, "rtdd:icelollybox_1", playerId) # 还原未定型
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a取出了2根" + flavor + "冰棍")
return True
# ============================================================================
# 四、炒冰盒(加冰块 / 加水果 / 取冰沙)
# ============================================================================
def _handleFryIceBox(self, blockName, playerId, pos, dim, itemDict, itemName, key):
# ---- 空 + 冰块 → 放冰 ----
if blockName == "rtdd:frytheicebox":
if itemName not in ICE_ITEMS:
return False
if not self.checkCooldown(key, 0.5):
return True
self.consumeItem(playerId, itemDict, 1)
self.replaceBlock(pos, dim, "rtdd:frytheicebox_1", playerId)
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a已放入冰块,右键放入水果炒冰")
return True
# ---- 放冰 + 水果 → 各水果炒冰盒 ----
if blockName == "rtdd:frytheicebox_1":
info = FRYICE_FRUITS.get(itemName)
if not info:
return False
if not self.checkCooldown(key, 0.5):
return True
iceBox, _icecream, flavor = info
self.consumeItem(playerId, itemDict, 1)
self.replaceBlock(pos, dim, iceBox, playerId)
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a" + flavor + "炒冰做好了!")
return True
# ---- 各水果炒冰盒 空手右键 → 取出1个冰沙, 还原空 ----
info = FRYICE_BOX_INFO.get(blockName)
if not info:
return False
_fruit, icecream, flavor = info
if not icecream:
return False # 该口味无成品冰沙(防御), 正常五种口味均有
if itemName and itemName != "minecraft:air":
return False
if not self.checkCooldown(key, 0.5):
return True
self.dropItems(dim, pos, [{"name": icecream, "count": 1}])
self.replaceBlock(pos, dim, "rtdd:frytheicebox", playerId) # 还原空
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a取出了" + flavor + "冰沙")
return True
# ============================================================================
# 五、冰棍堆(组合式: 插苹果冰棍 / 空手取回 LIFO)
# ============================================================================
def _handlePopsiclePile(self, blockName, playerId, pos, dim, itemDict, itemName, key):
# ---- 堆(橙子+西瓜) + 苹果冰棍 → 堆2(橙子+西瓜+苹果) ----
if blockName == POPSICLE_PILE_2:
if itemName != POPSICLE_PILE_ADD:
return False
if not self.checkCooldown(key, 0.5):
return True
self.consumeItem(playerId, itemDict, 1)
self.replaceBlock(pos, dim, POPSICLE_PILE_3, playerId)
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a把苹果冰棍插到冰棍堆上")
return True
# ---- 堆2 空手右键 → 取出苹果冰棍(后放先取), 退回堆 ----
if blockName == POPSICLE_PILE_3:
if itemName and itemName != "minecraft:air":
return False
if not self.checkCooldown(key, 0.5):
return True
self.dropItems(dim, pos, [{"name": POPSICLE_PILE_ADD, "count": 1}])
self.replaceBlock(pos, dim, POPSICLE_PILE_2, playerId)
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a取走了苹果冰棍")
return True
return False
# ============================================================================
# 六、水果类型 读/写(榨汁机果汁持久化, 字段 rtdd_jf)
# ============================================================================
def _writeFruit(self, dim, pos, itemName):
"""把水果类型写入榨汁机方块实体数据(替换方块后调用, 带 0.1s 延迟兜底)"""
try:
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
if blockData is None:
gameComp.AddTimer(0.1, self._writeFruitDelayed, dim, pos, itemName)
return
blockData["rtdd_jf"] = str(itemName)
except Exception:
gameComp.AddTimer(0.1, self._writeFruitDelayed, dim, pos, itemName)
def _writeFruitDelayed(self, dim, pos, itemName):
"""延迟写入兜底(block entity 未初始化时)"""
try:
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
if blockData is not None:
blockData["rtdd_jf"] = str(itemName)
except Exception:
pass
def _readFruit(self, pos, dim):
"""从方块实体数据读出水果类型, 无记录返回 None"""
try:
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
if blockData is None:
return None
return blockData["rtdd_jf"] or None
except Exception:
return None
# ============================================================================
# 七、工具方法:获取玩家主手物品
# ============================================================================
def getCarriedItem(self, playerId):
itemComp = comp.CreateItem(playerId)
return itemComp.GetPlayerItem(
serverApi.GetMinecraftEnum().ItemPosType.CARRIED, 0, True)
# ============================================================================
# 八、工具方法:消耗物品
# ============================================================================
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),
"userData": itemDict.get("userData", {})})
# ============================================================================
# 九、工具方法:替换方块
# ============================================================================
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,234 @@
# -*- coding: utf-8 -*-
"""
================================================================================
乡野饮料·计时转换引擎(服务端)
================================================================================
只处理"乡野饮料"(xy_drink) 分类下的【计时自动转换】:
冰棍盒(未定型 icelollybox_1) → 冰棍盒(定型 icelollybox_2) 约30秒
【与右键转换解耦】
同分类的"右键切换"(榨汁/冰棍盒加水果/炒冰盒/冰棍堆)在 YinliaoService。
本类只负责计时,两者通过 FeatureDispatcher 协作:
- YinliaoService 手持冰块右键 未定型 → 调 FeatureDispatcher.startTimerFor
转发到本类的 startTimer 启动定型计时(冰块消耗由右键服务处理)
- 服务器 tick(ServerBlockEntityTickEvent)由 FeatureDispatcher 按 timerOwner
路由到本类的 handleBlockEntityTick,到点即转换为 定型
【计时怎么做到"退出游戏继续计时"】
不做倒计时,只记"开始时刻":
- 启动时把 开始时间戳(rtdd_tstart)+总时长(rtdd_tdur) 写进方块实体数据
- 每帧 tick 判断 当前时间-开始时刻 >= 总时长 → 到点转换
- 时间戳是绝对时间(time.time()),服务器重启/玩家离线,真实时间仍在流逝,
回来后 tick 直接算差值,剩余时间自然准确
【事件监听在哪?】
本类【不】监听任何事件!
ServerBlockEntityTickEvent 由 FeatureDispatcher 统一监听,再按 timerOwner
路由到本类的 handleBlockEntityTick。
【本类怎么做(自上而下的方法系列)】
handleBlockEntityTick ← 入口:分发器转发 tick 事件
└─ _tickProcess ← 判断是否到点 / autoStart=False 不自动补写
└─ _finishTimer ← 到点执行转换 + 完成提示
startTimer ← 入口:YinliaoService 转发过来启动定型
└─ _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 ..YinliaoData import ICE_LOLLY_TIMER
# ---- 统一分发器(本类把自己注册进计时路由表,让分发器把 tick 事件转发过来) ----
from ...service.FeatureDispatcher import FeatureDispatcher
class YinliaoTimerService(BlueFoxServerSystem):
"""
乡野饮料·计时转换引擎
【核心数据结构】
iceLollyTimers = {计时源方块名: 规则dict} # 目前只有 冰棍盒未定型
"""
def __init__(self, namespace, systemName):
super(YinliaoTimerService, self).__init__(namespace, systemName)
print "===== YinliaoTimerService init ====="
# ---- 收集计时型规则(source → 规则) ----
self.iceLollyTimers = {ICE_LOLLY_TIMER["source"]: ICE_LOLLY_TIMER}
# ---- 注册到分发器的计时路由表(tick 事件按方块名路由到这里) ----
FeatureDispatcher.registerTimer(self)
# ============================================================================
# 一、给 FeatureDispatcher 的接口
# ============================================================================
def getTimerBlocks(self):
"""返回本功能的所有"计时源方块"集合(tick 路由用)"""
return set(self.iceLollyTimers.keys())
def handleBlockEntityTick(self, args):
"""由 FeatureDispatcher 转发的计时方块 tick 入口"""
dim = args.get("dimension")
pos = (args.get("posX"), args.get("posY"), args.get("posZ"))
blockName = args.get("blockName", "")
self._tickProcess(dim, pos, blockName)
# ============================================================================
# 二、定型计时启动(YinliaoService 手持冰块右键未定型后转发调用)
# ============================================================================
def startTimer(self, dim, pos, blockName, playerId=None):
"""
启动定型计时:把 开始时间戳+时长 写入方块实体数据。
返回 True = 本次真正启动(右键服务据此消耗冰块)
返回 False = 已在定型中 / 无规则(不重复消耗)
"""
recipe = self.iceLollyTimers.get(blockName)
if not recipe:
return False
# 去重:如果该方块已经在本规则计时中,不重置(保持原剩余时间)
try:
data = blockEntityDataComp.GetBlockEntityData(dim, pos)
if data and data["rtdd_tid"] == blockName:
return False
except Exception:
pass
self._writeTimerData(dim, pos, recipe, playerId)
return True
# ============================================================================
# 三、tick 检查与到点转换
# ============================================================================
def _tickProcess(self, dim, pos, blockName):
"""
tick 主逻辑:
1) 按事件自带的当前方块名判断是否计时源
2) 是计时源但没有计时数据 → 只有 autoStart=True 才自动补写。
冰棍盒定型 autoStart=False:必须右键(放冰块)激活, 这里不补写
3) 有计时数据 → 到点执行转换(重启后时间戳照常生效)
注意:GetBlockEntityData 返回的对象不支持 .get(),统一用 [] 读取。
"""
try:
recipe = self.iceLollyTimers.get(blockName)
if not recipe:
return # 当前方块不是计时源
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
if blockData is None:
return
try:
tid = blockData["rtdd_tid"]
except Exception:
tid = ""
if tid != blockName:
# 冰棍盒必须右键(放冰块)激活定型: autoStart=False 时不自动补写计时
if recipe.get("autoStart", True):
self._writeTimerData(dim, pos, recipe, None)
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)
self._clearTimerData(dim, pos)
if recipe.get("doneTip"):
self._notifyIfOnline(data["rtdd_tp"], recipe["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", 60)
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):
"""替换指定位置的方块(计时回调时没有玩家, 用 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)
@@ -48,7 +48,7 @@
"netease:block_entity": { "netease:block_entity": {
"client_tick": false, "client_tick": false,
"movable": false, "movable": false,
"tick": false "tick": true
} }
} }
} }
@@ -4,7 +4,7 @@
"description": { "description": {
"identifier": "rtdd:melonicecreamplace", "identifier": "rtdd:melonicecreamplace",
"register_to_creative_menu": true, "register_to_creative_menu": true,
"category": "xy_jinyong" "category": "xy_drink"
}, },
"components": { "components": {
"netease:aabb": { "netease:aabb": {
@@ -27,6 +27,7 @@
"rtdd:grapeicecreamplace", "rtdd:grapeicecreamplace",
"rtdd:orangeicecreamplace", "rtdd:orangeicecreamplace",
"rtdd:peachicecreamplace", "rtdd:peachicecreamplace",
"rtdd:melonicecreamplace",
"rtdd:apple_popsicleplace", "rtdd:apple_popsicleplace",
"rtdd:grape_popsicleplace", "rtdd:grape_popsicleplace",
"rtdd:melon_popsicleplace", "rtdd:melon_popsicleplace",
@@ -79,7 +79,8 @@
"icon": "rtdd:brownrice", "icon": "rtdd:brownrice",
"list": [ "list": [
"rtdd:brownrice", "rtdd:brownrice",
"rtdd:bread_pile" "rtdd:bread_pile",
"rtdd:soybean"
] ]
}, },
{ {
@@ -0,0 +1,18 @@
{
"format_version": "1.10",
"minecraft:item": {
"description": {
"identifier": "rtdd:soybean",
"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"
}
}
}
}
+2 -1
View File
@@ -976,7 +976,7 @@ tile.rtdd:cookingcrab_2.name=炒锅_35(炒蟹_熟)
tile.rtdd:violet_beer_kegs_0.name=满酒桶_17(满酒桶_0) tile.rtdd:violet_beer_kegs_0.name=满酒桶_17(满酒桶_0)
tile.rtdd:cookingcrab_1.name=炒锅_34(炒蟹_生) tile.rtdd:cookingcrab_1.name=炒锅_34(炒蟹_生)
tile.rtdd:cookingbeeftenderloin.name=炒锅_23(胡萝卜牛里脊_生) tile.rtdd:cookingbeeftenderloin.name=炒锅_23(胡萝卜牛里脊_生)
tile.rtdd:melonicecreamplace.name=西瓜冰棍_2(西瓜冰棍)禁用 tile.rtdd:melonicecreamplace.name=西瓜冰沙
tile.rtdd:b_72.name=吊锅_18 tile.rtdd:b_72.name=吊锅_18
tile.rtdd:b_71.name=吊锅_17 tile.rtdd:b_71.name=吊锅_17
tile.rtdd:apple_popsicleplace.name=苹果冰棍 tile.rtdd:apple_popsicleplace.name=苹果冰棍
@@ -1235,3 +1235,4 @@ item.rtdd:scallions.name=葱
item.rtdd:grape.name=葡萄 item.rtdd:grape.name=葡萄
item.rtdd:peach.name=桃子 item.rtdd:peach.name=桃子
item.rtdd:orange.name=橙子 item.rtdd:orange.name=橙子
item.rtdd:soybean.name=黄豆
@@ -99,6 +99,9 @@
}, },
"orange": { "orange": {
"textures": "textures/items/ju" "textures": "textures/items/ju"
},
"soybean": {
"textures": "textures/items/soybean"
} }
} }
} }
@@ -28,3 +28,5 @@
- 面包盘 plate_20: 手持 原版面包 minecraft:bread - 面包盘 plate_20: 手持 原版面包 minecraft:bread
- 曲奇盘 plate_21: 手持 原版曲奇 minecraft:cookie - 曲奇盘 plate_21: 手持 原版曲奇 minecraft:cookie
- 熟鸡肉盘 plate_4: 手持 原版熟鸡肉 minecraft:cooked_chicken - 熟鸡肉盘 plate_4: 手持 原版熟鸡肉 minecraft:cooked_chicken
todo 转换后也都可以吃 (后续实现)
+73 -73
View File
@@ -3,105 +3,105 @@
======================================== ========================================
说明: 说明:
- 分为3个分组: 果汁·榨汁饮品 / 冰棍 / 冰棍盒·炒冰盒 - 分为3个分组: 果汁·榨汁饮品 / 冰棍 / 冰棍盒·炒冰盒
- 榨汁为逐级操作,冰棍/冰沙为放入水果+冷冻 - 榨汁为逐级右键操作; 冰棍/冰沙为放入水果+计时冷冻
- 【待确认】表示该步骤所需的具体操作尚未最终确定 - 【待确认】表示该步骤所需的具体操作尚未最终确定
======================================== ========================================
一、果汁·榨汁饮品(榨汁机系列) 一、果汁·榨汁饮品(榨汁机系列) —— 全流程右键转换, 果汁由第一步放入的水果决定
======================================== ========================================
机制说明(按批注重写):
不再用"玻璃杯+水果右键=对应果汁"。
榨汁机 空 → 放入水果 → 榨汁中1 → 榨汁中2 → 榨汁中3 → 完成 全程右键转换;
第一步放什么水果, 榨汁完成取出对应的果汁。
todo 功能实现的时候需要考虑 玩家退出游戏后 我的步骤已经到了 榨汁2->榨汁3 的过程中 那么我重新进入游戏后 我获得的果汁 还是 之前第一步 空 到放入水果这一步 记录的水果类型
1. 榨汁机(空) -> 榨汁机(放入水果) 1. 榨汁机(空) -> 榨汁机(放入水果)
转换条件:[手持 水果(食材分类) 右键 进行转换,消耗1个] 转换条件:[手持 水果(食材分类) 右键 进行转换, 消耗1个]
机制: 手持水果右键空榨汁机,消耗1个,放入水果 机制: 手持水果右键空榨汁机, 消耗1个, 放入水果并记录水果类型(决定最终果汁)
2. 榨汁机(放入水果) -> 榨汁机(榨汁中1) -> 榨汁机(榨汁中2) -> 榨汁机(榨汁中3) 2. 榨汁机(放入水果) -> 榨汁机(榨汁中1)
转换条件:[右键/操作依次转换] 转换条件:[右键 进行转换]
机制: 右键启动榨汁,逐级运转 机制: 右键启动榨汁, 进入榨汁中1
3. 榨汁机(榨汁中3) -> 榨汁机(完成) 3. 榨汁机(榨汁中1) -> 榨汁机(榨汁中2)
转换条件:[计时自动转换] 转换条件:[右键 进行转换]
机制: 计时约60秒榨汁完成 机制: 右键继续榨汁, 进入榨汁中2
4. 榨汁机(完成) -> 玻璃杯 4. 榨汁机(榨汁中2) -> 榨汁机(榨汁中3)
转换条件:[右键 进行转换,取出果汁] 转换条件:[右键 进行转换]
机制: 空手右键,获得玻璃杯(果汁) 机制: 右键继续榨汁, 进入榨汁中3
5. 玻璃杯 + 西瓜 -> 西瓜汁 5. 榨汁机(榨汁中3) -> 榨汁机(完成)
转换条件:[手持 西瓜 右键 进行转换,消耗1个] 转换条件:[右键 进行转换]
机制: 手持西瓜右键玻璃杯,榨成西瓜汁 机制: 右键榨汁完成
6. 玻璃杯 + 橙子 -> 橙汁 6. 榨汁机(完成) -> 对应果汁
转换条件:[手持 橙子 右键 进行转换,消耗1个] 转换条件:[右键 进行转换, 取出果汁]
机制: 手持橙子右键玻璃杯,榨成橙汁 机制: 空手右键, 获得 第一步放入水果 对应的果汁(按记录的水果类型)
- 西瓜 -> 西瓜汁 fruitjuice_1
7. 玻璃杯 + 苹果 -> 苹果汁 - 橙子 -> 橙汁 fruitjuice_2
转换条件:[手持 苹果 右键 进行转换,消耗1个] - 苹果 -> 苹果汁 fruitjuice_3
机制: 手持苹果右键玻璃杯,榨成苹果汁 - 桃子 -> 桃子汁 fruitjuice_4
- 葡萄 -> 葡萄汁 fruitjuice_6
8. 玻璃杯 + 桃子 -> 桃子汁 - 黄豆 -> 豆浆 fruitjuice_7
转换条件:[手持 桃子 右键 进行转换,消耗1个] (玻璃杯 fruitjuice_0 仅为成品容器, 不再作为"玻璃杯+水果"的中转)
机制: 手持桃子右键玻璃杯,榨成桃子汁
9. 玻璃杯 + 葡萄 -> 葡萄汁
转换条件:[手持 葡萄 右键 进行转换,消耗1个]
机制: 手持葡萄右键玻璃杯,榨成葡萄汁
10. 玻璃杯 + 黄豆 -> 豆浆
转换条件:[手持 黄豆 右键 进行转换,消耗1个]
机制: 手持黄豆右键玻璃杯,磨成豆浆
======================================== ========================================
二、冰棍(冰棍盒/炒冰盒系列) 二、冰棍(冰棍盒系列)
======================================== ========================================
11. 冰棍盒(冰棍未定型) -> 冰棍盒(冰棍定型) 7. 冰棍盒(冰棍未定型) -> 冰棍盒(冰棍定型)
转换条件:[放入冰柜/炒冰盒,计时冷冻] 转换条件:[手持 原版冰块[广义的冰] 右键 进行转换, 消耗1个]
机制: 放入冰柜冷冻,计时约60秒定型 机制: 手持冰块右键冰棍盒(未定型), 消耗1个, 计时约30秒定型
12. 冰棍盒(冰棍定型) -> 冰棍盒(苹果冰棍) / 冰棍盒(桃子冰棍) / 冰棍盒(橙子冰棍) / 冰棍盒(葡萄冰棍) / 冰棍盒(西瓜冰棍) 8. 冰棍盒(冰棍定型) -> 冰棍盒(苹果/桃子/橙子/葡萄/西瓜冰棍)
转换条件:[放入对应水果 右键 进行转换,消耗1个] 转换条件:[手持 对应水果 右键 进行转换, 消耗2个]
机制: 手持对应水果右键冰棍盒,消耗1个,制成该口味冰棍 机制: 手持对应水果右键冰棍盒(定型), 消耗2个, 制成该口味冰棍
13. 冰棍盒(各水果) -> 对应冰棍(苹果/葡萄/西瓜/橙子/桃子冰棍) 9. 冰棍盒(各水果) -> 对应冰棍 + 还原 冰棍盒(未定型)
转换条件:[右键 进行转换,取出冰棍] 转换条件:[右键 进行转换, 取出冰棍]
机制: 空手右键,取出冰棍 机制: 空手右键, 取出2根对应水果冰棍(苹果/桃子/橙子/葡萄/西瓜冰棍),
冰棍盒还原为 冰棍盒(未定型)
(按批注: 各水果冰棍盒取完冰棍后应还原成未定型)
14. 冰棍堆(橙子+西瓜) -> 冰棍堆2(橙子+西瓜+苹果) 10. 冰棍堆 组合逻辑(同厨具架: 与放入顺序无关, 后放先取)
转换条件:[手持 冰棍 右键 进行堆叠] 转换条件:[手持 冰棍 右键 冰棍堆 进行堆叠, 消耗1根]
机制: 手持冰棍右键,堆叠加入苹果冰棍 机制: 手持冰棍右键冰棍堆, 消耗1根, 按组合成型(先放A再放B == 先放B再放A)
- 橙子+西瓜 -> 冰棍堆(橙子+西瓜) popsiclepile_2
15. 冰棍堆2(橙子+西瓜+苹果) -> 冰棍堆(橙子+西瓜) - 橙子+西瓜+苹果 -> 冰棍堆2(橙子+西瓜+苹果) popsiclepile_3
转换条件:[右键 进行转换,取走苹果冰棍] 取回: 空手右键, 后放先取(LIFO), 取走最后放入的那根冰棍, 形态退回剩余组合
机制: 空手右键,取走1根苹果冰棍(数量递减 3->2) (其他口味组合暂无对应堆方块, 不支持堆叠)
======================================== ========================================
三、炒冰盒·冰沙 三、炒冰盒·冰沙
======================================== ========================================
15. 炒冰盒(空) -> 炒冰盒(放冰) 11. 炒冰盒(空) -> 炒冰盒(放冰)
转换条件:[手持 冰块 右键 进行转换,消耗1个] 转换条件:[手持 原版冰块[广义的冰] 右键 进行转换, 消耗1个]
机制: 手持冰块右键,消耗1个,放入冰块 机制: 手持冰块右键炒冰盒(空), 消耗1个, 放入冰块
16. 炒冰盒(放冰) + 苹果 -> 炒冰盒_1(苹果炒冰盒) 12. 炒冰盒(放冰) + 苹果 -> 苹果炒冰盒
转换条件:[手持 苹果 右键 进行转换,消耗1个] 转换条件:[手持 苹果 右键 进行转换, 消耗1个]
机制: 手持苹果右键,消耗1个,制成苹果炒冰 机制: 手持苹果右键炒冰盒(放冰), 消耗1个, 制成苹果炒冰
17. 炒冰盒(放冰) + 葡萄 -> 炒冰盒_3(葡萄炒冰盒) 13. 炒冰盒(放冰) + 葡萄 -> 葡萄炒冰盒
转换条件:[手持 葡萄 右键 进行转换,消耗1个] 转换条件:[手持 葡萄 右键 进行转换, 消耗1个]
机制: 手持葡萄右键,消耗1个,制成葡萄炒冰 机制: 手持葡萄右键, 消耗1个, 制成葡萄炒冰
18. 炒冰盒(放冰) + 西瓜 -> 炒冰盒_4(西瓜炒冰盒) 14. 炒冰盒(放冰) + 西瓜 -> 西瓜炒冰盒
转换条件:[手持 西瓜 右键 进行转换,消耗1个] 转换条件:[手持 西瓜 右键 进行转换, 消耗1个]
机制: 手持西瓜右键,消耗1个,制成西瓜炒冰 机制: 手持西瓜右键, 消耗1个, 制成西瓜炒冰
19. 炒冰盒(放冰) + 橘子 -> 炒冰盒_5(橘子炒冰盒) 15. 炒冰盒(放冰) + 橘子 -> 橘子炒冰盒
转换条件:[手持 橘子 右键 进行转换,消耗1个] 转换条件:[手持 橘子 右键 进行转换, 消耗1个]
机制: 手持橘子右键,消耗1个,制成橘子炒冰 机制: 手持橘子右键, 消耗1个, 制成橘子炒冰
20. 炒冰盒(放冰) + 桃子 -> 炒冰盒_4(桃子炒冰盒) 16. 炒冰盒(放冰) + 桃子 -> 桃子炒冰盒
转换条件:[手持 桃子 右键 进行转换,消耗1个] 转换条件:[手持 桃子 右键 进行转换, 消耗1个]
机制: 手持桃子右键,消耗1个,制成桃子炒冰 机制: 手持桃子右键, 消耗1个, 制成桃子炒冰
21. 炒冰盒(各水果) -> 对应冰沙(苹果/葡萄/橘子/桃子冰沙) 17. 炒冰盒(各水果) -> 对应冰沙 + 还原 炒冰盒(空)
转换条件:[右键 进行转换,取出冰沙] 转换条件:[右键 进行转换, 取出冰沙]
机制: 空手右键,取出冰沙 机制: 空手右键, 取出1个对应冰沙(苹果/葡萄/橘子/桃子冰沙),
炒冰盒还原为 炒冰盒(空)
(按批注: 各水果炒冰盒取完冰沙后应还原成空)
======================================== ========================================
四、成品方块清单 四、成品方块清单
+38
View File
@@ -253,3 +253,41 @@ FurnitureData+FurnitureService 实现, 注册到 FeatureDispatcher)
- 附魔书/药水效果在掉落物中保留(取回时重建, 词条/效果不丢) [已实现] - 附魔书/药水效果在掉落物中保留(取回时重建, 词条/效果不丢) [已实现]
- 与售卖盒区分: 售卖盒破坏重放仍可右键取物; 书柜这类破坏后记录随方块消失, 重放取不到 [已实现] - 与售卖盒区分: 售卖盒破坏重放仍可右键取物; 书柜这类破坏后记录随方块消失, 重放取不到 [已实现]
########################################
【乡野饮料】(xy_drink) 2026-09-04
########################################
实现于 xiangyemod/yinliao (YinliaoData + YinliaoService + YinliaoTimerService, 注册到 FeatureDispatcher)
一、榨汁机 (果汁·榨汁饮品, juicer 系列, 全流程右键)
- 空(juicer) + 水果(消耗1) → 放入水果(juicer_0), 并把水果类型写入方块实体数据(rtdd_jf) [已实现]
- 放入水果 → 榨汁中1 → 榨汁中2 → 榨汁中3 → 完成: 右键逐级推进(每级0.5s冷却) [已实现]
- 完成(juicer_4) 空手右键 → 掉落对应果汁 + 还原空(juicer) [已实现]
- 果汁由第一步放入的水果决定: 西瓜片→西瓜汁(fruitjuice_1)/橙子→橙汁(_2)/苹果→苹果汁(_3)/
桃子→桃子汁(_4)/葡萄→葡萄汁(_6)/黄豆→豆浆(_7) [已实现]
- 持久化: 每次换态 SetBlockNew 会清空方块实体, 都"先读旧→替换→写回新", 玩家在 榨汁2→榨汁3
过程中退出游戏, 重进后取出的果汁仍是第一步放入的水果类型 [已实现]
- 黄豆(rtdd:soybean) 物品已从Java版迁入(物品/纹理/名称齐全) [已实现]
二、冰棍盒 (冰棍, icelollybox 系列)
- 未定型(icelollybox_1) + 冰块(消耗1, 原版冰/冰砖/蓝冰/霜冰) → 启动30秒定型计时
(autoStart=False, 右键激活; 已在定型中不重复消耗) → 定型(icelollybox_2) [已实现]
- 定型 + 对应水果(消耗2) → 各口味冰棍盒(icelollyboxb 橙子/c 苹果/d 桃子/e 葡萄/f 西瓜) [已实现]
- 各口味冰棍盒 空手右键 → 掉落2根对应冰棍 + 还原未定型 [已实现]
- 定型计时用绝对时间戳写方块实体数据, 退出重进继续计时 [已实现]
三、冰棍堆 (组合, popsiclepile_2/3)
- 冰棍堆(橙子+西瓜 popsiclepile_2) + 苹果冰棍(消耗1) → 冰棍堆2(橙子+西瓜+苹果 popsiclepile_3) [已实现]
- 冰棍堆2 空手右键 → 掉落苹果冰棍(后放先取 LIFO) + 退回冰棍堆 [已实现]
四、炒冰盒 (冰棍盒·炒冰盒, frytheicebox 系列)
- 空(frytheicebox) + 冰块(消耗1) → 放冰(frytheicebox_1) [已实现]
- 放冰 + 对应水果(消耗1) → 各口味炒冰盒(苹果/葡萄/西瓜/橘子/桃子) [已实现]
- 各口味炒冰盒 空手右键 → 掉落1个对应冰沙 + 还原空 [已实现]
- 五种口味冰沙齐全: 西瓜冰沙(melonicecreamplace) 已从禁用移回乡野饮料, 正常取出 [已实现]
五、分类/分组调整
- 西瓜冰沙(melonicecreamplace): 分类 禁用(xy_jinyong) → 乡野饮料(xy_drink),
名称 西瓜冰棍_2(西瓜冰棍)禁用 → 西瓜冰沙, 放入 冰棍(drink_03_popsicle) 分组 [已调整]
- 黄豆(rtdd:soybean): 归乡野食材(xy_ingredients), 放入 谷物·米面(ingr_08_grain) 分组 [已调整]
+25
View File
@@ -106,3 +106,28 @@
(乡野食物转化关系.txt 一、章鱼小丸子 todo 已标注) (乡野食物转化关系.txt 一、章鱼小丸子 todo 已标注)
(原版食物-成品盘转换 已实现于 xiangyemod/yuanbanfangzhi, 见 全部功能做了什么.txt) (原版食物-成品盘转换 已实现于 xiangyemod/yuanbanfangzhi, 见 全部功能做了什么.txt)
二、原版食物-成品盘可食用
- 方块: plate_16(南瓜派盘)/plate_17(生鱼片)/plate_18(西瓜盘)/plate_20(面包盘)/
plate_21(曲奇盘)/plate_4(熟鸡肉盘)
- 设想: 盘子转换(装盘)后的成品盘 都可以直接右键食用
- 未做原因: 属于吃的东西, 食用部分由统一的"总食用功能"后续实现
(乡野食物转化关系.txt 末尾 todo 已标注 "转换后也都可以吃")
########################################
【乡野饮料】(xy_drink)
########################################
(主要转换已实现于 xiangyemod/yinliao, 见 全部功能做了什么.txt)
一、果汁/豆浆/冰棍/冰沙 的食用功能
- 成品: fruitjuice_1~7(西瓜汁/橙汁/苹果汁/桃子汁/葡萄汁/豆浆)、各 popsicleplace(冰棍)、
各 icecreamplace(冰沙, 含西瓜冰沙)
- 设想: 这些饮料成品都可以直接右键食用
- 未做原因: 属于吃的东西, 食用部分由统一的"总食用功能"后续实现
(乡野饮料转化关系.txt 未标注具体食用规则)
二、冰棍堆 其他口味组合
- 方块: popsiclepile_2/3(冰棍堆)
- 说明: 目前只支持 橙子+西瓜(堆) → 橙子+西瓜+苹果(堆2), 即只有"插苹果冰棍"一种组合
- 未做原因: 其他口味组合暂无对应堆方块, 不支持堆叠 (转化文档已注明)