diff --git a/game/core/config_db.gd b/game/core/config_db.gd index dbdfe8fa..4b316db8 100644 --- a/game/core/config_db.gd +++ b/game/core/config_db.gd @@ -342,20 +342,6 @@ func shop_price_b() -> float: return float(shop.price_b) if shop != null else 0.0 -# relic 保底卡基价 / 散折(economy_spend 激活时购买价折扣——表未入库 = 表 schema 缺省值) -func shop_relic_base_price() -> float: - return float(shop.relic_base_price) if shop != null else 0.0 - - -func shop_spend_discount() -> float: - return float(shop.spend_discount) if shop != null else 0.0 - - -# 英雄显示名(单位快照读取面单源——与敌人行 display_name 同轴) -func hero_display_name() -> String: - return String(hero.display_name) if hero != null else "" - - # 行族数组读取(ShopTable 属性名 = 行族名;表未已入库/属性缺失 = 空集非错误态) func _shop_rows(family: String) -> Array: if shop == null: diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index 71c5c4b9..2cedb72f 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -763,7 +763,7 @@ func current_price(prow: Dictionary) -> int: # + 池货架面 pool_price 同折〕——商店刷新价 shop_refresh_cost 与三选一付费刷新不折) func _spend_discounted(price: int) -> int: if _tree_rules.has("economy_spend"): - return maxi(1, int(floor(float(price) * db.shop_spend_discount()))) + return maxi(1, int(floor(float(price) * float(db.shop.spend_discount)))) return price @@ -879,7 +879,7 @@ func pool_price(kind: String) -> int: var row: Dictionary[Variant, Variant] = db.shop_pool_row(kind) if row.is_empty(): if kind == "relic": - return _spend_discounted(int(ceil(db.shop_relic_base_price() * pow(db.shop_price_b(), float(maxi(0, int(level_row.level_index) - 1))) * pow(db.shop_price_a(), float(_shop_visit_buys))))) + return _spend_discounted(int(ceil(float(db.shop.relic_base_price) * pow(db.shop_price_b(), float(maxi(0, int(level_row.level_index) - 1))) * pow(db.shop_price_a(), float(_shop_visit_buys))))) return 999999 var base := float(maxi(1, int(row.get("base_price", 1)))) var price_f: float = base * pow(db.shop_price_b(), float(maxi(0, int(level_row.level_index) - 1))) \ diff --git a/game/core/unit_container.gd b/game/core/unit_container.gd index 4b89a5e6..47a5f2da 100644 --- a/game/core/unit_container.gd +++ b/game/core/unit_container.gd @@ -52,7 +52,7 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides return false # max_hp = 生效上限(非入参 hero_hp)——跨战斗携带 HP 时上限不得压扁 # (恢复点/治疗包的满血守卫与治疗上限随之失效) - _hero = {"hp": hero_hp, "max_hp": eff_max_hp, "atk": eff_atk, "is_hero": true, "alive": true, "display_name": db.hero_display_name(), "enemy_id": "hero", "armor_type": hero_armor_type if hero_armor_type != "" else db.forge_default_armor_type(), "attack_type": hero_attack_type if hero_attack_type != "" else db.forge_default_attack_type()} # 护甲/攻击类型轴快照(读注入值,空串回退锻造工坊配置默认值 normal/light;甲效矩阵消费面) + _hero = {"hp": hero_hp, "max_hp": eff_max_hp, "atk": eff_atk, "is_hero": true, "alive": true, "display_name": String(db.hero.display_name), "enemy_id": "hero", "armor_type": hero_armor_type if hero_armor_type != "" else db.forge_default_armor_type(), "attack_type": hero_attack_type if hero_attack_type != "" else db.forge_default_attack_type()} # 护甲/攻击类型轴快照(读注入值,空串回退锻造工坊配置默认值 normal/light;甲效矩阵消费面) _enemies.clear() # 难度曲线与Boss DifficultyTable 修饰:施加于行值、行值快照之前——MVP 空表 = 恒 1.0 无行为(AC-10);混编行集内全部敌种同乘((L,D) 精确匹配一次取值——F9 同乘不改变异,敌人系统 v0.15) var mod: Dictionary[Variant, Variant] = db.resolve_difficulty_modifiers(level_index, node_depth) var hp_mult: float = float(mod.get("hp_mult", 1.0)) diff --git a/game/data/schemas/shop_table.gd b/game/data/schemas/shop_table.gd index 27837afe..8028e36e 100644 --- a/game/data/schemas/shop_table.gd +++ b/game/data/schemas/shop_table.gd @@ -22,11 +22,24 @@ const SHELF_KINDS := ["attack", "max_hp", "armor", "ap", "feat_ticket", "heal"] func validate() -> Array: var errs: Array[Dictionary] = [] - # 行族空护栏(legacy 四轴 + pool_shelf 皆为 ConfigDB 消费面——缺族即致命) + _validate_families(errs) + _validate_gp_drops(errs) + _validate_level_bonus(errs) + _validate_products(errs) + _validate_pool_shelf(errs) + _validate_price_factors(errs) + return errs + + +# 行族空护栏(legacy 四轴 + pool_shelf 皆为 ConfigDB 消费面——缺族即致命) +func _validate_families(errs: Array[Dictionary]) -> void: for family in ["gp_drops", "level_gp_bonus", "products", "price_level_bonus", "pool_shelf"]: if get(family).is_empty(): errs.append({"category": "empty_table", "message": "ShopTable 行族为空: %s" % family}) - # gp_drops:tier 词表封闭 + 掷定域 + 均值锚 + tier 唯一 + + +# gp_drops:tier 词表封闭 + 掷定域 + 均值锚 + tier 唯一 +func _validate_gp_drops(errs: Array[Dictionary]) -> void: var seen_tier := {} for row in gp_drops: var tier: String = row.get("tier", "") @@ -44,7 +57,10 @@ func validate() -> Array: errs.append({"category": "domain", "message": "gp_drops %s 掷定域越域 [0,999]: %d..%d..%d" % [tier, rmin, mean, rmax]}) if rmin > mean or mean > rmax: errs.append({"category": "cross_field", "message": "gp_drops %s 违反 roll_min(%d) ≤ mean(%d) ≤ roll_max(%d)——均值恒等期望锚" % [tier, rmin, mean, rmax]}) - # level_gp_bonus / price_level_bonus:level_index 连续从 1 递增 + 加值域 + + +# level_gp_bonus / price_level_bonus:level_index 连续从 1 递增 + 加值域 +func _validate_level_bonus(errs: Array[Dictionary]) -> void: for family in ["level_gp_bonus", "price_level_bonus"]: var arr: Array[Variant] = get(family) var seen := {} @@ -61,7 +77,10 @@ func validate() -> Array: if li != i + 1: errs.append({"category": "structural", "message": "%s level_index 不连续(须从 1 连续递增): 位 %d = %d" % [family, i + 1, li]}) break - # products:product_id 唯一 + attr 词表 + 价格域 [1,999] + + +# products:product_id 唯一 + attr 词表 + 价格域 [1,999] +func _validate_products(errs: Array[Dictionary]) -> void: var seen_pid := {} for row in products: var pid: String = row.get("product_id", "") @@ -85,7 +104,10 @@ func validate() -> Array: var inc: int = int(row.get("price_increment", -1)) if inc < 0 or inc > 999: errs.append({"category": "domain", "message": "%s price_increment 越域 [0,999]: %d" % [pid, inc]}) - # pool_shelf:kind_id 词表封闭(四切边后种)+ 唯一 + display_name + base_price 域 [1,999] + + +# pool_shelf:kind_id 词表封闭(四切边后种)+ 唯一 + display_name + base_price 域 [1,999] +func _validate_pool_shelf(errs: Array[Dictionary]) -> void: var seen_kind := {} for row in pool_shelf: var kind: String = row.get("kind_id", "") @@ -99,7 +121,10 @@ func validate() -> Array: var base: int = int(row.get("base_price", 0)) if base < 1 or base > 999: errs.append({"category": "domain", "message": "pool_shelf %s base_price 越域 [1,999]: %d" % [kind, base]}) - # 价格因子域(乘法语义——A/b 均 ≥1 单调不降;上界防手滑投值;provisional 值本体归难度曲线与Boss) + + +# 价格因子域(乘法语义——A/b 均 ≥1 单调不降;上界防手滑投值;provisional 值本体归难度曲线与Boss) +func _validate_price_factors(errs: Array[Dictionary]) -> void: if price_a < 1.0 or price_a > 3.0: errs.append({"category": "domain", "message": "price_a 越域 [1.0,3.0]: %f" % price_a}) if price_b < 1.0 or price_b > 2.0: @@ -108,4 +133,3 @@ func validate() -> Array: errs.append({"category": "domain", "message": "relic_base_price 越域 [1.0,99.0]: %f" % relic_base_price}) if spend_discount <= 0.0 or spend_discount > 1.0: errs.append({"category": "domain", "message": "spend_discount 越域 (0.0,1.0]: %f" % spend_discount}) - return errs diff --git a/game/presentation/views/battle_end_overlay.gd b/game/presentation/views/battle_end_overlay.gd index cd77093e..bfb40e2e 100644 --- a/game/presentation/views/battle_end_overlay.gd +++ b/game/presentation/views/battle_end_overlay.gd @@ -24,7 +24,6 @@ var _bonus_row_l: HBoxContainer var _bonus_name: Label var _bonus_val: Label var _cele: TextureRect # 庆祝立绘(idle 占位浮动) -var _celebrate_tw: Tween # 庆祝浮动循环句柄(每调先 kill,close 必杀——无限循环禁裸创建) var _btn: Button var _board_rect := Rect2() @@ -159,9 +158,10 @@ func close() -> void: _seal.visible = false _seal.modulate = Color.WHITE _cele.visible = false - if _celebrate_tw != null and _celebrate_tw.is_valid(): - _celebrate_tw.kill() - _celebrate_tw = null + var tw: Tween = get_meta(&"celebrate_tw", null) # 循环句柄随节点存(跨 open/close,close 必杀) + if tw != null and tw.is_valid(): + tw.kill() + remove_meta(&"celebrate_tw") _clear_crest_bricks() @@ -285,10 +285,11 @@ func _play_dust_sadle() -> void: func _play_celebrate() -> void: if UiKit.motion_scale < 1.0: return - if _celebrate_tw != null and _celebrate_tw.is_valid(): - _celebrate_tw.kill() - _celebrate_tw = _cele.create_tween() - var tw := _celebrate_tw + var prev: Tween = get_meta(&"celebrate_tw", null) + if prev != null and prev.is_valid(): + prev.kill() + var tw := _cele.create_tween() + set_meta(&"celebrate_tw", tw) tw.set_loops() tw.tween_property(_cele, "position:y", _cele.position.y - 4.0, UiKit.motion(0.6)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) tw.tween_property(_cele, "position:y", _cele.position.y, UiKit.motion(0.6)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) diff --git a/game/presentation/views/standee_body.gd b/game/presentation/views/standee_body.gd index a5396a34..d946c735 100644 --- a/game/presentation/views/standee_body.gd +++ b/game/presentation/views/standee_body.gd @@ -19,7 +19,6 @@ var slot_row: StatusSlotRow # 状态效果唯一展示面 = 壳层伴随 var _kind := "enemy" var _portrait_sprite: Sprite2D = null # 资产挂点(null = 未落位,字形在位) var _portrait_tex: Texture2D = null # 落位纹理引用(set_portrait_texture 装载;字形回退判据) -var _portrait_flip := false # 主角三态切换:state 名 → 纹理(battle_stage_view 组合点注册);缺态=保持现纹理 # (缺位回退链延伸——三态素材缺任一不炸不空)。敌站桩不注册不消费。 var _hero_tex := {} @@ -96,7 +95,7 @@ func set_data(d: Dictionary) -> void: _armor = String(d.get("armor_type", "light")) _hp_max = float(maxi(1, int(d.get("max_hp", 1)))) _set_hp(int(d.get("hp", 0))) - set_order(int(d.get("order", 1))) + _set_order(int(d.get("order", 1))) set_pending(String(d.get("pending_type", "")), String(d.get("pending_skill", ""))) set_effects(d.get("effects", [])) _refresh_portrait_glyph() @@ -149,7 +148,7 @@ func set_effects_cleared_feedback() -> void: tw.tween_callback(note.queue_free) -func set_order(i: int) -> void: +func _set_order(i: int) -> void: # 顺位徽标 = 行主序生成序(固定顺位——敌人系统 顺位标记义务) _order = i queue_redraw() @@ -207,7 +206,6 @@ func _play_intent_motion() -> void: # flip = 立绘朝向翻转(素材基准朝左时传 true 呈现朝右)。盒高 96px = 48px 立绘源整数 2× # (圣经 §8.2 禁非整数缩放)——源换代时随源重算。 func set_portrait_texture(tex: Texture2D, flip: bool = false) -> void: - _portrait_flip = flip if tex == null: if _portrait_sprite != null: _portrait_sprite.visible = false diff --git a/game/tests/unit/core/level_shop_economy_test.gd b/game/tests/unit/core/level_shop_economy_test.gd index a0de0c92..0288f789 100644 --- a/game/tests/unit/core/level_shop_economy_test.gd +++ b/game/tests/unit/core/level_shop_economy_test.gd @@ -259,14 +259,14 @@ func _init() -> void: func _smoke_table_migration() -> void: print("--- 数值入表(shop 两 knob + hero 显示名 · 原值迁移)---") - Kit.check("shop.relic_base_price 表值 == 迁移前字面量(15.0)〔wd11〕", _db.shop_relic_base_price() == 15.0) - Kit.check("shop.spend_discount 表值 == 迁移前字面量(0.8)〔wd11〕", _db.shop_spend_discount() == 0.8) - Kit.check("hero.display_name 表值 == 迁移前字面量(探险者)〔wd16〕", _db.hero_display_name() == "探险者") + Kit.check("shop.relic_base_price 表值 == 迁移前字面量(15.0)〔wd11〕", _db.shop.relic_base_price == 15.0) + Kit.check("shop.spend_discount 表值 == 迁移前字面量(0.8)〔wd11〕", _db.shop.spend_discount == 0.8) + Kit.check("hero.display_name 表值 == 迁移前字面量(探险者)〔wd16〕", _db.hero.display_name == "探险者") # 消费端读表:spawn 真实构造链的英雄单位显示名与表值同 var units = load("res://core/unit_container.gd").new(_db) var spawned: bool = units.spawn(1, 0, int(_db.hero.max_hp)) Kit.check("spawn 后英雄单位 display_name 读表同值(探险者)〔wd16〕", - spawned and String(units.hero_unit().get("display_name", "")) == _db.hero_display_name()) + spawned and String(units.hero_unit().get("display_name", "")) == String(_db.hero.display_name)) # ---- 生成图深度扩面覆盖断言(迭代 35 卡② S5/V11——节点源=卡① 生成器产图, @@ -1301,7 +1301,7 @@ func _smoke_shop_pool() -> void: Kit.check("货架 kind 在封闭词表或保底 relic 卡(%s)" % ckind, is_relic or shelf_kinds.has(ckind)) var expect_p: int if is_relic: - expect_p = int(ceil(_db.shop_relic_base_price() * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0)) + expect_p = int(ceil(float(_db.shop.relic_base_price) * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0)) else: var prow: Dictionary = _db.shop_pool_row(ckind) expect_p = int(ceil(float(int(prow.get("base_price", 0))) * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0)) diff --git a/game/tests/unit/presentation/standee_fx_sweep_test.gd b/game/tests/unit/presentation/standee_fx_sweep_test.gd index bc38985a..b533a59b 100644 --- a/game/tests/unit/presentation/standee_fx_sweep_test.gd +++ b/game/tests/unit/presentation/standee_fx_sweep_test.gd @@ -104,13 +104,14 @@ func _init() -> void: var ov = BattleEndScript.new() root.add_child(ov) ov.open_victory("判定行", "继续", Rect2(0, 0, 100, 100)) - var first: Tween = ov._celebrate_tw + var first: Tween = ov.get_meta(&"celebrate_tw", null) Kit.check("首开胜利画面创建庆祝句柄〔wd4〕", first != null and first.is_valid()) ov.open_victory("判定行", "继续", Rect2(0, 0, 100, 100)) + var second: Tween = ov.get_meta(&"celebrate_tw", null) Kit.check("重开胜利画面:旧句柄已 kill、新句柄在位(至多一个活跃)〔wd4〕", - first != null and not first.is_valid() and ov._celebrate_tw != null and ov._celebrate_tw.is_valid()) + first != null and not first.is_valid() and second != null and second.is_valid()) ov.close() - Kit.check("close 必杀庆祝句柄(句柄清空)〔wd4〕", ov._celebrate_tw == null) + Kit.check("close 必杀庆祝句柄(句柄清空)〔wd4〕", ov.get_meta(&"celebrate_tw", null) == null) # ---- ③ 互斥四口(d5/d8)---- var dbg = DebugOverlayScript.new(null, 0, 0) diff --git a/game/tests/unit/presentation/theme_projection_test.gd b/game/tests/unit/presentation/theme_projection_test.gd index dd71c5b6..09bb9178 100644 --- a/game/tests/unit/presentation/theme_projection_test.gd +++ b/game/tests/unit/presentation/theme_projection_test.gd @@ -690,8 +690,8 @@ func _smoke_contrast_inks() -> void: sb_src.contains("HORIZONTAL_ALIGNMENT_LEFT, -1, 13, UiKit.GOLD_BRIGHT")) Kit.check("V4 魔法护甲徽标墨=HERO_BLUE_BRIGHT", sb_src.contains("col = UiKit.HERO_BLUE_BRIGHT")) Kit.check("V4 重甲徽标墨=GOLD_BRIGHT", sb_src.contains("col = UiKit.GOLD_BRIGHT")) - Kit.check("V4 GOLD_DEEP 语义岗不动(standee:199 意图动画判据)", - sb_src.contains("if _intent_col == UiKit.GOLD_DEEP:")) + Kit.check("V4 意图蓄力档判据=分档布尔(standee_body:179 CHARGE_SKILL_IDS 判定;旧 GOLD_DEEP 恒不可达判据清零)〔wd2〕", + sb_src.contains("_intent_charge = pid in CHARGE_SKILL_IDS") and not sb_src.contains("if _intent_col == UiKit.GOLD_DEEP:")) var ap_src: String = FileAccess.get_file_as_string("res://presentation/views/ap_bar_view.gd") Kit.check("V4 RAP 反馈行墨=STONE_DEEP(原位行锚定)", ap_src.contains('UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)')) @@ -807,7 +807,7 @@ func _all_labels(root: Node) -> Array: # ---- 生涯页只读投影一致性(汇总段六类九项逐行 == SaveManager 计数访问器/ -# MetaProgression 访问器读数;投影拼装与 main._career_data 同构(同访问器族同序)。 +# MetaProgression 访问器读数;投影拼装与 HudPresenter.career_data 同构(同访问器族同序)。 # 只读性连接巡检与零动效断言归 ui_struct_probe 呈现侧,本段只管数据面+旧档载入显示 # (v5 样档升 v6——完成定义 6「计数域缺省补零、页正常显示」))---- func _career_overlay_projection() -> void: @@ -822,7 +822,7 @@ func _career_overlay_projection() -> void: sv.apply_adventure_summary({"result": "abandoned", "cleared": false, "levels_cleared": 1, "stopped_at": 2, "keystone_total": 0, "master_seed": 22}) sv.write_to_disk() - # 投影拼装(main._career_data 同构)+ 视图喂入 + # 投影拼装(HudPresenter.career_data 同构)+ 视图喂入 var data := {"total": sv.adventure_total(), "cleared": sv.adventure_cleared(), "clears": [], "boss_first_kills": 0, "boss_first_total": _db.level_count(), "boss_kills": sv.boss_kill_count(), "keystone": mp.keystone_balance()}