# fish_clam_roast_system.gd —— Metin2 40250 官方珍珠蚌开珠与活鱼宰杀/篝火炙烤盛宴系统 1:1 # 100% 对照 40250 服务端 fishing.cpp, char_item.cpp (ITEM_FISH, 27987, USE_ABILITY_UP, USE_INVISIBILITY), item_proto.txt class_name FishClamRoastSystem extends RefCounted signal fish_gutted(result_type: String, item_vnum: int, item_name: String) signal clam_opened(result_type: String, item_vnum: int, item_name: String) signal fish_grilled(raw_vnum: int, cooked_vnum: int, count: int) signal cooked_fish_consumed(fish_vnum: int, effect_type: String, value: int, duration: float) signal campfire_spawned(pos: Vector2, duration: float) # 官方核心 Vnum 常量 const VNUM_CLAM := 27987 # 珍珠蚌 (Clam / Istiridye) const VNUM_STONE_PIECE := 27990 # 碎石 (Stone Piece) const VNUM_WHITE_PEARL := 27992 # 白珍珠 (White Pearl) const VNUM_BLUE_PEARL := 27993 # 蓝珍珠 (Blue Pearl) const VNUM_BLOOD_PEARL := 27994 # 血红珍珠 (Blood Pearl) const VNUM_FISH_BONE := 27995 # 鱼骨 (Fishbone) const VNUM_EARTHWORM := 27801 # 蚯蚓/饵料 (Earthworm) const VNUM_CAMPFIRE_ITEM := 27600 # 篝火道具 const MOB_CAMPFIRE := 12000 # 篝火 NPC/Mob # 40250 官方鱼类资料表 (alive_vnum, dead_vnum, cooked_vnum, buff/effect) const FISH_DATA: Dictionary = { 27803: { "name": "鲈鱼 (Sudak)", "alive_vnum": 27803, "dead_vnum": 27833, "cooked_vnum": 27863, "effect": "instant_hp", "value": 180, "duration": 0.0, "desc": "立即恢复 180 点生命值" }, 27804: { "name": "鲱鱼 (Ringa)", "alive_vnum": 27804, "dead_vnum": 27834, "cooked_vnum": 27864, "effect": "instant_sp", "value": 150, "duration": 0.0, "desc": "立即恢复 150 点魔法值" }, 27806: { "name": "鲤鱼 (Sazan)", "alive_vnum": 27806, "dead_vnum": 27836, "cooked_vnum": 27866, "effect": "move_speed", "value": 20, "duration": 600.0, "desc": "移动速度 +20 持续 10 分钟" }, 27807: { "name": "三文鱼/鲑鱼 (Som)", "alive_vnum": 27807, "dead_vnum": 27837, "cooked_vnum": 27867, "effect": "instant_sp", "value": 300, "duration": 0.0, "desc": "立即恢复 300 点魔法值" }, 27808: { "name": "草鱼 (Ot Sazanı)", "alive_vnum": 27808, "dead_vnum": 27838, "cooked_vnum": 27868, "effect": "attack_speed", "value": 20, "duration": 600.0, "desc": "攻击速度 +20 持续 10 分钟" }, 27810: { "name": "水针鱼 (Zargana)", "alive_vnum": 27810, "dead_vnum": 27840, "cooked_vnum": 27870, "effect": "strength", "value": 10, "duration": 600.0, "desc": "力量 (STR) +10 持续 10 分钟" }, 27813: { "name": "凤尾鱼 (Hamsi)", "alive_vnum": 27813, "dead_vnum": 27843, "cooked_vnum": 27873, "effect": "dexterity", "value": 10, "duration": 600.0, "desc": "敏捷 (DEX) +10 持续 10 分钟" }, 27814: { "name": "海鲈鱼 (Levrek)", "alive_vnum": 27814, "dead_vnum": 27844, "cooked_vnum": 27874, "effect": "cure_poison", "value": 1, "duration": 0.0, "desc": "祛除体内的一切猛毒状态" }, 27817: { "name": "泥鳅 (Çopra)", "alive_vnum": 27817, "dead_vnum": 27847, "cooked_vnum": 27877, "effect": "invisibility", "value": 1, "duration": 300.0, "desc": "进入幽冥迷雾隐身状态 5 分钟" } } # 逆向索引 var _dead_to_cooked: Dictionary = {} var _alive_to_cooked: Dictionary = {} var _cooked_to_data: Dictionary = {} # 当前激活的烤鱼增益状态列表 var _active_buffs: Dictionary = {} # effect_type -> {"value": int, "duration": float} # 当前地图是否存在有效篝火 var is_near_active_campfire: bool = false var campfire_time_left: float = 0.0 func _init() -> void: for k in FISH_DATA.keys(): var data = FISH_DATA[k] _alive_to_cooked[data["alive_vnum"]] = data _dead_to_cooked[data["dead_vnum"]] = data _cooked_to_data[data["cooked_vnum"]] = data # ========================================== # 1. 活鱼宰杀/使用 (fishing.cpp: UseFish) # ========================================== func gut_live_fish(inventory: Array, slot_index: int, forced_roll: int = -1) -> Dictionary: if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null: return {"ok": false, "reason": "EMPTY_SLOT", "msg": "该格位没有活鱼!"} var item = inventory[slot_index] var vnum = int(item.get("vnum", 0)) if not _alive_to_cooked.has(vnum): return {"ok": false, "reason": "NOT_ALIVE_FISH", "msg": "此物品不是可以剖杀的鲜活鱼类!"} var fish = _alive_to_cooked[vnum] # 扣减活鱼数量 _consume_item_one(inventory, slot_index) # 40250 原版随机概率 1~10000: # r >= 4001 (60%): 产出死鱼 (dead_vnum) # r >= 2001 (20%): 产出鱼骨 (FISH_BONE_VNUM 27995) # r < 2001 (20%): 肚中有物 (蚌壳 27987、饵料 27801 或失手滑入水中) var r = forced_roll if forced_roll > 0 else randi_range(1, 10000) if r >= 4001: _give_or_stack_item(inventory, fish["dead_vnum"], 1) fish_gutted.emit("dead_fish", fish["dead_vnum"], "死" + fish["name"]) return { "ok": true, "result": "dead_fish", "vnum": fish["dead_vnum"], "name": "死" + fish["name"], "msg": "你熟练地宰杀了活鱼,获得了可用于烤制的死鱼。" } elif r >= 2001: _give_or_stack_item(inventory, VNUM_FISH_BONE, 1) fish_gutted.emit("fish_bone", VNUM_FISH_BONE, "鱼骨") return { "ok": true, "result": "fish_bone", "vnum": VNUM_FISH_BONE, "name": "鱼骨", "msg": "活鱼体内只剩下一截晶莹剔透的坚硬鱼骨。" } else: # r < 2001 if r <= 1000: # 珍稀产出:珍珠蚌 (Clam 27987) _give_or_stack_item(inventory, VNUM_CLAM, 1) fish_gutted.emit("clam", VNUM_CLAM, "珍珠蚌") return { "ok": true, "result": "clam", "vnum": VNUM_CLAM, "name": "珍珠蚌", "msg": "难以置信!你在鱼肚中发现了一枚紧闭的神秘【珍珠蚌】!" } elif r <= 1500: _give_or_stack_item(inventory, VNUM_EARTHWORM, 1) fish_gutted.emit("earthworm", VNUM_EARTHWORM, "蚯蚓") return { "ok": true, "result": "earthworm", "vnum": VNUM_EARTHWORM, "name": "蚯蚓", "msg": "鱼肚里还残留着一条未消化的蚯蚓饵料。" } else: # 滑入水中/空 fish_gutted.emit("escape", 0, "溜走") return { "ok": true, "result": "escape", "vnum": 0, "name": "", "msg": "活鱼猛烈扑腾,从你的指缝中滑落回了深水之中!" } # ========================================== # 2. 珍珠蚌开珠 (char_item.cpp:2965 Case 27987) # ========================================== func open_clam( inventory: Array, slot_index: int, forced_branch_roll: int = -1, forced_pearl_roll: int = -1 ) -> Dictionary: if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null: return {"ok": false, "reason": "EMPTY_SLOT", "msg": "该格位没有物品!"} var item = inventory[slot_index] var vnum = int(item.get("vnum", 0)) if vnum != VNUM_CLAM: return {"ok": false, "reason": "NOT_A_CLAM", "msg": "该物品不是珍珠蚌!"} # 消耗 1 枚珍珠蚌 _consume_item_one(inventory, slot_index) # 40250 官方第一轮分支 (1~100): # r <= 50 (50%): 碎石 (27990) # r > 50 (50%): 判定珍珠概率表 prob_table {80, 90, 97} var r = forced_branch_roll if forced_branch_roll > 0 else randi_range(1, 100) if r <= 50: _give_or_stack_item(inventory, VNUM_STONE_PIECE, 1) clam_opened.emit("stone", VNUM_STONE_PIECE, "碎石") return { "ok": true, "result": "stone", "vnum": VNUM_STONE_PIECE, "name": "碎石", "msg": "珍珠蚌打开了,但里面只包裹着一块毫无价值的普通碎石。" } else: # 珍珠判定表 (1~100) # roll <= 80 (80%): 空蚌壳 (什么都没有) # 81 <= roll <= 90 (10%): 白珍珠 (27992) # 91 <= roll <= 97 (7%): 蓝珍珠 (27993) # roll >= 98 (3%): 血红珍珠 (27994) var p_roll = forced_pearl_roll if forced_pearl_roll > 0 else randi_range(1, 100) if p_roll <= 80: clam_opened.emit("empty", 0, "空蚌") return { "ok": true, "result": "empty", "vnum": 0, "name": "", "msg": "蚌壳碎裂开来,里面空空如也,什么也没有留下。" } elif p_roll <= 90: _give_or_stack_item(inventory, VNUM_WHITE_PEARL, 1) clam_opened.emit("white_pearl", VNUM_WHITE_PEARL, "白珍珠") return { "ok": true, "result": "white_pearl", "vnum": VNUM_WHITE_PEARL, "name": "白珍珠", "msg": "璀璨温润!你从蚌肉中剥出了一颗纯净的【白珍珠】!" } elif p_roll <= 97: _give_or_stack_item(inventory, VNUM_BLUE_PEARL, 1) clam_opened.emit("blue_pearl", VNUM_BLUE_PEARL, "蓝珍珠") return { "ok": true, "result": "blue_pearl", "vnum": VNUM_BLUE_PEARL, "name": "蓝珍珠", "msg": "幽蓝如海!你收获了一颗弥足珍贵的【蓝珍珠】!" } else: _give_or_stack_item(inventory, VNUM_BLOOD_PEARL, 1) clam_opened.emit("blood_pearl", VNUM_BLOOD_PEARL, "血红珍珠") return { "ok": true, "result": "blood_pearl", "vnum": VNUM_BLOOD_PEARL, "name": "血红珍珠", "msg": "旷世奇珍!蚌心处赫然闪烁着血煞夺目的极品【血红珍珠】!" } # ========================================== # 3. 篝火炙烤 (char_item.cpp:6880, fishing.cpp: Grill) # ========================================== func place_campfire(duration: float = 600.0) -> void: is_near_active_campfire = true campfire_time_left = duration campfire_spawned.emit(Vector2.ZERO, duration) func grill_fish(inventory: Array, slot_index: int, near_campfire_override: bool = false) -> Dictionary: var can_grill = is_near_active_campfire or near_campfire_override if not can_grill: return {"ok": false, "reason": "NO_CAMPFIRE", "msg": "烤鱼必须在点燃的营火 (篝火) 旁进行!"} if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null: return {"ok": false, "reason": "EMPTY_SLOT", "msg": "该格位没有鱼类!"} var item = inventory[slot_index] var vnum = int(item.get("vnum", 0)) var count = int(item.get("count", 1)) var fish_data = null if _dead_to_cooked.has(vnum): fish_data = _dead_to_cooked[vnum] elif _alive_to_cooked.has(vnum): fish_data = _alive_to_cooked[vnum] else: return {"ok": false, "reason": "NOT_GRILLABLE", "msg": "此物品无法用于篝火炙烤!"} var cooked_vnum = int(fish_data["cooked_vnum"]) # 40250 item->SetCount(0); AutoGiveItem(grill_vnum, count); inventory[slot_index] = null _give_or_stack_item(inventory, cooked_vnum, count) fish_grilled.emit(vnum, cooked_vnum, count) return { "ok": true, "cooked_vnum": cooked_vnum, "count": count, "name": "烤" + fish_data["name"], "msg": "营火噼啪作响,鱼肉散发出浓郁扑鼻的焦香!你获得了 %d 条【烤%s】。" % [count, fish_data["name"]] } # ========================================== # 4. 食用熟鱼获得增益 (char_item.cpp: USE_POTION / USE_ABILITY_UP) # ========================================== func eat_cooked_fish(inventory: Array, slot_index: int) -> Dictionary: if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null: return {"ok": false, "reason": "EMPTY_SLOT", "msg": "该格位没有熟鱼!"} var item = inventory[slot_index] var vnum = int(item.get("vnum", 0)) if not _cooked_to_data.has(vnum): return {"ok": false, "reason": "NOT_COOKED_FISH", "msg": "此物品不是烤制完成的熟鱼!"} var data = _cooked_to_data[vnum] _consume_item_one(inventory, slot_index) var effect = str(data["effect"]) var val = int(data["value"]) var dur = float(data["duration"]) if dur > 0.0: _active_buffs[effect] = { "value": val, "time_remaining": dur, "name": "烤" + data["name"] } cooked_fish_consumed.emit(vnum, effect, val, dur) return { "ok": true, "effect": effect, "value": val, "duration": dur, "desc": data["desc"], "msg": "你大快朵颐地吃下了香喷喷的烤鱼,%s!" % data["desc"] } func update(delta: float) -> void: if campfire_time_left > 0.0: campfire_time_left -= delta if campfire_time_left <= 0.0: campfire_time_left = 0.0 is_near_active_campfire = false # 更新 BUFF 持续倒计时 var to_remove: Array = [] for eff in _active_buffs.keys(): var b = _active_buffs[eff] b["time_remaining"] -= delta if b["time_remaining"] <= 0.0: to_remove.append(eff) for eff in to_remove: _active_buffs.erase(eff) func get_active_buffs() -> Dictionary: return _active_buffs func clear_buffs() -> void: _active_buffs.clear() # ========================================== # 辅助函数: 背包堆叠与消耗 # ========================================== func _consume_item_one(inventory: Array, slot: int) -> void: var item = inventory[slot] var count = int(item.get("count", 1)) if count > 1: item["count"] = count - 1 else: inventory[slot] = null func _give_or_stack_item(inventory: Array, vnum: int, count: int) -> void: # 尝试堆叠 for it in inventory: if it != null and int(it.get("vnum", 0)) == vnum: it["count"] = int(it.get("count", 1)) + count return # 寻找首个空槽 for i in range(inventory.size()): if inventory[i] == null: inventory[i] = {"vnum": vnum, "count": count} return # 若无空槽则追加 inventory.append({"vnum": vnum, "count": count}) # 序列化与反序列化 func serialize() -> Dictionary: return { "active_buffs": _active_buffs.duplicate(true), "campfire_time_left": campfire_time_left, "is_near_active_campfire": is_near_active_campfire } func deserialize(data: Dictionary) -> void: _active_buffs = data.get("active_buffs", {}).duplicate(true) campfire_time_left = float(data.get("campfire_time_left", 0.0)) is_near_active_campfire = bool(data.get("is_near_active_campfire", false))