diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/modMain.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/modMain.py index b08c30a..4c50ac2 100644 --- a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/modMain.py +++ b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/modMain.py @@ -10,6 +10,8 @@ from xiangyemod.shouban.service.ShoubanService import ShoubanService from xiangyemod.shichangmaoyi.service.MarketService import MarketService from xiangyemod.yuanbanfangzhi.service.YuanbanService import YuanbanService 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(): pass diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/dongwuyangzhi/service/AnimalTimerService.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/dongwuyangzhi/service/AnimalTimerService.py index a8a4c42..7cdb471 100644 --- a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/dongwuyangzhi/service/AnimalTimerService.py +++ b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/dongwuyangzhi/service/AnimalTimerService.py @@ -111,21 +111,23 @@ class AnimalTimerService(BlueFoxServerSystem): 启动计时:把 计时标识+开始时间戳+时长 写入方块实体数据。 - 已处于本规则计时中的方块不会重复重置时间(保持原剩余时间) - 动物计时 autoStart 默认 True:放置/填充后自动开始(tick 也会自动补写) + 返回 True=找到规则并处理(含已计时中);False=不属于本计时功能 """ recipe = self.animalTimers.get(blockName) if not recipe: - return + return False # 去重:如果该方块已经在本规则计时中,不重置(保持原剩余时间) try: data = blockEntityDataComp.GetBlockEntityData(dim, pos) if data and data["rtdd_tid"] == blockName: - return + return True except Exception: pass self._writeTimerData(dim, pos, recipe, playerId) # 计时开始提示 if playerId and recipe.get("startTip"): comp.CreateMsg(playerId).NotifyOneMessage(playerId, recipe["startTip"]) + return True # ============================================================================ # 三、tick 检查与到点转换 diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/jiaju/service/demo.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/jiaju/service/demo.py new file mode 100644 index 0000000..1eafce1 --- /dev/null +++ b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/jiaju/service/demo.py @@ -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) \ No newline at end of file diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/service/FeatureDispatcher.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/service/FeatureDispatcher.py index 36b65e8..14976e5 100644 --- a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/service/FeatureDispatcher.py +++ b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/service/FeatureDispatcher.py @@ -106,15 +106,17 @@ class FeatureDispatcher(BlueFoxServerSystem): 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) if not service: return False - service.startTimer(dim, pos, blockName, playerId) - return True + return service.startTimer(dim, pos, blockName, playerId) def __init__(self, namespace, systemName): """ diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/shaokao/service/BarbecueTimerService.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/shaokao/service/BarbecueTimerService.py index 5700cb1..223c137 100644 --- a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/shaokao/service/BarbecueTimerService.py +++ b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/shaokao/service/BarbecueTimerService.py @@ -118,28 +118,30 @@ class BarbecueTimerService(BlueFoxServerSystem): 启动烤制计时:先检查下方篝火,通过后写 开始时间戳+时长 到方块实体数据。 - 启动前检查:下方不是篝火 → 拒绝并提示(烧烤必须右键激活,不会自动烤) - 已在本规则计时中的方块不会重复重置时间(保持原剩余时间) + 返回 True=找到规则并处理(含已计时中/篝火检查失败);False=不属于本计时功能 """ recipe = self.barbecueTimers.get(blockName) if not recipe: - return + return False # 启动前检查下方篝火(烧烤必须下方是篝火才能开始烤制) if not self.checkBelow(recipe, dim, pos): if playerId: comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§c支杆下方需要放一个篝火(营火)才能烤制!") - return + return True # 去重:如果该方块已经在本规则计时中,不重置(保持原剩余时间) try: data = blockEntityDataComp.GetBlockEntityData(dim, pos) if data and data["rtdd_tid"] == blockName: - return + return True except Exception: pass self._writeTimerData(dim, pos, recipe, playerId) # 烤制开始提示 if playerId and recipe.get("startTip"): comp.CreateMsg(playerId).NotifyOneMessage(playerId, recipe["startTip"]) + return True # ============================================================================ # 三、tick 检查与到点转换 diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/yinliao/YinliaoData.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/yinliao/YinliaoData.py new file mode 100644 index 0000000..9ebef35 --- /dev/null +++ b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/yinliao/YinliaoData.py @@ -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()} diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/yinliao/__init__.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/yinliao/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/yinliao/client/YinliaoClient.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/yinliao/client/YinliaoClient.py new file mode 100644 index 0000000..c1ce936 --- /dev/null +++ b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/yinliao/client/YinliaoClient.py @@ -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 =====" diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/yinliao/client/__init__.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/yinliao/client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/yinliao/service/YinliaoService.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/yinliao/service/YinliaoService.py new file mode 100644 index 0000000..2da257d --- /dev/null +++ b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/yinliao/service/YinliaoService.py @@ -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 diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/yinliao/service/YinliaoTimerService.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/yinliao/service/YinliaoTimerService.py new file mode 100644 index 0000000..1805897 --- /dev/null +++ b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/yinliao/service/YinliaoTimerService.py @@ -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) diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/yinliao/service/__init__.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/yinliao/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/behavior_pack_v8jz0zSR/netease_blocks/icelollybox_1.json b/behavior_pack_v8jz0zSR/netease_blocks/icelollybox_1.json index 05eceab..6076ba2 100644 --- a/behavior_pack_v8jz0zSR/netease_blocks/icelollybox_1.json +++ b/behavior_pack_v8jz0zSR/netease_blocks/icelollybox_1.json @@ -48,7 +48,7 @@ "netease:block_entity": { "client_tick": false, "movable": false, - "tick": false + "tick": true } } } diff --git a/behavior_pack_v8jz0zSR/netease_blocks/melonicecreamplace.json b/behavior_pack_v8jz0zSR/netease_blocks/melonicecreamplace.json index f643b3e..3924d1e 100644 --- a/behavior_pack_v8jz0zSR/netease_blocks/melonicecreamplace.json +++ b/behavior_pack_v8jz0zSR/netease_blocks/melonicecreamplace.json @@ -4,7 +4,7 @@ "description": { "identifier": "rtdd:melonicecreamplace", "register_to_creative_menu": true, - "category": "xy_jinyong" + "category": "xy_drink" }, "components": { "netease:aabb": { diff --git a/behavior_pack_v8jz0zSR/netease_group/drink_groups.json b/behavior_pack_v8jz0zSR/netease_group/drink_groups.json index 3c3cfe3..fe9e922 100644 --- a/behavior_pack_v8jz0zSR/netease_group/drink_groups.json +++ b/behavior_pack_v8jz0zSR/netease_group/drink_groups.json @@ -27,6 +27,7 @@ "rtdd:grapeicecreamplace", "rtdd:orangeicecreamplace", "rtdd:peachicecreamplace", + "rtdd:melonicecreamplace", "rtdd:apple_popsicleplace", "rtdd:grape_popsicleplace", "rtdd:melon_popsicleplace", diff --git a/behavior_pack_v8jz0zSR/netease_group/ingredients_groups.json b/behavior_pack_v8jz0zSR/netease_group/ingredients_groups.json index 1a392b9..0c32bc1 100644 --- a/behavior_pack_v8jz0zSR/netease_group/ingredients_groups.json +++ b/behavior_pack_v8jz0zSR/netease_group/ingredients_groups.json @@ -79,7 +79,8 @@ "icon": "rtdd:brownrice", "list": [ "rtdd:brownrice", - "rtdd:bread_pile" + "rtdd:bread_pile", + "rtdd:soybean" ] }, { diff --git a/behavior_pack_v8jz0zSR/netease_items_beh/rtdd_soybean.json b/behavior_pack_v8jz0zSR/netease_items_beh/rtdd_soybean.json new file mode 100644 index 0000000..bfb6300 --- /dev/null +++ b/behavior_pack_v8jz0zSR/netease_items_beh/rtdd_soybean.json @@ -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" + } + } + } +} diff --git a/resource_pack_Are13Lvd/texts/zh_CN.lang b/resource_pack_Are13Lvd/texts/zh_CN.lang index c80d32a..42fb612 100644 --- a/resource_pack_Are13Lvd/texts/zh_CN.lang +++ b/resource_pack_Are13Lvd/texts/zh_CN.lang @@ -976,7 +976,7 @@ tile.rtdd:cookingcrab_2.name=炒锅_35(炒蟹_熟) tile.rtdd:violet_beer_kegs_0.name=满酒桶_17(满酒桶_0) tile.rtdd:cookingcrab_1.name=炒锅_34(炒蟹_生) tile.rtdd:cookingbeeftenderloin.name=炒锅_23(胡萝卜牛里脊_生) -tile.rtdd:melonicecreamplace.name=西瓜冰棍_2(西瓜冰棍)禁用 +tile.rtdd:melonicecreamplace.name=西瓜冰沙 tile.rtdd:b_72.name=吊锅_18 tile.rtdd:b_71.name=吊锅_17 tile.rtdd:apple_popsicleplace.name=苹果冰棍 @@ -1234,4 +1234,5 @@ item.rtdd:costillavacaes.name=牛肋 item.rtdd:scallions.name=葱 item.rtdd:grape.name=葡萄 item.rtdd:peach.name=桃子 -item.rtdd:orange.name=橙子 \ No newline at end of file +item.rtdd:orange.name=橙子 +item.rtdd:soybean.name=黄豆 \ No newline at end of file diff --git a/resource_pack_Are13Lvd/textures/item_texture.json b/resource_pack_Are13Lvd/textures/item_texture.json index 888d0ad..668d0dc 100644 --- a/resource_pack_Are13Lvd/textures/item_texture.json +++ b/resource_pack_Are13Lvd/textures/item_texture.json @@ -99,6 +99,9 @@ }, "orange": { "textures": "textures/items/ju" + }, + "soybean": { + "textures": "textures/items/soybean" } } } \ No newline at end of file diff --git a/tuilun/乡野食物转化关系.txt b/tuilun/ok/乡野食物转化关系.txt similarity index 97% rename from tuilun/乡野食物转化关系.txt rename to tuilun/ok/乡野食物转化关系.txt index 0605cd3..ce06607 100644 --- a/tuilun/乡野食物转化关系.txt +++ b/tuilun/ok/乡野食物转化关系.txt @@ -28,3 +28,5 @@ - 面包盘 plate_20: 手持 原版面包 minecraft:bread - 曲奇盘 plate_21: 手持 原版曲奇 minecraft:cookie - 熟鸡肉盘 plate_4: 手持 原版熟鸡肉 minecraft:cooked_chicken + +todo 转换后也都可以吃 (后续实现) diff --git a/tuilun/乡野饮料转化关系.txt b/tuilun/乡野饮料转化关系.txt index 86263c7..b9551eb 100644 --- a/tuilun/乡野饮料转化关系.txt +++ b/tuilun/乡野饮料转化关系.txt @@ -3,105 +3,105 @@ ======================================== 说明: - 分为3个分组: 果汁·榨汁饮品 / 冰棍 / 冰棍盒·炒冰盒 -- 榨汁为逐级操作,冰棍/冰沙为放入水果+冷冻 +- 榨汁为逐级右键操作; 冰棍/冰沙为放入水果+计时冷冻 - 【待确认】表示该步骤所需的具体操作尚未最终确定 ======================================== -一、果汁·榨汁饮品(榨汁机系列) +一、果汁·榨汁饮品(榨汁机系列) —— 全流程右键转换, 果汁由第一步放入的水果决定 ======================================== +机制说明(按批注重写): + 不再用"玻璃杯+水果右键=对应果汁"。 + 榨汁机 空 → 放入水果 → 榨汁中1 → 榨汁中2 → 榨汁中3 → 完成 全程右键转换; + 第一步放什么水果, 榨汁完成取出对应的果汁。 +todo 功能实现的时候需要考虑 玩家退出游戏后 我的步骤已经到了 榨汁2->榨汁3 的过程中 那么我重新进入游戏后 我获得的果汁 还是 之前第一步 空 到放入水果这一步 记录的水果类型 1. 榨汁机(空) -> 榨汁机(放入水果) -转换条件:[手持 水果(食材分类) 右键 进行转换,消耗1个] -机制: 手持水果右键空榨汁机,消耗1个,放入水果 +转换条件:[手持 水果(食材分类) 右键 进行转换, 消耗1个] +机制: 手持水果右键空榨汁机, 消耗1个, 放入水果并记录水果类型(决定最终果汁) -2. 榨汁机(放入水果) -> 榨汁机(榨汁中1) -> 榨汁机(榨汁中2) -> 榨汁机(榨汁中3) -转换条件:[右键/操作依次转换] -机制: 右键启动榨汁,逐级运转 +2. 榨汁机(放入水果) -> 榨汁机(榨汁中1) +转换条件:[右键 进行转换] +机制: 右键启动榨汁, 进入榨汁中1 -3. 榨汁机(榨汁中3) -> 榨汁机(完成) -转换条件:[计时自动转换] -机制: 计时约60秒榨汁完成 +3. 榨汁机(榨汁中1) -> 榨汁机(榨汁中2) +转换条件:[右键 进行转换] +机制: 右键继续榨汁, 进入榨汁中2 -4. 榨汁机(完成) -> 玻璃杯 -转换条件:[右键 进行转换,取出果汁] -机制: 空手右键,获得玻璃杯(果汁) +4. 榨汁机(榨汁中2) -> 榨汁机(榨汁中3) +转换条件:[右键 进行转换] +机制: 右键继续榨汁, 进入榨汁中3 -5. 玻璃杯 + 西瓜 -> 西瓜汁 -转换条件:[手持 西瓜 右键 进行转换,消耗1个] -机制: 手持西瓜右键玻璃杯,榨成西瓜汁 +5. 榨汁机(榨汁中3) -> 榨汁机(完成) +转换条件:[右键 进行转换] +机制: 右键榨汁完成 -6. 玻璃杯 + 橙子 -> 橙汁 -转换条件:[手持 橙子 右键 进行转换,消耗1个] -机制: 手持橙子右键玻璃杯,榨成橙汁 - -7. 玻璃杯 + 苹果 -> 苹果汁 -转换条件:[手持 苹果 右键 进行转换,消耗1个] -机制: 手持苹果右键玻璃杯,榨成苹果汁 - -8. 玻璃杯 + 桃子 -> 桃子汁 -转换条件:[手持 桃子 右键 进行转换,消耗1个] -机制: 手持桃子右键玻璃杯,榨成桃子汁 - -9. 玻璃杯 + 葡萄 -> 葡萄汁 -转换条件:[手持 葡萄 右键 进行转换,消耗1个] -机制: 手持葡萄右键玻璃杯,榨成葡萄汁 - -10. 玻璃杯 + 黄豆 -> 豆浆 -转换条件:[手持 黄豆 右键 进行转换,消耗1个] -机制: 手持黄豆右键玻璃杯,磨成豆浆 +6. 榨汁机(完成) -> 对应果汁 +转换条件:[右键 进行转换, 取出果汁] +机制: 空手右键, 获得 第一步放入水果 对应的果汁(按记录的水果类型) +- 西瓜 -> 西瓜汁 fruitjuice_1 +- 橙子 -> 橙汁 fruitjuice_2 +- 苹果 -> 苹果汁 fruitjuice_3 +- 桃子 -> 桃子汁 fruitjuice_4 +- 葡萄 -> 葡萄汁 fruitjuice_6 +- 黄豆 -> 豆浆 fruitjuice_7 +(玻璃杯 fruitjuice_0 仅为成品容器, 不再作为"玻璃杯+水果"的中转) ======================================== -二、冰棍(冰棍盒/炒冰盒系列) +二、冰棍(冰棍盒系列) ======================================== -11. 冰棍盒(冰棍未定型) -> 冰棍盒(冰棍定型) -转换条件:[放入冰柜/炒冰盒,计时冷冻] -机制: 放入冰柜冷冻,计时约60秒定型 +7. 冰棍盒(冰棍未定型) -> 冰棍盒(冰棍定型) +转换条件:[手持 原版冰块[广义的冰] 右键 进行转换, 消耗1个] +机制: 手持冰块右键冰棍盒(未定型), 消耗1个, 计时约30秒定型 -12. 冰棍盒(冰棍定型) -> 冰棍盒(苹果冰棍) / 冰棍盒(桃子冰棍) / 冰棍盒(橙子冰棍) / 冰棍盒(葡萄冰棍) / 冰棍盒(西瓜冰棍) -转换条件:[放入对应水果 右键 进行转换,消耗1个] -机制: 手持对应水果右键冰棍盒,消耗1个,制成该口味冰棍 +8. 冰棍盒(冰棍定型) -> 冰棍盒(苹果/桃子/橙子/葡萄/西瓜冰棍) +转换条件:[手持 对应水果 右键 进行转换, 消耗2个] +机制: 手持对应水果右键冰棍盒(定型), 消耗2个, 制成该口味冰棍 -13. 冰棍盒(各水果) -> 对应冰棍(苹果/葡萄/西瓜/橙子/桃子冰棍) -转换条件:[右键 进行转换,取出冰棍] -机制: 空手右键,取出冰棍 +9. 冰棍盒(各水果) -> 对应冰棍 + 还原 冰棍盒(未定型) +转换条件:[右键 进行转换, 取出冰棍] +机制: 空手右键, 取出2根对应水果冰棍(苹果/桃子/橙子/葡萄/西瓜冰棍), + 冰棍盒还原为 冰棍盒(未定型) + (按批注: 各水果冰棍盒取完冰棍后应还原成未定型) -14. 冰棍堆(橙子+西瓜) -> 冰棍堆2(橙子+西瓜+苹果) -转换条件:[手持 冰棍 右键 进行堆叠] -机制: 手持冰棍右键,堆叠加入苹果冰棍 - -15. 冰棍堆2(橙子+西瓜+苹果) -> 冰棍堆(橙子+西瓜) -转换条件:[右键 进行转换,取走苹果冰棍] -机制: 空手右键,取走1根苹果冰棍(数量递减 3->2) +10. 冰棍堆 组合逻辑(同厨具架: 与放入顺序无关, 后放先取) +转换条件:[手持 冰棍 右键 冰棍堆 进行堆叠, 消耗1根] +机制: 手持冰棍右键冰棍堆, 消耗1根, 按组合成型(先放A再放B == 先放B再放A) +- 橙子+西瓜 -> 冰棍堆(橙子+西瓜) popsiclepile_2 +- 橙子+西瓜+苹果 -> 冰棍堆2(橙子+西瓜+苹果) popsiclepile_3 +取回: 空手右键, 后放先取(LIFO), 取走最后放入的那根冰棍, 形态退回剩余组合 +(其他口味组合暂无对应堆方块, 不支持堆叠) ======================================== 三、炒冰盒·冰沙 ======================================== -15. 炒冰盒(空) -> 炒冰盒(放冰) -转换条件:[手持 冰块 右键 进行转换,消耗1个] -机制: 手持冰块右键,消耗1个,放入冰块 +11. 炒冰盒(空) -> 炒冰盒(放冰) +转换条件:[手持 原版冰块[广义的冰] 右键 进行转换, 消耗1个] +机制: 手持冰块右键炒冰盒(空), 消耗1个, 放入冰块 -16. 炒冰盒(放冰) + 苹果 -> 炒冰盒_1(苹果炒冰盒) -转换条件:[手持 苹果 右键 进行转换,消耗1个] -机制: 手持苹果右键,消耗1个,制成苹果炒冰 +12. 炒冰盒(放冰) + 苹果 -> 苹果炒冰盒 +转换条件:[手持 苹果 右键 进行转换, 消耗1个] +机制: 手持苹果右键炒冰盒(放冰), 消耗1个, 制成苹果炒冰 -17. 炒冰盒(放冰) + 葡萄 -> 炒冰盒_3(葡萄炒冰盒) -转换条件:[手持 葡萄 右键 进行转换,消耗1个] -机制: 手持葡萄右键,消耗1个,制成葡萄炒冰 +13. 炒冰盒(放冰) + 葡萄 -> 葡萄炒冰盒 +转换条件:[手持 葡萄 右键 进行转换, 消耗1个] +机制: 手持葡萄右键, 消耗1个, 制成葡萄炒冰 -18. 炒冰盒(放冰) + 西瓜 -> 炒冰盒_4(西瓜炒冰盒) -转换条件:[手持 西瓜 右键 进行转换,消耗1个] -机制: 手持西瓜右键,消耗1个,制成西瓜炒冰 +14. 炒冰盒(放冰) + 西瓜 -> 西瓜炒冰盒 +转换条件:[手持 西瓜 右键 进行转换, 消耗1个] +机制: 手持西瓜右键, 消耗1个, 制成西瓜炒冰 -19. 炒冰盒(放冰) + 橘子 -> 炒冰盒_5(橘子炒冰盒) -转换条件:[手持 橘子 右键 进行转换,消耗1个] -机制: 手持橘子右键,消耗1个,制成橘子炒冰 +15. 炒冰盒(放冰) + 橘子 -> 橘子炒冰盒 +转换条件:[手持 橘子 右键 进行转换, 消耗1个] +机制: 手持橘子右键, 消耗1个, 制成橘子炒冰 -20. 炒冰盒(放冰) + 桃子 -> 炒冰盒_4(桃子炒冰盒) -转换条件:[手持 桃子 右键 进行转换,消耗1个] -机制: 手持桃子右键,消耗1个,制成桃子炒冰 +16. 炒冰盒(放冰) + 桃子 -> 桃子炒冰盒 +转换条件:[手持 桃子 右键 进行转换, 消耗1个] +机制: 手持桃子右键, 消耗1个, 制成桃子炒冰 -21. 炒冰盒(各水果) -> 对应冰沙(苹果/葡萄/橘子/桃子冰沙) -转换条件:[右键 进行转换,取出冰沙] -机制: 空手右键,取出冰沙 +17. 炒冰盒(各水果) -> 对应冰沙 + 还原 炒冰盒(空) +转换条件:[右键 进行转换, 取出冰沙] +机制: 空手右键, 取出1个对应冰沙(苹果/葡萄/橘子/桃子冰沙), + 炒冰盒还原为 炒冰盒(空) + (按批注: 各水果炒冰盒取完冰沙后应还原成空) ======================================== 四、成品方块清单 diff --git a/tuilun/全部功能做了什么.txt b/tuilun/全部功能做了什么.txt index b66674f..d7c6e49 100644 --- a/tuilun/全部功能做了什么.txt +++ b/tuilun/全部功能做了什么.txt @@ -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) 分组 [已调整] + diff --git a/tuilun/全部功能没做什么.txt b/tuilun/全部功能没做什么.txt index e2e863c..44937d4 100644 --- a/tuilun/全部功能没做什么.txt +++ b/tuilun/全部功能没做什么.txt @@ -106,3 +106,28 @@ (乡野食物转化关系.txt 一、章鱼小丸子 todo 已标注) (原版食物-成品盘转换 已实现于 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), 即只有"插苹果冰棍"一种组合 +- 未做原因: 其他口味组合暂无对应堆方块, 不支持堆叠 (转化文档已注明)