From a6270bac541891becc66a2c25d96d939bd393f44 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 12 Oct 2026 23:50:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=BF=AD=E4=BB=A323-T3):=20=E9=94=BB?= =?UTF-8?q?=E9=80=A0=E5=86=99=E5=8F=A3=E6=97=8F=E2=80=94=E2=80=94=E8=B7=AF?= =?UTF-8?q?=E7=BA=BF=E8=B4=AD=E4=B9=B0=EF=BC=88=E5=87=A0=E4=BD=95=E4=BB=B7?= =?UTF-8?q?=EF=BC=89+=E5=B9=B6=E8=81=94=E5=8F=8C=E9=80=9A=E9=81=93?= =?UTF-8?q?=E8=A7=A3=E9=94=81+=E9=80=89=E4=B8=AD=E5=88=87=E6=8D=A2?= =?UTF-8?q?=EF=BC=88=E5=85=8D=E8=B4=B9=EF=BC=89+=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E8=BD=B4=E8=AF=BB=E6=95=B0=EF=BC=88MetaProgression=20=E5=8D=95?= =?UTF-8?q?=E7=82=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 契约 9 写口唯一(UI 只发意图——意图信号接线随 T6 UI 同批落): - forge_route_unlocked:解锁态派生单点(初始持有∨直购清单∨练满前一档 levels[prereq]≥cap——不落档) - forge_buy_level:几何价=ForgeTable.level_price(20/30/45/67/101),扣钥石+等级+1+即时写盘;未解锁/满级/余额不足=拒绝零副作用 - forge_buy_unlock:一次性解锁价 200 直购;已解锁(含派生)=拒绝零收费——两通道互不影响、不设串联 - forge_select_weapon/armor:免费切换只受理已解锁路线(冒险锁死归参数面快照,契约 8) - forge_weapon_atk_bonus/forge_armor_ac_bonus/hero_attack_type/hero_armor_type:T4 注入面取数单点(异常选中态防御回退 0/默认值) - 冒烟 V2/V3/V4/V6 共 15 断言(隔离档 forge/forge2/forge3——add_keystone 置余额+直写存档域注入口径) - 轻闸:冒烟 923/0(908 基线 +15) --- game/core/meta_progression.gd | 108 ++++++++++++++++++++++++++++++++++ game/smoke/smoke_all.gd | 74 ++++++++++++++++++++++- 2 files changed, 181 insertions(+), 1 deletion(-) diff --git a/game/core/meta_progression.gd b/game/core/meta_progression.gd index e8975e74..4a07359e 100644 --- a/game/core/meta_progression.gd +++ b/game/core/meta_progression.gd @@ -123,6 +123,114 @@ func treasure_shop_status() -> Array: return out +# ---- 锻造工坊写口族(迭代 23 T3——契约 9:扣币/升级/解锁/选中/写盘只经本组;UI 只发意图信号)---- +# 设计权威 design/gdd/systems/跨局循环/锻造工坊.md v0.6:钥石单币(契约 3——GP 局内一次性货币 +# 永不互兑)、并联双通道解锁(4b-1——练满前一档 OR 一次性解锁价,互不影响不设串联)、 +# 一次冒险一武器一甲(契约 8——营地免费切换、冒险开始锁死由参数面快照保证)。 +# 等级语义(Q1):未解锁/未锻造=0 加值(裸体态);锻造第一级=「获得装备」=首个 +1;逐级递进。 + +# 路线解锁态派生(不落档——初始持有∨直购∨练满前一档;派生单点只此一处) +func forge_route_unlocked(route_id: String) -> bool: + var row: Dictionary = db.forge_route(route_id) + if row.is_empty(): + return false + if bool(row.get("initial_owned", false)): + return true + if save.forge_unlocked_purchases().has(route_id): + return true + var prereq := String(row.get("prereq_route", "")) + if prereq != "": + var prow: Dictionary = db.forge_route(prereq) + if not prow.is_empty() and save.forge_route_level(prereq) >= int(prow.get("cap", 0)): + return true + return false + + +# 路线购买(几何价=基础价×150%^已购级数——锚表 20/30/45/67/101;等级+1 即时写盘)。 +# 未解锁/已满级/余额不足/未知路线 = 拒绝且零副作用(spend_keystone 内嵌余额闸,失败不扣币不写盘)。 +func forge_buy_level(route_id: String) -> bool: + var row: Dictionary = db.forge_route(route_id) + if row.is_empty() or not forge_route_unlocked(route_id): + return false + var lvl := save.forge_route_level(route_id) + if lvl >= int(row.get("cap", 0)): + return false + var price := ForgeTable.level_price(row, lvl) + if not save.spend_keystone(price): + return false + save.set_forge_route_level(route_id, lvl + 1) + return save.write_to_disk() + + +# 一次性解锁价直购(并联双通道②)。已解锁(含练满前一档派生)= 拒绝零收费——两通道互不影响: +# 未练满也可直购;直购后练满不重复收(练满通道本就是派生、零收费)。初始持有行无解锁面。 +func forge_buy_unlock(route_id: String) -> bool: + var row: Dictionary = db.forge_route(route_id) + if row.is_empty() or bool(row.get("initial_owned", false)): + return false + if forge_route_unlocked(route_id): + return false + var price := int(row.get("unlock_price", 0)) + if price < 1 or not save.spend_keystone(price): + return false + save.add_forge_unlock(route_id) + return save.write_to_disk() + + +# 当前武器/甲选中切换(免费——只受理已解锁路线;写存档域+即时写盘。冒险开始锁死归参数面快照, +# 契约 8;冒险中无切换入口=UI 面无路径可达本口) +func forge_select_weapon(route_id: String) -> bool: + var row: Dictionary = db.forge_route(route_id) + if row.is_empty() or String(row.get("kind", "")) != "weapon" or not forge_route_unlocked(route_id): + return false + save.set_forge_weapon(route_id) + return save.write_to_disk() + + +func forge_select_armor(route_id: String) -> bool: + var row: Dictionary = db.forge_route(route_id) + if row.is_empty() or String(row.get("kind", "")) != "armor" or not forge_route_unlocked(route_id): + return false + save.set_forge_armor(route_id) + return save.write_to_disk() + + +# 当前选中武器攻击加值 / 当前甲 AC 加值(路线等级×每级增量——0=裸体态/未锻造合法态)。 +# 选中态异常(未解锁/表缺行)防御回退 0——注入面只认合法态,禁把脏选中带进冒险。 +func forge_weapon_atk_bonus() -> int: + var rid := save.forge_weapon() + var row: Dictionary = db.forge_route(rid) + if row.is_empty() or String(row.get("kind", "")) != "weapon" or not forge_route_unlocked(rid): + return 0 + return save.forge_route_level(rid) * int(row.get("per_level_gain", 0)) + + +func forge_armor_ac_bonus() -> int: + var rid := save.forge_armor() + var row: Dictionary = db.forge_route(rid) + if row.is_empty() or String(row.get("kind", "")) != "armor" or not forge_route_unlocked(rid): + return 0 + return save.forge_route_level(rid) * int(row.get("per_level_gain", 0)) + + +# 两根类型轴当前生效值(选中路线类型轴;选中态异常/未解锁=锻造配置默认值——契约 2 回退面)。 +# 默认值与柜台解锁状态解耦:树钥匙未点亮时本函数照常返回默认值(配置常驻可读)。 +func hero_attack_type() -> String: + var rid := save.forge_weapon() + var row: Dictionary = db.forge_route(rid) + if row.is_empty() or String(row.get("kind", "")) != "weapon" or not forge_route_unlocked(rid): + return db.forge_default_attack_type() + return String(row.get("type_axis", "")) + + +func hero_armor_type() -> String: + var rid := save.forge_armor() + var row: Dictionary = db.forge_route(rid) + if row.is_empty() or String(row.get("kind", "")) != "armor" or not forge_route_unlocked(rid): + return db.forge_default_armor_type() + return String(row.get("type_axis", "")) + + # ---- Boss 两档与收束入账(结算屏文案数据源)---- # 明细 {big_reward, boss_drops, total, new_unlocks};入账与标记经 save.apply_adventure_summary # (keystone_drops 副本随摘要入 recent_summaries)。 diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index fa3ee5f1..82796d08 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -4431,6 +4431,77 @@ func _smoke_forge_axes() -> void: and String(uc.hero_unit().get("armor_type", "")) == "light", "主角单位快照两轴 = 锻造配置默认值 normal/light(零注入回退——行为连续)〔锻造工坊批 V5〕") + # ---- T3 写口族(V2 购买/几何价 · V3 并联双通道 · V4 两币分工 · V6 选中切换)---- + # 注入口径(两分法):add_keystone 置余额/直写存档域构造状态 → 直调写口 → 断余额/等级/读回 + var sv2 = _meta_new_save(_META_SMOKE_PATHS[13]) + var mp2 = load("res://core/meta_progression.gd").new(_db, sv2) + # V2①:长剑首购=20(0 级位几何价)、等级 0→1、扣币即盘、reload 持久 + sv2.add_keystone(100) + check(mp2.forge_buy_level("sword") and sv2.forge_route_level("sword") == 1 + and sv2.keystone_balance() == 80, + "长剑锻造首购:扣 20 钥石、等级 0→1(Q1——第一级=获得装备=首个 +1)〔锻造工坊批 V2〕") + var sv2r = _meta_new_save(_META_SMOKE_PATHS[13]) + check(sv2r.forge_route_level("sword") == 1 and sv2r.keystone_balance() == 80, + "购买持久(reload 后等级与余额不变——即时写盘)〔锻造工坊批 V2〕") + # V2②:二级=30(1 级位);六路线独立(买长剑不动轻甲) + check(mp2.forge_buy_level("sword") and sv2.forge_route_level("sword") == 2 + and sv2.keystone_balance() == 50 and sv2.forge_route_level("light") == 0, + "长剑二级购:扣 30、等级→2;轻甲独立零扰动(六路线各练各的)〔锻造工坊批 V2〕") + # V2③:余额不足拒且零副作用(等级/选中/解锁三面零变化) + var sv2d = _meta_new_save(_META_SMOKE_PATHS[14]) + var mp2d = load("res://core/meta_progression.gd").new(_db, sv2d) + var fr_dry: Dictionary = sv2d.forge_routes_snapshot() + check(not mp2d.forge_buy_level("sword") and sv2d.keystone_balance() == 0 + and sv2d.forge_routes_snapshot() == fr_dry, + "余额不足购买拒绝且零副作用(0 余额——三域零变化)〔锻造工坊批 V2〕") + # V2④:已满级拒购(cap 闸);未解锁拒购 + sv2.set_forge_route_level("sword", 5) + check(not mp2.forge_buy_level("sword") and not mp2d.forge_buy_level("spear"), + "已满级拒购/未解锁拒购(cap 闸+解锁闸——零副作用)〔锻造工坊批 V2〕") + # V3① 通道一:练满前一档(长剑 cap)→ 长枪/法杖派生解锁(零收费);轻甲未满→甲线仍锁 + check(mp2.forge_route_unlocked("spear") and mp2.forge_route_unlocked("staff") + and not mp2.forge_route_unlocked("heavy") and not mp2.forge_route_unlocked("ward"), + "通道一:长剑练满→长枪/法杖派生解锁;轻甲未练满→重甲/魔法护甲仍锁〔锻造工坊批 V3〕") + check(mp2.forge_buy_level("spear") and sv2.forge_route_level("spear") == 1 + and sv2.keystone_balance() == 30, + "派生解锁后长枪可购(首购 20——余额 50−20)〔锻造工坊批 V3〕") + # V3② 通道二:一次性解锁价直购(未练满也可买——两通道互不影响) + var sv3 = _meta_new_save(_META_SMOKE_PATHS[15]) + var mp3 = load("res://core/meta_progression.gd").new(_db, sv3) + sv3.add_keystone(500) + check(not mp3.forge_route_unlocked("ward") and mp3.forge_buy_unlock("ward") + and sv3.keystone_balance() == 300 and mp3.forge_route_unlocked("ward"), + "通道二:魔法护甲一次性解锁价 200 直购开(轻甲未练满也可买)〔锻造工坊批 V3〕") + # V3③ 直购后练满不重复收(练满通道=派生零收费;已解锁再直购=拒绝) + sv3.set_forge_route_level("light", 5) + check(mp3.forge_route_unlocked("ward") and not mp3.forge_buy_unlock("ward") + and sv3.keystone_balance() == 300, + "直购后练满不重复收(已解锁=拒绝零收费——两通道互不影响)〔锻造工坊批 V3〕") + # V3④ 不设串联门槛(法杖直购开——长枪零状态不阻;dry 档充值走直购) + sv2d.add_keystone(500) + check(mp2d.forge_buy_unlock("staff") and mp2d.forge_route_unlocked("staff") + and sv2d.forge_route_level("spear") == 0 and not mp2d.forge_route_unlocked("spear"), + "不设串联门槛(法杖直购开——长枪零状态不阻)〔锻造工坊批 V3〕") + # V4:任一购买/解锁只扣钥石——GP 侧零触碰(meta 域无 GP 键=域界扫描同款;此处钉余额差=当级价) + var ks4: int = sv3.keystone_balance() + check(mp3.forge_buy_level("ward") and sv3.keystone_balance() == ks4 - 20 + and sv3.forge_route_level("ward") == 1, + "两币分工:购买只扣钥石(余额差=当级价 20——GP 零触碰、无材料计数)〔锻造工坊批 V4〕") + # V6①:营地切换=写存档域+免费(余额零动);旧路线练度保留(只不生效) + var ks6: int = sv3.keystone_balance() + check(mp3.forge_select_armor("ward") and sv3.forge_armor() == "ward" + and sv3.keystone_balance() == ks6, + "选中切换免费(余额零动——写存档域即盘)〔锻造工坊批 V6〕") + check(sv3.forge_route_level("light") == 5, + "切换后旧路线练度保留(轻甲 5 级不丢——回家待命只不生效)〔锻造工坊批 V6〕") + # V6②:未解锁路线拒选零副作用(dry 档:长枪未解锁) + check(not mp2d.forge_select_weapon("spear") and sv2d.forge_weapon() == "sword", + "未解锁路线拒选零副作用〔锻造工坊批 V6〕") + # V6③:类型轴读数随选中走(切换后读 ward;未解锁选中态防御回退默认值) + check(mp3.hero_armor_type() == "ward" and mp2d.hero_attack_type() == "normal" + and mp2d.hero_armor_type() == "light", + "类型轴当前生效值随选中走(ward;未解锁档回退默认 normal/light)〔锻造工坊批 V5/V6〕") + # ---- 暂停层只读投影一致性(迭代 17 卡③——R4 数据只读同源:暂停层展示值逐项 == MetaProgression # 访问器读数;投影拼装与 main._pause_data 同构(同访问器族同序)。只读性连接巡检与零动效 @@ -4564,7 +4635,8 @@ const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry "user://smoke_meta_cap.json", "user://smoke_meta_white.json", "user://smoke_meta_attune.json", "user://smoke_meta_attune2.json", "user://smoke_meta_tree.json", "user://smoke_meta_career.json", - "user://smoke_meta_career_v5.json", "user://smoke_meta_treasure_unlock.json", "user://smoke_meta_treasure_blank.json"] + "user://smoke_meta_career_v5.json", "user://smoke_meta_treasure_unlock.json", "user://smoke_meta_treasure_blank.json", + "user://smoke_meta_forge.json", "user://smoke_meta_forge2.json", "user://smoke_meta_forge3.json"] func _meta_smoke_cleanup() -> void: