Files
mtgodot-poc/project/lucky_lottery_fortune_system.gd
T
shenandshen 66d217b313 feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复:
  - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

278 lines
9.5 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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})