# fishing_rod_reel_system.gd —— Metin2 40250 官方钓鱼抛竿拉杆与浮漂反应小游戏机制 1:1 # 100% 对照 40250 服务端 fishing.cpp (CreateFishingEvent, Take, FishingPractice, RealRefineRod), fishing.h class_name FishingRodReelSystem extends RefCounted signal line_cast(rod_level: int, bait_name: String) signal fish_bite(reaction_time_window: float) signal fish_caught(fish_vnum: int, fish_name: String, fish_length_cm: float, practice_points: int) signal fish_escaped(reason: String, practice_points: int) signal rod_refined(new_vnum: int, success: bool, new_level: int) signal fishing_error(reason: String) # 40250 官方钓竿 Vnum (27400 基础钓鱼竿+0 ~ 27500 +9) const BASE_ROD_VNUM := 27400 # 钓鱼竿+0 const MAX_ROD_LEVEL := 9 # 40250 官方饵料 Vnum const VNUM_PASTE := 27800 # 鱼饵面团 (Paste) const VNUM_EARTHWORM := 27801 # 蚯蚓 (Earthworm) # 40250 渔夫 NPC const NPC_FISHERMAN := 9009 # 钓竿进阶最大熟练度点数表与成功率 (对照 item_proto Value(2) 与 Value(3)) const ROD_LEVEL_DATA: Dictionary = { 0: {"max_points": 10, "refine_chance": 100, "base_catch_rate": 0.50}, 1: {"max_points": 20, "refine_chance": 90, "base_catch_rate": 0.55}, 2: {"max_points": 40, "refine_chance": 80, "base_catch_rate": 0.60}, 3: {"max_points": 80, "refine_chance": 70, "base_catch_rate": 0.65}, 4: {"max_points": 120, "refine_chance": 60, "base_catch_rate": 0.70}, 5: {"max_points": 160, "refine_chance": 50, "base_catch_rate": 0.75}, 6: {"max_points": 200, "refine_chance": 40, "base_catch_rate": 0.80}, 7: {"max_points": 250, "refine_chance": 30, "base_catch_rate": 0.85}, 8: {"max_points": 300, "refine_chance": 20, "base_catch_rate": 0.90}, 9: {"max_points": 350, "refine_chance": 10, "base_catch_rate": 0.95} } # 常见垂钓渔获池 (对照 fishing.cpp) const CATCH_POOL: Array[Dictionary] = [ {"vnum": 27803, "name": "鲈鱼 (Sudak)", "min_len": 15.0, "max_len": 35.0, "weight": 35}, {"vnum": 27806, "name": "鲤鱼 (Sazan)", "min_len": 25.0, "max_len": 65.0, "weight": 25}, {"vnum": 27808, "name": "草鱼 (Ot Sazanı)", "min_len": 20.0, "max_len": 55.0, "weight": 20}, {"vnum": 27810, "name": "水针鱼 (Zargana)", "min_len": 30.0, "max_len": 70.0, "weight": 10}, {"vnum": 27817, "name": "泥鳅 (Çopra)", "min_len": 8.0, "max_len": 18.0, "weight": 5}, {"vnum": 70201, "name": "漂白水 (Bleach)", "min_len": 0.0, "max_len": 0.0, "weight": 3}, {"vnum": 27987, "name": "珍稀珍珠蚌 (Clam)", "min_len": 5.0, "max_len": 12.0, "weight": 2} ] # 当前垂钓状态 var is_fishing: bool = false var has_bait: bool = false var current_bait_vnum: int = 0 var rod_level: int = 0 var rod_practice_points: int = 0 var bite_timer: float = 0.0 var is_bite_active: bool = false var bite_reaction_time_left: float = 0.0 const REACTION_WINDOW_SECONDS := 3.0 # 3 秒拉杆反应时间窗口 # ========================================== # 1. 挂饵与抛竿 (fishing.cpp: CreateFishingEvent) # ========================================== # 穿戴/设定当前鱼竿 func equip_rod(level: int, current_points: int = 0) -> void: rod_level = clamp(level, 0, MAX_ROD_LEVEL) var max_p = ROD_LEVEL_DATA[rod_level]["max_points"] rod_practice_points = clamp(current_points, 0, max_p) # 挂上鱼饵 (面团 27800 或 蚯蚓 27801) func attach_bait(inventory: Array, bait_slot: int) -> Dictionary: if is_fishing: fishing_error.emit("ALREADY_FISHING") return {"ok": false, "reason": "ALREADY_FISHING", "msg": "正在垂钓中,无法更换鱼饵!"} if bait_slot < 0 or bait_slot >= inventory.size() or inventory[bait_slot] == null: fishing_error.emit("INVALID_SLOT") return {"ok": false, "reason": "INVALID_SLOT", "msg": "未找到指定的鱼饵!"} var item = inventory[bait_slot] var vnum = int(item.get("vnum", 0)) if vnum != VNUM_PASTE and vnum != VNUM_EARTHWORM: fishing_error.emit("NOT_BAIT") return {"ok": false, "reason": "NOT_BAIT", "msg": "该物品无法作为鱼饵挂在鱼钩上!"} _consume_item_one(inventory, bait_slot) has_bait = true current_bait_vnum = vnum var bait_name = "鱼饵面团" if vnum == VNUM_PASTE else "蚯蚓" return {"ok": true, "bait_vnum": vnum, "bait_name": bait_name, "msg": "将【%s】小心地挂在了鱼钩上。" % bait_name} # 抛竿垂钓 func cast_line(forced_wait_time: float = -1.0) -> Dictionary: if not has_bait: fishing_error.emit("NO_BAIT") return {"ok": false, "reason": "NO_BAIT", "msg": "鱼钩上空无一物!请先挂上蚯蚓或面团饵料。"} if is_fishing: fishing_error.emit("ALREADY_FISHING") return {"ok": false, "reason": "ALREADY_FISHING", "msg": "钓线已经抛入水中,静候鱼儿上钩。"} is_fishing = true is_bite_active = false has_bait = false # 抛竿时消耗鱼钩上的鱼饵 # 40250 等待上钩时间: 2~7 秒 bite_timer = forced_wait_time if forced_wait_time > 0.0 else randf_range(2.0, 7.0) bite_reaction_time_left = 0.0 var bait_name = "鱼饵面团" if current_bait_vnum == VNUM_PASTE else "蚯蚓" line_cast.emit(rod_level, bait_name) return {"ok": true, "msg": "钓竿轻扬,鱼线带着微澜划破水面,沉入了平静的湖心……"} # ========================================== # 2. 反应窗口拉杆提竿 (fishing.cpp: Take) # ========================================== func pull_rod(inventory: Array, forced_catch_idx: int = -1, forced_success: bool = false) -> Dictionary: if not is_fishing: fishing_error.emit("NOT_FISHING") return {"ok": false, "reason": "NOT_FISHING", "msg": "你并未处于垂钓状态!"} var max_p = ROD_LEVEL_DATA[rod_level]["max_points"] # 提前起竿 (鱼儿未咬钩) if not is_bite_active: is_fishing = false bite_timer = 0.0 # 哪怕失败亦获得 1 点熟练度练习 (FishingPractice) if rod_practice_points < max_p: rod_practice_points += 1 fish_escaped.emit("PULLED_TOO_EARLY", rod_practice_points) return { "ok": false, "reason": "PULLED_TOO_EARLY", "practice_points": rod_practice_points, "msg": "起竿太早了!鱼钩划破空荡荡的水面,鱼饵不翼而飞。" } # 鱼已咬钩:计算捕获概率 is_fishing = false is_bite_active = false # 熟练度 +1 if rod_practice_points < max_p: rod_practice_points += 1 var base_rate = float(ROD_LEVEL_DATA[rod_level]["base_catch_rate"]) if current_bait_vnum == VNUM_EARTHWORM: base_rate += 0.05 # 蚯蚓比面团成功率多 5% var roll = randf() var success = (roll <= base_rate) or forced_success if success: # 捕获成功:从鱼池选取 var picked_fish: Dictionary = {} if forced_catch_idx >= 0 and forced_catch_idx < CATCH_POOL.size(): picked_fish = CATCH_POOL[forced_catch_idx] else: var total_w := 0 for f in CATCH_POOL: total_w += int(f["weight"]) var r = randi_range(1, total_w) var cur := 0 for f in CATCH_POOL: cur += int(f["weight"]) if r <= cur: picked_fish = f break var fish_vnum = int(picked_fish["vnum"]) var fish_name = str(picked_fish["name"]) var length_cm = randf_range(float(picked_fish["min_len"]), float(picked_fish["max_len"])) _give_or_stack_item(inventory, fish_vnum, 1) fish_caught.emit(fish_vnum, fish_name, length_cm, rod_practice_points) return { "ok": true, "fish_vnum": fish_vnum, "fish_name": fish_name, "length_cm": length_cm, "practice_points": rod_practice_points, "msg": "水花四溅!你收紧鱼线,成功钓上了一条 %.1f cm 的【%s】!" % [length_cm, fish_name] } else: # 挣脱脱钩 fish_escaped.emit("FISH_GOT_AWAY", rod_practice_points) return { "ok": false, "reason": "FISH_GOT_AWAY", "practice_points": rod_practice_points, "msg": "大鱼猛烈摆尾,挣脱鱼钩游回了深水,只留下一串水泡……" } # ========================================== # 3. 渔夫 NPC 鱼竿升星锻造 (fishing.cpp: RealRefineRod) # ========================================== func can_refine_rod() -> bool: if rod_level >= MAX_ROD_LEVEL: return false var max_p = ROD_LEVEL_DATA[rod_level]["max_points"] return rod_practice_points >= max_p func refine_rod_at_fisherman(forced_roll: int = -1) -> Dictionary: if not can_refine_rod(): fishing_error.emit("PRACTICE_NOT_MAXED") return { "ok": false, "reason": "PRACTICE_NOT_MAXED", "msg": "鱼竿熟练度尚未练满!需要 %d/%d 熟练度点数方可前往渔夫处升级。" % [ rod_practice_points, ROD_LEVEL_DATA[rod_level]["max_points"] ] } var chance = int(ROD_LEVEL_DATA[rod_level]["refine_chance"]) var r = forced_roll if forced_roll > 0 else randi_range(1, 100) if r <= chance: # 锻造升级成功 rod_level += 1 rod_practice_points = 0 var new_vnum = BASE_ROD_VNUM + rod_level rod_refined.emit(new_vnum, true, rod_level) return { "ok": true, "success": true, "new_level": rod_level, "new_vnum": new_vnum, "msg": "渔夫巧手翻飞,金光闪烁!你的钓鱼竿成功进阶为【钓鱼竿+%d】!" % rod_level } else: # 锻造失败:降 1 级 (40250 官方机制) rod_level = max(0, rod_level - 1) rod_practice_points = 0 var new_vnum = BASE_ROD_VNUM + rod_level rod_refined.emit(new_vnum, false, rod_level) return { "ok": true, "success": false, "new_level": rod_level, "new_vnum": new_vnum, "msg": "精炼失败了!鱼竿受损,品质降为了【钓鱼竿+%d】。" % rod_level } # ========================================== # 4. 心跳推进与超时判定 # ========================================== func update(delta: float) -> void: if not is_fishing: return if not is_bite_active: # 等待鱼咬钩 bite_timer -= delta if bite_timer <= 0.0: is_bite_active = true bite_reaction_time_left = REACTION_WINDOW_SECONDS fish_bite.emit(REACTION_WINDOW_SECONDS) else: # 鱼咬钩等待提竿窗口 bite_reaction_time_left -= delta if bite_reaction_time_left <= 0.0: # 超时逃跑 is_fishing = false is_bite_active = false var max_p = ROD_LEVEL_DATA[rod_level]["max_points"] if rod_practice_points < max_p: rod_practice_points += 1 fish_escaped.emit("REACTION_TIMEOUT", rod_practice_points) func _consume_item_one(inventory: Array, slot: int) -> void: if slot < 0 or slot >= inventory.size() or inventory[slot] == null: return 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 { "rod_level": rod_level, "rod_practice_points": rod_practice_points, "has_bait": has_bait, "current_bait_vnum": current_bait_vnum } func deserialize(data: Dictionary) -> void: rod_level = int(data.get("rod_level", 0)) rod_practice_points = int(data.get("rod_practice_points", 0)) has_bait = bool(data.get("has_bait", false)) current_bait_vnum = int(data.get("current_bait_vnum", 0))