- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
278 lines
9.5 KiB
GDScript
278 lines
9.5 KiB
GDScript
# lucky_lottery_fortune_system.gd —— Metin2 40250 官方幸运乐透刮刮乐与老妪占卜转运系统 1:1
|
||
# 100% 对照 40250 服务端 fortune_telling.quest, xxx_change_to_lotto.quest, questlua_pc.cpp (give_lotto)
|
||
class_name LuckyLotteryFortuneSystem
|
||
extends RefCounted
|
||
|
||
signal fortune_told(tier: String, exp_bonus: float, gold_bonus: float, desc: String)
|
||
signal talisman_acquired(item_vnum: int, cost: int)
|
||
signal lottery_scratched(digits: Array, prize_tier: String, prize_gold: int, prize_item_vnum: int)
|
||
signal system_error(reason: String)
|
||
|
||
# NPC 与道具 Vnum 常量
|
||
const NPC_OLD_WOMAN := 9006 # 老妪 (Old Woman)
|
||
const FORTUNE_COST_GOLD := 7777 # 占卜消耗金币 (40250 原版 7777)
|
||
const TALISMAN_COST_GOLD := 15000 # 辟邪转运符费用
|
||
const VNUM_LOTTO_TICKET := 50027 # 乐透彩票奖券 (Lotterielos)
|
||
const VNUM_LOTTO_BOOK := 50001 # 幸运奖券号 (Glücksbuch)
|
||
const VNUM_TALISMAN := 70052 # 转运符咒 (Karma Talisman)
|
||
const VNUM_BLESSING_SCROLL := 25040 # 祝福卷轴 (特等奖附赠)
|
||
|
||
# 占卜卦象运势常量
|
||
const TIER_GREAT_LUCK := "great_luck" # 大吉 (+10% 经验, +10% 金币掉率)
|
||
const TIER_GOOD_LUCK := "good_luck" # 中吉 (+5% 经验, +5% 金币掉率)
|
||
const TIER_NORMAL := "normal" # 平 (无额外加成)
|
||
const TIER_BAD_LUCK := "bad_luck" # 凶 (-10% 金币掉率)
|
||
|
||
const FORTUNE_DESCRIPTIONS: Dictionary = {
|
||
TIER_GREAT_LUCK: {
|
||
"name": "【大吉·紫气东来】",
|
||
"text": "老妪眯起双眼惊叹道:“祥云笼罩,鸿运当头!你今天的所有修行与征战都将得到福星的高照庇护!”",
|
||
"exp_bonus": 0.10,
|
||
"gold_bonus": 0.10
|
||
},
|
||
TIER_GOOD_LUCK: {
|
||
"name": "【中吉·吉星照拂】",
|
||
"text": "老妪慈祥地微笑道:“运势平稳向上,行事多有小顺,今日野外历练收获定然不错。”",
|
||
"exp_bonus": 0.05,
|
||
"gold_bonus": 0.05
|
||
},
|
||
TIER_NORMAL: {
|
||
"name": "【平常·云淡风轻】",
|
||
"text": "老妪看了看签文道:“波澜不惊,顺其自然。一切还需依靠你自身的勤勉苦练。”",
|
||
"exp_bonus": 0.0,
|
||
"gold_bonus": 0.0
|
||
},
|
||
TIER_BAD_LUCK: {
|
||
"name": "【大凶·晦暗蒙尘】",
|
||
"text": "老妪脸色微微一变:“乌云蔽日,近来出行需万分小心,财运恐有所折损!老身这里有一枚开光转运符……”",
|
||
"exp_bonus": 0.0,
|
||
"gold_bonus": -0.10
|
||
}
|
||
}
|
||
|
||
# 当前玩家占卜运势状态
|
||
var current_fortune_tier: String = ""
|
||
var fortune_time_remaining: float = 0.0 # 40250 原版持续 8 小时 (28800 秒)
|
||
var has_karma_talisman: bool = false
|
||
var lotto_history: Array[Dictionary] = []
|
||
|
||
# ==========================================
|
||
# 1. 老妪占卜祈福 (fortune_telling.quest)
|
||
# ==========================================
|
||
func tell_fortune(
|
||
player_level: int,
|
||
player_gold: int,
|
||
forced_tier: String = ""
|
||
) -> Dictionary:
|
||
# 等级需达到 20 级
|
||
if player_level <= 20:
|
||
system_error.emit("LEVEL_TOO_LOW")
|
||
return {
|
||
"ok": false,
|
||
"reason": "LEVEL_TOO_LOW",
|
||
"msg": "老妪摇了摇头:“年轻的冒险者,你还不满 20 级,未来的命运尚在凝聚之中,现在还不是占卜的时候。”"
|
||
}
|
||
|
||
# 检查金币
|
||
if player_gold < FORTUNE_COST_GOLD:
|
||
system_error.emit("NOT_ENOUGH_GOLD")
|
||
return {
|
||
"ok": false,
|
||
"reason": "NOT_ENOUGH_GOLD",
|
||
"msg": "占卜诚意金不足!老妪需要 %d 枚金币作为供奉卦金。" % FORTUNE_COST_GOLD
|
||
}
|
||
|
||
var remaining_gold = player_gold - FORTUNE_COST_GOLD
|
||
|
||
# 判定运势
|
||
var picked_tier = ""
|
||
if forced_tier != "" and FORTUNE_DESCRIPTIONS.has(forced_tier):
|
||
picked_tier = forced_tier
|
||
else:
|
||
var r = randi_range(1, 100)
|
||
if r <= 20:
|
||
picked_tier = TIER_GREAT_LUCK
|
||
elif r <= 50:
|
||
picked_tier = TIER_GOOD_LUCK
|
||
elif r <= 80:
|
||
picked_tier = TIER_NORMAL
|
||
else:
|
||
picked_tier = TIER_BAD_LUCK
|
||
|
||
current_fortune_tier = picked_tier
|
||
fortune_time_remaining = 28800.0 # 8 小时
|
||
has_karma_talisman = false
|
||
|
||
var info = FORTUNE_DESCRIPTIONS[picked_tier]
|
||
var can_buy_talisman = (picked_tier == TIER_BAD_LUCK or picked_tier == TIER_NORMAL)
|
||
|
||
fortune_told.emit(picked_tier, info["exp_bonus"], info["gold_bonus"], info["text"])
|
||
|
||
return {
|
||
"ok": true,
|
||
"tier": picked_tier,
|
||
"tier_name": info["name"],
|
||
"text": info["text"],
|
||
"exp_bonus": info["exp_bonus"],
|
||
"gold_bonus": info["gold_bonus"],
|
||
"remaining_gold": remaining_gold,
|
||
"can_buy_talisman": can_buy_talisman,
|
||
"talisman_cost": TALISMAN_COST_GOLD
|
||
}
|
||
|
||
# 购买转运符咒化解凶煞
|
||
func buy_karma_talisman(inventory: Array, player_gold: int) -> Dictionary:
|
||
if player_gold < TALISMAN_COST_GOLD:
|
||
system_error.emit("NOT_ENOUGH_GOLD_TALISMAN")
|
||
return {"ok": false, "reason": "NOT_ENOUGH_GOLD_TALISMAN", "msg": "购买辟邪转运符金币不足!需要 15,000 金币。"}
|
||
|
||
var remaining_gold = player_gold - TALISMAN_COST_GOLD
|
||
_give_or_stack_item(inventory, VNUM_TALISMAN, 1)
|
||
has_karma_talisman = true
|
||
|
||
# 若当前为大凶,直接洗去负面金币减成,转化为中吉
|
||
if current_fortune_tier == TIER_BAD_LUCK or current_fortune_tier == TIER_NORMAL:
|
||
current_fortune_tier = TIER_GOOD_LUCK
|
||
|
||
talisman_acquired.emit(VNUM_TALISMAN, TALISMAN_COST_GOLD)
|
||
return {
|
||
"ok": true,
|
||
"talisman_vnum": VNUM_TALISMAN,
|
||
"new_tier": current_fortune_tier,
|
||
"remaining_gold": remaining_gold,
|
||
"msg": "你佩戴上了老妪开光的辟邪转运符,周身晦暗之气顿时消散,运势转危为安!"
|
||
}
|
||
|
||
# ==========================================
|
||
# 2. 乐透刮刮乐奖券开奖 (xxx_change_to_lotto.quest)
|
||
# ==========================================
|
||
func play_scratch_lottery(
|
||
inventory: Array,
|
||
ticket_slot: int,
|
||
forced_digits: Array = []
|
||
) -> Dictionary:
|
||
if ticket_slot < 0 or ticket_slot >= inventory.size() or inventory[ticket_slot] == null:
|
||
system_error.emit("INVALID_TICKET_SLOT")
|
||
return {"ok": false, "reason": "INVALID_TICKET_SLOT", "msg": "未找到指定的乐透彩票!"}
|
||
|
||
var item = inventory[ticket_slot]
|
||
var vnum = int(item.get("vnum", 0))
|
||
if vnum != VNUM_LOTTO_TICKET and vnum != VNUM_LOTTO_BOOK:
|
||
system_error.emit("NOT_A_LOTTO_TICKET")
|
||
return {"ok": false, "reason": "NOT_A_LOTTO_TICKET", "msg": "该物品并不是乐透彩票奖券!"}
|
||
|
||
# 消耗 1 张彩票
|
||
_consume_item_one(inventory, ticket_slot)
|
||
|
||
# 刮开 3 位幸运数字 (1~9)
|
||
var digits: Array = []
|
||
if forced_digits.size() == 3:
|
||
digits = forced_digits.duplicate()
|
||
else:
|
||
digits = [randi_range(1, 9), randi_range(1, 9), randi_range(1, 9)]
|
||
|
||
var d1 = int(digits[0])
|
||
var d2 = int(digits[1])
|
||
var d3 = int(digits[2])
|
||
|
||
var prize_tier := ""
|
||
var prize_gold := 0
|
||
var prize_item := 0
|
||
var prize_name := ""
|
||
|
||
if d1 == d2 and d2 == d3:
|
||
# 三星连珠:特等大奖 (1,000,000 金币 + 祝福卷轴 25040)
|
||
prize_tier = "grand_jackpot"
|
||
prize_gold = 1000000
|
||
prize_item = VNUM_BLESSING_SCROLL
|
||
prize_name = "【特等头奖】1,000,000 金币 + 祝福卷轴"
|
||
_give_or_stack_item(inventory, prize_item, 1)
|
||
elif d1 == d2 or d2 == d3 or d1 == d3:
|
||
# 双星闪耀:二等奖 (200,000 金币)
|
||
prize_tier = "second_prize"
|
||
prize_gold = 200000
|
||
prize_item = 0
|
||
prize_name = "【二等奖】200,000 金币"
|
||
else:
|
||
# 安慰奖:30,000 金币
|
||
prize_tier = "consolation_prize"
|
||
prize_gold = 30000
|
||
prize_item = 0
|
||
prize_name = "【幸运鼓励奖】30,000 金币"
|
||
|
||
var record = {
|
||
"digits": digits,
|
||
"prize_tier": prize_tier,
|
||
"prize_gold": prize_gold,
|
||
"prize_item": prize_item
|
||
}
|
||
lotto_history.append(record)
|
||
lottery_scratched.emit(digits, prize_tier, prize_gold, prize_item)
|
||
|
||
return {
|
||
"ok": true,
|
||
"digits": digits,
|
||
"prize_tier": prize_tier,
|
||
"prize_name": prize_name,
|
||
"prize_gold": prize_gold,
|
||
"prize_item": prize_item,
|
||
"msg": "刮开刮刮乐涂层,赫然显现数字 [%d, %d, %d]!恭喜斩获 %s!" % [d1, d2, d3, prize_name]
|
||
}
|
||
|
||
# 获取当前占卜增益属性
|
||
func get_active_fortune_bonus() -> Dictionary:
|
||
if current_fortune_tier == "" or fortune_time_remaining <= 0.0:
|
||
return {"exp_bonus": 0.0, "gold_bonus": 0.0, "tier": ""}
|
||
var info = FORTUNE_DESCRIPTIONS[current_fortune_tier]
|
||
return {
|
||
"tier": current_fortune_tier,
|
||
"exp_bonus": info["exp_bonus"],
|
||
"gold_bonus": info["gold_bonus"],
|
||
"time_remaining": fortune_time_remaining
|
||
}
|
||
|
||
# 时间推移
|
||
func update(delta: float) -> void:
|
||
if fortune_time_remaining > 0.0:
|
||
fortune_time_remaining -= delta
|
||
if fortune_time_remaining <= 0.0:
|
||
fortune_time_remaining = 0.0
|
||
current_fortune_tier = ""
|
||
|
||
# 序列化与反序列化
|
||
func serialize() -> Dictionary:
|
||
return {
|
||
"current_fortune_tier": current_fortune_tier,
|
||
"fortune_time_remaining": fortune_time_remaining,
|
||
"has_karma_talisman": has_karma_talisman,
|
||
"lotto_history": lotto_history.duplicate(true)
|
||
}
|
||
|
||
func deserialize(data: Dictionary) -> void:
|
||
current_fortune_tier = str(data.get("current_fortune_tier", ""))
|
||
fortune_time_remaining = float(data.get("fortune_time_remaining", 0.0))
|
||
has_karma_talisman = bool(data.get("has_karma_talisman", false))
|
||
lotto_history = data.get("lotto_history", []).duplicate(true)
|
||
|
||
# 内部消耗与堆叠工具
|
||
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})
|