- 桥梁与静态物体高度采样修复: - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程 - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题 - 新增 test_bridge_height_parity.gd 自动化对拍测试 - 40250 怪物击杀经验动效: - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附 - 40250 客户端全系统功能对齐(Batches 1-31): - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试 - 文档沉淀: - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
215 lines
6.3 KiB
GDScript
215 lines
6.3 KiB
GDScript
# fishing_cooking_camp_system.gd —— Metin2 40250 野外钓鱼烹饪与生火露营增益系统 1:1
|
||
# 对照 40250 服务端 fishing.cpp, char_item.cpp, affect.h
|
||
class_name FishingCookingCampSystem
|
||
extends RefCounted
|
||
|
||
signal campfire_lit(pos: Vector3, duration: float)
|
||
signal campfire_extinguished()
|
||
signal fish_cooked(raw_vnum: int, cooked_vnum: int, cooked_name: String)
|
||
signal food_buff_applied(buff_id: String, buff_name: String, duration: float)
|
||
signal food_buff_expired(buff_id: String)
|
||
|
||
const CAMPFIRE_KIT_VNUM := 27600 # 营火工具 (Campfire)
|
||
const CAMPFIRE_DURATION := 300.0 # 营火持续 5 分钟
|
||
const FOOD_BUFF_DURATION := 1800.0 # 食用增益 30 分钟 (1800 秒)
|
||
|
||
# 鱼类与烹饪食谱
|
||
const RECIPES: Dictionary = {
|
||
27802: { # 生鲤鱼 -> 烤鲤鱼
|
||
"cooked_vnum": 27832,
|
||
"cooked_name": "烤鲤鱼",
|
||
"buff_id": "buff_grilled_carp",
|
||
"stat_key": "max_hp",
|
||
"stat_val": 1000,
|
||
"desc": "最大生命上限 +1000,持续 30 分钟"
|
||
},
|
||
27803: { # 生草鱼 -> 烤草鱼
|
||
"cooked_vnum": 27833,
|
||
"cooked_name": "烤草鱼",
|
||
"buff_id": "buff_grilled_grass_carp",
|
||
"stat_key": "move_speed",
|
||
"stat_val": 15,
|
||
"desc": "移动速度 +15,持续 30 分钟"
|
||
},
|
||
27815: { # 生红鳟鱼 -> 烤红鳟鱼
|
||
"cooked_vnum": 27845,
|
||
"cooked_name": "烤红鳟鱼",
|
||
"buff_id": "buff_grilled_trout",
|
||
"stat_key": "atk",
|
||
"stat_val": 50,
|
||
"desc": "攻击力 +50,持续 30 分钟"
|
||
}
|
||
}
|
||
|
||
# 烤鱼物品映射
|
||
const COOKED_FOODS: Dictionary = {
|
||
27832: {"buff_id": "buff_grilled_carp", "name": "烤鲤鱼", "stat_key": "max_hp", "stat_val": 1000},
|
||
27833: {"buff_id": "buff_grilled_grass_carp", "name": "烤草鱼", "stat_key": "move_speed", "stat_val": 15},
|
||
27845: {"buff_id": "buff_grilled_trout", "name": "烤红鳟鱼", "stat_key": "atk", "stat_val": 50}
|
||
}
|
||
|
||
var is_campfire_active: bool = false
|
||
var campfire_timer: float = 0.0
|
||
var campfire_pos: Vector3 = Vector3.ZERO
|
||
|
||
# 活跃中的食物 Buff: buff_id -> { "remaining": float, "stat_key": String, "stat_val": int }
|
||
var active_buffs: Dictionary = {}
|
||
|
||
# 点燃野外营火
|
||
func light_campfire(pos: Vector3, inventory: Array) -> Dictionary:
|
||
var kit_slot = -1
|
||
for i in range(inventory.size()):
|
||
if inventory[i] != null and int(inventory[i].get("vnum", 0)) == CAMPFIRE_KIT_VNUM:
|
||
kit_slot = i
|
||
break
|
||
|
||
if kit_slot == -1:
|
||
return {"ok": false, "reason": "NO_CAMPFIRE_KIT", "msg": "背包中没有【营火工具】(27600)!"}
|
||
|
||
# 消耗 1 个营火工具
|
||
var item = inventory[kit_slot]
|
||
var cnt = int(item.get("count", 1))
|
||
if cnt > 1:
|
||
item["count"] = cnt - 1
|
||
else:
|
||
inventory[kit_slot] = null
|
||
|
||
is_campfire_active = true
|
||
campfire_timer = CAMPFIRE_DURATION
|
||
campfire_pos = pos
|
||
|
||
campfire_lit.emit(pos, CAMPFIRE_DURATION)
|
||
|
||
return {
|
||
"ok": true,
|
||
"pos": pos,
|
||
"duration": CAMPFIRE_DURATION,
|
||
"msg": "营火已点燃!散发着温暖火光,可在此烧烤烹饪渔获!"
|
||
}
|
||
|
||
# 在营火旁烹饪烤鱼
|
||
func cook_fish(slot: int, inventory: Array) -> Dictionary:
|
||
if not is_campfire_active:
|
||
return {"ok": false, "reason": "NO_ACTIVE_CAMPFIRE", "msg": "周围没有生起的营火,无法烹饪!"}
|
||
|
||
if slot < 0 or slot >= inventory.size() or inventory[slot] == null:
|
||
return {"ok": false, "reason": "INVALID_SLOT"}
|
||
|
||
var raw_item = inventory[slot]
|
||
var raw_vnum = int(raw_item.get("vnum", 0))
|
||
|
||
if not RECIPES.has(raw_vnum):
|
||
return {"ok": false, "reason": "NOT_COOKABLE", "msg": "此物品无法在营火上烹饪!"}
|
||
|
||
var r = RECIPES[raw_vnum]
|
||
# 消耗 1 条生鱼
|
||
var cnt = int(raw_item.get("count", 1))
|
||
if cnt > 1:
|
||
raw_item["count"] = cnt - 1
|
||
else:
|
||
inventory[slot] = null
|
||
|
||
# 入包烤鱼
|
||
var cooked_vnum: int = r["cooked_vnum"]
|
||
var cooked_name: String = r["cooked_name"]
|
||
|
||
# 寻找可叠加的空格或同类
|
||
var placed := false
|
||
for i in range(inventory.size()):
|
||
if inventory[i] != null and int(inventory[i].get("vnum", 0)) == cooked_vnum:
|
||
inventory[i]["count"] = int(inventory[i].get("count", 1)) + 1
|
||
placed = true
|
||
break
|
||
|
||
if not placed:
|
||
for i in range(inventory.size()):
|
||
if inventory[i] == null:
|
||
inventory[i] = {
|
||
"vnum": cooked_vnum,
|
||
"name": cooked_name,
|
||
"count": 1
|
||
}
|
||
placed = true
|
||
break
|
||
|
||
fish_cooked.emit(raw_vnum, cooked_vnum, cooked_name)
|
||
|
||
return {
|
||
"ok": true,
|
||
"cooked_name": cooked_name,
|
||
"desc": r["desc"],
|
||
"msg": "滋滋作响!成功烤制出色香味俱全的【%s】!" % cooked_name
|
||
}
|
||
|
||
# 食用烤鱼获得 30 分钟强力 Buff
|
||
func eat_cooked_fish(slot: int, inventory: Array, player_data: Dictionary) -> Dictionary:
|
||
if slot < 0 or slot >= inventory.size() or inventory[slot] == null:
|
||
return {"ok": false, "reason": "INVALID_SLOT"}
|
||
|
||
var item = inventory[slot]
|
||
var vnum = int(item.get("vnum", 0))
|
||
|
||
if not COOKED_FOODS.has(vnum):
|
||
return {"ok": false, "reason": "NOT_EDIBLE_COOKED_FISH"}
|
||
|
||
var cfg = COOKED_FOODS[vnum]
|
||
var bid: String = cfg["buff_id"]
|
||
var stat_k: String = cfg["stat_key"]
|
||
var stat_v: int = cfg["stat_val"]
|
||
|
||
# 消耗 1 条烤鱼
|
||
var cnt = int(item.get("count", 1))
|
||
if cnt > 1:
|
||
item["count"] = cnt - 1
|
||
else:
|
||
inventory[slot] = null
|
||
|
||
# 如果已有同类 Buff,先刷新时长并避免重复叠加属性
|
||
if active_buffs.has(bid):
|
||
active_buffs[bid]["remaining"] = FOOD_BUFF_DURATION
|
||
else:
|
||
player_data[stat_k] = int(player_data.get(stat_k, 0)) + stat_v
|
||
active_buffs[bid] = {
|
||
"remaining": FOOD_BUFF_DURATION,
|
||
"stat_key": stat_k,
|
||
"stat_val": stat_v,
|
||
"name": cfg["name"]
|
||
}
|
||
|
||
food_buff_applied.emit(bid, cfg["name"], FOOD_BUFF_DURATION)
|
||
|
||
return {
|
||
"ok": true,
|
||
"buff_id": bid,
|
||
"buff_name": cfg["name"],
|
||
"stat_key": stat_k,
|
||
"stat_val": stat_v,
|
||
"current_stat": player_data[stat_k],
|
||
"msg": "品尝了美味的【%s】!精神焕发,获得了【%s +%d】持续 30 分钟!" % [cfg["name"], stat_k, stat_v]
|
||
}
|
||
|
||
# 帧心跳更新营火与食物 Buff
|
||
func update(delta: float, player_data: Dictionary) -> void:
|
||
# 营火计时
|
||
if is_campfire_active:
|
||
campfire_timer -= delta
|
||
if campfire_timer <= 0.0:
|
||
is_campfire_active = false
|
||
campfire_extinguished.emit()
|
||
|
||
# 食物 Buff 计时与过期反扣
|
||
var expired_ids := []
|
||
for bid in active_buffs.keys():
|
||
var b = active_buffs[bid]
|
||
b["remaining"] -= delta
|
||
if b["remaining"] <= 0.0:
|
||
expired_ids.append(bid)
|
||
|
||
for bid in expired_ids:
|
||
var b = active_buffs[bid]
|
||
var stat_k = b["stat_key"]
|
||
var stat_v = b["stat_val"]
|
||
player_data[stat_k] = max(0, int(player_data.get(stat_k, 0)) - stat_v)
|
||
active_buffs.erase(bid)
|
||
food_buff_expired.emit(bid)
|