Compare commits

..
Author SHA1 Message Date
DKChan ff5fcd644d 迭代46收尾文档(审计结论+审计修正): 独立验收审计通过(零缺漏——对照表逐行/自跑八套件/削弱全申报面/无变异撤销取证,报告落reports)·处置审计附加发现①两新件.uid边车补提交②S8_FILES与S7_B1_SET同步增补battle_refresh_flow(新件零await,S8/S7实跑绿)·实现报告补审计结论节 2026-10-31 09:20:13 +08:00
DKChan c10bbc1114 迭代46收尾文档(实现报告+反哺点): 卡A/B收口·卡C停摆(L3#2实板可读面不达标——14×14基座档28px<30px,停机呈报落主区reports)·实际对照表V1-V12与卡B六行·结构预算S13前后·目检五项登记·实现决定十一条备案·反哺点九条(奇物/行动经济/战斗结算/地图战斗资源/architecture三行/art-bible两行/battle.md随裁决/术语表零增) 2026-10-31 08:56:57 +08:00
DKChan fde1795dd0 迭代46卡B(名字行提档与地台色合并): 名字行五取值点FS_CAPTION→FS_HINT(12→14·消散提示:114合法位零触碰)·_name_baseline偏移17→18(0.35h字号档推导14×0.35≈5,T0实测ts=(14,20)记录)·name_line_probe增name_font_size键·ui_kit删STAND_STONE_EDGE/STAND_STONE_RIM两常量+原位增PLAZA_EDGE=Color8(76,64,56)·三绘制点改指(描边→PLAZA_EDGE·顶缘线/口沿高光→BRICK)·standee测试V6换代149→150(9px档→8px档)+V10/V11/V12增量(探针实读/两绘制点源码锚/FS_CAPTION计数锚==1/常量表退役零键/BRICK·PLAZA_EDGE逐位/改指源码锚)·golden三张0.00%留痕(换值色差≤5低于容差≤16——重锚无需执行)·grep退役常量仅剩测试断言串(预期例外)·26 checks全绿·gdstyle零新增键 2026-10-31 08:47:56 +08:00
DKChan 45b53d6897 迭代46卡A-T8修正(S7): S7_B1_SET增补battle_refresh_flow.gd——换图刷新流程件=C13拆分边界的B1域拆件(承载旧资源retire_mapping/装板编排调用,卡A§八明列新结算族件);判据口径不变(B2/B3/B4对外API仍限B1域调用);architecture模块归属BattleRefreshFlow行走反哺点 2026-10-31 08:34:07 +08:00
DKChan f498d2ec85 迭代46卡A-T8修正(S9): 四处显式非参数化容器注解改:=推断(R1无类型容器——emit返回值承接形态;零行为改动) 2026-10-31 08:24:26 +08:00
DKChan bf7aa514b6 迭代46卡A-T6(数据行与牌库计数): adventure_feats换代37行(普通14/优秀13/稀有10/传说0·牌库102+37=139——旧30行断言按实数换代零放宽)·V11专项(validate零违例·七行稀有度cap对表·desc禁词·七件计数器引用闭合[value_counter/when-id全有写入方——预存行消费点记账形态不入判据域]·三计数器在册)·refresh_feats 85 checks绿·adventure_feats 138 checks绿 2026-10-31 08:08:37 +08:00
DKChan 208e5652db 迭代46卡A-T5(五件后续效果): 授予段round_end heal value_counter兑付(坑道小憩——units.heal封顶·满血未生效行·先于set清零行消费层值)·石层缓冲/蓄力周转全走既有消费面(统一damage_taken与attack_bonus value_counter零新码)·feats.tres+3行(石层缓冲common/蓄力周转excellent/坑道小憩common·trigger结构按卡§十.T6)·V6全局固定减伤(pierce8→6双刷不叠加·未刷新轮不减·地形max(下限,ev−2))·V9轮末恢复(白盒预置上限3闸·回复3层清0·满血未生效·自然累计2)·V8蓄力消费(攻击后清0·战斗新建0)·V7延迟群伤(刷新零伤害行·战斗段末×2)·窄测三件全绿(44/70/134) 2026-10-31 08:06:06 +08:00
DKChan be7ef54e30 迭代46卡A-T4(成功事实与命中施加): feats.tres+2行(崩壁冲击/矿道红利——群伤open_ticket与grant_gp订阅面)·V4成功事实专项(非Active/每轮上限/RAP不足三拒面零事实零换图——成功恰一次经probe_br_counter计数与board_changed双观测)·V10矿道红利专项(escrow每次成功+1·fx_grant_gp行在案·拒绝零变化)·refresh_feats 48 checks全绿 2026-10-31 07:56:07 +08:00
DKChan c062b62d12 迭代46卡A-T3(两件基座): action_economy dig_paid命中环累计grant_reserve_ap(扣费受理后一次性入池·连锁不二次·拒绝轮零入池)+request_refresh收折后价入参(缺省-1=表基线逐位不变)·新建BattleRefreshFlow(可用性门/折价扣费/换图/成功事实——C13拆分边界,_effective_refresh_cost声明序第一命中floor(base×mult/100),can与执行同源)·编排器两公开方法单行转发(删旧刷新体,C10行数只降)·授予段apply_board_refreshed成功事实单点+奖励授予件defer_ticket案C同队列(类变量零增员)+来源豁免fx_board_refresh·feats.tres+2基座行(矿脉储能/省力周转·excellent·cap1)·T3四专项+V12侧全绿·round_flow 134 checks回归绿 2026-10-31 07:52:14 +08:00
DKChan b44d3234b8 迭代46卡A-T2(全局damage_taken统一): 新增_emit_hero_damage_taken唯一发射口(payload必含kind/amount/source/cell/round/hp_ratio)·_enemy_shield_apply改名_apply_hero_damage_effects=唯一消费族(组合次序shield→damage_reduce→ac_bonus→counter_add冻结)·敌方攻击改走统一口(掷点后交付前时点不变)·_resolve_board_flat_hero下限前接入(hero_damage_consumed标记闸防重复)·地形路径并入全局消费族并删本地_terrain_damage_reduce(全局受伤语义=shield同面消费)·私有可能净增1≤2·refresh_feats_test新建V5两专项(四路径逐路恰一次+无重复消费,临时承伤计数专长经ConfigDB注入)·窄测refresh_feats/damage_controller/seal_polarity/round_flow全绿(44/70/134 checks) 2026-10-31 07:44:24 +08:00
DKChan 94a05afd49 迭代46卡A-T1(词表扩签): EVENTS+refresh_request/board_refreshed(20→22)·MINE_EVENTS同步换图刷新面·EFFECT_KINDS+grant_reserve_ap/apply_discount(32→34)·零fact/op/DO_KEYS新增·forge_table计数断言20/32→22/34换代·effect_words新增两事件两效果在册断言(旧出域负例全保留)·孤立验证两件全绿(20 checks/exit0) 2026-10-31 07:35:16 +08:00
DKChan b5b2c55557 docs: 迭代46定义已入库 2026-10-31 07:25:42 +08:00
DKChan e153e8a3c3 美术(战斗区背景入包批): 三关背景与共用地面终稿入包 game/assets/art/scene/——作者选定 dungeon v10c/underdark v10c/deep_underdark v10c/ground v12b;背景 1280×252 不透明 32 色(实测平均 L 0.0148/0.0132/0.0111),地面 1280×100 顶缘透明 32 色,按作者裁保留原亮度(不透明域 L≈0.132,低于 §2 世界中层带 0.22–0.45,登记为作者例外);art-bible v3.29→v3.30:§6.1 新增「B 区舞台背景与共用地面的交付绑定」条(尺寸/色数/alpha/读数/例外指向)+ground_h 终稿高 100 回填句+资产渠道句现状化;art-status-checklist 增交付行、art-assets-checklist 现状句换代、asset-path-map scene 行区分「战斗背景已入包/营地仍登记超前」;battle-stage-prompts v1.7→v1.8 状态句换代;待办清单实况记选定与读数并保留 STAGE_BG 随代码批消化;入包对应代码侧需求卡片在册(board_type 选图/共用地面层/ground_h 落表/四柱光柱与 STAGE_BG 退役待迭代编排);验证=path_check 0 问题+pipeline_gate PASS+文档域闸全绿 2026-10-31 06:47:29 +08:00
DKChan 770bfe62ae 订正(账面): 当前位下一步换代——迭代46已由并行编排提交da24047c编入三卡(换图系双基座七件批+站桩名字行提档与地台色合并批+棋盘格边长口径收口批,≈9.125工作日),本笔iter45收尾提交行文时该编排未入眼,「两卡入编排待发令」过时;改=iter46待开工待用户iter发令,待编池不动 2026-10-31 06:43:26 +08:00
DKChan 457e073901 收尾(迭代45批后收尾): 反馈点七条处置闭合——①architecture落文六条: breakpoint_store行现状化(已建件-迭代45,构造句改述=九引用束解包原名成员保迁移函数体逐字+resume_adventure留宿主壳与宿主侧回填语句按R2所有权缝分家+实测类变量15贴顶禁再增)+board_hover行现状化(引用束实为7项=三引用+四Callable,卡片预估5项偏差实况在案)+单格两件与站桩两件两行现状化(成员数12/13与25/12/11及宿主30→10/19→10与实现一致零改述)+gdstyle计数截断缺陷节补清偿结果(两件已随迭代45真实清偿且基线refresh收账,上游报issue仍在册);第6条adventure_runner/board_view键账终值半=基线已在册承载(31/23留债收账归refresh批已完成),architecture无对应计数行零落文面;②第7条工具观察迁待办新条(run_all_gates 02关联档多套件词传参与run_all选跑解析器不兼容——两修法待用户裁决,直改类待执行);③目检三项登记体验验证清单㊸(悬停三语义抽验/站桩意图三档与三态观感/单格六步时序整帧零预期差异确认);④产出物退役九件(吸收即死+证据只留最新): iter45实现报告/反哺点/验收审计/两停机呈报+呈现层状态件批/gdstyle尾波补件批两卡+单格绘制状态件/站桩状态件两设计过程稿——判据全过: 反馈点七条全处置(1-6落文/7登记),遗留义务全迁移(键账终值=基线在册/02传参=待办新条/目检三项=㊸),引用面=roadmap行保留字+尾波条目随整删出清;⑤账面: gdstyle尾波补件批条目整条删除(两卡合回+键账留债收账在册,状态机出口),roadmap迭代45行状态已完成+卡片/报告两列退役保留字+当前位换代(迭代1-45全完成,下一步=两卡入编排待用户发令) 2026-10-31 06:41:41 +08:00
DKChan da24047c51 编排(迭代46): 三卡编入——换图系双基座七件批(8.125天中值,凭据=奇物v1.3+行动经济v1.3+战斗结算v1.4+地图战斗资源v1.3)+站桩名字行提档与地台色合并批(0.5天,凭据=art-bible v3.29)+棋盘格边长口径收口批(0.5天,无设计线段实况证据指认);卡序照用户点名1+3+4抄写,合计中值9.125工作日居5-10常规区间零特批;roadmap迭代46行待开工+三待办条目推进已编迭代46(交叉核4链);四查全过(三卡盘上在/凭据齐/估时9.125/流水线闸绿) 2026-10-31 06:32:26 +08:00
DKChan 78c8f83419 文档(载体默认换代批·用户裁): 图优先载体默认 HTML+SVG——AGENTS.md 载体句换代(默认 visualize 技能 HTML/SVG 内联渲染·流程结构图与空间对比像素示意同一通道·无内联渲染退 Mermaid·再退字符画)+doc-conventions §3 流程行换代(默认文档内嵌 HTML/SVG 表现力优先·图体行数计入节点篇幅·过滤型网页端改 Mermaid·无渲染环境字符画)+节点内顺序句随改;2026-10-31 用户裁,理由=表现效果更好,接受网页端过滤内嵌 SVG 与 git diff 可读性代价 2026-10-31 06:15:08 +08:00
DKChan e34dceb1a9 文档(图优先表达批): AGENTS.md 回答风格规则加图优先小节+格式优先级换代——判据=多实体明确关系(顺序/包含/依赖/状态转移/布局对比)内容直接画图不写长段,单点事实与纯数据不画;载体=节点连边图默认Mermaid代码块·空间对比与像素级示意走visualize技能HTML/SVG·无渲染查看端退回字符画;边界=硬数值与前提留文字·图不承载唯一数值依据·图文禁重复;doc-conventions 表达分工同步扩(§3流程行扩「字符画或Mermaid」收编结构关系类,复杂结构图优先Mermaid源码即文本,§2箭头链句与节点内顺序句随改);依据=cnoe-io/LMCache/vercel-labs三份AGENTS.md在案条款+Anthropic系统提示词artifacts图示必须可视化规则,2026-10-31 用户裁 2026-10-31 06:04:46 +08:00
DKChan d478cf70ea 写卡(站桩名字行提档与地台色合并批): 需求卡片v1.1定稿零待裁点——名字行五取值点FS_CAPTION→FS_HINT+基线0.35h重算(牌顶+18)+probe增name_font_size键/STAND_STONE_RIM·EDGE两常量退役改指(受光线与口沿高光→BRICK,描边→新PLAZA_EDGE承载#4C4038)/golden板面三张申报重锚(§3申报制);断言面V10-V12三锚三件(probe实读+源码锚+FS_CAPTION计数锚==1+常量表经load().get_script_constant_map——类引用直调parse error grilling实测)/V6基线149→150申报+run_all.gd注释换代;grilling复核三阻断两建议全落(V11写法parse error/三测量点缺判据/三绘制点改指缺正证/对照表对应句/结构图漏行);能力清单补行落表(迭代43移交小件①了结——站桩呈现定值与素材绑定行+覆盖域表战斗表现行,README工作树同批含并行会话换图系卡能力行五行顺带落地);待办清单两点条目+已写卡·移交小件①落位;估时0.5工作日 2026-10-31 05:59:52 +08:00
DKChan 789b15f34f 设计(迭代43批后待裁两点): 名字行提档FS_HINT 14归六档+换色合并三处拆解——§7.4小字号豁免条款补站桩名字行句(必读信息不入②装饰caption在册面,几何随档联动,验收句扩两绘制位取FS_HINT)+§4.1换色合并登记句换代(①地台顶缘受光线②地台描边两对照合并执行=绘制点改指目标色+源常量删除,③舞台底对改注随§6.1舞台背景插画批一并处置退役或改指禁单独改值出批,均2026-10-31用户裁);待办清单两点条目闭合(文档设计已闭合,凭据=art-bible v3.29入实况,缺口删除)+战斗区背景图替换条目实况补STAGE_BG随批消化句;v3.28→v3.29 2026-10-31 05:18:31 +08:00
DKChan 9eedf8965d 收尾(迭代44批后收尾): 反馈点六条处置闭合——①architecture落文六条: ui_registry/RoundCycle/BattleFacts三行现状化(已建件-迭代44,四件类变量3/9/5/14与ModalFlow随容器构造实况,两新件构造句按实态=三参直构/零参直构,实测值8·3/15·4换代)+装配节两句实况(ModalFlow先在座/shot与fx_shot取证探针随批改址)+main_ui_build行层序事实源句改指ui_registry.LAYER_CONTRACT+ui.apply_layer_contract+S13测量面注销句现状化(收尾现行形态=run()尾每批一次,申报剥离凭据215da315/v1.1重裁在案)+S13豁免第三条注销句转已实施(宿主实测15);附带main_overlay_flow行模态口指址刷新(root.ui.modals.flow.open/close——与头注词面修复合拍,验收审计缺漏2同族);②gdstyle基线refresh收账(--refresh --force,16→5键): 消失=main.gd49/orchestrator38+文件长1035/tune_sim await/board_cell_unit30/standee_body19五键清偿+iter42归绿6键离账(board_view allocation+_paint_cell三键/level_runner _enter_node),重登记=留债三键按实测28→23/32→31/50→33,零新增键(布局批新码gdstyle全净);③目检两项登记体验验证清单㊷(三屏切换与模态开合/战斗一局全流程观感);④产出物退役十件(吸收即死+证据只留最新): iter44实现报告/反哺点/验收审计+组合根注册面/战斗编排归约/测速循环三卡+组合根注册面/战斗编排类变量归约/测速循环去await三设计过程稿+grilling五卡复核——全部判据核过: 反馈点11条全处置(1-6落文/7 refresh本笔执行/8条件性观察无未决随本笔备案/9净零/10被条目删除覆盖/11迁待办新条),遗留义务全迁移(目检㊷/refresh基线/头注已修/死preload登记),引用面=roadmap行保留字+战斗域条目改git提交号;⑤账面: 零豁免五键清偿批条目整条删除(五键全清+两迭代合回+refresh收账,状态机出口),战斗域结构深拆批条目净零重立(宿主瘦身子项完成记实况引bee73db5,原卡片引用随退役改git提交号——多子项条目与卡片死期规则的闸机结构冲突按net-zero先例解),main.gd死preload八条新登记(直改类待执行),roadmap迭代44行卡片/报告两列退役保留字+当前位换代(迭代1-44全完成,下一步=iter45收尾) 2026-10-31 05:12:31 +08:00
DKChan 4b174877be 补提交(布局几何单点化批·代码面+工具面+architecture行·跨会话遗留): battle_layout.tres 新表单行五字段(stage_h/info_panel_w/ap_bar_h/log_band_h_dig/log_band_h_settle)经 schema battle_layout_table.gd+域容器 config_domain_presentation.gd 收进 ConfigDb.presentation(ConfigDB 三容器→四容器,manifest 增 battle_layout 键,B区高度裁决 2026-10-30 用户裁=取代码标准 stage_h);views 五件构造签名增 layout 参(BattleStageView/HeroInfoPanelView/BoardZoneView/ApBarView/BattleLogView 常量高度改表值驱动)+ui_assembler 装配传 layout+probes 两件改址+main.gd 结算带注释去数值;tools/layout_config.py 新件+render_sample_sheet 两版读布局表现算(样张工具面);文档面: architecture.md ConfigDB 行换代(四容器成员 7/7/5/1、类变量 10)+HUD框架.md 三区表/装板不等式/调参表去数值化改指布局表+数据配置.md+interaction-patterns.md 明细带句去数值(battle.md 区表与待办清单登记面已随 1cdef6ba 在库);消费漏网 L1 就地补改九处(构造签名换型与 iter45 断点件收编的测试改址漏网,判据零变化): theme_projection_test:386/player_copy_sweep_test:44,98/adventure_feats_test:772/standee_fx_sweep_test:75/feat_vouchers_test:292 六处补 layout 实参,adventure_feats_test:791/level_shop_economy_test:1480/feat_vouchers_test:81 三处断点成员直读改经 a._bp_store(iter45卡②六成员搬移漏网,02 闸合回后首全量暴露);load_tables 超函数长键清偿(容器装配块抽 _assign_loaded 私有协函数——表行按域收进四容器+resolver 注入单一职责,与 _cross_validate 委托先例同型,S13 新增红修至基线口径内);窄验: 四件修复套件单跑全绿+gdstyle 基线外新违规 0 2026-10-31 05:05:08 +08:00
DKChan 39907d82f6 工程(闸机提速批·随动件入库): ①run_all_gates.sh 文档域增量档本体已由并行会话 a7c63850 先行入库(同内容零冲突),本笔补齐其随动四件——ci_runner.sh 回执采信修正(回执 commit 与在跑尖端对齐才采信红项,不对齐写 note 标记:闸起手自检即中止时不写新回执,盘上旧回执当本次红项会撒谎——2026-10-31 实跑缺陷修正)+规约三份随动: AGENTS 改动四类与五段铁律的直改通道句收敛(『登记在案后由用户发令』→『由用户发令』),process-conventions §2 同句与写卡段配套维护句同步,doc-conventions §12.1 直改序列去登记段+条目定位句换代(『现在不做、以后才做的事』登记一条;条目记录走到哪一段,不是各段的开工前置) 2026-10-31 05:03:38 +08:00
DKChan f76c878e4f 补提交(跨会话旧报告欠账+uid边车): 四份在册引用而未跟踪的过程件入库——盘点-验证与清单体系-2026-10-28(接收方=待办清单『验证与清单体系整改』条目,阶段0盘点明细)+审计-全库架构审查-2026-10-29(接收方=待办清单『架构审查优化讨论』条目,七节读数)+工程改动报告-存量断言精简C档-2026-10-28(接收方=同条目阶段2闭合记录,6组33→7+冻结清单对账1587→1561)+评估-美术资产清单与性能预算-2026-10-30(接收方=待办清单卡④条目『评估报告同步』句,美术师交付物两章合并评估,未改权威文档);tune_sim_batch_parity_test.gd.uid 边车补齐(iter44卡③对拍套件.gd 在库而.uid漏提交——.gd/.uid同步纪律) 2026-10-31 05:01:33 +08:00
DKChan 21c4a5e915 美术(战斗区背景+营地场景图两线文档批·跨会话遗留入库): ①战斗区背景线——art-bible §6.1 场景构成换代(背景层=三关变体 scene_stage_bg_dungeon/_underdark/_deep_underdark 按 board_type 选图不走 tint,顶部空区+纵深洞厅远景三关母题地精/卓尔/灰矮人,不画近景地面不画光柱;连续地面层 scene_stage_ground 读 ground_h 三关共用;光柱不迁入背景,资产到位后四柱程序层直接退役)+battle-stage-prompts.md 新件入库(分层生图提示词卡,三变体+共用地面,依据 art-bible §6.1/§6.3/§4.1+布局表 stage_h/ground_h)+art-prompts §5.1 转指路+asset-path-map/art-assets-checklist 同步+art-pipeline-tools.md 新件(美术后处理与校验工具清单——tools/ 下像素化/校验/色板脚本用法,与 art-prompts/camp-scene-prompts 分工=出图与后处理两段)+待办清单战斗区背景条目实况缺口换代;②营地场景图线——2026-10-31 用户裁样张过审(保留全部待选稿,各素材第一张样本稿为终稿),art-bible §6.2 十张分层条款落文,camp.md v2.1→v2.2(scene_camp_bg 背景层 z0 不透明全幅+scene_camp_wall 围墙层 z1 门洞留透明缺口透出背景层小路+八物件层),camp-scene-prompts §2.3/§4 现行坐标与分层验收事实源,提案-营地场景图美术生成方案补现行口径指针(2026-10-22 讨论口径留痕不改,执行以五份现行文档为准),待办清单卡④缺口换代(P5 落文销号,样张过审+条款版本两项齐;余 P3/P4/§5 五待裁点/终稿后处理未清);在途图归位 artwork/2026-10-30-战斗区背景/(vibe_images 越界目录已清) 2026-10-31 05:01:06 +08:00
DKChan 2bc82febd3 文档(换图系双基座七件落文·跨会话遗留入库): 换图系专长流派设计定稿落四份 owner 文档——奇物 v1.1→v1.2(事件族增换图系第六族+设计原则补基座先行/行为改变检验两条+feats.tres 30行实数换代=基础27+迁入2+规则豁免1)+行动经济 v1.2+战斗结算 v1.3+地图战斗资源 v1.2(七件=掘进储能/折价换图/弃图轰击/换图蓄势/换图庇护/弃图休整/弃图回收;双基座可叠加,后续效果在本轮五段结算内兑付,弃图回收按旧图探索进度发清图奖励);game/tests/README.md 能力行六行入库(换图系五行=refresh_feats_test.gd 计划测试锚定,代码未随批未建;另布局几何单点化批的『战斗屏三区几何与棋盘格边长』覆盖行+覆盖域表 HUD框架 行扩句随同文件搭车——其代表用例 board_gen_reward_test._smoke_layout_cell_formula 系棋盘格边长口径收口卡计划测试);提案-弃图刷新系专长流派 v0.4 落档(接收方=待办清单『封锁-蓄力combo设计素材』条目,用户裁保留为写卡参考凭据) 2026-10-31 05:00:37 +08:00
DKChan a7c63850b7 工程(提交点文档域增量化批·pre-commit 触发粒度): run_all_gates.sh 增量档——--staged <清单文件> 仅配 --docs,触发矩阵唯一落点(任一 .md→06/06b/07/08/08d 先按清单内 .md 扫描·待办清单/roadmap/reports→08c 全量·systems/tests-README/reports→08e 全量·10 恒跑),快路径红必全量复跑定谳(清单面外红不入判定面,与 CI 全量严格对齐),未触发闸记 SKIP 入 not_run;user 目录隔离收窄至代码域(文档域不启引擎省 0.8 秒);pre-commit 钩子收暂存清单(quotepath 关闭保中文名,NUL 转行落临时文件)传 --staged,清单建不起回落全量档;不带 --staged=现行全量档 CI 零影响;窄测三态过(纯代码全 SKIP 6.2s/登记面 08c 触发+面外红定谳注记 7.9s/设计文档快路径 7.3s),Windows 瓶颈=python 进程启动,实际省 1-3 秒;横幅钩子与 config 注册不入 git(.zcode gitignored)随盘生效 2026-10-31 04:28:03 +08:00
DKChan 654aa73986 修复(迭代45·CI追绿): resume_adventure超函数长度键清偿——壳吸收宿主侧回填语句后55行超默认50,把续玩收束段(停点直回/已兑付重发/无停点续玩起步19行)按原语句序抽为宿主私有_resume_pending_or_start,壳降34行语句序逐句不变所有权零变化;干净树复验三断点套件全绿+S13零新键;CI同轮02/09另两红经干净树分离判定系布局几何单点化批未提交代码脏改,非本批面(实现报告§6.15) 2026-10-31 01:47:26 +08:00
DKChan a86a8dc367 批件(迭代45): 实现报告/反哺点清单/验收审计报告/两停机呈报五件落reports——对照表V1-V4+补件V1/V2全绿·实现决定记录15条·键账终值留债口径(30→10/19→10清偿,31/27/23留债)·能力行核对零回表·目检登记三项·审计通过零缺漏;接收方=roadmap迭代45行与两登记条目 2026-10-31 01:47:06 +08:00
DKChan 1cdef6bae3 文档(布局几何单点化批·文档面与登记面直改): battle.md 区表/线框/明细带去数值化——分区几何唯一事实源改指 battle_layout.tres 五字段,算例表退役改「算例不入文档」句并清除 .py 路径引用(08e 判据清偿);待办清单登记面——iter43 条目三点→两点换代(B 区高度已裁并落地记载在实况,标题来源字段恢复裸值=08c 判据清偿)+战斗区背景条目改读布局表+换图系双基座七件条目状态推进(文档设计已闭合+已写卡)+清单头句对齐「条目记进度」口径;提交点文档域闸八闸全绿,CI 在案红由 post-commit 自动追绿复验 2026-10-31 01:30:10 +08:00
DKChan f817ab2ddf 合并(迭代45): 呈现层状态件批+gdstyle尾波补件批两卡合回——卡①单格两绘制域件(cell_base_layer/cell_mark_overlay)+站桩两状态件(standee_vitals/standee_motion)建件·宿主30→10与19→10真实清偿·探针u.face改址(含设计清单漏计第五处L1修复)·golden三张0.00%;卡②board_hover悬停域件+breakpoint_store断点域件(R2整族归件,v1.1用户裁)·27键冻结·键账31/27/23留债;独立验收审计通过零缺漏;两停机呈报裁决链完整(L3③潜伏红定谳绿=T0实跑十八断言绿·前提已被413124b0消解/规格缺口R2裁决) 2026-10-31 00:33:42 +08:00
DKChan 9b0b164d38 iter45-卡2:board_hover+breakpoint_store拆件(R2整族归件,27键冻结,键账31/27/23留债)〔补件V1-V2〕 2026-10-31 00:33:02 +08:00
DKChan e7920e8fe0 iter45-卡1:单格两绘制域件+站桩两状态件+探针u.face改址(golden三张0.00%,键账30→10/19→10清偿)〔状态件V1-V4〕 2026-10-31 00:33:02 +08:00
DKChan b9546e7b69 注销(待办): 本地CI化改造条目整条删除(net-zero)——批已落地并首跑验证: 机件四件(ci_runner/post-merge/post-commit/pre-commit)+guard瘦身+规约四份换代随 c91bbddd 在库;CI首跑读数=master尖 c91bbddd(含并行会话合回的迭代44三卡 bee73db5)verdict green 零红闸 398秒,ci-status.json与ci-history.jsonl流水账在案;卡片欠账补提交 7a2d19fd 修掉 08c 源头(并行会话合回时唯一在飞红);post-commit 钩子在本批两笔提交点实走无状态零触发路径符合设计,post-merge 首个真触发=iter45合回 2026-10-30 16:59:07 +08:00
DKChan c91bbdddee 工程(本地CI化改造批·验证模型换代): 合回后跑测试制落地——①机件四件: tools/ci_runner.sh(merge=合回验证点对master尖无条件全量/commit=仅CI红态追绿;锁mkdir+在跑尖端判定+rerun标记=跑完补跑coalesce连发触发只测最新尖端;起手--editor --quit刷类名缓存接管原合并后零复跑动作;读闸回执写ci-status.json+ci-history.jsonl红闸与关键日志行在案)+hooks/post-merge(主工作区merge落地即触发,worktree内合并不触发)+hooks/post-commit(红态追绿触发)+pre-commit换代(撤代码面拦截=主区直改全面放开,合并收尾提交MERGE_HEAD豁免,保留秒级文档闸);②guard瘦身: 合回闸回执六况校验与主区代码面Edit/commit拦截退役,留--no-verify拦截+iter数卡校验+规则闸;③run_all_gates.sh头注换代(强制验证点=合回后CI全量,手动档降开发期)+.gates/.gitignore收CI产物;④规约四份换代: AGENTS(运行验证首条/规范导航行/车道铁律/交付验收红线/改动四类)、process-conventions(§1四条重写=主区直改车道+合回后CI+worktree降为大批约定+执法三层,§3第8/9条=CI验证载体+重跑语义「同一份字节禁止第二次人手跑」)、test-conventions §4(开发期窄测+合回CI+追绿)、iter技能五处(闸域/T0/闸序列/总收口/坑速查);⑤待办条目登记。设计讨论会话内闭合零待裁点,用户已发令;收口=迭代44合回CI首跑 2026-10-30 16:48:00 +08:00
DKChan 7a2d19fd12 补提交(写卡段欠账): 需求卡片-棋盘格边长口径收口批-2026-10-30 入库——待办清单:326 引用在先而文件一直未跟踪,主区盘上存在故 08c 流水线闸查盘放行,worktree 冷检出无此件即红(iter44 收口期 08c 红的成因);跨区失配随本地 CI 化自然消解(CI 在主区跑,盘上=库内同一面),本件补齐引用实体的历史在库 2026-10-30 16:47:20 +08:00
DKChan bee73db51b 合并(迭代44): 组合根注册面批+战斗编排归约批+测速循环批v1.1三卡合回——S13零豁免三键真实清偿(main.gd 49→15/battle_orchestrator 38→15/tune_sim裸键消除)·批末全量域闸13/14绿(02/05/09三闸从基线红改善归绿·唯一红08c为并行写卡会话在飞件)·独立验收审计通过·卡③v1.1停卡重裁实况见实现报告§5 2026-10-30 16:23:46 +08:00
DKChan 932edff505 收尾(工程批·工程小件批): 三条目完成整条删除(迭代42移交小件四条/恒真断言五条/闸基线三键换代——全部随 engsmall-20261030 批落地,合并提交 23bc6cc2)+批报告落档 reports/工程改动报告-工程小件批-2026-10-30.md;iter技能坑速查补条同批落盘(.zcode 不入 git) 2026-10-30 08:11:21 +08:00
DKChan 23bc6cc243 合回(工程批·工程小件批): 五件全清——run_all.gd --only 逗号解析修复(d32ff342)+level_runner 注释与 S8 消息订正(dd2de10e)+恒真断言五条判据修正(6a3203a8)+闸基线 02/05/09 三键换代(28223074,按回执实测绿重锚);批末闸两次全绿,最终回执 28223074 domain=code scope=related;孤立验证=逗号四词形态实跑四套件全绿(level_shop_economy 192 checks) 2026-10-30 08:08:40 +08:00
DKChan df6cafcb31 写卡(零豁免补件批·v1.1): T2机制按用户裁决改R2整族归件——实现段停卡呈报裁决落地:四_resume_*按所有权缝语句分家(件侧=主角账三件片段回填/curio池与特性还原/stop与bank片段回填/六成员改写;宿主侧=八项重绑定与两宿主调用按原语句序吸收进resume_adventure壳),快照两函数body逐字宿主输入逐调传参(件禁反向持宿主——RefCounted成环),纯读侧三函数逐字;宿主留_snapshot_breakpoint一行委托壳(CurioStop的Callable目标零改)与set_save_manager转发件;27键冻结与语句序零变化不变;裁决记录入§11 2026-10-30 08:08:07 +08:00
DKChan 2822307436 工程(工程小件批·件5): 闸基线 02/05/09 三键换代——按本批回执 6a3203a8 实测绿重锚(02 关联档分片4/4全绿/05 渲染闸真跑绿/09 静态扫描绿),三键旧红指纹(f9865f1a5a44/de89589e49ed/1a9ad773489f)全部归绿消除,改善态记账入 note;原登记=「02/09 两键」2026-10-30 并入 05 2026-10-30 08:01:50 +08:00
DKChan 6a3203a8db 工程(工程小件批·件4): 恒真与名实不符断言五条判据修正(存量断言精简A+B批余账④转正,按test-conventions §8.4真实入口口径改绑可观测面)——①feat_vouchers:51 恒真cond改绑投放装配面:读数==Σ表配额−本会话已兑付一张(curio_quota表驱动+兑付摘牌义务双绑);②treasure_effects:263 去恒真尾缀绑清图判定未触发(天眼窗口≠全板构造面稳定);③④level_shop_economy:594/:697 恒真min改绑胜局HP>0旗+扫描面非空(≥1胜局)双条件,HP≤0胜局即红;⑤system_contracts:66 标签互异半缺失补值集去重计数==原计数(同文件:44-55字典法同款);孤立验证=四套件实跑全绿(level_shop_economy 192 checks) 2026-10-30 07:54:40 +08:00
DKChan dd2de10ed1 工程(工程小件批·件2/3): 两处陈旧文本订正——level_runner.gd:12 node_entered 信号头注 shop={gp} 补 pool 态载荷形态(实际:507-509 两形态 legacy {gp}/pool {gp,shelf:true});static_scan.sh S8 ok 消息文件集 9→10(S8_FILES 实含 10 文件,board_installer.gd 随迭代42入集,扫面无缩小仅消息过期) 2026-10-30 07:54:18 +08:00
DKChan d32ff34256 工程(工程小件批·件1): run_all.gd --only 逗号形态解析修复——解析段补 split(",") 拆分对齐头注契约<词[,词…]>,此前整串当一个词对套件路径子串匹配,闸脚本关联档空格转逗号拼接单旗标传参多词必零命中 quit(2)(6caf56b5 起缺陷,迭代42施工曾以逐词重复传参绕过);孤立验证=逗号拼接四词形态实跑四套件全绿 filtered 2026-10-30 07:54:17 +08:00
DKChan 4e04d2f1ba iter44-卡3:卡③(测速循环批v1.1): 信号步进状态机消await-in-loop裸键·双实现对拍·行为改动零处·含并master与三卡产出物 2026-10-30 07:13:23 +08:00
DKChan d1c1d3822a iter44-卡2:卡②(战斗编排归约批): RoundCycle+BattleFacts两新件·宿主38→15·键账15/15/8·行为零变化·含消费漏网补改 2026-10-30 07:13:23 +08:00
DKChan d2b36e59cb iter44-卡1:卡①(组合根注册面批): ui_registry+三域容器建件·main.gd 49→15·全消费面改址main.ui.*·四闸主证行为零变化 2026-10-30 07:13:23 +08:00
DKChan 14054d9dfb 注销(待办): 技术债观察登记条目整条删除(lambda连接防回归)——三层社区标准方案已闭环:①预防层code-conventions §4.1两条明文纪律在册(多行lambda禁嵌match/信号处理体一律命名方法+bind;捕获循环变量先落地再捕获)②体外扫描层error_scan.py三段扫日志命中即红在役③捕获语义类静默错误由测试断言承接;静态层确有缝(gdstyle 23条quality规则无lambda专项)但本事项仅是数数观察不拦任何坑,补缝唯一方式=闸件加规则,而危险形态(多行lambda嵌match)全库117处实测零存量、登记半月零事故,为零命中形态写闸件属过度工程;社区论证凭据=官方提案godot-proposals #4736永久拒绝try/catch、错误码哲学为设计决策非版本bug 2026-10-30 06:38:33 +08:00
DKChan d2f282d9bc 登记(待办): 棋盘封印印记图形换代一条——现件 seal_mark.png 仍是经典扫雷旗形(旗面+旗杆),2026-09-28 只换色未换形;用户欲改法阵或锁链,形未定故只登记待美术设计,缺口列五待裁点(形/封印黑不变槽是否松动/32px 内是否侵入数字字形区/战末金蜡封徽记与悬停印记形光标两处连带件/与施印特效+印记常显两件的合并关系),实况记消费面两处与文档侧仍写「旗」的口径点(art-bible §4.1 旗杆前提+§3.2、art-assets-checklist §3.3/§4、battle.md:128/:270),改动面=文档+美术素材;格式与退役词闸全绿 2026-10-30 06:16:13 +08:00
DKChan ab3b4a9d80 同步(工程批收尾): test-conventions §7覆盖率句两处旧口径随a2ec3e8b裁决换代——「是否引入待办清单在册待裁」改直陈不引入(条目已删裁决已落),「现行补洞=零指名单测清单」改能力清单覆盖域表(零指名机制已被能力清单模型取代);规约修订经doc-design入口落笔,零待裁点,用户已发令;断链面已核仅此一处 2026-10-30 06:10:21 +08:00
DKChan fbed817fbe 注销(待办): qa/收尾孤儿遗留余二项整条删除——第5项字面量墨收测前置登记两头失效:主任务(iter19卡②墨色收测)随卡退役无现役承接,前置立常量不成立(实测bcard四处色值仅选中底#8A5A34在art-bible色板在册[木色§7.2],✓章#3A2A08/描述#4A3A28/脚注#6E4534三处游离色板外,真要动第一步是美术线色板准入/换色合并非ui_kit常量化);第7项商店房截图纳屏诉求剩余面只剩像素级(结构面ui_struct_probe _audit(shop)与交互面ui_click_probe已覆盖),像素面shot_probe头注已定谳不纳屏(真实路径不可达+合成投喂违反该探针口径),真缺口(商店屏文字对比度无机检)按未实测不启用不立新条,试玩出观感问题再按当时形态登记 2026-10-30 06:00:04 +08:00
DKChan 0caa2f5b7c 刷新(待办): 技术债观察条目长函数半边核销——点名四件(level_map_screen选路装配原155行/camp_overlay_view原152行/debug_overlay原114行/treasure_item_table schema校验群)实测现各文件最长函数仅25-44行全部消解,S13质量闸扫面覆盖core+data+presentation基线冻结禁恶化已接管防回归;条目缩为lambda连接观察(76处读数归属2026-10-15,测试面其后多轮重组分布口径未重测) 2026-10-30 05:54:45 +08:00
DKChan 30fdec1a62 写卡(迭代 44 卡③测速循环批 v1.1 停卡重裁): 实现段 T0 三重证据定谳卡③核心前提已被迭代 41 测试债批消化——215da315 提交信息明载「tune_sim 运行态收尾归还 run」+wd8 三断言(现位 :145-147)与 ui_tune_probe 全链路实跑两面全绿(停卡呈报=iter44 worktree reports/iter44-实现报告-2026-10-30.md §5),按停卡呈报建议案重写卡片——剥离已申报行为改动 V3(收尾置假还原无对象,还原已由迭代 41 完成并申报;完成定义改「diff 中行为改动零处」,对照表 V3 行改定义为非行为改动行=收尾四语句同帧同相位迁入 _finish_batch 的回归锚面,wd8/probe 判据「零改动转绿」改「零改动保持绿」),T0 义务改现状复核(两面实跑应绿——绿=按无行为改动形态开工,红=停卡呈报),夹具=现行产品码 await 形态逐字副本、与产品码新形态申报差异两处减一处(只余夹具暴露 stats 供对拍),全部 file:line 按 2026-10-30 master 重锚(run :36-54/_run_one_level :58-68 尾部实为败局记账/wd8 :139-147/基线键 :15;S8 九文件漂移订正十文件),architecture v1.25 S13 注销句保持(注销对象是 await 豁免不依赖行为改动申报,句内申报半句随实现批现状化——同步面注记),§16/§17/§13 随动;待办清单条目实况同批订正该失效申报句并登记重裁凭据(状态行维持已写卡+已编迭代 44——v1.1 落定即重立),roadmap 迭代 44 行不动 2026-10-30 05:52:44 +08:00
DKChan a2ec3e8b94 工程(渲染闸脚本错误扫描): 三段探针日志逐段过 tools/error_scan.py 补三硬闸之二——shot(shot_checker 之后)/fx/board 各一次,命中或日志不可读即红,标记与 run_smoke 同源;头注补⑤与「新增日志段必须同步加扫描」纪律
验证=合成日志三分支(clean 0/命中 1/不可读 2)+真跑 render_gate exit=0 全绿(三段零命中、残留 Godot=0、golden changed 0.00%)+文档域闸五件 PASS。
文档同步=architecture 测试域两处「SCRIPT ERROR 扫描缺失」换代、game/tests/README.md 工具行补判定面。
清单=「测试域遗留」整条删除(第 1 项被能力清单模型取代/第 3 项与 test-conventions §6 存量不清扫口径冲突且多进程分片后前提消失/第 4 项行覆盖率用户裁无意义/第 2 项即本批);基线条目扩为 02/05/09 三键(05 主区实测绿,baseline 红键属代码面,随下一代码批在 worktree 内重生成)
2026-10-30 05:39:33 +08:00
DKChan 2a5174d2a9 文档设计(美术出图登记轻量化): 出图登记义务四字段(模型名/分辨率/seed/对话日期)缩为必注两项「用途+生成时间(YYYY-MM-DD HH:MM)」,参数有则记、无则不填,登记不再作正稿遴选前置——旧口径卡住用户在途出图管线(线上对话式生图拿不到模型名与seed,分辨率与时间文件属性自带);立规处remote-gen-prompts头注改写(标题复现性→出处备查),转抄两处同步(art-assets-checklist §3.5/art-prompts §5流程注),手绘件登记项(手绘者/日期/源档尺寸)并入同一轻量口径(作者有则记);art-prompts版本号v2.10→v2.11;待办清单条目同批登记并删除(net-zero) 2026-10-30 04:52:50 +08:00
DKChan 3607271575 注销(待办): 在途草稿整理批(3349 失管草稿)整条删除——审计 P2-1 口径错配:ui-reuse-2026-09-23 系下载的第三方 CC0/OFL 素材包(占 3043/91%),vN 命名率与冗余量法对第三方包不适用,实测重复文件仅浪费 0.1MB;artwork/ 与 archive/ 均 gitignored,不进提交/构建/交付,不构成维护成本;用户裁不处理。留档约束:该包不可整删——quantize_ui_skin/render_sample_sheet/render_sample_sheet_v3/render_level_map_sheet 四个工具脚本按路径引用其中的字体/面板/图标/石纹 2026-10-30 04:30:19 +08:00
DKChan 9154f03b48 收尾(场景探针闸合并批): 待办条目完成整条删除——03/04并「03 场景探针」一行,窄试车十段exit0 2m22s,worktree全量闸all_green=True 3m25s,基线两键随批换代(b1e0f29f);引用面三处同步(README/process-conventions/iter技能) 2026-10-30 04:29:01 +08:00
DKChan b1e0f29fe3 合回(工程批·场景探针闸合并·基线换代): baseline.json两旧键(03探针--click/04探针--full)换「03 场景探针」green键;worktree全量闸复跑all_green=True(15闸全0,03场景探针真跑十件)——批本体55a8f270,验证3m25s 2026-10-30 04:28:39 +08:00
DKChan cfb528b917 工程(场景探针闸合并·基线换代): baseline.json删「03 探针 --click」「04 探针 --full」两旧键、预置「03 场景探针」green键(实测真值=本worktree全量闸03场景探针exit0十件全跑);02/05/09存量红键未动——归「闸基线两键换代」在册条目 2026-10-30 04:20:46 +08:00
DKChan 55a8f27026 工程(场景探针闸合并): 03/04两闸并一行「03 场景探针」——gate调用probe_gate.sh --click --full一次跑齐十件(公共五件只跑一遍,窄试车2m22s十段exit0),04号让空不重排;影响面SKIP块与头注/影响面注释同步;README闸03/04提法与process-conventions影响面句换代;probe_gate.sh零改动(--click/--full保留为开发期单跑开关) 2026-10-30 04:08:48 +08:00
DKChan f9fa8d85fa 登记(工程批): 场景探针闸合并——03/04两闸并一行+闸名换代(probe_gate.sh零改动,公共五件去重复跑) 2026-10-30 04:05:02 +08:00
DKChan c8f5091d0f 收尾(迭代43批后收尾): 反哺点13条处置闭合——落文四处(art-bible v3.23 §1.4样张登记句与§6.1素底牌实装事实/art-prompts v2.10 §5产出状态/战斗表现 v1.2五项过闸状态/test-conventions粒度句撤销措辞按§8.4口径清偿),消解三条(AGENTS红线已换禁变异禁撤销新口径/血条tween缺陷已由迭代41测试债批修复/该条留档备查),移交两条目(批后待裁三点=B区高度270vs252对齐方向·名字行12px档位归属·换色常量退役/批后小件=能力清单补行归写卡段·意图徽标四色墨CHECKS补断言),方向稿目检登记体验验证清单㊶(方向稿气质/改前改后对照/类型微光provisional观感复核并入,㉘节陈旧批指订正)——产出物退役:iter43实现报告/反哺点/验收审计/代拍板追认单与两卡需求卡片(2026-10-28定稿卡+2026-10-16旧v1.4卡)按吸收即死删除;二三关棋盘观感条目陈旧前置句换代(样张目标规格清单已落art-bible v3.22)——账面换代:待办清单样张条目整条删除+roadmap迭代43行已完成(2026-10-30)/卡片报告列退役保留字+当前位换代(迭代位1–43全完成,下一步=迭代44开工前宜裁PARITY查因重锚与待裁三点) 2026-10-30 03:23:05 +08:00
DKChan d3b977480e 合并工程批(parity-rm-20261030): 对拍闸退役批合回——回放确定性对拍闸(原11号条件闸)整闸退役:闸脚本6c段删除/PARITY_TOUCH_FACES改名CROSSCUT_TOUCH_FACES(02横切回落保留)/基线删条目/parity_anchor.py+parity_compare.py+evidence/parity锚三件删除/tests README能力行与工具行删/架构文档闸清单与验收红线清理/待办清单对拍挂账清理(含并行收尾批新登记的查因重锚条目注销)/迭代44在册卡V0验收载体随退役换代七处(用户裁:整局重放对拍代价不可承受,正确性由既有单测/集成/探针/渲染golden承载);两度并master(闸机提速批+迭代42批后收尾)零冲突;全量域闸全绿回执e79c04e8(domain=full,scope=full,changed=[]) 2026-10-30 02:59:54 +08:00
DKChan e79c04e891 工程(对拍闸退役批·连带二): 删除并行收尾批新登记的「PARITY对拍锚漂移查因与重锚」条目——退役批已删锚与工具,条目主体不存在,查因重锚随闸退役一并注销 2026-10-30 02:56:22 +08:00
DKChan 05d1988fcd 并master(迭代42批后收尾)——架构v1.26反哺点落文+批末移交小件登记 2026-10-30 02:55:50 +08:00
DKChan 7ec2af581a 收尾(迭代42批后收尾): 反哺点两处分置闭合(反哺点1落文=architecture v1.26 ShopSession行对外暴露格按委托壳拆除后实测直连面重写——直连方法清单16+白盒2与set_shop_pool调用面改述;反哺点2豁免登记被零豁免裁决消解不落,v1.25豁免注销在先)——产出物退役:iter42实现报告/反哺点清单/验收审计与两卡需求卡片按吸收即死删除(卡片死期=实现批合回;报告遗留义务先迁:S13重锚6键离账折入零豁免条目收账句、PARITY对拍锚漂移查因与重锚新立条目、批末小件四条新立条目)——账面换代:待办清单尾波清偿批与商店委托层拆除两条目整条删除+roadmap迭代42行已完成/卡片报告列退役保留字+iter41注脚152死键refresh已执行订正(fafb4fee)+当前位换代(迭代位1–42全完成、迭代43已合回待收尾、下一步=iter43收尾与迭代44开工) 2026-10-30 02:54:15 +08:00
DKChan 6b17d8f307 并master(闸机提速与防作废批合回)——02分片并行+代码域起手自检+worktree素材同步,与对拍退役批同改闸脚本,冲突就地解 2026-10-30 02:46:54 +08:00
DKChan 42c7d879ed 工程(对拍闸退役批·连带): 迭代44在册卡「战斗编排归约批」七处对拍引用随退役换代——V0验收主载体由PARITY对拍改挂关联套件并集(用户裁:其他测试函数保证正确性)、能力行引用换代、§2待裁点⑤注销、§3触面句与§4契约句、§13零触碰声明、§14 L3条款④⑤重排、§16审计凭据句——pipeline_gate 13卡全过 2026-10-30 02:38:57 +08:00
DKChan 8491505929 工程(对拍闸退役批): 删除回放确定性对拍闸及全部配套件——闸脚本6c段整删、PARITY_TOUCH_FACES改名CROSSCUT_TOUCH_FACES(02横切回落判定保留)、回执指纹NEED-APPROVAL分支清理、基线删11号条目、parity_anchor.py/parity_compare.py两件删除、evidence/parity锚删除、tests README能力行与工具行删行、adventure_monte_carlo台账行换代、架构文档闸清单删条件件与验收红线史句清理、待办清单数值窗口删对拍锚重锚句与触面粗粒度挂账行删、settle_segments_test头注去失指句——用户裁决:整局重放对拍代价不可承受,正确性由既有单测/集成/探针/渲染golden承载 2026-10-30 02:28:28 +08:00
DKChan db5abb8527 收尾(闸机提速与防作废批): 待办条目完成整条删除——实测全量闸3分29秒全绿66套件(对照基线约24分);窄试车6态;素材同步实战补拷247件;iter技能合回段三连成文;C批探针渲染并行未立项 2026-10-30 02:23:57 +08:00
DKChan 4ecdcf444f 合回(工程批): 闸机提速与防作废——02分片并行(run_all.gd加--shard/--shard-count,串行道=RNG日志会话六套件钉第1片防同种子日志互踩,空片放行,逐套件日志_s<k>防覆写)+gate_unit多进程(GATE_SHARDS默认4,=1回落原路径,片序合并日志供错误扫描与指纹取材——指纹绿记green红取FAIL排序集,免记账换代)+code域起手自检(熵增预检红灯即止/master推进黄牌/素材自动同步)+tools/sync_worktree_assets.sh(增量拷assets与.import边车,缺件或缺.godot时编辑器导入刷新,相对路径防杂散目录);窄试车6态+全量闸实测3分29秒全绿66套件(基线24分) 2026-10-30 02:23:18 +08:00
DKChan 1ee65d854b 工程(闸机提速批): 02分片并行+代码域起手自检+worktree素材同步——run_all.gd加--shard/--shard-count分片(串行道=RNG日志会话六套件钉第1片防同种子日志互踩;空片放行;逐套件日志_s<k>段防跨片覆写)+gate_unit多进程并行(GATE_SHARDS默认4,=1回落单进程原路径;日志按片序合并供错误扫描与指纹取材——指纹绿闸记green红闸取FAIL行排序集合,不受片序影响)+run_all_gates.sh code域起手自检(熵增预检红灯即止/master推进黄牌/worktree素材自动同步)+tools/sync_worktree_assets.sh(主区→worktree增量拷assets art与.import边车、缺件或缺.godot时编辑器导入刷新;相对路径拷贝防杂散顶层目录) 2026-10-30 02:17:05 +08:00
DKChan 31c801cd5a 登记(工程批): 闸机提速与防作废批——02分片并行+代码域起手自检+worktree素材同步(来源=迭代42合回六轮重跑复盘会话; C批探针渲染并行缓做不立项) 2026-10-30 02:11:47 +08:00
DKChan 65dff22fcf 合并迭代42: gdstyle尾波清偿批+level_runner商店委托层拆除 2026-10-30 01:42:26 +08:00
DKChan 0fc656c6ac iter42-收尾: 实现报告反哺点验收审计落reports+PARITY归因落档(迭代40/41存量) 2026-10-30 01:37:01 +08:00
DKChan 9c12d0a683 iter42-卡2: level_runner委托壳19枚拆除+shop公开化直连键7=33+残留零真残留+基线记账03/04/05(用户裁) 2026-10-30 01:34:06 +08:00
DKChan 66e6d0452a iter42-卡1: gdstyle尾波清偿批——board_view五段拆分与构建器/装板域拆BoardInstaller/wr5改址,键1-4+9+10归绿 2026-10-30 01:33:47 +08:00
DKChan 3b66d1a689 合回(迭代43): 战斗一屏样张批单卡合回——五项实装定值(血条#A8321F/条底收常量/徽标FS_HINT/护甲三枚PNG/名字行素底不压地台/地台件九宫格接装)+样张脚本扩展(两版出口/整屏四项S1–S4/战斗表现五项P1–P5/V1/V10/A5b,机检30/30全绿)+新单元套件standee_present_values(V5–V9十八断言)+theme_projection V4锚换代(§5.5同生死,双半强替代);全量双域闸全绿回执34eba7a3(domain=full,scope=related,16闸15绿+对拍SKIP);独立验收审计通过(三自查跑批原样绿,V1–V10十行全过,削弱零缺漏);批件三份+审计报告落reports,反哺点13条归文档批 2026-10-30 00:23:22 +08:00
DKChan 0c5eb2133d 审计(迭代43): 独立验收审计通过可合回——审计报告落reports(三自查跑批原样全绿:样张30/30、单测18/18、theme_projection164/164;V1–V10十行全过;削弱检查零缺漏,V4锚换代证实双半强替代);实现报告§7结论落位+V6行号笔误订正;反哺点13补意图徽标四色墨机检断言(审计附注,复算4.97–5.62全过) 2026-10-30 00:22:31 +08:00
DKChan 34eba7a332 iter43-卡1:战斗一屏样张批:五项实装定值落码——血条填充#A8321F经UiKit.HP_BAR_FILL(§4.2语义物件,#689CE8退役值零残留)/条底收HP_BAR_TRACK/顺位徽标提档FS_HINT/护甲三键换24×24 PNG(UiKit.TEX_ARMOR_*单点,缺位回退字形)/名字行垫HP_BAR_BACK素底牌且基线上移地台顶上方(§6.1/§7.2,几何单点_name_plate_rect+name_line_probe扩出口)/地台件TEX_STAGE_PLAZA九宫格横向拉伸接装(缺位回退程序化三段)——样张脚本扩展:draw_base两版画布出口(1280×800基座+1920×1080全局等比1.35×左右黑边96px)/B区按代码252出样/mockup镜像五项定值/S3b色板变体第二套/S5三组关键帧条/S6剪影三语境+描边对照+遮挡对照组/整屏四项断言S1–S4/战斗表现五项P1–P5/A7护甲对比锚/V1两版尺寸/V10基线并排/A5b成稿净空(机检30/30全绿)+--gen-assets四件素材产出入包;新单元套件standee_present_values(V5–V9真实入口断言18条全绿)登记manifest;三枚护甲PNG入包;批件三份落reports(实现报告/反哺点12条/追认单11条);并master合入态订正(迭代41测试债批清偿存量红与tween缺陷) 2026-10-30 00:00:01 +08:00
DKChan d9e5c019fd 收尾(迭代 41 测试债批): 待办条目「迭代 41 测试债修复」完成整条删除+收尾实现报告落 reports/(登记范围完成度+实测扩出的四处迭代 41 特许合回遗留产品回归:数字色牌守卫/open_victory opts 改型/站桩补间目标/调参模拟运行态收尾,含 golden 2.74%→0.00% 与墨族自适应读数证据+闸回执读数+实机目检三项);新立条目「闸基线 02/09 两键换代」承接已归绿存量红指纹的重生成(代码面,随下一代码批落) 2026-10-29 23:07:39 +08:00
DKChan 43906cdd8c 合回(迭代 41 测试债批): 代码域闸全绿回执 afd1a41f(域=code、scope=full、02/09 基线红→现绿、零新增红)——9 套件+S9/S10/S11 归零+死常量 SCHEMA_VERSION 删除+iter41 特许合回遗留的四处产品回归修复(数字色牌守卫/open_victory opts/standee 补间目标/调参模拟运行态收尾)+探针与测试面随拆件改址 2026-10-29 23:03:57 +08:00
DKChan afd1a41feb 修复(迭代 41 测试债批·02 收口): config_db_test meta 行族改直读表成员(_meta_rows 访问器随 iter41 退役)——02 单元与集成全套件归绿 2026-10-29 22:54:37 +08:00
DKChan 413124b0b8 修复(迭代 41 测试债批·闸面续修): 03/04/05 三闸归绿——①board_cell_unit 数字色牌守卫恢复(iter41 卡2 分层拆分丢 if plate: 致 dungeon 数字一律垫牌,L1 棋盘 golden 由 2.74% 回 0.00% 逐像素复现基线);②curio_interact_controller.open_victory 第 4 参随 opts 改型(字符串→{"gp_summary":…},胜利总结面续玩恢复路径运行期断链);③探针随 iter41 拆件改址(ui_input_probe 格面成员入 st 六处、ui_struct_probe level_screen._runner→._map._runner);④config_db_test 商店行族改直读表成员(_shop_rows 访问器已退役) 2026-10-29 22:45:09 +08:00
DKChan 39c1fc69fb chore(todo): format 2026-10-29 22:29:36 +08:00
DKChan 833d483ef8 编排(迭代44/45): 零豁免五键四卡+补件批按用户裁编法编入——迭代44=组合根注册面批+战斗编排归约批+测速循环批(中值合计≈7.875个工作日,卡序A/B/D),迭代45=呈现层状态件批+gdstyle尾波补件批(≈7个工作日,卡序C先/补件后-board_hover半边硬前置);编排四查过(五卡在盘/凭据齐=architecture v1.25+设计文档+grilling复核过/估时常规区间/闸PASS交叉核7链);战斗域结构深拆批条目状态累积已写卡+已编迭代44两段(专指宿主瘦身子项,其余两子项仍待文档设计);编入不等于开工,实现段由用户/iter发令 2026-10-29 21:58:36 +08:00
DKChan 9855bf0a10 复核(grilling五卡)+修正: 复核报告落盘——五卡全部有条件成立/零回炉,file:line抽验约90处命中,键账成员迁移账全平,与五项用户裁决零冲突;blocker1项修正(卡A与iter42文件交叠失实声明改为如实+开工前置=iter42合回);应修9项全落——卡A/B互写ui_input_probe共享触面禁并行,卡一board_hover半边硬前置升格+resume_adventure行号订正:141+V1判据面改型后口径+引用束计数实数,卡一表D补_flush_row_queue转发壳归宿,卡二对照表行2/11静态判据移域口径申报(行2改回归主证/行11改全量闸半边),卡三算术订正(删24项=37-22-2+2新=15账平),卡五测量面专属能力行「调参测速批量模拟」当场入表并改锚三行(废悬置) 2026-10-29 21:06:12 +08:00
DKChan 755dd8232c 写卡(零豁免五键·后两卡): 《呈现层状态件批》v1.0(单格两层域件+站桩两件并一卡,30→10/19→10真实25成员纠偏,潜伏红T0定谳随批修复,估时3-5天)+《测速循环批》v1.0(process_frame信号步进状态机,申报行为改动=收尾置假还原,双实现对拍入批,估时1-1.5天)——均零待裁点开工门U0;五键批条目翻已写卡(四卡路径逐行入实况,战斗编排归约批记账归战斗域结构深拆批条目);测量面能力行锚定按库内先例申报,专属行入表列写卡段后续 2026-10-29 20:30:37 +08:00
DKChan 672407dd5a 写卡(零豁免五键·前两卡): 《组合根注册面批》v1.0(main 49→15,ui_registry+三域容器,867改址,估时3-4天)+《战斗编排归约批》v1.0(RoundCycle/BattleFacts,38→15,估时2.5-3.75天,编队前置=iter42合回)——均零待裁点开工门U0;卡B接收方=在册条目「战斗域结构深拆批」宿主瘦身子项(实况补承接更新:设计闭合v1.25轴丁+豁免第三条注销,其余两子项仍待文档设计);卡B静态键账判据按test-conventions §8.4出口移散文申报 2026-10-29 20:05:45 +08:00
DKChan 183f4da3a4 设计闭合(零豁免五键+补件批): architecture v1.25落文——S13三条豁免注销(main/battle_orchestrator/tune_sim),七新件登记行(ui_registry/board_hover/RoundCycle/BattleFacts/cell两层域件/standee两件/breakpoint_store),轴丁两件设计闭合,attune改判,gdstyle inner-class计数截断缺陷登记;用户再裁恢复D2/D4拆件并按D5留债写入尾波条目(批末豁免登记步骤不执行);新卡《gdstyle尾波补件批》v1.0(零待裁点,估时2.5-3.5天,待iter42合回编入);待办清单新增补件批与零豁免五键两条目;roadmap迭代42状态按用户口述订正为进行中 2026-10-29 19:49:43 +08:00
DKChan 215da31591 修复(迭代 41 测试债批): 9 套件归绿+静态扫描 S9/S10/S11 清零+删死常量 SCHEMA_VERSION——两个产品回归随批修(standee 补间目标改子对象 st/tune_sim 运行态收尾归还 run),测试面按 iter41 拆件改址(slots/meta_state/crossrun/_forge/_side);S9 类型闸 24 处(node_map 三处给 Array[int] 真类型,其余动态边界 type-hint: ignore 逐处注记)+S10 头注日期 4 处+S11 退役词 1 处;config_db 头注「--script 无 autoload」订正为实测事实(autoload 并存、共享 .tres resolver 归最后装载者) 2026-10-29 06:04:40 +08:00
191 changed files with 8859 additions and 5642 deletions

No files matched your search

+15 -7
View File
@@ -16,7 +16,7 @@
## 运行与验证(工作目录 = 含 project.godot 的切片目录)
- **交付前验收 = 跑 `tools/run_all_gates.sh`,按本批改动域选档(`--code` 只跑代码域/`--docs` 只跑文档域/不带参数=两域全量),所选域全绿才合回 master**——它是全部验证闸的唯一入口与唯一清单,**域划分与选闸算法也只写在它这一个地方**,闸件增减与分域调整都只改它,文档禁另列清单。域按**文件性质**判:代码域=`game/` 下非 `.md` 的行为载体(代码/测试脚本/探针/场景/数据表/素材),`game/` 内 `.md` 与 `game/` 外一切路径=文档域;同批两类都有时跑全量档。代码域按**关联档选测**(2026-10-29 用户裁):02 单元与集成只跑「需求卡片申报 ∪ diff 推导」并集的套件(关联档=只跑与本批改动关联的那部分测试),00 冒烟/01 系统测试/09 静态扫描三件保险恒跑不降,横切面改动(公共件/数据表/autoload/数值·存档·RNG·结算触面)回落全部套件;算法与套件族映射表只在闸脚本内定义。所选域全绿即可合回;全量回归由用户自发发起,合入后无自动兜底。跑完写回执 `game/tests/.gates/receipt.json`(含 `domain` 与 `scope` 字段),合回时由 worktree-guard 按 diff 文件性质推出该验哪个域再校验回执(六况拦截细则见 process-conventions §3.9);非代码面的直改批验收=commit 点 git 钩子自动跑 `--docs`,无 worktree 与回执流程
- **合回验证 = 本地 CI(2026-10-30 用户裁)**:git merge 合回 master 落地后,post-merge 钩子(`tools/hooks/post-merge`)后台触发 `tools/ci_runner.sh` 跑一次全量域闸(调 `tools/run_all_gates.sh` 不带参数档,02 全套件),结果落 `game/tests/.gates/ci-status.json`(commit/绿红/红闸名单+关键日志行/耗时)+`ci-history.jsonl` 流水账;CI 红态下每个修复提交经 post-commit 钩子(`tools/hooks/post-commit`)自动追绿复验,绿态与无状态零触发——主区小批直提不重复跑全量,同一份字节禁止第二次人手跑。CI 红不回滚合并,红项在案、修复提交追绿;开工先读 CI 状态,红未修不开新批。跑批起手自动 `--editor --quit` 刷类名缓存(.godot 缓存不入 git,防合并后失配假红)。`tools/run_all_gates.sh` 仍是全部验证闸的**唯一入口与唯一清单,域划分与选闸算法只写在它这一个地方**,闸件增减与分域调整都只改它,文档禁另列清单;`--code`/`--docs`/`--quick` 手动档只服务开发期按域跑批(开发期窄测纪律见 process-conventions §6),不构成验收;全量回归由用户自发发起
- 单件直跑(调试期最窄确认用,**不构成验收**):冒烟测试 `python tools/run_smoke.py`(`game/tests/smoke_boot.tscn` 极小启动场景)· 系统测试 `python tools/run_system_test.py`(宿主 `game/tests/system_test.gd`)· 场景探针 `…console.exe --headless --path . res://probe/ui_probe.tscn` · 单元与集成 `… --script res://tests/run_all.gd`
- 调参批量:`--script res://sim/adventure_monte_carlo.gd`(全冒险期望)· `res://sim/reward_expectation.gd`(奖励期望);关卡完成率/难度类测量一律走全冒险
- 数值验证:`python tools/balance_verifier.py`(采集端 `res://sim/verifier_batch.gd`,报告与原始数据落 `sim-data/`)
@@ -40,7 +40,7 @@
| 时机 | 权威文档 |
|---|---|
| 批次开工 / 流程纪律 | `design/engineering/process-conventions.md`——车道铁律(代码面 worktree 批/其余主区直改)、五段流水线、批次清单、测试两入口与 MC/闸机细则、像素纪律 |
| 批次开工 / 流程纪律 | `design/engineering/process-conventions.md`——车道铁律(主区直改+直提;大批约定走 worktree)、合回后本地 CI、五段流水线、批次清单、测试两入口与 MC/闸机细则、像素纪律 |
| 流水线段入口 | 技能对照——doc-design=文档设计(兼任反哺点落文)/asset-ingest=美术录入/req-card=写卡/iter-plan=迭代编排/iter=实现(五者均装于项目 `.zcode/skills/`);登记不立技能;批后收尾与中间文档退役=batch-closure(同一目录);格式与状态机权威=`design/engineering/doc-conventions.md` §12 |
| 定代码方案 / 写代码 / 改代码前 | `design/engineering/code-conventions.md`——结构/契约/语言/数值/流程五类硬纪律(反例库 = 同目录 code-conventions-cases.md) |
| 定测试方案 / 写测试 / 改测试前 | `design/engineering/test-conventions.md`——探针标配 / 回归断言 / 提交前验证 / 验收清单落码 |
@@ -77,7 +77,15 @@
### 格式优先级
枚举用列表,二维对照用表格,按内容形态选;散文是最后手段。
能画图的内容一律画图说明,图是第一优先载体;枚举用列表,二维对照用表格;散文是最后手段。
### 图优先
**判据**:描述含多个实体、实体间有明确关系(先后顺序、包含、依赖、状态转移、布局对比)的内容时,直接画图说明,不写长段文字;单点事实(一句装得下)不画,纯数据对照用表格。
**载体**:默认用 visualize 技能出 HTML/SVG(会话内联渲染,表现力优先),流程/状态/结构/实体关系与空间对比、像素级示意、界面草图同一通道;查看端不支持内联渲染时退回 Mermaid 代码块,仍不行用字符画。
**边界**:硬数值与前提条件留在文字里,图不承载唯一数值依据;图与文禁重复说同一件事,文字只留图说不清的部分。
## 主动检索提醒
@@ -85,12 +93,12 @@
## 硬性红线(勿违反;操作细则在 process-conventions.md 与各规约)
- 车道铁律(判据与改动域划分同一,按文件性质切)——代码面(`game/` 下非 `.md` 的行为载体)唯一强制独立 worktree(迭代批全套仪式不变);其余一切面(含 `game/` 内 `.md` 与 `.zcode/`、`.qoder/` 等不入 git 面)主工作区直改+直提,commit 点由 git pre-commit 钩子(`tools/hooks/pre-commit`,`core.hooksPath` 挂载)现场跑文档域闸,绿落地红拦;主区直提代码面被该钩子拦;`--no-verify` 仅属用户,agent 禁用(细则见 process-conventions §1)
- 车道铁律(判据与改动域划分同一,按文件性质切;2026-10-30 用户裁本地 CI 化)——一切面(含代码面 `game/` 下非 `.md` 的行为载体与 `.zcode/`、`.qoder/` 等不入 git 面)主工作区直改+直提;commit 点由 git pre-commit 钩子(`tools/hooks/pre-commit`,`core.hooksPath` 挂载)现场跑文档域闸,绿落地红拦,合并收尾提交跳过(归合回后 CI);代码面全量验证归合回后本地 CI(`tools/hooks/post-merge` 触发,见「运行与验证」首条);大批次(迭代实现批等)按约定仍走独立 worktree(冻结→件核→并 master→重塑→主区合回→CI 验证),约定非强制;`--no-verify` 仅属用户,agent 禁用(细则见 process-conventions §1)
- **仓库根禁止设会话状态文件**(`active.md` 已废除)——现状看 roadmap「当前位」,待办看待办清单,过程看 git 提交信息,产出物看 `reports/`;任何文档禁止另设「当前状态快照」节,两处记同一件事必然漂移(细则见 doc-conventions 第 10 节)
- 测试执行两入口铁规——只有迭代 loop 开发与用户点名两个触发入口;其余会话零测试,需测试支撑先请示并注明未跑测
- 交付前验收 = 跑 `tools/run_all_gates.sh` **按改动域**全绿(唯一入口、唯一闸清单、唯一域划分与选闸算法,禁在文档另列);代码域按**关联档选测**——02 单元与集成跑「卡片申报 ∪ diff 推导」并集套件、三件保险(冒烟/系统测试/静态扫描)恒跑、横切面回落全量,所选域绿即可合回,全量回归由用户自发(2026-10-29 用户裁,合入后无自动兜底)(算法见闸脚本);合回 master 由 worktree-guard 按 diff 文件性质推域校验回执强制(process-conventions §3);非代码面直改批的验收=commit 点 git 钩子自动跑 `--docs`(process-conventions §1)
- **改动四类**——**需求改动**(游戏代码,含随逻辑写/改的测试代码):唯一走「写需求卡片+编入迭代批次」通道;迭代实现批禁写 `design/` 整个目录、两份根流程清单(`待办清单.md`/`体验验证清单.md`)与 `game/tests/README.md`(能力清单,归写卡段维护)(含新增/修改/删除),设计文档对它只读、是写测试与实现代码的输入依据;两份流程清单的条目状态只由流水线各段推进,实现批禁止顺手改;产出物只准写 `reports/`;批末只跑代码域闸。**工程改动**(tools/、闸件、模拟测量工具、纯测试维护——不改变游戏行为的配套代码)、**文档设计改动**、**美术素材改动**:不写需求卡片、不编迭代批次,登记在案后由用户发令在主工作区直改+直提(commit 点 git 钩子自动跑文档域闸),完成即删条目;触及代码面的工程改动(如纯测试维护)仍走 worktree 批;文档设计类写入走 doc-design 技能(登记条目格式由流水线闸强制)
- 五段流水线铁律——**需求改动**(游戏代码)按「登记 → 文档设计/美术设计 → 写需求卡片 → 迭代编排 → 实现」五段推进,段间不自动流转、每段转进由用户发令;编入迭代的卡片必须设计闭合+零待裁点——需求卡片任何工作量皆可写、皆可编入迭代,工作量限制只属迭代层(迭代估时总量 <5 个工作日=「冻结待凑批」、不可开工);流水线闸 `tools/pipeline_gate.py` 强制(已注册进 run_all_gates.sh 文档域);实现段零裁断——卡片没写清=上游缺陷,停该卡退回上游,禁内联求裁(process-conventions §2,格式与状态机=doc-conventions §12)。工程/文档设计/美术素材三类改动不走写卡与编排段:登记 →(如需设计讨论)→ 用户发令直改 → 主区直改+commit 点闸 → 完成删条目(车道细则=process-conventions §1)
- 合回后验证 = 本地 CI 全量域闸全绿(唯一强制验证点;唯一入口、唯一闸清单、唯一域划分与选闸算法仍只在 `tools/run_all_gates.sh`,禁在文档另列;触发与追绿语义见「运行与验证」首条与 process-conventions §1)。开发期手动跑批按需选档(`--code` 关联档选测——02 跑「卡片申报 ∪ diff 推导」并集套件、三件保险恒跑、横切面回落全量——算法只在闸脚本内定义),手动跑不构成验收;全量回归由用户自发发起。文档面直改批验收=commit 点 git 钩子自动跑 `--docs`(process-conventions §1)
- **改动四类**——**需求改动**(游戏代码,含随逻辑写/改的测试代码):唯一走「写需求卡片+编入迭代批次」通道;迭代实现批禁写 `design/` 整个目录、两份根流程清单(`待办清单.md`/`体验验证清单.md`)与 `game/tests/README.md`(能力清单,归写卡段维护)(含新增/修改/删除),设计文档对它只读、是写测试与实现代码的输入依据;两份流程清单的条目状态只由流水线各段推进,实现批禁止顺手改;产出物只准写 `reports/`;合回后由本地 CI 全量验证(红项修复提交追绿,process-conventions §1)。**工程改动**(tools/、闸件、模拟测量工具、纯测试维护——不改变游戏行为的配套代码)、**文档设计改动**、**美术素材改动**:不写需求卡片、不编迭代批次,由用户发令在主工作区直改+直提(commit 点 git 钩子自动跑文档域闸);大体量改动(含触及代码面的工程改动如纯测试维护)按约定走 worktree 批(非强制);文档设计类写入走 doc-design 技能(登记条目格式由流水线闸强制)
- 五段流水线铁律——**需求改动**(游戏代码)按「登记 → 文档设计/美术设计 → 写需求卡片 → 迭代编排 → 实现」五段推进,段间不自动流转、每段转进由用户发令;编入迭代的卡片必须设计闭合+零待裁点——需求卡片任何工作量皆可写、皆可编入迭代,工作量限制只属迭代层(迭代估时总量 <5 个工作日=「冻结待凑批」、不可开工);流水线闸 `tools/pipeline_gate.py` 强制(已注册进 run_all_gates.sh 文档域);实现段零裁断——卡片没写清=上游缺陷,停该卡退回上游,禁内联求裁(process-conventions §2,格式与状态机=doc-conventions §12)。工程/文档设计/美术素材三类改动不走写卡与编排段:(如需设计讨论)→ 用户发令直改 → 主区直改+commit 点闸(车道细则=process-conventions §1)
- MC/模拟:无数值改动零跑;档位硬闸见 `sim-data/README.md`「MC 采集档位表」,一档只能由用户发起
- 闸机轮次纪律:新码先孤立验证 → 出红只跑红件最小单元 → 长批先最小参数试车 → 开工扫坑册(process-conventions §6)
- **验收闭合**——每个需求点与每个行为改动都要有从真实入口进入的验收断言,可 grep 定位、在当轮真实跑批中执行并通过,判据绑被测改动特有的可观测提交面;**禁变异类测试与撤销类取证**(2026-10-24/2026-10-28 用户裁——人为制造红属伪造证据),证明做不出=停卡呈报;禁删断言/放宽阈值换绿;合回前独立验收审计未过则禁止合并(细则=test-conventions §8+iter 技能第 5 步)
+9 -4
View File
@@ -130,8 +130,8 @@
### 3.5 营地场景图
- **要什么**:营地场景图 9 张——`scene_camp_bg`(背景)+ `scene_camp_gate / forge / chest / notebook / dummy / board / campfire`(七个可点物件)+ `scene_camp_tent`(承载件)。钥石顶栏图标沿用现役 `icon_keystone`。
- **来源/规格**:**✦ 插画级**(作者自跑远程生图管线);烛光黄 `#F6C667` + 木色 `#8A5A34`(art-bible §6.2);2× 源档出图、最近邻降采样到 1280×800;后处理=抠底/降采样/色板量化/描边统一。**每张出图须登记模型名+分辨率+seed+对话/日期**。
- **要什么**:营地场景图 10 张——`scene_camp_bg`(背景层,不透明全幅=天空+远山+整片地面+门洞内一条通向远处的小路)+ `scene_camp_wall`(围墙层,透明底=横贯全宽密排木桩墙+两根门柱,门洞留透明缺口)+ `scene_camp_gate / forge / chest / notebook / dummy / board / campfire`(七个可点物件)+ `scene_camp_tent`(承载件)。钥石顶栏图标沿用现役 `icon_keystone`。
- **来源/规格**:**✦ 插画级**(作者自跑远程生图管线);烛光黄 `#F6C667` + 木色 `#8A5A34`(art-bible §6.2);2× 源档出图、最近邻降采样到 1280×800;后处理=抠底/降采样/色板量化/描边统一。**出图登记照 [remote-gen-prompts](remote-gen-prompts.md) 头注轻量口径执行:用途+生成时间必注,其余参数有则记。**
- **排期**:营地交互重做**美术自立批**(不占迭代位;UI 贴图重构批已搁置)。终稿落 `game/assets/art/scene/`,消费常量 `UiKit.TEX_CAMP_*`。
- **现状**:全占位。
@@ -155,13 +155,18 @@
| 资产 | 需求 | 风格/规格要点 | 来源 |
|---|---|---|---|
| 受击反馈 / 死亡表现 / 顺位标记 | 敌人系统登记的资产需求**下限三类** | 死亡=血条归零后**灰化+碎裂退场,禁止瞬间消失**;顺位标记需在敌多时可读 | ⚙/✦ 挂自产精灵——死亡灰化碎裂是语义硬规则,不可整包复用;烟尘碎屑粒子可 ♻ 改色 |
| B 区舞台背景插画 `scene_stage_bg.png` | 战斗舞台背景(art-bible §6.1 资产契约) | **✦ 插画级**;源 1280×270、B 区全高含明细带底、显示 1×;**只出远景 + 中景两带**(近景带在结算期被 84px 明细带底整条盖住);三关换挡走 §4.3 `wall_ambient` tint 槽,不按板出三张 | ✦ 插画级(作者自跑远程生图管线) |
| 站桩石板地台件 `scene_stage_plaza.png` | 近景地台独立件(art-bible §6.1/§8.2) | 源 48×12、九宫格横向拉伸、顶缘受光线 2px 不随拉伸变形;跨三关恒定,不吃 `wall_ambient` tint | ⚙/✦ 自产 |
| B 区舞台背景层 `scene_stage_bg_dungeon.png` | 战斗舞台背景·dungeon 变体(art-bible §6.1 资产契约) | **✦ 插画级**;源 = 基准屏宽 × `stage_h`(读布局表 `battle_layout.tres`)、B 区全高、显示 1×;顶部空区+纵深洞厅远景(远/中/近三层;暖石棕、宽岩台/层理、地精巢穴支洞/木栈道/木桩围栏),不画近景地面;按 `board_type` 选图,不走 `wall_ambient` tint | ✦ 插画级(作者自跑远程生图管线) |
| B 区舞台背景层 `scene_stage_bg_underdark.png` | 战斗舞台背景·underdark 变体(art-bible §6.1 资产契约) | **✦ 插画级**;源 = 基准屏宽 × `stage_h`、B 区全高、显示 1×;顶部空区+纵深洞厅远景(远/中/近三层;冷蓝灰、巨柱/石笋/蛛网/螺旋塔剪影/巨菇/石桥),不画近景地面;按 `board_type` 选图,不走 `wall_ambient` tint | ✦ 插画级(作者自跑远程生图管线) |
| B 区舞台背景层 `scene_stage_bg_deep_underdark.png` | 战斗舞台背景·deep_underdark 变体(art-bible §6.1 资产契约) | **✦ 插画级**;源 = 基准屏宽 × `stage_h`、B 区全高、显示 1×;顶部空区+纵深洞厅远景(远/中/近三层;灰黑、石笋城郭/棱角石厅/石桥/矿道口/烟囱塔、冷银/铜灰矿脉),不画近景地面;按 `board_type` 选图,不走 `wall_ambient` tint | ✦ 插画级(作者自跑远程生图管线) |
| B 区连续近景地面层 `scene_stage_ground.png` | 近景泥土地面层(art-bible §6.1 图层归属) | **✦ 插画级**;源 = 基准屏宽 × `ground_h`(布局表新增字段);干净压实泥土为主、全幅只零星几颗小石、散点分布,顶缘不规则泥土边/阴影缝作分隔带;三关共用、跨三关恒定、不吃 `wall_ambient`;独立层压在 B 区底部,小石台在其上 | ✦ 插画级(作者自跑远程生图管线) |
| 站桩石板地台件 `scene_stage_plaza.png` | 近景地台独立件(art-bible §6.1/§8.2) | 源 48×12、九宫格横向拉伸、顶缘受光线 2px 不随拉伸变形;跨三关恒定,不吃 `wall_ambient` tint;连续地面层在其下方,先保留 | ⚙/✦ 自产 |
| 特殊技动画终稿 | 并入 §2.3 三档意图动画,不另起体系 | 攻/防两型旧口径由三档替代 | ✦/⚙ 自产(同 §2.3) |
| 连锁坍塌显形演出 | 连锁展开触发时的连锁演出 | 与音频方向同批配对登记;「坍塌显形」=连锁展开的玩家可见名 | ♻ 粒子/碎块改色 + ⚙ 坍塌结构自产 |
| 封印特效 | 该动作的呈现名 = **封印**(art-bible §3.2):**施印瞬间特效 + 印记常显形态**两件 | 印记旗面走封印黑 `#000000`(现行封印色,色史归 art-bible §4.1);特效走运动+色相(§2:状态/情绪不走明度,空间光照可走);现行占位 = 封印黑「旗」字形;印记常显形态可与 §3.3 旗面像素化合并出 | ✦/⚙ 自产(精灵+粒子,烟尘光点可 ♻ 改色) |
| 弹道/退场等动画密度 | 战斗表现输入清单,方案未定 | 样张锁动画方案后启动 | ♻ 通用弹道/光点/消散粒子改色为主 |
**现状**:三关舞台背景层与连续地面层已交付(2026-10-31 入包 `game/assets/art/scene/`:dungeon v10c / underdark v10c / deep_underdark v10c,均 1280×252;共用地面 v12b,1280×100,按作者裁保留原亮度 L≈0.132);小石台件已在役。
---
## 5. 明确不在本清单
+19 -12
View File
@@ -1,6 +1,6 @@
# 美术圣经(Art Bible)— Dungeon Sweeper
> 项目:Dungeon Sweeper(扫雷 × 自动战斗 闯关 Roguelike)· 状态:**第 1–9 节定稿**;现行为 v3.22,历次修订明细见 git 提交史
> 项目:Dungeon Sweeper(扫雷 × 自动战斗 闯关 Roguelike)· 状态:**第 1–9 节定稿**;现行为 v3.30,历次修订明细见 git 提交史
>
> **Art Director Sign-Off(AD-ART-BIBLE)**:历次批均未签核——此为 Lean/Solo 模式(非 PHASE-GATE)下的现行治理状态;full 门禁签核时机待用户表态。
> 风格基调:地牢题材但**非暗黑风**——轻快明亮的幻想冒险风,全年龄;像素风;角色 2~3 头身。
@@ -59,6 +59,8 @@
**整屏样张闸**:格子样张验"格",整屏样张验"屏"——战斗屏/关卡屏/菜单屏/营地屏各出一张整屏样张,验收四项:①全部文字(含菜单/说明/tooltip/按钮)对其直接背景 ≥4.5:1(WCAG AA),大字(≥18px 或加粗档)底线 3:1、目标 4.5:1;②明度分层三档带成立(信息面层 > 世界中层 > 世界底层,层间灰度可分);③整屏焦点预算(§3.4:首焦唯一——数字区/立绘/主按钮,无第二焦点竞争;负空间服务焦点,不属空窗);④文字禁直接压纹理底(垫容器或罩层)。验收顺序 = **方向稿目检先行**(防"机检过闸但气质否决"式返工),目检过后机检冻结。执行时机 = 屏级视觉落码开工前;过闸前一切屏级视觉为占位临时态(§7.1 完成条款)。**样张判定物的产出手段与素材输入级别不进圣经定值**——圣经只定样张要验什么,不定样张以何种手段产出、也不定它取哪一级别素材输入。验收:机检〈整屏样张四项断言全绿,且目检先行过审〉
**战斗一屏样张登记(合并提交 3b66d1a6)**:战斗一屏样张批整屏四项机检全绿——抽样读数:文字对比最弱 5.34:1;舞台底 L0.036/已开格底 L0.089 落两子带、间隔 0.053;首焦=数字区高对比能量占比 59%;名字行素底牌平底零地台色。样张整体过闸待方向稿目检先行(真人检测,登记于体验验证清单 ㊶)
**样张现状**:格子样张已冻结,冻结凭据 = git 提交 7a847c7(机检报告)与 §4.1/§8.2 定值表;样张 v3 产物原件已丢失、未入 git。现行冻结态:色板与帧数档维持冻结;数字形制 = 亮墨三档直画 + 色牌后备(值见 §4.1);新增图类型/变异色板须重跑本节测试。验收:机检〈冻结项定值均以 §4.1/§8.2 对应 owner 表为准〉
---
@@ -210,7 +212,7 @@
- **重甲冷灰金属材质色 `#868C9A`(ARMOR_PLATE_COLD,token 名暂拟、随实装批登记)**|语义/UI token 板(**分层①**,占 1 枚额度,**余量 2→1**;用户裁):重甲档甲材质色;character-design §5.3 通道 = 甲面覆盖率 × 甲材质色,甲材质色按甲档统一,故石君王岩石板甲**同引本枚、不另计额度**;材质色跨屏恒定,落分层①不落分层②(环境色才随场景走)。过门情况:第二道门按宽松口径(禁止三通道全 <8)与在册色逐一比对,**无三通道全 <8 的对 → 过门**;§4.2 语义检查过门(色相 222°,不占红/金/蓝/紫赤/绿语义段、不在 300°–345° 禁入带;材质色不进信息层与语义物件岗)。对比实测:对深素底 `#282220` 4.65:1、对舞台底 `#3E362C` 3.52:1(图形 ≥3:1 门槛);相对亮度 L≈0.261 落 §2 世界中层 0.22–0.45 带。验收:机检〈本值入板后全板色对中三通道差全部 <8/255 的对数 = 0〉
- **卓尔浅紫灰皮 `#A79CB8`(DRIDER_SKIN,token 名暂拟、随实装批登记)**|语义/UI token 板(**分层①**,占 1 枚额度,**余量 1→0,额度用尽**;用户裁):蛛化卓尔精灵上体皮色(体块主色,L≈0.355 落 §2 世界中层带)。过门情况:第二道门同上行口径,**无三通道全 <8 的对 → 过门**;§4.2 语义检查过门(色相 264°,避开 300°–345° 禁入带,紫赤独占仍归 `#8F2F6A`;不占本节五语义段)。对比实测:对深素底 `#282220` **6.04:1**、对舞台底 `#3E362C` **4.57:1**——**两处均过图形 ≥3:1 门槛**(本轮新算)。验收:机检〈本值入板后全板色对中三通道差全部 <8/255 的对数 = 0〉
- **换色合并登记五处(用户裁,部件色落值)**|按本表「不过门即触发换色合并、禁加色绕行」执行,五处**零新增额度**(各件原拟新增候选一律不取):① 主角铲刃浅铁灰(原拟 `#A8AEB8`)→ 并入 `FOG_MEMORY_INK #C4C4C4`,**部件取值 = `#C4C4C4`**;② 主角提灯铜色(原拟 `#C08A3A`)→ 并入 `GOLD_DEEP #B07A14`,**部件取值 = `#B07A14`**;③ 食人魔酋长暖褐皮(原拟 `#B08050` / `#A97C4E`)→ 并入 `暖石 #B8A582`,**部件取值 = `#B8A582`**;④ 石君王灰皮(原拟 `#9A968C` / `#949089`,按**次级皮**口径——板甲视同体块主色、灰皮属次级皮故不受 L 0.22–0.45 约束)→ 并入 `STONE #786E62`,**部件取值 = `#786E62`**;⑤ 轻甲护甲图标木色 `#8A5A34`(深素底下 2.72:1 不过门;原拟新增木色亮档 `#C49A5E`)→ 并入 `WARM_FAINT #CDB88C`,**部件取值 = `#CDB88C`**(8.08:1)。**一色两岗代价已随裁定接受**:① 的 `#C4C4C4` 语义岗 = 迷雾记忆墨(§6.5)、② 的 `#B07A14` 属金族(§4.2 金只说收益与兑现)——两者均作角色随身件材质色用,不进信息层与语义物件岗;③④⑤ 无语义岗冲突。验收:机检〈五处部件取值点全部改指合并目标色,四枚原拟新增候选值零入板〉
- **换色合并登记三处**|按本表「不过门即触发换色合并、禁加色绕行」执行,三处零新增额度:`#3E342A`(舞台底,代码 `STAGE_BG`)→ 并入 `#3E362C`(状态槽底);`#C4AC84`(地台顶缘受光线,代码 `STAND_STONE_RIM`)→ 并入 `#C9AF83`(掩尘,并与 §8.2 高墙口沿高光 `Color8(196,172,132)` 统一为同一枚);`#5A4C3A`(地台描边,代码 `STAND_STONE_EDGE`)→ 并入 `#4C4038`。验收:机检〈全仓三枚源常量零引用,绘制点全部改指合并目标色〉
- **换色合并登记三处**|按本表「不过门即触发换色合并、禁加色绕行」执行,三处零新增额度:①`#C4AC84`(地台顶缘受光线,代码 `STAND_STONE_RIM`)→ 并入 `#C9AF83`(掩尘,并与 §8.2 高墙口沿高光 `Color8(196,172,132)` 统一为同一枚);②`#5A4C3A`(地台描边,代码 `STAND_STONE_EDGE`)→ 并入 `#4C4038`;③`#3E342A`(舞台底,代码 `STAGE_BG`)→ 并入 `#3E362C`(状态槽底)。①②的绘制点全部改指合并目标色、源常量删除——程序化回退绘制与已落合并值的贴图保持一致;③随 §6.1 舞台背景插画批一并处置:背景资产接管该绘制面时本常量随绘制点退役或改指,禁单独为改值出批。验收:机检〈①②两枚源常量全仓零引用,绘制点全部改指合并目标色〉
**对比硬指标(可测)**:数字可读性由**格底明度分档**承载——棋盘数字亮墨 **L≥0.75** 对**已开格底 L 0.08–0.14** ⇒ 直画对比 **≥4.5:1 恒成立**(WCAG AA,样张逐组合抽测冻结);**色牌后备**:色牌素色恒定 **L≥0.70**、牌上数字深墨 **L≤0.027** ⇒ 任意牌墨组合 ≥4.5:1(实测样例:#33261B/#F5EAD3 = 12.3:1)。未开格底槽(含全部图类型)相对亮度 **L≥0.30**(墙顶面受光档)、HSV 饱和 **≤0.55**、避 Boss 禁入色相带;已开格底走世界底层档(见上)。物件识别不依赖格底(§5.1 轮廓通道),旗面、金色不作识别依赖。验收:机检〈亮墨对已开格底 ≥4.5:1;未开格底 L≥0.30、饱和 ≤0.55;任意牌墨组合 ≥4.5:1〉 ←原二;地形变异 AC-9
@@ -374,17 +376,21 @@
### 6.1 战斗舞台背景(B 区环境)
- **双舞台常驻下的 B 区环境**:战斗画面区(站桩立绘所在)的背景 = 明亮地窟,与 §1 风格基调同源——暖倾向、无压抑;背景整体**低对比自然后退**(§3.4 焦点预算延伸):背景禁止构成完整凸形、禁止抢 B 区立绘与 D 区数字的焦点,识别层级恒为 立绘 > 背景。验收:目检〈背景无完整凸形、立绘最抢眼〉
- **舞台正面目标**:B 区 = **地窟看台**——立绘脚下地台/落影承载(世界中层),背景环境层低饱和后退、明度走世界底层档,允许光源氛围(洞顶光柱/暖光晕候选);角色禁止直接贴平色底。规格与终值归整屏样张闸(§1.4)冻结。**地台面上零文字**——暖白 `#F0E2C8` 对地台板身 `#968264` 实测仅 2.89:1,已越 §7.2 的 4.5:1 硬指标,落法按 §7.2「文字禁直接压纹理底」(垫容器、素牌或罩层),非新条款;站桩文字位改垫素底或落进结算明细带。验收:目检〈立绘脚下有地台/落影,非贴平色底〉+机检〈B 区站桩文字位正下方像素非地台板身色 #968264〉
- **舞台正面目标**:B 区 = **地窟看台**——立绘脚下地台/落影承载(世界中层),背景环境层低饱和后退、明度走世界底层档;角色禁止直接贴平色底。规格与终值归整屏样张闸(§1.4)冻结。**地台面上零文字**——暖白 `#F0E2C8` 对地台板身 `#968264` 实测仅 2.89:1,已越 §7.2 的 4.5:1 硬指标,落法按 §7.2「文字禁直接压纹理底」(垫容器、素牌或罩层),非新条款;站桩文字位改垫素底或落进结算明细带(实装=站桩名字行垫 `HP_BAR_BACK` 深档素底牌、文字基线上移至地台顶上方——2026-10-30 落地)。验收:目检〈立绘脚下有地台/落影,非贴平色底〉+机检〈B 区站桩文字位正下方像素非地台板身色 #968264〉
- **明度分层适用**:背景氛围的情绪表达仍禁止以压暗/渐晕实现(§2 禁令);空间纵深与洞穴感走明度分层允许域——世界底层档、光照氛围、色相饱和与形体承载(§3.2 同款逻辑)。验收:方向性
- **图类型联动**:背景 tint 走 §4.3 `wall_ambient` 槽,随三关换挡(方向候选见 §6.3);紫赤仅 Boss 战允许短暂进入背景氛围纹样(§2.6 色相独占的舞台侧延伸,高饱和独占不变),非 Boss 战高饱和紫赤禁入。验收:机检〈背景吸色无非 Boss 高饱和紫赤〉
- **资产渠道**:✦ 插画级(演出类场景贴图,清单 §4 联动登记);战斗一屏样张(§1.4 清单 1.2)含背景出样。**资产契约(尺寸候选,样张冻结终稿)**:基准一张 = `scene_stage_bg.png`(源 1280×270,B 区全高含明细带底;三关换挡走 §4.3 `wall_ambient` tint 槽,不按板出三张——换皮成本复用棋盘同款逻辑),显示 1×;呈现约束按本节各条(低对比后退/明度世界底层档/光柱氛围保留)。程序化平色+光柱现行使用 = 占位;插画工单归 PRD,交付后 art-status-checklist 同步 ✅。**出图只出远景 + 中景两带**:结算期明细带底 84px 会盖住 B 区 78%–100% 段(约 59px),近景带在结算期整条不可见——画进背景等于画一件永远看不见的东西。远景/中景的构图占比归整屏样张(§1.4)。验收:目检〈背景插画内零地台结构件〉
- **近景地台走独立件**:`scene_stage_plaza.png`,源 48×12、九宫格横向拉伸、顶缘受光线 2px 不随拉伸变形(规格见 §8.2)。独立的两条理由——① 上条的 84px > 59px 覆盖账;② §4.3 `wall_ambient` tint 作用于整张背景,地台若画进背景会随三关换明度,而 §6.1 舞台正面目标把地台定死为世界中层承载面,随关卡变会破 §2 明度分层的层间关系。独立件跨关恒定。验收:机检〈地台件按九宫格拉伸后顶缘受光线宽度恒为 2px〉
- **图类型联动**:背景层按 `board_type` 选三张变体(`scene_stage_bg_dungeon.png` / `scene_stage_bg_underdark.png` / `scene_stage_bg_deep_underdark.png`,方向取 §6.3 三关色板候选);**背景不再走 §4.3 `wall_ambient` tint**(该槽保留给棋盘/世界的墙雾氛围,避免对三张背景二次染色)。紫赤仅 Boss 战允许短暂进入背景氛围纹样(§2.6 色相独占的舞台侧延伸,高饱和独占不变),非 Boss 战高饱和紫赤禁入。验收:机检〈背景吸色无非 Boss 高饱和紫赤;背景素材不引用 `wall_ambient` 槽〉
- **场景构成(2026-10-31 用户裁)**:B 区舞台从远到近分三段——**顶部空区**:洞顶以上的昏暗空间,只留深色空区,**不画钟乳石、吊锥、悬挂物或任何可辨形状**;**远景(纵深洞厅)**:不是贴墙的后壁,按远/中/近三层组织——远层暗剪影(远处洞口/通道、层叠岩体、洞底轮廓)、中层主要细节层(岩台、岩柱、凹壁、裂缝)、近层左右暗色岩框;剪影被中层部分遮挡、不贴顶、不孤立,**禁贴墙感、禁单件圆柱贴顶、禁砖石墙/规则砌缝/方整石块**;三关母题(地精=支洞/木栈道/木桩围栏;卓尔=巨柱/石笋/蛛网/螺旋塔剪影/巨菇/石桥;灰矮人=石笋城郭/棱角石厅/石桥/矿道口/烟囱塔)见 `battle-stage-prompts.md` §3.2;**分隔带**:远壁根部与近景地面之间一条不规则碎石带/阴影缝,边界清楚但不笔直,**禁瓷砖缝、踢脚线、规则台阶**;**近景地面**:干净压实泥土为主,全幅只零星几颗小石、散点分布,中央平缓留空给角色站位,**禁成排/成堆碎石、满铺砂砾、泥点噪声、瓷砖、规则铺装、草、木地板**。纵深与三段构图归 §1.4 样张;逐条要素、分层提示词与否定后缀 = `battle-stage-prompts.md`(本文件不重复维护)。验收:目检〈顶部无钟乳石/吊锥;远景有纵深、非贴墙;无孤立圆柱贴顶;近景为泥土碎石;分隔带不规则;中央无大障碍〉
- **图层归属(2026-10-31 用户裁)**:背景层 = 三关变体 `scene_stage_bg_dungeon/_underdark/_deep_underdark.png`(顶部空区+纵深洞厅远景,不画近景地面;按 `board_type` 选一张);近景地面 = **独立连续地面层** `scene_stage_ground.png`(泥土/砂砾/碎石地面,全宽、底边贴 B 区底、顶缘即分隔带;高度读布局表 `ground_h`,字段由地面层实装批新增并回填,本批终稿高 100、代码批落表);现有每角色小石台 `scene_stage_plaza.png` 保留,叠在连续地面之上。连续地面层与小石台**三关共用**、跨关恒定、**不吃 `wall_ambient` tint**(世界中层承载面随关卡换明度会破 §2 层间关系);背景三张变体各自带关卡色族,不再叠 tint。验收:机检〈三张背景文件名带 board_type 后缀;地面层/小石台文件名无 board_type 后缀;背景与地面层均不引用 `wall_ambient` 槽〉
- **资产渠道与契约**:✦ 插画级(演出类场景贴图,清单 §4 联动登记);战斗一屏样张(§1.4 清单 1.2)含背景出样。**背景层 = 三张按关卡变体** `scene_stage_bg_dungeon.png` / `scene_stage_bg_underdark.png` / `scene_stage_bg_deep_underdark.png`(源 = 基准屏宽 × `stage_h`,读布局表 `game/data/tables/battle_layout.tres`;显示 1×;按 `board_type` 选图;**不走 `wall_ambient` tint**);三张共用本节构图规则,仅岩体形态与色族按 §6.3 三关方向不同。**背景只画顶部空区+纵深洞厅远景,不画近景地面**:结算期明细带整条压住 B 区底带(带高 = `log_band_h_settle`,读布局表),近景地面在独立层;画进背景等于画一件永远看不见、且会随背景变体重复的东西。**连续地面层** = `scene_stage_ground.png`(源 = 基准屏宽 × `ground_h`,读布局表;显示 1×;三关共用、跨关恒定、不吃 tint);地面层是极宽薄条,出图与后处理按 `battle-stage-prompts.md` §2.2 的工艺(可平铺纹理条/最宽档裁切,禁横向拉伸)。**光柱不迁入背景**:三张背景均不画光柱/光带/光束/光晕与任何光源实体,明暗只由岩体层理与暗部承载;现行四柱程序层是临时占位,资产到位后直接退役(代码批)。资产已入包(2026-10-31),四柱程序层与平色底由代码批退役/改指;出图工单 = `battle-stage-prompts.md`,交付记录见本节「B 区舞台背景与共用地面的交付绑定」。验收:目检〈三关背景可辨为不同洞窟,非同一图换色;远景有纵深、非贴墙;无孤立圆柱贴顶;背景无地台/地面结构件、无砖石墙、无瓷砖地面、无光柱/光带;地面层为泥土/砂砾/碎石〉
- **B 区舞台背景与共用地面的交付绑定(2026-10-31 入包)**:三张背景与一张地面已按本节契约入包 `game/assets/art/scene/`——`scene_stage_bg_dungeon.png` / `scene_stage_bg_underdark.png` / `scene_stage_bg_deep_underdark.png` 均 1280×252、不透明全幅、单张 ≤32 色、按 `board_type` 选图;`scene_stage_ground.png` 1280×100、顶缘以上透明、单张 ≤32 色、三关共用。地面按作者 2026-10-31 裁保留候选 v12b 原亮度(实测不透明域 L≈0.132,低于 §2 世界中层 0.22–0.45,登记为作者例外;后续调亮归调参/替换,不重开设计);背景实测 L:dungeon 0.0148、underdark 0.0132、deep_underdark 0.0111(后两者低于世界底层下沿带下限 0.014 不超过 0.003,登记读数)。验收:机检〈四件文件名与尺寸逐位相符;地面顶缘以上 alpha=0、不透明域 ≤32 色;背景不透明全幅 ≤32 色〉+目检〈三关可辨、无光柱、地面读作泥土〉。参考指针:`asset-path-map.md` `scene/` 行、`battle-stage-prompts.md` §2/§4。
- **近景地面与地台走独立件**:连续地面层 `scene_stage_ground.png` 与角色脚下小石台 `scene_stage_plaza.png` 均为独立层,三关共用、跨关恒定。小石台源 48×12、九宫格横向拉伸、顶缘受光线 2px 不随拉伸变形(规格见 §8.2);连续地面层的顶缘即分隔带(不规则碎石/阴影缝,不是直线亮边)。独立的两条理由——① 三张背景按关卡变体,地面若画进任一背景就得三张各画一遍、且三关站位面会漂移;② 结算期明细带整条压住底带(带高读 `log_band_h_settle`),地面在独立层才能跨关恒定并保持世界中层承载面不变。小石台先保留,是否退役归实装批/后续裁决。验收:机检〈地台件按九宫格拉伸后顶缘受光线宽度恒为 2px〉+目检〈连续地面为泥土碎石、顶缘不规则〉
### 6.2 营地场景
- **情绪与色域**:承载 §2.8——全作最暖:烛光黄 `#F6C667` × 木色 `#8A5A34`(§7.2),中等饱和,无棋盘式高对比。
- **场景构成**:一张可点击的营地场景图(1280×800,与工程档同幅面)。远景=天空与远山;中景=木围墙(密排竖木桩、桩顶削尖、不透缝无横木)与**半开的营地大门**(门洞内一条通向远处的小路);近景=中央篝火、左侧锻炉铁砧、右侧帐篷与宝箱、帐篷旁的笔记本,另有木桩与挂在围墙上的布告板。**中景带自底部往上约 2/3 处起**;**近景物件可覆盖围墙(围墙是纯背景板),唯一例外=中间门洞必须留空、不被任何物件遮挡**。七个可点物件的功能绑定归 `camp.md`,本节只定画法。
- **交付形态**:**背景 1 层 + 七个物件各 1 层**(**帐篷**为承载件、不可点),共 8 张透明 PNG + 一份坐标表(贴图矩形/命中矩形/z 序)。**篝火例外**:命中矩形只取下半(火堆),上半(火焰)只作特效展示。
- **场景构成**:一张可点击的营地场景图(1280×800,与工程档同幅面)。远景=天空与远山;中景=**独立成层的木围墙**(密排竖木桩、桩顶削尖、不透缝无横木)与**半开的营地大门**;近景=中央篝火、左侧锻炉铁砧、右侧帐篷与宝箱、帐篷旁的笔记本,另有木桩与挂在围墙上的布告板。**中景带自底部往上约 2/3 处起**;**近景物件可覆盖围墙,唯一例外=中间门洞必须留空、不被任何物件遮挡**。七个可点物件的功能绑定归 `camp.md`,本节只定画法。
- **图层拆分(10 张)**:场景按「远景/中景/近景要素各自成层」拆——`scene_camp_bg` 背景层(z0,不透明全幅)=天空+远山+整片地面+门洞内一条通向远处的小路;`scene_camp_wall` 围墙层(z1,透明底)=横贯全宽密排木桩墙(左右两段)+两根略高的门柱,**门洞处留透明缺口**透出背景层的小路;`scene_camp_board` 布告板(z2)、`scene_camp_gate` 门扇(z3)、`scene_camp_dummy` 木桩/`scene_camp_forge` 锻炉/`scene_camp_tent` 帐篷(z4)、`scene_camp_chest` 宝箱/`scene_camp_notebook` 笔记本(z5)、`scene_camp_campfire` 篝火(z6,最前)=八个物件层,各透明底单件一张。**帐篷**为承载件、不可点。围墙独立成层的好处:桩高可单独锁死、门洞做透明缺口、背景层不混入暖木色。提示词与坐标 = `camp-scene-prompts.md`,消费常量 = `asset-path-map`。
- **交付形态**:**背景层 1 张(不透明全幅)+ 围墙层 1 张 + 八个物件层 = 10 张**,其中 9 张透明底、1 张不透明全幅 + 一份坐标表(贴图矩形/命中矩形/z 序)。**篝火例外**:命中矩形只取下半(火堆),上半(火焰)只作特效展示。围墙层落盘按细条 `(0, 240, 1280, 70)` 裁,不做全幅(全幅约 3.9 MB 显存,细条约 0.35 MB)。
- **色彩与氛围依据**:营地场景的冷暖分工(冷的大面积后退、暖的小面积前进)、空气透视三档与大面积用色规则 = `reports/提案-营地色彩与氛围脚本-2026-10-23.md`。本节不复述色值;新增环境色按 §4.1 场景环境子板条款登记(每屏一套、≤32 色、过 §4.2 语义检查与 RGB 三通道差 ≥8/255 两道门)。
- **交互四态(零新素材,全部程序化)**:悬停=**轮廓亮边 + 浮动**(浮动须整像素吸附);按下=位移 1px;**禁用/未解锁=降明度 + 去饱和**(§2 已缩窄至情绪域,状态表达不受限),**仅锻炉(锻造工坊)与宝箱(秘宝匣)两件会有此态**;已解锁=常态。
- **钥石余额**:常驻画面顶部的一行小 HUD 栏(高 48px,**浮层覆盖,不挤压画面**)。
@@ -394,6 +400,7 @@
### 6.3 三关图类型色板候选(board_type 换挡方向)
- **模板与不变槽**:换皮只换 §4.3 四槽(`tile_closed`/`tile_open`/`wall_ambient`/`variant_tint`),信息与语义层全图类型恒定——本节只给三关**方向性候选**,终值按 §4.3 现行规则重跑 §1.4 格子样张冻结。**终值的取值程序**:先跑「三关色相 × 五组类型微光」互辨抽测(抽测任务登记在待办清单),再按 §4.3 现行规则重跑 §1.4 格子样张冻结——本节不填终值色值,待该抽测与格子样张重跑后落本节修订。验收:机检〈本节三关行无 `tile_closed`/`wall_ambient` 终值色值字面量(终值未冻结前禁填)〉
- **三关背景变体**:§6.1 的三张背景取本节三关方向作为岩体形态、剪影语言与色族差异(dungeon 暖石棕+地精巢穴支洞/木栈道/木桩围栏;underdark 冷蓝灰+卓尔巨柱/石笋/蛛网/螺旋塔剪影/巨菇/石桥;deep_underdark 灰黑+灰矮人石笋城郭/棱角石厅/矿道口/烟囱塔);背景不复用 `wall_ambient` tint(该槽作用于棋盘/世界),背景色值归战斗屏场景环境子板。验收:目检〈三关背景可辨为不同洞窟,非同一图换色〉
- **深度轴叙事**(§2.7 下探过渡):色相层进预告「更深更险」,明度档不动——
- **dungeon 地精洞窟**(第一关):现行掩尘系暖石棕(`#C9AF83` 基)即基准档,`wall_ambient` 暖棕;
- **underdark 幽暗地域**(第二关;方向:**冷调幽暗**):主调**灰白蓝**候选(低饱和蓝灰域,`tile_closed`),少量紫黑倾向点缀——紫系取紫罗兰域(≈256°)候选,禁入带 300°–345° 不变;`wall_ambient` 冷蓝灰;
@@ -445,7 +452,7 @@
### 7.3 布局栅格
- **结构**:双舞台三区(B 战斗画面/C 数据框/D 棋盘;字母保持不重编)为结构冻结项,分区尺寸与像素级裁决归 battle.md(HUD框架承载 1280×800 基座锚点值,如扫雷图区 1036×504、结算明细带展开 84px)。验收:机检〈三区结构常驻〉
- **结构**:双舞台三区(B 战斗画面/C 数据框/D 棋盘;字母保持不重编)为结构冻结项,分区几何的唯一事实源 = `game/data/tables/battle_layout.tres`,分区尺寸与像素级裁决归 battle.md(HUD框架只载 1280×800 基座口径,不复写数值)。验收:机检〈三区结构常驻〉
- **栅格规则(候选)**:面板间距/内边距按 4px 步进档(8/12/16/24 候选),全 UI 恒定;数据区直角细线不占圆角预算。终值归样张。验收:目检〈间距落在步进档〉
### 7.4 字体与字号
@@ -453,7 +460,7 @@
- **字体**:全局一套像素字,单套即终稿——缝合像素字体 Fusion Pixel(OFL,已接入 `game/assets/fonts/ui_font_fusion_pixel_zh_hans.ttf`),拉丁字符与数字沿用其自带拉丁字形,不另设拉丁专件(现状即终稿;旧拉丁候选 Kenney Fonts 已弃;生产工单=清单 §3.6);墨色走主墨系(§7.2);换字体 = 全局决策。验收:机检〈主题字体指向 Fusion Pixel 件〉
- **字号**:像素字体**整数倍缩放,禁非整数缩放**(像素模糊);下限(1280×800 基座):棋盘数字 ≥18px(公式 = `max(18, ceil(格边长 × 0.5))`不变)、**正文/明细 ≥16px、tooltip ≥14px**(1280×800 低于 1080p 基准,同字号实际更小)。验收:机检〈静态文字位点字号 ≥ 档位表值〉
- **字号档位表(六档)**:全站 UI 文字字号只经 `UiKit.FS_*` 单点取值,裸数字字号弃用——`FS_CAPTION 12`/`FS_HINT 14`/`FS_BODY 16`/`FS_SUB 18`/`FS_TITLE 24`/`FS_H1 34`。旧实测档按就近归档收编(10/11/12→CAPTION、13/14→HINT、15/16→BODY、17/18/20→SUB、21/22/24/26→TITLE、32/40→H1;静态位点全归档,数据驱动动态字号位点除外);棋盘数字公式 `maxi(18, ceil(格边长×0.5))` 与站桩血数 11px(条带内读数豁免)不入表。注:六档相邻比 12→14→16→18 三段 <1.25×,系裁决值优先于字重纪律的档差参考线,该参考线对新层级设计仍有效。验收:机检〈全站无裸数字字号,位点走 FS_*〉
- **小字号豁免条款(合并单条不开两口子)**:下款两类**豁免于本节下限**——①**条带内读数**:压在窄条带(血条/进度条等 ≤12px 高带)内居中的数字,受条带高度物理约束,维持 11px 下限,且必须按 §7.2 垫素色牌后计对比(实装=B 区站桩血数 11px 压 HP_BAR_BACK 牌 14.3:1);②**装饰性 overline/caption**:不承载须精确读取的信息、仅作分区标题或氛围小字者,可至 **12px**。**豁免逐处清单(撤除史归 git 提交史,余清单如下)**:条带内读数=B 区站桩血数一处;装饰 caption 档(FS_CAPTION)在册面=棋盘底操作提示条/营地柜台说明文与结构位预留注记/图例 RAP 词条说明/设置警示文两处/关卡地图底栏组标(生命/属性/特性)与分隔系/站桩消散提示/B 区状态槽微计数(×N●○)/bcard 脚注/RAP 反馈行/刷新余量徽标——清单外 11-13px 文字一律按正文/tooltip 下限提档,新增 caption 位须同批增补本清单。**站桩顺位徽标 ①②③ 不增列本清单**:该处原为 13px 字形,既不在①条带内读数在册面、也不在②装饰 caption 在册面,属「清单外 11–13px 文字」,按本条纪律提档至 `FS_HINT` 14 即归六档;为它单独入册等于开第二口子,与「合并单条不开两口子」的立条理由正面相撞。②装饰 caption 在册面逐处维持不变。验收:机检〈豁免文字位均可对上本清单,清单外无 <14px;站桩顺位徽标绘制点取 UiKit.FS_HINT,且②在册面不含该位〉
- **小字号豁免条款(合并单条不开两口子)**:下款两类**豁免于本节下限**——①**条带内读数**:压在窄条带(血条/进度条等 ≤12px 高带)内居中的数字,受条带高度物理约束,维持 11px 下限,且必须按 §7.2 垫素色牌后计对比(实装=B 区站桩血数 11px 压 HP_BAR_BACK 牌 14.3:1);②**装饰性 overline/caption**:不承载须精确读取的信息、仅作分区标题或氛围小字者,可至 **12px**。**豁免逐处清单(撤除史归 git 提交史,余清单如下)**:条带内读数=B 区站桩血数一处;装饰 caption 档(FS_CAPTION)在册面=棋盘底操作提示条/营地柜台说明文与结构位预留注记/图例 RAP 词条说明/设置警示文两处/关卡地图底栏组标(生命/属性/特性)与分隔系/站桩消散提示/B 区状态槽微计数(×N●○)/bcard 脚注/RAP 反馈行/刷新余量徽标——清单外 11-13px 文字一律按正文/tooltip 下限提档,新增 caption 位须同批增补本清单。**站桩顺位徽标 ①②③ 不增列本清单**:该处原为 13px 字形,既不在①条带内读数在册面、也不在②装饰 caption 在册面,属「清单外 11–13px 文字」,按本条纪律提档至 `FS_HINT` 14 即归六档;为它单独入册等于开第二口子,与「合并单条不开两口子」的立条理由正面相撞。**站桩名字行同理提档**:名字行(敌人名与护甲文字)承载须精确读取的信息,原取 12px 不在①②两在册面、属「清单外 11–13px 文字」,按本条纪律提档至 `FS_HINT` 14 归六档、不增列②在册面,名字行几何(素底牌矩形、文字基线、宽度测量)随档位联动。②装饰 caption 在册面逐处维持不变。验收:机检〈豁免文字位均可对上本清单,清单外无 <14px;站桩顺位徽标与站桩名字行绘制点取 UiKit.FS_HINT,且②在册面不含该两位〉
- **字重纪律(字重两档不启用)**:层级手段为两件——**字号档差(相邻层级 ≥1.25×)+ 墨色深浅**;全局单字重,不上第二个字重(**重开钩子已注销**——整屏样张设计轮已重开并复核:屏内各级文字全部能归进本节六档字号,顺位徽标与站桩名字行两处按本节纪律提档即可,屏内实测对比全部达标(暖白对舞台底 9.49:1、GOLD_BRIGHT 6.47:1、血数 14.3:1),**未实证标题层级不足**,维持单字重,不另立条目);数字禁描边/阴影伪加粗不变(字形为唯一数值载体,§4.1 同源纪律)。验收:机检〈主题仅注册单字重〉/目检〈屏内标题与正文层级仅靠字号档差与墨色深浅两件即可分辨,无一处需要第二字重才读得清〉
### 7.5 营地界面
@@ -514,8 +521,8 @@
- **画布基准(暂定值)**:
- **主角人设原图 = 48×72**(1× 基准,暂定;与站姿降采样可读性验证一致);
- **棋盘小人 = 32×32 二头身派生版(暂定,允许更小)**——独立资产,自人设定稿压缩派生(character-design §5.4),不吞数字位、占位 ≤ 0.75× 格边长;板越小占比摩擦越大(13×13 / 14×14 更小,算例见 [battle.md](../gdd/ux/battle.md)「棋盘格尺寸公式」),32px 源档的去留已定终值 = **保留 32×32 源档**(沿「独立小源档」路线;N=12 板下显示占位上限 27px = 0.84 倍,正落本节乙类非整数缩放许可范围,**连豁免都不用再办**),待决标注注销;
- **站桩立绘源档 = 八敌逐键定档**:主角 48 不变;八敌源档高 = goblin 36 / giant_spider 38 / duergar 46 / hook_horror 58 / bugbear 62 / ogre_chief 74 / drider 90 / duergar_king 94,显示一律 **×2 整数档(NEAREST)** = 72–188。定档依据 = 生物体型三约束(钩爪恐魔>灰矮人、灰矮人<熊地精、食人魔>熊地精——体量=物种体型,非强度线性映射,见 character-design §5.2);可读性实测门槛 = 源高 ≥32(24 糊/32 可读/48 清楚/64 细节完整),全阶梯 36–94 达标;派生管线 = pixel-1.png 紧裁 → 等比缩至档高 → dom_block_resize 块内众数降采样(tools/pixelize_candidates.py `STANDEE_TIER_H`,`SIZES` 降格为管线上游中间分辨率表);体量存在感经整屏对比校验(B 区 270/立绘显示上限 188)。验收:机检〈standee_asset_check 逐键档高校验〉
- **站桩立绘排版**:① 区高 B 区 270(≈1/3)/C 区 530(≈2/3),**扫雷盘 438×438 原尺寸不缩、格子不变**(区高事实源 battle.md 区表;压缩由 C 区面板空档吸收);② 站桩内部重排——立绘双脚落石台上缘(弃盒内居中)、**血条+状态/名字行落底排**(立绘→血条→状态 顺序,全场血条对齐一线;血条嵌石板位、台缘线留作地面线、落影保留接地感),状态空时该行退化为名字行;③ 大体档(显示 >150px 级:drider/duergar_king)**允许向上溢出盒顶 N px**(N 由实装批按实测尺寸算定,**与背景/光柱无关**——现行溢出区恰为临时光柱氛围层〔§6.1 方向稿四柱,x 基准 110/900/1055/1210@1280,`battle_stage_view.gd:55-63`〕,正式舞台背景插画 `scene_stage_bg.png` 到位后该层由背景绘制吸收,**溢出判定不随之变更**;横向禁止压到顶部中央轮次·阶段指示,溢出上限由实装批用真图复算 x 区间定),血条仍落底(落底排的第二个收益=头部探顶不带出血条);意图徽标悬浮头顶上方(探顶档压胸);④ 「血条悬浮头顶+立绘盒内居中」排式已弃——盒顶固定 y 与盒内居中在高盒下拉开血条-立绘空洞。验收:目检〈血条一线对齐、大体档溢出不压轮次指示〉
- **站桩立绘源档 = 八敌逐键定档**:主角 48 不变;八敌源档高 = goblin 36 / giant_spider 38 / duergar 46 / hook_horror 58 / bugbear 62 / ogre_chief 74 / drider 90 / duergar_king 94,显示一律 **×2 整数档(NEAREST)** = 72–188。定档依据 = 生物体型三约束(钩爪恐魔>灰矮人、灰矮人<熊地精、食人魔>熊地精——体量=物种体型,非强度线性映射,见 character-design §5.2);可读性实测门槛 = 源高 ≥32(24 糊/32 可读/48 清楚/64 细节完整),全阶梯 36–94 达标;派生管线 = pixel-1.png 紧裁 → 等比缩至档高 → dom_block_resize 块内众数降采样(tools/pixelize_candidates.py `STANDEE_TIER_H`,`SIZES` 降格为管线上游中间分辨率表);体量存在感经整屏对比校验(B 区高读 `stage_h`/立绘显示上限 188)。验收:机检〈standee_asset_check 逐键档高校验〉
- **站桩立绘排版**:① 区高 B 区 ≈1/3 屏 / C 区 ≈2/3 屏,**扫雷盘原尺寸不缩、格子不变**(区高事实源 = 布局表 `battle_layout.tres`;压缩由 C 区面板空档吸收);② 站桩内部重排——立绘双脚落石台上缘(弃盒内居中)、**血条+状态/名字行落底排**(立绘→血条→状态 顺序,全场血条对齐一线;血条嵌石板位、台缘线留作地面线、落影保留接地感),状态空时该行退化为名字行;③ 大体档(显示 >150px 级:drider/duergar_king)**允许向上溢出盒顶 N px**(N 由实装批按实测尺寸算定,**与背景/光柱无关**——现行溢出区恰为临时光柱氛围层〔§6.1 方向稿四柱,x 基准 110/900/1055/1210@1280,`battle_stage_view.gd:47-56`〕,正式舞台背景插画(三关变体)到位后该层直接退役(背景不画光柱),**溢出判定不随之变更**;横向禁止压到顶部中央轮次·阶段指示,溢出上限由实装批用真图复算 x 区间定),血条仍落底(落底排的第二个收益=头部探顶不带出血条);意图徽标悬浮头顶上方(探顶档压胸);④ 「血条悬浮头顶+立绘盒内居中」排式已弃——盒顶固定 y 与盒内居中在高盒下拉开血条-立绘空洞。验收:目检〈血条一线对齐、大体档溢出不压轮次指示〉
- **站桩石板地台件**:`scene_stage_plaza.png`,源 48×12、九宫格横向拉伸、顶缘受光线 2px 不随拉伸变形;跨三关恒定、不吃 §4.3 `wall_ambient` tint(独立理由见 §6.1 近景地台独立件条)。验收:机检〈地台件按九宫格拉伸后顶缘受光线宽度恒为 2px〉
### 8.3 导出格式与命名规范
+293
View File
@@ -0,0 +1,293 @@
# 美术后处理与校验工具清单
> **性质**:本文件是 `tools/` 下美术资产处理与校验脚本的用法清单。回答三件事:这类资产该跑哪件工具、命令怎么敲、判据与坑在哪。
> **边界**:只管工具用法,不立美术规格。色值、画布、体量、命名与验收条款归 [art-bible](art-bible.md)(§4.1 主色板、§8 资产与技术规范)和 [art-assets-checklist](art-assets-checklist.md);命令与源码冲突时以源码现行为准,美术规格冲突时以 art-bible 为准。素材入库与代码接线归 [asset-path-map](asset-path-map.md),不在本文件。
> **分工**:[art-prompts](art-prompts.md) 管角色级提示词与五步出图流程,[camp-scene-prompts](camp-scene-prompts.md) 管营地场景分层提示词与四步流程;两卡只把「后处理」一步指到本文件。提示词怎么写出图、出完图怎么处理,是两件事。
> **命令前提**:以下命令均在仓库根执行。素材不入 git,worktree 缺件时部分脚本回退主检出 `D:/code/g-plan2`,部分直接返回错误,见各节「已知坑与适用边界」。
## 按资产类型选工具
| 资产类型 | 先跑 | 再跑或校验 | 说明 |
|---|---|---|---|
| 角色精灵(八敌、主角) | `tools/pixelize_candidates.py` | `tools/standee_asset_check.py`、`tools/palette_check.py` | 需要收网格时加 `--fixer` |
| 场景不透明全幅(营地背景层、战斗舞台背景) | 无专用工具 | `tools/palette_check.py` 只出读数 | 见「三条已知边界」第 3 条 |
| 场景透明分层(围墙层、八个物件层) | 无专用工具 | `tools/palette_check.py` 只出读数 | 现役批量处理的键表只认角色键 |
| UI 框材(面板、按钮、石框、底噪、图标) | `tools/quantize_ui_skin.py` | `tools/palette_check.py`、`tools/path_check.py` | 主题小件另跑 `tools/gen_theme_icons.py` |
| 地形瓦片 | `tools/recolor_terrain_tiles.py` | `tools/palette_check.py` | 只换色,不重绘 |
| 整屏样张 | `tools/render_sample_sheet_v3.py`、`tools/render_level_map_sheet.py` | `tools/shot_checker.py`;棋盘回归用 `tools/golden_board_crop.py` 加 `tools/golden_compare.py` | 样张脚本生成后自带机检 |
## 三条已知边界
1. **`pixelize_candidates.py` 不能处理任意资产。** `main()` 写死 `SIZES` 角色键表,只遍历这八个键的子目录;目录名不在表里就跳过,没有单文件入口,也没有通用目录模式。
2. **`palette_check.py` 不做色相禁入带检查,也没有全图明度带检查。** 它只统计单张色数,以及对基线判新增色近色;HSV 300°–345° 禁入带与 §4.1 的明度带不在它的检查范围。
3. **现存工具链没有「不透明全幅背景件」的专用入口。** 角色批量处理按角色键,UI 量化按键名,瓦片换色按 `terrain_*_dungeon` 命名,整屏样张是合成器;没有一件接受一张不透明全幅场景图并输出同尺寸成品。`camp-scene-prompts.md` 第 3 步写明背景层不去底,但这一步没有对应的仓库命令。
## tools/pixelize_candidates.py
**一句话定位**:把远程生图的角色候选批量处理成透明底像素精灵;带 `--standee48` 时按逐键档高产出站桩源档。
**归属**:仓库内 `tools/pixelize_candidates.py`。
**输入与输出**:默认批量模式读 `<批次根>/<角色键>/v*.png`,角色键取 `SIZES` 的八个键;输出 `<角色键>/pixel-<n>.png`、四联底测试条 `pixel-<n>-strip.png`,批次根再出一张 `0-像素化预览.png`。站桩模式读 `<批次根>/<角色键>/pixel-1.png` 与 `<批次根>/hero-ref/edit/normal_1x.png`,有 `attack_1x.png`、`hit_1x.png` 时一并产出;输出 `character_enemy_<键>.png`、`character_hero_portrait.png`、`character_hero_attack.png`、`character_hero_hit.png`。
**用法命令**:
```bash
python tools/pixelize_candidates.py <候选批次目录>
python tools/pixelize_candidates.py <候选批次目录> --fixer
python tools/pixelize_candidates.py <候选批次目录> --standee48
python tools/pixelize_candidates.py <候选批次目录> --standee48 --standee-out game/assets/art/character
python tools/pixelize_candidates.py <候选批次目录> --fixer --fixer-exe D:/code/pixel-art-fixer/rust/target/release/pixelfixer.exe
```
**判据或口径**:`SIZES` 是中间图最长边,不是最终体量——goblin/giant_spider/hook_horror 96,bugbear/duergar 128,ogre_chief/drider/duergar_king 192。`STANDEE_TIER_H` 是站桩档高——goblin 36、giant_spider 38、duergar 46、hook_horror 58、bugbear 62、ogre_chief 74、drider 90、duergar_king 94;主角取 `STANDEE_SLOT_H` 48。站桩源宽上限 75,脚贴画布底缘。去底走奶油描边屏障法,屏障法保留面积低于底色门路径的 60% 时自动回落;随后吃描边。像素化顺序:内容裁剪、BOX 预滤到 4 倍、中位切分量化 64 色(`QUANT_COLORS`)、NEAREST 降到目标、alpha 二值(阈值 128)、删小于 10px 的孤立不透明域、边缘色内插 2 遍。四联底测试条的四底色在 `STRIP_BGS`。正常结束退出 0;源码没有显式退出码,未捕获异常按 Python 默认非零。
**已知坑与适用边界**:键表固定,任意资产不能直接跑,见「三条已知边界」第 1 条。候选文件名须以 `v` 开头且不含 `pixel`;命名解析只取 `v` 后一位数字,`v10.png` 会与 `v1.png` 落到同一个 `pixel-1.png`。`--standee48` 分支读完 `pixel-1.png` 后直接返回,`--fixer` 在站桩分支不生效;要先跑一次带 `--fixer` 的批量模式。`--fixer` 调用失败只打印一行并回退原图,批次继续,不会以非零退出码中断。去底依赖奶油描边;描边被压暗开缺口会漏灌,脚本按 60% 面积阈值回落,但仍假定底色均匀浅色。输出是角色精灵,不处理不透明全幅件。
## 外部工具:pixel-art-fixer
**一句话定位**:检测伪像素网格并把图重建为原生分辨率的真像素图;是 `--fixer` 的收网格后端。
**归属**:仓库外 `D:/code/pixel-art-fixer`,Retro Diffusion 开源,MIT。可执行文件 `D:/code/pixel-art-fixer/rust/target/release/pixelfixer.exe`;`pixelize_candidates.py` 的默认路径同此,可用 `--fixer-exe` 覆写。
**输入与输出**:PNG 或 JPEG,最短边不小于 16px、总像素不超过 4MP。`process` 输出原生 1x PNG,并在 stdout 打一行 JSON,含 `cols`、`rows`、`step_x`、`step_y`、`consensus`、`mode` 与耗时。透明按每格多数保留。
**用法命令**:
```bash
D:/code/pixel-art-fixer/rust/target/release/pixelfixer.exe process <输入.png> <输出.png> full
D:/code/pixel-art-fixer/rust/target/release/pixelfixer.exe process <输入.png> <输出.png> fast
D:/code/pixel-art-fixer/rust/target/release/pixelfixer.exe process <输入.png> <输出.png> full legacy
D:/code/pixel-art-fixer/rust/target/release/pixelfixer.exe recon <输入.png> <step_x> <step_y> <cols> <rows> <输出.png>
D:/code/pixel-art-fixer/rust/target/release/pixelfixer.exe full <输入.png>
```
**判据或口径**:`process` 做自动检测加重建,默认重建走 `two_stage_pack`——量化只用于决定结构,颜色取原图对应像素;加 `legacy` 改走旧的 mode-vote 重建。`recon` 是已知网格参数只做重建,默认 raw 不量化,传 `palette` 才启用自动色板,另可传 `dark`。`full` 与 `fast` 只检测不写图,逐图打 JSON;`full` 跑完整仲裁,`fast` 只跑便宜检测器。退出码:成功 0;`process` 输入不合法或写图失败 1;用法错误或未知命令 2。
**已知坑与适用边界**:输出常是几十到几百像素的原生图,显示必须 NEAREST 放大。它是收网格器,不是降噪或调色器;色数与色板仍归 `palette_check.py` 与 `standee_asset_check.py`。`recon` 不检测网格,参数给错就按错的网格重建。`pixelize_candidates.py` 只在 `--fixer` 时调用 `process`,失败打印一行后回退原图。仓库内的可执行文件是本地构建产物;换机器要按 README 的 Rust 用法执行 `cargo build --release`。
## tools/standee_asset_check.py
**一句话定位**:站桩素材硬闸,逐件校验档高、宽度、底锚、色数、alpha 二值与描边亮度。
**归属**:仓库内 `tools/standee_asset_check.py`。
**输入与输出**:默认读 `game/assets/art/character/` 下固定 11 件——八敌 `character_enemy_<键>.png`,主角 `character_hero_portrait.png`、`character_hero_attack.png`、`character_hero_hit.png`。输出 PASS/FAIL 文本,或 `--json` 机器可读结果。
**用法命令**:
```bash
python tools/standee_asset_check.py
python tools/standee_asset_check.py game/assets/art/character
python tools/standee_asset_check.py game/assets/art/character --json
```
**判据或口径**:档高逐键取自 `pixelize_candidates.STANDEE_TIER_H`,主角三态 48。源宽不大于 75,即乘 2 后不超站桩盒宽 150。画布底行至少一个不透明像素,即脚贴底缘。色数不大于 32,只统计 alpha>0 的唯一 RGB。alpha 只允许 0 或 255,出现中间值即判非整数倍缩放痕迹。敌人轮廓像素中暖白描边占比不低于 40%;暖白指亮度不低于 150 且饱和不高于 0.35。主角三态豁免描边断言。全过 0,任一缺失或红灯 1。
**已知坑与适用边界**:主角三态描边实测约 0.4% 暖白,属暗边已知缺口,本闸豁免、不判红;要验主角描边得另找证据。文件名必须与固定清单完全一致,缺一件即失败;目录里多出的 PNG 不检查。它只查站桩 11 件,不覆盖棋盘小人、场景层、UI 或瓦片。色数口径必须排除全透明像素;按所有像素计会假报 33 色。
## tools/palette_check.py
**一句话定位**:扫目录内 PNG,报单张色数与新增色,并按基线色板判新增色近色。
**归属**:仓库内 `tools/palette_check.py`。
**输入与输出**:递归读目录下所有 `.png`;可选 `--baseline` 目录作为基线色板。输出逐张 PASS/WARN/FAIL、小计与累计新增色。
**用法命令**:
```bash
python tools/palette_check.py <待检目录>
python tools/palette_check.py <待检目录> --baseline <基线目录>
```
**判据或口径**:单张色数等于 alpha>0 的唯一 RGB 数,排除全透明。无 `--baseline` 时是只读模式,只出读数、不判红。有基线时,本图颜色减去基线颜色并集得到新增色;新增色与基线任一色的最大单通道差小于 8/255 即判近色红灯。48 色是设计色板 token 上限,不是单张 PNG 的色数;单张 32 色冻结由 `standee_asset_check.py` 执法,本工具的单张色数只报读数。退出码:0 通过,1 有新增色红灯,2 没给目录。
**已知坑与适用边界**:近色只判新增色对基线,不做存量色两两互查。不做色相禁入带检查,也没有全图明度带检查,见「三条已知边界」第 2 条。单张色数超过 32 只标 WARN 或读数,不进退出码;文件头写的「超单张上限判红」与现行源码不一致,按源码为准。文件头提到 `--tokens`,但现行 `main()` 没有这个参数分支,`TOKEN_MAX=48` 目前只是常量。基线目录不存在时不会返回错误:基线色集为空,新增色判据失效。只排除 alpha=0 的像素,半透明像素的 RGB 也计入色数。
## tools/quantize_ui_skin.py
**一句话定位**:把 UI 复用包里的面板、按钮、石框、底噪与图标按 token 明度带量化改色,产出 `game/assets/art/ui/` 下的终稿件。
**归属**:仓库内 `tools/quantize_ui_skin.py`。
**输入与输出**:源包固定为 `artwork/ui-reuse-2026-09-23/` 下的 Kenney 与 OGA CC0 件;当前仓缺位时回退主检出 `D:/code/g-plan2`。输出 14 件到 `game/assets/art/ui/`——5 件框材与底噪、4 枚营地图标、5 枚节点图标。
**用法命令**:
```bash
python tools/quantize_ui_skin.py
```
**判据或口径**:面板、按钮、石框与底噪按五条硬编码 ramp 做明度重映射,保留 alpha 与纹理结构;ramp 常量在脚本顶部 `RAMP_*`。图标走单色重着色:钥石取奖励金,柜台四件取暖白;节点五件取暖白并烘到 20×20。节点图标自检三项——全像素只能是 `NODE_WARM` 或透明、尺寸 20×20、两两 alpha 剪影错位率不低于 0.08;失败 `SystemExit(1)`。复用闸四道(量化、禁入带、灰度可辨、CC0 授权)的来源说明在文件头;脚本实际自检只跑节点图标这三项,框材件没有逐件断言。全过 0,失败 1,源件缺位抛 `FileNotFoundError`。
**已知坑与适用边界**:源包路径、子目录名与文件名全部硬编码;换包或改名直接 `FileNotFoundError`,不是通用量化器。源包在 `artwork/`,整目录不入 git;worktree 或新检出缺件时才会回退主检出,主检出也没有就失败。只服务这一批 UI 复用件,不处理角色、场景或瓦片。输出覆盖同名件;改色值要改脚本顶部 ramp 常量后重跑,再让 Godot 重导入。
## tools/recolor_terrain_tiles.py
**一句话定位**:把 dungeon 地形瓦片按家族与亮度秩序换色,产出 underdark 与 deep_underdark 两套。
**归属**:仓库内 `tools/recolor_terrain_tiles.py`。
**输入与输出**:默认源与输出都是 `game/assets/art/board/`;只匹配 `terrain_*_dungeon.png` 与 `terrain_*_dungeon_<数字>.png`。输出把文件名里的 `_dungeon` 换成 `_underdark` 或 `_deep_underdark`。
**用法命令**:
```bash
python tools/recolor_terrain_tiles.py
python tools/recolor_terrain_tiles.py --palettes underdark
python tools/recolor_terrain_tiles.py --src <源目录> --out <输出目录> --palettes underdark,deep_underdark
```
**判据或口径**:源图每个唯一色先按 RGB 距离归到 dungeon 锚槽的四家族——墙顶、立面、地面、雷坑;再在家族内按亮度秩序线性映射到目标色表同家族槽列。目标色表 `PALETTES` 硬编码在脚本里,槽值来源为归档包 `archive/归档-terrain-walls-2026-09-26.zip` 内的生成器。映射确定性,无随机;脚本打印每枚源图的唯一色数。成功 0;源目录没有匹配瓦片时打印「零输出」并返回 1。
**已知坑与适用边界**:只做换色,不重绘、不增删结构;结构件缺件不能用它补。家族归属靠最近锚色距离,偏离锚槽的颜色可能归错家族。只认 dungeon 命名,其他图类型或其他前缀不处理。默认输出覆盖同目录同名件;源瓦片不入 git,跑前确认盘上源件在。
## tools/gen_theme_icons.py
**一句话定位**:程序化生成 UI 主题小位图——滑轨柄三态、SpinBox 上下箭头、OptionButton 箭头、勾选框四态。
**归属**:仓库内 `tools/gen_theme_icons.py`。
**输入与输出**:无输入参数;输出 9 张 PNG 到 `game/assets/art/ui/gen/`。
**用法命令**:
```bash
python tools/gen_theme_icons.py
```
**判据或口径**:色值取 `game/presentation/views/ui_kit.gd` 的既有常量,脚本头注明溯源;不新增美术素材、不改 art-bible §7。脚本幂等,重复执行覆盖同名件。滚动条柄不走位图,由 `StyleBoxFlat` 承载,本脚本不产出。无显式退出码;正常结束 0,写入异常按 Python 默认非零。
**已知坑与适用边界**:脚本没有验收断言,生成完要目检并让 Godot 重导入。改色值只能改脚本顶部常量再重跑,脚本不读 `ui_kit.gd` 的运行时值。只出主题小件,不处理面板、图标或场景。产出在素材目录,不入 git。
## tools/shot_checker.py
**一句话定位**:整屏截图机检,判屏级骨架、文字对比、背景纹理、明度三档带与文字是否还在框内。
**归属**:仓库内 `tools/shot_checker.py`。
**输入与输出**:截图目录,默认屏清单 `shot_menu.png`、`shot_camp.png`、`shot_level.png`、`shot_battle.png`;同目录 `shot_meta.json` 提供取样框。输出逐项 PASS/FAIL 摘要。
**用法命令**:
```bash
python tools/shot_checker.py <截图目录>
python tools/shot_checker.py <截图目录> shot_menu.png shot_battle.png
```
**判据或口径**:A 屏级骨架——PNG 存在、任一边 1280、灰度 std 不低于 8、唯一色数不低于 64。B 文字对比——战斗 C 区面板与明细带提示的 WCAG 不低于 4.5:1。C 明度三档——战斗屏信息面高于世界中,世界中高于世界底。D 文字禁压纹理——文字框背景中带灰度 std 不高于 18。F 文字在——菜单标题框内与中位灰度差大于 60 的像素占比落在 2% 到 55%。B/C/D 只在清单含 `shot_battle.png` 时跑,F 只在含 `shot_menu.png` 时跑,其余屏只跑 A。全过 0,缺参数或任一失败 1。
**已知坑与适用边界**:截图必须由窗口化 `shot_probe` 产出;headless 没有渲染面,截图是空的。`shot_meta.json` 缺失时 B/C/D 直接判 meta 几何缺失,不会跳过。尺寸断言按「任一边等于 1280」放行;1920×1080 截图两边都不等于 1280,会被判非工程档,与文件头「1920 档 1.5×」说明不一致,按源码为准。焦点预算是目检项,机检不判。
## tools/golden_board_crop.py
**一句话定位**:按 `board_shot_probe` 输出的 `board_meta.json` 从整屏截图裁出棋盘区,供 golden 逐像素比对。
**归属**:仓库内 `tools/golden_board_crop.py`。
**输入与输出**:三个参数依次是 `board_meta.json`、截图目录、输出目录;读截图目录内 `board_<tag>.png`,按 meta 矩形裁剪,写输出目录同名文件。
**用法命令**:
```bash
python tools/golden_board_crop.py <board_meta.json> <截图目录> <输出目录>
```
**判据或口径**:meta 键即 tag,矩形字段为 x/y/w/h;越界按图像边界收拢。meta 缺失、空表、截图缺失、矩形字段非法或退化,均返回 1;全部裁出 0。禁硬编码像素:布局改动只重跑探针刷新 meta。
**已知坑与适用边界**:依赖 `board_shot_probe` 的 meta 与截图命名;手工截图或改名不认。只裁剪不缩放,输出尺寸随 meta 矩形。它不跑比对;比对是 `golden_compare.py`。
## tools/golden_compare.py
**一句话定位**:黄金图与实测截图逐屏灰度对比,报视觉回归。
**归属**:仓库内 `tools/golden_compare.py`。
**输入与输出**:黄金图目录、新截图目录、可选屏清单;默认四屏 `shot_menu.png`、`shot_camp.png`、`shot_level.png`、`shot_battle.png`。输出逐屏 changed 占比与 PASS/FAIL。
**用法命令**:
```bash
python tools/golden_compare.py game/tests/golden <截图目录>
python tools/golden_compare.py game/tests/golden <棋盘裁剪目录> board_L1_dungeon.png board_L2_underdark.png board_L3_deep_underdark.png
```
**判据或口径**:两图尺寸必须一致;逐像素灰度差大于 16 记为 changed。changed 占比超过 2% 判失败;尺寸不符、缺基线或缺截图也判失败。全过 0,有失败 1。
**已知坑与适用边界**:只比灰度,不比色相;纯换色但明度相同可能漏过。基线在 `game/tests/golden/`,资产换代要显式重制并按该目录 README 记录判定,不能拿重制基线掩盖回归。默认四屏不含棋盘与关卡图;比棋盘要传裁剪后的文件名,比关卡图要传 `level_map.png`。截图分辨率必须与基线一致,基线是 1280 档。
## tools/render_sample_sheet_v3.py
**一句话定位**:战斗一屏样张的当前生成器,出 1280×800 与 1920×1080 两版,并在生成过程中跑断言。
**归属**:仓库内 `tools/render_sample_sheet_v3.py`。
**输入与输出**:读 `game/assets/art/board` 的 21 枚瓦片、`game/assets/art/character` 的小人与立绘、`game/assets/ui/icons` 的状态图标;素材缺位回退主检出。输出 `artwork/mockups/sample-batch-v3-final-2026-09-30.png`、`sample_battle_screen_1280x800.png`、`sample_battle_screen_1920x1080.png`;有基线时再出 `sample_battle_screen_before_after.png`;机检报告写到 `reports/`。
**用法命令**:
```bash
python tools/render_sample_sheet_v3.py
python tools/render_sample_sheet_v3.py --save-baseline
python tools/render_sample_sheet_v3.py --gen-assets
```
**判据或口径**:生成即验。F 段管方案甲拼装、旗位、脚锚、0 格字形、微光带限、立面双明度带、悬崖同源;A 段管色板对表、禁入带、周界闭合、明度对比、混排、净空、观感密度;整屏扩展含 S1–S4、P1–P5、V1、V10、A5b。全绿 0,有红 1,报告逐项列 PASS/FAIL。分区几何不设脚本常量:B 区高 / C 区宽 / AP 条高 / 明细带两态高经 `tools/layout_config.py` 读 `game/data/tables/battle_layout.tres`,与游戏侧 `ConfigDb.presentation.battle_layout` 同源。`--gen-assets` 产四件——`scene_stage_plaza.png` 48×12 与 `ui_armor_light`、`ui_armor_ward`、`ui_armor_heavy` 三枚 24×24;尺寸或色数不过直接 `RuntimeError`。`--save-baseline` 只把当前整屏存成基线并返回 0,整屏断言不跑,板面断言即使有红也不改退出码。
**已知坑与适用边界**:固定输出文件名带 2026-09-30,重跑覆盖同名件。字体硬编码 `C:\Windows\Fonts\msyh.ttc`,非 Windows 环境起不来。第一次跑要先 `--save-baseline`,否则 V10 因缺基线判红。素材不入 git;worktree 缺件会抛 `FileNotFoundError`,回退主检出后仍缺也失败。它产的是样张与机检报告,不是游戏内资产;唯一例外是 `--gen-assets` 的四件。
## tools/render_level_map_sheet.py
**一句话定位**:关卡画面样张生成器,出岔路、入口、单链与换层示意,并跑断言。
**归属**:仓库内 `tools/render_level_map_sheet.py`。
**输入与输出**:读 `artwork/ui-reuse-2026-09-23/` 的节点图标与字体、`game/assets/art/ui/backdrop_stone.png`;画布公式、停点标题与 `HIT_PAD` 从 `game/presentation/views/level_map_screen.gd` 解析。输出 `artwork/mockups/level-map-sheet-v1-2026-09-28.png`;机检报告写到 `reports/`。
**用法命令**:
```bash
python tools/render_level_map_sheet.py
```
**判据或口径**:断言 L1 图标互辨、L2 四态与 Boss 环灰度、L3 对比不低于 4.5:1、L4 禁入带零命中、L5 画布自适应、L6 独立屏底、L7 占位区不侵入;另有 SRC 同源、ST 态齐备、S3 换层。全绿 0,有红 1,报告逐项列 PASS/FAIL。节点图标 20px、通道线宽 2/3/4px 等冻结值在脚本顶部;画布公式不自立,解析自 `level_map_screen.gd`。
**已知坑与适用边界**:依赖 `level_map_screen.gd` 存在且常量写法可解析;改常量写法会让脚本抛 `FileNotFoundError` 或解析失败。字体同样硬编码 Windows 字体。固定输出文件名带 2026-09-28,重跑覆盖。样张是验收图,不是游戏资产。
## tools/render_sample_sheet.py
**一句话定位**:战斗一屏样张 v2 生成器,现行 v3 的前一版。
**归属**:仓库内 `tools/render_sample_sheet.py`。
**输入与输出**:读同一批游戏素材,输出 `artwork/mockups/sample-batch-v2-2026-09-28.png`;机检报告写到 `reports/`。
**用法命令**:
```bash
python tools/render_sample_sheet.py
```
**判据或口径**:断言 F1–F7 与 A1–A6;全绿 0,有红 1。产物为 1280×800 单版,没有 v3 的 1920 档、整屏 S/P 段与改前改后并排。
**已知坑与适用边界**:已被 `render_sample_sheet_v3.py` 取代,只用于复现 v2 旧口径,不要拿它出新冻结。固定输出名与报告名带 2026-09-28,重跑覆盖。字体与素材依赖同 v3。
## tools/path_check.py
**一句话定位**:扫描 `asset-path-map.md` 声明的目录与现役文件,以及代码里的 `res://assets/art` 引用,报悬空与退役命名。
**归属**:仓库内 `tools/path_check.py`。
**输入与输出**:可选 `--repo` 仓库根、`--code` 代码根,默认 `.` 与 `game`。输出 L1 目录、L2 文件、L3 代码引用三段 PASS/FAIL。
**用法命令**:
```bash
python tools/path_check.py
python tools/path_check.py --repo . --code game
```
**判据或口径**:L1 查 `asset-path-map` §1 的声明目录,`design/art/candidates` 按需创建豁免。L2 查 `game/assets/art` 的四个语义子目录与现役文件清单,含主角 idle/walk/portrait、八敌、铲三帧、封印印记。L3 grep 代码里的 `res://assets/art` 引用,核对盘上存在性,并检出 `board_floor`、`board_wall`、`pillar_dungeon`、`production/mockups` 等退役命名;路径模板含 `%s` 或 `%d` 的跳过。worktree 素材未同步时,缺失项标 SKIP 不判红;主区完整素材环境才全量判。全过 0,有红灯 1。
**已知坑与适用边界**:依赖 `asset-path-map.md` 的声明清单与代码常量;新增素材目录不更新文档就不会被查。素材不入 git,worktree 空或半同步会大量 SKIP,不能据此判断主区健康。只核对路径存在与退役命名,不核对图像内容、尺寸或色板。
+11 -45
View File
@@ -1,7 +1,7 @@
# 生图提示词卡(角色级资产)v2.9
# 生图提示词卡(角色级资产)v2.12
> **性质**:美术清单生图通道(✦ 用户自跑,现走线上 API)的执行侧文件——每资产一段**自包含完整提示词**:正向 = 通用底座+主体+动作已拼妥、负向 = 通用底座+增补已并妥,整段复制直接贴进工作流即可,**不需要读其他文件、不需要拼装**。人物规格转写自 [character-design.md](character-design.md)(人物设计 v1.0,自 art-bible §5 迁出,圣经 §5 为存根),风格/色彩条款转写自 [art-bible](art-bible.md)(§4.1 主色板 / §4.2 语义色规则);冲突时人物规格以 character-design 为准、色彩以 art-bible 为准。
> **边界**:本文件只管**角色级**资产的生图通道。图标/格面/样张走脚本管线(⚙ 代理出),规格在 art-bible §4.6 与 [art-assets-checklist](art-assets-checklist.md),不需要提示词。**场景级资产(营地场景图的背景层与八个物件层)另立 [camp-scene-prompts](camp-scene-prompts.md),本文件不覆盖**;两卡的角色/场景分工以「谁的资产谁出卡」为界,同一段提示词不两处维护。战斗舞台的场景级两件与护甲类型图标三枚的**出图工单**登记在本文件 §5(camp-scene-prompts 只管营地场景图,战斗屏这两类无处可落);§5 只把各权威节的定值收进逐件工单,不立新值。意图动画三档是动画资产,等一屏样张锁方案后另立规格卡,本文件不覆盖。
> **边界**:本文件只管**角色级**资产的生图通道。图标/格面/样张走脚本管线(⚙ 代理出),规格在 art-bible §4.6 与 [art-assets-checklist](art-assets-checklist.md),不需要提示词。**场景级资产(营地场景图的背景层与八个物件层)另立 [camp-scene-prompts](camp-scene-prompts.md);战斗舞台背景层与连续地面层另立 [battle-stage-prompts](battle-stage-prompts.md),本文件不覆盖**;两卡的角色/场景分工以「谁的资产谁出卡」为界,同一段提示词不两处维护。战斗舞台的小石台件与护甲类型图标三枚的**出图工单**登记在本文件 §5;§5 只把各权威节的定值收进逐件工单,不立新值。意图动画三档是动画资产,等一屏样张锁方案后另立规格卡,本文件不覆盖。
> **状态**:提示词卡成文,出图未开始;按「美术圣经 §5 现行」重生成(正序:B 路线),继承 seed 纪律 / 色相禁入带 / `#F2F2F2` 生成底色;沿革见 git 提交史。
---
@@ -11,7 +11,7 @@
1. **出图**:按下面卡片把正向/负向提示词粘进生图渠道的提示词栏(**渠道 = 远程对话式生图为主,本地 ComfyUI 为备选**——2026-10-06 拍板;对话操作纪律见 [remote-gen-prompts](remote-gen-prompts.md)),每个资产出 **2–3 版候选**(建议固定 3 个 seed 连拍,便于横评)。
2. **入库**:候选原图(带底)放 `artwork/candidates/<资产名>/`,文件名 `v1.png / v2.png / v3.png`。这个目录只存候选,不进引擎。
3. **裁决**:你按 §4 验收速查预筛,挑出每一资产的入围候选拍板。
4. **后处理**:喊我一声,脚本管线代跑——去底转透明、最近邻降采样到目标尺寸、色板量化、四联底测试条(掩尘底/裸土底/暗底/灰度)。
4. **后处理**:喊我一声,脚本管线代跑——去底转透明、最近邻降采样到目标尺寸、色板量化、四联底测试条(掩尘底/裸土底/暗底/灰度)。后处理工具清单与用法见 [art-pipeline-tools](art-pipeline-tools.md)。
5. **转正**:终稿从 `artwork/candidates/` 转入切片资产目录,随表现层升级批接线;清单状态列改「已交付」。
**出图通道(2026-09-23 定稿)**:线上 API,用户自跑——本地 ComfyUI 生图路线已经用户拍板**彻底关闭**(母版工作流/像素 LoRA/Z-Image 模型已清退,不留复活预案,本卡不依赖任何本地管线)。
@@ -286,49 +286,15 @@ Seamless tileable pixel art texture of dry dirt ground, top-down, for a dungeon
## 5. 资产出图工单(四组九件)
> **范围注**:本文件原定位角色级(见文件头边界句),§4bis 已有「地形类同库登记」的先例,本节循同一先例补登记战斗屏两类——战斗舞台的场景级两件(① ②)与护甲类型图标三枚(③),两组在营地卡与清单里都无处落工单;角色级立绘重产(④)是本文件的本职。四组的规格定值清单 §4 已有登记,本节补的是「逐件照着就能出件」那一层。
> **范围注**:本文件原定位角色级(见文件头边界句),§4bis 已有「地形类同库登记」的先例,本节循同一先例补登记战斗屏的**小石台件(②)**与护甲类型图标三枚(③),两组在营地卡与清单里都无处落工单;战斗舞台背景层(①)与连续地面层的元素/提示词已另立 [battle-stage-prompts](battle-stage-prompts.md),§5.1 只留指路;角色级立绘重产(④)是本文件的本职。四组的规格定值清单 §4 已有登记,本节补的是「逐件照着就能出件」那一层。
> **定值纪律(§8 头注 定值单点纪律)**:本节**指路不立值**——色值 = 圣经 §4.1(含场景环境子板)、尺寸与缩放 = §8.2、明度带 = §2、甲档 = character-design §5.3、命名 = §8.3 + asset-path-map。本节新增零定值;凡圣经未定的值,一律进本节 §5.5 缺口,不在本节填空。
> **验收标注纪律**:逐条按三档标 `←机检: <断言>` / `←目检: <判据>` / `←方向性`,照圣经头注 2026-10-06 用户拍板的写法。
> **流程**:出图前后一律走 §0 五步;出图登记义务(模型名 + 分辨率 + seed + 对话/日期)照 remote-gen-prompts 头注,随候选图存 `artwork/candidates/<资产名>/`,缺登记的候选不入正稿遴选。
> **流程**:出图前后一律走 §0 五步;出图登记义务照 remote-gen-prompts 头注轻量口径(用途+生成时间必注,其余参数有则记),随候选图存 `artwork/candidates/<资产名>/`;登记不作正稿遴选前置。
> **产出状态**:组二地台件与组三护甲三枚已按各节 ⑤「程序化产出即终稿」产出入包(`scene_stage_plaza.png` 48×12、`ui_armor_light/ward/heavy.png` 24×24,路径见各组 ① 字段;登记=战斗一屏样张批,合并提交 3b66d1a6);组一背景插画已迁至 [battle-stage-prompts](battle-stage-prompts.md)(仍待出图),组四立绘重产仍待出图。
### 5.1 组一 · B 区舞台背景插画(1 件)
| 字段 | 值 |
|---|---|
| ① 路径与命名 | `game/assets/art/scene/scene_stage_bg.png`——域键 `scene`(asset-path-map §3),命名照 §8.3 `<域>_<对象键>_<状态\|变体>[_<序号>].png`。**三关不按板出三张,文件名不带 `_dungeon`/`_underdark`/`_deep_underdark` 后缀**(三关换挡走 §4.3 `wall_ambient` tint 槽,与棋盘同款换皮逻辑) |
| ② 尺寸档与像素网格 | 源 1280×270(B 区全高含明细带底),显示 1×;九宫格 = 否。甲/乙两分类不覆盖环境件(§8.2 甲类 = 文本与语义件、乙类 = 角色与单位精灵),本件在 1280×800 基座下满幅铺 1×、不缩放;1920 档口径见 §8.2「1920×1080 档策略」条,本节不另立值 |
| ③ 色板约束 | 进**战斗屏场景环境子板**(§4.1 分层②:每屏一套 ≤32 色、跨屏不共享、**不占 48 token 额度**——48 额度余量现为 0(2026-10-28 用尽),本件不动它)。**零新增**——主体三色:远景最深 `#282220`(默认;备选 `#3A2E22`,二选一,选后者则图内禁用 `#362B20`,两者三通道差 4/3/2 全部 <8,是 §4.1 欠账清单六对之一);中景洞壁 `#3E362C` 与中景受光壁面 `#453723` 交替,**两色只靠笔触与纹理分出块面,不靠第三个色**;需过渡色时只从在册的 `#362B20`/`#4C4038` 取。单张 PNG 像素域色数 ≤32。第二道门 = 与既有任一色三通道差不得全部 <8/255(§4.1 增量纪律),不过门即换色合并、禁加色绕行 |
| ④ 明度带归属 | **世界底层下沿子带 L 0.014–0.06**(§2 世界底层两子带)——全图落该子带,不得越进上沿子带 L 0.08–0.14(那是已开格底与迷雾记忆态合成底的地盘)。三色实测: `#282220` L≈0.016 / `#3E362C` L≈0.039 / `#453723` L≈0.042 |
| ⑤ 是否程序化件 | 否 → **须出图**(✦ 插画级,清单 §4 已定来源 = 作者自跑远程生图管线)。现行程序化平色 + 光柱 = 占位(◐),按 §7.1 占位完成条款须在终稿闸前换代 |
| ⑥ 验收判据 | 见下方逐条判据 |
| ⑦ 出图操作要点 | 见下方提示词与负面约束 |
**逐条判据**
- 构图只出远景 + 中景两带:远景 0–35%、中景 35–78%,78%–100% 段不画 ←机检: 画布 1280×270 逐位相符;`y ≥ 210`(78%) 段内 `#968264` 像素数 = 0 且该段与中景段无独立地台结构件
- 三关只走 tint 槽 ←机检: 仓内 `scene_stage_bg*` 文件名无 board_type 后缀变体(三关方向指路 §6.3,该节终值未冻结,属实装批,不阻塞本件出图)
- 色板零新增 ←机检: 单张像素域色数 ≤32 且逐色在 §4.1 战斗屏场景环境子板在册
- 明度带 ←机检: 全图取样点相对亮度 L 全部落 [0.014, 0.060]
- 禁入带 ←机检: 全图无 HSV 色相 300°–345° 像素(清单 §0 复用闸②吸色检查同口径)
- 临时光柱层退役 ←机检: 本件入包后 `battle_stage_view.gd` 四柱临时光柱层退役(§8.2 站桩排版③条;该条的立绘溢出判定不随之变更)
- 背景自然后退 ←目检: 立绘与 C 区数字在前,本件不构成完整凸形、无第二焦点(§3.4 焦点预算 / §6.1 低对比后退)
- 气质 ←方向性: 明亮地窟、暖倾向、无压抑(§1 风格基调;§9.2 Darkest Dungeon 式压暗在反面清单)
**出图画布**:2560×540(终稿 2×,保证整数倍降采样;平台不支持该宽高比时取可用的最宽档出图、按上下居中裁到 4.74:1,禁横向拉伸变形)。
**正向提示词(整段可贴)**:
```
Pixel art style. True pixel art game background illustration drawn with chunky square pixels, strict pixel grid alignment, hard edges, no anti-aliasing, no gradients, flat colors only, a limited palette of about eight colors, retro 16-bit look, Aseprite style, all ages, bright cheerful fantasy cavern, not dark and not grim, flat side view, orthographic, no perspective distortion, very wide horizontal band about 4.7 to 1 aspect ratio. This is the BACK WALL of a bright underground cavern arena seen straight on, and it is a BACKGROUND ONLY: it must contain no characters, no creatures, no floor platform and no props, because the standing stone platform and the characters are drawn on top of it as separate pieces. TWO BANDS ONLY, and the bottom twenty-two percent of the frame stays completely empty. FAR BAND (the top zero to thirty-five percent of the frame height): the deepest hollow of the cavern, a flat dark far wall receding into shadow, one or two rows of very simple flat stalactite silhouettes hanging from the top edge, the darkest area of the picture, its darkest value is hex color #282220, no light sources, no small detail, everything reduced to a few large flat shapes. MIDDLE BAND (thirty-five to seventy-eight percent of the frame height): the cavern wall, broad flat facets of stone alternating between hex colors #3E362C and #453723, the facets separated only by their brush texture and by a few slightly darker seam lines, with two or three soft vertical light shafts falling from the cave roof across the wall, the shafts being flat low-contrast bands of light and not beams of particles. WHOLE IMAGE: every color is very dark and low in contrast, the entire background recedes behind the characters, there are no bright highlights and no rim lights, no single element forms a complete solid shape that competes for attention, and all wall facets stay large and simple.
```
**负向提示词(整段可贴)**:
```
Do not add any of the following: realistic, photorealistic or 3D rendering; painterly brushwork, smooth anti-aliased lines, gradient or dithered shading, glossy highlights; blur or compression artifacts; a vignette, dark corners, or any mood darkening; any purple or magenta anywhere, any color with a hue between 300 and 345 degrees; any text, letters, numbers, watermark, signature or logo; any frame, border, panel, arrow or UI element; any characters, people, creatures or animals; any floor, ground plane, stone platform, pedestal, stage floor or baseboard in the lower part of the frame, because the bottom twenty-two percent of the frame must stay completely empty; any torch, brazier, lantern, candle or glowing object as an object, only the flat light shafts described above; bright or saturated colors, any area lighter than hex #453723; high contrast edges, rim lights, black outlines; rubble, chains, bones, murals, paintings, furniture or any extra prop.
```
**后处理**:§0 五步第 4 步——本件是**不透明全幅件**,去底与四联底测试条两步不适用,只走最近邻降采样到 1280×270 + 色板量化到本节 ③ 所列色;终稿入包后走 §1.4 整屏样张闸。
> **本件已迁出本文件**:逐条要素、分层提示词、否定后缀与验收 = [battle-stage-prompts](battle-stage-prompts.md)(背景层 §2.1/§4);场景构成与图层归属 = [art-bible](art-bible.md) §6.1;尺寸读 `game/data/tables/battle_layout.tres`。本文件不再复写本件提示词,避免两处维护。
### 5.2 组二 · 近景地台件(1 件)
@@ -342,7 +308,7 @@ Do not add any of the following: realistic, photorealistic or 3D rendering; pain
| ⑥ 验收判据 | 见下方逐条判据 |
| ⑦ 出图操作要点 | 见下方工艺要点(手绘/程序化,无须 AI 生图) |
**独立成件的两条理由(§6.1「近景地台走独立件」条,写进工单备查)**:① 结算期明细带底 84px 会盖住 B 区 78%–100% 段(约 59px),84 > 59,近景带在结算期整条不可见——画进背景等于画一件永远看不见的东西;② §4.3 `wall_ambient` tint 作用于整张背景,地台若画进背景会随三关换明度,而 §6.1 舞台正面目标把地台定死为世界中层承载面,随关卡变会破 §2 明度分层的层间关系。
**独立成件的两条理由(§6.1「近景地面与地台走独立件」条,写进工单备查)**:① 背景层按关卡出三张变体,地台若画进任一张就得三张各画一遍、且三关站位面会漂移;② 结算期明细带整条压住 B 区底带(带高 = `log_band_h_settle`,读布局表),地台在独立层才能跨关恒定并保持 §6.1 定死的世界中层承载面不变。
**逐条判据**
@@ -378,7 +344,7 @@ Do not add any of the following: realistic, photorealistic or 3D rendering; pain
- ←机检: 源 24×24、显示 1× 整数倍(§8.2 显示档登记例外条:素材一字节禁改)
- ←机检: 文件名逐位等于 `ui_armor_light` / `ui_armor_ward` / `ui_armor_heavy`
- ←机检: 单张像素域色数 ≤32
- ←机检: 三枚对其实际挂载底色的图形对比 ≥3:1(§4.1 图标槽柔和档条的图形参考线硬约束)。**挂载底色 = 站桩名字行的素底,不是舞台底也不是地台面**——站桩名字行是文字,§7.2「一切文字(含菜单/说明/tooltip/按钮/底栏,**无区域例外**)」+「文字禁直接压纹理底——垫容器或罩层」两条覆盖它;§6.1 舞台正面目标条(2026-10-28 v3.22 定值)已就站桩文字位显式落法:「站桩文字位改垫素底或落进结算明细带」,并明写「**非新条款**」,§7.2 同批加「站桩石板地台面同理」句、两处机检断言同文。舞台背景插画 `scene_stage_bg.png` 的中景两色只靠笔触与纹理分块面(5.1 ③),属纹理底,落法门触发。**故「站桩名字行落法 = 垫素底」是既有条款的落法,本节不立新条款**(§5 头注定值纪律:本节指路不立值)。素底明暗档 **2026-10-28 已裁 = 深档 `#282220`**(§7.4 血数垫牌 `HP_BAR_BACK` 在册同款,零新增;浅档 `#F5EAD3` 落选,两档对照与理据见 §5.5 缺口 2 已裁条)
- ←机检: 三枚对其实际挂载底色的图形对比 ≥3:1(§4.1 图标槽柔和档条的图形参考线硬约束)。**挂载底色 = 站桩名字行的素底,不是舞台底也不是地台面**——站桩名字行是文字,§7.2「一切文字(含菜单/说明/tooltip/按钮/底栏,**无区域例外**)」+「文字禁直接压纹理底——垫容器或罩层」两条覆盖它;§6.1 舞台正面目标条(2026-10-28 v3.22 定值)已就站桩文字位显式落法:「站桩文字位改垫素底或落进结算明细带」,并明写「**非新条款**」,§7.2 同批加「站桩石板地台面同理」句、两处机检断言同文。舞台背景插画(`scene_stage_bg_<board_type>` 三关变体;battle-stage-prompts §2.1)属纹理底,落法门触发。**故「站桩名字行落法 = 垫素底」是既有条款的落法,本节不立新条款**(§5 头注定值纪律:本节指路不立值)。素底明暗档 **2026-10-28 已裁 = 深档 `#282220`**(§7.4 血数垫牌 `HP_BAR_BACK` 在册同款,零新增;浅档 `#F5EAD3` 落选,两档对照与理据见 §5.5 缺口 2 已裁条)
- ←机检: B 区站桩名字行与护甲图标正下方像素非地台板身色 `#968264`(§6.1 / §7.2 同文断言)
- ←目检: 灰度下按甲面覆盖率三档可辨——无甲板 → 符纹带 → 大面积甲板(character-design §5.3 灰度通道条)
- ←目检: 三枚互辨,身份只靠形状不靠色相(§4.5 ⑨ 图标键族:色相不承载身份)
@@ -444,7 +410,7 @@ Do not add any of the following: realistic, photorealistic or 3D rendering; pain
1. 出件顺序 = 先主角、再三 Boss(Alpha 范围 = 主角 + 三 Boss 重产,其余敌种当前可读则保留到 Beta)。
2. **源密度判据 = 三判据②**:显示 96–188px 下脸、铲、旗、物种可认——照这条反推源档即可,逐键定档已完成(§8.2),本批不必再猜档位。
3. 手绘起稿顺序 = 剪影(§5.1 archetype)→ 体块主色 → 暗部与受光 → 描边 → 落影底盘。
4. 逐件 2–3 版候选;登记义务项 = 手绘者 / 日期 / 源档尺寸(手绘件无模型名与 seed,按此三项替换 §0 的登记项),随候选图存 `artwork/candidates/<资产名>/`。
4. 逐件 2–3 版候选;登记照 remote-gen-prompts 头注轻量口径(用途+生成时间必注,手绘作者有则记),随候选图存 `artwork/candidates/<资产名>/`。
5. 候选评审 = 并排横评,**隐去文件名与来源后按三判据逐条判读**(§0 五步第 1 步「便于横评」的手绘侧落法),不靠记忆与印象拍板。
6. 出件 = 透明 PNG(Alpha 直出,禁先出白底图再抠);入包前过 §8.3 色板检查器(报新增色 / 近色告警 / 超 48 上限拦截)。
@@ -490,7 +456,7 @@ Do not add any of the following: realistic, photorealistic or 3D rendering; pain
- ←机检: 本值入板后全板色对中三通道差全部 <8/255 的对数 = 0
2. **站桩名字行的落法 + 素底明暗档——已裁(2026-10-28)= 垫素底、素底取深档 `#282220`**,本节转指路(原记为「轻甲木色在深底上不过 3:1」;核出落法是它的前置,故先落这一条)。
- **推导核实 = 成立,且是既有条款的落法、非新条款**:站桩名字行是文字,§7.2「一切文字(含菜单/说明/tooltip/按钮/底栏,**无区域例外**)」+「文字禁直接压纹理底——垫容器或罩层」两条覆盖它;§6.1 舞台正面目标条(2026-10-28 v3.22 定值)已就站桩文字位显式落法——「站桩文字位改垫素底或落进结算明细带」并明写「**非新条款**」,§7.2 同批加「站桩石板地台面同理」句,两处机检断言同文。舞台背景插画 `scene_stage_bg.png` 中景两色只靠笔触与纹理分块面(5.1 ③),属纹理底 → 落法门触发。**已回填**:站桩名字行落法 = **垫素底**(§5.3 共用验收条 + 5.3.1 / 5.3.3 对比判据)。
- **推导核实 = 成立,且是既有条款的落法、非新条款**:站桩名字行是文字,§7.2「一切文字(含菜单/说明/tooltip/按钮/底栏,**无区域例外**)」+「文字禁直接压纹理底——垫容器或罩层」两条覆盖它;§6.1 舞台正面目标条(2026-10-28 v3.22 定值)已就站桩文字位显式落法——「站桩文字位改垫素底或落进结算明细带」并明写「**非新条款**」,§7.2 同批加「站桩石板地台面同理」句,两处机检断言同文。舞台背景插画(`scene_stage_bg_<board_type>` 三关变体;battle-stage-prompts §2.1)属纹理底 → 落法门触发。**已回填**:站桩名字行落法 = **垫素底**(§5.3 共用验收条 + 5.3.1 / 5.3.3 对比判据)。
- **原冲突 = 素底明暗档圣经未定死(2026-10-28 已由作者裁,本节不替裁的冲突至此闭合)**:§4.1 只在册浅档 `#F5EAD3`(数字色牌,圣经唯一素色牌值),§7.4 血数垫牌在用的深档 `HP_BAR_BACK #282220` 亦在册。两档后果**相反**(下表留作裁定依据账):
| 受影响的位 | 浅档 `#F5EAD3` | 深档 `#282220` |
+1
View File
@@ -40,6 +40,7 @@
| 连锁坍塌显形演出 | ⬜ | [art-assets-checklist](art-assets-checklist.md) §4 |
| 封印特效(动作=封印,两件:施印特效+印记常显) | ⬜ | [art-bible](art-bible.md) §3.2 + [art-assets-checklist](art-assets-checklist.md) §4 |
| 弹道 / 退场等动画密度 | ◐ 程序化弹道在役(play_projectile 0.16s+0.19s 到达拍调度;密度终稿归战斗表现) | [art-assets-checklist](art-assets-checklist.md) §4(战斗表现输入清单) |
| B 区舞台背景三关变体+共用连续地面层(场景件) | ✅ 已交付(2026-10-31 入包 `game/assets/art/scene/`:三张背景 1280×252、共用地面 1280×100;地面按作者裁保留候选 v12b 原亮度 L≈0.132) | [art-bible](art-bible.md) §6.1 · [battle-stage-prompts](battle-stage-prompts.md) §2 |
---
+1 -1
View File
@@ -27,7 +27,7 @@
|---|---|---|
| `character/` | 角色棋盘帧 + B 区立绘 | character_hero_idle/walk(棋盘 32×32 朝左基准)、character_hero_portrait(B 区立绘 46×48)+ character_enemy_<键> 八敌 |
| `fx/` | 特效帧 | shovel_swing_f0/f1/f2(挥铲三帧,握把锚 (18,12)) |
| `scene/` | **场景图**(背景层 + 可点物件层;营地场景图首批) | `scene_camp_bg`(背景)+ `scene_camp_gate / forge / chest / notebook / dummy / board / campfire`(七个可点物件)+ `scene_camp_tent`(承载件,不可点)+ `scene_stage_bg`(B 区战斗舞台背景,✦ 插画级,只出远景+中景两带)+ `scene_stage_plaza`(B 区近景地台,48×12 九宫格件)——登记超前:营地场景图批落地前盘上未建 |
| `scene/` | **场景图**(背景层 + 地面层 + 围墙层 + 可点物件层;营地场景图首批) | 营地 10 张:`scene_camp_bg`(背景层,不透明全幅=天空+远山+地面+门洞小路)+ `scene_camp_wall`(围墙层,透明底、门洞留透明缺口)+ `scene_camp_gate / forge / chest / notebook / dummy / board / campfire`(七个可点物件)+ `scene_camp_tent`(承载件,不可点)+ `scene_stage_bg_dungeon` / `scene_stage_bg_underdark` / `scene_stage_bg_deep_underdark`(B 区战斗舞台背景层三关变体,✦ 插画级,顶部空区+远景天然洞壁,不画近景地面,按 board_type 选图)+ `scene_stage_ground`(B 区连续近景地面层,✦ 插画级,泥土/砂砾/碎石+顶缘分隔带,跨关恒定)+ `scene_stage_plaza`(B 区角色脚下小石台,48×12 九宫格件)——B 区战斗舞台背景与连续地面 2026-10-31 已入包;登记超前:营地场景图批落地前盘上未建 |
| `board/` | 砖面/封印印记(三关换皮 + 封印印记件) | terrain_floor_<board_type>_<n>.png(三关地面系)、seal_mark.png |
## 4. 坐标/朝向等工程侧约定备查
+182
View File
@@ -0,0 +1,182 @@
# 战斗舞台背景 · 分层生图提示词卡 v1.8
> **性质**:B 区战斗舞台背景(顶部空区+远景天然洞壁+近景连续泥土地面+分隔带)的出图执行侧文件。背景按三关出三张变体,地面三关共用一张;场景级资产,与角色级 [art-prompts](art-prompts.md) 并列,两文件互不覆盖。
> **依据**:场景构成与图层归属 = [art-bible](art-bible.md) §6.1;三关方向 = art-bible §6.3;色板 = art-bible §4.1(战斗屏场景环境子板);尺寸 = `game/data/tables/battle_layout.tres`(背景读 `stage_h`;地面读 `ground_h`,该字段由地面层实装批新增并回填);命名与路径 = [asset-path-map](asset-path-map.md);后处理 = [art-pipeline-tools](art-pipeline-tools.md)。
> **边界**:只管三张背景变体与一张共用地面的提示词与验收;角色、格面、图标、小石台 `scene_stage_plaza.png` 与样张走 art-prompts 与脚本管线,不在本卡。
> **建立**:2026-10-31 · 状态:三关背景变体与共用地面已按本工单出图并入包(选定=dungeon v10c/underdark v10c/deep_underdark v10c/ground v12b,落点与读数见 art-bible §6.1「B 区舞台背景与共用地面的交付绑定」);出图通道由作者自定(参考候选可用内置 ImageGen,水印可留;入包终稿须干净)。
---
## 0. 使用流程(四步)
1. **出图**:按 §2 把提示词贴进生图渠道;每次出图先放 §1 的风格参考图。**先只出 dungeon 背景 1 张**,作者确认风格前缀后,再出 underdark / deep_underdark 两张变体与连续地面层。
2. **入库**:候选原图放 `artwork/candidates/scene_stage_bg/`(三关变体用 `dungeon_*` / `underdark_*` / `deep_underdark_*` 命名)与 `artwork/candidates/scene_stage_ground/`;`_登记.txt` 记用途与生成时间。
3. **后处理**:三张背景不去底(不透明全幅);连续地面层去底转透明(顶缘以上键控)+最近邻降采样+色板量化+底测试条;工具清单与已知边界见 [art-pipeline-tools](art-pipeline-tools.md)。
4. **转正**:终稿转入 `game/assets/art/scene/`;三张背景按 `board_type` 选图、连续地面层接线与布局表 `ground_h` 字段由实装批落码。
---
## 1. 风格参考图(每次出图都要带)
| 参考图 | 作用 |
|---|---|
| `game/assets/art/character/character_hero_idle.png` | 定像素密度、平涂质感、描边与暖色族 |
| `game/assets/art/board/terrain_floor_dungeon_1.png` | 定岩面/地面的笔触与色族;只借质感,不抄瓦片形状与拼块接缝 |
| `game/assets/art/scene/scene_stage_plaza.png` | 定近景层色族与顶缘处理;只借色与像素密度,不借石板铺装 |
**禁止**拿未经验证的自造图当风格参考——风格错了整批全废。元素参考可用 D&D 洞穴/地下城的岩层、裂缝、凹壁、碎石等常见构成(见 §3),但不把外部图当风格锚。
贴参考图时,在提示词末尾接这一句:
```
Style reference: the attached images are this game's own finished pixel art — match only their rendering style, the same pixel density, flat shading, hard edges and warm limited palette. They are style references only: do not copy their subjects, characters, gear, props, tile patterns or any specific rock or ground layout, and do not put any character into the scene.
```
---
## 2. 两层提示词
### 2.1 背景层(三关变体,按 `board_type` 选一张)
**画什么**:顶部空区(深色、无物);**纵深洞厅远景**——远层暗剪影(远处洞口/通道、层叠岩体、洞底轮廓)、中层主要细节层(岩台、岩柱、凹壁、裂缝、建筑剪影)、近层左右暗色岩框;三关母题按 §3.2。
**不画什么**:近景地面、地台、平台;贴墙感、单件圆柱/贴图直接贴顶、悬挂物;砖石墙、规则砌缝、方整石块、瓷砖/铺装;钟乳石、吊锥、洞顶结构;角色、生物、道具、火把/灯/发光物;文字/UI。
**三关变体**:
| 关卡 `board_type` | 资产 | 岩体/构图 | 色族 |
|---|---|---|---|
| dungeon | `scene_stage_bg_dungeon.png` | 深远洞厅:远层支洞/通道口;中层岩台+歪斜木栈道/脚手架+削尖木桩围栏 | 暖石棕(`#282220` / `#3E362C` / `#453723`) |
| underdark | `scene_stage_bg_underdark.png` | 深远洞厅:巨型石柱、细长石笋、蛛网、螺旋塔剪影、巨菇、裂谷石桥 | 冷蓝灰+少量紫罗兰灰(约 256° 域;避 300°–345°) |
| deep_underdark | `scene_stage_bg_deep_underdark.png` | 深远洞厅:石笋城郭、棱角石厅/扶壁、跨谷石桥、矿道口、烟囱塔群 | 灰黑主调+金属质矿脉 |
**出图画布**:三张同规格——终稿 2× = (2× 基准屏宽) × (2× `stage_h`),尺寸读 `battle_layout.tres`。平台不支持该宽高比时取可用最宽档、按中央 5:1 横带裁到目标宽高比(基准屏宽 / `stage_h`),禁横向拉伸。当前内置参考通道最宽 2560×1080,按中央带裁。
**共用底座正向提示词(三张共用)**:
```
Pixel art style. True pixel art game background layer drawn with chunky square pixels, strict pixel grid alignment, hard edges, no anti-aliasing, no gradients, flat colors only, a limited palette of about eight colors, retro 16-bit look, Aseprite style, all ages, bright cheerful fantasy cavern, not grim, flat side view, orthographic, no perspective distortion. A very wide horizontal composition, about 5 to 1 aspect ratio; if the canvas is taller than that band, paint the band across the middle and fill the area above and below with flat dark continuation #282220, keeping all detail inside the band. This is the FAR BACKGROUND of an underground cavern arena: the camera stands at the arena edge and looks across a deep cave chamber, so the view must read as a distant cavern interior with real depth, NOT as a flat wall close behind the characters. BACKGROUND LAYER ONLY: no ground, no floor, no platform, no characters, no creatures, no props in the near view, no torches, no lanterns, no light sources of any kind, no light shafts, no beams of light, no glow. TOP BAND (the top fifth of the band): completely empty flat solid darkness #282220, no texture, no ceiling, no stalactites, no hanging cones, no spikes, no objects, and nothing touches the top edge. DEPTH LAYERS, from far to near: FAR LAYER: the farthest depth, simple dark silhouettes of distant rock masses and two or three dark side tunnel openings; no continuous floor line, the far depth fades into darkness; these are the darkest and simplest shapes, their edges partly hidden behind the midground, and they never form isolated cutouts against the top band. MIDGROUND: the main readable layer across the middle of the band — several enormous rock masses and broad ledges at different heights that overlap and partly hide each other, irregularly spaced, not a row of evenly spaced pillars; a few tall natural rock pillars rise from behind the ledges; the central part of the frame opens into a deeper recess with one large dark tunnel mouth far behind it; shallow alcoves and recesses, cracks and fissures; this layer carries most of the detail, low in contrast; the rock reads as huge natural cave masses, not a built wall, with only a few faint, short, broken strata hints that fade out, never continuous wavy stripes. NEAR FRAME: at the left and right edges, two dark rock masses frame the view, slightly darker than the midground; no isolated cylinder, column or prop pasted against the top; no hanging shapes. CENTER: the central half of the band stays calm and low in detail so the characters and UI can read. The overlapping ledges, pillars and far silhouettes must create several depth planes, not one flat backdrop. BOTTOM BAND (the bottom third of the band): simple flat dark rock in #282220 fading into shadow, no floor, no ground plane, no continuous horizontal ground line, no stone platform, no pedestal, no baseboard, no props — this area will be covered by a separate ground layer and UI. WHOLE IMAGE: very dark and low in contrast, no bright highlights, no rim lights, no vignette, no complete solid shape that competes for attention; no dense horizontal wavy lines, no repeated banding, no zebra stripes, no wood-grain pattern.
```
**三关变体段(接在共用底座后)**:
dungeon:
```
Use warm limestone for the far and mid rock: dark warm brown-grey, from the darkest #282220 through #3E362C to the lighter #453723; broad calm rock faces and a few wide low ledges; only a couple of faint, broken strata hints, not bands across the wall. GOBLIN LAIR DETAILS, fully built into the rock, only in the midground and far layer, small and low contrast, partly hidden: one broken crude wooden walkway anchored into a distant ledge with its posts set into the rock, its middle hidden behind a rock outcrop; one short pointed stake fence embedded along the edge of a far ledge, partly behind a rock mass; a dark side tunnel mouth with a rough timber frame built into the rock; no isolated fence segments, no floating walkway pieces, no structures sitting on top of ledges like icons; never in the near frame, never at the top, never lit.
```
underdark:
```
Use cool desaturated blue-grey rock for the far and mid rock: the rock faces are dark cool blue-grey, not warm brown; the cracks are sharper and more vertical; in the far layer add simple dark silhouettes of a few giant natural rock pillars whose tops fade into the dark top band, slender stone spires rising from distant ledges, thin muted grey web strands between rocks, a few curved spiral tower silhouettes, huge mushroom silhouettes on far ledges, and a distant chasm crossed by a thin stone bridge; add only a few thin, muted blue-violet mineral seams around 256 degrees hue, never saturated purple or magenta; no glowing fungi, no faerie fire, no bricks, no regular masonry.
```
deep_underdark:
```
Use very dark neutral grey-black rock for the far and mid rock: the rock faces are grey-black and angular, with broken, step-like ledges; in the far layer add simple dark silhouettes of a city carved into a giant stalagmite, with angular stone halls, buttresses and stepped terraces, a few stone bridges spanning a rift, several mine entrances with dark timber frames, and a row of chimney and vent stacks; a few thin cool silver and copper-grey mineral veins run through the rock as matte metal, not glowing; no forge fire, no glow, no smoke plume, no bricks, no regular masonry.
```
**共用否定后缀(三张共用)**:
```
Do not add any of the following: realistic, photorealistic or 3D rendering; painterly brushwork, smooth anti-aliased lines, gradient or dithered shading, glossy highlights; black or heavy dark outlines; blur or compression artifacts; a vignette, dark corners, or any mood darkening; any saturated purple or magenta anywhere, any color with a hue between 300 and 345 degrees (the underdark's muted 256-degree blue-violet seams are allowed and must stay desaturated); brick patterns, rectangular cut stone blocks, regular masonry joints, mortar lines, tiled surfaces, paved surfaces, a near-view carved or dressed stone wall, a flat wall close behind the characters, a single isolated column or cylinder pasted against the top band, any element touching the top edge, isolated fence segments, floating walkway pieces, structures sitting on top of ledges like icons, a continuous horizontal floor line; dense horizontal wavy lines, repetitive wavy banding, zebra stripes, wood grain, layer-cake stripes, continuous parallel strata across the whole wall; stalactites, stalagmites used as hanging shapes, hanging cones, ceiling spikes, cave curtains; any ground, floor plane, stone platform, pedestal, stage floor, baseboard, path or paving in the lower band; any character, person, creature or animal; any light source of any kind, any torch, brazier, lantern, candle, crystal, glowing object, light shaft, god ray, beam of light, glow, bloom or bright patch; any props in the near view, bones, chains, rubble pile, furniture, murals, paintings, grass, moss, plants, trees, small foreground fungi or glowing fungi (the underdark's far-layer giant mushroom silhouettes are allowed and must stay dark and unlit); bright or saturated colors, any area lighter than the variant's lightest wall color; high contrast edges, rim lights, black outlines; text, letters, numbers, watermark, signature, logo; frame, border, panel, arrow or UI element.
```
**组合方式**:每张背景 = 共用底座 + 对应关卡变体段 + 共用否定后缀,拼起来即整段可贴。三张仅岩体形态与色族不同,构图规则一致。
**后处理**:三张均不去底;最近邻降采样到终稿尺寸,色板量化到 art-bible §4.1 战斗屏场景环境子板(三张共用该子板,新增冷/灰色按 §4.1 两道门入板),单张 ≤32 色;四道机检见 §4。
### 2.2 连续地面层(`scene_stage_ground`,三关共用,顶缘以上透明)
**画什么**:全宽近景地面——**干净压实泥土为主**,整片平整、中低明度;全幅只保留**零星 3–5 颗小石**,散点分布、不成排不成堆;顶缘为不规则泥土边+阴影缝,可带 2–3 颗小石;中央平缓留空;地面跨三关恒定。
**不画什么**:瓷砖、砖缝、规则铺装、石板块、踢脚线/直亮边;草、木地板;远景/洞壁/天空;角色、生物、道具、火把/灯;中央大石或碎石堆挡站位;满铺碎石、密集砂砾、成排/成堆碎石、亮色浅棕大片、泥点噪声、垃圾杂物。
**出图画布**:终稿 = 基准屏宽 × `ground_h`(`battle_layout.tres` 新增字段,地面层实装批回填);本件是极宽薄条,平台无对应宽高比时按横向可平铺纹理条出图(左右无缝,宽度取平台方档,高度取 `ground_h` 的整数倍),终稿裁/平铺到目标宽高比,禁横向拉伸。试片阶段可按构图比例先出,终稿尺寸读表。
**正向提示词**:
```
Pixel art style. True pixel art game layer drawn with chunky square pixels, strict pixel grid alignment, hard edges, no anti-aliasing, no gradients, flat colors only, a limited palette of about eight colors, retro 16-bit look, Aseprite style, all ages, bright cheerful fantasy cavern, not grim, flat side view, orthographic, no perspective distortion. A full-width horizontal texture strip of the NEAR GROUND of a bright underground cavern arena, seen from the front, drawn on a plain flat uniform light grey #F2F2F2 background for easy keying: the ground strip occupies the lower part of the canvas and extends down to the bottom edge, and everything above its top edge is clean empty #F2F2F2 with nothing drawn in it. The ground is mostly CLEAN packed bare dirt with large calm flat areas of nearly unbroken color, medium-dark warm brown, not light tan and not bright; across the whole strip there are only THREE OR FOUR small stones in total, scattered singly and far apart, plus two or three small stones along the top edge; no rows of pebbles, no clusters, no gravel patches, no piles; the middle half of the strip is smooth and clear so characters can stand there; the surface is only slightly uneven with two or three thin cracks; no dense speckles, no heavy noise, no scattered pebbles spread everywhere, no muddy patches, no dirt clumps, no debris. The TOP EDGE of the ground is an irregular edge of packed dirt with a thin dark shadow seam, with only two or three small stones, clearly marking the boundary between the ground and the far wall behind it; the top edge must NOT be a straight tile edge, NOT a clean bright rim, NOT a masonry joint, NOT a regular curb, NOT a continuous row of stones or equal-sized pebbles. Colors: packed earth body in warm tan #968264 with large darker areas of #7A6448 and #5E4C38, darker soil patches #453723 and #3E362C, only the few small stones in #C9AF83, dark cracks and shadow seam in #4C4038 and #282220; the overall ground is medium-dark, not light tan and not bright, no large light-colored areas; matte flat colors, no gloss. No regular paving, no tiles, no bricks, no stone slabs, no grass, no plants.
```
**负向提示词**:
```
Do not add any of the following: realistic, photorealistic or 3D rendering; painterly brushwork, smooth anti-aliased lines, gradient or dithered shading, glossy highlights; black or heavy dark outlines; blur or compression artifacts; a vignette, dark corners, or any mood darkening; any purple or magenta anywhere, any color with a hue between 300 and 345 degrees; tiles, tile joints, paving, rectangular stone slabs, bricks, masonry, mortar lines, a clean straight rim, a bright curb or a regular edge along the top of the ground; grass, moss, plants, trees, fungi; any character, person, creature or animal; any prop, tool, bone, chain, furniture, torch, brazier, lantern, crystal or glowing object; any far wall, cave wall, background scenery or sky above the ground, the area above the top edge must stay plain flat uniform light grey #F2F2F2; any large boulder or rubble pile in the middle of the ground, the middle must stay clear for standing; many stones, rows of pebbles, clusters of rubble, gravel patches, stone piles, a continuous row of stones along the top edge, light tan or bright ground, large light-colored areas, dense speckles, heavy noise, scattered pebbles spread everywhere, muddy patches, dirt clumps, debris, garbage, a messy or dirty ground surface; any complete solid shape that competes for attention; bright or saturated colors, anything lighter than #C9AF83 inside the ground itself (the flat #F2F2F2 keying area above the top edge is required and allowed); high contrast edges, rim lights, black outlines; text, letters, numbers, watermark, signature, logo; frame, border, panel, arrow or UI element.
```
**后处理**:去底(`#F2F2F2` 键控,顶缘以上转透明)+ 最近邻降采样到终稿尺寸 + 色板量化;地面主体色落 art-bible §4.1 世界中层色族,单张 ≤32 色;四道机检见 §4。
---
## 3. 纵深结构与三关母题(2026-10-31 作者认可)
### 3.1 纵深结构(三关共用)
| 层次 | 放什么 | 规则 |
|---|---|---|
| 远层 | 远处洞口/通道、层叠岩体、洞底轮廓、远景建筑剪影 | 压到最深,细节最少;边缘被中层遮住,不孤立、不贴顶 |
| 中层 | 岩台、岩柱、凹壁、裂缝、主要建筑剪影 | 细节主要放这里;低对比 |
| 近层 | 左右暗色岩体作画框 | 不挡中央;禁单独圆柱/贴图贴顶 |
| 顶部/底部 | 顶部空区、底部暗部 | 顶部不挂钟乳石/吊锥;底部留给地面层 |
| 中央 | 角色站位区 | 1/2–2/3 宽保持低细节、无大障碍 |
### 3.2 三关母题(推荐项启用,可选项关闭)
| 关卡 | 推荐项(本版启用) | 可选项(本版关闭) |
|---|---|---|
| dungeon 地精 | 远处支洞/通道口、木质栈道/脚手架、削尖木桩/围栏残段 | 骨堆/头骨图腾、蘑菇丛/苔藓、暗河/细水流、丢弃箱桶/破布 |
| underdark 卓尔 | 巨型天然石柱、细长石笋群/尖塔、蛛网/蛛丝、弧形塔楼/螺旋尖塔剪影、巨型蘑菇剪影、裂谷+石桥、紫罗兰灰矿脉 | 雕刻拱门/门廊、蛛形雕刻 |
| deep_underdark 灰矮人 | 挖空石笋的城郭剪影、棱角石厅/扶壁/台阶、跨裂谷石桥、矿道口/木框矿井、烟囱/通风塔群、铜灰/银灰矿脉 | 铁箍/铁门/锁链、暗湖边缘、矮人面雕像 |
### 3.3 通用排除
钟乳石/吊锥/悬挂物、砖石墙/规则砌缝/瓷砖/铺装、贴墙感、单件圆柱贴顶、光柱/光带/光源/发光物、火把/灯、角色/生物、文字/UI、紫/洋红。
**近景地面**:独立连续地面层,画法与验收见 §2.2,不并入背景。
---
## 4. 验收(机检 + 目检)
**三张背景变体 `scene_stage_bg_<board_type>`**
- 每张画布 = (基准屏宽, `stage_h`) 逐位相符(终稿);文件名带 `_dungeon` / `_underdark` / `_deep_underdark` 后缀。
- 顶部空区无钟乳石/吊锥/悬挂物,且无任何元素触碰画面上缘;远景为天然岩体,无砖石/规则砌缝/瓷砖/方整石块。
- 纵深分层:远/中/近三层可辨;远层剪影被中层部分遮挡、不孤立、不贴顶;无贴墙感、无单件圆柱/贴图直接贴顶;无连续地平线/地面;无孤立栅栏/栈道图标。
- 背景不含近景地面、地台、平台;底带只留暗部(结算期被明细带压住)。
- 无光柱/光带/光束/光晕,无光源实体(火把/灯/发光物);明暗只由岩体层理与暗部承载。
- 明度:全图取样点 L ∈ [0.014,0.060](世界底层下沿带,art-bible §2)。
- 无 300°–345° 色相;无角色/生物/道具/文字/UI;无完整凸形焦点(立绘 > 背景)。
- 背景不引用 `wall_ambient` tint;三张按 `board_type` 选图。
- 目检:三关色族与母题——dungeon 暖石棕+支洞/木栈道/木桩围栏;underdark 冷蓝灰+巨柱/石笋/蛛网/螺旋塔/巨菇/石桥;deep_underdark 灰黑+石笋城郭/棱角石厅/石桥/矿道口/烟囱塔;可选项不出现;三张可辨为不同洞窟,不是同一张图换色。
**共用连续地面层 `scene_stage_ground`**
- 画布 = (基准屏宽, `ground_h`)(终稿;`ground_h` 读布局表);文件名无 board_type 后缀。
- 地面为干净压实泥土为主,全幅只零星几颗小石、散点分布;无瓷砖/砖缝/规则铺装/石板块/草。
- 顶缘 = 不规则泥土边/阴影缝,只带 2–3 颗小石;不是直线亮边/踢脚线,也不是一排石子;顶缘以上键控后透明、无残留底色。
- 中央 1/2–2/3 宽平缓、无大石阻挡;无成排/成堆碎石、满铺砂砾、泥点噪声、垃圾杂物。
- 地面主体色 L ∈ [0.22,0.45] 的中低段(世界中层,art-bible §2;裂缝/阴影不按明度带判定;避免浅棕/亮面)。
- 三关共用、不吃 `wall_ambient`;无角色/生物/道具/文字/UI。
- 目检:地面读作泥土/碎石,不是瓷砖或石板铺装。
**参考与入包**:参考候选可带水印;入包终稿须无水印;四件终稿均按 §0 后处理与 `art-pipeline-tools` 的判据执行。
---
## 5. 待裁与待办(不阻塞提示词成文)
1. **三张背景按 `board_type` 选图**——游戏代码改动,走需求卡片+迭代;背景 `wall_ambient` tint 退役,棋盘/世界的该槽保留。
2. **`ground_h` 字段**——布局表新增字段,地面层实装批回填;本卡试片按构图比例,终稿读表。
3. **冷/灰色入板**——underdark / deep_underdark 需要的冷蓝灰、灰黑与矿脉色按 art-bible §4.1 战斗屏场景环境子板两道门入板;三张背景共用该子板 ≤32 色。
4. **地面层出图工艺**——极宽薄条;默认横向可平铺纹理条(左右无缝);若作者要整幅不拼接,另议。
5. **小石台 `scene_stage_plaza.png`**——先保留;是否退役归实装批/后续裁决。
6. **四柱光柱程序层退役**——光柱不迁入背景;背景资产入包后该程序层退役(代码批)。
7. **风格前缀**——首张 dungeon 背景试片后由作者确认,确认后才出另外两张与地面层。
8. **可选要素池**——按 §3.2 可选项本版关闭;要加走本卡修订。
---
## 6. 不在本卡
角色与敌种立绘、棋盘格面/墙面纹理、UI 图标、护甲图标、小石台 `scene_stage_plaza.png`、整屏样张与游戏代码接线;这些各归 art-prompts、脚本管线与实装批。
+50 -26
View File
@@ -1,9 +1,9 @@
# 营地场景 · 分层生图提示词卡 v1.0
# 营地场景 · 分层生图提示词卡 v1.2
> **性质**:营地场景图(1280×800,一张可点击场景图 + 八个物件层)的出图执行侧文件。**场景级资产,与角色级 [art-prompts](art-prompts.md) 并列,两文件互不覆盖**。
> **性质**:营地场景图(1280×800,一张可点击场景图 + 十张分层:背景层 1 + 围墙层 1 + 八个物件层)的出图执行侧文件。**场景级资产,与角色级 [art-prompts](art-prompts.md) 并列,两文件互不覆盖**。
> **依据**:色彩与氛围 = `reports/提案-营地色彩与氛围脚本-2026-10-23.md`(本卡不复述色值);坐标与层序 = `artwork/2026-10-23-营地场景/布局草案-1280x800.html`;物件命名与消费链 = [asset-path-map](asset-path-map.md);画法与交付形态 = [art-bible](art-bible.md) §6.2。冲突时色彩以色彩脚本为准,画法以 art-bible 为准。
> **边界**:只管营地场景图的分层出图。角色、图标、格面走 art-prompts.md 与脚本管线,不需要提示词。
> **建立**:2026-10-25 · 状态:九条提示词成文,出图未开始(出图通道由作者自定)
> **建立**:2026-10-25 · 状态:十条提示词成文(背景层 1 + 围墙层 1 + 八个物件层),出图通道由作者自定
---
@@ -11,10 +11,10 @@
1. **出图**:按下面各条把提示词贴进生图渠道。出图前先放**风格参考图**(见 §1)。
2. **入库**:候选原图放 `artwork/candidates/<资产名>/`,文件名 `v1.png / v2.png / v3.png`。
3. **后处理**:脚本管线代跑——去底转透明、降采样到终稿尺寸(整数倍)、色板量化、四联底测试条。
3. **后处理**:脚本管线代跑——去底转透明、降采样到终稿尺寸(整数倍)、色板量化、四联底测试条。**背景层不去底(全幅不透明)**;围墙层与八个物件层去底转透明。后处理工具清单与用法见 [art-pipeline-tools](art-pipeline-tools.md)。
4. **转正**:终稿转入切片资产目录,按坐标表接线。
**出图顺序(先锁后产,不能跳)**:概念脚本 → **本卡提示词** → 贴风格参考图出背景层 → 作者确认风格前缀 → 才扩产八件。
**出图顺序(先锁后产,不能跳)**:概念脚本 → **本卡提示词** → 贴风格参考图**先出背景层 `scene_camp_bg` 1 张** → 作者确认风格前缀 → 才扩产围墙层 `scene_camp_wall` 与八个物件层(共 9 张)。
---
@@ -37,24 +37,40 @@ Style reference: the attached images are this game's own finished pixel art and
---
## 2. 背景层(`scene_camp_bg`,不可点,不透明全幅)
## 2. 背景层与围墙层(两张,不可点)
围墙从背景层拆出、独立成层:围墙层可单独锁死桩高、门洞做成透明缺口透出背景层的小路;背景层不混入暖木色、更易守住冷色占比判据(判据见 §4)。两层都不可点;按乙工序门**先只出背景层 1 张**供作者确认风格前缀,确认后才扩产围墙层与八个物件层。
### 2.1 背景层(`scene_camp_bg`,不可点,不透明全幅)
**出图画布**:2560×1600(终稿 1280×800 的 2 倍,保证整数倍降采样)
**画什么**:天空、远山、地面、木围墙、门柱与门洞
**不画什么**:七个物件、门扇、角色、接地阴影——那些是上面的独立图层
**画什么**:天空、远山、整片地面、门洞位置一条通向远处的小路
**不画什么**:木围墙、门柱、门扇、七个物件、角色、接地阴影——围墙与门柱是 §2.2 的独立层,门洞是它的透明缺口;小路在本层画好、透出来即可
```
Pixel art style. True pixel art game background layer drawn with chunky square pixels, strict pixel grid alignment, hard edges, no anti-aliasing, no gradients, flat colors only, a limited palette of about 24 colors, retro 16-bit look, Aseprite style, all ages, flat side view, orthographic, no perspective distortion. Wide horizontal composition, 16:10 aspect ratio. This is the BACKGROUND LAYER ONLY of a small adventurer camp: sky, distant hills, ground and the wooden palisade wall around it. It must contain no camp objects, no gate leaves, no characters and no drop shadows, because all of those are drawn as separate transparent layers on top of this one. COLOR SCRIPT — cool recedes, warm advances: the sky, the hills, the ground and the wall are all COOL, pale, desaturated and low in contrast; there is no warm accent anywhere in this layer, warm color belongs only to the separate object layers drawn on top. ATMOSPHERIC DEPTH: every plane that is farther away is lighter, closer to the value of the sky, cooler, lower in contrast and simpler in detail, while the nearest plane is the darkest and highest in contrast. SKY (top of frame): the lightest thing in the picture, flat pale cool off-white with a faint blue-grey tint, no yellow or cream tone, no sun anywhere; the very top strip of the frame, about one sixteenth of the frame height, stays completely calm and empty because a HUD bar is drawn over it; at most one or two extremely simple flat low-contrast clouds. FAR DISTANCE: two or three layers of low rolling hill silhouettes in pale cool blue-grey, each farther layer lighter, cooler and lower in contrast than the one in front, fading towards the value of the sky, no trees, no individual foliage; the foot of the hills is cut off and hidden behind the wall, so there is no visible horizon line and no distant ground band between the hills and the wall. MIDDLE GROUND: the base line where the logs meet the ground sits at two thirds of the frame height measured from the bottom edge; at that height runs a wooden palisade wall across the whole width of the frame, built from closely spaced vertical logs standing side by side with no gaps between them, every log cut to a sharp point at the top, a solid barrier that completely blocks the view of the outside — no horizontal rails, no see-through gaps, no ranch-style post-and-rail fence; the wall is painted in a muted, greyed wood brown. In the middle of the wall one wide gap holds two thick gate posts slightly taller than the wall, and between them an opening showing a dirt path running away into the distance; the gate opening itself stays empty, because the two gate leaves are a separate layer. FOREGROUND GROUND: the darkest area of the picture, dry packed bare earth in a neutral desaturated grey-brown, never golden, amber or warm yellow, hard flat color blocks, hard edges, no gradient, no grass, no grass tufts, no weeds and no plants of any kind; the ground meets the foot of the wall cleanly with no shadow band and no gradient. Do not add any of the following: realistic, photorealistic or 3D rendering; painterly brushwork, smooth anti-aliased lines, gradient or dithered shading, glossy highlights; black or heavy dark outlines; drop shadows, contact shadows or any shadow band; blur or compression artifacts; a vignette, dark corners, or any mood darkening; night, dusk, moon, stars, sunset or sunrise light; a sun or any light source drawn in the sky; a visible horizon line, a distant ground band or any distant grass strip below the hills; trees, forests or foliage in the distance; grass, grass tufts, weeds or plants in the foreground; a ranch-style post-and-rail fence, a see-through fence, flat-topped posts or horizontal rails; any warm, golden, amber or yellow cast on the sky, on the hills or on the ground; any camp object, furniture, crate, barrel, fire, tent or tool; any gate leaf, door panel or hinge; any purple or magenta anywhere; any text, letters, numbers, watermark, signature or logo; any frame, border, panel, arrow or UI element; any characters, people, creatures or animals.
Pixel art style. True pixel art game background layer drawn with chunky square pixels, strict pixel grid alignment, hard edges, no anti-aliasing, no gradients, flat colors only, a limited palette of about 24 colors, retro 16-bit look, Aseprite style, all ages, flat side view, orthographic, no perspective distortion. Wide horizontal composition, 16:10 aspect ratio. This is the FAR BACKGROUND LAYER ONLY of a small adventurer camp: sky, distant hills and the whole ground. It must contain no palisade wall, no gate posts, no camp objects, no gate leaves, no characters and no drop shadows, because all of those are drawn as separate transparent layers on top of this one. COLOR SCRIPT — cool recedes, warm advances: the sky, the hills and the ground are all COOL, pale, desaturated and low in contrast; there is no warm accent anywhere in this layer, warm color belongs only to the separate object layers drawn on top. ATMOSPHERIC DEPTH: every plane that is farther away is lighter, closer to the value of the sky, cooler, lower in contrast and simpler in detail, while the nearest plane is the darkest and highest in contrast. SKY (top of frame): the lightest thing in the picture, flat pale cool off-white with a faint blue-grey tint, no yellow or cream tone, no sun anywhere; the very top strip of the frame, about one sixteenth of the frame height, stays completely calm and empty, reserved as clear space with nothing in it; at most one or two extremely simple flat low-contrast clouds. FAR DISTANCE: two or three layers of low rolling hill silhouettes in pale cool blue-grey, each farther layer lighter, cooler and lower in contrast than the one in front, fading towards the value of the sky, no trees, no individual foliage. HILLS TO GROUND: the hills fade gradually and smoothly down into the ground, with NO visible horizon line and NO distant ground band — the transition must not read as a separate horizontal stripe of a different color. GATE PATH: in the middle of the frame, between x=1040 and x=1520, one clearly visible dirt path runs from the ground at two thirds of the frame height measured from the bottom edge away into the distance between the hills, narrowing as it recedes, painted in a muted desaturated grey-brown earth tone, never golden or warm yellow; this path shows through the empty gate opening of the wall layer above. FOREGROUND GROUND: the darkest area of the picture, dry packed bare earth in a neutral desaturated grey-brown, never golden, amber or warm yellow, hard flat color blocks, hard edges, no gradient, no grass, no grass tufts, no weeds and no plants of any kind. The foreground ground is completely empty apart from the dirt path: no objects, no icons, no badges, no markers, no decorations of any kind. Do not add any of the following: realistic, photorealistic or 3D rendering; painterly brushwork, smooth anti-aliased lines, gradient or dithered shading, glossy highlights; black or heavy dark outlines; drop shadows, contact shadows or any shadow band; blur or compression artifacts; a vignette, dark corners, or any mood darkening; night, dusk, moon, stars, sunset or sunrise light; a sun or any light source drawn in the sky; a visible horizon line, a separate horizontal distant ground band or any distant grass strip below the hills; trees, forests or foliage in the distance; grass, grass tufts, weeds or plants in the foreground; a palisade wall, wooden fence, fence posts, gate posts or any wall structure; any warm, golden, amber or yellow cast on the sky, on the hills or on the ground; any camp object, furniture, crate, barrel, fire, tent or tool; any gate leaf, door panel or hinge; any purple or magenta anywhere; any text, letters, numbers, watermark, signature or logo; any frame, border, panel, arrow or UI element; any icon, badge, item tile, emblem, sign, marker or symbol anywhere in the frame; any characters, people, creatures or animals.
```
### 出图画布上的坐标(终稿 ×2)
### 2.2 围墙层(`scene_camp_wall`,不可点,透明底)
**出图画布**:2560×1600(与背景层同画布,门洞 x 坐标直接对齐);墙横贯画在出图 y480–620 这条**窄带**内,其余为纯色底 `#F2F2F2`。落盘由 S4 后处理裁成终稿外接矩形 `(0, 240, 1280, 70)` / 出图 `(0, 480, 2560, 140)` 的细条,**不做全幅 1280×800 落盘**(全幅约 3.9 MB 显存,细条约 0.35 MB)。
**画什么**:横贯全宽的密排竖木桩墙(左右两段)+ 两根略高的门柱,门洞处完全透明
**不画什么**:天空、远山、地面、小路、门扇、七个物件、角色;门洞内不画任何东西,透出背景层的小路
```
Pixel art style. True pixel art game layer drawn with chunky square pixels, strict pixel grid alignment, hard edges, no anti-aliasing, no gradients, flat colors only, a limited palette of about 24 colors, retro 16-bit look, Aseprite style, all ages, flat side view, orthographic, no perspective distortion. Wide horizontal composition, 16:10 aspect ratio. This is the PALISADE WALL LAYER ONLY of a small adventurer camp, drawn on a plain flat uniform light grey #F2F2F2 background for easy keying. A wooden palisade wall runs straight across the full width of the frame, seen from the front, built from closely spaced vertical logs standing side by side with no gaps between them, every log cut to a sharp point at the top, a solid barrier with no horizontal rails, no see-through gaps, no ranch-style post-and-rail fence. The wall is SHORT and LOW: including the pointed tips the whole band occupies only about one eighteenth of the total frame height, a thin horizontal strip, NOT a tall barrier; the logs stand only about one eighteenth of the total frame height above their base line. In the middle of the wall one wide gap holds two thick gate posts slightly taller than the wall, and the entire opening between them is completely EMPTY — plain flat uniform light grey #F2F2F2 showing through, no logs, no path, no ground, no scenery inside the opening, because the dirt path and the hills belong to the background layer below and the gate leaves are a separate object layer. The wall is painted in a muted greyed wood brown, with a thin warm cream outline around the whole silhouette, flat shading. Do not add any of the following: realistic, photorealistic or 3D rendering; painterly brushwork, smooth anti-aliased lines, gradient or dithered shading, glossy highlights; black or heavy dark outlines; drop shadows or contact shadows; blur or compression artifacts; a vignette or dark corners; any sky, hills, ground, grass, dirt path or scenery of any kind; any gate leaf, door panel or hinge; any camp object, furniture, crate, barrel, fire, tent or tool; any characters, people, creatures or animals; any purple or magenta anywhere; any text, letters, numbers, watermark, signature or logo; any frame, border, panel, arrow or UI element.
```
### 2.3 出图画布上的坐标(终稿 ×2)
| 元素 | 终稿 1280×800 | 出图画布 2560×1600 |
|---|---|---|
| 顶栏 HUD 保留带 | y ≤ 48 | **y ≤ 96**(保持空旷) |
| 围墙带 | y 267–310 | **y 534–620** |
| 门洞(必须留空) | x 520–760 | **x 1040–1520** |
| 门柱 | x 500–520 / 760–780 | **x 1000–1040 / 1520–1560** |
| 背景层 `scene_camp_bg` | 全幅 1280×800 | 全幅 2560×1600 |
| 围墙带(wall 层桩身) | y 267–310(**高 43px**) | y 534–620(**高 86px**) |
| 门柱(wall 层,略高) | y 240–310;x 500–520 / 760–780 | y 480–620;x 1000–1040 / 1520–1560 |
| 门洞(wall 层透明缺口) | x 520–760 | x 1040–1520 |
| 门洞内小路(画在 bg 层) | x 520–760,自 y310 向上收窄至远山 | x 1040–1520,自 y620 向上收窄至远山 |
---
@@ -62,7 +78,7 @@ Pixel art style. True pixel art game background layer drawn with chunky square p
### 组装方式(不要逐条重复前缀)
每件提示词 = **风格前缀** + **本件正文** + **否定后缀**。前缀与后缀在**本节单点定义**,九张共用,避免同一段话九处维护。
每件提示词 = **风格前缀** + **本件正文** + **否定后缀**。前缀与后缀在**本节单点定义**,**八件共用**,避免同一段话八处维护。背景层与围墙层用各自的完整提示词(§2.1/§2.2),不用本节的物件前缀。
**风格前缀**(八件共用,照抄):
@@ -78,16 +94,16 @@ Do not add any of the following: realistic, photorealistic or 3D rendering; pain
### 逐件正文(只替换这一段)
| 件 | 层 | 终稿矩形 | 出图画布 | 正文 |
| 件 | z 序 | 终稿矩形 | 出图画布 | 正文 |
|---|---|---|---|---|
| 布告板 | L1 | 130×140 | 260×280 | §3.1 |
| 门扇 | L2 | 240×220 | 480×440 | §3.2 |
| 木桩 | L3 | 120×180 | 240×360 | §3.3 |
| 锻炉·铁砧 | L4 | 240×240 | 480×480 | §3.4 |
| 帐篷 | L5(不可点) | 300×170 | 600×340 | §3.5 |
| 宝箱 | L6 | 130×90 | 260×180 | §3.6 |
| 笔记本 | L7 | 100×80 | 200×160 | §3.7 |
| 篝火 | L8 | 160×160 | 320×320 | §3.8 |
| 布告板 | 2 | 130×140 | 260×280 | §3.1 |
| 门扇 | 3 | 240×220 | 480×440 | §3.2 |
| 木桩 | 4 | 120×180 | 240×360 | §3.3 |
| 锻炉·铁砧 | 4 | 240×240 | 480×480 | §3.4 |
| 帐篷 | 4(不可点) | 300×170 | 600×340 | §3.5 |
| 宝箱 | 5 | 130×90 | 260×180 | §3.6 |
| 笔记本 | 5 | 100×80 | 200×160 | §3.7 |
| 篝火 | 6 | 160×160 | 320×320 | §3.8 |
**3.1 布告板(→ 图鉴)**
```
@@ -133,10 +149,18 @@ A campfire made of a ring of grey stones around crossed stacked logs with a warm
## 4. 验收(机检 + 目检)
**背景层**
**背景层 `scene_camp_bg`**
- 黄度 =(R+G)/2−B 分区取样:天空 ≤20、远山 ≤20、地面 ≤35
- 冷色像素占比 ≥55%;本层不应出现任何暖色高饱和像素
- 目检:顶部约 1/16 完全空旷;无地平线;围墙密排尖桩、桩顶削尖、不透缝;门洞空着没有门扇
- 目检:顶部约 1/16 完全空旷;**无地平线、无远景地面带**(远山自然渐隐过渡到地面);不含围墙、门柱、门扇、物件、角色
- 目检/机检:门洞 x1040–1520 处**有一条清晰可辨、向远处收窄的土路**(非纯色块),透出「出去即冒险」
- 机检/目检:全幅**无任何独立物件/图标/徽章/标记**(前景地面除小路外全空);参考做法=中下方块区独立色数应≈邻区(旧口径 v1 该区仅 179 色)
**围墙层 `scene_camp_wall`**
- 透明底 `#F2F2F2` 可去底、奶油描边闭合
- **桩身高度 = 出图 y534–620(终稿 43px);门柱略高 y480–620。桩身超出围墙带下沿 y620 达 5px 以上即判红**
- 目检:密排竖木桩、桩顶削尖、不透缝、无横木;门洞 x1040–1520 **完全透明**(透出 bg 小路)、无门扇
- 单张色数 ≤32(量化后)、缩放为整数倍无拉伸痕迹
**八件**
- 单件居中、四周留边、整件完整不出画
@@ -150,7 +174,7 @@ A campfire made of a ring of grey stones around crossed stacked logs with a warm
## 5. 待裁(不阻塞出图,阻塞坐标表与定稿)
1. **帐篷尺寸**:现 300×170 待重定,重定后按比例重出
2. **门扇高宽**:现 240×220,围墙带只有 43px 高,比例待裁
2. **门扇高宽**:现 240×220,围墙带只有 43px 高;围墙独立成层后门扇叠在门洞上,两者高宽关系待裁
3. **布告板位置**:现 y330 悬空在围墙下方,建议 `(845, 268, 130, 140)` 挂回围墙带
4. **描边颜色追认**:本卡按奶油色(去底技术前提);若改参照物的深灰褐,需换去底方案
5. **营地环境子板色值**:art-bible §4.1 为数值单点,色值待作者拍板,本卡不复述
+1 -1
View File
@@ -2,7 +2,7 @@
> **渠道定位**:生图渠道 = **远程对话式生图为主,本地 ComfyUI 为备选**;本文件是**对话操作单**(怎么开对话/贴什么参考图/怎么续写),提示词正文词库归 [art-prompts](art-prompts.md)——分工:词库改词库、操作单改流程,同一段正文不双处维护。
> **冲突优先级**:画法与色彩以 [art-bible](art-bible.md) 为准,人物规格以 [character-design](character-design.md) 为准;本单为执行侧操作单,不另立规格。
> **出图登记义务(复现性)**:每张产出登记 **模型名 + 分辨率 + seed(可取则取)+ 对话/日期**,随候选原图同存 `artwork/candidates/<资产名>/`;缺登记的候选不入正稿遴选。
> **出图登记义务(出处备查)**:每张产出随图注明 **用途**(备选/参考/其他什么用)与 **生成时间**(YYYY-MM-DD HH:MM 格式),随候选原图同存 `artwork/candidates/<资产名>/`;模型名、seed、分辨率等参数有则随手记、无则不填,分辨率这类文件属性里自带的值不必抄。登记不作正稿遴选前置——缺注明不落选,遴选时问清用途即可。
> 开新对话出图:同一角色 2–3 版候选各开新对话(同对话续写=修图);同角色换姿势在同对话续写。分辨率开大档;候选原图存 `artwork/candidates/<资产名>/`。
## 1. 主角战斗立绘
+26 -16
View File
@@ -2,8 +2,8 @@
## 文档状态
- 版本:1.24
- 最近更新:2026-10-29(尾波清偿批设计闭合落文——「战斗编排域拆件定稿」节增补轴丙 BoardInstaller(`core/board_installer.gd`,实现批建件):装板域六函数迁出、宿主复位留薄壳、BoardGenerator 实例随件;battle_orchestrator 行 B1 域文件集句两新件→三新件随动;迭代 41 反哺注记转正文现状,沿革查 git)
- 版本:1.26
- 最近更新:2026-10-30(迭代 42 反哺点落文——ShopSession 行对外暴露格按委托壳拆除后实测直连面重写:完整直连方法清单+set_shop_pool 调用面改述;沿革查 git)
- 引擎:Godot 4.7.2-stable(GDScript 单语言;2026-09-07 锁定)
- 评审模式:solo(`review-mode.txt`,2026-09-12 用户拍板——TD/LP 门禁按 solo 跳过,用户授权架构产出自主推进、迭代期回调)
- 覆盖 GDD:现行全集(根索引=`design/gdd/02-系统清单.md`,文档树构成以根索引为准)
@@ -85,7 +85,7 @@
| 模块 | 独占职责 | 对外暴露 | 消费依赖 | 引擎 API |
|---|---|---|---|---|
| **ConfigDB**(autoload,D2) | 全部玩法数值表(`data/tables/*.tres` + `data/schemas/*.gd`);SCHEMA_VERSION;加载/校验/override/诊断状态机。**行族访问器归位各 schema 表类(表管自己的行查询),本类只留加载/校验/override/诊断门面;表持有成员按域收进三张域容器 Resource(`config_domain_combat/run/crossrun.gd`——已建件(迭代 41),类型化成员 7/7/5);三张 resolver 表(board_gen.resolve_row/enemy.resolve_rowset/difficulty.resolve_modifiers)经 `load_tables` 装配段注入各容器,resolver 文件与键零搬移**——红线不变:本类仍是唯一加载与持有单点,"经 ConfigDB 单点读取"=经其持有的表对象 | 类型化域容器属性(每域一张容器);公开面 8(push_diag/is_fatal/reset_diagnostics/diagnostics/diagnostics_count/load_tables/set_reload_gate/reload)、类变量 9 | 无(基础层顶点) | Resource/.tres 加载、autoload 生命周期——LOW |
| **ConfigDB**(autoload,D2) | 全部玩法数值表与表现层布局配置表(`data/tables/*.tres` + `data/schemas/*.gd`);SCHEMA_VERSION;加载/校验/override/诊断状态机。**行族访问器归位各 schema 表类(表管自己的行查询),本类只留加载/校验/override/诊断门面;表持有成员按域收进四张域容器 Resource(`config_domain_combat/run/crossrun/presentation.gd`——前三件迭代 41 已建件,第四件表现层布局域 2026-10-30 建件,类型化成员 7/7/5/1);三张 resolver 表(board_gen.resolve_row/enemy.resolve_rowset/difficulty.resolve_modifiers)经 `load_tables` 装配段注入各容器,resolver 文件与键零搬移**——红线不变:本类仍是唯一加载与持有单点,"经 ConfigDB 单点读取"=经其持有的表对象 | 类型化域容器属性(每域一张容器);公开面 8(push_diag/is_fatal/reset_diagnostics/diagnostics/diagnostics_count/load_tables/set_reload_gate/reload)、类变量 10 | 无(基础层顶点) | Resource/.tres 加载、autoload 生命周期——LOW |
| **SlotStore**(`core/slot_store.gd`,D3 存档槽位域件,已建件——迭代 41 存量债批) | 存档三槽位文件域唯一所有者(自 save_manager 收编槽位族):槽位挂载与模板、激活与建删、槽位摘要窥视(peek 族);改名覆盖原语(_rename_overwrite)留 save_manager(IO 原语单点) | 槽位读写原语族+摘要投影(公开 10/类变量 4;SaveManager 与菜单消费方经 `save.slots.` 直连) | save_manager(注入路径与模板配置);FileAccess——LOW |
| **MetaStateStore**(`core/meta_state_store.gd`,D3 存档内元进度数据件,已建件——迭代 41 存量债批) | 存档内元进度数据的读写唯一层(自 save_manager 收编 accessor 族:钥石/道具解锁/树节点与叉侧/锻造路线/同调/真视/计数器/摘要——数据读写原语,不含业务规则);save_manager 与 meta_progression 双 God Class 的共同病根=该数据域摊在两文件,本件即其归一 | 写口原语 13+只读投影 6(tree_state/forge_state/attune_state/career_state 四投影+keystone_balance/item_unlocked)=公开 19(add_keystone 私有 `_add_keystone`),零 _data 副本零缓存;MetaProgression 业务层与营地/锻造 UI 消费方经 `save.meta_state.` 直连(meta 的纯转发读方法已退役) | save_manager(序列化接线);FileAccess——LOW |
| **RngService**(autoload,D2) | master_seed、各流 scope 计数器(封闭流词表现值以 `RngServiceCore.PURPOSE_TAGS` 为准;增流走 P1 评审) | `set_master_seed()`(唯一调用方 C2);`open_stream(stream)` 有状态门面;`derive_at(stream, scope)` 纯函数;只读计数器 | ConfigDB(无) | 无——纯逻辑 |
@@ -106,20 +106,23 @@
| **reward_economy**(B4) | 格面资源身份映射(install 期预分配、Ready 只读、换图映射消亡)、极性路由字典面((类型, 极性)→ 字典行查询;路由执行归 B1 授予段)、F6 当量供给投影 | 封闭 API:roll_board / query_identity / query_full_snapshot / grant / grant_clear / retire_mapping / polarity_row / phase / weighted_pick;另有 is_mapped 静默布尔投影(批量工具用) | ConfigDB(RewardTable);BattleFxHost 经注入 Callable `luck_bias_query` 取偏运合计(reward_roll 发射与 bias_rewards 求和移至效果层完成);reward_roll 流(scope = 每次 install 批) | 无 |
| **helper_items**(A5,切片已实装秘宝域) | HelperItemTable、探测显影语义 | (契约规范源:`../../gdd/systems/跨局循环/秘宝.md`——桥接位:A2 查询接口 + A4 counter_item_ids) | A2 / A4 / B4 | 无 |
| **CurioDeck**(`core/curio_deck.gd`,A5 秘宝域令牌池件,迭代 37 自 AdventureRunner 拆出) | 三选一/商店共享池的持久多重集、抽签(候选抽取)、cap 库存与专长凭证三计数与判定唯一所有者——件只管池与判定,停点编排与兑付推进留冒险编排器 | `draw_candidates(channel)` 抽签;`consume_deck_token` / `consume_pool_purchase` 池消耗;`cap_ok` 上限判定;`feat_ticket_consider` 凭证判定;`stock_snapshot` / `pool_stock` 只读投影;商店购买消费经注入 Callable 链至 CurioStop 兑付单点(迭代 39 起转纯池——bind_handlers 反向回调已退役) | ConfigDB(秘宝令牌行集);注入 RngServiceCore(reward_pick 封闭流,禁自建随机) | 无(RefCounted 纯逻辑) |
| **ShopSession**(`core/shop_session.gd`,商店会话件,迭代 37 自 LevelRunner 拆出) | 关卡商店双模式(legacy 固定三件加定价/pool 共享令牌池)的货架、定价、购买与刷新唯一宿主——账本/钱包/装配集归宿主 LevelRunner(共享引用经 `bind` 注入,出账直接落同一引用改写宿主账本);legacy 模式零 RNG 零池消费(off 对照承载) | 货架与定价只读面(`shop_products` / `shop_shelf` / `current_price` / `shop_refresh_cost`)+写入口(`buy_product` / `buy_pool_item` / `shop_refresh` / `player_request_refresh` / `open_pool`);外部调用面由 LevelRunner 同签名委托承载(外部零变化) | ConfigDB(商店行集);注入 RngServiceCore;宿主钱包/账本引用;pool 模式令牌消耗与判定经注入 Callable 转 CurioDeck | 无 |
| **ShopSession**(`core/shop_session.gd`,商店会话件,迭代 37 自 LevelRunner 拆出) | 关卡商店双模式(legacy 固定三件加定价/pool 共享令牌池)的货架、定价、购买与刷新唯一宿主——账本/钱包/装配集归宿主 LevelRunner(共享引用经 `bind` 注入,出账直接落同一引用改写宿主账本);legacy 模式零 RNG 零池消费(off 对照承载) | 货架与定价只读面(`shop_products` / `shop_shelf` / `current_price` / `shop_refresh_cost`)+写入口(`buy_product` / `buy_pool_item` / `shop_refresh` / `player_request_refresh` / `open_pool`);外部调用面=调用方经宿主 LevelRunner 公开成员 `shop` 直连(调用直连不留委托壳,2026-10-26 Middle Man 裁定;迭代 42 拆除),直连消费的方法=`shop_products`/`current_price`/`purchased_tier`/`buy_product`/`player_request_refresh`/`set_shop_pool`/`pool_price`/`shop_shelf`/`shop_refresh_free_left`/`shop_refresh_cost`/`shop_refresh`/`spend_gp`/`buy_pool_item`/`is_shop_pool`/`shop_bp_shelf`/`shop_bp_visit_buys`(另白盒两入口仅测试侧:`_draw_shelf`/`_spend_discounted`);set_shop_pool 调用面=pool 注入经 LevelRunner 构造 ctx 供给(构造期内部直调,原委托块注释「AdventureRunner._start_current_level 唯一调用方」系陈旧表述、随壳删除作废),测试面直连 `shop.set_shop_pool` | ConfigDB(商店行集);注入 RngServiceCore;宿主钱包/账本引用;pool 模式令牌消耗与判定经注入 Callable 转 CurioDeck | 无 |
| **HeroLedger**(`core/hero_ledger.gd`,冒险域主角实体账,已实装(2026-10-27 迭代 39 拆件批)) | 冒险生命周期主角数值与获得记录唯一所有者:生命与 GP 钱包(gain/spend/clear 单写原语,钱包事件发射点归件)、锻造强化快照、调试百分比、攻击/甲类型轴快照、特性持有集(活引用注入——关内兑付当关生效)、本局钥石增量与逐杀明细、真视使用记录;获得记录三层结构=获得记录列表(三选一件/商店购买/战末奖励各一条)→属性增益明细(带来源指回)→四轴汇总(由明细推导);生效上限与回血封顶公式单点归件 | 构造注入;单写原语族+只读投影;断点自交片段(账本键+获得日记键) | ConfigDB(表值读取);由 AdventureRunner 冒险开始创建、注入 CurioStop/level_runner/ShopSession/战斗 encounter | 无(RefCounted 纯逻辑) |
| **AttuneLoadout**(`core/attune_loadout.gd`,同调配装件,已实装(2026-10-27 迭代 39 拆件批)) | 装备中的秘宝件对象集唯一所有者(每件内含充能单位与重置周期);同调充能的物化行为唯一事实源——种行/补行改写时同步(RunLedger 充能字典轨与差额对账函数退役);旧档 aux_items 同调行迁移归件 | 构造注入;充能查询/消耗/按周期重置原语;断点自交片段(attunement_charges 键,键名与策略不变) | ConfigDB(秘宝行集);TreasureItems(同调行投放面);注入战斗使用路径 | 无 |
| **CurioStop**(`core/curio_stop.gd`,三选一停点件,已实装(2026-10-27 迭代 39 拆件批)) | 三选一停点编排唯一所有者(架构原则 9 本域承载):停点三态(关卡完成三选一在途/已兑付待推进/奖励节点三选一在途)、刷新三源账(刷新件 X/营地池 Y/刷新券存量/付费刷新计数)、兑付方法族(pick/refresh/redeem/skip)、凭证出箱(RAP/时沙待注入);三选一信号面自带;零自动推进停点语义承载 | 对外=开停点/刷新/兑付/放行谓词/交断点片段五组入口+信号面(curio_choice_presented/curio_resolved);由 AdventureRunner 关卡完成受理时调用 | ConfigDB;注入 CurioDeck(抽签/摘牌)与 HeroLedger(落账);随机只经 CurioDeck 的 reward_pick 封闭流 | 无 |
| **breakpoint_store**(`core/breakpoint_store.gd`,冒险域·断点组装与回放件,已建件——迭代 45) | 存档断点组装与回放唯一承载(自 adventure_runner 收编 6 成员+函数族):断点快照拼盘、回放回填、读侧策略分发;`BREAKPOINT_FIELDS` 27 键登记单源随件(键名/策略标签/键序冻结) | 构造=引用束注入(九引用束成员解包为原名成员——保迁移函数体逐字);`resume_adventure` 留宿主(壳+宿主侧回填语句——R2 所有权缝分家);断点键面零变化;实测类变量 15(贴顶禁再增) | adventure_runner(宿主);注入主角账三件与 curio/stop/bank/loadout 引用 | 无(RefCounted 纯逻辑) |
### 功能层
| 模块 | 独占职责 | 对外暴露 | 消费依赖 | 引擎 API |
|---|---|---|---|---|
| **battle_orchestrator**(B1) | 段序骨架(七段结构冻结)、轮 facts 缓冲、可回放步骤流、[明细行协议](../../gdd/systems/战斗循环/战斗结算.md#明细流水每一行都能指回你刚才的哪一步棋)(基础行型+扩展行两层 + 字段分层,词表以 owner「明细流水」节为准)、条款③⑤闭合对账、**战斗域全部跨系统调用权**、效果引擎宿主面与树规则挂点施加面(**已拆出 BattleFxHost 并实装(2026-10-28 迭代 40——battle_fx_host.gd/reward_grant.gd 两件在库,效果引擎宿主面归 BattleFxHost 行承载,拆件细则见「战斗编排域拆件定稿」节)**) | `begin_battle(encounter)`;窗口/刷新意图提交面(扮演 D1);`level_clear()`;步骤流(输出契约) | B2/B3/B4 全调用面(**唯一调用方=B1 域文件集:battle_orchestrator.gd + settlement/ 五段段类 + damage_controller + 拆件后三新件**);A3 request_refresh 代理 + 回发;A2 install/retire + 三事件订阅;A1 generate(attempt 非 scope 事件);D2 SettlementTable + set_reload_gate | 无(零随机、禁 await——切片 88 冒烟实证可回放) |
| **battle_orchestrator**(B1) | 段序骨架(七段结构冻结)、轮 facts 缓冲、可回放步骤流、[明细行协议](../../gdd/systems/战斗循环/战斗结算.md#明细流水每一行都能指回你刚才的哪一步棋)(基础行型+扩展行两层 + 字段分层,词表以 owner「明细流水」节为准)、条款③⑤闭合对账、**战斗域全部跨系统调用权**、效果引擎宿主面与树规则挂点施加面(**已拆出 BattleFxHost 并实装(2026-10-28 迭代 40——battle_fx_host.gd/reward_grant.gd 两件在库,效果引擎宿主面归 BattleFxHost 行承载,拆件细则见「战斗编排域拆件定稿」节);2026-10-29 零豁免裁设计闭合:RoundCycle/BattleFacts 两新件随批(宿主 38→15、S13 豁免第三条注销——见下两行与定稿节轴丁)**) | `begin_battle(encounter)`;窗口/刷新意图提交面(扮演 D1);`level_clear()`;步骤流(输出契约) | B2/B3/B4 全调用面(**唯一调用方=B1 域文件集:battle_orchestrator.gd + settlement/ 五段段类 + damage_controller + 拆件后三新件**);A3 request_refresh 代理 + 回发;A2 install/retire + 三事件订阅;A1 generate(attempt 非 scope 事件);D2 SettlementTable + set_reload_gate | 无(零随机、禁 await——切片 88 冒烟实证可回放) |
| **BattleFxHost**(`core/battle_fx_host.gd`,B1 域·效果引擎宿主件,2026-10-26 编排域拆件设计定稿——实现批建件) | 效果引擎挂点施加面唯一承载(自 battle_orchestrator 收编 fx 族):事件发射与事实判定(_fx_emit/_fx_fact)、主角 AC 基线与本轮/本战 AC 累积、bonus_delta 四轴累积(战末宿主 drain 汇入账本)、揭示待兑队列与兑付(含 _fx_revealing 再入闸——同步信号回环防二次缓冲二次兑付)、树规则合成订阅的 round/battle 桶计数器持有(桶随编排器生灭语义不变) | 构造=引用束注入(防 _init max-parameters 新键);bind 时注入 emit Callable(行发射经宿主协议面单点)与兑付 Callable(RewardGrant 授予面——揭示队列 flush 回调);只读投影分两类消费方:AC 累积/bonus_delta 供宿主呈现快照与战末 drain,揭示态供宿主 A2 订阅旁路判定;`luck_bias_for(cell_count) -> int` 偏运合计查询口(体内三参形 dispatch——reward_roll 发射与 bias_rewards 求和自 RewardEconomy 收回本件,RewardEconomy 经注入 Callable 消费) | EffectEngine(EffectContext 引用束);RngServiceCore(fx_reveal 封闭流——除该流外零随机);A2 minesweeper(板读与 fx_reveal 调用);A3 action_economy(queue_ap_delta/charge_layers 读);B3 status_container 读;B2 unit_container 读(hp_ratio);DamageController(set_hero_ac_override);ConfigDB;encounter 引用 | 无(零 await——效果引擎全同步解释器) |
| **RewardGrant**(`core/reward_grant.gd`,B1 域·奖励授予件,2026-10-26 编排域拆件设计定稿——实现批建件) | 奖励授予唯一兑付管线(自 battle_orchestrator 收编 grant 族):极性字典路由与封印溢价兑付(_grant_routed)、三种行应用(flat/percent/gp)、伤害单据提交面(_submit 族)、清图奖励与身份授予、秘宝使用编排(use_treasure 四步)、战斗域 GP 缓冲 escrow 与授予段暂存伤害闸(_defer_board_damage/_deferred_board) | use_treasure 编排入口(treasure_interact_controller 经组合根现值回调直连);授予入口(结算段类直调);只读投影(escrow 读与 GP 掉落合计——宿主呈现快照与 _roll_gp_drop 消费) | B4 RewardEconomy(polarity_row/grant);B1 域 DamageController(开单);BattleFxHost 引用(treasure_used 事件发射与计数器消耗);A2 minesweeper(校验回调/fx_reveal/remove_mines_in);A3 action_economy(相位闸/grant_ap/queue_ap_delta);B3 status_container(apply_effect/池读);B2 unit_container(hero_unit/heal);ConfigDB;注入 emit Callable、宿主 fatal Callable(battle_result 状态机唯一写点留宿主)、宿主 attune 读投影(_attune_safe_seen)、encounter 账本引用;TreasureItems 经宿主 setter 转发注入(库存注入时序在宿主构造之后) | 无(零随机零 await——授予期零随机口径,roll 值授予前已定) |
| **damage_controller**(B1 域·伤害单据管线) | 伤害单据唯一兑现管线(单据九栏目声明→分量掷点/克制修正/伤害下限归一→明细行发射;双方同规) | 兑付入口(编排侧开单)+ fx 揭示回调注入位 | B2 单位实例(单据目标传入);B1 注入(EffectContext 引用束与编排回调) | 无 |
| **BoardInstaller**(`core/board_installer.gd`,B1 域·装板域件,2026-10-29 尾波清偿批设计闭合——实现批建件,细则见「战斗编排域拆件定稿」节轴丙) | 棋盘安装与变异布置域唯一承载(自 battle_orchestrator 收编装板族六函数):生成受理、全局作用域变异决议、资源摇号资格域组装(roll_board 恰一次)、region 变异决议与连通片布置;BoardGenerator 实例随件自建自接线(宿主成员退役)——宿主侧装板后状态复位(封印/完美计数清零、板序推进、board_changed 发射、秘宝叠加层退役)留宿主薄壳,时序逐字保持 | `install(during_round) -> bool` 单入口;构造=引用束注入(rng/db/minesweeper/reward_economy/encounter 只读引用+宿主 fatal Callable——防 _init max-parameters 新键) | A1 board_generator(件内持有);A2 install_board;B4 roll_board/board_skew 写;A4 变异决议行集经 ConfigDB;board_gen/variant_place 两封闭流随件、流名与开流时点零漂移 | 无(零 await——装板全同步) |
| **RoundCycle**(`core/round_cycle.gd`,B1 域·轮循环驱动件,已建件——迭代 44) | 轮循环驱动域唯一承载(自宿主收编 7 成员):结算五段段件持有与驱动、重入门闸、开轮摇号、踩雷账 | 三参直构(host/fx_host_ref/reward_grant_ref——五段构造透传不落成员);段驱动入口由宿主接线调用;实测类变量 8/公开方法 3 | B1 宿主;settlement/ 五段段类;A3 相位读 | 无 |
| **BattleFacts**(`core/battle_facts.gd`,B1 域·战斗事实件,已建件——迭代 44) | 战斗事实域唯一承载(自宿主收编 14 成员):受理事实(encounter)、轮 facts 缓冲(reveals/seals/path)、封印判定账、事实溯源、同调快照读面;attune 状态两成员随件(行为函数留宿主) | 零参直构;宿主与段类经注入引用读写;实测类变量 15(贴顶)/公开方法 4(reset_round+三 mark 写口) | B1 宿主;A2/A3 只读 | 无 |
| **settlement/ 五段段类**(B1 域·结算段件) | B1 每轮结算序列的五段逻辑件(授予/增益减益/战斗/衰减/判定——细则见下「战斗域装配现状」节) | 段处理入口(B1 接线调用);可直接 `new()` 单测 | B2/B3/B4 兑付面与 B1 轮 facts 缓冲(各段按职责) | 无 |
| **level_runner**(C1-lite,切片已建) | 关卡域生命周期(节点链推进、共享 B4 + HP 携带、关卡末 level_clear) | 节点推进接口;LevelTable(`level.tres`——节点 enum {battle,reward,rest,boss}、末节点必须 Boss、ConfigDB 跨表校验⑧) | B1(直构 BattleOrchestrator 接线战斗)、ConfigDB LevelTable | 无 |
| **run_structure**(C2) | 冒险/关卡序列、失败重开、**master_seed 唯一接线方** | (契约规范源:`../../gdd/systems/冒险循环/冒险结构.md`) | RngService.set_master_seed、C3/D2 | 无 |
@@ -134,13 +137,14 @@
- **轴甲两件**:battle_orchestrator(1313 行/50 成员,S13 全库单文件最大户)按变化原因拆出 BattleFxHost(效果引擎宿主域)与 RewardGrant(奖励授予域)两协作件;宿主保留架构在册职责全部——战斗状态机与生命周期、轮 facts 缓冲(_facts_reveals/seals/path)、A2 三事件订阅、结算五段驱动、attune 族原位、行发射基建(_emit_row/_assert_row_source/_fatal)、呈现投影(presentation/diagnostic_snapshot 唯一出口不变)、刷新面、nest_preview_count(巢穴预告读数口——外部直读面 main.gd 与 treasure_effects_test 随批点名冻结)。拆件动机与残留债口径见待办清单「gdstyle 存量债全量清偿」条目。
- **轴丙一件(2026-10-29 尾波清偿批设计闭合——实现批建件)**:battle_orchestrator 装板域六函数(`_install_new_board`/`_resolve_global_variant`/`_place_variants`/`_place_variants_apply`/`_variants_fringe`/`_adjacent_to_packs`,约 140 行;全仓仅宿主三调用点——begin_battle/中途刷新受理/player_request_refresh,实查零外部调用)按变化原因拆出 BoardInstaller,职责语句「棋盘安装与变异布置域」(生成→全局变异决议→装板受理→资源摇号→变异布置;单一变化原因=棋盘设置顺序与变异布置算法)。形态两修正(实测裁,与初案「逐字搬运」之差):①宿主复位块不随件——`_install_new_board` 体内五处计数清零、板序推进、board_changed 发射与秘宝叠加层退役留宿主薄壳 `_install_and_reset`,三调用点改调薄壳、时序逐字;②`_board_generator` 全文件仅装板族使用——实例随件自建自接线(fatal 经注入 Callable),宿主成员退役(类变量 38→37,禁无效成员义务)。encounter(23 处跨宿主)与 _reward_economy(18 处)确认留宿主、经引用束注入。文件长键(1035/1000)随本件迁出归绿;attune 族与 begin_battle 受理段序禁动不变。
- **轴丁两件(2026-10-29 零豁免裁设计闭合——实现批建件)**:RoundCycle(轮循环驱动件)与 BattleFacts(战斗事实件)见功能层表两行——宿主 38(轴丙落地后 37)→15,S13 豁免第三条注销;宿主保留面改登记为「战斗状态机与生命周期、行发射基建(_emit_row/_assert_row_source/_fatal)、呈现投影(两快照出口签名不变)、刷新面、nest_preview_count」;attune 族处置由下一条改判;treasure_inventory 注入改址追认——原「外部赋值路径零改址」句失效,level_runner 经 RewardGrant 出口直注;成员级对账以需求卡片为卡内契约(开工 T0 复核)。
- **跨件单向接线**:RewardGrant 持 BattleFxHost 引用(treasure_used 事件发射与计数器消耗);BattleFxHost 对授予面走 bind 时注入的兑付 Callable(揭示队列 flush 回调)——禁双向强引用(两件均 RefCounted,环引用随每战新建泄漏)。行发射经 bind 时注入的 emit Callable(协议面单点留宿主——CurioDeck bind_handlers 同款先例,禁件回调宿主私有族);轮 facts 缓冲留宿主、fx 件经注入取值;_seal_consumed 留宿主(与 A2 seal_judged 订阅同域);fatal 经注入 Callable 转宿主 _fatal(battle_result 状态机唯一写点不变)。注入 Callable 亦属接线面、同单向纪律——禁为省参数改持对象引用。授予件对账本零强引用:战斗期消费经 `battle_hooks` 三键回调束(attune_consume/vision_add/negative_blast,bind_battle 缺键 assert),宿主侧三单点记账(2026-10-28 迭代 40 合并适配实装——RunLedger 类已随迭代 39 退役,直持对象引用无处可指)。
- **战斗域读面(RunLedger 退役后)**:账本读经编排器 `_journal()`/`_attune_loadout()` 现值派生(GainJournal/AttuneLoadout,enc 派生、无成员接收);冒险桶经 `enc["adv_bucket"]` 下传 `battle_fx_host.mount_buckets`;战末 bonus 四轴经 `GainJournal.gain_bonus` 汇入(2026-10-27 迭代 39 主角账三件落地、迭代 40 合并适配承接)。
- **settlement/ 段类随动**:段类构造注入直持宿主+两新件三引用(Middle Man 禁令封转发壳——调用点改指),授予段读写面点名:_grant_routed×5/_submit×6/escrow 写/_fx_new_resource_types/_feat_first_dig_used/_fx_bonus_delta 写/_apply_ac_bonus/_fx_enqueue_reveal/_flush_pending_reveals/_grant_clear_reward/counters_ctx;战斗段 _defer_board_damage 复位与 _deferred_board 结账改指 RewardGrant。四个授予段 helper(_feat_yield_adjust/_harmful_variants/_harmful_adjacent/_hits_of)与 _pool_effective/perfect_sweep_earned 留宿主(段类经宿主引用直调,现状形态)。
- **attune 族原位保留**:同调配装 8 成员不进新件——迭代 39 副卡(AttuneLoadout 充能单源化)语义领地,两批零冲突;本波残留债归尾波。RewardGrant 经宿主 attune 读投影取 _attune_safe_seen。
- **attune 族处置(2026-10-29 零豁免裁改判,原「原位保留」失效)**:attune 状态两成员归 BattleFacts、行为函数留宿主——迭代 39 副卡(AttuneLoadout 充能单源化)语义领地不破,外部三面消费点零改址;RewardGrant 经宿主 attune 读投影取 _attune_safe_seen 形态不变。
- **呈现零变化**:两快照出口签名与载荷逐字不变,内部改读两件只读投影(快照本体不读揭示态——揭示态投影消费方=宿主 A2 订阅旁路判定);treasure_interact_controller 三触点分家——use_treasure 经组合根现值回调改注 RewardGrant(每战新建编排器,禁一次性直引防悬挂),minesweeper 读与 attune_convert_state 仍走宿主取值回调(双取值面);nest_preview_count 直读面零变化。
- **S13 键账收敛义务**:12 个超标键随函数迁入新文件=基线外 NEW(use_treasure 长度/分支/局部/返回×4、_grant_routed 参数 8、_dispatch_row 参数 6、_apply_percent_row 参数 6、_apply_gp_row 参数 6、_apply_flat_row 参数 7+返回 9、_submit_flat_hero 参数 7、_fx_fact 返回 7)——卡内同步收敛为本波义务(_grant_routed 等签名随引用束注入改形、use_treasure 体拆分),禁波 0 合并 refresh 吸收新文件未还键;新件 _init 引用束注入防 max-parameters 新键。
- **验收红线**:游戏行为零变化(RNG 消费序零漂移——fx_reveal 封闭流随件迁移取号序不变、信号顺序契约冻结、明细行协议词表零触碰、同种子逐位一致);battle_orchestrator 头注发射点指址随拆件改写(词表不动,指址须真);行数恶化债清偿义务(迭代 38 特批带红合并挂账 c4c69701——本波还债归绿);PARITY 触面(battle_orchestrator.gd 被改)实跑对拍须经用户批准、不得借机重锚;S13 改善非清零口径(宿主残留 8–10 键+随迁键卡内收敛后新件零残留为达标线)。
- **验收红线**:游戏行为零变化(RNG 消费序零漂移——fx_reveal 封闭流随件迁移取号序不变、信号顺序契约冻结、明细行协议词表零触碰、同种子逐位一致);battle_orchestrator 头注发射点指址随拆件改写(词表不动,指址须真);行数恶化债清偿义务(迭代 38 特批带红合并挂账 c4c69701——本波还债归绿);S13 改善非清零口径(宿主残留 8–10 键+随迁键卡内收敛后新件零残留为达标线)。
- **S7/S8 随动**:RewardGrant 调 B4 API,两新件一并登记进 S7 B1 域文件集与 S8 结算路径 await 文件集(static_scan.sh 两清单独立、同批更新——防 await 面漏闸);轴丙 BoardInstaller 同规(调 B4 roll_board,实现批随批增行)。
### 冒险域拆分(现状登记——2026-10-26 设计闭合,2026-10-27 迭代 39 拆件批实装,本节各条即现行形态)
@@ -149,7 +153,7 @@
- **CurioDeck 转纯池**:bind_handlers 反向回调退役;商店购买消费经注入 Callable 链至 CurioStop 兑付单点——牌库只管抽签/消耗/cap 判定/凭证计数。
- **RunLedger 退役**:四账各归其主——加算区/购买记录/钥石增量/特性集/真视记录归 HeroLedger,充能归 AttuneLoadout,冒险桶字典归应用层持有;战斗侧 encounter 注入面由单一账类引用改为多路(桶字典+实体账+配装件);ShopSession 账本引用面同步换代,商店属性商品加算改经 HeroLedger 单写原语。
- **level_runner**:依赖改构造注入一次给齐(后置 setter 群与商店重绑机制退役;注入活引用禁快照——特性集共享引用语义保留);商店白盒转发属性撤除;战斗→关卡信号转发族保留(战斗实例每节点新建,关卡层做战斗域稳定面)。
- **断点片段化**:各件自写自读自己的键(关卡层交关卡域五键片段:node_id/visited_ids/aux_items/shop_shelf/shop_visit_buys),缺键回退策略随领域知识归各件;应用层唯一落盘口(收齐各件片段拼成顶层快照后写盘),写触发点不变(五处);既有 27 键逐键冻结;新增获得日记键沿扩列不升版先例——旧档缺日记回退汇总键直载并跳过比对,日记在档时由日记推导汇总、与留存值不符即报错中止(迁移正确性自检)。
- **断点片段化**:各件自写自读自己的键(关卡层交关卡域五键片段:node_id/visited_ids/aux_items/shop_shelf/shop_visit_buys),缺键回退策略随领域知识归各件;应用层唯一落盘口(收齐各件片段拼成顶层快照后写盘),写触发点不变(五处);既有 27 键逐键冻结;新增获得日记键沿扩列不升版先例——旧档缺日记回退汇总键直载并跳过比对,日记在档时由日记推导汇总、与留存值不符即报错中止(迁移正确性自检)。2026-10-29 零豁免裁随动:拼盘与读侧策略机组拆出 breakpoint_store(核心层表新行),应用层落盘口与写触发点不变——宿主经件组装快照后写盘,27 键冻结不变。
- **信号直连**:adventure_runner 六转发信号族(node_entered/choices_presented/step_row/round_started/board_changed/wallet_changed)退役;呈现层在 level_started 重取点重订阅关卡对象信号(旧实例连接随引用释放消亡);「关卡开始信号前零发射」契约写进关卡件头注;「恰好连接一次」断言用 is_connected 守卫(release 模板剥 assert);三选一信号面归 CurioStop。
- **调用直连**:不留中间人委托壳(纯转传=Middle Man 反模式,2026-10-26 用户裁定);呈现控制器经取值回调持件——回调只许捕获长活根对象,禁件方法 Callable(防悬挂);约 30 个调用文件改址;测试/探针直连件读写。
- **验收红线**:游戏行为零变化(RNG 流名 reward_pick 与消费序零漂移、信号顺序契约冻结、同种子逐位一致);gdstyle 基线禁改(类变量计数下降属改善);怪物寻址 resolve_enemy_rowset 调用面与关卡参数档装配零变化(未来关卡主题的数据缝不堵)。
@@ -160,11 +164,15 @@
|---|---|---|---|---|
| **D1 HUD**(呈现面拆分持续换代:组合根 main.gd + 三屏壳(MenuScreen/CampScreen/AdventureScreen)+ views/ 视图单元(现行承载面见「呈现层装配与会话生命周期」节;早期 9 单元清单与 LevelStripView 已随场景换代退役);营地覆盖层=四柜台并排〔树淬炼主界面/锻造工坊·秘宝匣钥匙门/图鉴常开——迭代 21 T6 程序占位重排〕) | 全部 Control/CanvasLayer(例外:BoardView 棋盘本体与 BattleStageView 站桩本体 = Control 壳 + SubViewportContainer + SubViewport + Node2D 世界〔BoardWorld/CellUnit;StageWorld/StandeeBody——B 区 slot_row 伴随件留壳层〕,探针契约面不变——棋盘 Node2D 迁移批 Phase A 2026-09-23 / 战斗舞台 Phase B 2026-09-26)、输入→意图翻译(**唯一提交路径 = settle_batch,不直调 A3**)、呈现义务状态记录(A2×6 / B2×4 / B3×4 / B4×5 / B1×3) | 视图单元接口(信号外发、setter 只读注入) | A2 compute_move_path / ActionOutcome;A3 ap/rap;B1 步骤流行协议(数据源);ConfigDB display_name | **MEDIUM**:Control / CanvasLayer / draw_* / InputEvent / RichTextLabel——对照 breaking-changes.md 4.5–4.7 条目(CanvasItem 线条无抗锯齿羽化、设备 ID 常量、RichTextLabel 图片参数两轮变化) |
| **组合根拆件**(呈现侧写口子件:curio_interact_controller / camp_shop_presenter / treasure_interact_controller) | 代组合根执行核心写调用(冒险节点后续推进/营地购买与刷新/道具使用——依赖与回调由 main 构造注入) | 各拆件的写入口(仅组合根装配面可达;视图零直连) | 构造注入的核心写引用(推进/商店/道具各域——详见各拆件头注) | 无 |
| **ui_registry**(`presentation/ui_registry.gd`,界面注册面,已建件——迭代 44) | 呈现层接口面唯一承载:层序合同(LAYER_CONTRACT)迁驻与合同寻址、三域句柄容器 ui_screens(9 屏壳与覆盖层句柄)/ui_battle(5 战区句柄)/ui_modals(13 模态句柄+ModalFlow);组合根经其一级寻址接线,探针挂载假设迁移为 `main.ui.<域容器>.<句柄>`;七个探针别名/转发成员随批消灭 | 三容器属性+合同寻址口(RefCounted 门面,不挂树、零会话期引用);四件(UiRegistry/UiScreens/UiBattle/UiModals)类变量 3/9/5/14,ModalFlow 随 UiModals 容器构造即建 | 构造注入:三屏壳、覆盖层、战区句柄、ModalFlow(main_ui_build 交付随之改型) | 无(RefCounted) |
| **board_hover**(`views/board_hover.gd`,悬停预承诺呈现域件,已建件——迭代 45) | 悬停反馈域唯一承载(自 board_view 收编 11 函数族+_hover/_last_click 两成员):三语义光标、目标格边框、BFS 路径预览、chord 双击首遇锚;_r0 出生展开区成员留宿主 | 构造=引用束注入实为 7 项(cells/minesweeper/diggable 引用+格命中换算/光标写入/hover_forbidden/chord_attempted 四 Callable——卡片预估 5 项,11 函数逐字迁移的最小必需面);三入站口留宿主委托;hover_probe 读面留宿主 | board_view(宿主);输入事件经宿主入站口 | 无(RefCounted) |
| **cell_base_layer/cell_mark_overlay**(`views/`,单格两绘制域件,已建件——迭代 45) | 单格绘制域拆分(自 board_cell_unit 状态件 `_CellPaintState` 收编,入参与该层绘制命令同件):基座层(底面/描边/立面/投影/接触带/倒角,12 成员)与标识叠加层(标记/字形/徽章/封印/点迹/高亮/退色,13 成员);宿主 _draw 退为六步时序器,类变量 30→10 | 构造注入;绘制命令由宿主 _draw 按层序调用 | board_cell_unit(宿主);gdstyle 键各文件独立计数(12/13 均 ≤15) | 无(RefCounted) |
| **standee_vitals/standee_motion**(`views/`,站桩两状态件,已建件——迭代 45) | 站桩状态域拆分(自 `_StandeeState` 真实 25 成员收编):身份命值(hp/atk/状态值族,12 成员)与资产演出(动作/姿态/位移演出族,11 成员);三个演出探针计数器留宿主,宿主类变量 19→10 | 构造注入;st(沿用)/mo 两实例由宿主持有 | standee_body(宿主);battle_stage_view 写入面逐字保持 | 无(RefCounted) |
| **HudPresenter**(`presentation/hud_presenter.gd`,迭代 37 加注) | 战斗屏**数据投影+文案拼装静态族**——核心状态→呈现文案的全部翻译集中于此(B/C/D 区文案与投影),静态无状态 | `refresh(…)` 全量重查装配(组合根每次动作/结算返回后调用,漏调即陈旧呈现)+只读静态投影族 | 核心读取只走 `settlement.presentation_snapshot()` 成文快照面(禁链式穿透结算内部);ConfigDB display_name | 无(RefCounted 静态) |
| **board_resolvers**(`presentation/board_resolvers.gd`,迭代 37 拆出) | 棋盘每格渲染数据的只读解析回调四函数(奖励徽章身份——挖掘期 Active 相位内、地形变异数字遮蔽与角标、调试加成生效值)——有态绑定件,与无态静态投影族 HudPresenter 职责互斥 | 构造注入 db、调试浮窗与结算活态取值,产出绑定 Callable 供组合根接线 | ConfigDB / DebugOverlay / 结算活态取值 Callable | 无(纯投影) |
| **main_ui_build**(`presentation/main_ui_build.gd`,迭代 37 拆出) | `_build_ui` 八步拆分件——组合根 UI 装配分步承接(三屏壳实例化、视图句柄接线、覆盖层装配与模态编排注入按步拆开);main 侧留同名薄委托,装配序与 z 序零改动 | 分步静态函数(成员访问经 root 显式传入) | 层序逻辑不在此件——唯一事实源仍是 `main.LAYER_CONTRACT` + `_apply_layer_contract()` | 同 D1(Control 装配面) |
| **main_ui_build**(`presentation/main_ui_build.gd`,迭代 37 拆出) | `_build_ui` 八步拆分件——组合根 UI 装配分步承接(三屏壳实例化、视图句柄接线、覆盖层装配与模态编排注入按步拆开);main 侧留同名薄委托,装配序与 z 序零改动 | 分步静态函数(成员访问经 root 显式传入) | 层序逻辑不在此件——唯一事实源=`ui_registry.LAYER_CONTRACT`+`ui.apply_layer_contract(root)`(迭代 44 迁驻注册面) | 同 D1(Control 装配面) |
| **main_adventure_flow**(`presentation/main_adventure_flow.gd`,迭代 37 拆出) | main 冒险流程族——菜单开局/续玩后的冒险编排接线、节点事件分发、终局面与选路屏开合;逐字搬运,main 侧留同名薄委托(探针 `main.X` 路径零迁移) | 流程静态函数(root 显式) | 局态全局住 run_scope 节点——本件零成员态、零新增信号连接(连接语句随原段搬运) | 无 |
| **main_overlay_flow**(`presentation/main_overlay_flow.gd`,迭代 37 拆出) | main 浮层流程族——设置/状态面板/生涯页/选档界面/图例/钱包事件的开合与数据投喂;逐字搬运,main 侧留同名薄委托 | 流程静态函数(root 显式) | 全部开合走组合根模态单口(`root._open_modal/_close_modal`)——本件零成员态 | 同 D1 |
| **main_overlay_flow**(`presentation/main_overlay_flow.gd`,迭代 37 拆出) | main 浮层流程族——设置/状态面板/生涯页/选档界面/图例/钱包事件的开合与数据投喂;逐字搬运,main 侧留同名薄委托 | 流程静态函数(root 显式) | 全部开合走组合根模态编排口(`root.ui.modals.flow.open/close`)——本件零成员态 | 同 D1 |
| **main_debug_flow**(`presentation/main_debug_flow.gd`,迭代 37 拆出) | main 调试流程族——调试浮窗开合与上下文同步、批量模拟入口、两层调试对局起手、诊断转储;逐字搬运,main 侧留同名薄委托 | 流程静态函数(root 显式) | 调试会话状态机归 debug_session.gd——本件只接线与出 UI,零成员态 | 同 D1 |
| **TilePalette**(`presentation/views/tile_palette.gd`,迭代 37 拆出) | 瓦片排版/装载:高墙/地面瓦片整批装载与运行时镜像合成、地板明度归一、数字墨族按板自适应判据、格坐标确定性散列与十六正交情形墙瓦片查表 | 查表族(读件持棋盘引用——棋盘视图 rebuild 经 `bind_board` 同步);装载缓存七成员持于件内(同板型零重载、换板重载时序同源) | 零绘制面、零玩法数据写入(绘制调用面留棋盘视图);纹理路径单点在件内 | Resource 纹理加载——LOW |
| **HeroSprite**(`presentation/views/hero_sprite.gd`,迭代 37 拆出) | 英雄精灵四态与落位装配(落位与位移演出、撞墙帧闪、挖掘弹跳、脚底锚与持握手位出口) | 由棋盘视图 `_init` 建为子件(注入世界根与壳层容器引用)——精灵本体经世界根 ensure_hero 建于 SubViewport 世界内,本件只驱动其状态 | BoardWorld 世界根;素材缺位=零精灵(棋盘视图字形「人」回退路径在役) | Node2D / create_tween(Tween 系 Node 方法——MEDIUM 域对照 breaking-changes.md) |
@@ -182,7 +190,7 @@
### 三屏壳与组合根句柄面
- 呈现场景拆为三屏壳:MenuScreen(开始菜单屏)/CampScreen(营地屏)/AdventureScreen(冒险屏——内含关卡地图与战斗两容器)。
- 屏成员经组合根 main.gd 一级句柄暴露(探针挂载假设 `main.X` 路径零迁移;**S13 豁免登记 2026-10-27:组合根类变量面即探针契约面——约 46 个句柄成员被探针挂载假设钉死,max-class-variables 对本件按架构契约豁免(键永久基线内、禁再恶化),豁免理由在册本节,禁为清键改写探针挂载契约**);局态一律住 RunScope 节点,禁在屏/视图侧持有局态引用。
- 屏成员寻址自实现批起迁经界面注册面(**已建件——迭代 44**):探针挂载假设 `main.X` 迁移为 `main.ui.<域容器>.<句柄>`(ui_screens/ui_battle/ui_modals 三域容器,约 867 处探针与系统测试引用随批改址);原 S13 豁免登记〔2026-10-27「组合根类变量面即探针契约面」〕注销;main.gd 类变量 49→15,注册面形态见「模块归属·呈现层」表 ui_registry 行;ModalFlow 实例随 UiModals 容器构造建立(装配早期注入点依赖其先在座);取证探针 shot_probe/fx_shot_probe 随批改址;局态一律住 RunScope 节点,禁在屏/视图侧持有局态引用。
- **组合根拆件**=组合根构造注入、代组合根执行核心写调用的呈现侧子件(现行三件:curio_interact_controller/camp_shop_presenter/treasure_interact_controller——冒险节点后续推进、营地购买与刷新、道具使用)。视图禁直连拆件;核心写调用的可达路径仍是「视图→信号→组合根(含拆件)→核心」,依赖规则与组合根单点装配不变。成员见「模块归属·呈现层」表。
### 视觉层装配合同
@@ -230,14 +238,14 @@
| **run_system_test**(系统测试运行器) | `tools/run_system_test.py` | 系统测试总入口:headless 跑真实玩法路径断言面(宿主=`res://tests/system_test.gd`)+ 三闸判定(口径同 run_smoke,摘要行兼容两种宿主格式) | 进程退出码(0=PASS / 1=FAIL) | 三闸齐备(同 run_smoke:退出码/摘要失败数/脚本错误扫描;wall-clock 超时在位) |
| **run_all**(单元与集成测试) | `game/tests/run_all.gd` | 64 套统一入口(61 单元+3 集成,2026-10-28 实数):套件 manifest 逐条「套件 → 期望退出码」,致命类 expect=1 独立子进程实跑;`negative` 字段标负面套件(仅豁免错误扫描一闸);选跑态 `--only=<词[,词…]>`(直跑形态对套件路径子串匹配只跑命中套件,零命中=用法错 quit(2),仅供开发期最窄确认——不产闸回执、不作验收凭据,批末全量闸不经选跑分支) | 逐套件结果 + 汇总退出码(任一套件失败 quit(1)) | 套件级退出码协议在位;三硬闸齐备(入口内扫子套件输出 + 编排层扫 `unit.log`;`Kit.finish` 零断言返回专用码 2)。子进程输出须经系统命令解释器重定向落盘再读——`OS.execute` 的输出管道在本机恒空 |
| **probe_gate**(场景探针) | `tools/probe_gate.sh` | headless 场景探针段编排:默认段 ui_input_probe / ui_struct_probe / layout_probe / ui_contrast_probe / **screen_flow_probe**(屏壳切换),`--click` 加跑 ui_click_probe,`--full` 加跑 ui_probe / ui_tune_probe / t17_defer_probe / fx_probe;孤儿 Godot 预清 + 批末残留清点(必须 0) | 段级 FAIL 输出 + 汇总退出码 | 每段 timeout 守卫在位(挂死 124/137 = FAIL,不静默);判定统一走 `verdict` 函数——退出码为唯一权威判据,PASS 串降级为附加判据,逐段跑 `tools/error_scan.py` |
| **render_gate**(视觉 golden) | `tools/render_gate.sh` | 窗口化渲染闸:①shot_probe 七屏截图 → shot_checker A-D 语义断言;②fx_shot_probe / board_shot_probe 取证(超时/零字节/文件缺失任一 = FAIL);③棋盘 golden 逐像素容差比对(差 ≤16 / changed ≤2%,provisional)+ 菜单 golden;④批末孤儿残留清零 | 段级 FAIL 输出 + 汇总退出码 | 查退出码在位(每段 timeout 守卫 + 退出码/取证文件双检);**SCRIPT ERROR 扫描缺失**;断言数下限不适用(判定主体 = golden 容差与取证文件存在性,非断言计数) |
| **render_gate**(视觉 golden) | `tools/render_gate.sh` | 窗口化渲染闸:①shot_probe 七屏截图 → shot_checker A-D 语义断言;②fx_shot_probe / board_shot_probe 取证(超时/零字节/文件缺失任一 = FAIL);③棋盘 golden 逐像素容差比对(差 ≤16 / changed ≤2%,provisional)+ 菜单 golden;④批末孤儿残留清零;⑤三段探针日志逐段脚本错误扫描 | 段级 FAIL 输出 + 汇总退出码 | 查退出码在位(每段 timeout 守卫 + 退出码/取证文件双检);SCRIPT ERROR 扫描在位(逐段 `tools/error_scan.py`,标记与 run_smoke 同源);断言数下限不适用(判定主体 = golden 容差与取证文件存在性,非断言计数) |
| **static_scan**(静态扫描域) | `game/tests/static_scan.sh` | 静态扫描域 S1–S10 可执行载体(grep 规则 + `tools/audit_type_hints.py`〔S9〕+ `tools/audit_file_headers.py`〔S10〕) | fail 聚合退出码(任一硬失败规则命中即 exit 1) | 查退出码在位;SCRIPT ERROR 扫描与断言数下限不适用(逐规则命中即红,无运行时错误面与断言计数);S3 现为 warning 级、不参与 fail 判定(见下表) |
### 改动域划分(2026-10-17 用户裁「代码批只跑代码闸、文档批只跑文档闸」)
| 域 | 闸 | 本批改动落在这些路径即属该域 |
|---|---|---|
| **代码域** | run_smoke、run_system_test、run_all、probe_gate `--click`、probe_gate `--full`、render_gate、static_scan、回放确定性对拍(条件件) | **唯一触发=`game/` 下非 `.md` 的行为载体**(代码、测试脚本、探针、场景、数据表、素材)。`game/` 内的 `.md`(README、生成清单)归文档域——2026-10-26 用户裁:判据按文件性质,不按路径前缀。2026-10-18 用户裁「跨领域执法禁绝」(提交 `7bdf53e3`)后,原 `tools/`、`artwork/` 两前缀不再把改动拉进代码域 |
| **代码域** | run_smoke、run_system_test、run_all、probe_gate `--click`、probe_gate `--full`、render_gate、static_scan | **唯一触发=`game/` 下非 `.md` 的行为载体**(代码、测试脚本、探针、场景、数据表、素材)。`game/` 内的 `.md`(README、生成清单)归文档域——2026-10-26 用户裁:判据按文件性质,不按路径前缀。2026-10-18 用户裁「跨领域执法禁绝」(提交 `7bdf53e3`)后,原 `tools/`、`artwork/` 两前缀不再把改动拉进代码域 |
| **文档域** | gdd_lint、doc_link_audit、doc_term_audit、流水线闸 | `game/` 内的 `.md` 与 `game/` 外的一切路径:`design/`、根规则文件(`AGENTS.md`/`roadmap.md`/`待办清单.md`/`体验验证清单.md`)、`tools/` 工具脚本、`.zcode/` 配置、`sim-data/`、`reports/` |
| **两域都含** | repo_structure_audit(熵增闸:顶层目录/根文件/图像白名单/退役路径/design 子目录五查——文档批新增或挪动 `.md` 同样会绊它;纯文本扫描秒级) | — |
@@ -259,7 +267,7 @@
| 硬闸 | run_smoke | run_system_test | run_all | probe_gate | render_gate | static_scan |
|---|---|---|---|---|---|---|
| **退出码优先** | 已落实(闸①) | 已落实(闸①,口径同 run_smoke) | 已落实(套件 manifest expect 协议) | 已落实(`verdict` 函数逐段判退出码,PASS 串不作判据) | 已落实(段级退出码 + 取证双检) | 已落实(fail 聚合退出) |
| **SCRIPT ERROR 扫描** | 已落实(闸③,外部扫描——GDScript 无脚本错误陷阱 API,闸设在外部包装) | 已落实(闸③,标记清单与 run_smoke 同源) | 已落实(入口内扫子套件输出;子进程的 stdout/stderr 须先落盘——`OS.execute` 管道在本机恒空) | 已落实(逐探针段 `tools/error_scan.py`,标记与 run_smoke 同源) | **缺失**(本批未收——判定主体是 golden 容差与取证存在性;登记待办清单) | 不适用(静态域无运行时错误面) |
| **SCRIPT ERROR 扫描** | 已落实(闸③,外部扫描——GDScript 无脚本错误陷阱 API,闸设在外部包装) | 已落实(闸③,标记清单与 run_smoke 同源) | 已落实(入口内扫子套件输出;子进程的 stdout/stderr 须先落盘——`OS.execute` 管道在本机恒空) | 已落实(逐探针段 `tools/error_scan.py`,标记与 run_smoke 同源) | 已落实(三段探针日志逐段 `tools/error_scan.py`,标记与 run_smoke 同源) | 不适用(静态域无运行时错误面) |
| **断言数下限** | 无独立下限(闸②摘要行必须在案,防「跑一半仍 PASS」;脚本错误吞断言路径由闸③兜住) | 同 run_smoke(过渡期宿主摘要行必须在案) | 已落实(`Kit.finish` 零断言返回专用退出码 2——与「断言失败=1」区分,防致命类套件的 expect 协议放过) | 已落实(退出码 0 但无该探针签名标记亦判红——防静默退出) | 不适用(golden 容差 + 取证存在性判定) | 不适用(逐规则命中判定;另有扫描面为空即判红的零命中自检) |
背景事实(run_smoke 头注在案):GDScript 运行时脚本错误不改进程退出码、不计入冒烟失败数、且静默中断所在函数剩余断言——2026-09-19 状态记录 83 实证「309 项 0 失败」实为断言未跑全。这是 SCRIPT ERROR 扫描必须存在于判定层的原因。
@@ -310,9 +318,11 @@
本节为测试域(测试载体、判定口径、静态扫描域)的事实源。新增违规案例仍须先补 static_scan 规则再修码(code-conventions 缺陷-规范对照表纪律)。
**S13 测量面豁免登记(2026-10-27)**:`game/presentation/tune_sim_controller.gd` 的循环内 `await`(测速循环逐帧让出)是调参测量的 wall-clock 步进承载语义——重构即测量口径漂移,违行为零变化红线。该键按架构豁免(键永久基线内),豁免理由在册本段;同件 `print` 残留不豁免(信息已由 toast/JSONL 承载,随全清批删除)。
**S13 测量面豁免注销(2026-10-29 零豁免裁)**:原循环内 `await` 豁免〔2026-10-27〕注销——tune_sim_controller 改 process_frame 信号常驻连接+步进状态机(每帧一局、帧相位逐点对齐,已建件——迭代 44 卡③);运行态收尾现行形态=run() 尾每批一次执行(卡③ v1.0 申报的「每局置假还原」前提已被迭代 41 测试债批 215da315 先行消化,行为改动申报随 v1.1 停卡重裁剥离)。
**S13 豁免登记第三条(2026-10-28 迭代 40 卡①)**:`game/core/battle_orchestrator.gd` 的 max-class-variables 键——宿主保留面为「战斗编排域拆件定稿」节第 1 款锁定的职责承载面(状态机与生命周期、账本读面、attune 族原位、行发射基建、呈现投影),类变量阈值 15 在保留面锁定下不可达,按全清口径第二支臂登记架构豁免(键永久基线内、禁再恶化;实测键账 50→38)。
**S13 豁免第三条注销(2026-10-29 零豁免裁)**:原 battle_orchestrator max-class-variables 豁免〔2026-10-28 迭代 40 卡①〕注销——RoundCycle/BattleFacts 两新件设计闭合(宿主实测 15,已建件——迭代 44 卡② 合回,见「战斗编排域拆件定稿」节轴丁)。
**gdstyle 计数截断缺陷登记(2026-10-29 两设计批独立实证)**:gdstyle v0.4.0 对文件内 inner class 的成员计数存在截断——内嵌类中带行尾注释的属性声明截断其后续成员计数(standee_body 实测 31 声明仅记 19、board_cell_unit 33 仅记 30)。基线登记值属欠估;闸机扫描与基线同源、判定口径自洽不误报,债的真值以实查为准——零豁免各批按真实口径设计(实数低于阈值,不依赖解析缺陷)。**清偿结果(2026-10-31)**:board_cell_unit 30→10、standee_body 19→10 已随迭代 45 真实清偿,基线 refresh 已收账(欠估口径旁证=33/31 声明与实测吻合)。登记待办:向 gdstyle 上游报 issue(仍在册)。
---
+5 -5
View File
@@ -18,7 +18,7 @@
写文档像向朋友当面解释你的游戏:
1. **完整句子优先**:一段只说一件事,长句拆短句。
2. **禁止碎片笔记体**:不写压缩碎片,不把 `A→B→C` 箭头链当叙述(流程交给字符画,见第 3 节),不用只有作者自己懂的缩略语。
2. **禁止碎片笔记体**:不写压缩碎片,不把 `A→B→C` 箭头链当叙述(流程交给图,见第 3 节),不用只有作者自己懂的缩略语。
3. **每节人话开头**:每小节第一段先用白话说清"这是什么、为什么需要它";只读各节第一段就能看懂全文故事线。
4. **术语首现即解释**:专业术语或项目概念首次出现用一句话解释;玩家可见专属名词(如"显影尘")同样要说清。
5. **叙述优先于引用**:AC-x、F-x 等编号引用不进正文叙述,只出现在验收标准与附录。
@@ -30,11 +30,11 @@
|---|---|---|
| 规则的来龙去脉、行为逻辑 | 散文段 | 每段 ≤240 字,一段一件事 |
| 枚举、数值、边界、依赖 | 表格 | 表格只放可列举的事实;禁把需要解释的内容塞进单元格让读者自己拼 |
| 流程、状态流转 | 字符画(代码块) | 禁用散文逐步描述流程 |
| 流程、状态流转、结构关系 | 文档内嵌 HTML/SVG(默认)· Mermaid(网页发布保底)· 字符画(无渲染环境保底) | 禁用散文逐步描述流程;默认在文档内嵌 HTML/SVG(表现力优先,本地预览即渲染,图体行数计入节点篇幅);发布到会过滤内嵌 HTML 的网页端时改 Mermaid;无渲染查看环境用字符画 |
| 公式 | 结论先行 | 先一行加粗结论,再放公式代码块、变量表与验算表 |
| 体验锚点 | 关键词表 | 支柱/关键词/机制承诺 三列表,禁长篇情绪描述 |
节点内顺序:先人话,再表格枚举,再字符流程图,最后公式举证。**规则两段式**:先写"人话阶段"(作用于哪个系统、什么情况下、谁触发、触发后发生什么),再写"举证阶段"(逐效果给出计算方法;纯流程规则明写"无计算")。规则名禁止纯名词短语(反面例:"获取路由")。
节点内顺序:先人话,再表格枚举,再流程图(内嵌 HTML/SVG 或字符画),最后公式举证。**规则两段式**:先写"人话阶段"(作用于哪个系统、什么情况下、谁触发、触发后发生什么),再写"举证阶段"(逐效果给出计算方法;纯流程规则明写"无计算")。规则名禁止纯名词短语(反面例:"获取路由")。
## 4. 用语纪律(见名知意 · 所见即所得 · 禁自造黑话)
@@ -136,7 +136,7 @@
### 12.1 待办清单条目格式与状态机
待办清单是流水线的唯一登记处:要做的事先登记一条,随流水线段推进改状态,做完整条删除。一条条目分两层——**标题=登记元数据**(何时登记、从哪来、紧急度,登记后不再变),**正文=四个属性**(随进程演进)。四个属性各管一件事:`实况` 管凭据,`缺口` 管这条的**输入**(还缺哪一次设计),`改动面` 管这条的**产出**(要动哪些面),`状态` 管这条走到流水线哪一段。
待办清单是流水线的唯一登记处:**现在不做、以后才做的事**登记一条,随流水线段推进改状态,做完整条删除。条目记录某件事走到了哪一段,不是各段的开工前置。一条条目分两层——**标题=登记元数据**(何时登记、从哪来、紧急度,登记后不再变),**正文=四个属性**(随进程演进)。四个属性各管一件事:`实况` 管凭据,`缺口` 管这条的**输入**(还缺哪一次设计),`改动面` 管这条的**产出**(要动哪些面),`状态` 管这条走到流水线哪一段。
标题格式(尖括号处按实际填写,方括号内为可选段):
@@ -173,7 +173,7 @@
文档线:待文档设计 → 文档设计中 → 文档设计已闭合
美术线:待美术设计 → 美术设计中 → 美术设计已闭合
下游共用序列(需求改动):待写卡 → 已写卡 → 已编迭代 N → 完成即整条删除
直改序列(工程改动/文档设计改动/美术素材改动):登记 → 待执行 → 完成即整条删除(经设计讨论的可前置闭合段,如「文档设计已闭合+待执行」)
直改序列(工程改动/文档设计改动/美术素材改动):待执行 → 完成即整条删除(经设计讨论的可前置闭合段,如「文档设计已闭合+待执行」)
累积示例:文档设计已闭合+待美术设计+已写卡
```
+10 -10
View File
@@ -7,22 +7,22 @@
## 1. 分支纪律(车道铁律)
改动按文件性质分两车道(2026-10-26 用户裁,取代 2026-09-19 立的「一切改动走 worktree」铁律;判据与 §3 第 8 条改动域划分同一):
一切面主工作区直改+直提(2026-10-30 用户裁本地 CI 化:验证挪到合回后,主区直提不再拦任何面;判据与 §3 第 8 条改动域划分同一):
1. **worktree 批车道(唯一强制面=代码面)**:一切触及代码面(`game/` 下非 `.md` 的行为载体:代码、测试脚本、探针、场景、数据表、素材)的改动——迭代实现批与触及代码面的工程改动(如纯测试维护)——先建独立 worktree:`git worktree add ../g-plan2-<任务名> -b <任务分支>`;所有改动与验证都在 worktree 内进行(未跟踪文件不会出现在新 worktree,需手工搬运);建区后立刻跑 `tools/setup_worktree_userdir.sh`——给该区独立 Godot user 目录(测试夹具、试玩日志、截图产物不与其他区共用;主区不生成,真实档保持原位)。迭代批开工细则见 §3 与 iter 技能
2. **worktree 批收尾**:批次收口顺序固定为**冻结 → 件核 → 并 master → 重塑 → 跑一次域闸 → 合回**——代码与断言改完即冻结,全仓件核 grep 查残留;master 已被别的批次推进时先 `git merge master` 解冲突(必须在跑闸之前,闸跑完再并=那次闸白跑);**重塑**=迭代分支(iter<N> 形)在跑闸前执行 `python tools/reshape_cards.py`,把分支线性史按卡末态快照压成一卡一笔(可选追加一笔纯文档收尾),master 台账由此每卡一行——工具自带「重塑前后树逐字节一致」断言与 backup 分支,不改任何文件内容;非迭代工程小批免重塑;然后按本批改动域跑**一次** `tools/run_all_gates.sh`(见 §3.9),全绿后在主工作区 `git merge --no-ff <任务分支>`(合回闸对迭代分支数卡校验,未归卡提交拦下),再 `git worktree remove`+删分支。闸后代码面不得再改一个字节(回执作废须重跑),只允许追加 `reports/`、`design/` 等文档件提交。游戏类测试(冒烟/单测/MC/探针)批内默认零执行(两入口铁规,见 §4),用户点名或当场问才跑
3. **直改车道(代码面之外的一切面)**:`design/`、`reports/`、`tools/`、根规则与清单文件、`game/` 内 `.md`、`.zcode/`、`.qoder/`——主工作区直接改、直接提交,无 worktree、无冻结/件核/合并仪式。验收=commit 点 git pre-commit 钩子(`tools/hooks/pre-commit`,经 `core.hooksPath` 挂载,在位性由 run_all_gates.sh 跑批自检)现场跑 `tools/run_all_gates.sh --docs`(秒级文本扫描):绿即落地,红即拦下,修红后重新 commit 钩子自动重跑;跑完把闸输出呈报对话即完成义务。预期大体量改动(跨文档结构重构等)时由用户开工点名自愿开 worktree,非规则强制
4. **执法分层(全机械,零自觉依赖)**:①git pre-commit 钩子——直改车道的闸机检,并对主工作区直提代码面的提交一律拦(代码面唯一合法路径=worktree 批合回);任务 worktree 内的代码面提交跳过(归批末闸+合回闸管,不打扰迭代批中间提交)。②`.zcode/hooks/worktree-guard.sh`(PreToolUse,本机件不入 git)——agent 侧:主工作区代码面的 Edit/Write 与 staged 含代码面的 commit 拦截、`git commit --no-verify` 拦截(该旗标仅属用户)、合回闸回执六况校验(§3.9)。git 层钩子对一切提交者生效(含用户手工提交,紧急出口 `--no-verify`);ZCode 层只约束 agent
1. **主区直改车道(一切面)**:`game/`(含代码面=`game/` 下非 `.md` 的行为载体)、`design/`、`reports/`、`tools/`、根规则与清单文件、`.zcode/`、`.qoder/`——直接改、直接提交。验收分两层:commit 点 git pre-commit 钩子(`tools/hooks/pre-commit`,经 `core.hooksPath` 挂载,在位性由 run_all_gates.sh 跑批自检)现场跑 `tools/run_all_gates.sh --docs`(秒级文本扫描)绿即落地红即拦,合并收尾提交跳过(MERGE_HEAD 在,内容归合回后 CI);代码面的全量验证归合回后本地 CI(下条)。开发期纪律不变:卡内只测自己改的东西、出红只跑红件最小单元(§6)
2. **合回后验证(本地 CI,2026-10-30 用户裁)**:git merge 合回 master 落地后 post-merge 钩子(`tools/hooks/post-merge`)后台触发 `tools/ci_runner.sh merge` 对 master 尖跑一次全量域闸——唯一强制全量验证点;CI 红态下每个修复提交经 post-commit 钩子(`tools/hooks/post-commit`)自动追绿复验,绿态与无状态零触发。重跑判据:同一份字节禁止第二次人手跑;有意义的重跑只剩「验证修复」一类。CI 红不回滚合并——红项记 `game/tests/.gates/ci-status.json`,修复提交追绿;开工先读 CI 状态,红未修不开新批。跑批起手 `--editor --quit` 刷类名缓存(在册坑:.godot 缓存不入 git,合并后失配假红假绿)——原「合并后零复跑」的秒级动作由跑批件接管。并跑语义=跑完补跑(coalesce):跑批期间新触发不中断当前轮,轮尾对最新尖端补跑,绝不逐笔各跑一轮
3. **worktree 批车道(大批约定,非强制)**:迭代实现批与大体量改动按约定仍走独立 worktree:`git worktree add ../g-plan2-<任务名> -b <任务分支>`;建区后立刻跑 `tools/setup_worktree_userdir.sh`——给该区独立 Godot user 目录(测试夹具、试玩日志、截图产物不与其他区共用;主区不生成,真实档保持原位)。收口顺序固定为**冻结 → 件核 → 并 master → 重塑 → 主区合回 → CI 验证**——代码与断言改完即冻结,全仓件核 grep 查残留;master 已被别的批次推进时先 `git merge master` 解冲突;**重塑**=迭代分支(iter<N> 形)合回前执行 `python tools/reshape_cards.py`,把分支线性史按卡末态快照压成一卡一笔(可选追加一笔纯文档收尾),master 台账由此每卡一行——工具自带「重塑前后树逐字节一致」断言与 backup 分支,不改任何文件内容;非迭代工程小批免重塑;然后主工作区 `git merge --no-ff <任务分支>`(合回守卫对迭代分支数卡校验,未归卡提交拦下),合并落地 post-merge 钩子触发 CI,红则修复提交(主区直提)追绿,全绿后 `git worktree remove`+删分支。游戏类测试(冒烟/单测/MC/探针)批内默认零执行(两入口铁规,见 §4),用户点名或当场问才跑
4. **执法分层(全机械,零自觉依赖)**:①git pre-commit 钩子——一切提交的文档域闸机检(合并收尾提交豁免)。②post-merge/post-commit 钩子——本地 CI 触发(仅主工作区;任务 worktree 内的合并不触发)。③`.zcode/hooks/worktree-guard.sh`(PreToolUse,本机件不入 git)——agent 侧:`git commit --no-verify` 拦截(该旗标仅属用户)、iter<N> 分支合回的一卡一提交数卡校验、规则闸(写 design//game/ 前核对会话规约读取记录)。git 层钩子对一切提交者生效(含用户手工提交,紧急出口 `--no-verify`);ZCode 层只约束 agent
## 2. 五段流水线
**需求改动**(游戏代码,含随逻辑写/改的测试)按五段流水线走:**登记 → 文档设计/美术设计(两条设计线)→ 写需求卡片 → 迭代编排 → 实现**;工程/文档设计/美术素材三类改动不走写卡与编排段,走「登记 →(如需设计讨论)→ 用户发令直改 → 主区直改+commit 点闸 → 完成删条目」(车道细则见 §1)。两层口径(2026-10-25 用户立):需求卡片=需求单位的实施文档,任何工作量皆可写、皆可编入迭代;工作量限制只属迭代层——迭代估时总量 <5 个工作日=「冻结待凑批」,不可开工。段间不自动流转,每段转进由用户发令;每段一个技能入口(五者均装于项目 `.zcode/skills/`:doc-design/asset-ingest/req-card/iter-plan/iter),登记不立技能(条目格式由流水线闸强制)。设计工作在会话内进行、用户主导、禁无监管自跑。
**需求改动**(游戏代码,含随逻辑写/改的测试)按五段流水线走:**登记 → 文档设计/美术设计(两条设计线)→ 写需求卡片 → 迭代编排 → 实现**;工程/文档设计/美术素材三类改动不走写卡与编排段,走「(如需设计讨论)→ 用户发令直改 → 主区直改+commit 点闸」(车道细则见 §1)。两层口径(2026-10-25 用户立):需求卡片=需求单位的实施文档,任何工作量皆可写、皆可编入迭代;工作量限制只属迭代层——迭代估时总量 <5 个工作日=「冻结待凑批」,不可开工。段间不自动流转,每段转进由用户发令;每段一个技能入口(五者均装于项目 `.zcode/skills/`:doc-design/asset-ingest/req-card/iter-plan/iter),登记不立技能(条目格式由流水线闸强制)。设计工作在会话内进行、用户主导、禁无监管自跑。
| 段 | 入口技能 | 产出 | 出入口铁律 |
|---|---|---|---|
| 一·登记 | 无(闸管格式) | 待办清单条目 | 条目=标题登记元数据(日期/来源/可选优先级)+正文四属性(实况/缺口/改动面/状态);标题登记后不再变,正文随进程演进。格式与状态机权威=doc-conventions 第 12 节 |
| 二·文档设计/美术设计 | doc-design(文档线,兼任反哺点写入)/asset-ingest(美术录入;样张素材由用户主导产出) | GDD 文档或 art-bible/过审样张素材 | 用户提起或从待办清单取出;会话内进行、用户主导、禁无监管自跑;经用户点头确认心智一致才许写入文档;完成判据=待裁点清零,带未裁点的是讨论稿;设计线闭合凭据记入待办清单实况清单(文档线=文档路径+版本号,美术线=art-bible 条款版本);缺哪条线写在缺口属性,闭合与否写在状态段 |
| 三·写需求卡片 | req-card | 需求卡片 | 设计线闭合后用户发令;新玩法/新系统/新内容必须有设计定稿前置,例外=需求性工程条目(缺陷修复/重构等改游戏行为)凭实况里的证据指认跳过设计线;配套维护类(工程债/测试基建/工具维护,不动游戏行为)=工程改动直改类,不走本段,登记后「待执行」由用户发令直改;卡片必经 grilling 多轮复核、零待裁点才成立;写完在待办清单状态记「已写卡」,卡片路径记入实况清单(防遗忘) |
| 三·写需求卡片 | req-card | 需求卡片 | 设计线闭合后用户发令;新玩法/新系统/新内容必须有设计定稿前置,例外=需求性工程条目(缺陷修复/重构等改游戏行为)凭实况里的证据指认跳过设计线;配套维护类(工程债/测试基建/工具维护,不动游戏行为)=工程改动直改类,不走本段,由用户发令直改;卡片必经 grilling 多轮复核、零待裁点才成立;写完在待办清单状态记「已写卡」,卡片路径记入实况清单(防遗忘) |
| 四·迭代编排 | iter-plan | roadmap 迭代行 | 编排四查:①每卡卡片文件存在 ②每卡设计闭合凭据齐(或需求性工程条目的证据指认)③估时与冻结线——所选卡估时按区间**中值**求和(单位工作日、写精确值不取整):**卡片任何工作量均可编入**(2026-10-25 用户立:工作量限制属迭代层、非卡片层);编入后迭代总量 <5 个工作日=迭代行状态「冻结待凑批」、不可开工,凑批 ≥5 后由编排段改「待开工」(迭代体量偏好,2026-10-21 用户立、2026-10-25 改冻结制);5–10 个工作日为常规区间;超过 10 个工作日须经用户确认(确认前呈报整批/拆批两案等裁,禁自行拆分或塞入),确认后迭代行随带日期特批标记;超过 20 个工作日禁止编(硬上限,用户确认也不放行,必须拆批)④写完 roadmap 行后跑流水线闸确认绿;编入未完成迭代,新开迭代行经用户确认 |
| 五·实现 | iter | 代码与配置,可运行的游戏 | 需求卡片永不单独施工,统一由用户指定迭代号触发(=U0 开工令),按 iter 技能与本文 §3 执行;**本段是代码实现批:禁写 `design/` 与根规则文件,产出物只落 `reports/`(见 §3 第 12 条)**;设计权威文档(GDD/art-bible)批内一律不更新;完成人闸=实机目检清单+实现决定记录写齐即完成批内义务 |
@@ -48,8 +48,8 @@
7. **验收改动先行声明**——验收变化(基线换代/探针增改/旧断言改判)预先写进需求卡片,禁事后补
**完成**
8. **批末按域验收才合并(worktree 批;直改车道无批末动作,验收在 commit 点由 git 钩子执行,见 §1 第 3 条)**——在任务 worktree 内跑 `tools/run_all_gates.sh`,**按本批改动域选档**:按文件性质判——代码面=`game/` 下非 `.md` 的行为载体(代码、测试脚本、探针、场景、数据表、素材)→ `--code`;文档件=`game/` 内 `.md`(README、生成清单)与 `game/` 外一切路径(`design/`、根规则文件、`tools/` 工具脚本、`.zcode/` 配置等)→ `--docs`;两类同批都有 → 全量档不带参数。代码域按**关联档选测**(2026-10-29 用户裁):02 单元与集成只跑「需求卡片申报 ∪ diff 推导」并集的套件——申报=在册需求卡片对照表「验收测试」列提到的测试文件名,推导=闸脚本内套件族映射表按改动文件归族;改动命中公共件/数据表/autoload/数值·存档·RNG·结算触面或映射不到 → 横切回落全部套件;00 冒烟/01 系统测试/09 静态扫描三件保险恒跑不降;03/04 探针与 05 渲染闸维持影响面口径(本批 game/ 改动全在测量面 game/tests、game/sim 时记 SKIP)。所选域全绿即可合回 master,全量回归由用户自发发起(合入后无自动兜底)。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。该脚本是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。跑完写回执 `game/tests/.gates/receipt.json`(含 `domain`、`scope`(full|related,02 跑子集=related)与 `dirty_game` 字段),合回时由 worktree-guard 按 diff 文件性质推域强制校验(六况拦截:缺回执/quick 模式/回执未覆盖分支尖=回执 commit 之后代码面又有改动、或跑闸时代码面带着未提交改动(`dirty_game`)/域不覆盖本次改动/含新增红/master HEAD 非回执 commit 祖先=合并将产出未测内容;guard 只验域覆盖与全绿,不复核选套件清单)。回执与分支尖的对齐判据是**代码面**不是 commit 号:回执之后只追加文档件(`game/` 内 `.md`、`reports/`、`design/` 等)提交,回执照样有效,不构成复跑理由。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占合回全量——其质量义务=落批时人工确认闸可跑通且判据口径未变,闸真崩溃时合回闸 fail-closed(跑不出回执即拦),不以合回全量为兜底
9. **合并后主区零复跑**(2026-10-24 用户裁:合回重跑验的是与回执同一份字节,零信息)——合回落地后 code 批只做两件秒级事:`--editor --quit` 重刷类名缓存(在册坑:.godot 缓存不入 git,与合并后代码失配会假红假绿)+冒烟启动确认可开机(合计约 1 分钟);docs 批零动作。全量验证**每批跑一次**=worktree 批末按域闸,顺序是「先并入 master、再跑闸」(§1 第 2 条);只有两种情形允许把这次跑作废重来——① 闸红且修法动了 `game/`,② 闸已开跑后 master 又被推进(合并将产出未测内容,由合回闸祖先判定拦下,§3.9 第六况)。「跑都跑了再跑一遍保险」「闸后顺手改点 `game/` 再补一跑」都不构成复跑理由;master 上永不跑全量
8. **合回后 CI 验证(worktree 批;主区直改小批的验收在 commit 点由 git 钩子执行,见 §1 第 1 条)**——主工作区 `git merge --no-ff <任务分支>` 落地后 post-merge 钩子自动跑全量域闸(不带参数档,02 全套件),判定与红项落 `game/tests/.gates/ci-status.json`。全量是唯一强制档:跨卡触面、探针与未申报套件的消费漏网只有全量闸能抓(迭代 44 实证),关联档选测的盲区由它兜底。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。该脚本是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。开发期手动跑批按需选档(按文件性质判域:代码面→`--code`,文档件→`--docs`,两类都有→全量;`--code` 关联档选测——02 只跑「需求卡片申报 ∪ diff 推导」并集套件、00 冒烟/01 系统测试/09 静态扫描三件保险恒跑、横切面回落全量——算法见闸脚本),手动跑不构成验收。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占 CI 全量——其质量义务=落批时人工确认闸可跑通且判据口径未变;闸真崩溃时 CI 判红在案(跑不出回执=verdict 红,fail-closed 语义保留)
9. **重跑语义(2026-10-30 用户裁,取代原「每批一次/合并后零复跑」口径)**——同一份字节禁止第二次人手跑;允许的重跑只剩两类:① CI 红后的修复提交(post-commit 钩子自动追绿复验,每笔修复至多触发一轮);② 用户自发发起的全量回归。「跑都跑了再跑一遍保险」「顺手改点再补一跑」都不构成重跑理由。类名缓存刷新与开机确认由 CI 跑批起手接管(§1 第 2 条),合并后无手工动作
10. **批次完成后的登记义务**——合回 master 后按 doc-conventions §12.1 推进待办清单条目状态(做完整条删除),涉及迭代编排的在 roadmap 迭代总表与顶部「当前位」节换代——执行入口=项目技能 batch-closure(批后收尾与中间文档退役),跨会话教训写记忆库。批次过程与结论由 git 提交信息承载,禁止再写会话状态文件(见 doc-conventions 第 10 节)。`design/` 与根规则文件(`AGENTS.md`/`roadmap.md`/`待办清单.md`/`体验验证清单.md`)实现批禁写,见 §12
11. **文档同步闸(实现批禁写文档)**——实现批**禁写 `design/` 整个目录与根规则文件**,**另禁写 `game/tests/README.md`(能力清单,归写卡段维护,test-conventions §8.9)**,要往外交代的东西一律落 `reports/`:实现报告 `reports/iter<N>-实现报告-<日期>.md`(各卡状态+实机目检总清单+遗留读数义务+实现决定记录——超白名单/超卡片明列范围事项逐条记「决了什么/为什么/代价/撤销方式」,全部按卡执行时只写一行「零拍板」)、GDD 反哺点清单 `reports/iter<N>-反哺点-<日期>.md`(每条写清哪个文件哪一节、改成什么、依据是什么)。设计权威文档(GDD/art-bible)**任何批都不直改**:用户在会话中逐点裁决后,由工程/文档设计改动批(直改类)写入 `design/`(执行入口=doc-design 技能),并把反哺条目从 `reports/` 清掉(吸收即死;退役执行=batch-closure 技能)。机械面文档(roadmap 行/术语表/README 工具注)同样归工程/文档设计改动批在合并前更新。反哺点未落地期间相关 GDD 节视为待反哺,读前先查 `reports/` 与待办清单;需求卡片必列「关联文档同步面」按两层标注
12. **实机目检归用户,只登记不阻塞**——真人检测项登记进实现报告目检总清单与体验验证清单即完成批内义务;禁把等目检当停工理由或完成前置
@@ -62,7 +62,7 @@
**MC/verifier 触发细则**:仅在数值验证场景触发(改数值前后取证);normal 档为主、perfect 按需;**无数值改动的会话禁跑 MC**;结构位移类「存档锚」须先向用户请示定性再跑;需求卡片预设步骤的前提消失时(如「带值裁定后重锚」实际未投值)停问勿照字面执行。
**分级周期口径**:全部验证闸收编在 `tools/run_all_gates.sh` 一个入口下(**脚本即清单,且即域划分的唯一出处**),分三档——`--code`/`--docs` 按本批改动域只跑该域闸(闸件清单与域划分只写在脚本头部注释,本文不重列);`--quick` 是全量档的中段自检形态(跳过渲染闸与静态扫描);**不带参数=两域全量**,用于同一批既有代码面又有文档件改动。完成出库按所选域全绿,回执由合回闸按 diff 文件性质推域强制校验。真人实机目检归用户不进自动闸。
**分级周期口径**:全部验证闸收编在 `tools/run_all_gates.sh` 一个入口下(**脚本即清单,且即域划分的唯一出处**),分三档——`--code`/`--docs` 按本批改动域只跑该域闸(闸件清单与域划分只写在脚本头部注释,本文不重列);`--quick` 是全量档的中段自检形态(跳过渲染闸与静态扫描);**不带参数=两域全量**。强制验证点=合回后本地 CI 的全量档(§1 第 2 条);分档只服务开发期手动跑批。真人实机目检归用户不进自动闸。
## 5. MC 采集档位细则
+4 -4
View File
@@ -25,9 +25,9 @@
每个缺陷修复必须在探针或冒烟中留下能抓住该缺陷的断言(例:封印动作耗尽 AP → 必须进入第 2 轮;
选用道具 → 库存态必须变 Used)。没有回归断言的修复视为未完成。
## 4. 提交前验证
## 4. 验证时机(合回后本地 CI 制,2026-10-30 换代)
交付前跑 `tools/run_all_gates.sh`(不带 `--quick`),按改动域选档、所选域全绿——该脚本是全部验证闸的唯一入口与唯一清单,域划分与选闸算法只写在脚本里,本文禁另列闸件清单。闸件增减只改脚本。代码域按**关联档选测**(2026-10-29 用户裁):02 单元与集成只跑「需求卡片申报 ∪ diff 推导」并集的套件,00 冒烟/01 系统测试/09 静态扫描三件保险恒跑不降,横切面改动(公共件/数据表/autoload/数值·存档·RNG·结算触面)回落全部套件;所选域全绿即可合回,全量回归由用户自发发起(合入后无自动兜底)。
开发期窄测义务不变:每批开发中,本批新增/改动的验收测试自己跑绿即收口(卡内只测自己改的东西,process-conventions §6);主区小改孤跑验证后直提。全量验证归合回后的本地 CI:git merge 合回 master 触发 post-merge 钩子跑 `tools/run_all_gates.sh` 不带参数档(两域全量,02 全套件——跨卡触面与关联档选测的盲区由全量兜底),结果落 `game/tests/.gates/ci-status.json`;CI 红态下修复提交经 post-commit 钩子自动追绿。同一份字节禁止第二次人手跑。该脚本是全部验证闸的唯一入口与唯一清单,域划分与选闸算法只写在脚本里,本文禁另列闸件清单。
行为探针使用随机化玩家行为(封印/停手/刷新/无效点击)——自动玩家不做的动作就是测试盲区。
## 5. 测试函数怎么写——验收清单条目的落码规范
@@ -35,7 +35,7 @@
需求卡片 §8「改动—验收对照表」每一条都要落成可执行的验证;本文定统一写法,卡里定每批的具体内容(测什么/注入哪里)。写卡期查不出执行载体的条目=不可测,当场呈报,禁拖到实现期。
1. **执行载体五选一,逐条声明**:单元测试(`tests/run_all`)/系统测试断言(`tests/system_test.gd`)/场景探针(`probe/` 下 `*_probe`)/MC 同种子逐项比对/人工核对(仅限无法自动化的纪律类需求点,如命名锁定、数值占位)。行为类条目以自动化载体为主。
2. **粒度与对应关系**:每条清单条目至少一个自动断言,且断言必须从该条目的真实入口进入;多条条目可合用一个测试函数,但撤销对应改动时每条都要能单独变红。断言失败消息末尾带编号尾缀〔卡短名 Vn〕(如〔秘宝批 V3〕)——断言与卡片清单可互相 grep 查找。**工具面局限备案(2026-10-27 迭代 39 验收审计)**:`Kit.check_quiet` 静默收集形态(计数不入判定即打印、失败消息进 problems 汇总)的断言消息不进 `tools/extract_checks.py` 扫描面——验收尾缀 grep 对该通道失效;如需覆盖属提取器改版(工程批)。
2. **粒度与对应关系**:每条清单条目至少一个自动断言,且断言必须从该条目的真实入口进入;多条条目可合用一个测试函数,但每条都要有自己单独可判红的判据(取证口径见 §8.4 唯一口径——禁变异/禁撤销)。断言失败消息末尾带编号尾缀〔卡短名 Vn〕(如〔秘宝批 V3〕)——断言与卡片清单可互相 grep 查找。**工具面局限备案(2026-10-27 迭代 39 验收审计)**:`Kit.check_quiet` 静默收集形态(计数不入判定即打印、失败消息进 problems 汇总)的断言消息不进 `tools/extract_checks.py` 扫描面——验收尾缀 grep 对该通道失效;如需覆盖属提取器改版(工程批)。
3. **断言词汇按验证对象五选一**:
| 词汇 | 适用场景 |
@@ -67,7 +67,7 @@
- 「跑一遍没崩=通过」无效:测试绿只证明**当前代码没犯这个错**,不证明**犯了这个错会被抓住**;断言存在不等于断言执行。
- **脚本错误会静默中断所在函数的剩余断言,而套件仍报全绿**(机制事实):GDScript 无脚本错误陷阱 API,错误不改进程退出码、不计失败数。三处实证:①2026-09-19「309 项 0 失败」实为断言未跑全(状态记录 83);②敌人表行集纪律 10 条断言因定型数组赋值错误从未执行(迭代 29 修复后两条正向见红=夹具本缺锚行);③`Callable` 形参类型写错致解析函数返回 null、后续断言整段蒸发。
- 由此产生两条义务:**写闸件**按 §6 配三硬闸;**写验收函数**必须自证断言全数执行——真实入口断言在当轮执行并通过(§8.4)为主证,零命中自检(扫描面为空即判红)为辅证。
- **覆盖率不是目标**:行覆盖率度量缺失(Godot 无内建,第三方属新基建,是否引入待办清单在册待裁);现行补洞=零指名单测清单+人工判定,判定可靠性靠上列自证手段,不靠覆盖率数字。
- **覆盖率不是目标**:不引入行覆盖率度量(Godot 无内建,第三方属新基建);现行补洞=能力清单覆盖域表+人工判定,判定可靠性靠上列自证手段,不靠覆盖率数字。
## 8. 改动级验收闭合(2026-10-21 立)
+29 -3
View File
@@ -11,21 +11,23 @@
>
> **兑付频率(先看这条,它是三条渠道的共同前提)**:奇物**不是打一场战斗给一份**。关内的普通战斗节点打完了什么也不给;一关之内奇物**只兑付一次**,触发点是**关底 Boss 被击败**——而按 [冒险结构](冒险结构.md) 的定义,打掉关底 Boss 恰好就等于"通过这一关",这两件事是同一个时刻,不是一条链上的两步。
>
> 状态:**已实现**(2026-10-04 双层 BD 实施批落地);现行为 v1.1。本文件是奇物的唯一定义文档。
> 状态:**已实现**(2026-10-04 双层 BD 实施批落地);现行为 v1.2。本文件是奇物的唯一定义文档。
>
> 数值纪律:本文件只写概念、取值域与指路;每件给多少、效果是什么一律不进正文,事实源是 `data/tables/feats.tres` 与 `data/tables/adventure.tres`。
## 专长(重设计·基础框架 v1)
> 本节是专长分类的现行事实源(2026-10-07 用户终审认同写入文档)。件表为设计声明——数值全 provisional 归调参批;命名全为代号,正式命名归用户。**新框架已随迭代 20 专长实装批落地(2026-10-11):`feats.tres` 29 行=基础 27+迁入 2,行为与数据行 1:1。**
> 本节是专长分类的现行事实源(2026-10-07 用户终审认同写入文档)。件表为设计声明——数值全 provisional 归调参批;命名全为代号,正式命名归用户。**新框架已实装:`feats.tres` 30 行=基础 27+迁入 2+规则豁免 1,行为与数据行 1:1。**
### 设计原则
1. **触发器句式**:每件专长的效果声明为「当 X 时 Y」(on / when / do 三字段,词表=效果引擎封闭集)。件的性格来自它挂在哪个事件上;combo 靠事件共享自然涌现——不设流派标签、不设档位机制。
2. **事件族组织**(设计侧分组工具,不上卡面):开格系 / 封印系 / 踩雷系 / 节奏系 / 战斗系五族。族名禁上玩家可见面(命名铁律)。
2. **事件族组织**(设计侧分组工具,不上卡面):开格系 / 封印系 / 踩雷系 / 节奏系 / 战斗系 / 换图系六族。族名禁上玩家可见面(命名铁律)。
3. **准入校验**:代价与收益须同账可核算——「纯代价件」禁止出生;契约件的代价必须一句话讲得清(契约不可怕,糊涂账才可怕)。
4. **信息红线**:禁止任何效果让数字说谎(信息规则不豁免);揭示类效果只揭真格。
5. **防兑换环**:互相转换的通道必须有损耗或每轮上限;单向通道优先。
6. **基座先行**:流派先确定基座专长——通过改变资源来源、动作成本或频率、动作结算、动作之间的关系,让原本偶尔用于止损的动作成为可以持续使用的主要打法。后续专长再围绕这个新行为提供输出、生存或资源收益;泛用数值增长不能代替基座。
7. **行为改变检验**:基座必须能回答「拿到前后操作有何不同、新行为靠什么资源持续、后续专长为何依附这个新行为」。只是同一操作后的结算数字变大,不算形成新流派。
> **落行注记(迭代 20 反哺写入,2026-10-12 追认⑤)**:「每第 N 次 / 积满触发」的数据行条件写 **N−1**——效果引擎分发时 when 先于 do 读计数器现值(预评估),故「满 N 触发」落行写 N−1(例:地质学家「满 4 种」写 eq 3、封印匠「每第 3 次」写 mod_eq ref 2、双管镐「每第 2 次」写 mod_eq ref 1)。行为仍为每第 N 次准点触发,单元测试钉住;新件落行照此口径。
@@ -95,6 +97,28 @@
|---|---|---|
| 血税 | 本局:踩雷 −2 最大生命;标中 +1 攻击(**均下一战起生效**——战末汇入冒险加算区) | 双向都痛都爽,一句话讲得清;双向同账、本局累积、冒险收束清零。生命上限/攻击系属战界重建属性,即时改会被重建冲掉,故滞后一战兑付(2026-10-12 追认⑨回写;卡面 desc 同步补「下一战起生效」) |
### 换图系(7 件,设计定稿,待实现)
换图系把「弃图刷新」从坏图逃生改成主动构筑行为。两件基座分别解决 RAP 的来源与价格;后续专长让成功刷新产生输出、生存或资源收益。七件不要求一次拿齐,不设强制先拿基座、集件标签或集件奖励。
| 层 | 代号 | 规则 | 行为职责 |
|---|---|---|---|
| 基座 | 掘进储能 | 每次付费挖格后额外获得 RAP;安全格与危险格同口径 | 持续挖图也能准备刷新,不再必须少挖停手 |
| 基座 | 折价换图 | 弃图刷新的 RAP 成本减半 | 同一份储备支持更多刷新 |
| 输出 | 弃图轰击 | 成功刷新后,在本轮战斗段对全体存活敌人造成一次伤害 | 刷新成为直接进攻行为 |
| 输出 | 换图蓄势 | 成功刷新攒层,层数有上限;下一次攻击附加全部层数对应的伤害后清零,战斗结束清空 | 连续换图蓄成一次单点爆发 |
| 生存 | 换图庇护 | 成功刷新后,在本轮授予段获得护盾 | 用刷新预防后续伤害 |
| 生存 | 弃图休整 | 成功刷新后,在本轮授予段回复生命,封顶生命上限 | 放弃当前图换取恢复 |
| 资源 | 弃图回收 | 成功刷新后,按旧图探索进度在本轮授予段回收清图奖励 | 先探索再主动弃图变现 |
共同规则:
- **双基座叠加**:掘进储能与折价换图可以同时持有,产 RAP 与成本减半分别生效;每轮刷新次数上限继续限频,不设互斥规则。
- **成功才触发**:后续五件只认成功换图;刷新被拒绝时不产生效果。同一轮多次成功刷新按发生顺序逐次登记。
- **轮末兑付**:刷新不新增结算段。群伤、护盾、回血与回收按[战斗结算](../战斗循环/战斗结算.md)的既有段序兑付,刷新瞬间不直接改这些战斗结果。
- **回收看旧图**:弃图回收读取被放弃旧图的探索进度,不把旧图或新图判为清图;具体口径归[地图战斗资源](../战斗循环/地图战斗资源.md)。
- **基线不变**:没有这些专长时,RAP 来源、刷新成本、清图奖励与刷新结果保持原规则。数值与稀有度归 `feats.tres`,落表前本节只作设计声明。
### 迁入登记(秘宝域迁出件;实装批落地后并入件表)
| 代号沿用 | 目标族 | on / when / do 要点 | 备注 |
@@ -454,6 +478,8 @@
10. 一个渠道一旦确定要出某个分类,该分类的成员就都在它的池子里——"渠道出哪些分类"不受这条约束,那是渠道轴的定位。
11. 凭证拿到即开,不进持有集,玩家无法囤积——选中或买下的同一时刻就开专长三选一。
12. 凭证带有计步保底:连续 5 次三选一展示未出现则第 6 次必出;计数跨奖励节点与商店共享,刷新后的重新展示计入次数,限购用满后保底不再触发。
13. 被称为流派基座的专长必须能说明拿到前后的操作差异与新行为的持续资源来源;只有同一操作后的泛用数值增长,不得登记为基座。
14. 换图系两件基座同时持有时都生效;后续五件只在成功刷新后触发,无专长基线不变;弃图回收按旧图探索进度结算,同一旧图至多回收一次。
> 实装批落地前,现役代码按旧 27 件专长目录与旧资源成员在同框架下运行;新成员口径(27 件基础件表 / 凭证件表)自实装批起验。
@@ -1,7 +1,7 @@
# 地图战斗资源
> 一句话:格面资源的产出、极性字典与清图奖励封装
> 状态:已定稿(自旧体系迁移);现行为 v1.1
> 状态:已定稿(自旧体系迁移);现行为 v1.2
## 设计目的
@@ -158,8 +158,29 @@
| 可配置可替换 | 内容 = 池行值;允许即时类(当场兑付,不入库) |
| 图类型差异化是预留插槽 | 现行只有默认槽;结构位已留,启用走棋盘生成词表落定 |
持有弃图回收专长时,同一清图奖励对象也可按旧图探索进度部分回收;这不改变正常清图与清图奖励的定义,例外口径见下节。
举证:本节为封装与流程规则,无计算。
### 弃图回收:按旧图探索进度兑付(设计定稿,待实现)
弃图回收让玩家不必挖完最后几个安全格,也能把已经推进的探索换成一部分清图奖励。它只读取被放弃的旧图,不把旧图判为清图,也不把新图判为已清图。
旧图退役前记录三项读数:装板初始已开安全格数、装板初始未开安全格数、退役时已开安全格数。探索进度定义为:
```
探索进度 =(退役时已开安全格数 − 装板初始已开安全格数)÷ 装板初始未开安全格数
```
进度限制在 0~1。玩家付费挖开与连锁展开的安全格都真实推进探索,计入分子;装板时白送的初始格、封锁格与危险格不计入。没有新探索就刷新,进度为 0,不能获得回收奖励。
回收兑付的是旧图在装板期已经摇定的清图奖励对象,按探索进度取对应份额;具体比例、档位、取整与数值归调参。它只发基础奖励的进度份额:
- 不触发完美封印加倍,不产生清图事件,不执行正常清图的整板兑付。
- 同一旧图只在成功退役时回收一次;新图不继承旧图探索进度。
- 刷新被拒绝时旧图不退役,也不回收;成功后的兑付时机归[战斗结算](战斗结算.md)本轮授予段。
- 正常清图仍发完整清图奖励对象;弃图回收不得让同等探索投入的收益无条件高于正常清图。
## 依赖与输出
本系统被两侧夹住:事实与数值从上游来,调用与呈现往下游去。下表就是删除任何一方时的影响面清单。
@@ -239,6 +260,9 @@
- 类型封闭与字典完整:表里出现九型之外的类型、或任一型缺正向/负向行,加载拒绝。
- 未生效可见:满层增益、满血正向治疗、无存活敌的群伤,各自在结算明细记"奖励未生效"行,零折算、零补偿。
- 加载护栏:池空、清图池无正当量、增益引用悬空——任一命中即加载时返回错误并中止;默认内容加载通过。
- 弃图回收进度:付费挖开与连锁展开的安全格计入探索,初始格、封锁格与危险格不计;无新探索时进度为 0,不能获得回收奖励。
- 弃图回收恰一次:成功退役的旧图按当次探索进度回收其预摇清图奖励对象,新图不继承进度,同一旧图不能重复回收;被拒绝的刷新零兑付。
- 回收不是清图:弃图回收不触发完美封印加倍、清图事件或正常清图整板兑付;正常清图仍发完整清图奖励对象。
- 表实一致:数据表字段与奖励表字段定义逐字段一致。
## 待办
@@ -1,7 +1,7 @@
# 战斗结算
> 一句话:轮末结算的编排与逐行兑现
> 状态:已定稿(自旧体系迁移);现行为 v1.2
> 状态:已定稿(自旧体系迁移);现行为 v1.3
## 设计目的
@@ -90,7 +90,7 @@
第一层按行型豁免两类:衰减行(到期移除本就是段级动作,不属于任何一格)、判定行(它是胜负出口的结论,不是棋盘事实)。这两类连来源类别都不必填。
第二层按来源类别豁免,封闭清单五类:主角攻击行(来源是主角单位本身)、敌方行动行(来源是当时动手的那个敌人单位)、清图奖励行(来源是"这张板被清掉"这一整板事实)、专长揭示掷格行(落空行没有真实棋盘格可指)、专长 GP 入账行(来源是钱包面而非棋盘面)——它们都不指向某个棋盘格,来源格校验因此跳过。
第二层按来源类别豁免,封闭清单六类:主角攻击行(来源是主角单位本身)、敌方行动行(来源是当时动手的那个敌人单位)、清图奖励行(来源是"这张板被清掉"这一整板事实)、专长揭示掷格行(落空行没有真实棋盘格可指)、专长 GP 入账行(来源是钱包面而非棋盘面)、专长刷新效果行(来源是刷新专长与被弃旧图,不指向单一格)——它们都不指向某个棋盘格,来源格校验因此跳过。
豁免不等于免检:按行型豁免的行仍要带齐公共字段、守住各自的语义约束(如衰减行的数值必须是本次移除的读数),按来源类别豁免的行仍必须写明来源类别,其余行必须指回一个真实存在的棋盘格。豁免只是免掉"来源格必须有效"这一条校验,不是免掉整份行协议。
@@ -266,6 +266,24 @@ EV 参考数不入正文(派生读数,需要时按判定式与现行表值
挖掘期刷新是玩家在挖掘中途主动换图,经[行动经济](../排雷循环/行动经济.md)扣费:扣费成功后按"旧板退役、生成新板、装板、资源预分配恰一次、变异布置"编排,本轮不打断——没有结算段、没有轮次结束。储备不足被拒是玩家决策的合法结果,不是编排错误。刷新图不发清图奖励,完美封印计数随装板重置。
### 换图系专长:刷新事实与本轮兑付(设计定稿,待实现)
持有换图系后续专长时,成功刷新除执行上述换图编排外,还产生一次 `board_refreshed` 事实。旧图退役前保存它的探索进度与清图奖励对象身份;新板就位后登记事实。刷新被拒绝时不产生事实,任何后续效果都不触发。同一轮多次成功刷新按发生顺序逐次登记,不合并成一次。
刷新只登记事实,不当场插入结算。各效果在既有五段内兑付:
| 专长 | 兑付规则 |
|---|---|
| 弃图轰击 | 每次刷新事实开一张全体敌人伤害单据,在本轮战斗段末经伤害控制器统一结账;目标按结账时的存活状态解析 |
| 换图蓄势 | 每次刷新事实当场累计一层,受表值上限约束;下一次主角攻击附加全部层数对应的伤害,随后清零;战斗结束清空 |
| 换图庇护 | 每次刷新事实在本轮授予段经既有护盾通道授予一次,不新开挖掘期状态入口 |
| 弃图休整 | 每次刷新事实在本轮授予段回复生命,按既有治疗规则封顶生命上限;满血时记未生效行 |
| 弃图回收 | 每次刷新事实在本轮授予段按旧图探索进度回收其清图奖励对象;进度与奖励口径归[地图战斗资源](地图战斗资源.md) |
弃图轰击若击杀最后敌人,仍由本轮判定段按既有顺序判胜,不新增挖掘期即时胜利出口。换图系效果使用扩展行留痕:来源写明专长与被弃旧图,弃图轰击走伤害单据,护盾、回血与回收各保留自己的兑付记录;它们不指向单一棋盘格,使用明细流水的专长刷新效果来源豁免,不伪造来源格。
弃图回收不是清图:不触发完美封印加倍,不发清图事件,不改变「刷新图不发清图奖励」。它兑的是被放弃旧图按探索进度回收的一部分奖励。
战斗开始按固定序列开张:关上配置热重载的闸门、初始化状态池、掷骰定敌方实际数量、生成敌人、装板并预分配资源恰一次、变异布置([地形变异](../排雷循环/地形变异.md)——棋盘设置顺序第 4 步:生成棋盘 → 打开状态初始化 → 战斗资源格设置 → 变异格设置,消费 `variant_place` 封闭流)、发开轮通知并执行开轮摇号(摇定首轮各敌本拍动作)。上一关的大奖励(如血上限提升)经战斗开始入参的覆盖值通道生效,内容归[冒险结构](../冒险循环/冒险结构.md)。
举证:本节为编排流程,无计算。
@@ -360,6 +378,9 @@ EV 参考数不入正文(派生读数,需要时按判定式与现行表值
- 无来源即炸:每行明细都能指回一个来源事实(棋盘格、单位、清图或段级结论);找不到来源即断言失败,行型与来源类别两层豁免清单见明细流水一节,豁免行仍受其它契约约束。
- 行型封闭:流水行型只出现本文登记的基础 9 类与扩展行;行型词表改动必须走本文修订并同步呈现层行格式化,未登记行型禁止落行。
- 伤害单据管线(2026-09-24):全部伤害(主角先手、敌方伤害技、棋盘克制直伤、群伤、反噬、地形变异)经伤害控制器单据管线兑现,兑付配方封闭五种;目标在结账时刻照规则解析;死亡事件由单位自行宣布——怪死其未结单据作废(零行、不进冷却不进保底计数)、主角死兑付终止;段界结账为现行口径、开单即结为回切结构位,两模式各自同种子同输入逐位复现。
- 换图系刷新事实:成功刷新保存旧图探索进度并登记一次事实,被拒绝时不登记;同一轮多次刷新按发生顺序逐次兑付,刷新本身不新增结算段或轮次结束。
- 换图系输出:弃图轰击在本轮战斗段末经伤害控制器结算;换图蓄势受层数上限约束,下一次主角攻击消费全部层数,战斗结束清空;群伤致敌清空时仍由本轮判定段判胜。
- 换图系生存与回收:护盾、回血与弃图回收在本轮授予段兑付并留下可归因扩展行;回复封顶生命上限;回收不触发清图事件或完美封印加倍。
- 条款3 对账(2026-10-05 判式换代批 T3——与行动经济 F1 条款3 重立同步):①聚合健康式 (1−ρ̂)·E_dig(u) − ρ̂·V_mine(u) ≥ 0 与 β 比值带 [1.2, 2.5] 判读逐 u 检算(清图支配性审计为常设重跑项,读数快照登记归 clause3 重立条);②封印溢价兑付正确:封印标中正向兑付 = 同型正向 ×(1+δ)(δ 读 `settlement.tres` flag_hit_premium,100=无溢价直通零扰动),三类形态全覆盖(flat power / percent 幅度 / gp 缓冲)、标空与挖格兑付不乘溢价、危险格与安全格同池结构零触碰。
## 待办
@@ -1,7 +1,7 @@
# 战斗表现
> 一句话:战斗动画与演出(旧体系未单独成文)
> 状态:范围草案(待设计);现行为 v1.1
> 状态:范围草案(待设计);现行为 v1.2
## 设计目的
@@ -92,6 +92,8 @@
判定与回退:第 1 项不过即演出元素回炉,禁止以牺牲可读性换氛围(改演出、不动数字口径);第 2–4 项不过对应资产或方案回炉重交,已过项结果保留。第 5 项(敌层插入位置)不产生过/不过,产出一条裁决记录:插入位置结论由[敌人系统](敌人系统.md)的呈现契约登记(~~敌人卡形态对照~~半边已于 2026-09-20 提前裁决完成——全站桩定稿,结论已回写 battle.md 起草条款)。样张通过 = 角色剪影、动画方案、战斗空间、敌层插入位置四件锁定物冻结;通过前不启动任何终稿资产与动画设计。
**现行状态(2026-10-30 战斗一屏样张批)**:五项验收的机检断言已随该批全绿(机器代理口径与实测读数查战斗一屏样张批 git 提交史,合并提交 3b66d1a6);样张整体过闸仍差方向稿目检先行(art-bible §1.4 闸形,目检项登记体验验证清单 ㊶)。
### 演出服从信息与可读性
两条现行硬约束,将来任何动画方案都禁止越过。其一,数字可读性高于氛围:挖掘期画面无杂音,动画禁止遮挡数字、禁止在挖掘期抢焦;结算期的色彩与能量走向以 art-bible 第 2 节为权威。其二,结算逐行兑现是信息载体:battle.md 已承诺不可整体跳过、只提供减动效选项(时长减半或直达终态,逐行兑现可加速不可省略),本文将来兑现其动画侧。
@@ -1,7 +1,7 @@
# 行动经济
> 一句话:每轮行动点怎么花、停手与刷新的代价
> 状态:已定稿(自旧体系迁移);现行为 v1.1
> 状态:已定稿(自旧体系迁移);现行为 v1.2
## 设计目的
@@ -91,6 +91,19 @@
停手与刷新合起来构成这本账的基线节拍:满行动点一轮停手,正好攒够一次刷新。行动点修正队列会扰动该节拍的实际兑现(+1 轮可多挖一格),不变式本身不动;对账归曲线调参批。
### 换图系专长对储备与刷新的改写(设计定稿,待实现)
换图系两件基座分别改写 RAP 的来源与刷新价格。它们只给持有专长的玩家增加规则,不改变无专长时的基线账本。
| 专长 | 规则 | 不变边界 |
|---|---|---|
| 掘进储能 | 每次付费挖格被受理后,额外获得表值 RAP;安全格与危险格同口径 | 连锁展开只随首格的一次付费动作结算,不逐格追加;封印、移动、刷新不产 RAP |
| 折价换图 | 弃图刷新的 RAP 成本为基线的 50% | 只改价格;AP、轮次、每轮刷新上限与换图流程不变 |
两件基座可以同时持有并叠加:付费挖格照常产 RAP,刷新同时按半价扣费。两条规则作用在不同账户入口,不互相覆盖,也不设互斥。刷新仍须储备达到折后门槛才受理;储备不足或超过每轮次数上限时拒绝,状态不变。RAP 仍在战斗结束清零,刷新本身不返 RAP。
无掘进储能时,挖格不新增 RAP;无折价换图时,刷新成本仍读全局基值。后续输出、生存与资源专长如何使用成功刷新这个事实,归[战斗结算](../战斗循环/战斗结算.md)。
### 下轮行动点修正(格面资源八型批 2026-09-20)
格面资源八型批(2026-09-20)新增 action 型格面资源:正向行 ap_up = 下轮行动点 +1,负向行 ap_drain(迟滞)= 下轮行动点 −1。修正不在授予当轮生效:结算期经行动经济的行动点修正队列登记(同轮多笔按净值累加),下一轮开始时(开轮通知)一次性兑现——兑现值 = 基线 ap_per_round + 排队净值,下限 0,兑现后队列清零;排队净值不跨轮保留。明细在结算流水记效果行(effect_id=ap_delta)。
@@ -236,6 +249,8 @@
| move_ap_cost | int | 表值(冻结 0) | 移动免费——冻结,非调参项 |
| chain_ap_cost | int | 表值(冻结 0) | 连锁免费——冻结,非调参项 |
| refresh_cap_per_round | int | 表值(provisional,结构位预留) | 每轮刷新上限,开轮重置计数 |
| (跨表)掘进储能产 RAP | int [1, 64] | 设计定稿,待入 `feats.tres` | 每次受理的付费挖格兑付一次;连锁不逐格追加 |
| (跨表)折价换图成本率 | percent | 50%(结构已裁,待入 `feats.tres`) | 只改持有者的刷新价格;双基座叠加时不覆盖掘进储能 |
| (跨表)跨冒险行动点加成 | int [0,10] / int [0,99],冒险表 | adventure_table bonus_ap_flat / bonus_cap_ap(provisional) | 三选一 AP 件兑现值与全冒险次数上限;加成轴语义见「跨冒险行动点加成轴」节 |
字段名注记:2026-09-18 措辞完成批已全链对齐正式名——表字段与本文行名统一为 `refresh_cap_per_round`,刷新入口命名与表字段口径对齐(命名统一登记兑现,切片临时名零残留)。
@@ -250,6 +265,8 @@
- 仲裁唯一:最后 1 点行动点挖开最后一个安全格时,判清图(剩余清零 + 换图 + 清图奖励),不判耗尽;输出恰一条。
- 停手 1:1:剩余 3 点停手,储备池加 3,下轮同图继续。
- 刷新规则:储备不足时刷新被拒绝且状态不变;同一轮第 3 次刷新被拒绝;刷新不重置行动点、不发清图奖励、旧图上的封印印记失效。
- 掘进储能:持有专长时,每次受理的付费挖格额外产一次 RAP,安全格与危险格同口径,连锁展开不逐格追加;无专长时挖格不新增 RAP。
- 折价换图与双基座:持有折价换图时按基线的 50% 扣费;与掘进储能同时持有则两条规则都生效,刷新仍受储备门槛与每轮次数上限约束。
- 储备清零:战斗结束信号到达后储备池归 0,行动点进入休眠。
- 跨字段校验:任一动作成本配置超过每轮预算时,加载期致命中止。
- 零随机:同一动作序列驱动两个独立行动点实例,轮次结束记录与储备轨迹逐项相同。
@@ -55,13 +55,13 @@ HUD框架站在玩家与全部玩法系统之间:挖掘期它是输入的翻
三区各管一块:
| 区 | 位置与尺寸 | 管什么 |
| 区 | 位置与几何 | 管什么 |
|---|---|---|
| B 战斗画面 | 全宽 × 270px | 主角站桩立绘、敌人站桩立绘 ×1–3(**全站桩形态,2026-09-20 用户拍板:无框立绘+底座、状态小图标挂血条下,英雄卡/敌人卡退役**)、轮次阶段指示、结算明细带 |
| C 角色信息 | 左下 240×530 | 生命 / 攻击 / 储备数据框、状态效果槽、小人立绘 |
| D 扫雷图 | 中下约 1036×530(原右下面板删除后横向加宽) | 行动点条与按钮组、棋盘本体、toast 槽 |
| B 战斗画面 | 全宽,高读布局表 `stage_h` | 主角站桩立绘、敌人站桩立绘 ×1–3(**全站桩形态,2026-09-20 用户拍板:无框立绘+底座、状态小图标挂血条下,英雄卡/敌人卡退役**)、轮次阶段指示、结算明细带 |
| C 角色信息 | 左下,宽读 `info_panel_w`,高 = 基准屏高 − B 区高 | 生命 / 攻击 / 储备数据框、状态效果槽、小人立绘 |
| D 扫雷图 | 中下,宽 = 基准屏宽 − C 区宽 − 列间距(原右下面板删除后横向加宽),高同 C 区 | 行动点条与按钮组、棋盘本体、toast 槽 |
分区尺寸与组件落位的像素级裁决归 [battle.md](../../ux/battle.md);三区结构与双舞台常驻是结构冻结项,修订走文档修订而非调参。节点链进度不在战斗屏呈现——它由关卡画面(选路覆盖层常驻形态)的节点态承载,见[关卡节点地图](../冒险循环/关卡节点地图.md)。
分区几何的唯一事实源 = `game/data/tables/battle_layout.tres`(经 `ConfigDb.presentation.battle_layout` 读取);分区尺寸与组件落位的像素级裁决归 [battle.md](../../ux/battle.md),两份文档只写结构关系、不复写数值。三区结构与双舞台常驻是结构冻结项,修订走文档修订而非调参。节点链进度不在战斗屏呈现——它由关卡画面(选路覆盖层常驻形态)的节点态承载,见[关卡节点地图](../冒险循环/关卡节点地图.md)。
**结论**:棋盘格边长由 [battle.md](../../ux/battle.md)「棋盘格尺寸公式」按棋盘区实测面积与板型算出;本文只判"塞不塞得进",不复制算式与算例。**分辨率档位(2026-09-30 迭代 9 三裁决案甲修订)**:两档=1280×800 基准档(本表全部定值的口径)+1920×1080 档=**全局等比**(stretch canvas_items+aspect keep——16:10 设计在 16:9 窗口为等比 1.35×+左右黑边各 ~96px);本节与各屏定值一律按 1280 基准档书写,1920 档由引擎等比承载、零重排。
@@ -74,9 +74,9 @@ HUD框架站在玩家与全部玩法系统之间:挖掘期它是输入的翻
| 变量 | 含义 | 现行值 |
|---|---|---|
| N | 棋盘边长格数 | 读[棋盘生成](../排雷循环/棋盘生成.md)(现行四档;事实源 board_gen.tres) |
| 棋盘区宽 / 高 | 扫雷图区基准尺寸 | 由 D 区尺寸扣行动点条 44px 得出,本表不复制现算值 |
| 棋盘区宽 / 高 | 扫雷图区基准尺寸 | 由 D 区尺寸扣行动点条高(读 `ap_bar_h`)得出,本表不复制现算值 |
装板可行性由上面的不等式保证;格边长的现行算例与可读下限见 [battle.md](../../ux/battle.md)。棋盘数字直接显示排雷玩法给的真值——呈现层禁止自己算数字,这是"数据层从不说谎"在呈现侧的镜像。
装板可行性由上面的不等式保证;格边长的可读下限见 [battle.md](../../ux/battle.md),算例由样张渲染脚本按布局表现算、不入文档。棋盘数字直接显示排雷玩法给的真值——呈现层禁止自己算数字,这是"数据层从不说谎"在呈现侧的镜像。
### 提示与弹层:轻提示随起随消、重弹层排队进出
@@ -244,7 +244,7 @@ HUD框架站在玩家与全部玩法系统之间:挖掘期它是输入的翻
## 调参表
本系统的参数全部是呈现节奏与可读性常量,不是玩法数值——玩法数值一律归[数据配置](数据配置.md)的表,与本表无关。修订本文常量走文档修订流程。**本表常量无数据表承载,为文档面唯一事实源;入驻 .tres 时本列改指路。**
本系统的参数全部是呈现节奏与可读性常量,不是玩法数值——玩法数值一律归[数据配置](数据配置.md)的表,与本表无关。修订本文常量走文档修订流程。**本表常量无数据表承载,为文档面唯一事实源;入驻 .tres 时本列改指路。** 布局几何(B 区高 / C 区宽 / AP 条高 / 明细带两态高)已入驻 `game/data/tables/battle_layout.tres`,不在本表重复。
| 参数 | 域 | 现行值(文档自持,无表承载) | 说明 |
|---|---|---|---|
@@ -254,7 +254,6 @@ HUD框架站在玩家与全部玩法系统之间:挖掘期它是输入的翻
| clear_celebration_ms | 每次清图 | 3000–5000 | 上限约束:三五秒即收 |
| font_min_board / font_min_detail / font_min_tooltip | 1280×800 基座 | 18 / 14 / 13 px | 可访问性基线 |
| reduce_motion_scale | 每段动画 | 0.5 或跳终态 | 减动效开关系数 |
| detail_band_height | 结算明细带 | 24 / 84 px | 摘要 / 实时流两态高度 |
结构冻结(非调参项,改动走文档修订):三区布局与分区尺寸、双舞台常驻、单意图帧纪律、弹层层级纪律、图标键映射契约、程序化占位策略、订阅纪律。性能预算为引用值:同屏 draw calls < 200、进入战斗屏 500ms 内可交互。
@@ -262,7 +261,7 @@ HUD框架站在玩家与全部玩法系统之间:挖掘期它是输入的翻
以下判据逐条可测,全部通过才算闭环达标。
- 三区无遮挡、无重叠、无内容裁切(棋盘全部格子与数字完整可见),分区尺寸与 battle.md 布局表一致;同屏 draw calls < 200;进入战斗屏 500ms 内首格可点。
- 三区无遮挡、无重叠、无内容裁切(棋盘全部格子与数字完整可见),分区几何与布局表 `battle_layout.tres` 一致;同屏 draw calls < 200;进入战斗屏 500ms 内首格可点。
- 任意板型装板后网格实高不超出棋盘区:N × 格边长 + (N−1) × 2px ≤ 棋盘区可用高;数字字号与整数倍缩放判据见 battle.md。
- 已开 / 未开格态在灰度截图下可逐格分类;已引爆危险格"可通行可读"(已开底加巢穴纹、可作移动目标)。
- 类型微光:微光只在英雄周围 8 格内的未开格显示;出圈即灭、入圈即亮;已开格与封锁格恒无微光;微光呈现不随格是雷是安变化(不泄危险格与安全格)。(迭代 30 卡①实装后转正式验收)
@@ -70,6 +70,7 @@
| 关卡表 | 逐关节点图与层名 | [冒险结构](../冒险循环/冒险结构.md) |
| 冒险表 | 关卡大奖励与钥石入账(单行全局) | [冒险结构](../冒险循环/冒险结构.md) |
| 元进度表 | 元进度字段(已建表 2026-09-21——现行五行族:属性锻造 / Boss 钥石 / 工坊阈值 / 换算率 / 同调刷新升级行族,顺带扩列) | [元进度](../跨局循环/元进度.md) |
| 战斗屏布局表(`battle_layout.tres`) | 分区几何与明细带两态高(单行;呈现层布局配置,非玩法数值——供呈现层与样张工具同源读取) | [战斗屏规格](../../ux/battle.md)(结构)/[HUD框架](HUD框架.md)(呈现常量) |
举证:本节为数据纪律约定,无计算。违规检测 = 静态扫描加人工复核,随各表回填定稿与里程碑检查执行。
+11 -15
View File
@@ -115,13 +115,13 @@
### Layout Zones
采用用户拍板的布局图(2026-09-12),按 1280×800 基座落尺寸:
采用用户拍板的布局图(2026-09-12),按 1280×800 基座落尺寸。**分区几何的唯一事实源 = `game/data/tables/battle_layout.tres`**(经 `ConfigDb.presentation.battle_layout` 读取;字段 `stage_h` / `info_panel_w` / `ap_bar_h` / `log_band_h_dig` / `log_band_h_settle`)。本节只写结构关系与内容职责,不复写数值;任何文档与代码不另立第二份布局值。
| 区 | 位置与尺寸 | 内容 |
| 区 | 位置与几何 | 内容 |
|---|---|---|
| **B 战斗画面** | 全宽 × 270px(2026-10-06 方案二拍板:演出 ≈1/3 屏,原 252) | 主角站桩立绘(左)+ 兑现演出区(中)+ 敌人站桩立绘 ×1–3(右)——**全站桩形态(2026-09-20 用户拍板,敌人卡形态待办裁决完成):主角与敌人均为无框立绘+底座,零卡片**;主角与敌人站桩 = **HP 与状态效果的唯一展示面(2026-09-26 信息归属拍板)**;底部结算明细带(24px 摘要 / 结算期 84px 实时流);顶部中央轮次·阶段指示 |
| **C 角色信息** | 左下 240px × 530px(2026-10-06 方案二:548→530) | 攻击值(含修正显示)、护甲 AC;秘宝槽、奇物列表(现行标题「特性」)——HP 条/RAP 池/状态效果槽/立绘占位/钱包行退役(2026-09-26 信息归属拍板:HP 与状态唯一展示面 = B 区站桩;RAP 迁 D 区 AP 条;GP/钥石迁出战斗屏) |
| **D 扫雷图** | 中下 ~1036px × 530px(2026-10-06 方案二:548→530——压缩全部由面板空档吸收,**棋盘 438×438 原尺寸不缩、格子不变**;原右下面板删除后横向加宽,吸收其宽度) | 顶部 AP 条(44px:左——AP 点阵 + 停手按钮;中——RAP 点阵+数字〔自 C 区迁入,贴弃图刷新按钮,2026-09-26 拍板〕;右——图例按钮 + 弃图刷新按钮〔雷图右上角,2026-09-12 拍板〕);棋盘主体(居中,格边长见下方「棋盘格尺寸公式」);toast 槽(上缘内侧) |
| **B 战斗画面** | 全宽,高读 `stage_h`(2026-10-06 方案二拍板:演出 ≈1/3 屏) | 主角站桩立绘(左)+ 兑现演出区(中)+ 敌人站桩立绘 ×1–3(右)——**全站桩形态(2026-09-20 用户拍板,敌人卡形态待办裁决完成):主角与敌人均为无框立绘+底座,零卡片**;主角与敌人站桩 = **HP 与状态效果的唯一展示面(2026-09-26 信息归属拍板)**;底部结算明细带(挖掘期收起高读 `log_band_h_dig`,结算期展开高读 `log_band_h_settle`);顶部中央轮次·阶段指示 |
| **C 角色信息** | 左下,宽读 `info_panel_w`,高 = 基准屏高 − B 区高 | 攻击值(含修正显示)、护甲 AC;秘宝槽、奇物列表(现行标题「特性」)——HP 条/RAP 池/状态效果槽/立绘占位/钱包行退役(2026-09-26 信息归属拍板:HP 与状态唯一展示面 = B 区站桩;RAP 迁 D 区 AP 条;GP/钥石迁出战斗屏) |
| **D 扫雷图** | 中下,宽 = 基准屏宽 − C 区宽 − 列间距,高同 C 区(2026-10-06 方案二:底行压缩全部由面板空档吸收,**棋盘原尺寸不缩、格子不变**;原右下面板删除后横向加宽,吸收其宽度) | 顶部 AP 条(高读 `ap_bar_h`:左——AP 点阵 + 停手按钮;中——RAP 点阵+数字〔自 C 区迁入,贴弃图刷新按钮,2026-09-26 拍板〕;右——图例按钮 + 弃图刷新按钮〔雷图右上角,2026-09-12 拍板〕);棋盘主体(居中,格边长见下方「棋盘格尺寸公式」);toast 槽(上缘内侧) |
> 布局为 B/C/D 三区——关卡进度条不在战斗屏(节点链进度常驻关卡画面,见 Navigation Position;字母不重编沿用 B/C/D)。
@@ -145,11 +145,7 @@
- N 的取值与每关板型的对应关系读[棋盘生成](../systems/排雷循环/棋盘生成.md),现行值的事实源是 `data/tables/board_gen.tres`。
- 数字字号 = max(18, ceil(格边长 × 0.5)),像素字体整数倍缩放(canvas_items stretch 保证清晰)。
按 1280×800 基准档、棋盘区可用高 486px(D 区 530 减行动点条 44)推算的算例(随布局与板型变化,不是契约值):
| 板型 N | 10 | 12 | 13 | 14 |
|---|---|---|---|---|
| 格边长 | 43px | 36px | 33px | 30px |
算例不入文档:棋盘区可用面积随布局表与板型变化,不是契约值——现算并绘在整屏样张上,本文件不复写。
### Component Inventory
@@ -196,12 +192,13 @@
### ASCII Wireframe
> **2026-09-26 信息归属拍板**:C 区 HP/RAP/状态槽/立绘占位/钱包行退役——HP 与状态唯一展示面 = B 区站桩,RAP 迁 D 区 AP 条贴弃图刷新按钮,GP/钥石迁出战斗屏;C 区改攻击数据框+两预留区。下方线框图已随拍板重绘;旧线框留档于 git 历史。
> **2026-09-23 停点前移批**:A 区关卡进度条退役,下方线框图为 B/C/D 三区(28px 纵向空间划归 C/D 底行 520 → 548;关卡内节点进度常驻关卡画面,战斗屏零进度承载);旧四区线框留档于 git 历史。
> **2026-09-23 停点前移批**:A 区关卡进度条退役,下方线框图为 B/C/D 三区(纵向空间划归 C/D 底行;关卡内节点进度常驻关卡画面,战斗屏零进度承载);旧四区线框留档于 git 历史。
> **几何标注已撤**:线框只示意结构,各区几何读 `game/data/tables/battle_layout.tres`,图中不标像素值。
> **2026-09-20 全站桩形态改版**:敌人卡形态裁决完成(卡片式退役、主角与敌人均站桩——无框立绘+底座、状态小图标挂血条下),下方线框图 B 区已随裁决重绘;旧卡片式线框留档于 git 历史。
```
1280 × 800 ────────────────────────────────────────────────────────────────────
┌─[B] 战斗画面 270px ──────────────────────────────────────────────────────────┐
┌─[B] 战斗画面(高读布局表) ──────────────────────────────────────────────────┐
│ /小人\ 第 3 轮 · 挖掘中 伏×2 伏×2 │
│ (落影) [ 兑现演出区 ] /蛆虫\ /蛆虫\ │
│ HP▓▓▓▓░ (结算期: 光带→弹道→受击) ①HP▓▓▓░ ②HP▓▓░░ │
@@ -209,8 +206,7 @@
│ ├─ 结算明细带 ────────────────────────────────────────────────────────────┤ │
│ │ ▸上轮: 主角攻击3→蛆虫-3 | 攻击+50%(1结算) | 警觉+1×2 … [展开明细▾] │ │ # 2026-09-17 数值压缩批:UI 示意文本同步(atk 7→3〔地精格数÷3 + L1 锚行;UX 示意按当前地精行值读〕、伏击 +3 → +1/层,幅度参考 data/tables/)
│ └────────────────────────────────────────────────────────────────────────┘ │
├─[C] 角色信息 240px ───┬─[D] 扫雷图 ~1036px ──────────────────────────────────┤
│ 548px │ 548px │
├─[C] 角色信息 ─────────┬─[D] 扫雷图 ───────────────────────────────────────────┤
│ ┌──────────────────┐ │ ┌─────────────────────────────────────────────────┐ │
│ │ 攻击 6 (+50%) │ │ │ AP ●●●●●○○○ 5/8 RAP ▓▓▓░ 3/8 │ │
│ │ 护甲 AC 12 │ │ │ [⏹停手] [?图例] [↻刷新 8|剩2] │ │
@@ -232,7 +228,7 @@
| State / Variant | Trigger | What Changes |
|---|---|---|
| **挖掘阶段(默认态)** | 轮次开始 / 结算完毕回 Active 相位 | B 区敌人待机(意图徽标全程可见——敌人系统 Rule 8⑤),D 区棋盘可交互,AP 条点亮;能量 2/10,唯悬停呼吸微光在动(§2.1) |
| **结算阶段** | round_end 触发(AP 尽/停手/清图) | D 区锁输入(棋盘去饱和 + 金橙色相偏移,明度不变,数字保持全对比);B 区播放五段编排演出(授予段 → 增益/减益段 → 战斗段 → 衰减段 → 判定段,战斗结算 段序冻结);明细带展开 84px 逐行滚动;阶段指示→"结算中";能量 7/10 |
| **结算阶段** | round_end 触发(AP 尽/停手/清图) | D 区锁输入(棋盘去饱和 + 金橙色相偏移,明度不变,数字保持全对比);B 区播放五段编排演出(授予段 → 增益/减益段 → 战斗段 → 衰减段 → 判定段,战斗结算 段序冻结);明细带按结算期档展开(高读 `log_band_h_settle`)逐行滚动;阶段指示→"结算中";能量 7/10 |
| **清图瞬间** | 全部安全格开完 | 金粒喷泉 + 彩纸 3–5s 即收(§2.3);清图奖励行强调呈现(H2,地图战斗资源×④——金框高亮行);随后换新图 install |
| **完美封印高光** | 战斗结算高光判定(全图标中/零误标类) | 全屏喝彩级反馈(§2.2 最高档):金光带加倍扫射 + 「完美封印 ×2」横幅——P2 爆发兑现节点(创意风险 #2 落点) |
| **封印标中高光** | 封印判定·标中 | 该印记化暴击弹道加重 + 明细行金框——chord 缺席的即时兑现补偿(创意风险 #3 落点) |
+5 -3
View File
@@ -1,6 +1,6 @@
# 营地场景 UX spec(camp)
> 状态:v2.1(现行)
> 状态:v2.2(现行)
> 事实源:本 spec(呈现)+ [元进度](../systems/跨局循环/元进度.md)(机制)
> 美术画法:[art-bible](../../art/art-bible.md) §6.2/§7.5;素材命名与路径:[asset-path-map](../../art/asset-path-map.md)
> 相邻:[level-map.md](level-map.md)(主菜单语境)
@@ -24,9 +24,11 @@
纵深=远景天空与远山/中景木围墙与营地大门/近景篝火与左右物件(作者纵深稿)。中景围墙是横贯全宽的密排竖木桩墙——木桩一根挨一根无缝、桩顶削尖、无横木,只在中段留缺口给大门。
**中景带自画面底部往上约 2/3 处起**;近景物件允许压住围墙(围墙是纯背景板),**唯一例外=中间门洞必须留空、不被任何物件遮挡**。围墙形态、门扇比例与帐篷尺寸的现行口径归 [art-bible](../../art/art-bible.md) §6.2,本 spec 不再复述。
**场景按要素分层(10 张)**:远景天空/远山/整片地面/门洞内的小路合成 `scene_camp_bg` 背景层(z0,不透明全幅);中景木围墙独立成 `scene_camp_wall` 围墙层(z1,透明底,门洞处留透明缺口);其余八个物件层各一张、由上往下叠。门洞是围墙层的透明缺口,透出背景层画的**一条通向远处的小路**,不可填死。
坐标草案与验收判据见 [提案](../../../reports/提案-营地场景图美术生成方案-2026-10-22.md) §2.5/§5。
**中景带自画面底部往上约 2/3 处起**;近景物件允许压住围墙(围墙不可点,z 序在近景物件之下),**唯一例外=中间门洞必须留空、不被任何物件遮挡**。围墙形态、门扇比例与帐篷尺寸的现行口径归 [art-bible](../../art/art-bible.md) §6.2,本 spec 不再复述。
坐标草案与分层验收判据见 [camp-scene-prompts](../../art/camp-scene-prompts.md) §2.3/§4(10 张口径现行事实源);2026-10-22 美术方案讨论留痕见 [提案](../../../reports/提案-营地场景图美术生成方案-2026-10-22.md)。
## 点选反馈(根态)
+1 -1
View File
@@ -81,7 +81,7 @@ MVP 未使用标准控件(不入库):toggle / dropdown / input field / tab
**Category**: Data Display
**Used In**: 战斗画面区底部(结算期第一注视点)
**Description**: 轮末结算逐行流水实时呈现 + 可回看——「扫雷之因逐行兑现为战果」(P2 归因)的承载面。挖掘期收起为 1 行上轮摘要;结算期展开 84px 实时流;点击开完整回看 overlay。
**Description**: 轮末结算逐行流水实时呈现 + 可回看——「扫雷之因逐行兑现为战果」(P2 归因)的承载面。挖掘期收起为 1 行上轮摘要;结算期展开为实时流(带高读 `log_band_h_settle`);点击开完整回看 overlay。
**Specification**:
- 数据源:[战斗结算](../systems/战斗循环/战斗结算.md#明细流水每一行都能指回你刚才的哪一步棋)步骤流(9 类行封闭协议(现行);公共字段:来源类别 / 来源格 / 轮次 / 显示名 / 数值 / 可空高光标记);**无来源行 = 断言失败**
Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

+29 -12
View File
@@ -93,11 +93,29 @@ func request_dig(chain_size: int, clears_map: bool) -> bool:
_reject("phase")
return false
var cost := _cost("dig")
# dig_paid 发射+施加(付费开掘发起、动作经济闸收对价前:
# dig_paid 发射+施加(付费开掘发起、动作经济闸收对价前——施加单点 _apply_dig_paid_hits):
# counter_spend 命中=尝试加收对价并积计数;资源不足走 fallback/耗尽即止=只付常规基础对价。
# counter_add〔双管镐 付费挖格计数〕/ grant_ap〔每第 2 次返 1 AP——对价减免,
# mod_eq 闸在行 when〕同面施加)
var apply_res := _apply_dig_paid_hits(cost)
cost = int(apply_res["cost"])
if _ap < cost:
_reject("resource_face")
return false # 拒绝轮零入池(矿脉储能只在扣费受理后兑付——C2 成功才登记同款纪律)
if int(apply_res["refund"]) > 0:
cost = maxi(0, cost - int(apply_res["refund"])) # 返还=对价减免(净费不低于 0——免费开格不产生负对价)
var accepted := _spend_cost_and_arbitrate(cost, clears_map)
if accepted and int(apply_res["rap_gain"]) > 0:
grant_reserve_ap(int(apply_res["rap_gain"])) # 每次受理的付费挖格产 RAP(一次性入池——每动作恰一挖,连锁不二次)
return accepted
# dig_paid 命中施加单点(迭代 46 卡A T3 自 request_dig 迁出——分支本体逐字保留:
# counter_spend 加收对价并积计数/counter_add 施加〔双管镐 付费挖格计数〕/
# grant_ap 对价减免〔每第 2 次返 1 AP——mod_eq 闸在行 when〕/grant_reserve_ap 矿脉储能累计;
# 命中序=声明序不变。返回 {cost, refund, rap_gain} 交受理面消费)
func _apply_dig_paid_hits(base_cost: int) -> Dictionary:
var cost := base_cost
var dig_refund := 0
var rap_gain := 0
if _fx != null:
for h in _fx.dispatch(_ctx, "dig_paid", {"ap_cost": cost, "ap_surplus": _ap - cost}):
if String(h.get("kind", "")) == "counter_spend":
@@ -110,12 +128,9 @@ func request_dig(chain_size: int, clears_map: bool) -> bool:
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
elif String(h.get("kind", "")) == "grant_ap":
dig_refund += int(h.get("value", 0))
if _ap < cost:
_reject("resource_face")
return false
if dig_refund > 0:
cost = maxi(0, cost - dig_refund) # 返还=对价减免(净费不低于 0——免费开格不产生负对价)
return _spend_cost_and_arbitrate(cost, clears_map)
elif String(h.get("kind", "")) == "grant_reserve_ap":
rap_gain += int(h.get("value", 0))
return {"cost": cost, "refund": dig_refund, "rap_gain": rap_gain}
func _spend_cost_and_arbitrate(cost: int, clears_map: bool) -> bool:
@@ -196,13 +211,15 @@ func finish_round_stop(transfer: bool, transfer_pct: int = 100) -> void:
# 轮内刷新:唯一入口 =
# Active 相位——仅扣 RAP,相位与 AP 均不动。刷新不打断轮次、不触发结算;换图编排归 战斗结算
# (retire + install 立即执行,玩家同轮继续挖新图)。决策主体 = 玩家(经 HUD框架发起)纪律不变。
func request_refresh() -> bool:
# cost_override=折后价入参(迭代 46 卡A T3——BattleRefreshFlow 折价读数传入;缺省 -1=表基线逐位不变)
func request_refresh(cost_override: int = -1) -> bool:
if _phase != Phase.ACTIVE:
_reject("phase")
return false
if _rap < db.run.action_economy.rap_refresh_cost:
var cost := int(db.run.action_economy.rap_refresh_cost) if cost_override < 0 else cost_override
if _rap < cost:
return false # EC13 资源不足拒绝——合法相位内资源拒绝,非编排错误
_rap -= db.run.action_economy.rap_refresh_cost
_rap -= cost
return true
+61 -231
View File
@@ -27,19 +27,14 @@ var levels_total := 0 # 序列总关数(读 level 表行
var master_seed := 0 # 本局主种子(摘要第五件事)
var summary := {} # 最近一次结算的冒险摘要(idle 后保留——重开才新生成)
var _runner: LevelRunner # 当前关卡行进器(每关新建——关卡域状态天然隔离)
var _pending_next_level := false # 大奖励已兑付、等 continue_to_next_level 确认
var _last_level_payload := {} # 最近一次关卡结束载荷(失败结算的败退节点明细来源——数值验证取数面)
var _last_treasure_cleared := 0 # additive(最近一次关卡末清空计数,summary 取数面)
var _treasure_unlocked := {} # aux 解锁门控集(params.treasure_unlocked 接收面)
var _attunement: Array[Variant] = [] # 同调装配集(params.attunement 接收面,start_level 外下传 LevelRunner)
var _save_manager = null # 断点承载注入(null = 不写断点;模拟/冒烟缺省)
var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递)
var _loadout_seeded := false # 同调行已种标记(每冒险一次——首关 bind 时点投行;行跨关存活后续关零动作)
# ---- 三选一停点(编排迁 CurioStop 件——停点三态/刷新三源/凭证出箱/信号面)----
var _curio: CurioDeck = null # 令牌池件(_init 建;纯池——多重集/抽签/cap/凭证判定唯一宿主)
var _stop: CurioStop = null # 停点件(_init 建;每冒险一次——关卡换实例不换件)
var _refresh_bank: RefreshBank = null # 刷新账件(_init 建——三源账与付费报价;宿主预算注入与断点片段直收)
var _loadout: AttuneLoadout = null # 同调配装件(_init 建;充能单源化——物化行唯一事实源)
var _bp_store: BreakpointStore # 断点组装与回放件(_init 建;六成员+27 键登记单源+读写侧机组随件——宿主读写经件)
var adventure_inventory: TreasureItems # 冒险级秘宝库存(单源化——同调未用行跨关存活的承载面;关卡域语义归 level 使用面)
var _tree_rules: Array[Variant] = [] # 树 rule 键集(Fork 激活侧 effects 透传;下传 enc.tree_rules 消费面)
var _shop_mode := "legacy" # 商店模式("legacy"=固定三件加定价〔off 对照承载,缺省零扰动〕/"pool"=共享池抽 3 买 1 重抽;真实入口 main.gd 显式投 pool)
@@ -57,6 +52,8 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
_snapshot_breakpoint)
_loadout = AttuneLoadout.new(db, _attunement)
adventure_inventory = TreasureItems.new(db)
_bp_store = BreakpointStore.new({"db": db, "rng": rng, "stats": _stats, "wallet": _wallet,
"journal": _journal, "curio": _curio, "stop": _stop, "bank": _refresh_bank, "loadout": _loadout})
# 件出口(组件交出非委托壳——呈现/测量面取主角账三件与令牌池单点;领域拆分批接口块)
@@ -107,7 +104,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
_reset_curio_state()
levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数)
level_index = 1
_pending_next_level = false
_bp_store._pending_next_level = false
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 缺省 legacy(off 对照零扰动)
_treasure_unlocked = params.get("treasure_unlocked", {}) if params.get("treasure_unlocked", {}) is Dictionary else {}
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive
@@ -115,7 +112,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
_refresh_bank.set_refresh_y(maxi(0, int(params.get("attune_refresh_y", 0)))) # additive(Y 冒险周期池)
_loadout.set_attunement(_attunement) # 装配集注入(活引用——种行面)
adventure_inventory.reset() # 冒险级库存重置(新局空库存)
_loadout_seeded = false # 同调行投行延至首关 bind 时点(库存就位后)
_bp_store._loadout_seeded = false # 同调行投行延至首关 bind 时点(库存就位后)
adventure_counters = {} # 冒险桶清零(效果引擎 adv_ 计数器宿主——冒险开始归零)
# 刷新练习开局注入(refresh_stock_grant 装配即得一次性加仓:
# 装配集 kind 分发扫描;只入存量账、临时账不动=V8 契约;resume 不重注入〔存量随断点已在〕)
@@ -152,99 +149,56 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
return false
master_seed = int(bp.get("master_seed", 0))
rng.restore_adventure(master_seed, int(bp.get("rng_counter", 0)))
_resume_ledger(bp, params)
_resume_curio_state(bp)
_resume_params(bp, params)
_resume_counters(bp)
# ---- 断点回放(R2 整族归件——件侧语句入 _bp_store、宿主侧语句按原语句序留本壳,序逐步保形)----
_bp_store.restore_ledger(bp, params) # 续玩账面回填——主角账三件片段各还原各键(件侧;clamp 类夹 ≥0;日记在档=推导比对载入、缺键=旧档汇总直载)
summary = {}
levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数)
level_index = int(bp.get("level_index", 1))
_bp_store._pending_next_level = false
_reset_curio_state() # 续玩三选一态回填前置清理(池与件计数归件、停点态归本类;桶归宿主字典)
_bp_store.restore_curio_state(bp) # 续玩三选一态回填(池余库/获取计数/特性持有集——件侧;dual_merge/keep_on_null 策略见 BREAKPOINT_FIELDS)
_bp_store.apply_resume_params(params) # 续玩参数面——数值账规范化与越界钳制(件侧)
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 续玩参数面同 start_adventure(缺省 legacy)
_treasure_unlocked = params.get("treasure_unlocked", {}) if params.get("treasure_unlocked", {}) is Dictionary else {}
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive
_refresh_bank.set_refresh_x_cap(_attune_refresh_x_total()) # 刷新账 X 基数预算注入(续玩参数面同 start)
_bp_store.restore_stop_bank_fragments(bp, params) # 停点件/刷新账片段回填(件侧——凭证计数;刷新存量/Y 池缺键回退参数面)
_tree_rules = params.get("tree_rules", []) if params.get("tree_rules", []) is Array else [] # A/B 面 rule 键集(续玩参数面同 start_adventure 同式,消费面 set_tree_rules 下传)
# 续玩计数器回填(adventure 域计数——缺键 = 旧版断点防御空集;_reset_curio_state 已清零在先——桶归宿主字典)
var bp_counters = _bp_store._bp_read("adventure_counters", bp)
if bp_counters is Dictionary:
adventure_counters = (bp_counters as Dictionary).duplicate(true)
return _resume_pending_or_start(bp)
# 续玩收束段(宿主侧——停点直回/已兑付重发/无停点续玩起步;R2 回放段的尾半,语句序与
# resume_adventure 内联时逐句相同):三选一停点态与待推进位经件读写,选路停点快照单跳传递。
func _resume_pending_or_start(bp: Dictionary) -> bool:
var bp_pending: Dictionary[Variant, Variant] = bp.get("bonus_pending", {}) as Dictionary
var bp_result: Dictionary[Variant, Variant] = bp.get("bonus_result", {}) as Dictionary
if not bp_pending.is_empty():
_pending_next_level = true
_bp_store._pending_next_level = true
_state = State.RUNNING
_stop.restore_pending(bp_pending) # 选择停点直回(续玩不重跑关卡——信号经停点件中继)
return true
if not bp_result.is_empty():
_pending_next_level = true
_bp_store._pending_next_level = true
_stop.restore_resolved(bp_result) # 已兑付不重兑付只重发(置态+重发收编一步——信号面归停点件)
_state = State.RUNNING
return true
_resume_bp = bp.duplicate(true)
_bp_store._resume_bp = bp.duplicate(true)
# 充能读侧以行集为源(键值仅校验读数——回填时点在首关 bind 行集就位后,经 _loadout_on_level_inventory)
_loadout_seeded = false
_bp_store._loadout_seeded = false
_loadout.set_attunement(_attunement)
adventure_inventory.reset()
_state = State.RUNNING
return _start_current_level()
# 续玩账面回填(生命/特性/钱包/加算区/已购档/钥石/逐杀明细/真视——主角账三件片段各还原各键;
# bonus 子键 clamp 类夹 ≥0;日记在档=推导比对载入、缺键=旧档汇总直载)
func _resume_ledger(bp: Dictionary, params: Dictionary) -> void:
_stats.restore_fragment(bp, params)
_wallet.restore_fragment(bp, params)
_journal.restore_fragment(bp, params)
summary = {}
levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数)
level_index = int(bp.get("level_index", 1))
_pending_next_level = false
# 续玩三选一态回填(池与件计数归件、停点态归本类;dual_merge/keep_on_null 策略见 BREAKPOINT_FIELDS)
func _resume_curio_state(bp: Dictionary) -> void:
_reset_curio_state()
var bp_taken: Dictionary[Variant, Variant] = bp.get("bonus_taken", {}) as Dictionary
for id in _curio.taken.keys():
_curio.taken[id] = maxi(0, int(bp_taken.get(id, 0)))
for id in bp_taken.keys():
_curio.taken[String(id)] = maxi(0, int(bp_taken.get(id, 0))) # 身份维键(relic:<feat_id>)随快照还原
# 令牌池余库还原(TP0 v2.2——快照缺键 = 旧版断点,保持满库重建态:旧档只读一版周期的防御口径;
# relic 令牌 feat_id 随还原)
var bp_deck = _bp_read("bonus_deck", bp)
if bp_deck is Array:
var restored: Array[Variant] = []
for t in (bp_deck as Array):
var tok: Dictionary[Variant, Variant] = t as Dictionary
var rtok := {"kind": String(tok.get("kind", "")), "tier": String(tok.get("tier", ""))}
if String(rtok["kind"]) == "relic":
rtok["feat_id"] = String(tok.get("feat_id", "")) # relic 令牌 feat_id 随还原
restored.append(rtok)
_curio.deck = restored
# 特性持有集还原(缺键 = 旧版断点防御空集——主角数值账字段单点)
_stats.feats = []
for r in (bp.get("relics", []) as Array):
_stats.feats.append(String(r))
# 续玩参数面回填(同 start_adventure 参数面;生命账本越界钳制在此落——clamp 类)
func _resume_params(bp: Dictionary, params: Dictionary) -> void:
_stats.rebase_override(params.get("hero_override", {})) # 续玩参数面同 start_adventure(规范化归数值账)
_stats.attack_type = String(params.get("attack_type", "")) # 续玩参数面同 start_adventure(账号层选中态随 resume 重注入)
_stats.armor_type = String(params.get("armor_type", ""))
# 生命账本越界钳制(T17——spawn 守卫 EC16 免疫):断点可能带出高于生效上限的账本值
#(如调试浮窗调高期写出的值,实例:hero_hp=28 > 基础 26)——恢复后钳到生效上限,
# 否则首战 spawn「hero_hp 入参越界」拒收 → 「敌人系统 spawn 被拒」fatal。钳制式单点归
# 数值账 clamp_hp_to_resume_cap(保守口径不含加算区——与原钳制式逐字同)。
_stats.clamp_hp_to_resume_cap(int(db.combat.hero.max_hp))
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 续玩参数面同 start_adventure(缺省 legacy)
_treasure_unlocked = params.get("treasure_unlocked", {}) if params.get("treasure_unlocked", {}) is Dictionary else {}
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive
_refresh_bank.set_refresh_x_cap(_attune_refresh_x_total()) # 刷新账 X 基数预算注入(续玩参数面同 start)
_stop.restore_fragment(bp, params) # 停点件片段回填(凭证计数)
_refresh_bank.restore_fragment(bp, params) # 刷新账片段回填(刷新存量/Y 池——Y 缺键回退参数面 params_fallback 类)
_tree_rules = params.get("tree_rules", []) if params.get("tree_rules", []) is Array else [] # A/B 面 rule 键集(续玩参数面同 start_adventure 同式,消费面 set_tree_rules 下传)
# 续玩计数器回填(冒险域计数——凭证计数/刷新存量/Y 池随停点件片段回填)
func _resume_counters(bp: Dictionary) -> void:
# adventure 域计数器回填(缺键 = 旧版断点防御空集;_reset_curio_state 已清零在先——桶归宿主字典)
var bp_counters = _bp_read("adventure_counters", bp)
if bp_counters is Dictionary:
adventure_counters = (bp_counters as Dictionary).duplicate(true)
func _reset_curio_state() -> void:
_curio.reset() # 池与件获取计数(池半归件;停点态归停点件下行清理)
_stop.reset_adventure_state()
_loadout_seeded = false
_bp_store._loadout_seeded = false
_stats.feats = []
adventure_counters = {}
@@ -263,7 +217,7 @@ func effective_max_hp() -> int:
# 断点承载注入(组合根冒险开始时调用;模拟/冒烟不注入 = 断点面整体静默)
func set_save_manager(sm: SaveManagerCore) -> void:
_save_manager = sm
_bp_store._save_manager = sm
# 当前关卡行进器读面(呈现层/冒烟用——每关新建实例,调用方须在 level_started 后重取)
@@ -278,13 +232,13 @@ func apply_rest(pct: int, hp: int, max_hp: int) -> int:
# 大奖励确认 → 进下一关(仅兑付后受理——零自动推进的冒险级停点;三选一未兑付拒绝推进)
func continue_to_next_level() -> bool:
if not _pending_next_level:
if not _bp_store._pending_next_level:
push_error("[冒险结构] continue_to_next_level 拒绝——无待确认的关卡推进")
return false
if _stop.curio_stop_pending():
push_error("[冒险结构] continue_to_next_level 拒绝——三选一停点未兑付(零自动推进契约)")
return false
_pending_next_level = false
_bp_store._pending_next_level = false
_stop.clear_resolved()
level_index += 1
return _start_current_level()
@@ -343,9 +297,9 @@ func _loadout_on_level_inventory() -> void:
var inv = _runner.treasure_inventory
if inv == null:
return
if not _loadout_seeded:
_loadout_seeded = true
var bp_charges: Variant = _bp_read("attunement_charges", _resume_bp, null) if not _resume_bp.is_empty() else null
if not _bp_store._loadout_seeded:
_bp_store._loadout_seeded = true
var bp_charges: Variant = _bp_store._bp_read("attunement_charges", _bp_store._resume_bp, null) if not _bp_store._resume_bp.is_empty() else null
if bp_charges is Dictionary:
_loadout.restore_fragment({"attunement_charges": bp_charges}, inv) # 旧档读侧:行集为源+键值校验读数
else:
@@ -368,8 +322,8 @@ func _loadout_on_level_inventory() -> void:
# apply_adventure_summary 逐关卡完成关次置位〕;末关 Boss 照常入账;明细随摘要 additive 键供结算面归因)
func _boss_keystone_drop(li: int) -> void:
var first := true
if _save_manager != null:
first = not _save_manager.meta_state.career_state().boss_first_kill.has(str(li))
if _bp_store._save_manager != null:
first = not _bp_store._save_manager.meta_state.career_state().boss_first_kill.has(str(li))
var row: Dictionary[Variant, Variant] = db.crossrun.meta.boss_keystone_row(li)
var amount: int = int(row.get("first_kill", 0)) if first else int(row.get("repeat_kill", 0))
_journal.record_keystone_drop({"level": li, "first": first, "amount": amount}) # 逐杀明细+累计单写原语
@@ -419,9 +373,9 @@ func _start_current_level() -> bool:
# 断点续玩分支:图由主种子+参数档重生成同图(产图与计数器号位无关,停点零重抽语义随之消解),
# resume_state 带停点句柄与已到序——重建到停点后经 present_resumed_choice 呈现(信号顺序契约同入口)。
var resume_state: Dictionary[Variant, Variant] = {}
if not _resume_bp.is_empty():
resume_state = {"node_id": int(_bp_read("node_id", _resume_bp, -1)),
"visited_ids": _bp_read("visited_ids", _resume_bp, []) as Array}
if not _bp_store._resume_bp.is_empty():
resume_state = {"node_id": int(_bp_store._bp_read("node_id", _bp_store._resume_bp, -1)),
"visited_ids": _bp_store._bp_read("visited_ids", _bp_store._resume_bp, []) as Array}
# 接口①开始关卡:生命经主角账共享交接(账面当前值即开局生命——冒险开始置满 / 大奖励回满已写好);
# 主角账三件与冒险桶注入(构造注入 ctx——拆件批 T3 收口形态)。
# defer_entry=true 起每关(含首关)——先 emit level_started(呈现层在
@@ -431,7 +385,7 @@ func _start_current_level() -> bool:
if ok:
_emit_level_started()
_present_level_entry(resume_state)
_resume_bp = {}
_bp_store._resume_bp = {}
return ok
@@ -455,7 +409,7 @@ func _present_level_entry(resume_state: Dictionary) -> void:
if resume_state.is_empty() or int(resume_state.get("node_id", -1)) < 0:
_runner.present_entry_choice()
else:
_runner.restore_treasure_items(_bp_read("aux_items", _resume_bp, []) as Array)
_runner.restore_treasure_items(_bp_store._bp_read("aux_items", _bp_store._resume_bp, []) as Array)
_runner.present_resumed_choice()
@@ -464,7 +418,7 @@ func _on_level_finished(payload: Dictionary) -> void:
if state != "running":
push_error("[冒险结构] 重复关卡结束宣布拒绝——state=%s(账本与摘要零变化)" % state)
return
_last_level_payload = payload.duplicate(true)
_bp_store._last_level_payload = payload.duplicate(true)
var result := String(payload.get("result", ""))
if result == "defeat":
_stats.hero_hp = 0 # 倒下记 0(生命账本战后回写口径)
@@ -473,7 +427,7 @@ func _on_level_finished(payload: Dictionary) -> void:
if result != "victory":
push_error("[冒险结构] 关卡结束结果出域: %s" % result)
return
_last_treasure_cleared = int(payload.get("aux_cleared", 0)) # additive(末关 summary 取数面)
_bp_store._last_treasure_cleared = int(payload.get("aux_cleared", 0)) # additive(末关 summary 取数面)
# Boss 钥石逐杀掉落(击杀掉落渠道,末关照常入账;首杀/复杀读 meta.tres boss_keystone)
_boss_keystone_drop(level_index)
if level_index < levels_total:
@@ -481,7 +435,7 @@ func _on_level_finished(payload: Dictionary) -> void:
#(信号同步派发——处理器内立即确认时 _pending_next_level 必须已就位);
# 停点件内部分回血先行 → 加权洗牌掷 3 → 三选一停点(顺序即契约——回血按当刻账本与上限结算,
# 选择停点零自动推进)。载荷_base 随附钱包读数(停点件零钱包依赖——GP 件入账回调仍走 grant_curio_gp)
_pending_next_level = true
_bp_store._pending_next_level = true
var stop_base: Dictionary[Variant, Variant] = payload.duplicate(true)
stop_base["gp"] = _wallet.gp
_stop.open_level_clear(stop_base)
@@ -499,7 +453,7 @@ func _settle(result: String) -> void:
"stopped_at": 0 if result == "completed" else level_index,
"keystone_total": _journal.keystone_gained(),
"master_seed": master_seed,
"aux_cleared_last": _last_treasure_cleared, # additive 扩签(末关清空反馈;非末关走 bonus 载荷)
"aux_cleared_last": _bp_store._last_treasure_cleared, # additive 扩签(末关清空反馈;非末关走 bonus 载荷)
"keystone_drops": _journal.keystone_drop_log(), # additive 扩签(逐杀掉落明细;settle 入账明细/复盘归因数据源)
"vision_used_delta": _journal.vision_used(), # additive 扩签(真视使用本局计数;settle 聚合入档)
"pool_stock": _pool_stock_snapshot(), # additive 扩签(终局池读数:清账前快照,商店买走 vs 奖励领走 vs 枯竭的终局面)
@@ -507,19 +461,19 @@ func _settle(result: String) -> void:
}
# 败退明细(数值验证工具取数面——tools/balance_verifier.py 止步节点分布数据源;
# 仅失败局附加,键缺省 = 未败退,既有五件事消费方零感知)
if result == "failed" and not _last_level_payload.is_empty():
if result == "failed" and not _bp_store._last_level_payload.is_empty():
summary["defeat"] = {
"level_index": level_index,
"node_id": int(_last_level_payload.get("node_id", -1)),
"node_type": String(_last_level_payload.get("node_type", "")),
"node_id": int(_bp_store._last_level_payload.get("node_id", -1)),
"node_type": String(_bp_store._last_level_payload.get("node_type", "")),
}
if _runner != null:
summary["gp_final"] = int(_runner.gp) # gp_final 清账前快照(终局面板取数源=runner 钱包单点)
_runner.close_wallet() # GP 冒险收束清零(三结局都清——经转发发 wallet_changed "clear" 供日志/呈现)
_reset_curio_state() # 三选一停点态收束清零(结算受理只算一次语义——重复宣布零变化)
if _save_manager != null:
_save_manager.clear_breakpoint() # 三结局清断点(退出冒险=放弃流程+清除断点)
_save_manager.write_to_disk()
if _bp_store._save_manager != null:
_bp_store._save_manager.clear_breakpoint() # 三结局清断点(退出冒险=放弃流程+清除断点)
_bp_store._save_manager.write_to_disk()
_state = State.SETTLING
adventure_finished.emit(summary)
_state = State.IDLE
@@ -530,131 +484,7 @@ func _on_level_choices_presented(_candidates: Array) -> void:
_snapshot_breakpoint()
# ---- 断点快照(字段登记单源:写侧拼装与读侧回退策略同登记一表)----
# 键序即快照键序(27 顶层键——写侧/读侧/键集断言三面同源)。读侧回退策略词表:
# plain 无特殊回退面(缺键取代码内缺省)/clamp 数值夹 ≥0/keep_on_null 缺键或类型不符保持既有态
# dual_merge 双向合并(bonus_taken)/params_fallback 缺键回退参数面(attune_refresh_y)
# token_migration 旧版候选形态迁移(bonus_pending.candidates)
# 注入缺省 null = 整面静默(模拟/冒烟零扰动)。
const BREAKPOINT_FIELDS := {
"level_index": {"strategy": "plain"},
"node_id": {"strategy": "plain"},
"hero_hp": {"strategy": "clamp"},
"bonus": {"strategy": "clamp"},
"gp": {"strategy": "plain"},
"shop_purchased": {"strategy": "plain"},
"shop_shelf": {"strategy": "plain"},
"shop_visit_buys": {"strategy": "plain"},
"attunement_charges": {"strategy": "keep_on_null"},
"graph_gen_v2": {"strategy": "plain"},
"master_seed": {"strategy": "plain"},
"rng_counter": {"strategy": "plain"},
"keystone": {"strategy": "plain"},
"visited_ids": {"strategy": "plain"},
"aux_items": {"strategy": "plain"},
"bonus_taken": {"strategy": "dual_merge"},
"bonus_deck": {"strategy": "keep_on_null"},
"relics": {"strategy": "plain"},
"bonus_pending": {"strategy": "token_migration"},
"feat_ticket_pity": {"strategy": "plain"},
"feat_ticket_sold": {"strategy": "plain"},
"adventure_counters": {"strategy": "keep_on_null"},
"vision_counter_used": {"strategy": "clamp"},
"refresh_stock": {"strategy": "clamp"},
"attune_refresh_y": {"strategy": "params_fallback"},
"bonus_result": {"strategy": "plain"},
"keystone_drops": {"strategy": "plain"},
"gain_journal": {"strategy": "plain"}, # 获得日记(拆件批 T4 扩列——唯一声明的断点扩展;旧档缺键=汇总直载跳过比对)
}
# ---- 断点快照委托(组装与读侧策略机组归 breakpoint_store 件——六成员+27 键登记单源随件;
# 本壳一行转发:CurioStop 构造注入的 Callable 目标与本类 _on_level_choices_presented 调用点零改)----
func _snapshot_breakpoint() -> void:
if _save_manager == null or state != "running":
return
var parts: Dictionary[Variant, Variant] = _bp_parts()
var bp: Dictionary[Variant, Variant] = {}
for key in BREAKPOINT_FIELDS.keys():
bp[key] = parts[key]
_save_manager.set_breakpoint(bp)
_save_manager.write_to_disk()
# 断点写侧取值(键集以 BREAKPOINT_FIELDS 为准——未登记键即取值越界,fail-fast)
func _bp_parts() -> Dictionary[Variant, Variant]:
var stats_frag: Dictionary[Variant, Variant] = _stats.breakpoint_fragment()
var wallet_frag: Dictionary[Variant, Variant] = _wallet.breakpoint_fragment()
var journal_frag: Dictionary[Variant, Variant] = _journal.breakpoint_fragment()
var bank_frag: Dictionary[Variant, Variant] = _refresh_bank.breakpoint_fragment()
var stop_frag: Dictionary[Variant, Variant] = _stop.breakpoint_fragment()
var level_frag: Dictionary[Variant, Variant] = _runner.breakpoint_fragment() if _runner != null else {
"node_id": -1, "visited_ids": [] as Array, "aux_items": [] as Array,
"shop_shelf": [] as Array, "shop_visit_buys": 0} # 三选一停点续玩分支 _runner 为 null——节点域按缺省落档
# 该两态的续玩只消费 bonus_* 附随件(选择停点/已兑付停点快照)
var parts: Dictionary[Variant, Variant] = {
"level_index": level_index,
"node_id": int(level_frag.get("node_id", -1)),
"hero_hp": int(stats_frag.get("hero_hp", 0)),
"bonus": journal_frag.get("bonus", {}),
"gp": int(wallet_frag.get("gp", 0)),
"shop_purchased": journal_frag.get("shop_purchased", {}),
"shop_shelf": level_frag.get("shop_shelf", []), # 货架入断点(离开店/非店停点 = 空架合法态)
"shop_visit_buys": int(level_frag.get("shop_visit_buys", 0)), # 本次进店已购次数(A 链定价输入)
"attunement_charges": (_loadout.breakpoint_fragment(_runner.treasure_inventory) if _runner != null and _runner.treasure_inventory != null else {"attunement_charges": {}}).get("attunement_charges", {}), # 同调主动件充能余量随断点(行派生——save v4 域键名策略不变)
"graph_gen_v2": 1, # 生成图换代代差标记(缺键 = 换代前旧断点 → resume 显式拒绝)
"master_seed": master_seed,
"rng_counter": rng.counter(),
"keystone": int(journal_frag.get("keystone", 0)),
"visited_ids": level_frag.get("visited_ids", []),
"aux_items": level_frag.get("aux_items", []),
"bonus_taken": _curio.taken.duplicate(true), # 三选一件获取次数(kind 维度派生账本——断点附随件与读数承载)
"bonus_deck": _curio.deck.duplicate(true), # 令牌池余库(TP0 v2.2——池消耗即上限,续玩余库逐枚还原)
"relics": stats_frag.get("relics", []), # 特性持有集(局内随机渠道获得态续玩防丢失)
"bonus_pending": stop_frag.get("bonus_pending", {}), # 三选一停点态(空 = 无停点——选择停点可中断契约)
"feat_ticket_pity": int(stop_frag.get("feat_ticket_pity", 0)), # T5 专长凭证保底计数(扩列向后兼容——旧档缺键缺省 0)
"feat_ticket_sold": int(stop_frag.get("feat_ticket_sold", 0)), # T5 专长凭证限购计数(同上)
"adventure_counters": adventure_counters.duplicate(true), # adventure 域计数器随断点(续玩跨战账不丢——桶归宿主字典)
"vision_counter_used": int(journal_frag.get("vision_counter_used", 0)), # 真视使用计数随断点(续玩跨战账不丢)
"refresh_stock": int(bank_frag.get("refresh_stock", 0)), # 刷新存量账随断点
"attune_refresh_y": int(bank_frag.get("attune_refresh_y", 0)), # Y 冒险周期池余量随断点(扩列向后兼容——旧档缺键回退参数面)
"bonus_result": stop_frag.get("bonus_result", {}), # 已兑付待推进态(空 = 无——中断续玩不重兑付)
"keystone_drops": journal_frag.get("keystone_drops", []), # 逐杀掉落明细(摘要归因续接)
"gain_journal": journal_frag.get("gain_journal", {}), # 获得日记(唯一声明的断点扩列——日记为现行事实源)
}
# 拼盘完整性自断言(fail-fast——片段并集与登记键集不符即中止落盘)
var stray: Array[Variant] = []
for key in parts.keys():
if not BREAKPOINT_FIELDS.has(key):
stray.append(key)
if not stray.is_empty():
push_error("[冒险结构] 断点拼盘含未登记键: %s(fail-fast——片段与登记单源失同步)" % str(stray))
return parts
# 读侧单点(BREAKPOINT_FIELDS 策略标签分发;未登记键 fail-fast 不静默按 plain 兜)。
# 二次消费面(_start_current_level 经 _resume_bp 取 node_id/visited_ids/aux_items)同走此口。
func _bp_read(key: String, bp: Dictionary, fallback: Variant = null) -> Variant:
if not BREAKPOINT_FIELDS.has(key):
push_error("[冒险结构] 断点字段未登记(读侧): %s" % key)
var v: Variant = bp.get(key, fallback)
if _bp_strategy(key) == "keep_on_null" and v != null and not (v is Dictionary or v is Array):
return null # 类型不符——保持既有态(调用方跳过覆盖)
return v
# 读侧策略标签(BREAKPOINT_FIELDS 读侧消费面单点)
func _bp_strategy(key: String) -> String:
return String((BREAKPOINT_FIELDS.get(key, {}) as Dictionary).get("strategy", "plain"))
# 数值类读侧(clamp 夹 ≥0 / params_fallback 断点缺键回退参数面整额注入 / plain 直取)
func _bp_read_int(key: String, bp: Dictionary, fallback: int, params: Dictionary = {}) -> int:
match _bp_strategy(key):
"clamp":
return maxi(0, int(bp.get(key, fallback)))
"params_fallback":
return maxi(0, int(bp.get(key, int(params.get(key, fallback)))))
_:
return int(bp.get(key, fallback))
_bp_store.snapshot(state, level_index, master_seed, _runner, adventure_counters)
+48
View File
@@ -0,0 +1,48 @@
## 战斗事实件:战斗事实的唯一记录面——本战受理事实(encounter:打什么、带什么进来)、
## 同调装配事实(attunement)、轮 facts 缓冲(reveals/seals/path)、封印判定账(去重两集+完美三账)、
## 装板溯源(seq/provenance)、条件事实计数(refresh_used、feat_first_dig_used、attune_safe_seen)。
## 零注入引用、零信号、零协作对象调用——纯数据+谓词+写口;缓冲写入点在宿主 A2 处理器,
## 身份快照捕获留宿主(fact 件不持协作引用,防它变成第二编排面)。
## 完美账写口单点=三 mark 方法(契约 2:perfect_hit_false 保持纯净,豁免只累加 perfect_forgiven);
## 其余成员公有直读写(段类/快照/宿主经 oc._facts.X——fx_host._fx_bonus_delta 跨件直读写同款形态)。
## reset_round 只清 reveals/seals/path 三项、不碰 refresh_used(_battle_over 后快照读到的
## refresh_used 必须保持末轮值)。
## 成员冻结:15/15 贴顶——实现期禁增员;扩容阀=先把封印判定账五成员二次拆出(SealLedger)再落新账。
class_name BattleFacts
extends RefCounted
var encounter: Dictionary[String, Variant] = {} # 本战受理事实(begin_battle assign 后全生命期只读)
var reveals: Array[Dictionary] = [] # 轮 facts 缓冲(cell_revealed 批)
var seals: Array[Dictionary] = [] # 封印判定批缓冲(seal_judged 批)
var path := "" # 轮末路径事实(round_end 载荷 path_tag)
var refresh_used := 0 # 本轮刷新计数(复位点=出口分发,语句随迁)
var board_seq := 0 # 板序号(本战斗第几张板——快照归因用)
var board_from_midround := false # 装板 provenance:轮内刷新换来的图=刷新图(清图奖励不发)
var seal_consumed := {} # 封印兑付资格账(危险格实例为键;install 重置)
var seal_miss_seen := {} # 封印未中行去重(每安全格实例一次)
var perfect_hit_true := 0 # 整图完美封印计数(install 重置)
var perfect_hit_false := 0 # 完美误标计数(纯净禁回退改写——契约 2)
var perfect_forgiven := 0 # 完美保险豁免累计(单向累加自有账)
var feat_first_dig_used := false # 条件事实计数(魔矢每战首次)
var attunement: Array[Variant] = [] # 本战装配集快照(enc.attunement 受理携带;受理后只读)
var attune_safe_seen := 0 # 积点制件安全开格计数(装配集自动兑付进度)
# 轮复位(原宿主 _reset_round_facts 逐字——三调用点:begin_battle 首板 install 后/出口分发/终局序列)
func reset_round() -> void:
reveals.clear()
seals.clear()
path = ""
# 完美封印三账写口(授予段直写升级为账本单点——契约 2 执行点)
func mark_perfect_hit() -> void:
perfect_hit_true += 1
func mark_perfect_miss() -> void:
perfect_hit_false += 1
func mark_perfect_forgiven() -> void:
perfect_forgiven += 1
+1
View File
@@ -0,0 +1 @@
uid://bo0eolh0vmvgb
+95 -208
View File
@@ -3,7 +3,7 @@
## 效果引擎施加面归 BattleFxHost、奖励授予管线归 RewardGrant(两拆件,经组件出口 fx_host()/
## reward_grant() 取件,禁链式穿透其内部);本类保留宿主职责面=状态机与生命周期、轮 facts 缓冲、
## A2 三事件订阅、五段驱动、attune 族、行发射基建(_emit_row/_assert_row_source/_fatal)、呈现投影、刷新面。
## 零 await;随机只经 rng 封闭流(encounter/behavior_roll/armor_roll/gp_drop)——同种子同输入逐位复现。
## 零 await;随机只经 rng 封闭流(_facts.encounter/behavior_roll/armor_roll/gp_drop)——同种子同输入逐位复现。
## 呈现层读取唯一出口 = presentation_snapshot()/diagnostic_snapshot(),禁链式穿透内部协作对象。
## 伤害只向 DamageController 开单(何时开单归编排、怎么算归控制器);刷新唯一通道 player_request_refresh,禁 RAP 满自动刷。
class_name BattleOrchestrator
@@ -31,9 +31,12 @@ signal settlement_snapshot(snap: Dictionary) ## 每结算末尾的全量状态
const SOURCE_EXEMPT_TYPES := ["decay", "verdict"]
# 行来源断言豁免集合(来源类别层)——事实来源是单位 / 清图而非棋盘格;
# fx_reveal=专长揭示掷格(落空行无真实格);
# fx_grant_gp=专长·GP 入账(钱包面而非棋盘面,行无格子可指)
const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "fx_reveal", "fx_grant_gp"]
# fx_grant_gp=专长·GP 入账(钱包面而非棋盘面,行无格子可指);
# fx_board_refresh=成功刷新事实族(迭代 46 卡A——崩壁冲击群伤行源=换图动作而非棋盘格)
const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "fx_reveal", "fx_grant_gp", "fx_board_refresh"]
var _facts: BattleFacts # 战斗事实件(battle_facts.gd——事实族 15 成员单点)
var _rounds: RoundCycle # 轮循环驱动件(settlement/round_cycle.gd——五段持有/段序/重入闸/摇号)
var db: ConfigDb
var rng: RngServiceCore
# E1 封装收口:协作对象 enemy_system/status_effects/reward_economy 私有化——跨层读取一律经 presentation_snapshot()/diagnostic_snapshot() 成文出口。
@@ -45,60 +48,43 @@ var action_economy: ActionEconomy
var _enemy_system: UnitContainer
var _status_effects: StatusContainer
var _reward_economy: RewardEconomy
var treasure_inventory: TreasureItems = null:
set(v):
treasure_inventory = v
if _reward_grant != null:
_reward_grant.treasure_inventory = v # 库存注入转发(注入时序在宿主构造之后——外部赋值路径零改址)
var _damage: DamageController # 伤害控制器(单据唯一兑付管线)
var _fx_host: BattleFxHost # 效果引擎宿主件(fx 族持有与兑付——拆件定稿轴件一)
var _reward_grant: RewardGrant # 奖励授予件(授予管线与秘宝使用——拆件定稿轴件二)
var encounter: Dictionary[String, Variant] = {}
var round_index := 0
var battle_result := "" # "" / "victory" / "defeat" / "fatal_error"
var _facts_reveals: Array[Dictionary] = []
var _facts_seals: Array[Dictionary] = []
var _facts_path := ""
var _settling := false
# 结算五段 Segment(段边界=审计五段,段序骨架冻结于
# SettlementTable.segment_order 语义:授予→增益/减益→战斗→衰减→判定。本类做段序编排+
# 行发射/开单原语单点——段体逻辑归 game/core/settlement/ 五类)
var _grant_seg = null # GrantSegment(①授予段——含封印判与 counter/grant 助手单点)
var _aura_seg = null # AuraSegment(②增益/减益段)
var _combat_seg = null # CombatSegment(③战斗段)
var _decay_seg = null # DecaySegment(④衰减段)
var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归本类)
var _refresh_used_this_round := 0 # 每轮刷新计数(Active 相位合计,兜底上限 = ActionEconomyTable.refresh_cap_per_round)
var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用)
var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换来的图 = true(清图奖励守卫——刷新图不发清图奖励)
var _seal_consumed := {} # 兑付资格以危险格实例为键(整图;换图重置)
# 同调装配运行态(begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动)
var attunement: Array[Variant] = [] # 装备中同调件 item_id 集
var _mines_hits: Dictionary[Variant, Variant] = {"total": 0, "last": 0, "round": 0} # 踩雷计数(触发条件事实面——本场累计/上轮转存/当轮累计三读数合一账)
# 转存时点=_roll_round_actions 轮末(轮内部分结算不更新 last);开战清零归 begin_battle
var _attune_safe_seen := 0 # 积点制件 battle 域安全开格计数(装配集 ap_charge_safe 行扫描;满 charge_n 自动兑付当轮 AP+1,无玩家发起入口)
# 同调装配运行态(begin_battle 经 enc 携带 attunement 键注入 facts;off=空集全部效果零扰动)
# 转存时点=roll_round_actions 轮末(轮内部分结算不更新 last);开战清零归 begin_battle
var nest_preview_count := -1 # 巢穴预告读数口(board_installed 命中落值;-1=未装配/未发射;呈现桥消费)
# 特性运行态(relic 持有集经 enc.feats 注入:效果行为=数据行 triggers 经
# 效果引擎分发;魔矢每战首次计数 battle 域。空集=零扰动)
var _feat_first_dig_used := false
# 加算日记读面(begin_battle 经 enc["journal"] 派生——encounter.assign 后全生命期可读;
# 主角攻击基值派生读(enc 携带 hero_atk 键受理后只读——E4 收口语义不变:表值运行期不可变,
# 派生值恒等;「读面即引用、无第二账」同款先例)
func _hero_atk() -> int:
return int(_facts.encounter.get("hero_atk", int(db.combat.hero.atk)))
# 加算日记读面(begin_battle 经 enc["journal"] 派生——encounter 受理 assign 后全生命期可读;
# null=调试/单场直构面,账面消费跳过。领域拆分批成员删除后的引用承接,读面即引用、无第二账)
func _journal() -> GainJournal:
return encounter.get("journal", null) if encounter.get("journal", null) is GainJournal else null
return _facts.encounter.get("journal", null) if _facts.encounter.get("journal", null) is GainJournal else null
# 同调配装件读面(begin_battle 经 enc["loadout"] 派生——充能单源化的 reset_cycle/consume_charge
# 消费面;null=直构零装配,充能面跳过)
func _attune_loadout() -> AttuneLoadout:
return encounter.get("loadout", null)
return _facts.encounter.get("loadout", null)
# 授予件战斗域回调束(bind_battle 注入——键集封闭三键:充能消耗/真视计数/负向踩雷计数;
# 宿主侧各转 _attune_loadout/_journal/_mines_hits 单点,授予件对账本零强引用)
# 宿主侧各转 _attune_loadout/_journal/_rounds.mines_hits 单点,授予件对账本零强引用)
func _battle_hooks() -> Dictionary[Variant, Variant]:
return {
"attune_consume": _hook_attune_consume,
@@ -108,8 +94,8 @@ func _battle_hooks() -> Dictionary[Variant, Variant]:
func _hook_attune_consume(item_id: String) -> void:
if _attune_loadout() != null and treasure_inventory != null:
_attune_loadout().consume_charge(item_id, treasure_inventory)
if _attune_loadout() != null and _reward_grant.treasure_inventory != null:
_attune_loadout().consume_charge(item_id, _reward_grant.treasure_inventory)
func _hook_vision_add() -> void:
@@ -120,8 +106,8 @@ func _hook_vision_add() -> void:
# 负向 blast(雷)踩雷计数入账(触发条件事实面——正向 spark_aoe/清图奖励不计入;
# 极性判定在授予件 _flat_blast 传点,本口只记账)
func _book_negative_blast() -> void:
_mines_hits["round"] += 1
_mines_hits["total"] += 1
_rounds.mines_hits["round"] += 1
_rounds.mines_hits["total"] += 1
# 效果引擎宿主侧事实单点(fx_host facts_cb 注入——round_index/battle_index/seals_count/nest_count/hp_ratio 五事实;charge_layers 归 fx_host 自答;未知事实回落 null)
@@ -136,10 +122,10 @@ func _fx_host_fact(fact: String) -> Variant:
"round_index":
return round_index
"battle_index":
return int(encounter.get("battle_index", 1)) # 关卡域战斗序(1 基——level_runner enc 携带;开局姿态窗条件面)
return int(_facts.encounter.get("battle_index", 1)) # 关卡域战斗序(1 基——level_runner enc 携带;开局姿态窗条件面)
"seals_count":
var hits_n := 0
for fevt in _facts_seals:
for fevt in _facts.seals:
if bool(fevt.get("hit", false)):
hits_n += 1
return hits_n
@@ -151,7 +137,7 @@ func _fx_host_fact(fact: String) -> Variant:
# attune 积点读投影(RewardGrant attune_safe_seen_cb 注入面——use_treasure 拒绝文案读数;经读投影出户防组件回调宿主私有族)
func _attune_safe_seen_read() -> int:
return _attune_safe_seen
return _facts.attune_safe_seen
# 组件出口(拆件后取件面——呈现与测试经此取件,禁链式穿透协作对象内部)
@@ -164,7 +150,7 @@ func reward_grant() -> RewardGrant:
func _attune_has(item_id: String) -> bool:
return attunement.has(item_id)
return _facts.attunement.has(item_id)
# 装配集只读出口(_attune_has 契约化出口,save v6 只读访问器先例。只读零副作用——
@@ -179,7 +165,7 @@ func attune_has(item_id: String) -> bool:
# kind 分发查询先例=_attune_refresh_x_total bonus_refresh。零装配=0)
func attune_param_sum(kind: String) -> int:
var total := 0
for iid_v in attunement:
for iid_v in _facts.attunement:
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v))
if (arow.get("effect_kinds", []) as Array).has(kind):
total += int(arow.get("magnitude", 0))
@@ -189,7 +175,7 @@ func attune_param_sum(kind: String) -> int:
# 装配集内 ap_charge_safe 行读取(取数源=装配集行扫描;
# 首行语义=装配集内至多一件积点制〔试炼镐〕)
func _attune_charge_safe_row() -> Dictionary:
for iid_v in attunement:
for iid_v in _facts.attunement:
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v))
if (arow.get("effect_kinds", []) as Array).has("ap_charge_safe"):
return arow
@@ -200,9 +186,9 @@ func _attune_charge_safe_row() -> Dictionary:
# (used handle 不回收=本轮已耗充能作废,补新 handle=「回满」;差额注入防双份=level_runner
# set_attune_charges 同构)。reset ∈ {battle, level}——adventure 周期归冒险开始种子面)
func _reseed_attune_charges(reset: String) -> void:
if _attune_loadout() == null or treasure_inventory == null:
if _attune_loadout() == null or _reward_grant.treasure_inventory == null:
return # 直构零装配/库存未就位——充能回满跳过(空装配同语义)
_attune_loadout().reset_cycle(reset, treasure_inventory) # 写时种行/补行(按周期筛——语义逐字承原循环体)
_attune_loadout().reset_cycle(reset, _reward_grant.treasure_inventory) # 写时种行/补行(按周期筛——语义逐字承原循环体)
# 积点制件进度读数(自动兑付无玩家发起入口,读数口=C 区秘宝槽悬停浮窗进度行:
@@ -213,18 +199,13 @@ func attune_convert_state() -> Dictionary:
if row.is_empty() or String(row.get("form", "")) != "active_charged":
return {"assembled": false, "points": 0, "need": 0, "display_name": "", "item_id": ""}
var cn := int(row.get("charge_n", 0))
return {"assembled": true, "points": _attune_safe_seen % cn if cn > 0 else 0, "need": cn,
return {"assembled": true, "points": _facts.attune_safe_seen % cn if cn > 0 else 0, "need": cn,
"display_name": String(row.get("display_name", "")), "item_id": String(row.get("item_id", ""))}
# 特性求值(效果行为=数据行 triggers 订阅,效果引擎 dispatch 分发;
# 流派档位质变=FeatsEngine 纯函数求值
# (档位机制本身系引擎基建非内容效果),两运行态注伤害控制器:档一 rider+1 / 档二 rider 可暴击)
var _seal_miss_seen := {} # 封印未中行去重(每安全格实例一次)
var _perfect_hit_true := 0 # 整图完美封印计数(install 重置)
var _perfect_hit_false := 0
var _perfect_forgiven := 0 # 完美保险豁免累计(单向累加自有账;契约 2 禁改写 _perfect_hit_false)
var _hero_atk := 0 # 主角攻击基值快照(encounter.hero_atk;E4 收口——表值运行期不可变)
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
@@ -234,6 +215,7 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
minesweeper = MinesweeperRules.new(db, action_economy)
_enemy_system = UnitContainer.new(db)
_status_effects = StatusContainer.new(db)
_facts = BattleFacts.new() # 战斗事实件(受理事实/轮缓冲/封印账/溯源/条件计数——成员迁移见设计 §4)
_reward_economy = RewardEconomy.new(db, rng)
_reward_economy.fatal_handler = _fatal # 协作件违约→本类 _fatal 封印(单点接线)
_damage = DamageController.new(db, rng, _enemy_system, _status_effects)
@@ -252,11 +234,8 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
minesweeper.round_end_board.connect(_on_round_end)
action_economy.round_end_stopped.connect(_on_round_end)
# 结算五段接线(授予/战斗两段持三引用=宿主+两协作件;信号连接纪律不涉段〔段零信号连接〕)
_grant_seg = GrantSegment.new(self, _fx_host, _reward_grant)
_aura_seg = AuraSegment.new(self)
_combat_seg = CombatSegment.new(self, _fx_host, _reward_grant)
_decay_seg = DecaySegment.new(self)
_verdict_seg = VerdictSegment.new(self)
# 结算五段接线随轮循环驱动件迁驻(round_cycle.gd——段件持有/段序/重入闸/摇号单点;构造序禁环不变)
_rounds = RoundCycle.new(self, _fx_host, _reward_grant)
# ---- 战斗开始序列(编排专章①:门 → 增益状态 → 敌人系统 → install+roll → 行动经济)----
@@ -269,7 +248,7 @@ func begin_battle(enc: Dictionary) -> bool:
return false
if not _install_and_reset():
return false
_reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险)
_facts.reset_round() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险)
_begin_battle_fx_state(enc)
_begin_battle_attune_params(enc)
_begin_battle_preview_economy(enc)
@@ -281,14 +260,14 @@ func begin_battle(enc: Dictionary) -> bool:
if not action_economy.notify_round_start():
_fatal("行动经济 首轮 notify_round_start 被拒")
return false
_roll_round_actions() # 开轮摇号(首轮同样摇;先于 round_started,挖掘期动画位可读)
_rounds.roll_round_actions() # 开轮摇号(首轮同样摇;先于 round_started,挖掘期动画位可读)
round_started.emit(round_index)
return true
# 受理段:encounter 装载+缓冲清零+护甲合流+重载闸+开域门+遭遇骰与敌人系统 spawn 受理门。
# 受理段:_facts.encounter 装载+缓冲清零+护甲合流+重载闸+开域门+遭遇骰与敌人系统 spawn 受理门。
func _begin_battle_accept(enc: Dictionary) -> bool:
encounter.assign(enc)
_facts.encounter.assign(enc)
_reward_grant.reset_battle() # 战斗域零 GP 持久(每场战斗缓冲从零起算;案C 闸复位)
_damage.set_hero_ac_override(int(enc.get("hero_armor_class", -1))) # 主角护甲合流入参(伤害控制器 defender_ac 消费)
db.set_reload_gate(true)
@@ -299,9 +278,9 @@ func _begin_battle_accept(enc: Dictionary) -> bool:
if dice.is_empty():
_fatal("敌人表 行集解析为空(%d,%d,%s)" % [int(enc.level_index), int(enc.node_depth), String(enc.get("node_type", "battle"))])
return false
# E4 收口:加成经 encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径)
_hero_atk = int(enc.get("hero_atk", int(db.combat.hero.atk)))
if not _enemy_system.spawn({"level_index": int(enc.level_index), "node_depth": int(enc.node_depth), "hero_hp": int(enc.hero_hp), "unit_count_overrides": dice[0], "hero_atk_override": _hero_atk, "hero_max_hp_override": int(enc.get("hero_max_hp", -1)), "enemy_atk_overrides": dice[1], "hero_attack_type": String(enc.get("attack_type", "")), "hero_armor_type": String(enc.get("armor_type", "")), "row_kind": String(enc.get("node_type", "battle"))}): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss)
# E4 收口:加成经 _facts.encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径;
# _hero_atk 派生读=受理后不变量,无快照成员)
if not _enemy_system.spawn({"level_index": int(enc.level_index), "node_depth": int(enc.node_depth), "hero_hp": int(enc.hero_hp), "unit_count_overrides": dice[0], "hero_atk_override": _hero_atk(), "hero_max_hp_override": int(enc.get("hero_max_hp", -1)), "enemy_atk_overrides": dice[1], "hero_attack_type": String(enc.get("attack_type", "")), "hero_armor_type": String(enc.get("armor_type", "")), "row_kind": String(enc.get("node_type", "battle"))}): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss)
_fatal("敌人系统 spawn 被拒")
return false
return true
@@ -309,7 +288,7 @@ func _begin_battle_accept(enc: Dictionary) -> bool:
# 遭遇骰(难度曲线与Boss GDD Core Rule 8):行有 unit_count_max 时按 [unit_count, max] 均匀骰;
# 行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值
# encounter 封闭流恰一 scope(数量骰先、atk 骰后——区间宽度为零的行不掷=「定值不骰」既有口径);
# _facts.encounter 封闭流恰一 scope(数量骰先、atk 骰后——区间宽度为零的行不掷=「定值不骰」既有口径);
# 敌人系统零随机纪律保持(收 override 即确定值)。
# 返回 [count_overrides, atk_overrides];行集为空 = 返回 [](fail 判据交受理段 _fatal)。
func _begin_battle_dice(enc: Dictionary) -> Array:
@@ -352,12 +331,12 @@ func _begin_battle_dice_roll(rows: Array, estream: RandomNumberGenerator, roll_a
# 引擎分发;流派档位=FeatsEngine 纯函数求值两运行态注伤害控制器。魔矢每战首次开格计数同步归零)
# 扩签运行态就位(归 fx_host)+账本挂接+桶挂引用+揭示/AC 两回调+rider 档位恒零+订阅重建。
func _begin_battle_fx_state(enc: Dictionary) -> void:
# 同调装配运行态注入(enc.attunement 携带;off=空集零扰动。淬毒 N 注入
# 同调装配运行态注入(enc._facts.attunement 携带;off=空集零扰动。淬毒 N 注入
# 伤害控制器、偏运 N 注入地图战斗资源——均须先于首板 install)
attunement = (enc.get("attunement", []) as Array).duplicate()
_attune_safe_seen = 0
_facts.attunement = (enc.get("attunement", []) as Array).duplicate()
_facts.attune_safe_seen = 0
_fx_host.reset_battle_state(int(enc.get("hero_armor_class", -1)))
_mines_hits = {"total": 0, "last": 0, "round": 0} # 踩雷计数开战清零(事实面生命周期同款)
_rounds.mines_hits = {"total": 0, "last": 0, "round": 0} # 踩雷计数开战清零(事实面生命周期同款)
var adv_bucket: Variant = enc.get("adv_bucket", null)
_fx_host.mount_buckets(adv_bucket if adv_bucket is Dictionary else {}, enc.get("level_counters")) # 冒险桶宿主字典(直构兜底自有空字典);关卡桶 LevelRunner 每关 new 下传、非 Dictionary 即不挂
_reward_grant.bind_battle(_battle_hooks()) # 授予件战斗域回调束(充能消耗/真视计数/踩雷计数)
@@ -368,13 +347,13 @@ func _begin_battle_fx_state(enc: Dictionary) -> void:
# 不设档位机制」):rider_tier 两注入恒零,散件单体效果=唯一形态
_damage.inject["rider_tier_bonus"] = 0
_damage.inject["rider_tier_crit"] = false
_feat_first_dig_used = false
_facts.feat_first_dig_used = false
# 树 rule 键消费(Fork 激活侧效果=效果引擎合成
# 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1)。其余 rule 键
# =stat 面投影不入战斗(T8 表内容 provisional 口径披露))
var tree_rules: Array[Variant] = enc.get("tree_rules", []) as Array
# 效果引擎订阅重建(装载序=feats 表序→treasure 表序→树规则,写死保种子复放——细则见 battle_fx_host.rebuild_subscriptions)
_fx_host.rebuild_subscriptions(feat_ids, attunement, tree_rules)
_fx_host.rebuild_subscriptions(feat_ids, _facts.attunement, tree_rules)
# 秘宝常时参数注入(装配集 kind 分发:远程镐挖距放宽/信标网络封印可达豁免;
@@ -415,19 +394,19 @@ func _begin_battle_preview_economy(enc: Dictionary) -> void:
# 板序推进+board_changed 发射+秘宝叠加层退役——时序逐字;复位职责留宿主(成员单点),不随件。
func _install_and_reset(during_round := false) -> bool:
var installer := BoardInstaller.new({"db": db, "rng": rng, "minesweeper": minesweeper,
"reward_economy": _reward_economy, "encounter": encounter, "fatal_cb": _fatal})
"reward_economy": _reward_economy, "encounter": _facts.encounter, "fatal_cb": _fatal})
if not installer.install(during_round):
return false
_seal_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8)
_seal_miss_seen.clear()
_perfect_hit_true = 0 # 整图完美计数随 install 重置
_perfect_hit_false = 0
_perfect_forgiven = 0 # 豁免累计随 install 重置(domain=battle 每战清零)
_board_from_midround_refresh = during_round # 轮内刷新装板 = 刷新图(清图奖励不发);常规换图/开战装板 = false
_board_seq += 1
_facts.seal_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8)
_facts.seal_miss_seen.clear()
_facts.perfect_hit_true = 0 # 整图完美计数随 install 重置
_facts.perfect_hit_false = 0
_facts.perfect_forgiven = 0 # 豁免累计随 install 重置(domain=battle 每战清零)
_facts.board_from_midround = during_round # 轮内刷新装板 = 刷新图(清图奖励不发);常规换图/开战装板 = false
_facts.board_seq += 1
board_changed.emit()
if treasure_inventory != null:
treasure_inventory.retire_overlays() # 叠加层随图消亡(换图/刷新图装板点;已花道具不返还)
if _reward_grant.treasure_inventory != null:
_reward_grant.treasure_inventory.retire_overlays() # 叠加层随图消亡(换图/刷新图装板点;已花道具不返还)
return true
@@ -448,9 +427,9 @@ func _on_cell_revealed(evt: Dictionary) -> void:
if String(evt.get("kind", "")) == "safe":
var cs_row: Dictionary[Variant, Variant] = _attune_charge_safe_row()
if not cs_row.is_empty():
_attune_safe_seen += 1
_facts.attune_safe_seen += 1
var cn_b: int = int(cs_row.get("charge_n", 999))
if cn_b > 0 and _attune_safe_seen % cn_b == 0:
if cn_b > 0 and _facts.attune_safe_seen % cn_b == 0:
action_economy.grant_ap(int(cs_row.get("magnitude", 1)))
if bool(evt.get("resource", false)):
# 身份快照随缓冲捕获(批次隔离——轮内刷新后 mapping 已换批,授予必须用缓冲期身份,
@@ -459,47 +438,20 @@ func _on_cell_revealed(evt: Dictionary) -> void:
if snap != null:
evt = evt.duplicate()
evt["reward_snapshot"] = snap
_facts_reveals.append(evt)
_facts.reveals.append(evt)
func _on_seal_judged(evt: Dictionary) -> void:
_facts_seals.append(evt)
_facts.seals.append(evt)
func _on_round_end(_payload: Dictionary) -> void:
_facts_path = String(_payload.get("path_tag", ""))
_maybe_settle()
_facts.path = String(_payload.get("path_tag", ""))
_rounds.settle_if_due()
func _maybe_settle() -> void:
if _settling or battle_result != "":
return
if action_economy.phase != action_economy.PHASE_WAITING:
return
_settle()
# ---- 每轮结算序列(五段——段序骨架冻结于 SettlementTable.segment_order)----
func _settle() -> void:
_settling = true
var ri := round_index
# 段序编排(五段——段体归 game/core/settlement/,本类只做段序+行发射/开单原语单点)
_grant_seg.run(ri) # ① 授予段(案C 闸随段装载)
_aura_seg.run(ri) # ② 增益/减益段(续存实例组合行)
_combat_seg.run(ri) # ③ 战斗段(主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕)
_decay_seg.run(ri) # ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减)
var verdict: String = _verdict_seg.run(ri) # ⑤ 判定段(防御性优先级 hero 先判)
if verdict != "continue":
return # 终局:_settling 不回置——_battle_over 已接管
_settling = false
# ---- 授予段 helpers(极性路由)----
# 开格产出乘区(贪婪矿灯/矿脉直觉/赌徒的血签事件收集器注入):
# positive=power ×(1+静态 mult%);negative=power ×(1+cost%)。
# 全零收集器=原快照原样返回(零扰动)。
func _feat_yield_adjust(snap: Dictionary, positive: bool, pos_m: int, neg_m: int) -> Dictionary:
if positive and pos_m <= 0:
return snap
@@ -556,57 +508,7 @@ func set_treasure_unlocked(ids: Dictionary) -> void:
# 随机消费只在换算层(本方法 + SkillLottery);单位容器收确定值(零随机纪律——armor_roll 先例同构)。
# 统一流:behavior_roll 无子流派生向量(位置交错,单点定位由事件日志承载)。
# 摇定后、兑现前死亡 → 本拍技能作废。
func _roll_round_actions() -> void:
var pity_m: int = int(db.combat.skill.pity_threshold)
# facts 组包(开轮摇号前一次成型——last_round_end 空字典首轮映射 none 唯一落点;hero 事实不进 facts,
# 容器内直读主角行)
var facts := {
"round_index": round_index,
"last_round_end": "none" if action_economy.last_round_end().is_empty() else String(action_economy.last_round_end().get("path_tag", "none")),
"mines_hit_last": int(_mines_hits["last"]),
"mines_hit_total": int(_mines_hits["total"]),
}
for u in _enemy_system.query_alive("enemy"):
# Boss 半血解锁闩(迭代14④ T4——开轮摇号前逐敌检查;一次过门单向闩;解锁出一次归因明细行
# 「XX 解锁了 YY」带 skill_unlock 高光键〔type=highlight 既有行型零新增〕,呈现层弹一次性
# 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机
# 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏)
for sid in _enemy_system.check_trigger_unlocks(u, facts):
var srow_u: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(sid)
_emit_row({"type": "highlight", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "highlight": "skill_unlock"},
round_index, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))],
0, {"skill_id": sid})
# enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射,
# 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 trigger 行值)
var pct_v := 0
for pe in (u.get("pool", []) as Array):
var pe_d: Dictionary[Variant, Variant] = pe
if String(pe_d.get("skill_id", "")) == sid and (pe_d.get("trigger", {}) as Dictionary).has("value"):
pct_v = int((pe_d.get("trigger", {}) as Dictionary).get("value", 0))
break
# target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记,
# 主角攻击面 _resolve_hero_attack 通用消费——fx_atk_bonus 键加算进 base_value)
var eht_hits := _fx_host.fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v,
"is_boss": String(encounter.get("node_type", "")) == "boss"})
for h in eht_hits:
if String(h.get("kind", "")) == "target_attack_bonus":
u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0))
var entries: Array[Variant] = _enemy_system.rollable_skills(u, facts)
if _enemy_system.basic_streak(u) >= pity_m:
var specials: Array[Variant] = _enemy_system.special_skills(u, facts)
if not specials.is_empty():
entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合
var stream: RandomNumberGenerator = rng.open_stream("behavior_roll")
_enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream))
_enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记)
_mines_hits["last"] = _mines_hits["round"] # 轮次真正结束转存(轮内部分结算不更新)
_mines_hits["round"] = 0
# ---- 战斗段/算术读数(段体与掷点/折扣/暴击/克制/池乘区/下限/交付管线归 damage_controller.gd
# 与 settlement/combat_segment.gd;本类只保留薄委托——_defender_ac 供结算/呈现快照消费)----
# 防守方护甲等级读数——实现归 伤害控制器 defender_ac(本委托供结算快照/呈现快照)
func _defender_ac(unit: Dictionary) -> int:
return _damage.defender_ac(unit)
@@ -643,12 +545,12 @@ func _emit_settlement_snapshot(verdict: String) -> void:
buffs.append({"id": eff.effect_id, "side": "enemy", "layers": eff.layers, "left": eff.remaining_settlements})
var mines_hit := 0
var safe_n := 0
for evt in _facts_reveals:
for evt in _facts.reveals:
if evt.kind == "mine":
mines_hit += 1
elif evt.kind == "safe":
safe_n += 1
var board_stats := {"seq": _board_seq}
var board_stats := {"seq": _facts.board_seq}
if minesweeper.current_board() != null:
board_stats["w"] = minesweeper.current_board().width
board_stats["h"] = minesweeper.current_board().height
@@ -656,18 +558,18 @@ func _emit_settlement_snapshot(verdict: String) -> void:
board_stats["seals"] = minesweeper.seal_count()
settlement_snapshot.emit({
"kind": "settlement",
"level": int(encounter.get("level_index", 0)),
"depth": int(encounter.get("node_depth", 0)),
"level": int(_facts.encounter.get("level_index", 0)),
"depth": int(_facts.encounter.get("node_depth", 0)),
"round": round_index,
"verdict": verdict,
"unit_count": enemies.size(),
"spawn_count": _enemy_system.all_enemies().size(), # 本战斗实例化总数(含死亡残位)——unit_count=0 时区分「首轮全灭」vs「空场」
"hero": {"hp": int(_enemy_system.hero_unit().get("hp", 0)), "max_hp": int(_enemy_system.hero_unit().get("max_hp", 0)), "atk": int(_enemy_system.hero_unit().get("atk", 0)), "armor_class": _defender_ac(_enemy_system.hero_unit())},
"economy": {"phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "refresh_used": _refresh_used_this_round},
"economy": {"phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "refresh_used": _facts.refresh_used},
"board": board_stats,
"enemies": enemies,
"buffs": buffs,
"round_facts": {"path": _facts_path, "safe": safe_n, "mines": mines_hit, "seal_events": _facts_seals.size(), },
"round_facts": {"path": _facts.path, "safe": safe_n, "mines": mines_hit, "seal_events": _facts.seals.size(), },
})
@@ -675,7 +577,7 @@ func _emit_settlement_snapshot(verdict: String) -> void:
func _exit_distribution() -> void:
var mode := "same_board"
if _facts_path == "cleared":
if _facts.path == "cleared":
mode = "new_board" # cleared 轮恒走 new_board(契约义务);非清图轮恒同图(等待期刷新通道已移除)
if mode == "new_board":
_reward_economy.retire_mapping()
@@ -689,32 +591,23 @@ func _exit_distribution() -> void:
round_index += 1
_fx_host.emit_round_start()
action_economy.notify_round_start()
_reset_round_facts()
_refresh_used_this_round = 0
_roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道)
_facts.reset_round()
_facts.refresh_used = 0
_rounds.roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道)
round_started.emit(round_index)
# 呈现层查询面(HUD 刷新按钮可用性)——挖掘期唯一通道
# 呈现层查询面(HUD 刷新按钮可用性)——挖掘期唯一通道(折价成本同源:判据归 BattleRefreshFlow)
func can_refresh() -> bool:
return battle_result == "" \
and _refresh_used_this_round < int(db.run.action_economy.refresh_cap_per_round) \
and action_economy.phase == action_economy.PHASE_ACTIVE \
and action_economy.rap >= int(db.run.action_economy.rap_refresh_cost)
return BattleRefreshFlow.can_refresh(self)
# 玩家经 HUD框架发起、战斗结算 代理调用(禁止 RAP 满自动刷)。
# 挖掘期唯一通道:Active 相位立即换图
# 继续挖(RAP -8;AP 保留、相位不动、不触发结算);非挖掘期请求一律拒绝——非清图轮恒同图。
# 挖掘期唯一通道:Active 相位立即换图继续挖(RAP 按折后价扣;AP 保留、相位不动、不触发结算);
# 非挖掘期请求一律拒绝——非清图轮恒同图。刷新门/折价扣费/换图/成功事实归 BattleRefreshFlow
# (迭代 46 卡A T3 拆出——编排器入口与唯一通道不变,C10 结构预算只降不升)。
func player_request_refresh() -> bool:
if not can_refresh():
return false
if not action_economy.request_refresh():
return false
_refresh_used_this_round += 1
_reward_economy.retire_mapping() # 旧图批次退役(地图战斗资源 Ready→retired;E6 新批重置前提)
minesweeper.retire_board(true) # 旧板退役(轮内通道)
return _install_and_reset(true) # 当场换图——失败即编排违例 fail-fast
return BattleRefreshFlow.execute(self)
func _battle_over(result: String) -> void:
@@ -737,11 +630,11 @@ func _battle_over(result: String) -> void:
_reward_economy.retire_mapping()
_enemy_system.despawn_all()
_status_effects.notify_battle_end()
if treasure_inventory != null:
treasure_inventory.retire_overlays() # 叠加层随战斗结束清空(库存不动——关卡域跨战斗)
if _reward_grant.treasure_inventory != null:
_reward_grant.treasure_inventory.retire_overlays() # 叠加层随战斗结束清空(库存不动——关卡域跨战斗)
action_economy.notify_combat_end() # → Idle,RAP 清零
db.set_reload_gate(false)
_reset_round_facts()
_facts.reset_round()
battle_finished.emit({"result": result, "hero_hp": hero_hp_left, "gp_dropped": gp_dropped})
@@ -749,7 +642,7 @@ func _battle_over(result: String) -> void:
# gp_dropped = gp_drop 封闭流掷定(tier 行的 roll_min..roll_max 均匀整数——每战斗末恰一 scope)
# + level_gp_bonus(level_index) + escrow。掷定行缺位 = 数据面缺行(fail 报错按 0 兑付——不哑掉)。
func _roll_gp_drop() -> int:
var tier := "boss" if String(encounter.get("node_type", "")) == "boss" else "normal"
var tier := "boss" if String(_facts.encounter.get("node_type", "")) == "boss" else "normal"
var drops: Dictionary[Variant, Variant] = db.run.shop.gp_drop_row(tier)
var roll := 0
if drops.is_empty():
@@ -757,11 +650,11 @@ func _roll_gp_drop() -> int:
else:
var stream: RandomNumberGenerator = rng.open_stream("gp_drop")
roll = stream.randi_range(int(drops.get("roll_min", 0)), int(drops.get("roll_max", 0)))
var drop := roll + db.run.shop.level_gp_bonus_at(int(encounter.get("level_index", 0)))
var drop := roll + db.run.shop.level_gp_bonus_at(int(_facts.encounter.get("level_index", 0)))
# 财富之道·聚(战斗掉落 GP 乘算·入账前:只乘战斗掉落两成分〔掷定+关卡加成〕,
# 店内 escrow 不乘〔范围=仅战斗掉落渠道〕;本函数返回值=结算框「GP 增量」行
# 显示源,落在入账前=显示即实得;乘数 provisional 150%)
if (encounter.get("tree_rules", []) as Array).has("economy_hoard"):
if (_facts.encounter.get("tree_rules", []) as Array).has("economy_hoard"):
drop = int(round(float(drop) * 1.5))
return drop + _reward_grant.escrow
@@ -812,7 +705,7 @@ func presentation_snapshot() -> Dictionary:
"phase": String(action_economy.phase),
"ap": int(action_economy.ap), "rap": int(action_economy.rap),
"can_refresh": can_refresh(),
"refresh_used": _refresh_used_this_round, # 受理计数入呈现成文面(HUD 余量单源读此处)
"refresh_used": _facts.refresh_used, # 受理计数入呈现成文面(HUD 余量单源读此处)
"hero": hero,
"enemies": enemies,
"hero_effects": _status_effects.query_active("hero").duplicate(true), # 值拷贝出户(快照消费方直改不渗入状态容器)
@@ -839,12 +732,12 @@ func diagnostic_snapshot() -> Dictionary:
return {
"battle_result": battle_result,
"round": round_index,
"settling": _settling,
"refresh_used_round": _refresh_used_this_round,
"board_seq": _board_seq,
"facts_path": _facts_path,
"facts_reveals": _facts_reveals.size(),
"facts_seals": _facts_seals.size(),
"settling": _rounds.is_settling(),
"refresh_used_round": _facts.refresh_used,
"board_seq": _facts.board_seq,
"facts_path": _facts.path,
"facts_reveals": _facts.reveals.size(),
"facts_seals": _facts.seals.size(),
"action_economy": {"phase": String(action_economy.phase), "ap": action_economy.ap, "rap": action_economy.rap},
"minesweeper_board_installed": minesweeper.current_board() != null,
"minesweeper_revealed": minesweeper.revealed_cells().size() if minesweeper.current_board() != null else 0,
@@ -855,12 +748,6 @@ func diagnostic_snapshot() -> Dictionary:
}
func _reset_round_facts() -> void:
_facts_reveals.clear()
_facts_seals.clear()
_facts_path = ""
# ---- 行协议(Core Rule:公共字段 + 行类型特有字段)----
func _emit_row(head: Dictionary, ri: int, display: String, value: int, extra: Dictionary) -> void:
@@ -911,6 +798,6 @@ func _fatal(msg: String) -> void:
## 完美封印谓词(两账相减:有效误标=误标总数−豁免数)
## 契约 2:_perfect_hit_false 保持纯净,豁免只累加 _perfect_forgiven,禁回退式改写与拦截式吞事件。
## 契约 2:_facts.perfect_hit_false 保持纯净,豁免只累加 _facts.perfect_forgiven,禁回退式改写与拦截式吞事件。
func perfect_sweep_earned() -> bool:
return _perfect_hit_false - _perfect_forgiven <= 0 and _perfect_hit_true >= 1
return _facts.perfect_hit_false - _facts.perfect_forgiven <= 0 and _facts.perfect_hit_true >= 1
+218
View File
@@ -0,0 +1,218 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 存档断点组装与回放唯一承载(AdventureRunner 拆件——单一变化原因=断点快照键面与回放策略):
## 断点快照拼盘、回放回填、读侧策略分发;BREAKPOINT_FIELDS 27 键登记单源随件(键名/策略标签/键序冻结)。
## 构造=引用束字典注入(db/rng/主角账三件/curio/stop/bank/loadout——件禁反向持宿主);
## 快照宿主输入(state/level_index/master_seed/runner/adventure_counters)逐调传参;
## resume_adventure 留宿主(壳+宿主侧回填语句——所有权缝分家,语句序逐步保形)。
class_name BreakpointStore
extends RefCounted
# ---- 断点快照(字段登记单源:写侧拼装与读侧回退策略同登记一表)----
# 键序即快照键序(27 顶层键——写侧/读侧/键集断言三面同源)。读侧回退策略词表:
# plain 无特殊回退面(缺键取代码内缺省)/clamp 数值夹 ≥0/keep_on_null 缺键或类型不符保持既有态
# dual_merge 双向合并(bonus_taken)/params_fallback 缺键回退参数面(attune_refresh_y)
# token_migration 旧版候选形态迁移(bonus_pending.candidates)
# 注入缺省 null = 整面静默(模拟/冒烟零扰动)。
const BREAKPOINT_FIELDS := {
"level_index": {"strategy": "plain"},
"node_id": {"strategy": "plain"},
"hero_hp": {"strategy": "clamp"},
"bonus": {"strategy": "clamp"},
"gp": {"strategy": "plain"},
"shop_purchased": {"strategy": "plain"},
"shop_shelf": {"strategy": "plain"},
"shop_visit_buys": {"strategy": "plain"},
"attunement_charges": {"strategy": "keep_on_null"},
"graph_gen_v2": {"strategy": "plain"},
"master_seed": {"strategy": "plain"},
"rng_counter": {"strategy": "plain"},
"keystone": {"strategy": "plain"},
"visited_ids": {"strategy": "plain"},
"aux_items": {"strategy": "plain"},
"bonus_taken": {"strategy": "dual_merge"},
"bonus_deck": {"strategy": "keep_on_null"},
"relics": {"strategy": "plain"},
"bonus_pending": {"strategy": "token_migration"},
"feat_ticket_pity": {"strategy": "plain"},
"feat_ticket_sold": {"strategy": "plain"},
"adventure_counters": {"strategy": "keep_on_null"},
"vision_counter_used": {"strategy": "clamp"},
"refresh_stock": {"strategy": "clamp"},
"attune_refresh_y": {"strategy": "params_fallback"},
"bonus_result": {"strategy": "plain"},
"keystone_drops": {"strategy": "plain"},
"gain_journal": {"strategy": "plain"}, # 获得日记(拆件批 T4 扩列——唯一声明的断点扩展;旧档缺键=汇总直载跳过比对)
}
# 引用束(宿主构造注入——成员名与宿主侧原名一致,迁移函数体逐字)
var db: ConfigDb
var rng: RngServiceCore
var _stats: HeroStats
var _wallet: GpWallet
var _journal: GainJournal
var _curio: CurioDeck = null # 令牌池件(池余库/获取计数随断点)
var _stop: CurioStop = null # 停点件(停点态/凭证计数片段)
var _refresh_bank: RefreshBank = null # 刷新账件(刷新存量/Y 池片段)
var _loadout: AttuneLoadout = null # 同调配装件(充能行集读侧)
# 断点域六成员(自宿主收编——宿主其余读写点经本件)
var _save_manager = null # 断点承载注入(null = 不写断点;模拟/冒烟缺省)
var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递)
var _pending_next_level := false # 大奖励已兑付、等 continue_to_next_level 确认
var _last_level_payload := {} # 最近一次关卡结束载荷(失败结算的败退节点明细来源——数值验证取数面)
var _last_treasure_cleared := 0 # additive(最近一次关卡末清空计数,summary 取数面)
var _loadout_seeded := false # 同调行已种标记(每冒险一次——首关 bind 时点投行;行跨关存活后续关零动作)
func _init(refs: Dictionary) -> void:
db = refs["db"]
rng = refs["rng"]
_stats = refs["stats"]
_wallet = refs["wallet"]
_journal = refs["journal"]
_curio = refs["curio"]
_stop = refs["stop"]
_refresh_bank = refs["bank"]
_loadout = refs["loadout"]
# 断点快照拼盘落盘(宿主 _snapshot_breakpoint 委托目标——宿主输入逐调传参)
func snapshot(state: String, level_index: int, master_seed: int, runner: LevelRunner, counters: Dictionary[Variant, Variant]) -> void:
if _save_manager == null or state != "running":
return
var parts: Dictionary[Variant, Variant] = _bp_parts(level_index, master_seed, runner, counters)
var bp: Dictionary[Variant, Variant] = {}
for key in BREAKPOINT_FIELDS.keys():
bp[key] = parts[key]
_save_manager.set_breakpoint(bp)
_save_manager.write_to_disk()
# 断点写侧取值(键集以 BREAKPOINT_FIELDS 为准——未登记键即取值越界,fail-fast)
func _bp_parts(level_index: int, master_seed: int, runner: LevelRunner, counters: Dictionary[Variant, Variant]) -> Dictionary[Variant, Variant]:
var stats_frag: Dictionary[Variant, Variant] = _stats.breakpoint_fragment()
var wallet_frag: Dictionary[Variant, Variant] = _wallet.breakpoint_fragment()
var journal_frag: Dictionary[Variant, Variant] = _journal.breakpoint_fragment()
var bank_frag: Dictionary[Variant, Variant] = _refresh_bank.breakpoint_fragment()
var stop_frag: Dictionary[Variant, Variant] = _stop.breakpoint_fragment()
var level_frag: Dictionary[Variant, Variant] = runner.breakpoint_fragment() if runner != null else {
"node_id": -1, "visited_ids": [] as Array, "aux_items": [] as Array,
"shop_shelf": [] as Array, "shop_visit_buys": 0} # 三选一停点续玩分支 runner 为 null——节点域按缺省落档
# 该两态的续玩只消费 bonus_* 附随件(选择停点/已兑付停点快照)
var parts: Dictionary[Variant, Variant] = {
"level_index": level_index,
"node_id": int(level_frag.get("node_id", -1)),
"hero_hp": int(stats_frag.get("hero_hp", 0)),
"bonus": journal_frag.get("bonus", {}),
"gp": int(wallet_frag.get("gp", 0)),
"shop_purchased": journal_frag.get("shop_purchased", {}),
"shop_shelf": level_frag.get("shop_shelf", []), # 货架入断点(离开店/非店停点 = 空架合法态)
"shop_visit_buys": int(level_frag.get("shop_visit_buys", 0)), # 本次进店已购次数(A 链定价输入)
"attunement_charges": (_loadout.breakpoint_fragment(runner.treasure_inventory) if runner != null and runner.treasure_inventory != null else {"attunement_charges": {}}).get("attunement_charges", {}), # 同调主动件充能余量随断点(行派生——save v4 域键名策略不变)
"graph_gen_v2": 1, # 生成图换代代差标记(缺键 = 换代前旧断点 → resume 显式拒绝)
"master_seed": master_seed,
"rng_counter": rng.counter(),
"keystone": int(journal_frag.get("keystone", 0)),
"visited_ids": level_frag.get("visited_ids", []),
"aux_items": level_frag.get("aux_items", []),
"bonus_taken": _curio.taken.duplicate(true), # 三选一件获取次数(kind 维度派生账本——断点附随件与读数承载)
"bonus_deck": _curio.deck.duplicate(true), # 令牌池余库(TP0 v2.2——池消耗即上限,续玩余库逐枚还原)
"relics": stats_frag.get("relics", []), # 特性持有集(局内随机渠道获得态续玩防丢失)
"bonus_pending": stop_frag.get("bonus_pending", {}), # 三选一停点态(空 = 无停点——选择停点可中断契约)
"feat_ticket_pity": int(stop_frag.get("feat_ticket_pity", 0)), # T5 专长凭证保底计数(扩列向后兼容——旧档缺键缺省 0)
"feat_ticket_sold": int(stop_frag.get("feat_ticket_sold", 0)), # T5 专长凭证限购计数(同上)
"adventure_counters": counters.duplicate(true), # adventure 域计数器随断点(续玩跨战账不丢——桶归宿主字典)
"vision_counter_used": int(journal_frag.get("vision_counter_used", 0)), # 真视使用计数随断点(续玩跨战账不丢)
"refresh_stock": int(bank_frag.get("refresh_stock", 0)), # 刷新存量账随断点
"attune_refresh_y": int(bank_frag.get("attune_refresh_y", 0)), # Y 冒险周期池余量随断点(扩列向后兼容——旧档缺键回退参数面)
"bonus_result": stop_frag.get("bonus_result", {}), # 已兑付待推进态(空 = 无——中断续玩不重兑付)
"keystone_drops": journal_frag.get("keystone_drops", []), # 逐杀掉落明细(摘要归因续接)
"gain_journal": journal_frag.get("gain_journal", {}), # 获得日记(唯一声明的断点扩列——日记为现行事实源)
}
# 拼盘完整性自断言(fail-fast——片段并集与登记键集不符即中止落盘)
var stray: Array[Variant] = []
for key in parts.keys():
if not BREAKPOINT_FIELDS.has(key):
stray.append(key)
if not stray.is_empty():
push_error("[冒险结构] 断点拼盘含未登记键: %s(fail-fast——片段与登记单源失同步)" % str(stray))
return parts
# 续玩账面回填(生命/特性/钱包/加算区/已购档/钥石/逐杀明细/真视——主角账三件片段各还原各键;
# bonus 子键 clamp 类夹 ≥0;日记在档=推导比对载入、缺键=旧档汇总直载)
#(宿主侧账面语句——summary/levels_total/level_index/_pending_next_level——按语句序留宿主壳)
func restore_ledger(bp: Dictionary, params: Dictionary) -> void:
_stats.restore_fragment(bp, params)
_wallet.restore_fragment(bp, params)
_journal.restore_fragment(bp, params)
# 续玩三选一态回填(池与件计数归件、停点态归宿主;dual_merge/keep_on_null 策略见 BREAKPOINT_FIELDS)
#(前置清理 _reset_curio_state 留宿主——桶归宿主字典;本函数承接其后的件侧还原语句,序逐步保形)
func restore_curio_state(bp: Dictionary) -> void:
var bp_taken: Dictionary[Variant, Variant] = bp.get("bonus_taken", {}) as Dictionary
for id in _curio.taken.keys():
_curio.taken[id] = maxi(0, int(bp_taken.get(id, 0)))
for id in bp_taken.keys():
_curio.taken[String(id)] = maxi(0, int(bp_taken.get(id, 0))) # 身份维键(relic:<feat_id>)随快照还原
# 令牌池余库还原(TP0 v2.2——快照缺键 = 旧版断点,保持满库重建态:旧档只读一版周期的防御口径;
# relic 令牌 feat_id 随还原)
var bp_deck = _bp_read("bonus_deck", bp)
if bp_deck is Array:
var restored: Array[Variant] = []
for t in (bp_deck as Array):
var tok: Dictionary[Variant, Variant] = t as Dictionary
var rtok := {"kind": String(tok.get("kind", "")), "tier": String(tok.get("tier", ""))}
if String(rtok["kind"]) == "relic":
rtok["feat_id"] = String(tok.get("feat_id", "")) # relic 令牌 feat_id 随还原
restored.append(rtok)
_curio.deck = restored
# 特性持有集还原(缺键 = 旧版断点防御空集——主角数值账字段单点)
_stats.feats = []
for r in (bp.get("relics", []) as Array):
_stats.feats.append(String(r))
# 续玩参数面——数值账规范化与越界钳制(件侧;宿主侧四重绑定与预算注入按语句序夹在两件侧调用之间)
func apply_resume_params(params: Dictionary) -> void:
_stats.rebase_override(params.get("hero_override", {})) # 续玩参数面同 start_adventure(规范化归数值账)
_stats.attack_type = String(params.get("attack_type", "")) # 续玩参数面同 start_adventure(账号层选中态随 resume 重注入)
_stats.armor_type = String(params.get("armor_type", ""))
# 生命账本越界钳制(T17——spawn 守卫 EC16 免疫):断点可能带出高于生效上限的账本值
#(如调试浮窗调高期写出的值,实例:hero_hp=28 > 基础 26)——恢复后钳到生效上限,
# 否则首战 spawn「hero_hp 入参越界」拒收 → 「敌人系统 spawn 被拒」fatal。钳制式单点归
# 数值账 clamp_hp_to_resume_cap(保守口径不含加算区——与原钳制式逐字同)。
_stats.clamp_hp_to_resume_cap(int(db.combat.hero.max_hp))
# 停点件/刷新账片段回填(凭证计数;刷新存量/Y 池——Y 缺键回退参数面 params_fallback 类)
func restore_stop_bank_fragments(bp: Dictionary, params: Dictionary) -> void:
_stop.restore_fragment(bp, params)
_refresh_bank.restore_fragment(bp, params)
# 读侧单点(BREAKPOINT_FIELDS 策略标签分发;未登记键 fail-fast 不静默按 plain 兜)。
# 二次消费面(_start_current_level 经 _resume_bp 取 node_id/visited_ids/aux_items)同走此口。
func _bp_read(key: String, bp: Dictionary, fallback: Variant = null) -> Variant:
if not BREAKPOINT_FIELDS.has(key):
push_error("[冒险结构] 断点字段未登记(读侧): %s" % key)
var v: Variant = bp.get(key, fallback)
if _bp_strategy(key) == "keep_on_null" and v != null and not (v is Dictionary or v is Array):
return null # 类型不符——保持既有态(调用方跳过覆盖)
return v
# 读侧策略标签(BREAKPOINT_FIELDS 读侧消费面单点)
func _bp_strategy(key: String) -> String:
return String((BREAKPOINT_FIELDS.get(key, {}) as Dictionary).get("strategy", "plain"))
# 数值类读侧(clamp 夹 ≥0 / params_fallback 断点缺键回退参数面整额注入 / plain 直取)
func _bp_read_int(key: String, bp: Dictionary, fallback: int, params: Dictionary = {}) -> int:
match _bp_strategy(key):
"clamp":
return maxi(0, int(bp.get(key, fallback)))
"params_fallback":
return maxi(0, int(bp.get(key, int(params.get(key, fallback)))))
_:
return int(bp.get(key, fallback))
+1
View File
@@ -0,0 +1 @@
uid://cw5qdd012h728
+18 -9
View File
@@ -1,14 +1,14 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 数值表单点访问入口:全部 .tres 表的加载、校验诊断与门面(垂直切片验证:改表即改玩法,数值零硬编码)。
## 玩法代码禁止绕过本单点自建 load();行查询归各表类(表管自己的行——2026-10-27 全清设计定稿),
## 表持有按域收进三容器(combat 战斗编排域/run 冒险 runner 域/crossrun 跨局元进度域),
## 消费面经 db.<容器>.<表> 直连。「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88)。
## headless --script 模式无 autoload——冒烟脚本显式实例化本类;加载失败 is_ready=false 加 fatal 封印,调用方须查。
## 玩法代码禁止绕过本单点自建 load();行查询归各表类(表管自己的行),
## 表持有按域收进四容器(combat 战斗编排域/run 冒险 runner 域/crossrun 跨局元进度域/
## presentation 表现层布局域),消费面经 db.<容器>.<表> 直连。
## 「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88)。
## 冒烟脚本可显式实例化本类(headless --script 亦会加载 autoload——共享 .tres 上的 resolver 归最后装载的实例);
## 加载失败 is_ready=false 加 fatal 封印,调用方须查。
class_name ConfigDb
extends Node
const SCHEMA_VERSION := 1
const TABLE_MANIFEST := {
"action_economy": "action_economy.tres",
"variation": "variation.tres",
@@ -29,11 +29,13 @@ const TABLE_MANIFEST := {
"adventure": "adventure.tres", # AdventureTable(大奖励与钥石行值单点)
"feats": "feats.tres", # FeatTable 规则改写型特性(定义表 Rule 4 豁免——TreasureItemTable 先例;空表合法态=主时间线零扰动)
"forge": "forge.tres", # ForgeTable——两根类型轴默认值+六路线行族;攻防所有权切分唯一数据面
"battle_layout": "battle_layout.tres", # BattleLayoutTable——战斗屏分区几何与明细带高度单行
}
var combat: ConfigDomainCombat # 战斗编排域七表(board_gen/variation/status_effects/enemy/skill/hero/settlement)
var run: ConfigDomainRun # 冒险 runner 域七表(level/difficulty/level_gen_params/reward/adventure/action_economy/shop)
var crossrun: ConfigDomainCrossrun # 跨局元进度域五表(meta/meta_tree/treasure_items/feats/forge)
var presentation: ConfigDomainPresentation # 表现层布局域一表(battle_layout)
var is_ready := false
var _diagnostics: Array[Dictionary] = [] # {severity, category, message}
@@ -46,6 +48,7 @@ func _init() -> void:
combat = ConfigDomainCombat.new()
run = ConfigDomainRun.new()
crossrun = ConfigDomainCrossrun.new()
presentation = ConfigDomainPresentation.new()
_resolver = ConfigDbResolver.new(push_diag,
func() -> Variant: return combat.board_gen.rows, func() -> Variant: return combat.enemy.rows, func() -> Variant: return run.difficulty) # 协作类持门面引用(诊断/致命通道回呼)
@@ -118,7 +121,14 @@ func load_tables(root: String, collect_only: bool) -> bool:
if has_errors and not collect_only:
# 游戏通道:致命(Rule 6)——is_ready 恒 false,调用方(主场景)中止构建
return false
# 容器装配(19 表成员按域收进三容器——2026-10-27 全清设计定稿)
# 容器装配(表行按域收进四容器+resolver 注入)归私有协函数——load_tables 只留加载/校验/致命门面
_assign_loaded(loaded)
is_ready = not has_errors
return not has_errors
# 容器装配协函数:20 表成员按域收进四容器(2026-10-27 全清设计定稿,2026-10-31 增表现层布局域)
func _assign_loaded(loaded: Dictionary) -> void:
combat.board_gen = loaded.get("board_gen")
combat.variation = loaded.get("variation")
combat.status_effects = loaded.get("status_effects")
@@ -138,12 +148,11 @@ func load_tables(root: String, collect_only: bool) -> bool:
crossrun.treasure_items = loaded.get("treasure_items")
crossrun.feats = loaded.get("feats")
crossrun.forge = loaded.get("forge")
presentation.battle_layout = loaded.get("battle_layout")
# resolver 协作件注入三表(resolve 委托承载——resolver 文件与键零搬移)
combat.board_gen._resolver = _resolver
combat.enemy._resolver = _resolver
run.difficulty._resolver = _resolver
is_ready = not has_errors
return not has_errors
# 跨表校验(悬空引用 / 池护栏——全部加载期致命)本体:
+1 -1
View File
@@ -1,4 +1,4 @@
## 战斗域容器(D2 域容器——config_db 表持有按域收敛,2026-10-27 全清设计定稿):战斗编排域七表类型化持有。
## 战斗域容器(D2 域容器——config_db 表持有按域收敛):战斗编排域七表类型化持有。
## 零方法零行为——纯持有结构;「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88 定稿注记)。
class_name ConfigDomainCombat
extends Resource
+1 -1
View File
@@ -1,4 +1,4 @@
## 跨局域容器(D2 域容器——config_db 表持有按域收敛,2026-10-27 全清设计定稿):跨局元进度域五表类型化持有
## 跨局域容器(D2 域容器——config_db 表持有按域收敛):跨局元进度域五表类型化持有
## (跨局循环=单局之外的长线进度域)。零方法零行为——纯持有结构;
## 「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88 定稿注记)。
class_name ConfigDomainCrossrun
+6
View File
@@ -0,0 +1,6 @@
## 表现层布局域容器(D2 域容器——config_db 表持有按域收敛):战斗屏布局一表类型化持有。
## 零方法零行为——纯持有结构;「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88 定稿注记)。
class_name ConfigDomainPresentation
extends Resource
var battle_layout: BattleLayoutTable
@@ -0,0 +1 @@
uid://bay1b1s3lrd1e
+1 -1
View File
@@ -1,4 +1,4 @@
## 冒险域容器(D2 域容器——config_db 表持有按域收敛,2026-10-27 全清设计定稿):冒险 runner 域七表类型化持有。
## 冒险域容器(D2 域容器——config_db 表持有按域收敛):冒险 runner 域七表类型化持有。
## 零方法零行为——纯持有结构;「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88 定稿注记)。
class_name ConfigDomainRun
extends Resource
+28 -10
View File
@@ -460,8 +460,8 @@ func _hero_attack_deliver(t: Dictionary, ctx: Dictionary, redemption: Dictionary
# 敌方伤害技:掷点 → 折扣/暴击 → 克制点数(attack_type vs 主角护甲类型——
# 无类型恒中性 delta 0,写法同 hero_attack 的 _counter_delta 消费位)→ hero_defense×enemy_output
# 池乘区 → 下限 → +rider(后置必中附加)→ 交付 → commit_action(作废单不 commit——死亡即不动手)。
# 掷点算术归 _enemy_roll;damage_taken 施加面归 _enemy_shield_apply(发射在本函数——
# 与减伤落快照的先后序固定)。
# 掷点算术归 _enemy_roll;damage_taken 统一发射+消费归 _emit_hero_damage_taken(发射在本函数——
# 与减伤落快照的先后序固定;「掷点后、交付前」消费时点不变)。
func _resolve_enemy_skill(t: Dictionary) -> void:
var hero: Dictionary[Variant, Variant] = units.hero_unit()
var roll := _enemy_roll(t)
@@ -469,9 +469,8 @@ func _resolve_enemy_skill(t: Dictionary) -> void:
# damage_taken 发射(kind=enemy_skill;铁壁整备减伤订阅面:shield 加法族合计 value×per(charge_layers),
# 稳健拓荒树规则同面(rebuild 合成订阅);减后过下限保底。damage_reduce〔背水 hp_ratio when〕/
# counter_add〔保险丝/石肤〕/ac_bonus〔石肤〕同面施加;hp_ratio fact 由 _fx_fact 补充事实面回落提供)
var dt_hits := _fx_hits("damage_taken", {"kind": "enemy_skill", "amount": total,
"source": String(t.context.get("skill_id", "")), "round": int(t.round_index)})
total = int(_enemy_shield_apply(dt_hits, total)["total"])
var dt_res := _emit_hero_damage_taken(total, "enemy_skill", String(t.context.get("skill_id", "")), t.origin_cell, int(t.round_index))
total = int(dt_res["total"])
units.apply_damage(hero, total)
var payload: Dictionary[Variant, Variant] = {"target": "hero", "skill_id": String(t.context.get("skill_id", "")), "baseline": int(t.context.get("baseline", 0)),
"roll": int(roll["roll"]), "ac": int(roll["threshold"]), "tier": int(roll["discount"]), "attack_type": String(t.attack_type)}
@@ -502,9 +501,22 @@ func _enemy_roll(t: Dictionary) -> Dictionary:
return {"roll": int(roll_read.roll), "threshold": threshold, "crit": crit, "discount": discount, "total": total}
# damage_taken 施加面(shield 加法族合计;damage_reduce 逐条减量〔背水〕;ac_bonus 转编排器单点
# 〔石肤 AC 累积归 set_hero_ac_override 消费面〕;counter_add 施加)。返回 {total, shield}。
func _enemy_shield_apply(hits: Array, total: int) -> Dictionary:
# 全局受伤发射单点(迭代 46 卡A T2——所有主角承伤路径唯一发射口:敌方攻击/地形/危险格/错误封印
# 资源直伤(后三者经 board_flat_hero 单据进入)都经此 dispatch 一次 damage_taken 并消费一次;
# 同一伤害单据重复发射由 context.hero_damage_consumed 标记闸拦(地形路径在开单点标记)。
# payload 必含 kind/amount/source/cell/round/hp_ratio;返回 {total, hits} 交调用侧按减后值开单/交付)。
func _emit_hero_damage_taken(amount: int, damage_kind: String, source: String, cell: Vector2i, ri: int) -> Dictionary:
var hu: Dictionary[Variant, Variant] = units.hero_unit()
var payload: Dictionary[Variant, Variant] = {"kind": damage_kind, "amount": amount, "source": source,
"cell": cell, "round": ri, "hp_ratio": int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1))))))}
var hits := _fx_hits("damage_taken", payload)
return {"total": int(_apply_hero_damage_effects(hits, amount)["total"]), "hits": hits}
# damage_taken 唯一消费族(组合次序冻结:shield 加法族合计→damage_reduce 逐条减量〔背水/石层缓冲〕→
# ac_bonus 转编排器单点〔石肤 AC 累积归 set_hero_ac_override 消费面〕→counter_add 施加〔保险丝/石肤/
# 换图系临时计数夹具〕。返回 {total, shield})。
func _apply_hero_damage_effects(hits: Array, total: int) -> Dictionary:
var shield := 0
for h in hits:
match String(h.get("kind", "")):
@@ -565,9 +577,15 @@ func _resolve_board_blast(t: Dictionary) -> void:
_outcomes[int(t.ticket_id)] = "redeemed"
# 棋盘主角直伤(反噬/治疗包负向/地形变异):无类型必中,下限保底;context 键并入行载荷
# 棋盘主角直伤(反噬/治疗包负向/危险格/错误封印资源直伤):无类型必中,下限保底;context 键并入行载荷。
# damage_taken 全局发射点(迭代 46 卡A T2——单据未标 hero_damage_consumed 时在下限前发射并消费一次;
# 地形路径已在授予段开单点发射并标记,此处跳过防重复)
func _resolve_board_flat_hero(t: Dictionary) -> void:
var dealt: int = maxi(int(db.combat.settlement.damage_roll.damage_floor), int(t.base_value)) # 伤害下限全来源
var dealt: int = int(t.base_value)
if not bool((t.get("context", {}) as Dictionary).get("hero_damage_consumed", false)):
var res_flat := _emit_hero_damage_taken(dealt, "board_flat_hero", String(t.get("source_class", "")), t.origin_cell, int(t.round_index))
dealt = int(res_flat["total"])
dealt = maxi(int(db.combat.settlement.damage_roll.damage_floor), dealt) # 伤害下限全来源(减后过下限)
units.apply_damage(units.hero_unit(), dealt)
var extra: Dictionary[Variant, Variant] = {"target": "hero"}
for k in t.context.keys():
+12 -4
View File
@@ -12,11 +12,15 @@ extends RefCounted
const EVENTS := ["dig_reveal", "dig_paid", "seal_hit", "mine_hit", "attack_commit", "clamp_check",
"armor_roll", "kill", "damage_taken", "enemy_hp_threshold", "round_start", "seal_settle",
"round_end", "reward_roll", "treasure_used", "charge_full", "dig_stop", "battle_won", "seal_missed",
"board_installed"]
"board_installed",
# 换图系双事件(refresh_request=玩家请求刷新〔省力周转 apply_discount 订阅面——折扣只改门槛
# 与扣费不改拒绝判据〕;board_refreshed=成功刷新事实〔换图系七件订阅主挂点——装新板后才发射〕)
"refresh_request", "board_refreshed"]
## 扫雷侧事件集(🔴 流派扫雷触发铁律新形态:每流派 ≥1 行订阅本集事件——schema 加载校验。
## 原 MINE_HOOKS 的同义迁移:挖掘/封印/轮末结算面事件)
const MINE_EVENTS := ["dig_reveal", "dig_paid", "seal_hit", "mine_hit", "seal_settle", "round_end", "reward_roll"]
## 原 MINE_HOOKS 的同义迁移:挖掘/封印/轮末结算面事件+轮内刷新面〔换图系〕)
const MINE_EVENTS := ["dig_reveal", "dig_paid", "seal_hit", "mine_hit", "seal_settle", "round_end", "reward_roll",
"refresh_request", "board_refreshed"]
## 效果类型词汇(解释器 switch 闭集——词表实数以 EFFECT_KINDS 常量为准,含共享行拆型:
## grant_gp/bias_wallet、counter_add/counter_spend、reveal_tiles/remove_mines/refresh_pick、
@@ -40,7 +44,11 @@ const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open
"mine_count_preview",
# forgive_seal_miss=完美保险(本场首次封印落空不计入完美
# 封印判定——订阅 seal_missed 的豁免效果,消费点在授予段;feats.tres 首例 domain=battle 行)
"forgive_seal_miss"]
"forgive_seal_miss",
# 换图系两效果(grant_reserve_ap=矿脉储能·付费挖格产 RAP〔消费点在行动经济 dig_paid
# 受理后一次性入池〕;apply_discount=省力周转·刷新成本折扣〔mode=refresh cost_mult 百分比,
# floor 取整——只改门槛与扣费、不改拒绝判据〕)
"grant_reserve_ap", "apply_discount"]
## fact 条件词汇(封闭枚举——payload 同名键直读面〔fact 取值序 payload
## 优先——effect_engine._fact_value 既有机制〕。new_resource_type=首开资源类型〔地质学家〕;
+2 -2
View File
@@ -9,7 +9,7 @@
class_name LevelRunner
extends RefCounted
signal node_entered(node: Dictionary, info: Dictionary) ## info 按 type:battle/boss={} · reward={}(三选一停点空载荷——内容经 present_node_curio 拉取) · rest={healed,hp,max_hp} · shop={gp}
signal node_entered(node: Dictionary, info: Dictionary) ## info 按 type:battle/boss={} · reward={}(三选一停点空载荷——内容经 present_node_curio 拉取) · rest={healed,hp,max_hp} · shop={gp}(pool 态加 shelf:true)
signal choices_presented(candidates: Array) ## 选路口(兑付确认后——候选节点引用数组,零自动推进)
signal step_row(row: Dictionary) ## 转发当前 战斗结算 结算行
signal round_started(round_index: int)
@@ -517,7 +517,7 @@ func _start_battle(node: Dictionary) -> bool:
_settlement = BattleOrchestrator.new(db, rng)
if treasure_inventory == null:
treasure_inventory = TreasureItems.new(db) # 惰性建——关卡域单实例(跨战斗延续)
_settlement.treasure_inventory = treasure_inventory # 库存注入(授予路由+use_treasure 编排消费面)
_settlement.reward_grant().treasure_inventory = self.treasure_inventory # 库存注入(经授予件出口直注——注入时序在宿主构造之后不变)
if _settlement != null:
_settlement.set_treasure_unlocked(_treasure_unlocked) # 解锁门控下传(装板摇号前)
# core↔core 接线只许方法 Callable(静态规则 S6——lambda 捕获 self 会延长
+6 -6
View File
@@ -187,8 +187,8 @@ func _vo_edges(nodes: Array, by_id: Dictionary, indeg: Dictionary, errs: Array)
## 阶段三·流事实域:入口集(入度 0)/终点集(无出边)与 Boss 终点判据、去路数形态
func _vo_flow_facts(nodes: Array, indeg: Dictionary, errs: Array) -> Dictionary:
var entries := []
var terminals := []
var entries: Array[int] = []
var terminals: Array[int] = []
for n in nodes:
var id0: int = int(n["id"])
var outs = n["next"]
@@ -277,8 +277,8 @@ func _vo_width_rules(nodes: Array, errs: Array) -> void:
## 计数规则③·参数档对照:战斗保底/入口数/终点数/层高(params 为空时跳过)
func _vo_params_check(params: Dictionary, nodes: Array, by_id: Dictionary, flow_ends: Array, errs: Array) -> void:
var entries: Array = flow_ends[0]
var terminals: Array = flow_ends[1]
var entries: Array[int] = flow_ends[0]
var terminals: Array[int] = flow_ends[1]
var battles := 0
for n in nodes:
if String(n["type"]) == "battle":
@@ -309,7 +309,7 @@ func _build(rg: RandomNumberGenerator) -> Dictionary:
var entries: Array[int] = []
for i in range(en):
entries.append(_mk("battle", 0))
var prevs: Array = _build_trunk(t, entries.duplicate())
var prevs: Array[int] = _build_trunk(t, entries.duplicate())
var terminals: Array[int] = []
for r in range(e):
@@ -326,7 +326,7 @@ func _build(rg: RandomNumberGenerator) -> Dictionary:
## 主链段(深度 1..t 逐层单链,主链段零 RNG):返回链末节点集(e>1 时收束为一处即归属大岔口)
func _build_trunk(t: int, prevs: Array) -> Array:
func _build_trunk(t: int, prevs: Array[int]) -> Array[int]:
for d in range(1, t + 1):
var nid := _mk("battle", d)
for p in prevs:
+6
View File
@@ -365,6 +365,12 @@ func outcome_of(ticket_id: int) -> String:
return "redeemed" if ticket_id < 0 else damage.outcome(ticket_id)
# 延迟单据入队(迭代 46 卡A T4——成功刷新的崩壁冲击群伤单走案C 同队列:授予段闸外唯一入口,
# 战斗段末 _board_deferred_settle 按开单序补提交统一结账;类变量零增员——复用 defer.queue)
func defer_ticket(t: Dictionary) -> void:
(defer.queue as Array).append(t)
# 主角直伤单据(反噬/治疗包负向——无类型必中,下限保底在控制器管线内)
func _submit_flat_hero(display: String, power: int, att: Dictionary, sync: bool) -> String:
var t := damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, String(att.get("source_class", "")), display,
@@ -0,0 +1,43 @@
class_name BattleRefreshFlow
extends RefCounted
## 换图刷新流程件(迭代 46 卡A T3/T4 自编排器拆出):刷新可用性门、折价扣费、换图执行与成功事实登记。
## 拆分边界(需求卡片 C13):本件只负责可用性、扣费、换图与成功事实登记——七件效果的算法留在
## 效果引擎、授予段与伤害控制器。调用路径=呈现层→编排器单行转发→本件→行动经济/装板壳/授予段。
## 反自动刷新(C11):只经玩家入口发起,禁 RAP 满自动刷——编排器公开面单行转发,无自动调用点。
## 成功序钉死(T4):折价扣 RAP→refresh_used+1→旧资源退役→旧板退役→装新板→授予段成功事实发射;
## 装板失败返回 false 且零效果——成功事实只在新板就位后发射。
## 折价成本读数(refresh_request 命中省力周转 apply_discount——只认 kind==apply_discount 且
## mode==refresh,取声明序第一命中,floor(base×cost_mult/100);无命中=表基线逐位不变。
## can_refresh 与 execute 同源取数〔V3——can 与执行同源判断〕;折扣只改变门槛与扣费,
## 不改变拒绝判据〔C2——每轮上限与相位判据照旧〕)
static func _effective_refresh_cost(oc: BattleOrchestrator) -> int:
var base := int(oc.db.run.action_economy.rap_refresh_cost)
for h in oc.fx_host().fx_emit("refresh_request", {"mode": "refresh", "base": base}):
if String(h.get("kind", "")) == "apply_discount" and String(h.get("mode", "")) == "refresh":
return int(floor(float(base) * float(int(h.get("cost_mult", 100))) / 100.0))
return base
## 可用性门(HUD 刷新按钮同源判据):战斗未终局、每轮上限未满、Active 相位、RAP 抵折后价。
static func can_refresh(oc: BattleOrchestrator) -> bool:
return oc.battle_result == "" \
and oc._facts.refresh_used < int(oc.db.run.action_economy.refresh_cap_per_round) \
and oc.action_economy.phase == oc.action_economy.PHASE_ACTIVE \
and oc.action_economy.rap >= _effective_refresh_cost(oc)
## 执行(玩家请求刷新唯一通道体):扣费受理后继续;扣费拒绝/门拒零效果零事实。
static func execute(oc: BattleOrchestrator) -> bool:
if not can_refresh(oc):
return false
if not oc.action_economy.request_refresh(_effective_refresh_cost(oc)):
return false
oc._facts.refresh_used += 1
oc._reward_economy.retire_mapping() # 旧图批次退役(地图战斗资源 Ready→retired;E6 新批重置前提)
oc.minesweeper.retire_board(true) # 旧板退役(轮内通道)
if not oc._install_and_reset(true): # 当场换图——失败即编排违例 fail-fast(零成功事实)
return false
oc._rounds._grant_seg.apply_board_refreshed(oc.round_index) # 成功事实:新板就位后一次发射
return true
@@ -0,0 +1 @@
uid://d2kjxlt5b2eqv
+2 -2
View File
@@ -26,7 +26,7 @@ func run(ri: int) -> void:
# 同一次 _settle 内,facts 已定。
var round_safe := 0
var round_mine := false
for evt in oc._facts_reveals:
for evt in oc._facts.reveals:
if evt.kind == "safe":
round_safe += 1
elif evt.kind == "mine":
@@ -53,7 +53,7 @@ func _hero_strike(ri: int) -> void:
var hero_target = oc._enemy_system.default_target()
if hero_alive and hero_target != null:
var ht := oc._damage.open_ticket(DamageController.KIND_HERO_ATTACK, "hero_attack", "主角攻击",
{"channel": "combat", "base_value": oc._hero_atk, "target_rule": DamageController.TARGET_FRONT_ALIVE,
{"channel": "combat", "base_value": oc._hero_atk(), "target_rule": DamageController.TARGET_FRONT_ALIVE,
"ri": ri, "origin_cell": Vector2i(-1, -1),
"attack_type": String(oc._enemy_system.hero_unit().get("attack_type", "normal"))})
oc._damage.submit(ht)
+71 -45
View File
@@ -6,7 +6,8 @@ extends RefCounted
# 效果施加归 BattleFxHost(段持三引用直调,零转发壳)——
# 本段只搬运时序,零算法改动(🔴 零行为变更契约:同种子完整局输出逐位一致)。
# run 编排骨架+批私有族(_reveal_batch/_terrain_batch/_seal_batch/_round_end_apply,段内施加面
# 再拆 _reveal_safe_apply/_reveal_mine_apply/_terrain_damage_reduce/_seal_hit_effects 等单面函数);
# 再拆 _reveal_safe_apply/_reveal_mine_apply/_seal_hit_effects 等单面函数);地形 damage_taken
# 消费随迭代 46 卡A T2 并入伤害控制器全局消费族(本段零本地减伤函数);
# kind 分派单点化(P1-1 同批):counter_add 五位三行组 / grant_material 三位兑付块
# 收 _apply_counter_hit / _apply_material_hit 两助手(分支本体逐字保留在助手内)。
@@ -32,9 +33,30 @@ func run(ri: int) -> void:
_round_end_apply(ri)
# 成功刷新事实发射+施加(迭代 46 卡A T4——BattleRefreshFlow 装新板后单点调用,每成功刷新恰一次;
# 拒绝刷新零事实〔C2〕。命中消费三面:counter_add→既有单点落账〔蓄力周转/石层缓冲/坑道小憩 计数〕;
# grant_gp→战斗内缓冲 escrow 入账+fx_grant_gp 行〔矿道红利——不绑清图奖励,C7〕;
# open_ticket channel=blast→崩壁冲击群伤单延迟入队〔案C 同队列,战斗段末按存活目标结账〕)
func apply_board_refreshed(ri: int) -> void:
var br_hits := fx_host.fx_emit("board_refreshed", {"round": ri})
for h in br_hits:
if String(h.get("kind", "")) == "counter_add":
_apply_counter_hit(h)
for h in br_hits:
if String(h.get("kind", "")) == "grant_gp":
reward_grant.escrow += int(h.get("value", 0))
oc._emit_row({"type": "effect", "source_class": "fx_grant_gp", "origin_cell": Vector2i(-1, -1)}, ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": reward_grant.escrow})
for h in br_hits:
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "blast":
var bb_t := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "fx_board_refresh", "崩壁冲击",
{"channel": "board", "base_value": int(h.get("value", 0)), "target_rule": DamageController.TARGET_ALL_ALIVE,
"ri": ri, "origin_cell": Vector2i(-1, -1)})
reward_grant.defer_ticket(bb_t)
# 开格/踩雷兑付批(轮 facts_reveals 资源事件序:safe 正向乘区兑付 / mine_hit 减半负向兑付)
func _reveal_batch(ri: int) -> void:
for evt in oc._facts_reveals:
for evt in oc._facts.reveals:
if not bool(evt.get("resource", false)):
continue # 封锁格(initial 通道)恒无资源——不入账(战斗结算 义务②)
var snap = evt.get("reward_snapshot")
@@ -61,7 +83,7 @@ func _reveal_safe(evt: Dictionary, snap: Dictionary, ri: int) -> void:
if is_new_type:
fx_host._fx_new_resource_types[ctype] = true
var dr_hits := fx_host.fx_emit("dig_reveal", {"cell": evt.cell, "round": ri,
"is_first_dig_of_battle": not oc._feat_first_dig_used,
"is_first_dig_of_battle": not oc._facts.feat_first_dig_used,
"terrain_kind": oc._harmful_variants(evt.get("variant_ids", []) as Array),
"is_edge_cell": evt.cell.x == 0 or evt.cell.y == 0
or evt.cell.x == board_b.width - 1 or evt.cell.y == board_b.height - 1,
@@ -91,8 +113,8 @@ func _reveal_safe_apply(dr_hits: Array, evt: Dictionary, ri: int) -> void:
if String(h.get("kind", "")) == "grant_material":
_apply_material_hit(h, evt.cell, ri)
var spell_hits: Array[Variant] = oc._hits_of(dr_hits, "spawn_spell")
if not spell_hits.is_empty() and not oc._feat_first_dig_used:
oc._feat_first_dig_used = true
if not spell_hits.is_empty() and not oc._facts.feat_first_dig_used:
oc._facts.feat_first_dig_used = true
var spell_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell", "奥术矢",
{"channel": "board", "base_value": int((spell_hits[0] as Dictionary).get("value", 0)),
"target_rule": DamageController.TARGET_FRONT_ALIVE, "ri": ri, "origin_cell": evt.cell, "attack_type": "magic"})
@@ -160,50 +182,35 @@ func _reveal_mine_apply(mh_hits: Array, snap: Dictionary, cell: Vector2i, ri: in
# 危险格恒无变异——布置段排除,双层叠罚不存在;封印格直挖照吃变异伤害——地形后果同 mine_hit 口径,
# 非奖励不作废;吞噬流资源兑付〔荆棘编织/地脉补偿〕中伤害形态行同样经案C闸暂存,非伤害形态照常当场)
func _terrain_batch(ri: int) -> void:
for evt in oc._facts_reveals:
for evt in oc._facts.reveals:
var vids: Array[Variant] = evt.get("variant_ids", [])
for vid in vids:
var vrow: Dictionary[Variant, Variant] = oc.db.combat.variation.row_by_id(String(vid))
if String(vrow.get("effect", "")) != "flat_damage":
continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面)
# 地形变异伤害单据——无类型必中,下限保底在控制器管线内
# damage_taken 发射在单据开立前:damage_reduce〔背水——hp_ratio when
# 面〕先收集再按减后值开单;counter_add〔保险丝/石肤〕+ ac_bonus〔石肤〕照常施加;
# grant_material〔荆棘/地脉〕移到单据提交后(兑付值序一致——收益不因减伤变化)
# damage_taken 统一发射(迭代 46 卡A T2——地形路径并入全局消费族 _emit_hero_damage_taken:
# 发射在单据开立前、减后值开单,组合消费含 shield〔全局受伤语义〕;
# hero_damage_consumed 标记入单据 context——战斗段末 _resolve_board_flat_hero 跳过重复发射;
# grant_material〔荆棘/地脉〕移到单据提交后(兑付值序一致——收益不因减伤变化))
var dt_amount := int(vrow.get("effect_value", 0))
var dt_hits := fx_host.fx_emit("damage_taken", {"kind": "terrain", "amount": dt_amount,
"source": String(vid), "cell": evt.cell, "round": ri, "terrain_kind": String(vid),
"hp_ratio": int(round(100.0 * float(int(oc._enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(oc._enemy_system.hero_unit().get("max_hp", 1))))))})
dt_amount = _terrain_damage_reduce(dt_hits, dt_amount)
var dt_res := oc._damage._emit_hero_damage_taken(dt_amount, "terrain", String(vid), evt.cell, ri)
dt_amount = int(dt_res["total"])
var vt := oc._damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)),
{"channel": "board", "base_value": dt_amount, "target_rule": DamageController.TARGET_HERO,
"ri": ri, "origin_cell": evt.cell, "context": {"variant_id": String(vid)}})
"ri": ri, "origin_cell": evt.cell, "context": {"variant_id": String(vid), "hero_damage_consumed": true}})
reward_grant.submit(vt, false)
for h in dt_hits:
for h in (dt_res["hits"] as Array):
if String(h.get("kind", "")) == "grant_material":
_apply_material_hit(h, evt.cell, ri)
# damage_taken 施加面(damage_reduce 减量收集〔背水——定值〕/ac_bonus〔石肤〕/counter_add〔保险丝/石肤〕;
# 返回减后 amount 交开单消费)
func _terrain_damage_reduce(dt_hits: Array, dt_amount: int) -> int:
for h in dt_hits:
match String(h.get("kind", "")):
"damage_reduce":
dt_amount = maxi(0, dt_amount - int(h.get("value", 0)))
"ac_bonus":
fx_host.apply_ac_bonus(h)
"counter_add":
_apply_counter_hit(h)
return dt_amount
# 封印判定批+战封印军规开单段(封印流:seal_judged 批后统一开单:轮末每面封印命中对最前存活敌
# 结算 board_typed 必中伤害〔复用零新配方;「封印所在巢穴敌」简化为 default_target 最前存活敌——
# 台账披露待追认〕;完美布防=本轮封印批全中(hit>0 且 miss==0)时封印伤 ×mult——全对推理奖励)。
# 伤害单据案C 暂存战斗段末兑付。
func _seal_batch(ri: int) -> void:
for evt in oc._facts_seals:
for evt in oc._facts.seals:
_judge_seal(evt, ri)
_seal_strike_batch(ri)
@@ -213,7 +220,7 @@ func _seal_batch(ri: int) -> void:
func _seal_strike_batch(ri: int) -> void:
var fs_hits := 0
var fs_misses := 0
for fevt in oc._facts_seals:
for fevt in oc._facts.seals:
if bool(fevt.get("hit", false)):
fs_hits += 1
else:
@@ -223,7 +230,7 @@ func _seal_strike_batch(ri: int) -> void:
var strike := _seal_strike_value(fs_fx, fs_hits, fs_misses)
if strike.is_empty():
return
for fevt in oc._facts_seals:
for fevt in oc._facts.seals:
if not bool(fevt.get("hit", false)):
continue
var st_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_seal_strike", "战封印军规",
@@ -259,6 +266,7 @@ func _seal_strike_value(fs_fx: Array, fs_hits: int, fs_misses: int) -> Dictionar
func _round_end_apply(ri: int) -> void:
var re_fx := fx_host.fx_emit("round_end", {"round": ri})
_round_end_blast_tickets(re_fx, ri)
_round_end_heal_value_counter(re_fx, ri) # 迭代 46 卡A T5——坑道小憩恢复兑付(先于清零行消费层值,声明序=heal 行在 counter set 行前)
for h in re_fx:
if String(h.get("kind", "")) == "counter_add":
_apply_counter_hit(h)
@@ -272,12 +280,30 @@ func _round_end_apply(ri: int) -> void:
var rap_pct: int = oc.attune_param_sum("rap_transfer_mult")
oc.action_economy.finish_round_stop(fx_host.engine.has_effect_on("round_end", "grant_ap", "transfer"),
rap_pct if rap_pct > 0 else 100)
if oc._facts_path == "cleared":
reward_grant.grant_clear_reward(ri, oc._board_from_midround_refresh, oc.perfect_sweep_earned())
if oc._facts.path == "cleared":
reward_grant.grant_clear_reward(ri, oc._facts.board_from_midround, oc.perfect_sweep_earned())
fx_host.flush_pending_reveals(ri) # 揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入)
oc._damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界)
# round_end heal value_counter 兑付(迭代 46 卡A T5——坑道小憩:层值=本轮成功刷新累计恢复点,
# units.heal 封顶、满血发未生效行;层清零由同事件 counter_add set 行承载〔声明序在后〕——本面
# 先于 counter_add 遍历消费层值即保序。行源=成功刷新事实族(无棋盘格——fx_board_refresh 豁免类))
func _round_end_heal_value_counter(re_fx: Array, ri: int) -> void:
for h in re_fx:
if String(h.get("kind", "")) != "heal" or String(h.get("value_counter", "")) == "":
continue
var layers := EffectEngine.counter(fx_host.counters_ctx, String(h.get("value_counter", "")))
if layers <= 0:
continue
var hu: Dictionary[Variant, Variant] = oc._enemy_system.hero_unit()
if int(hu.get("hp", 0)) >= int(hu.get("max_hp", 1)):
oc._emit_row({"type": "reward_ineffective", "source_class": "fx_board_refresh", "origin_cell": Vector2i(-1, -1)}, ri, "坑道小憩 未生效(满血)", 0, {"batch_key": String(h.get("value_counter", ""))})
continue
var healed: int = oc._enemy_system.heal(hu, layers)
oc._emit_row({"type": "heal", "source_class": "fx_board_refresh", "origin_cell": Vector2i(-1, -1)}, ri, "坑道小憩 回复", healed, {"target": "hero"})
# round_end blast 通道反打单(per-counter 积层读数×value=反打总量;source_class 用中性名,
# per-counter 反打通道保留泛用面;案C 暂存战斗段末兑付)
func _round_end_blast_tickets(re_fx: Array, ri: int) -> void:
@@ -305,10 +331,10 @@ func _judge_seal(evt: Dictionary, ri: int) -> void:
# 殉爆票据 → 施加面 → 雷区图谱额外兑付 → 基线正向兑付)
func _judge_seal_hit(evt: Dictionary, ri: int) -> void:
var cell: Vector2i = evt.cell
if oc._seal_consumed.has(cell):
if oc._facts.seal_consumed.has(cell):
return # 去重跳过;零明细行
oc._seal_consumed[cell] = true
oc._perfect_hit_true += 1
oc._facts.seal_consumed[cell] = true
oc._facts.mark_perfect_hit()
var fh_hits := fx_host.fx_emit("seal_hit", {"cell": cell, "round": ri,
"terrain_kind": oc._harmful_adjacent(cell)})
_seal_burst_tickets(fh_hits, cell, ri)
@@ -367,8 +393,8 @@ func _seal_preview_area(cell: Vector2i, ri: int) -> void:
var n_snap = oc._reward_economy.query_full_snapshot(n)
if n_snap is Dictionary and not types.has(String((n_snap as Dictionary).get("cell_type", ""))):
types.append(String((n_snap as Dictionary).get("cell_type", "")))
if oc.treasure_inventory != null and not types.is_empty():
oc.treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell))
if reward_grant.treasure_inventory != null and not types.is_empty():
reward_grant.treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell))
oc._emit_row({"type": "extended", "source_class": "fx_warning_beacon", "origin_cell": cell}, ri, "示警烟火·周围资源类型", 0, {"effect_id": "fx_preview_area", "types": types})
@@ -376,10 +402,10 @@ func _seal_preview_area(cell: Vector2i, ri: int) -> void:
# grant_ap mode:queue 下一轮兑现 / forgive_seal_miss 完美保险豁免 → 归因行 → 负向兑付)
func _judge_seal_miss(evt: Dictionary, ri: int) -> void:
var cell: Vector2i = evt.cell
oc._perfect_hit_false += 1
if oc._seal_miss_seen.has(cell):
oc._facts.mark_perfect_miss()
if oc._facts.seal_miss_seen.has(cell):
return # 每格实例一次去重
oc._seal_miss_seen[cell] = true
oc._facts.seal_miss_seen[cell] = true
# 危险直觉·守挂点(seal_missed 发射:契约 12 触发主语=「当回合发生的标错
# 动作」——去重闸=新旧标错判别器〔后续轮重判旧封印被闸挡回零触发〕;双条件②「结算时封印仍在
# 场」由发射面既有过滤承载〔被挖开的封印已随挖掘擦除零兑付〕;grant_ap mode:queue 下一轮
@@ -391,11 +417,11 @@ func _judge_seal_miss(evt: Dictionary, ri: int) -> void:
elif String(h.get("kind", "")) == "forgive_seal_miss":
# 完美保险(订阅式豁免:本战首次标错计一次豁免,只累加自有账
# _perfect_forgiven,绝不回写 _perfect_hit_false=契约 2 计数纯净;发射时机与消费点零改动=契约 3)
oc._perfect_forgiven += 1
oc._facts.mark_perfect_forgiven()
_apply_counter_hit(h) # 豁免同时记账(insurance_used battle 域——下次标错 when lt 1 不再命中)
oc._emit_row({"type": "extended", "source_class": "seal_miss", "origin_cell": cell}, ri, "完美保险·标错豁免", 0,
{"effect_id": "insurance_used", "forgiven": oc._perfect_forgiven})
var hl := "perfect_broken" if oc._perfect_hit_false - oc._perfect_forgiven == 1 else ""
{"effect_id": "insurance_used", "forgiven": oc._facts.perfect_forgiven})
var hl := "perfect_broken" if oc._facts.perfect_hit_false - oc._facts.perfect_forgiven == 1 else ""
oc._emit_row({"type": "seal_miss", "source_class": "seal_miss", "origin_cell": cell, "highlight": hl}, ri, "封印未中", 0, {})
var snap = oc._reward_economy.grant(cell)
if not (snap is Dictionary):
+106
View File
@@ -0,0 +1,106 @@
## 轮循环驱动件:每轮结算周期的唯一驱动——结算重入门闸(settling)、五段段序执行
## (段序骨架冻结于 SettlementTable.segment_order)、开轮摇号(共识点 B:技能选择前置到轮次开始,
## 摇定即本拍结算的选技)、踩雷计数账(转存时点=摇号轮末)。
## 单一变化原因:轮结算周期的推进结构(段序、重入闸、摇号时序)。
## 耦合边界:oc=宿主引用(五段段类既定同款直调形态)——经 oc 取 facts/协作件/行发射/出口分发;
## 五段构造参数与持有形态不变(grant/combat 三引用、aura/decay/verdict 单引用);判定段零改动
## (_exit_distribution 留宿主);随机只经 oc.rng 的 behavior_roll 封闭流(流名、开流时点、取号序逐字);
## 零信号连接、零 await;随编排器每战新建同灭。
class_name RoundCycle
extends RefCounted
var oc: BattleOrchestrator # 宿主引用(段类同款直调——facts/协作件/行发射/出口分发取数单点)
var mines_hits: Dictionary[Variant, Variant] = {"total": 0, "last": 0, "round": 0} # 踩雷计数(本场累计/上轮转存/当轮累计三读数合一账)
var _grant_seg = null # GrantSegment(①授予段——含封印判与 counter/grant 助手单点)
var _aura_seg = null # AuraSegment(②增益/减益段)
var _combat_seg = null # CombatSegment(③战斗段)
var _decay_seg = null # DecaySegment(④衰减段)
var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归宿主)
var _settling := false
func _init(host: BattleOrchestrator, fx_host_ref: BattleFxHost, reward_grant_ref: RewardGrant) -> void:
oc = host
# 结算五段接线(授予/战斗两段持三引用=宿主+两协作件;信号连接纪律不涉段〔段零信号连接〕——五行自宿主 _init 逐字迁入)
_grant_seg = GrantSegment.new(oc, fx_host_ref, reward_grant_ref)
_aura_seg = AuraSegment.new(oc)
_combat_seg = CombatSegment.new(oc, fx_host_ref, reward_grant_ref)
_decay_seg = DecaySegment.new(oc)
_verdict_seg = VerdictSegment.new(oc)
# 结算重入门闸+段序体(原宿主 _maybe_settle 三闸与 _settle 合体——闸序 settling→battle_result→phase
# 逐字;终局早退不回置 _settling,_battle_over 接管)
func settle_if_due() -> void:
if _settling or oc.battle_result != "":
return
if oc.action_economy.phase != oc.action_economy.PHASE_WAITING:
return
_settling = true
var ri := oc.round_index
# 段序编排(五段——段体归 game/core/settlement/,宿主只做行发射/开单原语单点)
_grant_seg.run(ri) # ① 授予段(案C 闸随段装载)
_aura_seg.run(ri) # ② 增益/减益段(续存实例组合行)
_combat_seg.run(ri) # ③ 战斗段(主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕)
_decay_seg.run(ri) # ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减)
var verdict: String = _verdict_seg.run(ri) # ⑤ 判定段(防御性优先级 hero 先判)
if verdict != "continue":
return # 终局:_settling 不回置——_battle_over 已接管
_settling = false
func is_settling() -> bool:
return _settling
# ---- 开轮摇号(共识点 B:技能选择前置到轮次开始,数值计算留结算段——原宿主函数逐字迁入)----
# 每实例:保底判定(连续执行普攻 ≥ M → 必出特殊技、特殊技内按权重、不受冷却限制)→ 排除冷却中技能
# (中签率 = 权重 ÷ 可摇权重合计——份额归一)→ 按行序逐敌从统一流顺序取号摇定本拍动作。
# 随机消费只在换算层(本方法 + SkillLottery);单位容器收确定值(零随机纪律——armor_roll 先例同构)。
# 统一流:behavior_roll 无子流派生向量(位置交错,单点定位由事件日志承载)。
# 摇定后、兑现前死亡 → 本拍技能作废。
func roll_round_actions() -> void:
var pity_m: int = int(oc.db.combat.skill.pity_threshold)
# facts 组包(开轮摇号前一次成型——last_round_end 空字典首轮映射 none 唯一落点;hero 事实不进 facts,
# 容器内直读主角行)
var facts := {
"round_index": oc.round_index,
"last_round_end": "none" if oc.action_economy.last_round_end().is_empty() else String(oc.action_economy.last_round_end().get("path_tag", "none")),
"mines_hit_last": int(mines_hits["last"]),
"mines_hit_total": int(mines_hits["total"]),
}
for u in oc._enemy_system.query_alive("enemy"):
# Boss 半血解锁闩(迭代14④ T4——开轮摇号前逐敌检查;一次过门单向闩;解锁出一次归因明细行
# 「XX 解锁了 YY」带 skill_unlock 高光键〔type=highlight 既有行型零新增〕,呈现层弹一次性
# 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机
# 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏)
for sid in oc._enemy_system.check_trigger_unlocks(u, facts):
var srow_u: Dictionary[Variant, Variant] = oc.db.combat.skill.row_by_id(sid)
oc._emit_row({"type": "highlight", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "highlight": "skill_unlock"},
oc.round_index, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))],
0, {"skill_id": sid})
# enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射,
# 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 trigger 行值)
var pct_v := 0
for pe in (u.get("pool", []) as Array):
var pe_d: Dictionary[Variant, Variant] = pe
if String(pe_d.get("skill_id", "")) == sid and (pe_d.get("trigger", {}) as Dictionary).has("value"):
pct_v = int((pe_d.get("trigger", {}) as Dictionary).get("value", 0))
break
# target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记,
# 主角攻击面 _resolve_hero_attack 通用消费——fx_atk_bonus 键加算进 base_value)
var eht_hits := oc._fx_host.fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v,
"is_boss": String(oc._facts.encounter.get("node_type", "")) == "boss"})
for h in eht_hits:
if String(h.get("kind", "")) == "target_attack_bonus":
u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0))
var entries: Array[Variant] = oc._enemy_system.rollable_skills(u, facts)
if oc._enemy_system.basic_streak(u) >= pity_m:
var specials: Array[Variant] = oc._enemy_system.special_skills(u, facts)
if not specials.is_empty():
entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合
var stream: RandomNumberGenerator = oc.rng.open_stream("behavior_roll")
oc._enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream))
oc._enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记)
mines_hits["last"] = mines_hits["round"] # 轮次真正结束转存(轮内部分结算不更新)
mines_hits["round"] = 0
+1
View File
@@ -0,0 +1 @@
uid://ce07smbypdcyj
+1 -1
View File
@@ -52,7 +52,7 @@ const TIER_QUOTAS := {"common": 70, "excellent": 25, "rare": 7, "legendary": 0}
@export var cap_segment: AdventureCapSegment = AdventureCapSegment.new() # 已拿上限段(curio_cap_* 9)
# —— 专长获取专长凭证(非 Boss 渠道的专长入口形态,奇物.md v2.7 渠道轴定位;
# 四口径契约④:拿到即开不可囤积/罕见只在概率不在价格/一局限购一次/计步保底连续 5 展示未出
# 四口径契约④:拿到即开不可囤积/稀有只在概率不在价格/一局限购一次/计步保底连续 5 展示未出
# 第 6 必出(计数=展示次数、刷新重展计入、节点与商店共享、触发清零、限购满不再触发)。
# 四参数全部 provisional 归曲线调参;limit/pity_step 系口径定死值,调参可复议)——
@export var feat_ticket_chance: int = 20 # [0,100] knob(provisional)——每次非 Boss 渠道展示专长凭证出现概率 %
+30
View File
@@ -0,0 +1,30 @@
## 战斗屏布局表 schema:分区几何与明细带高度的单行类型化读取(呈现层布局配置,非玩法数值)。
## 消费面 = BattleStageView / BattleLogView / HeroInfoPanelView / ApBarView(经 ConfigDb.presentation.battle_layout);
## 文档只写结构关系与表名,不复制数值。validate() 违例加载期致命(fail-fast)。
class_name BattleLayoutTable
extends Resource
@export var stage_h: int = 252 # B 区高(全宽战斗画面,含底部明细带)
@export var info_panel_w: int = 240 # C 区宽(左下角色信息列)
@export var ap_bar_h: int = 44 # D 区 AP 条高
@export var log_band_h_dig: int = 24 # 明细带挖掘期收起高(单行上轮摘要)
@export var log_band_h_settle: int = 84 # 明细带结算期展开高(实时结算流)
func validate() -> Array:
var errs: Array[Dictionary] = []
if stage_h < 120 or stage_h > 600:
errs.append({"category": "domain", "message": "stage_h 越域 [120,600]: %d" % stage_h})
if info_panel_w < 120 or info_panel_w > 480:
errs.append({"category": "domain", "message": "info_panel_w 越域 [120,480]: %d" % info_panel_w})
if ap_bar_h < 24 or ap_bar_h > 96:
errs.append({"category": "domain", "message": "ap_bar_h 越域 [24,96]: %d" % ap_bar_h})
if log_band_h_dig < 16 or log_band_h_dig > 80:
errs.append({"category": "domain", "message": "log_band_h_dig 越域 [16,80]: %d" % log_band_h_dig})
if log_band_h_settle < 24 or log_band_h_settle > 160:
errs.append({"category": "domain", "message": "log_band_h_settle 越域 [24,160]: %d" % log_band_h_settle})
if log_band_h_settle < log_band_h_dig:
errs.append({"category": "cross_field", "message": "log_band_h_settle(%d) < log_band_h_dig(%d)——展开态不得矮于收起态" % [log_band_h_settle, log_band_h_dig]})
if log_band_h_settle >= stage_h:
errs.append({"category": "cross_field", "message": "log_band_h_settle(%d) ≥ stage_h(%d)——明细带不得占满 B 区" % [log_band_h_settle, stage_h]})
return errs
@@ -0,0 +1 @@
uid://b3jh66yq4b7rp
+11
View File
@@ -0,0 +1,11 @@
[gd_resource type="Resource" script_class="BattleLayoutTable" load_steps=2 format=3]
[ext_resource type="Script" path="res://data/schemas/battle_layout_table.gd" id="1"]
[resource]
script = ExtResource("1")
stage_h = 252
info_panel_w = 240
ap_bar_h = 44
log_band_h_dig = 24
log_band_h_settle = 84
+210
View File
@@ -869,5 +869,215 @@ rows = [
}
}
]
},
{
"feat_id": "vein_reserve",
"display_name": "矿脉储能",
"rarity": "excellent",
"cap": 1,
"desc": "挖格储能:每次受理的付费挖格 +1 储备点(RAP)",
"triggers": [
{
"on": "dig_paid",
"do": {
"kind": "grant_reserve_ap",
"value": 1
}
}
]
},
{
"feat_id": "easy_swap",
"display_name": "省力周转",
"rarity": "excellent",
"cap": 1,
"desc": "刷新半价:弃图刷新成本为基线 50%",
"triggers": [
{
"on": "refresh_request",
"do": {
"kind": "apply_discount",
"mode": "refresh",
"cost_mult": 50
}
}
]
},
{
"feat_id": "wall_blast",
"display_name": "崩壁冲击",
"rarity": "excellent",
"cap": 1,
"desc": "刷后群伤:成功刷新后全体存活敌各受 2 点",
"triggers": [
{
"on": "board_refreshed",
"do": {
"kind": "open_ticket",
"channel": "blast",
"value": 2
}
}
]
},
{
"feat_id": "mine_profit",
"display_name": "矿道红利",
"rarity": "rare",
"cap": 1,
"desc": "刷后GP:每次成功刷新固定 +1 GP",
"triggers": [
{
"on": "board_refreshed",
"do": {
"kind": "grant_gp",
"value": 1
}
}
]
},
{
"feat_id": "stone_mitigation",
"display_name": "石层缓冲",
"rarity": "common",
"cap": 1,
"desc": "当轮减伤:本轮成功刷新后,每次受伤固定 −2(多刷不叠加)",
"triggers": [
{
"on": "board_refreshed",
"do": {
"kind": "counter_add",
"counter_id": "round_swap_guard",
"value": 1
}
},
{
"on": "damage_taken",
"when": [
{
"fact": "counter",
"id": "round_swap_guard",
"op": "gte",
"value": 1
}
],
"do": {
"kind": "damage_reduce",
"value": 2
}
}
]
},
{
"feat_id": "charge_rotation",
"display_name": "蓄力周转",
"rarity": "excellent",
"cap": 1,
"desc": "刷后蓄力:每次刷新 +1 层(上限 3),下一次攻击消费全部层数",
"triggers": [
{
"on": "board_refreshed",
"when": [
{
"fact": "counter",
"id": "swap_charge_layers",
"op": "lt",
"value": 3
}
],
"do": {
"kind": "counter_add",
"counter_id": "swap_charge_layers",
"value": 1
}
},
{
"on": "attack_commit",
"when": [
{
"fact": "counter",
"id": "swap_charge_layers",
"op": "gte",
"value": 1
}
],
"do": {
"kind": "attack_bonus",
"value_counter": "swap_charge_layers"
}
},
{
"on": "attack_commit",
"when": [
{
"fact": "counter",
"id": "swap_charge_layers",
"op": "gte",
"value": 1
}
],
"do": {
"kind": "counter_add",
"counter_id": "swap_charge_layers",
"set": 0
}
}
]
},
{
"feat_id": "tunnel_rest",
"display_name": "坑道小憩",
"rarity": "common",
"cap": 1,
"desc": "当轮恢复:每次刷新累计 1 点(上限 3),本轮轮末合计回复",
"triggers": [
{
"on": "board_refreshed",
"when": [
{
"fact": "counter",
"id": "round_swap_rest",
"op": "lt",
"value": 3
}
],
"do": {
"kind": "counter_add",
"counter_id": "round_swap_rest",
"value": 1
}
},
{
"on": "round_end",
"when": [
{
"fact": "counter",
"id": "round_swap_rest",
"op": "gte",
"value": 1
}
],
"do": {
"kind": "heal",
"value_counter": "round_swap_rest"
}
},
{
"on": "round_end",
"when": [
{
"fact": "counter",
"id": "round_swap_rest",
"op": "gte",
"value": 1
}
],
"do": {
"kind": "counter_add",
"counter_id": "round_swap_rest",
"set": 0
}
}
]
}
]
+3 -3
View File
@@ -7,9 +7,9 @@ extends RefCounted
## 三束按域收拢(u21——同 u20 形;views=视图句柄/getters=查询 Callable 活引用/flow=编排 Callable):
## 原 12 参长签名收 4 参——字段即原构造参数逐一同名迁移,键面=消费方改址合同。
var db
var views: Dictionary = {} # {camp_overlay, shop_overlay, toast, start_overlay, logger}
var getters: Dictionary = {} # {meta_get, save_get, runner_get}
var flow: Dictionary = {} # {open_modal, close_modal, refresh_presentations}
var views: Dictionary = {} # {camp_overlay, shop_overlay, toast, start_overlay, logger} # type-hint: ignore(三束异构混载——视图句柄/查询 Callable/编排 Callable)
var getters: Dictionary = {} # {meta_get, save_get, runner_get} # type-hint: ignore(同上——Callable 束)
var flow: Dictionary = {} # {open_modal, close_modal, refresh_presentations} # type-hint: ignore(同上——Callable 束)
func _init(db_ref, views_bundle: Dictionary, getters_bundle: Dictionary, flow_bundle: Dictionary) -> void:
+3 -3
View File
@@ -7,9 +7,9 @@ extends RefCounted
## 三束按域收拢(u20——views=视图句柄/getters=查询 Callable 活引用〔禁快照〕/flow=编排 Callable):
## 原 16 参长签名收 4 参——字段即原构造参数逐一同名迁移,键面=消费方改址合同。
var db
var views: Dictionary = {} # {curio_overlay, end_overlay, board_view, toast}
var getters: Dictionary = {} # {meta_get, save_get, adventure_get, runner_get, adventure_mode_get}
var flow: Dictionary = {} # {open_modal, open_modal_after_playback, close_modal, refresh_presentations, fatal_screen, gp_summary}
var views: Dictionary = {} # {curio_overlay, end_overlay, board_view, toast} # type-hint: ignore(三束异构混载——视图句柄/查询 Callable/编排 Callable)
var getters: Dictionary = {} # {meta_get, save_get, adventure_get, runner_get, adventure_mode_get} # type-hint: ignore(同上——Callable 束)
var flow: Dictionary = {} # {open_modal, open_modal_after_playback, close_modal, refresh_presentations, fatal_screen, gp_summary} # type-hint: ignore(同上——Callable 束)
func _init(db_ref, views_bundle: Dictionary, getters_bundle: Dictionary, flow_bundle: Dictionary) -> void:
@@ -78,7 +78,7 @@ func on_presented(payload: Dictionary) -> void:
return
ctx.views.end_overlay.open_victory(Narrator.battle_end_text("victory", false), "继续",
ctx.views.board_view.get_global_rect(),
"%s\n%s" % [String(ctx.flow.gp_summary.call()), Narrator.bonus_keystone_line(payload)])
{"gp_summary": "%s\n%s" % [String(ctx.flow.gp_summary.call()), Narrator.bonus_keystone_line(payload)]})
ctx.flow.open_modal_after_playback.call(ctx.views.end_overlay)
ctx.flow.refresh_presentations.call()
+5 -5
View File
@@ -196,11 +196,11 @@ static func outcome_text(outcome: Dictionary) -> String:
## 统计束收拢(u19——stats 六键 {wins, dmg_all, hp_left_win, hp_boss, rounds_all, death_nodes})
static func tune_sim_report(n: int, atk_pct: int, hp_pct: int, stats: Dictionary, base_hp: int) -> String:
var wins: int = stats.get("wins", 0)
var dmg_all: Array = stats.get("dmg_all", [])
var hp_left_win: Array = stats.get("hp_left_win", [])
var hp_boss: Array = stats.get("hp_boss", [])
var rounds_all: Array = stats.get("rounds_all", [])
var death_nodes: Dictionary = stats.get("death_nodes", {})
var dmg_all: Array = stats.get("dmg_all", []) # type-hint: ignore(stats 束 Variant 出值——元素类型归模拟域写入面,未参数化)
var hp_left_win: Array = stats.get("hp_left_win", []) # type-hint: ignore(同上)
var hp_boss: Array = stats.get("hp_boss", []) # type-hint: ignore(同上)
var rounds_all: Array = stats.get("rounds_all", []) # type-hint: ignore(同上)
var death_nodes: Dictionary = stats.get("death_nodes", {}) # type-hint: ignore(同上——键为败北节点标签)
var lines: Array[String] = []
lines.append("[b]模拟 N=%d(atk+%d%% / hp+%d%%)[/b]" % [n, atk_pct, hp_pct])
lines.append("关卡完成率 [color=#%s]%.0f%%[/color](%d/%d)" % [UiKit.HEX_LOG_WIN, 100.0 * float(wins) / float(maxi(1, n)), wins, n])
+77 -265
View File
@@ -1,7 +1,8 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 组合根:唯一的模式流转与 core↔view 接线处(垂直切片验证:线性第一关承载元进度循环)。
## db.hero 运行期只读,加成经 runner.set_hero_override / debug_session 生效值入战斗。
## 禁链式穿透 settlement.*(相位读取只走 presentation_snapshot);模态开合只走 _open_modal/_close_modal。
## 禁链式穿透 settlement.*(相位读取只走 presentation_snapshot);模态开合只走 ui.modals.flow(注册面编排口)。
## 界面句柄与层清单在 ui 注册面(ui_registry.gd)——屏壳/战区/模态三域句柄与 LAYER_CONTRACT 单点宿主。
## 局态全在 run_scope.gd 节点,禁在屏/视图侧持有局态引用;autoload 节点路径常量单点在 AUTOLOAD_PATH_*。
extends Control
@@ -15,10 +16,8 @@ const AUTOLOAD_PATH_META := "/root/MetaProgression"
const UiAssembler := preload("res://presentation/ui_assembler.gd")
const Narrator := preload("res://presentation/battle_narrator.gd")
const ModalStack := preload("res://presentation/modal_stack.gd")
const TuneSimController := preload("res://presentation/tune_sim_controller.gd")
const DebugSessionScript := preload("res://presentation/debug_session.gd")
const RowPacer := preload("res://presentation/row_pacer.gd") # 结算行排播泵
const TreasureInteractController := preload("res://presentation/treasure_interact_controller.gd") # 秘宝两步交互
const CurioInteractController := preload("res://presentation/curio_interact_controller.gd") # 三选一与替换子交互
const CampShopPresenter := preload("res://presentation/camp_shop_presenter.gd") # 营地/商店投影
@@ -31,33 +30,6 @@ const MenuScreenScene := preload("res://presentation/scenes/menu_screen.tscn")
const CampScreenScene := preload("res://presentation/scenes/camp_screen.tscn")
const AdventureScreenScene := preload("res://presentation/scenes/adventure_screen.tscn")
# ==================== 视觉层合同(唯一事实源——树序=登记序=z 序;改序只改此表) ====================
# kind: shell=屏壳容器(成员随壳不占独立树位;level_screen 例外另占登记位)/modal=模态层(入登记册,
# tags 随登记)/state=全屏态(占树位不入登记册)/node=非视觉节点(占树位不入登记册)。
# host=宿住屏壳(树序随宿主、登记序按本合同);shell=模态成员的屏壳(成员与屏壳同槽:屏壳占该树位、
# 成员占该登记位)。落位=_apply_layer_contract(全仓唯一层序落位点,add_child 之后统一排序+登记)。
const LAYER_CONTRACT: Array[Dictionary] = [
{"key": "adventure_screen", "kind": "shell"},
{"key": "menu_screen", "kind": "shell"},
{"key": "row_pacer", "kind": "node"},
{"key": "legend_overlay", "kind": "modal", "tags": []},
{"key": "node_overlay", "kind": "modal", "tags": []},
{"key": "level_screen", "kind": "modal", "tags": [ModalStack.TAG_SCREEN_STATE], "host": "adventure_screen"},
{"key": "curio_overlay", "kind": "modal", "tags": []},
{"key": "end_overlay", "kind": "modal", "tags": []},
{"key": "shop_overlay", "kind": "modal", "tags": []},
{"key": "camp_overlay", "kind": "modal", "tags": [], "shell": "camp_screen"},
{"key": "settings_overlay", "kind": "modal", "tags": []},
{"key": "status_panel", "kind": "modal", "tags": []},
{"key": "career_overlay", "kind": "modal", "tags": []},
{"key": "save_select_overlay", "kind": "modal", "tags": []}, # 选档界面(营地交互重做卡③——登记生涯同档)
{"key": "inter_overlay", "kind": "state", "tags": []},
{"key": "confirm_dialog", "kind": "modal", "tags": []},
{"key": "debug_overlay", "kind": "modal", "tags": [ModalStack.TAG_NON_LOCKING]},
{"key": "debug_badge", "kind": "float"}, # 调试入口全局浮层——末位=最顶绘制序;
# float 类不注册 ModalStack(open() 遮挡记账只隐藏登记层)→ 任何 UI 打开都不遮它
]
var db
var run_scope = null # 会话状态承载(run_scope.gd;_ready 即建闲时态,永非 null)
# SaveManager/MetaProgression 升格 autoload——成员名保留为「autoload 读视图」
@@ -71,99 +43,24 @@ var meta: MetaProgressionCore:
return get_node(AUTOLOAD_PATH_META)
var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件)
var _debug_entry := false # 调试会话门控(缺省 false=发布态:角标恒隐、浮窗无 UI 开启口;沿革查 git log)
var _seed_edit: LineEdit
var confirm_dialog: ConfirmDialog # 通用确认弹窗(消费方编排——现行消费方=选档删档确认;新冒险互斥确认已随卡②主菜单撤出,归卡④大门)
var ui: UiRegistry # 界面注册面(ui_registry.gd——界面句柄/层序合同/模态编排句柄的单点宿主)
var _confirm_consumer := "" # 确认弹窗消费方状态量(契约 3 编排——open 前设、分派后清)
var _confirm_consumer_slot := 0 # 消费方附随槽号(delete_slot 消费方专用)
var curio_overlay: CurioPickOverlay # 提升三选一覆盖层(level-clear.md v2 选择面承载;奖励节点共用)
var _start_overlay: Control
var _end_overlay: BattleEndOverlay
var _inter_overlay: Control
var _inter_label: Label
var level_screen: LevelMapScreen # 关卡画面句柄(指向 adventure_screen.level_screen)
var _battle_root: Control # 战斗屏宿主句柄(指向 adventure_screen.battle_root)
# 调试模式编排(单场/连续两层——状态机与战斗生命周期归 debug_session.gd,本类只接线与出 UI)
var _debug = null
# 视图(views/)——battle.md 三区(节点链进度
# 由关卡画面本体选路覆盖层承载)
var stage: BattleStageView # B 区(含 log_view 明细带)——句柄指向 adventure_screen
var log_view: BattleLogView
var info_panel: HeroInfoPanelView # C 区(攻击/AC 数据框+两预留区)
var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/fx)
var board_view: BoardView # 探针兼容别名(= board_zone.board_view)
var ap_bar: ApBarView # 探针兼容别名(= board_zone.ap_bar)
var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道/光带——A–D 之上、模态之下)
# 三屏壳实例(屏成员经本类句柄暴露;探针挂载假设 main.X 一级路径)
var _menu_screen = null # MenuScreen(开始菜单屏壳)
var _camp_screen = null # CampScreen(营地屏壳)
var _adventure_screen = null # AdventureScreen(冒险屏壳——内含关卡地图/战斗两容器)
# 拆件后的组合根持有面(职责见各类头注;装配见 _build_ui)
var _row_pacer = null # 结算行排播泵(row_pacer.gd——本类只入队/排空/读队列)
var _treasure_ui = null # 秘宝两步交互(treasure_interact_controller.gd——本类只接线与转发意图)
var _bonus = null # 三选一与替换子交互(curio_interact_controller.gd——本类只接线与回调注入)
var _camp_pres = null # 营地/商店投影(camp_shop_presenter.gd——本类只接线、探针缝保缝转发)
# 排播队列只读委托面(探针/测试读 main._row_queue——与 RowPacer 队列共享同一 Array 引用)
var _row_queue: Array[Variant]:
get:
return _row_pacer.pending_rows()
# 模态 overlay(z 序 = 登记序;pattern 6 层级纪律)
var legend_overlay: LegendOverlay
var node_overlay: NodeOverlay
var shop_overlay: ShopOverlay # 商店覆盖层(商店房节点兑付呈现)
var camp_overlay: CampOverlay # 营地覆盖层句柄(指向 camp_screen.camp_overlay)
var settings_store # 系统级设置独立存储(音量/全屏/减动效)
var settings_overlay: SettingsOverlay # 设置覆盖层(最小设置面)
var status_panel: StatusPanel # 局内状态面板·只读(钥石余额+营地三柜台进度查看;登记设置层同档)
var career_overlay: CareerOverlay # 生涯页·只读全屏页(跨局汇总六类九项;登记营地同档〔设置层旁、确认弹窗之前〕)
var save_select_overlay: SaveSelectOverlay # 选档界面·3 存档位(新档/进档/删档——营地交互重做卡③;登记生涯同档)
var debug_overlay: DebugOverlay # 调试浮窗(种子/数值调整/批量模拟/两层调试入口收一)
var debug_badge: Button # 调试浮层入口角标(UI 最顶层浮层——LAYER_CONTRACT 末位 float 类,任何 UI 不遮挡)
var _modal_flow = null # 模态编排纯逻辑类(modal_flow.gd——依赖仅 ModalStack+查询 Callable)
var _modals = null # 模态层级状态机(=_modal_flow.stack——探针 _list 登记序断言缝保留)
var _tune_sim = null # 调试浮窗批量模拟控制器(tune_sim_controller.gd)
# 结算框挂起态属性化(探针/冒烟直读写接缝语义零变化——读=armed,写=arm/disarm;计时器随会话住
# run_scope.settlement_stop_timer)
var _settlement_stop_pending: bool:
get:
return run_scope.settlement_stop_timer.is_armed() if run_scope != null else false
set(v):
if run_scope == null:
return
if v:
run_scope.settlement_stop_timer.arm()
else:
run_scope.settlement_stop_timer.disarm()
# 锁期点击重放(v3.1):延迟一帧重入唯一动作通道——此刻队列空锁已解,按普通动作受理
#(局面在演播期无变化:逻辑先行、演播只是回放——点击语义保真;_row_queue=RowPacer 委托面,
# 排播泵本体在 row_pacer.gd)。
var _deferred_modal: Control: # 演播后开窗挂起只读视图(t17_defer_probe 挂起态断言缝;本体归 _modal_flow)
get:
return _modal_flow._deferred if _modal_flow != null else null
# 演播后排窗(伤害过程演完才切结算窗口):排播队列在播=挂起,演播完边沿
# (_on_pump_ended 派发)再开——旧路径 _open_modal 前置 flush 会把击杀轮的伤害/死亡演出
# 同帧倒掉(「直接跳战斗胜利」缺陷本体)。队列空=立即开(行为同旧路径)。
func _open_modal_after_playback(m: Control) -> void:
_modal_flow.open_after_playback(m) # 演播后排窗挂起编排归 ModalFlow
# 演播完边沿派发(row_pacer.pump_ended 注入口——先开挂起 modal,后锁期点击重放):
func _on_pump_ended() -> void:
_modal_flow.pump_ended() # 派发编排归 ModalFlow(重放经注入 Callable 回本类)
func _replay_locked_click() -> void:
if run_scope.locked_click.is_empty() or not _row_queue.is_empty():
if run_scope.locked_click.is_empty() or not ui.screens.row_pacer.pending_rows().is_empty():
return
var click: Dictionary[Variant, Variant] = run_scope.locked_click
run_scope.locked_click = {}
@@ -173,7 +70,7 @@ func _replay_locked_click() -> void:
# 会话生命周期:_ready 建闲时态(永非 null——全组合根零空判责),
# 开局/续玩/调试两层各建新会话,旧会话整节点 queue_free()(帧末回收;新节点先挂树无真空窗)。
func _begin_session(adventure_session: bool) -> void:
_row_pacer.flush() # 会话更替即清队(待播行不住局态——机制保证,不靠时序约定)
ui.screens.row_pacer.flush() # 会话更替即清队(待播行不住局态——机制保证,不靠时序约定)
if run_scope != null:
run_scope.queue_free()
run_scope = RunScope.new()
@@ -194,10 +91,11 @@ func _ready() -> void:
settings_store = SettingsStore.new()
settings_store.load_settings()
settings_store.apply()
ui = UiRegistry.new() # 注册面先于装配建(_build_ui 八步向容器登记——装配时序零变化)
_build_ui()
_begin_session(false) # 闲时会话(gp_ledger 等探针注入面自 _ready 起恒在——ui_probe 合成段时序)
_start_overlay.visible = true
debug_badge.visible = _debug_entry # 浮层角标可见性须门控落定后赋值(构建/连接在 _build_ui);_debug_entry 缺省 false=发布态恒隐
ui.screens.start_overlay.visible = true
ui.screens.debug_badge.visible = _debug_entry # 浮层角标可见性须门控落定后赋值(构建/连接在 _build_ui);_debug_entry 缺省 false=发布态恒隐
# boot 菜单优先——启动不自动开浮窗(开始屏不得被调试面板盖住);
# 调试入口=UI 最顶层全局浮层角标(LAYER_CONTRACT 末位 float 类,任何 UI 不遮挡)
@@ -206,94 +104,13 @@ func _ready() -> void:
func _build_ui() -> void:
MainUiBuild.build(self)
debug_overlay.diagnose_requested.connect(_on_diagnose_pressed) # 诊断转储入口
ui.modals.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed) # 诊断转储入口
func _fatal_screen(msg: String) -> void:
MainUiBuild.fatal_screen(self, msg)
# ==================== 模态 overlay 层级管理(pattern 6 / 缺陷⑤——状态机归 modal_stack.gd) ====================
func _register_modal(m: Control, tags: Array = []) -> void:
# 撑满宿主(Control 默认锚 = 0×0——全屏变暗层与居中布局整体失效;route 选路层预览截图实证后
# 统一收口:六个模态同病,探针经信号直发绕过真实点击从未暴露。anchors_and_offsets 而非 anchors
# ——后者会把控件重置回最小内容尺寸,ui_assembler 头注同款纪律)
m.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_modal_flow.register(m, tags) # 排除组随层登记(non_locking=调试浮窗/screen_state=关卡地图屏)
# 合同层寻址(key→节点;纯查表不含层序语义——层序唯一事实源=LAYER_CONTRACT)
func _layer_node(key: String) -> Node:
match key:
"adventure_screen": return _adventure_screen
"menu_screen": return _menu_screen
"row_pacer": return _row_pacer
"legend_overlay": return legend_overlay
"node_overlay": return node_overlay
"level_screen": return level_screen
"curio_overlay": return curio_overlay
"end_overlay": return _end_overlay
"shop_overlay": return shop_overlay
"camp_screen": return _camp_screen
"camp_overlay": return camp_overlay
"settings_overlay": return settings_overlay
"status_panel": return status_panel
"career_overlay": return career_overlay
"save_select_overlay": return save_select_overlay
"inter_overlay": return _inter_overlay
"confirm_dialog": return confirm_dialog
"debug_overlay": return debug_overlay
"debug_badge": return debug_badge
return null
# 合同装配(_build_ui 尾调用):按 LAYER_CONTRACT 序统一落位——直接子层 move_child 到合同槽、
# 模态层按合同序登记;宿住层(host)树序随宿主只登记;成员层(shell)与屏壳同槽(屏壳占树位、成员占登记位)。
# 本函数是全仓唯一层序落位点(move_child 单口)——改层序只改 LAYER_CONTRACT。
func _apply_layer_contract() -> void:
var slot := 0
for entry: Dictionary in LAYER_CONTRACT:
var tags: Array[Variant] = entry.get("tags", [])
if entry.has("shell"):
var shell: Node = _layer_node(String(entry["shell"]))
var member: Node = _layer_node(String(entry["key"]))
if shell != null:
move_child(shell, slot)
slot += 1
if member != null:
_register_modal(member as Control, tags)
else:
push_error("[合同装配] 层缺失: %s" % String(entry["key"]))
continue
var node: Node = _layer_node(String(entry["key"]))
if node == null:
push_error("[合同装配] 层缺失: %s" % String(entry["key"]))
continue
if entry.has("host"):
_register_modal(node as Control, tags)
continue
move_child(node, slot)
slot += 1
if String(entry["kind"]) == "modal":
_register_modal(node as Control, tags)
func _open_modal(m: Control) -> void:
_modal_flow.open(m) # 挂起兜底/排播排空/遮挡记账/开侧同步编排归 ModalFlow
func _close_modal(m: Control) -> void:
_modal_flow.close(m) # 关闭恢复+关侧同步归 ModalFlow(同上)
func _on_modal_closed(m: Control) -> void:
_close_modal(m)
func _input_locked() -> bool:
# 模态/非战斗期棋盘输入锁(判据编排归 ModalFlow.locks_input:互斥域随层登记
# [non_locking=调试浮窗]+ 三例外经查询 Callable 注入;本函数留探针缝)
return _modal_flow.locks_input()
# ==================== 冒险模式入口(唯一试玩入口)====================
func _start_adventure_from_menu() -> void:
@@ -301,7 +118,7 @@ func _start_adventure_from_menu() -> void:
# 弹窗本体与两分支逻辑归卡④大门承接)。本函数为探针直调中间态保留(mn10——冒险段探针
# 直调位点逐个登记实现报告「已知中间态」;卡④到位后由大门两分支换代)。无激活态写盘拒绝
# 由卡① sv3 承接(结算不落盘)。
_start_overlay.visible = false
ui.screens.start_overlay.visible = false
_start_adventure()
@@ -309,16 +126,16 @@ func _start_adventure_from_menu() -> void:
#(确认才删、取消只关窗——裁决 3)。"new_adventure" 消费方已随卡② 主菜单确认链摘除退役
#(mn6/V5——主菜单零确认残留),分派机制保留(卡④ 到位接入第三消费方不返工)。
func _on_confirm_accepted() -> void:
_close_modal(confirm_dialog) # 模态栈单口
ui.modals.flow.close(ui.modals.confirm_dialog) # 模态栈单口
match _confirm_consumer:
"delete_slot":
save_manager.slots.delete_slot(_confirm_consumer_slot)
save_select_overlay.open_save_select(HudPresenter.save_select_data(save_manager)) # 就地刷新(该槽回空态)
ui.modals.save_select_overlay.open_save_select(HudPresenter.save_select_data(save_manager)) # 就地刷新(该槽回空态)
_confirm_consumer = ""
func _on_confirm_declined() -> void:
_close_modal(confirm_dialog) # 只关窗(取消无状态变更——裁决 3:取消不删);关侧恢复归 ModalFlow
ui.modals.flow.close(ui.modals.confirm_dialog) # 只关窗(取消无状态变更——裁决 3:取消不删);关侧恢复归 ModalFlow
_confirm_consumer = ""
@@ -334,16 +151,16 @@ func _resume_from_menu() -> void:
if not save_manager.has_breakpoint():
return
var bp: Dictionary[Variant, Variant] = save_manager.breakpoint_snapshot()
_close_modal(debug_overlay) # 开冒险路径显式关浮窗(续玩同口径)
_start_overlay.visible = false
ui.modals.flow.close(ui.modals.debug_overlay) # 开冒险路径显式关浮窗(续玩同口径)
ui.screens.start_overlay.visible = false
_begin_session(true)
_wire_adventure()
run_scope.level_active = true
_sync_status_entry() # 续玩恢复点亮状态面板入口角标
level_screen.set_abandon_available(true)
log_view.clear()
ui.modals.level_screen.set_abandon_available(true)
ui.battle.log_view.clear()
logger.start_session(int(bp.get("master_seed", 0)))
run_scope.adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct())
run_scope.adventure.set_hero_override(ui.modals.debug_overlay.atk_pct(), ui.modals.debug_overlay.hp_pct())
# 同调三键随续玩下传(build_adventure_params 统一构造,与开新局同源同口径)
if not run_scope.adventure.resume_adventure(bp, build_adventure_params(save_manager, meta,
{"treasure_unlocked": _treasure_ui.unlocked_set()})):
@@ -379,16 +196,16 @@ static func build_adventure_params(save_manager_ref: SaveManagerCore, meta_ref:
# 冒险模式(三关连探——AdventureRunner 编排;转发信号签名与 runner 一致,接线照原样对接;
# 信号连接只在组合根)
func _start_adventure() -> void:
_close_modal(debug_overlay) # 开冒险路径显式关浮窗(调试浮窗默认开态下开局不带窗;对局中经角标再开)
ui.modals.flow.close(ui.modals.debug_overlay) # 开冒险路径显式关浮窗(调试浮窗默认开态下开局不带窗;对局中经角标再开)
_begin_session(true)
_wire_adventure()
run_scope.level_active = true
_sync_status_entry() # 进对局点亮状态面板入口角标
level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期)
log_view.clear()
ui.modals.level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期)
ui.battle.log_view.clear()
var seed_val := _seed_value()
logger.start_session(seed_val)
run_scope.adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct())
run_scope.adventure.set_hero_override(ui.modals.debug_overlay.atk_pct(), ui.modals.debug_overlay.hp_pct())
# 永久强化注入:冒险开始参数面——meta 现算 override 显式传参(全零 = 白板)
# 开局参数面(同调三键同源构造——确定性承载=固定种子+参数档,首局教学图由种子域三分判定给 42)
if not run_scope.adventure.start_adventure(seed_val, build_adventure_params(save_manager, meta,
@@ -435,33 +252,33 @@ func _on_inter_next_pressed() -> void:
func _show_board_fx_for_node(node_type: String = "") -> void:
# Boss 遭遇变体:紫赤影子爬棋盘一次性占位演出(§2.6)
if node_type == "boss":
board_zone.fx.play_boss_shadow()
ui.battle.board_zone.fx.play_boss_shadow()
func _on_battle_finished(payload: Dictionary) -> void:
# 仅调试模式接线(冒险的关卡结束归 AdventureRunner._on_level_finished——大奖励/冒险终局分派)
var result: String = String(payload.get("result", ""))
_sync_settlement_visuals() # → 全关(battle_over 反馈面)
HudPresenter.battle_end_feedback(stage, ap_bar, true) # 灰化/idle 回正/RAP 清零/状态全清——HUD 单通道
HudPresenter.battle_end_feedback(ui.battle.stage, ui.battle.board_zone.ap_bar, true) # 灰化/idle 回正/RAP 清零/状态全清——HUD 单通道
var decision: Dictionary[Variant, Variant] = _debug.on_battle_finished(payload)
match String(decision.outcome):
"next_layer":
_inter_label.text = "第 %d 层通过!\nHP %d/%d 带入下一层" % [
ui.screens.inter_label.text = "第 %d 层通过!\nHP %d/%d 带入下一层" % [
int(decision.layer), int(decision.carry_hp), int(db.combat.hero.max_hp)]
_inter_overlay.visible = true
ui.screens.inter_overlay.visible = true
_refresh_presentations()
return
"level_end":
_end_overlay.open_victory(Narrator.battle_end_text("victory", true),
"返回", board_view.get_global_rect(), _end_victory_opts(payload))
ui.modals.end_overlay.open_victory(Narrator.battle_end_text("victory", true),
"返回", ui.battle.board_zone.board_view.get_global_rect(), _end_victory_opts(payload))
"standalone_end":
if result == "victory":
_end_overlay.open_victory(Narrator.battle_end_text("victory", false),
"回到遭遇选择", board_view.get_global_rect(), _end_victory_opts(payload))
ui.modals.end_overlay.open_victory(Narrator.battle_end_text("victory", false),
"回到遭遇选择", ui.battle.board_zone.board_view.get_global_rect(), _end_victory_opts(payload))
else:
_end_overlay.open_defeat(Narrator.battle_end_text("defeat", false),
"返回", board_view.get_global_rect())
_open_modal_after_playback(_end_overlay)
ui.modals.end_overlay.open_defeat(Narrator.battle_end_text("defeat", false),
"返回", ui.battle.board_zone.board_view.get_global_rect())
ui.modals.flow.open_after_playback(ui.modals.end_overlay)
_refresh_presentations()
@@ -499,22 +316,17 @@ func _on_step_row(row: Dictionary) -> void:
# 清图奖励可感知反馈(挖完全图须有奖励感——clear_blast 兑付对敌伤害
# 零可见,toast 桥接归组合根呈现层;零兑付(未生效)不弹安静留明细带)
if String(row.get("source_class", "")) == "clear_reward" and String(row.get("type", "")) != "reward_ineffective":
board_zone.toast.show_toast("清图奖励:%s" % String(row.get("display", "")).replace("(清图奖励)", ""), "info")
ui.battle.board_zone.toast.show_toast("清图奖励:%s" % String(row.get("display", "")).replace("(清图奖励)", ""), "info")
# 排播入口:减动效=延迟归零同帧直落(行呈现不跳过);常态入队按段阶梯播放
if UiKit.motion_scale < 1.0:
_row_pacer.consume_now(row)
ui.screens.row_pacer.consume_now(row)
return
_row_pacer.enqueue(row)
ui.screens.row_pacer.enqueue(row)
# 段间延迟取数面(减动效双态断言用——pacing_queue_test 直读;委托 row_pacer 同名面)
func pace_delay_for(prev_seg: String, next_seg: String) -> float:
return _row_pacer.pace_delay_for(prev_seg, next_seg)
# 强制排空(模态开启前调用——明细回看/停点覆盖层/终局面所见明细带恒完整;pacing_queue_test 直读)
func _flush_row_queue() -> void:
_row_pacer.flush()
return ui.screens.row_pacer.pace_delay_for(prev_seg, next_seg)
func _on_round_started(ri: int) -> void:
@@ -522,10 +334,10 @@ func _on_round_started(ri: int) -> void:
#(view 内判减动效=静态化变体)。减动效快路=分隔行同帧直落(不跳过——consume_now 复用
# _consume_row 轮首分隔行分支)。
if UiKit.motion_scale < 1.0:
_row_pacer.consume_now({"__round_line": ri})
ui.screens.row_pacer.consume_now({"__round_line": ri})
else:
_row_pacer.enqueue({"__round_line": ri})
settlement_fx.play_round_banner(ri)
ui.screens.row_pacer.enqueue({"__round_line": ri})
ui.battle.settlement_fx.play_round_banner(ri)
# 秘宝呈现同步:每战首轮装配链
# 显式重算〔🔴 契约复原语义:下一战装配链重算即含「未装配→写 0」复原〕。
# 挂点时序依据:round_started 首轮发射位在 begin_battle 尾(attunement 注入之后)。
@@ -534,7 +346,7 @@ func _on_round_started(ri: int) -> void:
_sync_treasure_glow()
# 巢穴预告呈现(装板报数量读数口消费:≥0=本战装配命中,toast 一次性提示)
if run_scope.settlement != null and int(run_scope.settlement.nest_preview_count) >= 0:
board_zone.toast.show_toast("巢穴预告——本板危险格共 %d 个" % int(run_scope.settlement.nest_preview_count), "info")
ui.battle.board_zone.toast.show_toast("巢穴预告——本板危险格共 %d 个" % int(run_scope.settlement.nest_preview_count), "info")
# 开战横幅——整图地形变异预告:首轮快照 board_variants
# 非空才发射(Boss 图恒带一型=替换制;普通图空快照零发射);轮横幅下方叠行,view 内判减动效。
if ri == 1 and run_scope.settlement != null:
@@ -542,7 +354,7 @@ func _on_round_started(ri: int) -> void:
if not bvs.is_empty():
var vlines: Array[String] = HudPresenter.board_variant_lines(db, bvs)
if not vlines.is_empty():
settlement_fx.play_board_variant_banner("地形变异 · " + " · ".join(vlines))
ui.battle.settlement_fx.play_board_variant_banner("地形变异 · " + " · ".join(vlines))
_refresh_presentations()
@@ -552,19 +364,19 @@ func _on_board_changed() -> void:
run_scope.settlement = run_scope.runner.current_battle()
if run_scope.settlement == null:
return
board_view.rebuild(run_scope.settlement.minesweeper.current_board(), run_scope.settlement.minesweeper)
ui.battle.board_zone.board_view.rebuild(run_scope.settlement.minesweeper.current_board(), run_scope.settlement.minesweeper)
_check_board_hints()
_refresh_presentations()
# 秘宝视野呈现桥:阔视野带 light_radius_bonus
# 装配参数合计显式写给 board_view.set_fog_glow_bonus(装配=半径 8+4 / 未装配=复原 0),写入即 setter
# 装配参数合计显式写给 ui.battle.board_zone.board_view.set_fog_glow_bonus(装配=半径 8+4 / 未装配=复原 0),写入即 setter
# 内 refresh 全板重算雾态。调用点=每战首轮装配链(_on_round_started ri==1)——纯呈现边界 🔴 契约 2:
# 只写呈现态参数,minesweeper 结算零触碰;off 态(未装配)写 0=缺省基线。
func _sync_treasure_glow() -> void:
if run_scope.settlement == null or board_view == null:
if run_scope.settlement == null or ui.battle.board_zone.board_view == null:
return
board_view.set_fog_glow_bonus(run_scope.settlement.attune_param_sum("light_radius_bonus"))
ui.battle.board_zone.board_view.set_fog_glow_bonus(run_scope.settlement.attune_param_sum("light_radius_bonus"))
# ---- 一次性非模态提示(每 run 一次——pattern 10)与诚实提示 ----
@@ -575,10 +387,10 @@ func _check_board_hints() -> void:
return
# 零合法板(棋盘生成 EC5 降级)诚实提示——非静默(toast,非模态)
if board.illegal_product:
board_zone.toast.show_toast("本板为零合法板降级产物——出图器已尽力,数据面诚实告知", "warn")
ui.battle.board_zone.toast.show_toast("本板为零合法板降级产物——出图器已尽力,数据面诚实告知", "warn")
# 接受最优降级(有合法板但无一达标)——降级标记独立于非法产物标记,同走诚实告知
elif board.degraded_product:
board_zone.toast.show_toast("本板为接受最优降级产物(未达可推理率及格线)——数据面诚实告知", "warn")
ui.battle.board_zone.toast.show_toast("本板为接受最优降级产物(未达可推理率及格线)——数据面诚实告知", "warn")
func _on_hover_forbidden(_cell: Vector2i) -> void:
@@ -594,7 +406,7 @@ func _on_chord_attempted(_cell: Vector2i) -> void:
func _show_hint(text: String, hint_id: String) -> void:
run_scope.hints_shown[hint_id] = true
logger.log_ux("ux_hint_shown", {"hint_id": hint_id})
board_zone.toast.show_toast(text, "info")
ui.battle.board_zone.toast.show_toast(text, "info")
# ---- 节点覆盖层"继续"(奖励/恢复点兑付确认)与节点图选路覆盖层 ----
@@ -706,15 +518,15 @@ func _on_wallet_changed(event: String, gp: int, extra: Dictionary)-> void:
func _on_board_action(cell: Vector2i, kind: String) -> void:
# 玩家动作 → 核心写调用的唯一通道(模态/无战斗拒绝在此收口)
if _input_locked():
if ui.modals.flow.locks_input():
return
# 结算演播期棋盘冻结(v3 口径——不可点/不可移/不可挖):
# 锁面=排播队列活跃(纯队列判据;排播泵尾迹窗口不可作演播中标志——见 row_pacer._process 注)。
# 队列排空即解锁、无状态残留;HUD 按钮不在锁面(停手/明细回看各自通路,开启前
# _flush_row_queue 自行排空)。
# 开层前排空钩子(flow.flush_rows——模态开启前明细带恒完整)。
# **点击不丢弃**(v3.1——「卡死」体感根因):锁期点击缓存最后一次,演播完边沿
# (_process 泵 _was_pumping 分支)自动重放——玩家操作零丢失、体感=「演播一停动作就生效」。
if not _row_queue.is_empty():
if not ui.screens.row_pacer.pending_rows().is_empty():
run_scope.locked_click = {"cell": cell, "kind": kind}
return
# 秘宝选中态劫持(两步交互第二步):点格=使用目标拾取,不落挖格;
@@ -734,22 +546,22 @@ func _on_board_action(cell: Vector2i, kind: String) -> void:
# settle_batch 内部同步走完 结算→战斗结束→关卡推进(事件同步发射)——关卡终局时 settlement 已被
# runner 置空,清图演出判定须先判 settlement(重入隐患守卫)
if accepted and kind == "dig" and run_scope.settlement != null and run_scope.settlement.minesweeper.current_board() != null and run_scope.settlement.minesweeper.clear_map_cleared():
board_zone.fx.play_clear_burst()
ui.battle.board_zone.fx.play_clear_burst()
if accepted and kind == "dig" and run_scope.settlement != null and run_scope.settlement.minesweeper != null:
# 挖掘演出:挥铲三帧 + 角色弹跳——触发=格子点开那一刻
#(settle_batch 同步受理即状态已变,动画纯表现与 refresh 同拍零竞态;
# 挖掘不移动英雄,出手方向 = 目标格−英雄格,圆心=英雄持握手位)
var hero_cell: Vector2i = run_scope.settlement.minesweeper.hero_pos()
board_zone.fx.play_shovel_swing(board_view.hero_hand_point(), cell - hero_cell, board_view.hero_sprite_scale())
board_view.play_hero_dig_bounce()
ui.battle.board_zone.fx.play_shovel_swing(ui.battle.board_zone.board_view.hero_hand_point(), cell - hero_cell, ui.battle.board_zone.board_view.hero_sprite_scale())
ui.battle.board_zone.board_view.play_hero_dig_bounce()
elif accepted and kind == "move":
board_view.notify_hero_move() # 原地移动(目标=当前格幂等受理)帧闪回执——refresh diff 前置告知
board_view.refresh()
ui.battle.board_zone.board_view.notify_hero_move() # 原地移动(目标=当前格幂等受理)帧闪回执——refresh diff 前置告知
ui.battle.board_zone.board_view.refresh()
_refresh_presentations()
func _on_stop_pressed() -> void:
if _input_locked():
if ui.modals.flow.locks_input():
return
# stop 经批次提交(排雷玩法 D1×6①——不直调 行动经济)
if run_scope.settlement == null:
@@ -758,15 +570,15 @@ func _on_stop_pressed() -> void:
logger.log_action("stop", Vector2i(-1, -1), not outcomes.is_empty() and bool(outcomes[0].accepted))
if not outcomes.is_empty() and String(outcomes[0].get("outcome", "")) == "voided":
# voided ≠ 拒绝——独立文案(pattern 1:防静默改判读感欺骗)
board_zone.toast.show_toast("本轮已清图,停手未执行 · 剩余 AP 已作废", "warn")
ui.battle.board_zone.toast.show_toast("本轮已清图,停手未执行 · 剩余 AP 已作废", "warn")
logger.log_ux("ux_reject", {"source": "voided", "sub_reason": "stop_voided"})
board_view.refresh()
ui.battle.board_zone.board_view.refresh()
_refresh_presentations()
func _on_refresh_confirmed() -> void:
# 弃图刷新(挖掘期 Active 当场换图继续挖——D4;RAP 按门槛扣费 · 旧板封印作废,每轮上限读表)
if _input_locked():
if ui.modals.flow.locks_input():
return
var ok: bool = false
if run_scope.level_active and run_scope.runner != null:
@@ -776,7 +588,7 @@ func _on_refresh_confirmed() -> void:
logger.log_action("refresh", Vector2i(-1, -1), ok)
if ok:
logger.log_ux("ux_refresh_attempt", {"result": "accepted", "round_refresh_index": _refresh_used_snapshot()})
board_zone.toast.show_toast("RAP−%d · 旧板封印作废——新图已就位,AP 保留继续挖" % int(db.run.action_economy.rap_refresh_cost), "gold")
ui.battle.board_zone.toast.show_toast("RAP−%d · 旧板封印作废——新图已就位,AP 保留继续挖" % int(db.run.action_economy.rap_refresh_cost), "gold")
else:
# 资源面/上限拒绝(EC13 合法拒绝可见;行动经济 无载荷——客户端按上下文推断)
var rap: int = int(run_scope.settlement.presentation_snapshot().get("rap", 0)) if run_scope.settlement != null else 0
@@ -784,11 +596,11 @@ func _on_refresh_confirmed() -> void:
var result := "rejected_rap" if rap < cost else "rejected_cap"
logger.log_ux("ux_refresh_attempt", {"result": result, "round_refresh_index": _refresh_used_snapshot() + 1})
if result == "rejected_rap":
board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 8)" % (cost - rap), "warn")
ap_bar.pulse_rap()
ui.battle.board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 8)" % (cost - rap), "warn")
ui.battle.board_zone.ap_bar.pulse_rap()
else:
board_zone.toast.show_toast("本轮刷新次数已用完(每轮 2 次)", "warn")
board_view.refresh()
ui.battle.board_zone.toast.show_toast("本轮刷新次数已用完(每轮 2 次)", "warn")
ui.battle.board_zone.board_view.refresh()
_refresh_presentations()
@@ -798,9 +610,9 @@ func _handle_outcome_feedback(outcome: Dictionary) -> void:
return
logger.log_ux("ux_reject", {"source": String(outcome.get("reject_reason", "")),
"sub_reason": String(outcome.get("sub_reason", ""))})
board_zone.toast.show_toast(HudPresenter.outcome_text(outcome), "warn")
ui.battle.board_zone.toast.show_toast(HudPresenter.outcome_text(outcome), "warn")
if String(outcome.get("reject_reason", "")) == "resource_face":
ap_bar.pulse_ap()
ui.battle.board_zone.ap_bar.pulse_ap()
# ==================== 全屏 States 状态机(挖掘/结算/窗口/终局)====================
@@ -816,18 +628,18 @@ func _refresh_used_snapshot() -> int:
func _refresh_presentations() -> void:
# 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查)
var refresh_remaining: int = maxi(0, int(db.run.action_economy.refresh_cap_per_round) - _refresh_used_snapshot())
HudPresenter.refresh(stage, info_panel, ap_bar, {"settlement": run_scope.settlement, "runner": run_scope.runner, "level_active": run_scope.level_active, "db": db, "refresh_remaining": refresh_remaining})
HudPresenter.refresh(ui.battle.stage, ui.battle.info_panel, ui.battle.board_zone.ap_bar, {"settlement": run_scope.settlement, "runner": run_scope.runner, "level_active": run_scope.level_active, "db": db, "refresh_remaining": refresh_remaining})
_treasure_ui.refresh_slots() # C 区秘宝槽重查(试炼镐积点进度随挖格刷新悬停浮窗)
_sync_settlement_visuals()
func _sync_settlement_visuals() -> void:
# 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开 84px
# 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开(高度读 battle_layout)
var settling := _is_settling()
if settling and not run_scope.was_settling:
settlement_fx.play_round_sweep(board_view.get_global_rect()) # 结算轮光带——时序敏感演出触发,例外留在组合根
ui.battle.settlement_fx.play_round_sweep(ui.battle.board_zone.board_view.get_global_rect()) # 结算轮光带——时序敏感演出触发,例外留在组合根
run_scope.was_settling = settling
HudPresenter.sync_settlement_visuals(stage, board_view, log_view, settling,
HudPresenter.sync_settlement_visuals(ui.battle.stage, ui.battle.board_zone.board_view, ui.battle.log_view, settling,
HudPresenter.phase_text(run_scope.settlement, run_scope.runner, run_scope.boss_active, db))
@@ -836,13 +648,13 @@ func _is_settling() -> bool:
# 判据若只跟逻辑态,演播期棋盘会提前恢复可挖观感=「没意识到在结算演出」感知缺口根源;
# 演播期维持去饱和+禁挖光(结算中语义),放完自然回挖掘认知。判据=纯队列(排播泵尾迹
# 窗口不可作演播中标志——row_pacer._process 注)。battle 收尾演播例外:
# overlay 开启即 _flush_row_queue 排空,视觉随队列空同步交接结算框。
# overlay 开启经 flow.flush_rows 钩子排空,视觉随队列空同步交接结算框。
# 相位取数经 presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透)。
if run_scope.settlement == null or not run_scope.settlement.is_battle_live():
return not _row_queue.is_empty()
return not ui.screens.row_pacer.pending_rows().is_empty()
if String(run_scope.settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING:
return true
return not _row_queue.is_empty()
return not ui.screens.row_pacer.pending_rows().is_empty()
# 相位行 —— 承接体见 HudPresenter.phase_text(caller 直接改引)
@@ -908,8 +720,8 @@ func _seed_value() -> int:
# 重放模板 0 即在此种子下标定〕;置位 true=教学局已收束的非首局走 ③ 随机);
# ③非首局空/非法框=引擎全局熵 randi()(Godot 4 启动自播种子,headless 同;不走 rng_service
# 封闭流、禁 randomize()/自建 RNG——组合根建种子属引擎熵豁免面,不进玩法 RNG 消费)
if _seed_edit.text.is_valid_int():
return int(_seed_edit.text)
if ui.modals.debug_overlay.seed_edit.text.is_valid_int():
return int(ui.modals.debug_overlay.seed_edit.text)
if save_manager.meta_state.career_state().first_adventure_done:
return randi() # rng-seed-exempt(非首局空框引擎熵——组合根建种子不进玩法 RNG 消费,static_scan S1/S2 标记豁免)
return 42
+45 -45
View File
@@ -30,10 +30,10 @@ static func _on_adventure_level_started(root: Variant, level_index: int, display
# 开场行只显示 atk/max_hp 两值,注记指向不存在的显示轴反成失真),白板局不带
var ov: Dictionary[Variant, Variant] = root.meta.hero_override()
var boosted: bool = int(ov.get("atk", 0)) > 0 or int(ov.get("max_hp", 0)) > 0 \
or root.debug_overlay.atk_pct() > 0 or root.debug_overlay.hp_pct() > 0 \
or root.ui.modals.debug_overlay.atk_pct() > 0 or root.ui.modals.debug_overlay.hp_pct() > 0 \
or root.run_scope.runner.journal().bonus_total("atk") > 0 \
or root.run_scope.runner.journal().bonus_total("max_hp") > 0
root.log_view.append_bb(root.Narrator.level_open_line(display_name, int(root.run_scope.adventure.master_seed),
root.ui.battle.log_view.append_bb(root.Narrator.level_open_line(display_name, int(root.run_scope.adventure.master_seed),
root.run_scope.runner.effective_atk(), root.run_scope.runner.effective_max_hp(), boosted))
# runner 六信号重连(is_connected 守卫——未连即接;守卫失败 push_error=重连恰好一次契约承载)
@@ -62,30 +62,30 @@ static func _on_runner_node(root: Variant, node: Dictionary, info: Dictionary) -
root.logger.attach_battle(root.run_scope.settlement)
root.run_scope.boss_active = t == "boss"
root._show_board_fx_for_node(t)
root.log_view.append_bb(root.Narrator.node_banner(root.run_scope.runner, node, t))
root.ui.battle.log_view.append_bb(root.Narrator.node_banner(root.run_scope.runner, node, t))
elif t == "reward":
# 奖励节点三选一(与关卡完成三选一共享同池同权重同上限;CurioPickOverlay 共用面)。
# 呈现编排归 curio_interact_controller.present_node_curio。
root._bonus.present_node_curio()
root.log_view.append_bb(root.Narrator.reward_line())
root.ui.battle.log_view.append_bb(root.Narrator.reward_line())
elif t == "rest":
var healed: int = int(info.get("healed", 0))
root._open_modal(root.node_overlay)
root.node_overlay.show_rest(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0)))
root.log_view.append_bb(root.Narrator.rest_line(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0))))
root.ui.modals.flow.open(root.ui.modals.node_overlay)
root.ui.modals.node_overlay.show_rest(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0)))
root.ui.battle.log_view.append_bb(root.Narrator.rest_line(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0))))
elif t == "shop":
# 商店房兑付:到达即开店——商品/货架快照与余额经 runner 只读投影喂入;
# 购买/离开归玩家意图(buy_requested/leave_pressed 接线),零自动推进。
# pool 面 = 共享池货架三选一同构卡;legacy 面 = 固定三件(off 对照承载)
root._open_modal(root.shop_overlay)
root.ui.modals.flow.open(root.ui.modals.shop_overlay)
if root.run_scope.runner.shop.is_shop_pool():
root.shop_overlay.open_shelf(root.run_scope.runner.shop.shop_shelf(), int(root.run_scope.runner.gp))
root.shop_overlay.set_visit_info(int(root.run_scope.runner.shop.shop_bp_visit_buys()), float(root.db.run.shop.price_a)) # 爬价规则行
root.shop_overlay.set_refresh_display(root.run_scope.runner.shop.shop_refresh_free_left(), root.run_scope.runner.shop.shop_refresh_cost(), int(root.run_scope.runner.gp) >= root.run_scope.runner.shop.shop_refresh_cost()) # MC-1
root.ui.modals.shop_overlay.open_shelf(root.run_scope.runner.shop.shop_shelf(), int(root.run_scope.runner.gp))
root.ui.modals.shop_overlay.set_visit_info(int(root.run_scope.runner.shop.shop_bp_visit_buys()), float(root.db.run.shop.price_a)) # 爬价规则行
root.ui.modals.shop_overlay.set_refresh_display(root.run_scope.runner.shop.shop_refresh_free_left(), root.run_scope.runner.shop.shop_refresh_cost(), int(root.run_scope.runner.gp) >= root.run_scope.runner.shop.shop_refresh_cost()) # MC-1
else:
root.shop_overlay.open_shop(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp))
root.shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载)
root.log_view.append_bb(root.Narrator.shop_line(int(root.run_scope.runner.gp)))
root.ui.modals.shop_overlay.open_shop(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp))
root.ui.modals.shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载)
root.ui.battle.log_view.append_bb(root.Narrator.shop_line(int(root.run_scope.runner.gp)))
root.logger.log_node(node, info, root.run_scope.runner.node_index)
root._refresh_presentations()
@@ -101,7 +101,7 @@ static func _on_adventure_finished(root: Variant, summary: Dictionary) -> void:
_exit_level_screen(root) # 终局兜底:独立屏在开态(放弃入口)也先回战斗屏呈现摘要——屏级互斥单一出口
root._sync_status_entry() # 离局熄灭状态面板入口角标(防菜单误现)
root._sync_settlement_visuals() # → 全关
HudPresenter.battle_end_feedback(root.stage, root.ap_bar, false) # 灰化/idle 回正/状态全清——HUD 单通道
HudPresenter.battle_end_feedback(root.ui.battle.stage, root.ui.battle.board_zone.ap_bar, false) # 灰化/idle 回正/状态全清——HUD 单通道
# 存档入账:settle = Boss 两档入账 + 工坊解锁 + 摘要入档 + 写盘(唯一写档点不变——
# 调试两层不经过本处理器,天然不写档不污染生涯数据)。
var detail: Dictionary[String, Variant] = {}
@@ -110,30 +110,30 @@ static func _on_adventure_finished(root: Variant, summary: Dictionary) -> void:
if not detail.is_empty():
text += "\n" + root.Narrator.keystone_settle_text(detail, root._bonus.item_name_lookup())
if result == "completed":
root._end_overlay.open_victory(text, "返回冒险", root.board_view.get_global_rect(),
root.ui.modals.end_overlay.open_victory(text, "返回冒险", root.ui.battle.board_zone.board_view.get_global_rect(),
{"gp_summary": "本局 GP 入账 · 合计 %d" % int(summary.get("gp_final", 0))}) # GP 行(取数=summary gp_final 清账前快照);钥石明细已在 text(keystone_settle_text)——三行账
else:
root._end_overlay.open_defeat(text, "返回", root.board_view.get_global_rect())
root.ui.modals.end_overlay.open_defeat(text, "返回", root.ui.battle.board_zone.board_view.get_global_rect())
root.logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷)
if root.run_scope.runner != null:
root.run_scope.runner.restore_hero_base()
root._open_modal_after_playback(root._end_overlay)
root.ui.modals.flow.open_after_playback(root.ui.modals.end_overlay)
root._refresh_presentations()
static func _on_battle_end_continue(root: Variant) -> void:
# 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石在收束时入存档)
root._close_modal(root._end_overlay)
root._end_overlay.close()
root.ui.modals.flow.close(root.ui.modals.end_overlay)
root.ui.modals.end_overlay.close()
# 三选一停点挂起分支:战斗总结结算(Boss 胜利钥石增量行)确认后进三选一面
if root._bonus.resume_if_pending(): # 三选一停点在途恢复入口(curio_interact_controller)
return
# 挂起分派:战斗胜利总结结算框的「下一节点」≠ 终局——进选路页(暂存候选恢复呈现)
if root._settlement_stop_pending:
root._settlement_stop_pending = false
if root.run_scope.settlement_stop_timer.is_armed():
root.run_scope.settlement_stop_timer.disarm()
_enter_level_screen(root)
root.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held())))
root.level_screen.open_choices(root.run_scope.pending_candidates, root.run_scope.runner)
root.level_screen.apply_status(level_map_status_payload(root.run_scope.runner))
root.ui.modals.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held())))
root.ui.modals.level_screen.open_choices(root.run_scope.pending_candidates, root.run_scope.runner)
root.ui.modals.level_screen.apply_status(level_map_status_payload(root.run_scope.runner))
root.run_scope.pending_candidates = []
root._refresh_presentations()
return
@@ -141,12 +141,12 @@ static func _on_battle_end_continue(root: Variant) -> void:
# 局态封印/账本/模态挂起全复位(局态字段清单见 run_scope.gd)
root._debug.reset()
root._begin_session(false)
root.debug_overlay.reset_sliders()
root._start_overlay.visible = true
root.ui.modals.debug_overlay.reset_sliders()
root.ui.screens.start_overlay.visible = true
root._refresh_presentations()
static func _on_inter_next_pressed(root: Variant) -> void:
root._inter_overlay.visible = false
root.ui.screens.inter_overlay.visible = false
if root.run_scope.is_adventure:
# 冒险模式:大奖励屏确认 → 进下一关(AdventureRunner 序列推进——零自动推进的冒险级停点)
if root.run_scope.adventure != null and not root.run_scope.adventure.continue_to_next_level():
@@ -158,7 +158,7 @@ static func _on_inter_next_pressed(root: Variant) -> void:
static func _on_node_continue(root: Variant) -> void:
root._close_modal(root.node_overlay)
root.ui.modals.flow.close(root.ui.modals.node_overlay)
if root.run_scope.runner != null:
# 确认 ≠ 推进——兑付确认后经 choices_presented 弹出选路口
root.run_scope.runner.continue_after_node()
@@ -167,33 +167,33 @@ static func _on_choices_presented(root: Variant, candidates: Array) -> void:
# 零自动推进的停点呈现:整幅节点图 + 图上点击选路(战斗胜利/兑付确认统一到达此处)
if root.run_scope.runner == null:
return
root.log_view.append_bb(root.Narrator.route_line(candidates, root.run_scope.runner))
root.ui.battle.log_view.append_bb(root.Narrator.route_line(candidates, root.run_scope.runner))
# 战斗胜利总结结算(battle.md OQ14):战斗胜利停点先弹 BattleEndOverlay
# 胜利形态(金蜡封+GP 增量+总量+判定行+「下一节点」+明细回看),确认后进选路页。判据 = 战斗节点
# 的选路停点且战斗已收场(battle 收尾分支 _settlement 置 null 先于本信号;入口/兑付/商店停点
# 的当前节点非 battle 型——三态天然分流)。Boss 胜/失败走 level_finished 另路不经此处。
if root.run_scope.is_adventure and root.run_scope.runner.current_battle() == null \
and String(root.run_scope.runner.current_node().get("type", "")) == "battle" and not candidates.is_empty():
root._settlement_stop_pending = true
root.run_scope.settlement_stop_timer.arm()
root.run_scope.pending_candidates = candidates.duplicate()
root._end_overlay.open_victory(root.Narrator.battle_end_text("victory", false), "下一节点",
root.board_view.get_global_rect(),
root.ui.modals.end_overlay.open_victory(root.Narrator.battle_end_text("victory", false), "下一节点",
root.ui.battle.board_zone.board_view.get_global_rect(),
{"gp_summary": "战利品 GP +%d · 合计 %d" % [root.run_scope.last_gain_amount, int(root.run_scope.runner.gp)]})
root._open_modal_after_playback(root._end_overlay)
root.ui.modals.flow.open_after_playback(root.ui.modals.end_overlay)
root._refresh_presentations()
return
_enter_level_screen(root)
root.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held())))
root.level_screen.open_choices(candidates, root.run_scope.runner)
root.level_screen.apply_status(level_map_status_payload(root.run_scope.runner))
root.ui.modals.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held())))
root.ui.modals.level_screen.open_choices(candidates, root.run_scope.runner)
root.ui.modals.level_screen.apply_status(level_map_status_payload(root.run_scope.runner))
root._refresh_presentations()
static func _on_route_chosen(root: Variant, node_id: int) -> void:
# 视图已按节点类型定去留——战斗/Boss 出屏(visible=false,此处收栈
# 记录一致性+回战斗屏);奖励/恢复/商店本屏保持激活(兑付覆盖层由节点事件域内弹出,模态栈
# 打开低层时自动隐藏本屏、关闭自动恢复)
if not root.level_screen.visible:
root._close_modal(root.level_screen)
if not root.ui.modals.level_screen.visible:
root.ui.modals.flow.close(root.ui.modals.level_screen)
_exit_level_screen(root)
if root.run_scope.runner != null and not root.run_scope.runner.pick_next_node(node_id):
push_error("[组合根] 选路被拒(编号 %d 不在候选)" % node_id)
@@ -202,15 +202,15 @@ static func _on_route_chosen(root: Variant, node_id: int) -> void:
static func _enter_level_screen(root: Variant) -> void:
root.run_scope.level_screen_active = true
root._battle_root.visible = false
root._open_modal(root.level_screen)
root.ui.screens.battle_root.visible = false
root.ui.modals.flow.open(root.ui.modals.level_screen)
root._sync_status_entry() # 屏级互斥切换同步(_open_modal 单口不识屏级状态)
static func _exit_level_screen(root: Variant) -> void:
root.run_scope.level_screen_active = false
root._battle_root.visible = true
if root.level_screen.visible:
root._close_modal(root.level_screen)
root.ui.screens.battle_root.visible = true
if root.ui.modals.level_screen.visible:
root.ui.modals.flow.close(root.ui.modals.level_screen)
root._sync_status_entry() # 同上
@@ -251,7 +251,7 @@ static func _on_abandon_requested(root: Variant) -> void:
return
if not root.run_scope.adventure.abandon():
root.logger.log_ux("ux_reject", {"source": "abandon", "sub_reason": "battle_in_flight"})
root.board_zone.toast.show_toast("战斗进行中——无法放弃", "warn")
root.ui.battle.board_zone.toast.show_toast("战斗进行中——无法放弃", "warn")
# ---- 营地覆盖层与钥石账本(写调用唯一归组合根)----
+15 -15
View File
@@ -13,7 +13,7 @@ static func _on_diagnose_pressed(root: Variant) -> void:
if root.run_scope.settlement != null:
ctx["settlement"] = root.run_scope.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读)
var path: String = root.logger.dump_diagnosis(ctx)
root.board_zone.toast.show_toast("已转储诊断: " + path, "info")
root.ui.battle.board_zone.toast.show_toast("已转储诊断: " + path, "info")
@@ -21,17 +21,17 @@ static func _show_debug_overlay(root: Variant) -> void:
# 门禁:对局中可开;本函数仍是唯一开窗入口,门禁逻辑收函数内:
# 棋盘输入锁/状态角标判据排除浮窗,
# 遮挡语义=ModalStack 登记制(他模态开时暂隐、关后自动恢复),此处零场景限制
if root.debug_overlay == null:
if root.ui.modals.debug_overlay == null:
return
root._open_modal(root.debug_overlay)
root.ui.modals.flow.open(root.ui.modals.debug_overlay)
_sync_debug_overlay(root)
# 调试浮窗角标点击(两屏同款处理器——与状态角标同形制守卫;仍收口 _show_debug_overlay 单口)
static func _on_debug_badge_pressed(root: Variant) -> void:
var map_on: bool = root.level_screen != null and root.level_screen.visible
if (map_on and root._modal_flow.badge_greyed_map()) or (not map_on and root._modal_flow.badge_greyed_battle()):
var map_on: bool = root.ui.modals.level_screen != null and root.ui.modals.level_screen.visible
if (map_on and root.ui.modals.flow.badge_greyed_map()) or (not map_on and root.ui.modals.flow.badge_greyed_battle()):
return
_show_debug_overlay(root)
@@ -40,10 +40,10 @@ static func _on_debug_badge_pressed(root: Variant) -> void:
# 浮窗上下文同步(当局数据刷新+对局中按钮禁用单点):调用面=开窗口与模态开合
# 单口(凡影响开态/对局态的路径必经其一——与 _sync_status_entry 同款纪律)
static func _sync_debug_overlay(root: Variant) -> void:
if root.debug_overlay == null:
if root.ui.modals.debug_overlay == null:
return
root.debug_overlay.set_context_locked(root.run_scope.settlement != null or root.run_scope.is_adventure) # 对局中批量模拟/两层调试入口禁用(长程异步模拟与活跃结算并存态不开放)
root.debug_overlay.update_run_data(_debug_run_data(root)) # 当局数据只读区块喂入(开窗+模态开合信号驱动)
root.ui.modals.debug_overlay.set_context_locked(root.run_scope.settlement != null or root.run_scope.is_adventure) # 对局中批量模拟/两层调试入口禁用(长程异步模拟与活跃结算并存态不开放)
root.ui.modals.debug_overlay.update_run_data(_debug_run_data(root)) # 当局数据只读区块喂入(开窗+模态开合信号驱动)
@@ -63,7 +63,7 @@ static func _debug_run_data(root: Variant) -> Dictionary:
static func _close_debug_overlay(root: Variant) -> void:
root._close_modal(root.debug_overlay) # 模态栈单口——开始层全程未隐藏,无需恢复
root.ui.modals.flow.close(root.ui.modals.debug_overlay) # 模态栈单口——开始层全程未隐藏,无需恢复
static func _on_tune_apply(root: Variant, atk_pct: int, hp_pct: int) -> void:
# 加成无持久态可"落"——各战斗开局经 board_resolvers.debug_hero_override/runner 现算生效(应用=关闭浮窗)
@@ -83,11 +83,11 @@ static func _start_debug_levels(root: Variant) -> void:
# settlement/编排态=状态机破坏;模拟在跑时 reset_sliders 会作废测量读数(d5 置灰之外的信号绕过闸)
if root.run_scope.is_adventure or root.run_scope.settlement != null or root._tune_sim.is_running():
return
root.debug_overlay.reset_sliders()
root._close_modal(root.debug_overlay) # 浮窗内入口发起——随开随收(对局期不可再开:门禁在 _show_debug_overlay)
root._start_overlay.visible = false
root.log_view.clear()
root.log_view.append_bb(root.Narrator.debug_open_line(root._seed_value(), 1, true))
root.ui.modals.debug_overlay.reset_sliders()
root.ui.modals.flow.close(root.ui.modals.debug_overlay) # 浮窗内入口发起——随开随收(对局期不可再开:门禁在 _show_debug_overlay)
root.ui.screens.start_overlay.visible = false
root.ui.battle.log_view.clear()
root.ui.battle.log_view.append_bb(root.Narrator.debug_open_line(root._seed_value(), 1, true))
root._begin_session(false)
if not root._debug.start_debug_levels():
root._fatal_screen("战斗启动失败——见输出日志")
@@ -100,6 +100,6 @@ static func _on_debug_battle_created(root: Variant, settlement_ref: BattleOrches
static func _on_debug_battle_ready(root: Variant, settlement_ref: BattleOrchestrator, _level: int, _carry: int) -> void:
root.run_scope.settlement = settlement_ref
root.board_view.rebuild(root.run_scope.settlement.minesweeper.current_board(), root.run_scope.settlement.minesweeper)
root.ui.battle.board_zone.board_view.rebuild(root.run_scope.settlement.minesweeper.current_board(), root.run_scope.settlement.minesweeper)
root._check_board_hints()
root._refresh_presentations()
+35 -35
View File
@@ -1,16 +1,16 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 组合根覆盖层流程承接件:设置/状态面板/生涯页/选档界面/图例/钱包事件的开合与数据投喂。
## 本件是 main.gd 同名函数的逐字搬运(成员访问改 root 前缀)——main 侧留同名薄委托,探针 main.X 路径零改动。
## 全部开合走组合根模态单口(root._open_modal/_close_modal);本件零成员态。
## 全部开合走组合根模态编排口(root.ui.modals.flow.open/close);本件零成员态。
extends RefCounted
static func _open_settings(root: Variant) -> void:
root._start_overlay.visible = false
root._open_modal(root.settings_overlay) # 模态栈单口
root.ui.screens.start_overlay.visible = false
root.ui.modals.flow.open(root.ui.modals.settings_overlay) # 模态栈单口
static func _on_settings_closed(root: Variant) -> void:
root._close_modal(root.settings_overlay) # 模态栈单口
root._start_overlay.visible = true
root.ui.modals.flow.close(root.ui.modals.settings_overlay) # 模态栈单口
root.ui.screens.start_overlay.visible = true
# ---- 局内状态面板·只读(入口角标在两屏装配段;开合与数据喂入归组合根)----
@@ -25,22 +25,22 @@ static func _status_data(root: Variant) -> Dictionary:
return HudPresenter.status_data(root.db, root.meta, root.run_scope.runner, root._treasure_ui)
static func _open_status_panel(root: Variant) -> void:
root._open_modal(root.status_panel) # 模态栈单口(开层前排空排播队列——既有层同款行为,目检确认项)
root.status_panel.open_status(_status_data(root))
root.ui.modals.flow.open(root.ui.modals.status_panel) # 模态栈单口(开层前排空排播队列——既有层同款行为,目检确认项)
root.ui.modals.status_panel.open_status(_status_data(root))
static func _on_status_closed(root: Variant) -> void:
root._close_modal(root.status_panel) # 模态栈单口
root.ui.modals.flow.close(root.ui.modals.status_panel) # 模态栈单口
static func _open_career(root: Variant) -> void:
root._start_overlay.visible = false
root._open_modal(root.career_overlay) # 模态栈单口
root.career_overlay.open_career(HudPresenter.career_data(root.db, root.save_manager, root.meta))
root.ui.screens.start_overlay.visible = false
root.ui.modals.flow.open(root.ui.modals.career_overlay) # 模态栈单口
root.ui.modals.career_overlay.open_career(HudPresenter.career_data(root.db, root.save_manager, root.meta))
static func _on_career_closed(root: Variant) -> void:
root._close_modal(root.career_overlay) # 模态栈单口
root._start_overlay.visible = true
root.ui.modals.flow.close(root.ui.modals.career_overlay) # 模态栈单口
root.ui.screens.start_overlay.visible = true
# 生涯页快照投影 —— 承接体见 HudPresenter.career_data(开层调用点直接改引)
@@ -52,13 +52,13 @@ static func _on_career_closed(root: Variant) -> void:
# 开层(模态栈单口;与开始层互斥=career 同款先例)。「进入存档」钮归卡② mn3——本函数为
# 该钮的唯一落点(卡③ 批内探针函数直调=中间态,实现报告「已知中间态」登记)。
static func _open_save_select(root: Variant) -> void:
root._start_overlay.visible = false
root._open_modal(root.save_select_overlay) # 模态栈单口
root.save_select_overlay.open_save_select(HudPresenter.save_select_data(root.save_manager))
root.ui.screens.start_overlay.visible = false
root.ui.modals.flow.open(root.ui.modals.save_select_overlay) # 模态栈单口
root.ui.modals.save_select_overlay.open_save_select(HudPresenter.save_select_data(root.save_manager))
static func _on_save_select_closed(root: Variant) -> void:
root._close_modal(root.save_select_overlay) # 模态栈单口
root._start_overlay.visible = true
root.ui.modals.flow.close(root.ui.modals.save_select_overlay) # 模态栈单口
root.ui.screens.start_overlay.visible = true
# 槽数据快照 —— 承接体见 HudPresenter.save_select_data(开层调用点直接改引)
@@ -85,18 +85,18 @@ static func _on_save_select_load(root: Variant, slot: int) -> void:
static func _on_save_select_delete(root: Variant, slot: int) -> void:
root._confirm_consumer = "delete_slot"
root._confirm_consumer_slot = slot
root._open_modal(root.confirm_dialog) # 模态栈单口(显示编排/挂起兜底/遮挡记账——与其余覆盖层同口)
root.confirm_dialog.open("删除存档位 %d?删除后该档不可恢复。" % slot, "删除", "取消")
root.ui.modals.flow.open(root.ui.modals.confirm_dialog) # 模态栈单口(显示编排/挂起兜底/遮挡记账——与其余覆盖层同口)
root.ui.modals.confirm_dialog.open("删除存档位 %d?删除后该档不可恢复。" % slot, "删除", "取消")
# 入口角标点击(两屏同款处理器):P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰,
# 探针 pressed.emit 绕过 disabled 时由此兜住;地图屏自身的登记开态不计——has_open_excluding)
static func _on_status_badge_pressed(root: Variant) -> void:
var map_on: bool = root.level_screen != null and root.level_screen.visible
var map_on: bool = root.ui.modals.level_screen != null and root.ui.modals.level_screen.visible
# P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰,探针 pressed.emit 绕过 disabled 时由此兜住;
# 地图屏自身的登记开态不计;判据排除调试浮窗:对局中浮窗开着时状态角标仍可点)
if (map_on and root._modal_flow.badge_greyed_map()) or (not map_on and root._modal_flow.badge_greyed_battle()):
if (map_on and root.ui.modals.flow.badge_greyed_map()) or (not map_on and root.ui.modals.flow.badge_greyed_battle()):
return
_open_status_panel(root)
@@ -106,17 +106,17 @@ static func _on_status_badge_pressed(root: Variant) -> void:
# 地图屏角标=随屏;置灰=登记册上除地图屏自身外任一层开着(防「看得见点不着」态)。
# 调用面=模态开合单口与屏级互斥口(凡影响开态/屏态的路径必经其一)。
static func _sync_status_entry(root: Variant) -> void:
if root.status_panel == null or root._modals == null or root.board_zone == null or root.level_screen == null:
if root.ui.modals.status_panel == null or root.ui.modals.flow == null or root.ui.battle.board_zone == null or root.ui.modals.level_screen == null:
return
var map_on: bool = root.level_screen.visible
root.board_zone.status_badge.visible = root.run_scope.level_active and not map_on
root.level_screen.status_badge.visible = map_on
var map_on: bool = root.ui.modals.level_screen.visible
root.ui.battle.board_zone.status_badge.visible = root.run_scope.level_active and not map_on
root.ui.modals.level_screen.status_badge.visible = map_on
# 置灰判据为排除式(调试浮窗开态不计入:对局中浮窗开着时角标仍可点)
root.board_zone.status_badge.disabled = root._modal_flow.badge_greyed_battle() # 互斥域谓词
root.level_screen.status_badge.disabled = root._modal_flow.badge_greyed_map()
root.ui.battle.board_zone.status_badge.disabled = root.ui.modals.flow.badge_greyed_battle() # 互斥域谓词
root.ui.modals.level_screen.status_badge.disabled = root.ui.modals.flow.badge_greyed_map()
# 调试浮层角标(全局浮层任何 UI 不遮挡——可见性不随场景变,只抬 offset
# 避让:地图屏底栏 85px 常驻,角标抬至底栏上方 16px;其余场景右下角 16px 贴底)
root.debug_badge.offset_bottom = -101.0 if map_on else -16.0
root.ui.screens.debug_badge.offset_bottom = -101.0 if map_on else -16.0
@@ -131,19 +131,19 @@ static func _on_wallet_changed(root: Variant, event: String, gp: int, extra: Dic
root.logger.log_wallet(event, gp, extra)
if String(event) == "gain":
root.run_scope.last_gain_amount = int(extra.get("amount", 0)) # GP 增量取数(每战恰一笔 gain)
if root.shop_overlay.visible and root.run_scope.runner != null:
if root.ui.modals.shop_overlay.visible and root.run_scope.runner != null:
if root.run_scope.runner.shop.is_shop_pool():
root.shop_overlay.refresh_shelf_state(int(root.run_scope.runner.gp)) # pool 面余额联动(货架内容仅买后重抽变)
root.ui.modals.shop_overlay.refresh_shelf_state(int(root.run_scope.runner.gp)) # pool 面余额联动(货架内容仅买后重抽变)
else:
root.shop_overlay.refresh_state(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp))
root.ui.modals.shop_overlay.refresh_state(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp))
root._refresh_presentations()
static func _on_legend_toggled(root: Variant) -> void:
if root.legend_overlay.is_open():
root._close_modal(root.legend_overlay)
if root.ui.modals.legend_overlay.is_open():
root.ui.modals.flow.close(root.ui.modals.legend_overlay)
else:
root._open_modal(root.legend_overlay)
root.ui.modals.flow.open(root.ui.modals.legend_overlay)
+116 -121
View File
@@ -1,7 +1,7 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 组合根 UI 装配分步承接件:三屏壳实例化、视图句柄接线、覆盖层装配与模态编排注入按步拆开。
## 本件是 main._build_ui 的逐字分步搬运(成员访问改 root 前缀)——main 侧留同名薄委托,装配序与 z 序零改动。
## 层序唯一事实源仍是 main.LAYER_CONTRACT + _apply_layer_contract(本件不持有层序逻辑)。
## 层序唯一事实源=root.ui.LAYER_CONTRACT + root.ui.apply_layer_contract(ui_registry.gd——本件不持有层序逻辑)。
extends RefCounted
const UiAssembler := preload("res://presentation/ui_assembler.gd")
@@ -35,50 +35,48 @@ static func _build_screens(root: Variant) -> void:
# 屏内 level_screen 与 battle_root 无同时可见态(屏级互斥+模态栈遮挡),层内先后零视觉差;
# 视觉共存对逐对验算:legend/battle(图例战斗中开——legend 在冒险屏壳之后=其上 ✓)、
# debug/level_screen(浮窗最后子=其上 ✓)、shop/level_screen(商店节点域弹出——模态均在屏壳上 ✓)。
root._adventure_screen = AdventureScreenScene.instantiate()
root.add_child(root._adventure_screen)
root._menu_screen = MenuScreenScene.instantiate()
root.add_child(root._menu_screen)
root._start_overlay = root._menu_screen.start_overlay
root._battle_root = root._adventure_screen.battle_root
root.stage = root._adventure_screen.stage
root.log_view = root._adventure_screen.log_view
root.info_panel = root._adventure_screen.info_panel
root.board_zone = root._adventure_screen.board_zone
root.board_view = root._adventure_screen.board_view # 探针兼容别名(= board_zone.board_view)
root.ap_bar = root._adventure_screen.ap_bar # 探针兼容别名(= board_zone.ap_bar)
root.settlement_fx = root._adventure_screen.settlement_fx
root.level_screen = root._adventure_screen.level_screen
root.ui.screens.adventure_screen = AdventureScreenScene.instantiate()
root.add_child(root.ui.screens.adventure_screen)
root.ui.screens.menu_screen = MenuScreenScene.instantiate()
root.add_child(root.ui.screens.menu_screen)
root.ui.screens.start_overlay = root.ui.screens.menu_screen.start_overlay
root.ui.screens.battle_root = root.ui.screens.adventure_screen.battle_root
root.ui.battle.stage = root.ui.screens.adventure_screen.stage
root.ui.battle.log_view = root.ui.screens.adventure_screen.log_view
root.ui.battle.info_panel = root.ui.screens.adventure_screen.info_panel
root.ui.battle.board_zone = root.ui.screens.adventure_screen.board_zone
root.ui.battle.settlement_fx = root.ui.screens.adventure_screen.settlement_fx
root.ui.modals.level_screen = root.ui.screens.adventure_screen.level_screen
static func _build_battle_wiring(root: Variant) -> void:
# 结算行排播泵(本体在 row_pacer.gd;本类注入视图句柄与呈现刷新回调,零信号连接)
root._row_pacer = RowPacer.new()
root._row_pacer.log_view = root.log_view
root._row_pacer.stage = root.stage
root._row_pacer.board_zone = root.board_zone
root._row_pacer.settlement_fx = root.settlement_fx
root._row_pacer.board_view = root.board_view
root._row_pacer.refresh_request = root._refresh_presentations
root._row_pacer.pump_ended = root._on_pump_ended # 演播完边沿统一派发(挂起 modal 优先,其次锁期点击重放)
root.add_child(root._row_pacer)
root.ui.screens.row_pacer = RowPacer.new()
root.ui.screens.row_pacer.log_view = root.ui.battle.log_view
root.ui.screens.row_pacer.stage = root.ui.battle.stage
root.ui.screens.row_pacer.board_zone = root.ui.battle.board_zone
root.ui.screens.row_pacer.settlement_fx = root.ui.battle.settlement_fx
root.ui.screens.row_pacer.board_view = root.ui.battle.board_zone.board_view
root.ui.screens.row_pacer.refresh_request = root._refresh_presentations
root.ui.screens.row_pacer.pump_ended = root.ui.modals.flow.pump_ended # 演播完边沿统一派发(挂起 modal 优先,其次锁期点击重放)
root.add_child(root.ui.screens.row_pacer)
# 秘宝两步交互(本体在 treasure_interact_controller.gd;settlement/runner/grant 随关卡/战斗更替,
# 经现值回调注入——仿 identity_resolver 只读回调先例;grant 每战新建编排器,现值取件防悬挂)
root._treasure_ui = TreasureInteractController.new({
"db": root.db, "meta": root.meta, "info_panel": root.info_panel, "toast": root.board_zone.toast,
"board_view": root.board_view,
"db": root.db, "meta": root.meta, "info_panel": root.ui.battle.info_panel, "toast": root.ui.battle.board_zone.toast,
"board_view": root.ui.battle.board_zone.board_view,
"settlement_get": func() -> Variant: return root.run_scope.settlement,
"runner_get": func() -> Variant: return root.run_scope.runner,
"grant_get": func() -> Variant: return root.run_scope.settlement.reward_grant() if root.run_scope.settlement != null else null,
})
root.board_zone.action_requested.connect(root._on_board_action)
root.board_zone.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(战斗屏 HUD 角标)
root.info_panel.treasure_slot_clicked.connect(root._treasure_ui.on_slot_clicked) # 两步交互第一步
root.board_zone.stop_pressed.connect(root._on_stop_pressed)
root.board_zone.refresh_confirmed.connect(root._on_refresh_confirmed)
root.board_zone.legend_toggled.connect(root._on_legend_toggled)
root.board_zone.hover_forbidden.connect(root._on_hover_forbidden)
root.board_zone.chord_attempted.connect(root._on_chord_attempted)
root.ui.battle.board_zone.action_requested.connect(root._on_board_action)
root.ui.battle.board_zone.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(战斗屏 HUD 角标)
root.ui.battle.info_panel.treasure_slot_clicked.connect(root._treasure_ui.on_slot_clicked) # 两步交互第一步
root.ui.battle.board_zone.stop_pressed.connect(root._on_stop_pressed)
root.ui.battle.board_zone.refresh_confirmed.connect(root._on_refresh_confirmed)
root.ui.battle.board_zone.legend_toggled.connect(root._on_legend_toggled)
root.ui.battle.board_zone.hover_forbidden.connect(root._on_hover_forbidden)
root.ui.battle.board_zone.chord_attempted.connect(root._on_chord_attempted)
root.logger = PlaytestLogger.new()
root.logger.ap_per_round = int(root.db.run.action_economy.ap_per_round)
@@ -88,136 +86,135 @@ static func _build_overlays(root: Variant) -> void:
# < 商店 < 营地 < 设置 < 状态面板(只读) < 生涯页(只读) < 确认弹窗 < 调试浮窗;
# 关卡画面住冒险屏壳(树序在模态之下)——登记序仍夹在节点层与三选一层之间,遮挡语义零改动)
var overlays: Dictionary[Variant, Variant] = UiAssembler.build_overlays(root, root.db)
root.legend_overlay = overlays.legend_overlay
root.node_overlay = overlays.node_overlay
root._end_overlay = overlays.end_overlay
root.shop_overlay = overlays.shop_overlay
root._camp_screen = CampScreenScene.instantiate()
root.add_child(root._camp_screen)
root.camp_overlay = root._camp_screen.camp_overlay
root.ui.modals.legend_overlay = overlays.legend_overlay
root.ui.modals.node_overlay = overlays.node_overlay
root.ui.modals.end_overlay = overlays.end_overlay
root.ui.modals.shop_overlay = overlays.shop_overlay
root.ui.screens.camp_screen = CampScreenScene.instantiate()
root.add_child(root.ui.screens.camp_screen)
root.ui.modals.camp_overlay = root.ui.screens.camp_screen.camp_overlay
# 营地屏壳插位(夹商店层与设置层之间)由 LAYER_CONTRACT 营地槽统一落位——禁散落 move_child
root.legend_overlay.closed.connect(root._on_modal_closed.bind(root.legend_overlay))
root.node_overlay.continue_pressed.connect(root._on_node_continue)
root.level_screen.route_chosen.connect(root._on_route_chosen)
root.level_screen.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(地图屏顶栏角标)
root.level_screen.abandon_requested.connect(root._on_abandon_requested) # 放弃入口
root._end_overlay.continue_pressed.connect(root._on_battle_end_continue)
root.ui.modals.legend_overlay.closed.connect(root.ui.modals.flow.close.bind(root.ui.modals.legend_overlay))
root.ui.modals.node_overlay.continue_pressed.connect(root._on_node_continue)
root.ui.modals.level_screen.route_chosen.connect(root._on_route_chosen)
root.ui.modals.level_screen.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(地图屏顶栏角标)
root.ui.modals.level_screen.abandon_requested.connect(root._on_abandon_requested) # 放弃入口
root.ui.modals.end_overlay.continue_pressed.connect(root._on_battle_end_continue)
static func _build_aux_panels(root: Variant) -> void:
root.settings_overlay = SettingsOverlay.new(root.settings_store) # 最小设置面(音量/全屏/减动效)
root.add_child(root.settings_overlay)
root.settings_overlay.closed.connect(root._on_settings_closed)
root.ui.modals.settings_overlay = SettingsOverlay.new(root.settings_store) # 最小设置面(音量/全屏/减动效)
root.add_child(root.ui.modals.settings_overlay)
root.ui.modals.settings_overlay.closed.connect(root._on_settings_closed)
# 局内状态面板·只读(登记设置层同档带 dim 遮罩,确认弹窗之前;
# 数据=组合根开层实时读 MetaProgression 访问器喂入,本层零写通路)
root.status_panel = StatusPanel.new()
root.add_child(root.status_panel)
root.status_panel.closed.connect(root._on_status_closed)
root.ui.modals.status_panel = StatusPanel.new()
root.add_child(root.ui.modals.status_panel)
root.ui.modals.status_panel.closed.connect(root._on_status_closed)
# 生涯页·只读全屏页(登记序=营地同档,设置层旁、确认弹窗之前;
# 树序同步=addChild 同位(登记序=树序=z 序纪律);数据=组合根开层实时读 SaveManager 计数
# 访问器+MetaProgression 访问器喂入,本层零写通路零跳转;入口=开始层「营地与系统」组生涯钮)
root.career_overlay = CareerOverlay.new()
root.add_child(root.career_overlay)
root.career_overlay.closed.connect(root._on_career_closed)
root.ui.modals.career_overlay = CareerOverlay.new()
root.add_child(root.ui.modals.career_overlay)
root.ui.modals.career_overlay.closed.connect(root._on_career_closed)
# 选档界面(营地交互重做卡③):登记序=生涯同档(树序同步=addChild 同位);三动作只发意图信号
#(激活/创建/删除+确认分派归组合根——视图零数据层依赖);入口=「进入存档」钮(卡② mn3 接线)。
root.save_select_overlay = SaveSelectOverlay.new()
root.add_child(root.save_select_overlay)
root.save_select_overlay.closed.connect(root._on_save_select_closed)
root.save_select_overlay.new_slot_requested.connect(root._on_save_select_new)
root.save_select_overlay.load_slot_requested.connect(root._on_save_select_load)
root.save_select_overlay.delete_slot_requested.connect(root._on_save_select_delete)
root.ui.modals.save_select_overlay = SaveSelectOverlay.new()
root.add_child(root.ui.modals.save_select_overlay)
root.ui.modals.save_select_overlay.closed.connect(root._on_save_select_closed)
root.ui.modals.save_select_overlay.new_slot_requested.connect(root._on_save_select_new)
root.ui.modals.save_select_overlay.load_slot_requested.connect(root._on_save_select_load)
root.ui.modals.save_select_overlay.delete_slot_requested.connect(root._on_save_select_delete)
static func _build_curio(root: Variant) -> void:
# 提升三选一覆盖层(z 序 = 节点层之上、终局层之下;确认后同拍推进;
# 插位由 LAYER_CONTRACT curio 槽统一落位——禁散落 move_child)
root.curio_overlay = CurioPickOverlay.new()
root.add_child(root.curio_overlay)
root.ui.modals.curio_overlay = CurioPickOverlay.new()
root.add_child(root.ui.modals.curio_overlay)
# 提升三选一与替换子交互(本体在 curio_interact_controller.gd;本类只接线与回调注入,
# 模态栈/演播后排窗/呈现刷新/fatal 经回调注入,adventure/runner 现值回调)
root._bonus = CurioInteractController.new(CurioFlowContext.new(root.db,
{"curio_overlay": root.curio_overlay, "end_overlay": root._end_overlay,
"board_view": root.board_view, "toast": root.board_zone.toast},
{"curio_overlay": root.ui.modals.curio_overlay, "end_overlay": root.ui.modals.end_overlay,
"board_view": root.ui.battle.board_zone.board_view, "toast": root.ui.battle.board_zone.toast},
{"meta_get": func() -> Variant: return root.meta, "save_get": func() -> Variant: return root.save_manager,
"adventure_get": func() -> Variant: return root.run_scope.adventure,
"runner_get": func() -> Variant: return root.run_scope.runner,
"adventure_mode_get": func() -> Variant: return root.run_scope.is_adventure},
{"open_modal": root._open_modal, "open_modal_after_playback": root._open_modal_after_playback,
"close_modal": root._close_modal, "refresh_presentations": root._refresh_presentations,
{"open_modal": root.ui.modals.flow.open, "open_modal_after_playback": root.ui.modals.flow.open_after_playback,
"close_modal": root.ui.modals.flow.close, "refresh_presentations": root._refresh_presentations,
"fatal_screen": root._fatal_screen,
"gp_summary": _gp_summary_cb(root)})) # 构造参数收拢 context(u20 三束)
root.curio_overlay.pick_confirmed.connect(root._bonus.on_pick_confirmed)
root.curio_overlay.continue_pressed.connect(root._bonus.on_continue)
root.curio_overlay.refresh_requested.connect(root._bonus.on_refresh)
root.ui.modals.curio_overlay.pick_confirmed.connect(root._bonus.on_pick_confirmed)
root.ui.modals.curio_overlay.continue_pressed.connect(root._bonus.on_continue)
root.ui.modals.curio_overlay.refresh_requested.connect(root._bonus.on_refresh)
static func _build_menu_wiring(root: Variant) -> void:
# 菜单屏接线(句柄经屏壳暴露;三入口化——进入存档/设置/退出游戏,卡② mn3:
# 进入存档钮落点=卡③选档层 _open_save_select,点击链在此闭环)
root._menu_screen.entersavebtn.pressed.connect(root._open_save_select)
root.ui.screens.menu_screen.entersavebtn.pressed.connect(root._open_save_select)
# 调试入口=UI 最顶层全局浮层角标(任何 UI 不遮挡——菜单/战斗/地图三枚
# 屏内角标由全局浮层承载;连接归组合根,树序/登记面见 LAYER_CONTRACT 末位)
root.debug_badge = UiAssembler.build_debug_badge(root)
root.add_child(root.debug_badge)
root.debug_badge.pressed.connect(root._on_debug_badge_pressed)
root.ui.screens.debug_badge = UiAssembler.build_debug_badge(root)
root.add_child(root.ui.screens.debug_badge)
root.ui.screens.debug_badge.pressed.connect(root._on_debug_badge_pressed)
# 营地/商店投影(本体在 camp_shop_presenter.gd;本类只接线、探针缝保缝转发;
# 购买写调用唯一经 MetaProgression/LevelRunner core 单口——组合根 lambda 现值注入)
root._camp_pres = CampShopPresenter.new(CampFlowContext.new(root.db,
{"camp_overlay": root.camp_overlay, "shop_overlay": root.shop_overlay,
"toast": root.board_zone.toast, "start_overlay": root._start_overlay, "logger": root.logger},
{"camp_overlay": root.ui.modals.camp_overlay, "shop_overlay": root.ui.modals.shop_overlay,
"toast": root.ui.battle.board_zone.toast, "start_overlay": root.ui.screens.start_overlay, "logger": root.logger},
{"meta_get": func() -> Variant: return root.meta, "save_get": func() -> Variant: return root.save_manager,
"runner_get": func() -> Variant: return root.run_scope.runner},
{"open_modal": root._open_modal, "close_modal": root._close_modal,
{"open_modal": root.ui.modals.flow.open, "close_modal": root.ui.modals.flow.close,
"refresh_presentations": root._refresh_presentations})) # 构造参数收拢 context(u21 三束)
root.shop_overlay.buy_requested.connect(root._camp_pres.on_shop_buy)
root.shop_overlay.refresh_requested.connect(root._camp_pres.on_shop_refresh) # MC-1
root.shop_overlay.leave_pressed.connect(root._camp_pres.on_shop_leave)
root.camp_overlay.buy_requested.connect(root._camp_pres.on_camp_buy)
root.camp_overlay.attune_toggled.connect(root._camp_pres.on_camp_attune_toggle)
root.camp_overlay.tree_buy_requested.connect(root._camp_pres.on_camp_tree_buy)
root.camp_overlay.tree_fork_toggled.connect(root._camp_pres.on_camp_fork_toggle)
root.camp_overlay.forge_buy_requested.connect(root._camp_pres.on_camp_forge_buy) # 锻造三意图(写口唯一归 MetaProgression)
root.camp_overlay.forge_unlock_requested.connect(root._camp_pres.on_camp_forge_unlock)
root.camp_overlay.forge_select_requested.connect(root._camp_pres.on_camp_forge_select)
root.camp_overlay.closed.connect(root._camp_pres.on_camp_closed)
root._menu_screen.settingsbtn.pressed.connect(root._open_settings) # 最小设置面入口
root._menu_screen.quitbtn.pressed.connect(root._quit_game) # 退出游戏提级一级菜单
root.ui.modals.shop_overlay.buy_requested.connect(root._camp_pres.on_shop_buy)
root.ui.modals.shop_overlay.refresh_requested.connect(root._camp_pres.on_shop_refresh) # MC-1
root.ui.modals.shop_overlay.leave_pressed.connect(root._camp_pres.on_shop_leave)
root.ui.modals.camp_overlay.buy_requested.connect(root._camp_pres.on_camp_buy)
root.ui.modals.camp_overlay.attune_toggled.connect(root._camp_pres.on_camp_attune_toggle)
root.ui.modals.camp_overlay.tree_buy_requested.connect(root._camp_pres.on_camp_tree_buy)
root.ui.modals.camp_overlay.tree_fork_toggled.connect(root._camp_pres.on_camp_fork_toggle)
root.ui.modals.camp_overlay.forge_buy_requested.connect(root._camp_pres.on_camp_forge_buy) # 锻造三意图(写口唯一归 MetaProgression)
root.ui.modals.camp_overlay.forge_unlock_requested.connect(root._camp_pres.on_camp_forge_unlock)
root.ui.modals.camp_overlay.forge_select_requested.connect(root._camp_pres.on_camp_forge_select)
root.ui.modals.camp_overlay.closed.connect(root._camp_pres.on_camp_closed)
root.ui.screens.menu_screen.settingsbtn.pressed.connect(root._open_settings) # 最小设置面入口
root.ui.screens.menu_screen.quitbtn.pressed.connect(root._quit_game) # 退出游戏提级一级菜单
static func _build_debug_and_resolvers(root: Variant) -> void:
var inter: Dictionary[Variant, Variant] = UiAssembler.build_inter_overlay(root)
root._inter_overlay = inter.overlay
root._inter_label = inter.label
root.ui.screens.inter_overlay = inter.overlay
root.ui.screens.inter_label = inter.label
inter.next_btn.pressed.connect(root._on_inter_next_pressed)
# 确认弹窗(新冒险互斥确认弹窗化):树序落在开始层/过场层之后 = 子级最末
#(纪律「登记序 = 树序 = z 序」——confirm_dialog.open 禁运行时 move_child 自提抬层;
# 弹窗只在开始菜单域弹出,绘制序恒最上;
# 诊断转储入口在调试浮窗)
root.confirm_dialog = ConfirmDialog.new()
root.add_child(root.confirm_dialog)
root.ui.modals.confirm_dialog = ConfirmDialog.new()
root.add_child(root.ui.modals.confirm_dialog)
# 消费方编排(卡③ 契约 3/sl8——禁双常驻静态接线):open 前设 _confirm_consumer,
# confirmed/cancelled 后按该状态量分派;分派面可容纳后续消费方(卡④ 到位直接接入不返工)
root.confirm_dialog.confirmed.connect(root._on_confirm_accepted)
root.confirm_dialog.cancelled.connect(root._on_confirm_declined)
root.ui.modals.confirm_dialog.confirmed.connect(root._on_confirm_accepted)
root.ui.modals.confirm_dialog.cancelled.connect(root._on_confirm_declined)
# 调试浮窗(主菜单三散件收一:随机种子/数值调整/批量模拟/两层调试入口;
# 全局可开浮窗:调试会话恒开=启动即开〔_ready〕+两屏角标再开+当局数据只读区块;
# 诊断转储入口在本浮窗,玩家画面零常驻调试件)。
# 树序在开始层与确认弹窗之后 = 浮于菜单之上的 Dialog(不隐藏开始层——与确认弹窗同款形态);
# 开窗唯一入口 = _show_debug_overlay(C6 单口——角标/探针/启动路径全收口)
root.debug_overlay = DebugOverlay.new(root.db, int(root.db.combat.hero.atk), int(root.db.combat.hero.max_hp))
root.add_child(root.debug_overlay)
root._seed_edit = root.debug_overlay.seed_edit # 种子读取面在浮窗(_seed_value 唯一消费点不变)
root.debug_overlay.closed.connect(root._on_modal_closed.bind(root.debug_overlay))
root.debug_overlay.apply_requested.connect(root._on_tune_apply)
root.debug_overlay.simulate_requested.connect(root._on_tune_simulate)
root.debug_overlay.debug_levels_requested.connect(root._start_debug_levels)
root.ui.modals.debug_overlay = DebugOverlay.new(root.db, int(root.db.combat.hero.atk), int(root.db.combat.hero.max_hp))
root.add_child(root.ui.modals.debug_overlay)
root.ui.modals.debug_overlay.closed.connect(root.ui.modals.flow.close.bind(root.ui.modals.debug_overlay))
root.ui.modals.debug_overlay.apply_requested.connect(root._on_tune_apply)
root.ui.modals.debug_overlay.simulate_requested.connect(root._on_tune_simulate)
root.ui.modals.debug_overlay.debug_levels_requested.connect(root._start_debug_levels)
# 棋盘解析回调族(有态绑定件——board_resolvers.gd 承接;组合根只接线;局部持有不占类成员位)
var resolvers = BoardResolversScript.new(root.db, root.debug_overlay, func() -> Variant: return root.run_scope.settlement)
var resolvers = BoardResolversScript.new(root.db, root.ui.modals.debug_overlay, func() -> Variant: return root.run_scope.settlement)
# 奖励徽章数据源:地图战斗资源 身份解析只读回调(已开格身份 icon——挖掘期 Active 相位内有效)
root.board_view.set_identity_resolver(resolvers.identity_lookup)
root.ui.battle.board_zone.board_view.set_identity_resolver(resolvers.identity_lookup)
# 数字遮蔽数据源:地形变异 hide_number 解析只读回调(信息型呈现——board_view 渲染面读取)
root.board_view.set_variant_mask_resolver(resolvers.variant_mask_lookup)
root.board_view.set_variant_mark_resolver(resolvers.variant_mark_lookup) # flat_damage 未开格角标
root.ui.battle.board_zone.board_view.set_variant_mask_resolver(resolvers.variant_mask_lookup)
root.ui.battle.board_zone.board_view.set_variant_mark_resolver(resolvers.variant_mark_lookup) # flat_damage 未开格角标
# 调试会话(英雄生效值预览回调绑同一解析件——创建晚于浮窗、随 _build_ui 一次接线)
root._debug = DebugSessionScript.new(root.db, root.get_node(root.AUTOLOAD_PATH_RNG),
func() -> Dictionary: return resolvers.debug_hero_override(), root._seed_value)
@@ -228,24 +225,22 @@ static func _build_modal_flow(root: Variant) -> void:
# 模态登记(缺陷⑤纪律——登记序 = 树序 = z 序):层序唯一事实源=LAYER_CONTRACT,
# _apply_layer_contract 统一落位(关卡画面宿住冒险屏壳、营地屏壳夹商店与设置层之间、
# 三选一对位终局层之下等特例均在合同数据内表达,禁散落 move_child)
# 模态编排注入(ModalFlow 纯逻辑类:依赖仅 ModalStack+查询 Callable)
root._modal_flow = ModalFlow.new()
root._modal_flow.start_visible_get = func() -> bool: return root._start_overlay.visible
root._modal_flow.inter_visible_get = func() -> bool: return root._inter_overlay.visible
root._modal_flow.settlement_get = func() -> Variant: return root.run_scope.settlement
root._modal_flow.rows_empty_get = func() -> bool: return root._row_pacer.pending_rows().is_empty()
root._modal_flow.flush_rows = root._flush_row_queue
root._modal_flow.on_open_sync = func() -> void:
# 模态编排注入(ModalFlow 纯逻辑类:依赖仅 ModalStack+查询 Callable——实例随 ui.modals 容器构造建立)
root.ui.modals.flow.start_visible_get = func() -> bool: return root.ui.screens.start_overlay.visible
root.ui.modals.flow.inter_visible_get = func() -> bool: return root.ui.screens.inter_overlay.visible
root.ui.modals.flow.settlement_get = func() -> Variant: return root.run_scope.settlement
root.ui.modals.flow.rows_empty_get = func() -> bool: return root.ui.screens.row_pacer.pending_rows().is_empty()
root.ui.modals.flow.flush_rows = root.ui.screens.row_pacer.flush
root.ui.modals.flow.on_open_sync = func() -> void:
root._sync_status_entry() # 状态面板入口角标可见性/置灰随开态同步
root._sync_debug_overlay() # 浮窗当局数据/按钮态随开态同步
root._modal_flow.on_close_sync = func() -> void:
root.ui.modals.flow.on_close_sync = func() -> void:
root._sync_status_entry() # 关闭恢复角标可点态(无残留——同源口径)
root._sync_debug_overlay() # 他层关闭复活浮窗时同步刷新(关后自动恢复语义配套)
root._modal_flow.replay_click = root._replay_locked_click
root._modals = root._modal_flow.stack # 探针 _list 登记序断言缝(ui_struct 登记序=z 序纪律断言)
root.ui.modals.flow.replay_click = root._replay_locked_click
# 视觉层合同装配(唯一事实源=LAYER_CONTRACT:树序+登记序同源派生——改序只改合同表)
root._apply_layer_contract()
root._tune_sim = TuneSimController.new(root.db, root.debug_overlay, int(root.db.combat.hero.max_hp))
root.ui.apply_layer_contract(root)
root._tune_sim = TuneSimController.new(root.db, root.ui.modals.debug_overlay, int(root.db.combat.hero.max_hp))
static func fatal_screen(root: Variant, msg: String) -> void:
var l := UiKit.label(msg,UiKit.FS_SUB, true, UiKit.RED_ALERT)
+47 -9
View File
@@ -2,16 +2,28 @@
## 调整板批量模拟控制器:逐次驱动关卡自动策略(与 headless 扫描同口径),进度/报告经面板 setter 注入。
## 零核心写——表基线复位由组合根在移交前完成,加成施加只走自动策略内部 runner.set_hero_override 单路径。
## presentation 不 preload smoke:驱动实例由调用方注入(须供与 level_autoplay 同款 run_level 签名),缺省 null=运行时 load 兜底。
## 逐帧步进:每局恰占一帧——第 0 局在调用点同步段执行,第 i 局(i≥1)在第 i 帧信号发射点执行,收尾恒在第 n 帧发射点;
## n=0 退化路径为调用点同步收尾零帧。
## _batch_finished 信号契约:整批收尾放行——发射时点=报告注入与运行态复位之后;消费方=run() 自身收尾放行,
## 外部 await run(n) 经此挂起至整批完成(协程身份契约)。
extends RefCounted
const SEED_BASE := 4200 # 模拟种子基(与历史实现同源——同种子可回放)
const PlaytestLogger := preload("res://presentation/playtest_logger.gd") # 试玩日志(同层依赖——运行时 load 收敛为 preload 常量)
signal _batch_finished
var db
var panel # 调试浮窗 setter 面:atk_pct/hp_pct/set_sim_running/set_sim_progress/set_sim_report
var _base_hp := 0 # 报告中的"基础 HP"口径注记值
var _driver = null # 关卡自动策略驱动(调用方注入;null = 旧行为运行时 load 兜底)
var _running := false # 运行态(run 全程 true——组合根互查守卫用,与面板置灰解耦)
var _sim_n := 0 # 本批局数(run 期活成员——批末失效、下次 run 重写)
var _sim_i := 0 # 已执行局数(步进计数)
var _sim_stats: Dictionary[Variant, Variant] = {} # stats 六键束(run 期承载)
var _sim_autoplay = null # 自动策略驱动实例(run 期承载)
var _sim_logger = null # 共享 JSONL 日志会话(run 期承载)
var _finished := false # 批完成旗标(_finish_batch 置位——收尾段同步承载)
func _init(db_ref, panel_ref, base_hp: int, driver = null) -> void:
@@ -44,16 +56,44 @@ func run(n: int) -> void:
panel.set_sim_progress("模拟中:0/%d …" % n)
var stats := {"wins": 0, "dmg_all": [], "hp_left_win": [], "hp_boss": [], "rounds_all": [], "death_nodes": {}}
var autoplay = _autoplay()
for i in range(n):
_run_one_level(autoplay, stats, i, panel, logger)
panel.set_sim_progress("模拟中:%d/%d …" % [i + 1, n])
await panel.get_tree().process_frame
logger.close_session()
panel.set_sim_report(HudPresenter.tune_sim_report(n, panel.atk_pct(), panel.hp_pct(), stats, _base_hp))
_sim_n = n
_sim_i = 0
_sim_stats = stats
_sim_autoplay = autoplay
_sim_logger = logger
_finished = false
if n > 0:
if not panel.get_tree().process_frame.is_connected(_step_frame):
panel.get_tree().process_frame.connect(_step_frame) # 恰好连接一次(迭代 39 先例同款守卫)
_step_frame() # 首步同步执行(第 0 局在调用栈内跑完——与 await 形态对齐)
await _batch_finished
else:
_finish_batch() # n=0:循环零进同步收尾,不挂起(与 await 形态一致零帧)
# 逐帧步进回调:i<n 执行一局+进度串+计数推进(每局恰占一帧);i 达 n 次帧断连→收尾→放行
func _step_frame() -> void:
if _sim_i < _sim_n:
_run_one_level(_sim_autoplay, _sim_stats, _sim_i, panel, _sim_logger)
panel.set_sim_progress("模拟中:%d/%d …" % [_sim_i + 1, _sim_n])
_sim_i += 1
else:
panel.get_tree().process_frame.disconnect(_step_frame) # 先断连防再入
_finish_batch()
_batch_finished.emit()
# 整批收尾单点(收尾四语句同帧同相位:close_session→报告→运行态复位——次序与 run() 尾现行形态逐句对齐)
func _finish_batch() -> void:
_finished = true
_sim_logger.close_session()
panel.set_sim_report(HudPresenter.tune_sim_report(_sim_n, panel.atk_pct(), panel.hp_pct(), _sim_stats, _base_hp))
panel.set_sim_running(false)
_running = false
## 单局模拟域(循环体提函数——结果四桶记账;u19 stats 六键同构承载)
func _run_one_level(autoplay, stats: Dictionary, i: int, panel, logger) -> void:
func _run_one_level(autoplay, stats: Dictionary, i: int, panel, logger) -> void: # type-hint: ignore(驱动/面板/日志件鸭子型注入——同 run 构造缝)
var st = autoplay.run_level(db, SEED_BASE + i, 1, panel.atk_pct(), panel.hp_pct(), false, logger)
(stats["dmg_all"] as Array).append(st.hero_dmg)
(stats["rounds_all"] as Array).append(st.rounds)
@@ -64,5 +104,3 @@ func _run_one_level(autoplay, stats: Dictionary, i: int, panel, logger) -> void:
elif st.result == "defeat":
var key := "%d.%s" % [st.defeat_node_index, st.defeat_node_type]
stats["death_nodes"][key] = int(stats["death_nodes"].get(key, 0)) + 1
panel.set_sim_running(false)
_running = false
+9 -7
View File
@@ -6,8 +6,8 @@ extends RefCounted
# 战斗屏三区(B 战斗画面 / C·D 两列——节点链
# 进度由关卡画面本体(选路覆盖层整图)承载,28px 纵向空间拨给 C/D 底行 520→548;
# 无顶栏进度条、无道具面板区)。
# 进度由关卡画面本体(选路覆盖层整图)承载,纵向空间归 C/D 底行;
# 无顶栏进度条、无道具面板区)。分区几何读 ConfigDb.presentation.battle_layout。
# + 结算演出覆盖层(A–D 之上、模态之下——跨区弹道/光带载体)。
# 返回句柄字典:{stage, log_view, info_panel, board_zone, board_view, ap_bar, settlement_fx}
# (返回句柄不含 gp_label/keystone_label——GP/钥石不在战斗屏呈现)
@@ -17,20 +17,22 @@ static func build_battle_ui(root: Control, db: ConfigDb) -> Dictionary:
root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
root.add_theme_constant_override("separation", 0)
# B 区:战斗画面(全宽 × 252px——常驻双舞台,布局决策 D1)
var stage := BattleStageView.new(db)
var layout: BattleLayoutTable = db.presentation.battle_layout
# B 区:战斗画面(全宽 × battle_layout.stage_h——常驻双舞台,布局决策 D1)
var stage := BattleStageView.new(db, layout)
root.add_child(stage)
# C/D 两列(548px——含顶栏让出的 28px 纵向空间)
# C/D 两列(高度 = 基座高 − B 区高——底行承载)
var bottom := HBoxContainer.new()
bottom.size_flags_vertical = Control.SIZE_EXPAND_FILL
bottom.add_theme_constant_override("separation", 4)
root.add_child(bottom)
var info_panel := HeroInfoPanelView.new()
var info_panel := HeroInfoPanelView.new(layout)
bottom.add_child(info_panel)
var board_zone := BoardZoneView.new()
var board_zone := BoardZoneView.new(layout)
bottom.add_child(board_zone)
board_zone.ap_bar.set_refresh_cost(int(db.run.action_economy.rap_refresh_cost)) # RAP 读数表值驱动
+10
View File
@@ -0,0 +1,10 @@
## 战区视图域句柄册:战斗屏 A–D 区视图与结算演出覆盖层的注册面域容器。
## 纯字段无方法;RefCounted 不挂树——节点本体树位与 add_child 归属由组合根与装配承接件负责。
class_name UiBattle
extends RefCounted
var stage: BattleStageView # B 区(含 log_view 明细带)
var log_view: BattleLogView
var info_panel: HeroInfoPanelView # C 区(攻击/AC 数据框+两预留区)
var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/fx)
var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道/光带——A–D 之上、模态之下)
+1
View File
@@ -0,0 +1 @@
uid://d35j5hry1f40v
+32
View File
@@ -0,0 +1,32 @@
## 模态层域句柄册:十三个模态覆盖层句柄+模态栈编排(ModalFlow——登记/开合/输入锁谓词)的注册面域容器。
## RefCounted 不挂树;模态登记唯一口=register_layer(anchors 撑满宿主+排入 ModalStack)。
## flow 随容器构造即建(装配早期注入点经 root.ui.modals.flow.* 取 Callable——晚于 flow 建立时点)。
class_name UiModals
extends RefCounted
var flow: ModalFlow
var legend_overlay: LegendOverlay
var node_overlay: NodeOverlay
var level_screen: LevelMapScreen
var curio_overlay: CurioPickOverlay
var end_overlay: BattleEndOverlay
var shop_overlay: ShopOverlay
var camp_overlay: CampOverlay
var settings_overlay: SettingsOverlay
var status_panel: StatusPanel
var career_overlay: CareerOverlay
var save_select_overlay: SaveSelectOverlay
var confirm_dialog: ConfirmDialog
var debug_overlay: DebugOverlay
func _init() -> void:
flow = ModalFlow.new() # 查询/钩子 Callable 由 _build_modal_flow 注入(注入时点不变——注入值面向已在座实例)
func register_layer(m: Control, tags: Array = []) -> void:
# 撑满宿主(Control 默认锚 = 0×0——全屏变暗层与居中布局整体失效;route 选路层预览截图实证后
# 统一收口:六个模态同病,探针经信号直发绕过真实点击从未暴露。anchors_and_offsets 而非 anchors
# ——后者会把控件重置回最小内容尺寸,ui_assembler 头注同款纪律)
m.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
flow.register(m, tags) # 排除组随层登记(non_locking=调试浮窗/screen_state=关卡地图屏)
+1
View File
@@ -0,0 +1 @@
uid://dbs4qveqvr5hg
+101
View File
@@ -0,0 +1,101 @@
## 界面注册面:呈现层界面元素清单的唯一事实源——哪些层、什么序、地址在哪。
## 持有三域容器(screens/battle/modals)、LAYER_CONTRACT 合同表、node(key) 合同寻址、
## apply_layer_contract(root) 唯一落位口(全仓唯一 move_child 点)。
## RefCounted 不挂树:零信号连接、零会话期引用、零核心写调用——组合根单点装配不变。
class_name UiRegistry
extends RefCounted
const ModalStack := preload("res://presentation/modal_stack.gd")
# ==================== 视觉层合同(唯一事实源——树序=登记序=z 序;改序只改此表) ====================
# kind: shell=屏壳容器(成员随壳不占独立树位;level_screen 例外另占登记位)/modal=模态层(入登记册,
# tags 随登记)/state=全屏态(占树位不入登记册)/node=非视觉节点(占树位不入登记册)。
# host=宿住屏壳(树序随宿主、登记序按本合同);shell=模态成员的屏壳(成员与屏壳同槽:屏壳占该树位、
# 成员占该登记位)。落位=apply_layer_contract(全仓唯一层序落位点,add_child 之后统一排序+登记)。
const LAYER_CONTRACT: Array[Dictionary] = [
{"key": "adventure_screen", "kind": "shell"},
{"key": "menu_screen", "kind": "shell"},
{"key": "row_pacer", "kind": "node"},
{"key": "legend_overlay", "kind": "modal", "tags": []},
{"key": "node_overlay", "kind": "modal", "tags": []},
{"key": "level_screen", "kind": "modal", "tags": [ModalStack.TAG_SCREEN_STATE], "host": "adventure_screen"},
{"key": "curio_overlay", "kind": "modal", "tags": []},
{"key": "end_overlay", "kind": "modal", "tags": []},
{"key": "shop_overlay", "kind": "modal", "tags": []},
{"key": "camp_overlay", "kind": "modal", "tags": [], "shell": "camp_screen"},
{"key": "settings_overlay", "kind": "modal", "tags": []},
{"key": "status_panel", "kind": "modal", "tags": []},
{"key": "career_overlay", "kind": "modal", "tags": []},
{"key": "save_select_overlay", "kind": "modal", "tags": []}, # 选档界面(营地交互重做卡③——登记生涯同档)
{"key": "inter_overlay", "kind": "state", "tags": []},
{"key": "confirm_dialog", "kind": "modal", "tags": []},
{"key": "debug_overlay", "kind": "modal", "tags": [ModalStack.TAG_NON_LOCKING]},
{"key": "debug_badge", "kind": "float"}, # 调试入口全局浮层——末位=最顶绘制序;
# float 类不注册 ModalStack(open() 遮挡记账只隐藏登记层)→ 任何 UI 打开都不遮它
]
var screens: UiScreens
var battle: UiBattle
var modals: UiModals
func _init() -> void:
screens = UiScreens.new()
battle = UiBattle.new()
modals = UiModals.new()
# 合同层寻址(key→节点;纯查表不含层序语义——层序唯一事实源=LAYER_CONTRACT)
func node(key: String) -> Node:
match key:
"adventure_screen": return screens.adventure_screen
"menu_screen": return screens.menu_screen
"row_pacer": return screens.row_pacer
"legend_overlay": return modals.legend_overlay
"node_overlay": return modals.node_overlay
"level_screen": return modals.level_screen
"curio_overlay": return modals.curio_overlay
"end_overlay": return modals.end_overlay
"shop_overlay": return modals.shop_overlay
"camp_screen": return screens.camp_screen
"camp_overlay": return modals.camp_overlay
"settings_overlay": return modals.settings_overlay
"status_panel": return modals.status_panel
"career_overlay": return modals.career_overlay
"save_select_overlay": return modals.save_select_overlay
"inter_overlay": return screens.inter_overlay
"confirm_dialog": return modals.confirm_dialog
"debug_overlay": return modals.debug_overlay
"debug_badge": return screens.debug_badge
return null
# 合同装配(_build_ui 尾调用):按 LAYER_CONTRACT 序统一落位——直接子层 move_child 到合同槽、
# 模态层按合同序登记;宿住层(host)树序随宿主只登记;成员层(shell)与屏壳同槽(屏壳占树位、成员占登记位)。
# 本函数是全仓唯一层序落位点(move_child 单口)——改层序只改 LAYER_CONTRACT。
func apply_layer_contract(root: Control) -> void:
var slot := 0
for entry: Dictionary in LAYER_CONTRACT:
var tags: Array[Variant] = entry.get("tags", [])
if entry.has("shell"):
var shell: Node = node(String(entry["shell"]))
var member: Node = node(String(entry["key"]))
if shell != null:
root.move_child(shell, slot)
slot += 1
if member != null:
modals.register_layer(member as Control, tags)
else:
push_error("[合同装配] 层缺失: %s" % String(entry["key"]))
continue
var layer: Node = node(String(entry["key"]))
if layer == null:
push_error("[合同装配] 层缺失: %s" % String(entry["key"]))
continue
if entry.has("host"):
modals.register_layer(layer as Control, tags)
continue
root.move_child(layer, slot)
slot += 1
if String(entry["kind"]) == "modal":
modals.register_layer(layer as Control, tags)
+1
View File
@@ -0,0 +1 @@
uid://d1hahbtan1te8
+16
View File
@@ -0,0 +1,16 @@
## 屏壳与全屏容器域句柄册:三屏壳、屏内一级子层、全屏态、浮层角标、排播泵节点的注册面域容器。
## 纯字段无方法;RefCounted 不挂树——节点本体树位与 add_child 归属由组合根与装配承接件负责。
class_name UiScreens
extends RefCounted
const RowPacer := preload("res://presentation/row_pacer.gd") # 排播泵取型(row_pacer.gd 非 class_name 全局)
var menu_screen: Control
var camp_screen: Control
var adventure_screen: Control
var start_overlay: Control
var battle_root: Control
var inter_overlay: Control
var inter_label: Label
var debug_badge: Button
var row_pacer: RowPacer
+1
View File
@@ -0,0 +1 @@
uid://dd8ep7nhmyrdq
+2 -2
View File
@@ -31,8 +31,8 @@ var _arm_timer := ArmedTimer.new() # 弃图刷新确认窗(T3/D3 P4 乙—
const REFRESH_CONFIRM_WINDOW_S := 4.0 # 弃图刷新确认窗(原 tween_interval 字面量升常量——语义零变化)
func _init() -> void:
custom_minimum_size = Vector2(0, 44)
func _init(layout: BattleLayoutTable) -> void:
custom_minimum_size = Vector2(0, layout.ap_bar_h)
_arm_timer.expired.connect(_disarm)
var sb := UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, 0, 1, Vector4(8, 4, 8, 4))
add_theme_stylebox_override("panel", sb)
+9 -7
View File
@@ -4,12 +4,12 @@ class_name BattleLogView
extends PanelContainer
const BAND_H_DIG := 24
const BAND_H_SETTLE := 84
const AGG_TYPES := ["reward_ineffective"]
const COL_GOLD := UiKit.HEX_GOLD
var _db
var _band_h_dig := 0
var _band_h_settle := 0
var _log: RichTextLabel
var _summary: Label
var _hint: Label
@@ -18,9 +18,11 @@ var _round_rows: Array[String] = [] # 本结算轮行缓冲(上轮摘要
var _pending_agg := {} # 聚合计数行 pending(type+batch_key → {row, count})
func _init(db_ref) -> void:
func _init(db_ref, layout: BattleLayoutTable) -> void:
_db = db_ref
custom_minimum_size = Vector2(0, BAND_H_DIG)
_band_h_dig = layout.log_band_h_dig
_band_h_settle = layout.log_band_h_settle
custom_minimum_size = Vector2(0, _band_h_dig)
size_flags_vertical = Control.SIZE_SHRINK_END
var vb := VBoxContainer.new()
vb.add_theme_constant_override("separation", 2)
@@ -29,7 +31,7 @@ func _init(db_ref) -> void:
vb.add_child(head)
# 明细条灰字对深灰底实测 2.63:1 → 暗底双色墨制=暖白族
_summary = UiKit.label("",UiKit.FS_BODY, false, UiKit.WARM)
_summary.custom_minimum_size = Vector2(0, BAND_H_DIG - 4)
_summary.custom_minimum_size = Vector2(0, _band_h_dig - 4)
_summary.clip_text = true
_summary.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
head.add_child(_summary)
@@ -39,7 +41,7 @@ func _init(db_ref) -> void:
_log = RichTextLabel.new()
_log.bbcode_enabled = true
_log.scroll_following = true
_log.custom_minimum_size = Vector2(0, BAND_H_SETTLE - 26)
_log.custom_minimum_size = Vector2(0, _band_h_settle - 26)
_log.clip_contents = true
_log.scroll_active = true
vb.add_child(_log)
@@ -53,7 +55,7 @@ func set_mode(mode: String) -> void:
var settle := mode == "settle"
_log.visible = settle
_summary.visible = not settle
custom_minimum_size = Vector2(0, BAND_H_SETTLE if settle else BAND_H_DIG)
custom_minimum_size = Vector2(0, _band_h_settle if settle else _band_h_dig)
func mode() -> String:
+6 -6
View File
@@ -24,11 +24,11 @@ var _viewport: SubViewport
var world: Node2D # StageWorld(preload 脚本实例——Node2D 世界根/字体挂点)
func _init(db_ref) -> void:
custom_minimum_size = Vector2(0, 252)
func _init(db_ref, layout: BattleLayoutTable) -> void:
custom_minimum_size = Vector2(0, layout.stage_h)
_build_backdrop()
_build_world_stage()
_build_text_half(db_ref)
_build_text_half(db_ref, layout)
_build_companion_layer(db_ref)
@@ -75,7 +75,7 @@ func _build_world_stage() -> void:
## Control 文本半区域(round_label / stage_center / log_view 原样保留——C1 永久 Control)
func _build_text_half(db_ref) -> void:
func _build_text_half(db_ref, layout: BattleLayoutTable) -> void: # type-hint: ignore(db 鸭子型注入——单测以最小 stub 构造)
var vb := VBoxContainer.new()
vb.add_theme_constant_override("separation", 4)
vb.set_anchors_preset(Control.PRESET_FULL_RECT)
@@ -86,7 +86,7 @@ func _build_text_half(db_ref) -> void:
add_child(vb)
_build_narr_row(vb)
_build_center_row(vb)
log_view = BattleLogView.new(db_ref)
log_view = BattleLogView.new(db_ref, layout)
vb.add_child(log_view)
@@ -124,7 +124,7 @@ func _build_center_row(vb: VBoxContainer) -> void:
## 壳层伴随件层域(最顶——世界站桩之上的 Control 触达面)+主角站桩注册(世界)+ 状态行伴随件(壳层)
func _build_companion_layer(db_ref) -> void:
func _build_companion_layer(db_ref) -> void: # type-hint: ignore(db 鸭子型注入——单测以最小 stub 构造)
_companion_layer = Control.new()
_companion_layer.set_anchors_preset(Control.PRESET_FULL_RECT)
_companion_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
+44 -367
View File
@@ -1,128 +1,31 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## CellUnit——BoardView SubViewport 世界内逐格渲染件(Node2D),全部程序化 _draw 占位视觉(纹理槽位已留未装载)。
## CellUnit——BoardView SubViewport 世界内逐格渲染件(Node2D),被动绘制壳:_draw 六步时序器
## +视野迷雾域+微光子件生命周期+格件几何;基座层与标识叠加两绘制域件 _init 自建持有
## (cell_base_layer/cell_mark_overlay——单一变化原因=迷雾规格与格件节点生命周期)。
## 无 class_name(类名缓存面纪律)——使用方经 preload 引用。
## 内部状态件(u3 子域拆件——契约 9 职责语句「单格绘制输入集」):本件为被动绘制壳,
## 状态件 _CellPaintState 承载全部绘制入参(单一变化原因=格面呈现),setter 触发宿主重绘语义
## 经 _owner 回引保持逐字;探针直读冻结成员(glyph/fog_state/plate/glyph_color)与 _glow_layer 住宿主。
## 探针直读冻结成员(glyph/fog_state/plate/glyph_color)与 _glow_layer 住宿主;
## hl_color/hl_visible/dot_visible/mutation_mark 探针读 u.face.*。
extends Node2D
const _GLOW_LAYER := preload("res://presentation/views/cell_type_glow_layer.gd")
const _BASE_LAYER := preload("res://presentation/views/cell_base_layer.gd")
const _MARK_OVERLAY := preload("res://presentation/views/cell_mark_overlay.gd")
# ── D 投影档绘制常量(研究件实测取色——立面高光/双段暗带/地面投影;黑罩 alpha 叠加实测等效)──
const FACADE_HIGHLIGHT_PX := 2.0 # 口沿高光线厚(像素)
const FACADE_NEAR_FRAC := 0.16 # 立面近段高(格边比——研究件 10/64)
const FACADE_FAR_FRAC := 0.25 # 立面远段高(格边比——研究件 16/64)
const FACADE_NEAR_COL := Color(0, 0, 0, 0.60) # 立面近段黑罩(墙顶 132→52 实测 α≈0.61)
const FACADE_FAR_COL := Color(0, 0, 0, 0.82) # 立面远段黑罩(墙顶 132→24 实测 α≈0.82)
const PROJECTION_NEAR_COL := Color(0, 0, 0, 0.28) # 投影贴墙段(地面 66→46 实测 α≈0.28)
const PROJECTION_MID_COL := Color(0, 0, 0, 0.16) # 投影中段(渐隐)
const PROJECTION_EDGE_COL := Color(0, 0, 0, 0.06) # 投影远段(渐隐收边)
# ── E 格高墙面(圣经 v3.5 冻结①——地面格南邻墙→南部 30% 承接墙面:墙顶纹理原亮度
# 直贴零暗档零阴影;墙脚阴线只画地面/墙面交界;(b)-(c) 交界
# 无边框由 BoardView 侧跳过墙格北向描边承载)──
const E_FACADE_FRAC := 0.30 # 墙面承接高(格边比 30%)
## 单格绘制输入集(被动绘制壳的全部绘制入参——BoardView 直写面;setter 触发宿主重绘语义逐字)
class _CellPaintState extends RefCounted:
var _owner: Node2D = null
var cell := Vector2i.ZERO # 板坐标(诊断观测面)
var edge := 46.0: # 格边长(BoardView.set_cell_area 单点驱动)
set(v):
edge = v
if _owner._glow_layer != null and is_instance_valid(_owner._glow_layer):
_owner._glow_layer.setup(edge, glow_group)
_owner.queue_redraw()
var bg_sb: StyleBoxFlat = null # 格底 StyleBox(BoardView 每次 paint 重建;呼吸微光就地改边框)
# ── 接触带微光绘制面(可挖域微光画在英雄格周界空间侧——art-bible §3.2 v2.8;
# 仅英雄格单元承载,BoardView.refresh 按可挖域写边/角码、_process 逐拍更新呼吸色)──
var contact_bands: Array[int] = [] # 正交接触边码 0=N/1=S/2=W/3=E(该侧整条边)
var contact_corners: Array[int] = [] # 斜向共享角码 0=NW/1=NE/2=SW/3=SE(角点+两侧各半条边)
var contact_col := Color(0, 0, 0, 0) # 当前呼吸色(alpha 0 = 不画)
var outline_sides: Array[int] = [] # 墙格地面朝向描边码 0=N/1=S/2=W/3=E(程序侧结构件)
# ── 高墙感 D 投影档(研究件四变体定 D=C+地面投影,ITB 同款高度线索)──
# 南立面 = 墙格南缘内侧「口沿高光线 + 双段暗带」(邻地面格才画,墙群内聚);地面投影 = 北邻墙格
# 在本已开格北缘投渐隐暗带。参数按研究件 64px 拼版实测比例换算(立面双段 0.16/0.25 格边、投影带
# 0.22 格边;坐标推导留档 commit)。素材缺位程序化回退同样服役(结构归程序——art-bible §8.5)。
var facade_south := false: # 南立面开关(BoardView paint 按「南邻已开格」写入)
set(v):
facade_south = v
_owner.queue_redraw()
var proj_band := 0.0: # 地面投影带宽(像素,0 = 不画——BoardView 按北邻墙判定写入)
set(v):
proj_band = v
_owner.queue_redraw()
var badge_glyph := "": # 奖励徽章文字(右下角金——封印之下、变异纹之上、不进数字区)
set(v):
badge_glyph = v
if _owner._glow_layer != null and is_instance_valid(_owner._glow_layer):
_owner._glow_layer.set_badge_mask(v != "")
_owner.queue_redraw()
var dot_visible := false: # BFS 路径预览点迹(中心 6×6 小人蓝)
set(v):
dot_visible = v
_owner.queue_redraw()
var hl_visible := false: # 焦点高亮(悬停态边框覆盖层)
set(v):
hl_visible = v
_owner.queue_redraw()
var hl_color := UiKit.GOLD: # 高亮色(金目标/封印朱/禁入灰禁红)
set(v):
hl_color = v
_owner.queue_redraw()
var font: Font = null # 字体槽位(null = ThemeDB.fallback_font——BoardView.set_world_font 注入)
var bevel := 0.0: # 顶面倒角强度(0=关;档位 0/0.35/0.6/1.0)
set(v):
bevel = v
_owner.queue_redraw()
# ── 素材槽位(现全 null = 程序化占位服役;art-bible §8.2 格面 32×32 暂定档)──
var tex_base: Texture2D = null # ① 砖面底
var tex_mutation: Texture2D = null # ② 变异纹带
var tex_glyph: Texture2D = null # ③ 数字
var tex_badge: Texture2D = null # ③' 徽章
var tex_seal: Texture2D = null: # ④ 封印(槽位可运行时换装〔封印〕——同族 facade/treasure_mark
set(v): # 均 setter 触发重绘(普通 var 赋值不重绘=封印插上不显示))
if tex_seal != v:
tex_seal = v
_owner.queue_redraw()
var tex_facade: Texture2D = null # ⑥ E 格墙面材质(v3.5——南邻墙顶 center 瓦片同源,BoardView 注入)
var facade_strip := false: # E 格开关(BoardView paint 按「已开格且南邻未开墙」写入——瓦片态专用)
set(v):
facade_strip = v
_owner.queue_redraw()
# 变异角标(程序化,tex_mutation 纹带槽暂留):
# "flat_damage" = 未开格伤害型变异,左下角危险红三角(开格有代价的策略纹理标记)
# 叠加层标记(秘宝整图叠加层的格面呈现;派生信息层零回写):
# "mine_reveal" = 真视格(危险红内缩框——「这格是雷」的确定决策标记,封印照常计费);
# "reward_preview" 格不复用本字段——预读徽章走 badge_glyph 注入(board_view refresh 注入 REWARD_BADGE 字形)
var treasure_mark := "":
set(v):
if treasure_mark != v:
treasure_mark = v
_owner.queue_redraw()
var mutation_mark := "":
set(v):
if mutation_mark != v:
mutation_mark = v
_owner.queue_redraw()
# ── 未开格类型微光(英雄 8 向相邻未开格的组粒度类型提示)
# 作用集 = 可挖域 ∩ 身份可查格,由 BoardView 侧派生下发;本件只承接绘制与子件生命周期。
# 静态:不呼吸、不入动效预算;相位门控(结算/道具窗口态隐藏)在 BoardView 侧。
var glow_group := 0:
set(v):
if glow_group != v:
glow_group = v
_owner._sync_glow_layer()
var removed_dim := false: # 中立移除退色层(BoardView paint 按排雷玩法 is_removed 写入)
set(v):
removed_dim = v
_owner.queue_redraw()
var st := _CellPaintState.new()
var edge := 46.0: # 格边长(BoardView.set_cell_area 单点驱动)
set(v):
edge = v
if _glow_layer != null and is_instance_valid(_glow_layer):
_glow_layer.setup(edge, glow_group)
queue_redraw()
var cell := Vector2i.ZERO # 板坐标(诊断观测面)
# ── 未开格类型微光(英雄 8 向相邻未开格的组粒度类型提示)
# 作用集 = 可挖域 ∩ 身份可查格,由 BoardView 侧派生下发;本件只承接绘制与子件生命周期。
# 静态:不呼吸、不入动效预算;相位门控(结算/道具窗口态隐藏)在 BoardView 侧。
var glow_group := 0:
set(v):
if glow_group != v:
glow_group = v
_sync_glow_layer()
var glyph := "": # 文字位——数字/雷/封印/人/?(现行视觉封印与数字互斥,同位承载;探针直读冻结)
set(v):
glyph = v
@@ -141,267 +44,51 @@ var fog_state := 0: # 探针直读冻结
queue_redraw()
var _glow_layer: Node2D = null
var base = null # 格面基座层绘制域件(_init 自建)
var face = null # 格面标识与叠加层绘制域件(_init 自建)
func _init() -> void:
st._owner = self
base = _BASE_LAYER.new(self)
face = _MARK_OVERLAY.new(self)
func _draw() -> void:
var rect := Rect2(Vector2.ZERO, Vector2(st.edge, st.edge))
_draw_base_layer(rect)
_draw_mutation_marks()
_draw_glyph_layer(rect)
_draw_overlay_layers(rect)
## ① 砖面底层域(占位 = StyleBoxFlat:格底色 + 暗化描边 + 3px 圆角;瓦片态描边覆盖层;
## 呼吸微光/主角蓝框由 BoardView 改 bg_sb 后触发重绘。素材缺位回退走 elif 程序化原样,零观感变化)
func _draw_base_layer(rect: Rect2) -> void:
if st.tex_base != null:
draw_texture_rect(st.tex_base, rect, false)
# 轮廓加强(轮廓线不明显):墙格地面朝向边补 2px 深墨线(§8.5 结构归程序——
# 素材 1px 墨线在 45/36px 非整数缩放下视觉衰减,程序线保底;墙-墙内界不画,保墙群内聚;
# 坐标推导:各边内贴 2px 矩形,相邻边矩形全长相交=周界闭合)
if not st.outline_sides.is_empty():
_draw_outline_sides()
_draw_south_facade()
_draw_north_projection()
_draw_e_facade()
# 瓦片态描边覆盖层:tex_base 在位时 BoardView 传入的 bg_sb
# 已降级为「透明底 + 描边」载体——呼吸微光/主角蓝框画在瓦片之上(art-bible §1.2 叠层序
# 不变:底 < 纹带 < 数字 < 封印 < 焦点高亮);素材缺位回退走 elif 程序化原样,零观感变化
if st.bg_sb != null:
draw_style_box(st.bg_sb, rect)
_draw_contact_or_fallback(rect)
## 接触带微光或平色回退域(英雄格周界空间侧——正交邻墙=整条边;斜向=共享角点+两侧各半条边;光不进墙格):
## 带宽 4px 贴格边内侧(坐标推导:N=[0,0,edge,4] S=[0,edge-4,edge,4] W=[0,0,4,edge] E=[edge-4,0,4,edge];
## 半条边=沿邻边自角点起 edge/2;角点圆 r=6 贴角点圆心,近白芯 r=2.5 提亮)
func _draw_contact_or_fallback(rect: Rect2) -> void:
if st.contact_col.a > 0.0:
_draw_north_projection() # 平色回退+微光并发的英雄格补画(瓦片路径已画——同像素重复无观感差)
_draw_e_facade() # E 格墙面补画(同上——英雄格兼 E 格时瓦片路径已画,重复无害)
_draw_contact_bands()
elif st.bg_sb != null:
draw_style_box(st.bg_sb, rect)
_draw_south_facade() # 平色回退路径——D 投影档两件同样服役(结构归程序)
_draw_north_projection()
_draw_bevel()
## ②③④ 中层域:变异纹带与标记/数字色牌与字形/徽章/封印
func _draw_glyph_layer(rect: Rect2) -> void:
# ② 变异纹带(无程序化纹带——槽位先行,贴图落位即用)
if st.tex_mutation != null:
draw_texture_rect(st.tex_mutation, rect, false)
# ③ 数字/徽章(数字 ≥ 格高 50% 墨族对格底 ≥4.5:1;徽章右下角金,遮蔽态 = "?")
if glyph != "":
_draw_digit_plate()
if st.tex_glyph != null:
draw_texture_rect(st.tex_glyph, rect, false)
else:
_draw_glyph(glyph, glyph_color, maxi(18, int(ceil(st.edge * 0.5)))) # 字号下限 18(§7.4 冻结公式)
if st.badge_glyph != "":
if st.tex_badge != null:
draw_texture_rect(st.tex_badge, rect, false)
else:
_draw_badge(st.badge_glyph)
# ④ 封印(现行视觉封印文字在 ③ 文字位承载——独立槽位留给封印图)
if st.tex_seal != null:
draw_texture_rect(st.tex_seal, rect, false)
## 数字色牌后备域(圣经 v3.5 冻结④:数字亮墨直画已开格底须垫牌;
## 牌=后备件——雷/「?」维持牌载保读性(牌上深墨族 12:1);
## 牌尺寸 0.60/0.62 格边居中;封印=未开格不垫牌)
func _draw_digit_plate() -> void:
var pw := st.edge * 0.60
var ph := st.edge * 0.62
var pr := Rect2((st.edge - pw) * 0.5, (st.edge - ph) * 0.5, pw, ph)
draw_rect(pr, UiKit.DIGIT_PLATE, true)
draw_rect(pr, UiKit.DIGIT_PLATE_EDGE, false, 1.0)
## ⑤⑥⑦ 顶层域:路径点迹 + 焦点高亮 + 视野迷雾 + 中立移除退色
func _draw_overlay_layers(rect: Rect2) -> void:
# ⑤ 路径点迹 + 焦点高亮(悬停态覆盖层,不参与命中)
if st.dot_visible:
var ds := 6.0
draw_rect(Rect2(Vector2((st.edge - ds) * 0.5, (st.edge - ds) * 0.5), Vector2(ds, ds)), UiKit.HERO_BLUE, true)
if st.hl_visible:
# 悬停高亮减淡(金框太明显——绘制时 55% alpha,hl_color 属性与探针断言面不动)
draw_rect(Rect2(1, 1, st.edge - 2.0, st.edge - 2.0), Color(st.hl_color, 0.55), false, 2.0) # 2px 内描边
_draw_fog_layer()
# ⑦ 中立移除退色层(退色降档:暗幕压顶全层覆盖,暗淡记忆态同族呈现语义;
# 顶层于迷雾之上——移除格不再参与迷雾语义。本实现移除集=未揭示危险格(无数字无封印印))
if st.removed_dim:
draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(0.12, 0.10, 0.09, 0.62), true)
var rect := Rect2(Vector2.ZERO, Vector2(edge, edge))
base.draw_base(self, rect) # 基座层整树(描边/立面/投影/接触带/倒角与其补画序)
face.draw_marks(self, rect) # 变异标记
face.draw_glyph_layer(self, rect, glyph, glyph_color, plate) # 字形/色牌/徽章/封印
face.draw_overlay_under(self, rect) # 点迹+高亮(雾层由宿主夹在其后)
_draw_fog_layer(rect) # 宿主迷雾域(原位——雾夹在 hl 与 removed 之间)
face.draw_removed_dim(self, rect) # 退色层
## ⑥ 视野迷雾层(最顶遮罩,§6.5 落文):
## 全黑=不透明纯黑盖全部叠层(含封印印/徽章/高亮——封印印记随格隐没,§6.5 显式推论);
## 记忆态=记忆板素色压平(纹理免疫——色牌/封印印/徽章随板隐没)+数字记忆墨重画(数字可读硬指标 ≥4.5:1)
func _draw_fog_layer() -> void:
func _draw_fog_layer(rect: Rect2) -> void:
if fog_state == 1:
draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(0, 0, 0, 1.0), true)
draw_rect(rect, Color(0, 0, 0, 1.0), true)
elif fog_state == 2:
_draw_fog_memory()
## 墙格轮廓域:地面朝向边补 2px 深墨线(各边内贴 2px 矩形,相邻边矩形全长相交=周界闭合)
func _draw_outline_sides() -> void:
var ow := 2.0
var oc := UiKit.FOG_OUTLINE
for b in st.outline_sides:
var or_ := Rect2()
match int(b):
0: or_ = Rect2(0, 0, st.edge, ow)
1: or_ = Rect2(0, st.edge - ow, st.edge, ow)
2: or_ = Rect2(0, 0, ow, st.edge)
3: or_ = Rect2(st.edge - ow, 0, ow, st.edge)
draw_rect(or_, oc, true)
## 接触带微光域:正交整条边 + 斜向共享角点与两侧半条边
func _draw_contact_bands() -> void:
var band := 4.0
var half := st.edge * 0.5
for b in st.contact_bands:
var r := Rect2()
match int(b):
0: r = Rect2(0, 0, st.edge, band)
1: r = Rect2(0, st.edge - band, st.edge, band)
2: r = Rect2(0, 0, band, st.edge)
3: r = Rect2(st.edge - band, 0, band, st.edge)
draw_rect(r, st.contact_col, true)
for cc in st.contact_corners:
_draw_contact_corner(int(cc), band, half)
## 斜向角点域:角点圆 r=6 贴角点圆心 + 近白芯 r=2.5 提亮 + 半条边(角点沿两条邻边各延伸 edge/2——
## 坐标推导:横带沿 N/S 自角点向板心侧 half;NW/SE 半带 x=y=角点坐标,NE/SE 半带自 edge-half 收进——
## 正交整边时重叠同色无害)
func _draw_contact_corner(cc: int, band: float, half: float) -> void:
var pt := Vector2.ZERO
match cc:
0: pt = Vector2(0, 0)
1: pt = Vector2(st.edge, 0)
2: pt = Vector2(0, st.edge)
3: pt = Vector2(st.edge, st.edge)
draw_circle(pt, 6.0, st.contact_col)
draw_circle(pt, 2.5, Color(1, 1, 1, minf(1.0, st.contact_col.a * 0.95)))
if cc == 0 or cc == 1:
# N 侧横带:NW 自 x=0、NE 自 x=edge-half
draw_rect(Rect2(pt.x if cc == 0 else pt.x - half, 0.0, half, band), st.contact_col, true)
if cc == 2 or cc == 3:
# S 侧横带:SW 自 x=0、SE 自 x=edge-half
draw_rect(Rect2(pt.x if cc == 2 else pt.x - half, st.edge - band, half, band), st.contact_col, true)
if cc == 0 or cc == 2:
# W 侧竖带:NW 自 y=0、SW 自 y=edge-half
draw_rect(Rect2(0.0, pt.y if cc == 0 else pt.y - half, band, half), st.contact_col, true)
if cc == 1 or cc == 3:
# E 侧竖带:NE 自 y=0、SE 自 y=edge-half
draw_rect(Rect2(st.edge - band, pt.y if cc == 1 else pt.y - half, band, half), st.contact_col, true)
## 顶面倒角域(光源左上——上/左内沿受光 2px、下/右内沿背光 2px;内缩 2px 让
## 呼吸微光/主角蓝边框保持最外沿可见;强度随档位)
func _draw_bevel() -> void:
if st.bevel <= 0.0:
return
var base: Color = st.bg_sb.bg_color
var lit: Color = base.lightened(0.22 * st.bevel)
var dark: Color = base.darkened(0.22 * st.bevel)
var t := 2.0
draw_rect(Rect2(t, t, st.edge - t * 2.0, t), lit, true)
draw_rect(Rect2(t, t, t, st.edge - t * 2.0), lit, true)
draw_rect(Rect2(t, st.edge - t * 2.0, st.edge - t * 2.0, t), dark, true)
draw_rect(Rect2(st.edge - t * 2.0, t, t, st.edge - t * 2.0), dark, true)
## 变异标记域:真视格=危险红内缩框/未开格伤害型变异=左下角危险红三角
func _draw_mutation_marks() -> void:
if st.treasure_mark == "mine_reveal":
var inset := st.edge * 0.14
draw_rect(Rect2(inset, inset, st.edge - inset * 2.0, st.edge - inset * 2.0),
Color(UiKit.RED_MINE, 0.9), false, 2.0)
if st.mutation_mark == "flat_damage":
var mw := st.edge * 0.28
var mpts := PackedVector2Array([Vector2(0, st.edge - mw), Vector2(0, st.edge), Vector2(mw, st.edge)])
draw_colored_polygon(mpts, Color(UiKit.RED_MINE, 0.85))
_draw_fog_memory(rect)
## 记忆态换装域(demo 口径「地板纹理 + 灰罩 α.66」:
## 地板纹理原样直画 + FOG_MEMORY_PLATE α.66 灰罩压顶;素材零改动、格边长零改动。
## 记忆墨 FOG_MEMORY_INK 对合成底归一后(真实渲染面)三关 5.71–5.93:1 ≥4.5:1(§7.2 硬指标;
## 测点行值与素材随动,见 ui_contrast_probe 迷雾段))
func _draw_fog_memory() -> void:
if st.tex_base != null:
draw_texture_rect(st.tex_base, Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), false)
draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(UiKit.FOG_MEMORY_PLATE, 0.66), true)
func _draw_fog_memory(rect: Rect2) -> void:
if base.tex_base != null:
draw_texture_rect(base.tex_base, rect, false)
draw_rect(rect, Color(UiKit.FOG_MEMORY_PLATE, 0.66), true)
if glyph != "":
_draw_glyph(glyph, UiKit.FOG_MEMORY_INK, maxi(18, int(ceil(st.edge * 0.5)))) # 字号下限 18(§7.4 冻结公式同源)
# 南立面(D 档件一——墙格南缘内侧,自底边向上:双段暗带在下、口沿高光线贴其上沿):
# 坐标推导(自格底边起算):远段=[edge−far, edge],近段=[edge−far−near, edge−far],
# 高光线=[edge−far−near−hl, edge−far−near]——与研究件「墙顶面→高光→暗段→更暗段→格界」下行序一致
func _draw_south_facade() -> void:
if not st.facade_south:
return
var far_h := st.edge * FACADE_FAR_FRAC
var near_h := st.edge * FACADE_NEAR_FRAC
var y_far := st.edge - far_h
var y_near := y_far - near_h
var y_hl := y_near - FACADE_HIGHLIGHT_PX
draw_rect(Rect2(0.0, y_hl, st.edge, FACADE_HIGHLIGHT_PX), UiKit.STAND_STONE_RIM, true) # 口沿高光(UiKit.STAND_STONE_RIM 并档——同值单点)
draw_rect(Rect2(0.0, y_near, st.edge, near_h), FACADE_NEAR_COL, true)
draw_rect(Rect2(0.0, y_far, st.edge, far_h), FACADE_FAR_COL, true)
# 地面投影(D 档件二——北邻墙格在本已开格北缘投渐隐暗带,自北缘向南三段衰减):
# 坐标推导:三等分 proj_band,α 0.28/0.16/0.06(研究件实测贴墙 α≈0.28、带末渐融入地面)
func _draw_north_projection() -> void:
if st.proj_band <= 0.0:
return
var seg := st.proj_band / 3.0
draw_rect(Rect2(0.0, 0.0, st.edge, seg), PROJECTION_NEAR_COL, true)
draw_rect(Rect2(0.0, seg, st.edge, seg), PROJECTION_MID_COL, true)
draw_rect(Rect2(0.0, seg * 2.0, st.edge, seg), PROJECTION_EDGE_COL, true)
# E 格高墙面(圣经 v3.5 冻结①——方向稿 draw_facade 移植):本格已开且南邻为墙 →
# 南部 30% 承接墙面。材质 = 墙顶 center 瓦片顶部 30% 区带**原亮度**直贴(零暗档零阴影);
# 墙脚阴线只画地面/墙面交界(strip 顶缘 1px);墙面底缘(格界处)不画任何线——直接接南侧墙顶
# (白边导致两截)。仅瓦片态服役(tex_facade 缺位 = 平色回退原语义不动)。
func _draw_e_facade() -> void:
if not st.facade_strip or st.tex_facade == null:
return
var strip_h := st.edge * E_FACADE_FRAC
var y0 := st.edge - strip_h
var ts := st.tex_facade.get_size()
draw_texture_rect_region(st.tex_facade, Rect2(0.0, y0, st.edge, strip_h),
Rect2(0.0, 0.0, ts.x, ts.y * E_FACADE_FRAC))
draw_rect(Rect2(0.0, y0, st.edge, 1.0), UiKit.E_FACADE_FOOT_COL, true)
# 文字位绘制:水平居中 + 垂直居中(基线 = (格高 − 字高)/2 + 上伸部)
func _draw_glyph(g: String, col: Color, fs: int) -> void:
var f := st.font if st.font != null else ThemeDB.fallback_font
if f == null:
return
var asc := f.get_ascent(fs)
var desc := f.get_descent(fs)
draw_string(f, Vector2(0.0, (st.edge - (asc + desc)) * 0.5 + asc), g,
HORIZONTAL_ALIGNMENT_CENTER, st.edge, fs, col)
face.draw_centered_text(self, rect, glyph, UiKit.FOG_MEMORY_INK, maxi(18, int(ceil(edge * 0.5)))) # 字号下限 18(§7.4 冻结公式同源)
# 微光子件生命周期:有组才建、组变即换、无组即撤——作用集 ≤8 格常驻,
# 其余格零节点零绘制成本。与父件 _draw() 分离的三条理由见本件头注(命令序列/重绘预算/材质挂点)。
func _sync_glow_layer() -> void:
if st.glow_group == 0:
if glow_group == 0:
if _glow_layer != null and is_instance_valid(_glow_layer):
_glow_layer.queue_free()
_glow_layer = null
@@ -409,15 +96,5 @@ func _sync_glow_layer() -> void:
if _glow_layer == null or not is_instance_valid(_glow_layer):
_glow_layer = _GLOW_LAYER.new()
add_child(_glow_layer)
_glow_layer.setup(st.edge, st.glow_group)
_glow_layer.set_badge_mask(st.badge_glyph != "")
# 徽章绘制:右下角金(文字贴角内缩 2–3px)
func _draw_badge(g: String) -> void:
var f := st.font if st.font != null else ThemeDB.fallback_font
if f == null:
return
var fs := maxi(10, int(st.edge * 0.28))
var sz := f.get_string_size(g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs)
draw_string(f, Vector2(st.edge - sz.x - 2.0, st.edge - 3.0), g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, UiKit.GOLD)
_glow_layer.setup(edge, glow_group)
_glow_layer.set_badge_mask(face.badge_glyph != "")
+130
View File
@@ -0,0 +1,130 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 悬停预承诺呈现域件(BoardView 拆件——单一变化原因=悬停反馈语义):
## 三语义光标、目标格边框、BFS 路径预览、chord 双击首遇锚的唯一承载(自 board_view 收编 11 函数族)。
## 无 class_name(类名缓存面纪律)——经宿主 board_view preload 引用;构造=引用束注入
## (cells/minesweeper/diggable 共享引用+格命中换算/壳级光标写入/hover_forbidden 与 chord 发射注入 Callable——
## 输入经宿主三入站口委托,禁反向持宿主引用);_r0 出生展开区成员留宿主。
extends RefCounted
const COL_HL_TARGET := UiKit.GOLD # 悬停目标格边框(金)
const COL_SEAL := UiKit.SEAL_BLACK # 封印黑(与宿主 :658 双侧服役——各自 alias UiKit 单源)
const CURSOR_DIG := Input.CURSOR_CROSS # 挖——镐形自绘纹理载体
const CURSOR_MOVE_SEM := Input.CURSOR_ARROW # 移动——箭头(命名避开 Control.CURSOR_MOVE 原生成员遮蔽)
const CHORD_WINDOW_MS := 350
var _cells := {} # 共享引用(宿主容器同对象——rebuild 只 clear 不重绑)
var _minesweeper = null # 排雷玩法 只读查询引用(宿主 rebuild 重绑时同拍回写本成员)
var _diggable := {} # 共享引用(可挖域——呼吸微光作用集同源)
var _cb_cell_at: Callable # 格命中换算(宿主 _cell_at——整图公式单点留宿主)
var _cb_cursor: Callable # 壳级光标写入(宿主 mouse_default_cursor_shape 单点)
var _cb_forbidden: Callable # hover_forbidden 信号发射(首遇 toast 去重归组合根)
var _cb_chord: Callable # chord_attempted 信号发射(chord 肌肉记忆提示锚)
var _hover := Vector2i(-9, -9)
var _last_click := {"cell": Vector2i(-9, -9), "ms": -100000}
func _init(bundle: Dictionary) -> void:
_cells = bundle["cells"]
_minesweeper = bundle["minesweeper"]
_diggable = bundle["diggable"]
_cb_cell_at = bundle["cell_at"]
_cb_cursor = bundle["cursor"]
_cb_forbidden = bundle["forbidden"]
_cb_chord = bundle["chord"]
func _hover_at(pos: Vector2) -> void:
var c: Vector2i = _cb_cell_at.call(pos)
if c.x < 0:
if _hover.x >= 0:
_on_cell_unhover(_hover)
elif c != _hover:
if _hover.x >= 0:
_on_cell_unhover(_hover)
_on_cell_hover(c)
func _on_shell_mouse_exited() -> void:
if _hover.x >= 0:
_on_cell_unhover(_hover)
# chord 肌肉记忆首遇锚:同格双左击快开尝试(350ms 窗)——每 run 一次提示由组合根去重
func _detect_chord(c: Vector2i) -> void:
var now := Time.get_ticks_msec()
if _last_click.cell == c and now - int(_last_click.ms) <= CHORD_WINDOW_MS:
_cb_chord.call(c)
_last_click.ms = -100000
return
_last_click = {"cell": c, "ms": now}
func _on_cell_hover(c: Vector2i) -> void:
_hover = c
_apply_hover_visuals(c)
func _on_cell_unhover(c: Vector2i) -> void:
if _hover == c:
_hover = Vector2i(-9, -9)
_clear_hover_visuals(c)
func _refresh_hover_after_paint() -> void:
# 状态随 refresh 变化后光标/边框语义需重算(悬停格四格态可能已变)
if _hover.x >= 0 and _cells.has(_hover):
_apply_hover_visuals(_hover)
func _apply_hover_visuals(c: Vector2i) -> void:
if _minesweeper == null or not _cells.has(c):
return
_clear_hover_visuals(c)
var u: Node2D = _cells[c]
var revealed: bool = _minesweeper.is_revealed(c)
var seal: bool = _minesweeper.has_seal(c)
var adjacent: bool = _diggable.has(c)
if revealed:
u.face.hl_visible = true
u.face.hl_color = COL_HL_TARGET # 移动目标格边框(金)
_set_cursor_shape(c, CURSOR_MOVE_SEM)
_show_path_preview(c)
elif adjacent:
u.face.hl_visible = true
u.face.hl_color = COL_HL_TARGET # 挖掘目标格边框(相邻封印格同此——左键直接挖)
_set_cursor_shape(c, CURSOR_DIG)
elif seal:
u.face.hl_visible = true
u.face.hl_color = COL_SEAL # 已承诺封印标记(封印黑;无交互可供)
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
else:
u.face.hl_visible = true
u.face.hl_color = UiKit.RED_FORBID # 不可及域灰禁框(禁止光标+首遇 toast 双通道)
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
_cb_forbidden.call(c)
func _clear_hover_visuals(c: Vector2i) -> void:
if not _cells.has(c):
return
_cells[c].face.hl_visible = false
_hide_path_preview()
func _set_cursor_shape(c: Vector2i, shape: int) -> void:
_cb_cursor.call(shape) # 壳级光标(悬停格语义决定——单格悬停恒一)
func _show_path_preview(c: Vector2i) -> void:
# 合法性指示:悬停已开格 → BFS 路径蓝细线点迹(compute_move_path 只读面)
if _minesweeper == null:
return
var path: Array[Variant] = _minesweeper.compute_move_path(c)
for pc in path:
if _cells.has(pc):
_cells[pc].face.dot_visible = true
func _hide_path_preview() -> void:
for c in _cells:
_cells[c].face.dot_visible = false
@@ -0,0 +1 @@
uid://dojreaughx7tf
+67 -149
View File
@@ -12,6 +12,7 @@ const _CELL_UNIT := preload("res://presentation/views/board_cell_unit.gd")
const _BOARD_WORLD := preload("res://presentation/views/board_world.gd")
const _TILE_PALETTE := preload("res://presentation/views/tile_palette.gd")
const _HERO_SPRITE := preload("res://presentation/views/hero_sprite.gd")
const _BOARD_HOVER := preload("res://presentation/views/board_hover.gd")
const COL_BRICK := UiKit.BRICK # 未开——掩尘完整地砖(色板原语 #C9AF83)
const COL_FLOOR := UiKit.FLOOR # 已开——裸土 #B39A78(碎裂裸露地面)
@@ -20,17 +21,13 @@ const COL_HERO := UiKit.HERO_BLUE # 小人边框——小人蓝
const COL_SEAL := UiKit.SEAL_BLACK # 封印黑 #000000
const COL_GOLD := UiKit.GOLD # 奖励金 #F0A828
const COL_CONTACT := UiKit.CONTACT_GLOW # 接触带微光基色(蓝族浅一档·近白蓝——§3.2 v2.8)
const COL_HL_TARGET := UiKit.GOLD # 悬停目标格边框(金)
const REWARD_BADGE := { # 已开格格面资源身份 icon(cell_type 九型键)
"attack": "攻", "defense": "守", "heal": "医", "blast": "爆", "weaken": "弱", "pierce": "穿", "magic": "魔",
"action": "行", # 行 = 行动点型(ap_up/ap_drain 两行)
"gp": "金", # 金 = GP 型(gp_gain/gp_loss 两行;金色系 ~40° 相位,禁入带紫赤 300–345° 外,取色 UiKit.GOLD 徽标色单点)
}
const BADGE_PLACEHOLDER := "?" # 未知图标键占位纹样(HUD框架:禁静默空图标)
const CURSOR_DIG := Input.CURSOR_CROSS # 挖——镐形自绘纹理载体
const CURSOR_SEAL := Input.CURSOR_POINTING_HAND # 封印——封印形自绘纹理载体
const CURSOR_MOVE_SEM := Input.CURSOR_ARROW # 移动——箭头(命名避开 Control.CURSOR_MOVE 原生成员遮蔽)
const CHORD_WINDOW_MS := 350
const SEP := 2 # 格间距(格尺寸公式与命中换算共用)
# 棋盘立体参数
@@ -67,8 +64,6 @@ var _contact_unit: Node2D = null # 接触带承载单元(=英雄格 CellUnit
var _install_tw: Tween # 入场淡入 tween(rebuild 重入先杀)
var _ripple_tws: Array[Tween] = [] # R0 涟漪逐格 tween(rebuild 重入全杀)
var _dig_glow := false # 挖掘期才允许唯一在动元素(art-bible §2.1)
var _hover := Vector2i(-9, -9)
var _last_click := {"cell": Vector2i(-9, -9), "ms": -100000}
var _r0 := {} # 出生展开区 R₀(地图战斗资源 mapping 资格域 = S∖R₀——徽章查询跳过,防 E19 诊断噪音)
var _desat_mat: ShaderMaterial
var _desat_tween: Tween
@@ -77,6 +72,7 @@ var _cell := 46.0 # 当前格边长(set_cell_area 按容器尺寸重
# ── 拆出件(状态单点在件内)──
var _palette = _TILE_PALETTE.new() # 瓦片件:装载缓存+查表+明度/墨族静态算法
var _hero_sprite = _HERO_SPRITE.new() # 英雄精灵件:_init 挂为本件子级(tween 需在树上)
var _hover_comp: _BOARD_HOVER # 悬停预承诺呈现域件:三语义光标/目标格边框/路径预览/chord 首遇锚(_init 建为子件——HeroSprite 挂载先例)
# ── 件成员同名转发(探针直读面零改动:读 _palette.seal_tex、读写 _hero_sprite.tex_idle;其余为随迁状态单点再导出)──
# ---- 视野迷雾(纯呈现层;现行事实源=HUD框架「视野迷雾」节 v0.13,渲染规格=art-bible §6.5 v3.5)----
@@ -120,6 +116,25 @@ func _init() -> void:
_hero_sprite.tex_idle = UiKit.load_tex(UiKit.TEX_HERO_IDLE)
_hero_sprite.tex_walk = UiKit.load_tex(UiKit.TEX_HERO_WALK)
_palette.load_board_tiles() # 高墙/地面瓦片整批守卫装载(缺位静默回退——英雄同款纪律,禁刷屏)
_hover_comp = _BOARD_HOVER.new({
"cells": _cells, "minesweeper": _minesweeper, "diggable": _diggable,
"cell_at": _cell_at, "cursor": _set_shell_cursor,
"forbidden": _emit_hover_forbidden, "chord": _emit_chord_attempted,
})
# ---- 悬停域注入面(件回调宿主的壳级单点——Callable 目标)----
func _set_shell_cursor(shape: int) -> void:
mouse_default_cursor_shape = shape # 壳级光标(悬停格语义决定——单格悬停恒一)
func _emit_hover_forbidden(c: Vector2i) -> void:
hover_forbidden.emit(c) ## 非相邻未开格悬停(首遇 toast 归组合根去重)
func _emit_chord_attempted(c: Vector2i) -> void:
chord_attempted.emit(c) ## 双击快开尝试(chord 肌肉记忆提示锚)
func board() -> BoardData:
@@ -128,7 +143,7 @@ func board() -> BoardData:
func _ready() -> void:
_apply_custom_cursors()
mouse_exited.connect(_on_shell_mouse_exited)
mouse_exited.connect(_hover_comp._on_shell_mouse_exited)
# 结算期/道具窗口态:D 区去饱和 + 金橙偏移锁输入(明度不变——art-bible §2);
@@ -182,7 +197,7 @@ func set_treasure_overlays(reveal_cells: Array, preview_cells: Array) -> void:
func set_world_font(f: Font) -> void:
_world.board_font = f
for c in _cells:
_cells[c].st.font = f
_cells[c].face.font = f
_cells[c].queue_redraw()
@@ -201,7 +216,7 @@ func set_cell_area(avail: Vector2) -> void:
var stride := _cell + float(SEP)
for c in _cells:
var u: Node2D = _cells[c]
u.st.edge = _cell
u.edge = _cell
u.position = Vector2(c) * stride
if _world.hero != null and _hero_sprite.last.x >= 0:
_place_hero(_hero_sprite.last, _hero_sprite.last, true) # 格边长变更(窗口缩放)——英雄即时随格重置(杀位移 tween 防飞陈旧落点)
@@ -224,6 +239,7 @@ func _refresh_slab() -> void:
func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void:
_board = board
_minesweeper = minesweeper
_hover_comp._minesweeper = _minesweeper # 件内只读引用随宿主重绑同拍回写(每战新排雷玩法对象)
_palette.bind_board(_board, _minesweeper) # 查表族棋盘态随板同步(换板后首拍 refresh 前置)
for child in _world.get_children():
child.queue_free()
@@ -234,7 +250,7 @@ func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void:
_contact_unit = null
_hero_sprite.last = Vector2i(-99, -99)
_r0.clear()
_hover = Vector2i(-9, -9)
_hover_comp._hover = Vector2i(-9, -9)
for c in _board.compute_r0():
_r0[c] = true
# 瓦片套随板图型切换(board_gen 词表 v1.1 dungeon/underdark/deep_underdark;
@@ -255,17 +271,17 @@ func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void:
# 逐格构建器(rebuild 建格循环唯一出函面):.new() 与属性装配逐字迁入,循环体只调构建器
func _build_cell_unit(c: Vector2i, stride: float) -> Node2D:
var u: Node2D = _CELL_UNIT.new()
u.st.cell = c
u.st.edge = _cell
u.cell = c
u.edge = _cell
u.position = Vector2(c) * stride
u.st.bevel = DEPTH_BEVEL # 新建格单元继承
u.base.bevel = DEPTH_BEVEL # 新建格单元继承
u.use_parent_material = true # 去饱和材质挂 BoardWorld——单元随世界材质灰化
if _palette.tiles_ok:
# NEAREST 采样(32×32 源 × 45/36px 格边非整数缩放接受档,
# 与英雄小人 1.5× 豁免同级(圣经 §8.2);只在瓦片
# 模式启用——程序化回退路径渲染零扰动(含格面文字采样不回退不变化)
u.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
u.st.font = _world.board_font
u.face.font = _world.board_font
_world.add_child(u)
return u
@@ -283,7 +299,7 @@ func refresh() -> void:
for y in _board.height:
for x in _board.width:
_paint_cell(Vector2i(x, y), hero)
_refresh_hover_after_paint()
_hover_comp._refresh_hover_after_paint()
_apply_contact_bands()
_update_hero(hero) # 英雄精灵 diff 驱动
@@ -433,33 +449,18 @@ func _update_world_size() -> void:
# 真实输入路径(Node2D 迁移批 Decision 3):壳层 _gui_input → 整图公式换算 → 复用既有 hover/点选/chord 出口
#(悬停族已拆 board_hover 件——本壳留三入站口委托)
func _gui_input(ev: InputEvent) -> void:
if _minesweeper == null:
return
if ev is InputEventMouseMotion:
_hover_at(ev.position)
_hover_comp._hover_at(ev.position)
elif ev is InputEventMouseButton and ev.pressed:
var c := _cell_at(ev.position)
if c.x >= 0:
_on_cell_input(c, ev)
func _hover_at(pos: Vector2) -> void:
var c := _cell_at(pos)
if c.x < 0:
if _hover.x >= 0:
_on_cell_unhover(_hover)
elif c != _hover:
if _hover.x >= 0:
_on_cell_unhover(_hover)
_on_cell_hover(c)
func _on_shell_mouse_exited() -> void:
if _hover.x >= 0:
_on_cell_unhover(_hover)
func _on_cell_input(c: Vector2i, ev: InputEvent) -> void:
if _minesweeper == null or not (ev is InputEventMouseButton and ev.pressed):
return
@@ -468,7 +469,7 @@ func _on_cell_input(c: Vector2i, ev: InputEvent) -> void:
action_requested.emit(c, "move")
else:
action_requested.emit(c, "dig")
_detect_chord(c)
_hover_comp._detect_chord(c)
elif ev.button_index == MOUSE_BUTTON_RIGHT:
if not _minesweeper.has_seal(c):
action_requested.emit(c, "seal_place")
@@ -476,89 +477,6 @@ func _on_cell_input(c: Vector2i, ev: InputEvent) -> void:
# 左键相邻封印格直接挖(规则引擎放行)。
# chord 肌肉记忆首遇锚:同格双左击快开尝试(350ms 窗)——每 run 一次提示由组合根去重
func _detect_chord(c: Vector2i) -> void:
var now := Time.get_ticks_msec()
if _last_click.cell == c and now - int(_last_click.ms) <= CHORD_WINDOW_MS:
chord_attempted.emit(c)
_last_click.ms = -100000
return
_last_click = {"cell": c, "ms": now}
# ---- 悬停预承诺(pattern 4):三语义光标 + 目标格边框 + BFS 路径预览 ----
func _on_cell_hover(c: Vector2i) -> void:
_hover = c
_apply_hover_visuals(c)
func _on_cell_unhover(c: Vector2i) -> void:
if _hover == c:
_hover = Vector2i(-9, -9)
_clear_hover_visuals(c)
func _refresh_hover_after_paint() -> void:
# 状态随 refresh 变化后光标/边框语义需重算(悬停格四格态可能已变)
if _hover.x >= 0 and _cells.has(_hover):
_apply_hover_visuals(_hover)
func _apply_hover_visuals(c: Vector2i) -> void:
if _minesweeper == null or not _cells.has(c):
return
_clear_hover_visuals(c)
var u: Node2D = _cells[c]
var revealed: bool = _minesweeper.is_revealed(c)
var seal: bool = _minesweeper.has_seal(c)
var adjacent: bool = _diggable.has(c)
if revealed:
u.st.hl_visible = true
u.st.hl_color = COL_HL_TARGET # 移动目标格边框(金)
_set_cursor_shape(c, CURSOR_MOVE_SEM)
_show_path_preview(c)
elif adjacent:
u.st.hl_visible = true
u.st.hl_color = COL_HL_TARGET # 挖掘目标格边框(相邻封印格同此——左键直接挖)
_set_cursor_shape(c, CURSOR_DIG)
elif seal:
u.st.hl_visible = true
u.st.hl_color = COL_SEAL # 已承诺封印标记(封印黑;无交互可供)
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
else:
u.st.hl_visible = true
u.st.hl_color = UiKit.RED_FORBID # 不可及域灰禁框(禁止光标+首遇 toast 双通道)
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
hover_forbidden.emit(c)
func _clear_hover_visuals(c: Vector2i) -> void:
if not _cells.has(c):
return
_cells[c].st.hl_visible = false
_hide_path_preview()
func _set_cursor_shape(c: Vector2i, shape: int) -> void:
mouse_default_cursor_shape = shape # 壳级光标(悬停格语义决定——单格悬停恒一)
func _show_path_preview(c: Vector2i) -> void:
# 合法性指示:悬停已开格 → BFS 路径蓝细线点迹(compute_move_path 只读面)
if _minesweeper == null:
return
var path: Array[Variant] = _minesweeper.compute_move_path(c)
for pc in path:
if _cells.has(pc):
_cells[pc].st.dot_visible = true
func _hide_path_preview() -> void:
for c in _cells:
_cells[c].st.dot_visible = false
# ---- 奖励徽章(已开格身份 icon——封印之下、纹带之上、不进数字区) ----
func _update_badge(c: Vector2i) -> String:
@@ -613,19 +531,19 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
var is_hero: bool = c == hero
# 封印槽统一维护:封印格=封印印记纹理;其余格清 null(封印格挖开后残留在 tex_seal
# 会让 _draw 无条件把封印印画在已开格上——tex_seal 绘制无 revealed 条件)。缺盘走字形回退。
u.st.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null
u.face.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null
if removed:
revealed = false # 移除格固定未开呈现基座(退色暗幕层承载;危险格本无数字,周边数字冻结)
seal = false
u.st.tex_seal = null
u.face.tex_seal = null
# 绘制层五段(字形与色牌→基底瓦片→立面投影→秘宝/变异叠加→迷雾)——段间交换经单元状态面
#(u.st/u.glyph/u.glyph_color),唯一例外=基底色 bg 经 ink 段返回值交 base 段(无单元场域承载)
#(u.base/u.face 域件与 u.glyph/u.glyph_color),唯一例外=基底色 bg 经 ink 段返回值交 base 段(无单元场域承载)
var bg: Color = _paint_cell_ink(c, u, revealed, seal)
_paint_cell_base(c, u, revealed, is_hero, bg)
_paint_cell_facade(c, u, revealed)
_paint_cell_overlays(c, u, revealed)
_paint_cell_fog(c, u, revealed, hero)
u.st.removed_dim = removed
u.face.removed_dim = removed
u.queue_redraw()
@@ -696,17 +614,17 @@ func _paint_cell_base(c: Vector2i, u: Node2D, revealed: bool, is_hero: bool, bg:
# 贴图缺位回退字形「人」(项目纹理槽惯例——贴图在位=格内字形退役,单位本体由独立精灵承载)
u.glyph = "人"
u.glyph_color = COL_HERO
u.st.tex_base = tile
u.st.bg_sb = sb
u.base.tex_base = tile
u.base.bg_sb = sb
# 立面投影段:未开墙格南缘立面/轮廓描边码;已开格北缘投影暗带/E 格高墙面——段内重置与赋值同序
func _paint_cell_facade(c: Vector2i, u: Node2D, revealed: bool) -> void:
u.st.outline_sides.assign([]) # typed Array[int](assign 保持引用语义)
u.st.facade_south = false
u.st.proj_band = 0.0
u.st.tex_facade = null
u.st.facade_strip = false
u.base.outline_sides.assign([]) # typed Array[int](assign 保持引用语义)
u.base.facade_south = false
u.base.proj_band = 0.0
u.base.tex_facade = null
u.base.facade_strip = false
# 封印槽不在此重置:尾部重置会与此上方条件赋值构成双写踩踏(把封印纹理恒清 null=素材在位时封印不可见);
# 上方 else 分支已是全路径清残留,尾部重置系冗余且有害
var south_n: Vector2i = c + Vector2i(0, 1)
@@ -716,26 +634,26 @@ func _paint_cell_facade(c: Vector2i, u: Node2D, revealed: bool) -> void:
if not revealed:
# 高墙感 D 投影档:南邻地面格 → 墙格南缘立「口沿高光+双段暗带」立面
#(邻地面才画——墙-墙内界保墙群内聚);板外=墙不画
u.st.facade_south = south_in and _minesweeper.is_revealed(south_n)
u.base.facade_south = south_in and _minesweeper.is_revealed(south_n)
# 轮廓加强:墙格四邻中已开(地面)格朝向记描边码(板外=墙不描——保墙群外轮廓只在墙/地交界)
for d in [Vector2i(0, -1), Vector2i(0, 1), Vector2i(-1, 0), Vector2i(1, 0)]:
var n: Vector2i = c + d
if n.x >= 0 and n.y >= 0 and n.x < _board.width and n.y < _board.height and _minesweeper.is_revealed(n):
var code := 0 if d.y < 0 else (1 if d.y > 0 else (2 if d.x < 0 else 3))
if code == 1 and u.st.facade_south:
if code == 1 and u.base.facade_south:
continue # 南缘由立面件承载(高光+双段暗带)——2px 描边让位防三重叠黑
if code == 0 and _palette.tiles_ok:
continue # v3.5 冻结① (b)-(c) 交界无边框:墙面直贴接墙顶(E 格墙面件在邻格承载)——北向描边不画(平色回退态保留描边)
u.st.outline_sides.append(code)
u.base.outline_sides.append(code)
else:
# 地面投影:北邻墙格在本已开格北缘投渐隐暗带(ITB 同款高度线索——带宽按格边比例常量)
if north_in and not _minesweeper.is_revealed(north_n):
u.st.proj_band = u.st.edge * WALL_PROJECTION_FRAC
u.base.proj_band = u.edge * WALL_PROJECTION_FRAC
# E 格高墙面(圣经 v3.5 冻结①——方向稿 is_facade_cell 口径):已开格南邻未开墙 → 南部 30%
# 承接墙面(材质=同板墙顶 center 瓦片,原亮度直贴);仅瓦片态启用——平色回退保持原语义
if south_in and not _minesweeper.is_revealed(south_n) and _palette.tiles_ok:
u.st.facade_strip = true
u.st.tex_facade = _palette.tiles.get("wall_center")
u.base.facade_strip = true
u.base.tex_facade = _palette.tiles.get("wall_center")
# 秘宝/变异叠加段:真视红框/预读徽章/变异角标/奖励徽章/类型微光组号——glyph/digit_col 局部镜像
@@ -746,21 +664,21 @@ func _paint_cell_overlays(c: Vector2i, u: Node2D, revealed: bool) -> void:
var glyph: String = u.glyph
var digit_col: Color = u.glyph_color
if not revealed:
u.st.treasure_mark = "mine_reveal" if _treasure_reveal.has(c) else ""
u.face.treasure_mark = "mine_reveal" if _treasure_reveal.has(c) else ""
if _treasure_preview.has(c) and glyph == "":
var pid = _identity_cb.call(c) if _identity_cb.is_valid() else null
if pid != null and REWARD_BADGE.has(String(pid.get("cell_type", ""))):
glyph = String(REWARD_BADGE[String(pid.get("cell_type", ""))])
digit_col = COL_GOLD
u.st.badge_glyph = glyph
u.face.badge_glyph = glyph
# ②' 变异角标:未开格带伤害型变异 → 危险红角标(已开=威胁已兑现标记退位);
# 回调缺位 = 无标记(程序化角标,tex_mutation 纹带槽暂留)
if not revealed and _vmark_cb.is_valid():
u.st.mutation_mark = String(_vmark_cb.call(c))
u.face.mutation_mark = String(_vmark_cb.call(c))
else:
u.st.mutation_mark = ""
u.st.badge_glyph = _update_badge(c)
u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
u.face.mutation_mark = ""
u.face.badge_glyph = _update_badge(c)
u.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
# 迷雾段:灯光区外未开格全黑/已开格记忆态灰显——纯呈现零逻辑(🔴 契约 1)
@@ -803,7 +721,7 @@ func _sync_glow_to_units() -> void:
for c in touched:
var u: Node2D = _cells.get(c)
if u != null:
u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
u.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
func _process(delta: float) -> void:
@@ -815,14 +733,14 @@ func _process(delta: float) -> void:
if UiKit.motion_scale < 1.0:
return # 减动效:呼吸停格(refresh/enable 时已施静态色——静态可辨、无持续运动)
var k := 0.5 + 0.5 * sin(_breath_t * 3.0) # 3 rad/s≈0.48Hz——无 >3 次/秒频闪(AC 基线)
_contact_unit.st.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k)
_contact_unit.base.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k)
_contact_unit.queue_redraw()
# 带几何写入(refresh / set_dig_glow_enabled(true) 挂点):清旧承载件→按英雄格可挖域写边/角码→施色
func _apply_contact_bands() -> void:
if _contact_unit != null and is_instance_valid(_contact_unit) and (_cells.get(_minesweeper.hero_pos()) != _contact_unit if _minesweeper != null else true):
_contact_unit.st.contact_col = Color(0, 0, 0, 0)
_contact_unit.base.contact_col = Color(0, 0, 0, 0)
_contact_unit.queue_redraw()
_contact_unit = null
if _board == null or _minesweeper == null or not _dig_glow:
@@ -839,19 +757,19 @@ func _apply_contact_bands() -> void:
for d in [Vector2i(-1, -1), Vector2i(1, -1), Vector2i(-1, 1), Vector2i(1, 1)]:
if _diggable.has(_minesweeper.hero_pos() + d):
corners.append(0 if (d.x < 0 and d.y < 0) else (1 if d.x > 0 and d.y < 0 else (2 if d.x < 0 else 3)))
hu.st.contact_bands = bands
hu.st.contact_corners = corners
hu.base.contact_bands = bands
hu.base.contact_corners = corners
# 减动效:呼吸停格——refresh/enable 重放时施**静态**色(本件头注「已施静态色」的落実);
# 0.5 = 呼吸周期中值(正常态 k 的均值)。否则减动效下每次 refresh 都会按当前相位重施色,
# 带色在 refresh 间跳变=持续运动假象,且令 on/off 双帧取证无法隔离单一变量。
var k := 0.5 if UiKit.motion_scale < 1.0 else 0.5 + 0.5 * sin(_breath_t * 3.0)
hu.st.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k)
hu.base.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k)
hu.queue_redraw()
func _reset_breath() -> void:
if _contact_unit != null and is_instance_valid(_contact_unit):
_contact_unit.st.contact_col = Color(0, 0, 0, 0)
_contact_unit.base.contact_col = Color(0, 0, 0, 0)
_contact_unit.queue_redraw()
@@ -898,9 +816,9 @@ func diggable_probe() -> Dictionary:
return _diggable.duplicate()
# 悬停格只读出口(case① 悬停到达断言消费面)
# 悬停格只读出口(case① 悬停到达断言消费面——读件投影,返回语义零变化)
func hover_probe() -> Vector2i:
return _hover
return _hover_comp._hover
# 迷雾参数只读快照(case⑮ 参数漂移断言消费面)
@@ -926,5 +844,5 @@ func cell_rect_probe(c: Vector2i) -> Rect2:
if host.size.x <= 0.0 or host.size.y <= 0.0 or vp_size.x <= 0.0 or vp_size.y <= 0.0:
return Rect2()
var k: Vector2 = host.size / vp_size
var local := Rect2(u.global_position, Vector2(u.st.edge, u.st.edge))
var local := Rect2(u.global_position, Vector2(u.edge, u.edge))
return Rect2(host.position + local.position * k, local.size * k)
+4 -4
View File
@@ -18,17 +18,17 @@ signal hover_forbidden(cell: Vector2i)
signal chord_attempted(cell: Vector2i)
func _init() -> void:
func _init(layout: BattleLayoutTable) -> void:
size_flags_horizontal = Control.SIZE_EXPAND_FILL
var vb := VBoxContainer.new()
vb.add_theme_constant_override("separation", 4)
add_child(vb)
_build_views(vb)
_build_views(vb, layout)
## 视图三件域:AP 条+棋盘区(含动效与 toast 叠层)
func _build_views(vb: VBoxContainer) -> void:
ap_bar = ApBarView.new()
func _build_views(vb: VBoxContainer, layout: BattleLayoutTable) -> void:
ap_bar = ApBarView.new(layout)
vb.add_child(ap_bar)
var board_area := Control.new()
board_area.size_flags_vertical = Control.SIZE_EXPAND_FILL
+204
View File
@@ -0,0 +1,204 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 格面基座层绘制域件(CellUnit 拆件——单一变化原因=基座层视觉规格):
## 底面/描边/高墙立面/地面投影/接触带微光/顶面倒角的全部绘制入参与绘制命令。
## 无 class_name(类名缓存面纪律)——经宿主 board_cell_unit preload 引用;宿主 _draw 按层序调
## draw_base(canvas, rect) 把绘制命令落到宿主画布;setter 经 _owner 回引触发宿主重绘语义逐字。
extends RefCounted
# ── D 投影档绘制常量(研究件实测取色——立面高光/双段暗带/地面投影;黑罩 alpha 叠加实测等效)──
const FACADE_HIGHLIGHT_PX := 2.0 # 口沿高光线厚(像素)
const FACADE_NEAR_FRAC := 0.16 # 立面近段高(格边比——研究件 10/64)
const FACADE_FAR_FRAC := 0.25 # 立面远段高(格边比——研究件 16/64)
const FACADE_NEAR_COL := Color(0, 0, 0, 0.60) # 立面近段黑罩(墙顶 132→52 实测 α≈0.61)
const FACADE_FAR_COL := Color(0, 0, 0, 0.82) # 立面远段黑罩(墙顶 132→24 实测 α≈0.82)
const PROJECTION_NEAR_COL := Color(0, 0, 0, 0.28) # 投影贴墙段(地面 66→46 实测 α≈0.28)
const PROJECTION_MID_COL := Color(0, 0, 0, 0.16) # 投影中段(渐隐)
const PROJECTION_EDGE_COL := Color(0, 0, 0, 0.06) # 投影远段(渐隐收边)
# ── E 格高墙面(圣经 v3.5 冻结①——地面格南邻墙→南部 30% 承接墙面:墙顶纹理原亮度
# 直贴零暗档零阴影;墙脚阴线只画地面/墙面交界;(b)-(c) 交界
# 无边框由 BoardView 侧跳过墙格北向描边承载)──
const E_FACADE_FRAC := 0.30 # 墙面承接高(格边比 30%)
var _owner: Node2D = null
var bg_sb: StyleBoxFlat = null # 格底 StyleBox(BoardView 每次 paint 重建;呼吸微光就地改边框)
# ── 接触带微光绘制面(可挖域微光画在英雄格周界空间侧——art-bible §3.2 v2.8;
# 仅英雄格单元承载,BoardView.refresh 按可挖域写边/角码、_process 逐拍更新呼吸色)──
var contact_bands: Array[int] = [] # 正交接触边码 0=N/1=S/2=W/3=E(该侧整条边)
var contact_corners: Array[int] = [] # 斜向共享角码 0=NW/1=NE/2=SW/3=SE(角点+两侧各半条边)
var contact_col := Color(0, 0, 0, 0) # 当前呼吸色(alpha 0 = 不画)
var outline_sides: Array[int] = [] # 墙格地面朝向描边码 0=N/1=S/2=W/3=E(程序侧结构件)
# ── 高墙感 D 投影档(研究件四变体定 D=C+地面投影,ITB 同款高度线索)──
# 南立面 = 墙格南缘内侧「口沿高光线 + 双段暗带」(邻地面格才画,墙群内聚);地面投影 = 北邻墙格
# 在本已开格北缘投渐隐暗带。参数按研究件 64px 拼版实测比例换算(立面双段 0.16/0.25 格边、投影带
# 0.22 格边;坐标推导留档 commit)。素材缺位程序化回退同样服役(结构归程序——art-bible §8.5)。
var facade_south := false: # 南立面开关(BoardView paint 按「南邻已开格」写入)
set(v):
facade_south = v
_owner.queue_redraw()
var proj_band := 0.0: # 地面投影带宽(像素,0 = 不画——BoardView 按北邻墙判定写入)
set(v):
proj_band = v
_owner.queue_redraw()
var bevel := 0.0: # 顶面倒角强度(0=关;档位 0/0.35/0.6/1.0)
set(v):
bevel = v
_owner.queue_redraw()
var tex_base: Texture2D = null # ① 砖面底(宿主迷雾记忆态公开单向读)
var tex_facade: Texture2D = null # ⑥ E 格墙面材质(v3.5——南邻墙顶 center 瓦片同源,BoardView 注入)
var facade_strip := false: # E 格开关(BoardView paint 按「已开格且南邻未开墙」写入——瓦片态专用)
set(v):
facade_strip = v
_owner.queue_redraw()
func _init(owner: Node2D) -> void:
_owner = owner
## ① 砖面底层域(占位 = StyleBoxFlat:格底色 + 暗化描边 + 3px 圆角;瓦片态描边覆盖层;
## 呼吸微光/主角蓝框由 BoardView 改 bg_sb 后触发重绘。素材缺位回退走 elif 程序化原样,零观感变化)
func draw_base(canvas: CanvasItem, rect: Rect2) -> void:
if tex_base != null:
canvas.draw_texture_rect(tex_base, rect, false)
# 轮廓加强(轮廓线不明显):墙格地面朝向边补 2px 深墨线(§8.5 结构归程序——
# 素材 1px 墨线在 45/36px 非整数缩放下视觉衰减,程序线保底;墙-墙内界不画,保墙群内聚;
# 坐标推导:各边内贴 2px 矩形,相邻边矩形全长相交=周界闭合)
if not outline_sides.is_empty():
_draw_outline_sides(canvas, rect)
_draw_south_facade(canvas, rect)
_draw_north_projection(canvas, rect)
_draw_e_facade(canvas, rect)
# 瓦片态描边覆盖层:tex_base 在位时 BoardView 传入的 bg_sb
# 已降级为「透明底 + 描边」载体——呼吸微光/主角蓝框画在瓦片之上(art-bible §1.2 叠层序
# 不变:底 < 纹带 < 数字 < 封印 < 焦点高亮);素材缺位回退走 elif 程序化原样,零观感变化
if bg_sb != null:
canvas.draw_style_box(bg_sb, rect)
_draw_contact_or_fallback(canvas, rect)
## 接触带微光或平色回退域(英雄格周界空间侧——正交邻墙=整条边;斜向=共享角点+两侧各半条边;光不进墙格):
## 带宽 4px 贴格边内侧(坐标推导:N=[0,0,edge,4] S=[0,edge-4,edge,4] W=[0,0,4,edge] E=[edge-4,0,4,edge];
## 半条边=沿邻边自角点起 edge/2;角点圆 r=6 贴角点圆心,近白芯 r=2.5 提亮)
func _draw_contact_or_fallback(canvas: CanvasItem, rect: Rect2) -> void:
if contact_col.a > 0.0:
_draw_north_projection(canvas, rect) # 平色回退+微光并发的英雄格补画(瓦片路径已画——同像素重复无观感差)
_draw_e_facade(canvas, rect) # E 格墙面补画(同上——英雄格兼 E 格时瓦片路径已画,重复无害)
_draw_contact_bands(canvas, rect)
elif bg_sb != null:
canvas.draw_style_box(bg_sb, rect)
_draw_south_facade(canvas, rect) # 平色回退路径——D 投影档两件同样服役(结构归程序)
_draw_north_projection(canvas, rect)
_draw_bevel(canvas, rect)
## 墙格轮廓域:地面朝向边补 2px 深墨线(各边内贴 2px 矩形,相邻边矩形全长相交=周界闭合)
func _draw_outline_sides(canvas: CanvasItem, rect: Rect2) -> void:
var ow := 2.0
var oc := UiKit.FOG_OUTLINE
for b in outline_sides:
var or_ := Rect2()
match int(b):
0: or_ = Rect2(0, 0, rect.size.x, ow)
1: or_ = Rect2(0, rect.size.x - ow, rect.size.x, ow)
2: or_ = Rect2(0, 0, ow, rect.size.x)
3: or_ = Rect2(rect.size.x - ow, 0, ow, rect.size.x)
canvas.draw_rect(or_, oc, true)
## 接触带微光域:正交整条边 + 斜向共享角点与两侧半条边
func _draw_contact_bands(canvas: CanvasItem, rect: Rect2) -> void:
var band := 4.0
var half := rect.size.x * 0.5
for b in contact_bands:
var r := Rect2()
match int(b):
0: r = Rect2(0, 0, rect.size.x, band)
1: r = Rect2(0, rect.size.x - band, rect.size.x, band)
2: r = Rect2(0, 0, band, rect.size.x)
3: r = Rect2(rect.size.x - band, 0, band, rect.size.x)
canvas.draw_rect(r, contact_col, true)
for cc in contact_corners:
_draw_contact_corner(canvas, rect, int(cc), band, half)
## 斜向角点域:角点圆 r=6 贴角点圆心 + 近白芯 r=2.5 提亮 + 半条边(角点沿两条邻边各延伸 edge/2——
## 坐标推导:横带沿 N/S 自角点向板心侧 half;NW/SE 半带 x=y=角点坐标,NE/SE 半带自 edge-half 收进——
## 正交整边时重叠同色无害)
func _draw_contact_corner(canvas: CanvasItem, rect: Rect2, cc: int, band: float, half: float) -> void:
var pt := Vector2.ZERO
match cc:
0: pt = Vector2(0, 0)
1: pt = Vector2(rect.size.x, 0)
2: pt = Vector2(0, rect.size.x)
3: pt = Vector2(rect.size.x, rect.size.x)
canvas.draw_circle(pt, 6.0, contact_col)
canvas.draw_circle(pt, 2.5, Color(1, 1, 1, minf(1.0, contact_col.a * 0.95)))
if cc == 0 or cc == 1:
# N 侧横带:NW 自 x=0、NE 自 x=edge-half
canvas.draw_rect(Rect2(pt.x if cc == 0 else pt.x - half, 0.0, half, band), contact_col, true)
if cc == 2 or cc == 3:
# S 侧横带:SW 自 x=0、SE 自 x=edge-half
canvas.draw_rect(Rect2(pt.x if cc == 2 else pt.x - half, rect.size.x - band, half, band), contact_col, true)
if cc == 0 or cc == 2:
# W 侧竖带:NW 自 y=0、SW 自 y=edge-half
canvas.draw_rect(Rect2(0.0, pt.y if cc == 0 else pt.y - half, band, half), contact_col, true)
if cc == 1 or cc == 3:
# E 侧竖带:NE 自 y=0、SE 自 y=edge-half
canvas.draw_rect(Rect2(rect.size.x - band, pt.y if cc == 1 else pt.y - half, band, half), contact_col, true)
## 顶面倒角域(光源左上——上/左内沿受光 2px、下/右内沿背光 2px;内缩 2px 让
## 呼吸微光/主角蓝边框保持最外沿可见;强度随档位)
func _draw_bevel(canvas: CanvasItem, rect: Rect2) -> void:
if bevel <= 0.0:
return
var base: Color = bg_sb.bg_color
var lit: Color = base.lightened(0.22 * bevel)
var dark: Color = base.darkened(0.22 * bevel)
var t := 2.0
canvas.draw_rect(Rect2(t, t, rect.size.x - t * 2.0, t), lit, true)
canvas.draw_rect(Rect2(t, t, t, rect.size.x - t * 2.0), lit, true)
canvas.draw_rect(Rect2(t, rect.size.x - t * 2.0, rect.size.x - t * 2.0, t), dark, true)
canvas.draw_rect(Rect2(rect.size.x - t * 2.0, t, t, rect.size.x - t * 2.0), dark, true)
# 南立面(D 档件一——墙格南缘内侧,自底边向上:双段暗带在下、口沿高光线贴其上沿):
# 坐标推导(自格底边起算):远段=[edge−far, edge],近段=[edge−far−near, edge−far],
# 高光线=[edge−far−near−hl, edge−far−near]——与研究件「墙顶面→高光→暗段→更暗段→格界」下行序一致
func _draw_south_facade(canvas: CanvasItem, rect: Rect2) -> void:
if not facade_south:
return
var far_h := rect.size.x * FACADE_FAR_FRAC
var near_h := rect.size.x * FACADE_NEAR_FRAC
var y_far := rect.size.x - far_h
var y_near := y_far - near_h
var y_hl := y_near - FACADE_HIGHLIGHT_PX
canvas.draw_rect(Rect2(0.0, y_hl, rect.size.x, FACADE_HIGHLIGHT_PX), UiKit.BRICK, true) # 口沿高光(合并归掩尘枚 BRICK——art-bible §4.1 换色合并①同值单点)
canvas.draw_rect(Rect2(0.0, y_near, rect.size.x, near_h), FACADE_NEAR_COL, true)
canvas.draw_rect(Rect2(0.0, y_far, rect.size.x, far_h), FACADE_FAR_COL, true)
# 地面投影(D 档件二——北邻墙格在本已开格北缘投渐隐暗带,自北缘向南三段衰减):
# 坐标推导:三等分 proj_band,α 0.28/0.16/0.06(研究件实测贴墙 α≈0.28、带末渐融入地面)
func _draw_north_projection(canvas: CanvasItem, rect: Rect2) -> void:
if proj_band <= 0.0:
return
var seg := proj_band / 3.0
canvas.draw_rect(Rect2(0.0, 0.0, rect.size.x, seg), PROJECTION_NEAR_COL, true)
canvas.draw_rect(Rect2(0.0, seg, rect.size.x, seg), PROJECTION_MID_COL, true)
canvas.draw_rect(Rect2(0.0, seg * 2.0, rect.size.x, seg), PROJECTION_EDGE_COL, true)
# E 格高墙面(圣经 v3.5 冻结①——方向稿 draw_facade 移植):本格已开且南邻为墙 →
# 南部 30% 承接墙面。材质 = 墙顶 center 瓦片顶部 30% 区带**原亮度**直贴(零暗档零阴影);
# 墙脚阴线只画地面/墙面交界(strip 顶缘 1px);墙面底缘(格界处)不画任何线——直接接南侧墙顶
# (白边导致两截)。仅瓦片态服役(tex_facade 缺位 = 平色回退原语义不动)。
func _draw_e_facade(canvas: CanvasItem, rect: Rect2) -> void:
if not facade_strip or tex_facade == null:
return
var strip_h := rect.size.x * E_FACADE_FRAC
var y0 := rect.size.x - strip_h
var ts := tex_facade.get_size()
canvas.draw_texture_rect_region(tex_facade, Rect2(0.0, y0, rect.size.x, strip_h),
Rect2(0.0, 0.0, ts.x, ts.y * E_FACADE_FRAC))
canvas.draw_rect(Rect2(0.0, y0, rect.size.x, 1.0), UiKit.E_FACADE_FOOT_COL, true)
@@ -0,0 +1 @@
uid://c2msg115y3oir
@@ -0,0 +1,145 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 格面标识与顶层叠加绘制域件(CellUnit 拆件——单一变化原因=标识与叠加层视觉规格):
## 变异标记/数字色牌与字形/奖励徽章/封印/路径点迹/焦点高亮/中立移除退色的全部绘制入参与绘制命令。
## 无 class_name(类名缓存面纪律)——经宿主 board_cell_unit preload 引用;宿主 _draw 按层序调
## draw_marks/draw_glyph_layer/draw_overlay_under/draw_removed_dim(雾层由宿主夹在 overlay_under 与 removed_dim 之间);
## setter 经 _owner 回引触发宿主重绘语义逐字(badge_glyph 的 glow mask 回引 _owner._glow_layer 原样)。
extends RefCounted
var _owner: Node2D = null
var badge_glyph := "": # 奖励徽章文字(右下角金——封印之下、变异纹之上、不进数字区)
set(v):
badge_glyph = v
if _owner._glow_layer != null and is_instance_valid(_owner._glow_layer):
_owner._glow_layer.set_badge_mask(v != "")
_owner.queue_redraw()
var dot_visible := false: # BFS 路径预览点迹(中心 6×6 小人蓝)
set(v):
dot_visible = v
_owner.queue_redraw()
var hl_visible := false: # 焦点高亮(悬停态边框覆盖层)
set(v):
hl_visible = v
_owner.queue_redraw()
var hl_color := UiKit.GOLD: # 高亮色(金目标/封印朱/禁入灰禁红)
set(v):
hl_color = v
_owner.queue_redraw()
var font: Font = null # 字体槽位(null = ThemeDB.fallback_font——BoardView.set_world_font 注入)
# ── 素材槽位(现全 null = 程序化占位服役;art-bible §8.2 格面 32×32 暂定档)──
var tex_mutation: Texture2D = null # ② 变异纹带
var tex_glyph: Texture2D = null # ③ 数字
var tex_badge: Texture2D = null # ③' 徽章
var tex_seal: Texture2D = null: # ④ 封印(槽位可运行时换装〔封印〕——同族 facade/treasure_mark
set(v): # 均 setter 触发重绘(普通 var 赋值不重绘=封印插上不显示))
if tex_seal != v:
tex_seal = v
_owner.queue_redraw()
# 变异角标(程序化,tex_mutation 纹带槽暂留):
# "flat_damage" = 未开格伤害型变异,左下角危险红三角(开格有代价的策略纹理标记)
# 叠加层标记(秘宝整图叠加层的格面呈现;派生信息层零回写):
# "mine_reveal" = 真视格(危险红内缩框——「这格是雷」的确定决策标记,封印照常计费);
# "reward_preview" 格不复用本字段——预读徽章走 badge_glyph 注入(board_view refresh 注入 REWARD_BADGE 字形)
var treasure_mark := "":
set(v):
if treasure_mark != v:
treasure_mark = v
_owner.queue_redraw()
var mutation_mark := "":
set(v):
if mutation_mark != v:
mutation_mark = v
_owner.queue_redraw()
var removed_dim := false: # 中立移除退色层(BoardView paint 按排雷玩法 is_removed 写入)
set(v):
removed_dim = v
_owner.queue_redraw()
func _init(owner: Node2D) -> void:
_owner = owner
## 变异标记域:真视格=危险红内缩框/未开格伤害型变异=左下角危险红三角
func draw_marks(canvas: CanvasItem, rect: Rect2) -> void:
if treasure_mark == "mine_reveal":
var inset := rect.size.x * 0.14
canvas.draw_rect(Rect2(inset, inset, rect.size.x - inset * 2.0, rect.size.x - inset * 2.0),
Color(UiKit.RED_MINE, 0.9), false, 2.0)
if mutation_mark == "flat_damage":
var mw := rect.size.x * 0.28
var mpts := PackedVector2Array([Vector2(0, rect.size.x - mw), Vector2(0, rect.size.x), Vector2(mw, rect.size.x)])
canvas.draw_colored_polygon(mpts, Color(UiKit.RED_MINE, 0.85))
## ②③④ 中层域:变异纹带与标记/数字色牌与字形/徽章/封印
##(glyph/glyph_color/plate 三者住宿主冻结面——探针直读成员,作参数传入)
func draw_glyph_layer(canvas: CanvasItem, rect: Rect2, glyph: String, glyph_color: Color, plate: bool) -> void:
# ② 变异纹带(无程序化纹带——槽位先行,贴图落位即用)
if tex_mutation != null:
canvas.draw_texture_rect(tex_mutation, rect, false)
# ③ 数字/徽章(数字 ≥ 格高 50% 墨族对格底 ≥4.5:1;徽章右下角金,遮蔽态 = "?")
if glyph != "":
if plate:
_draw_digit_plate(canvas, rect)
if tex_glyph != null:
canvas.draw_texture_rect(tex_glyph, rect, false)
else:
draw_centered_text(canvas, rect, glyph, glyph_color, maxi(18, int(ceil(rect.size.x * 0.5)))) # 字号下限 18(§7.4 冻结公式)
if badge_glyph != "":
if tex_badge != null:
canvas.draw_texture_rect(tex_badge, rect, false)
else:
_draw_badge(canvas, rect, badge_glyph)
# ④ 封印(现行视觉封印文字在 ③ 文字位承载——独立槽位留给封印图)
if tex_seal != null:
canvas.draw_texture_rect(tex_seal, rect, false)
## 数字色牌后备域(圣经 v3.5 冻结④:数字亮墨直画已开格底须垫牌;
## 牌=后备件——雷/「?」维持牌载保读性(牌上深墨族 12:1);
## 牌尺寸 0.60/0.62 格边居中;封印=未开格不垫牌)
func _draw_digit_plate(canvas: CanvasItem, rect: Rect2) -> void:
var pw := rect.size.x * 0.60
var ph := rect.size.x * 0.62
var pr := Rect2((rect.size.x - pw) * 0.5, (rect.size.x - ph) * 0.5, pw, ph)
canvas.draw_rect(pr, UiKit.DIGIT_PLATE, true)
canvas.draw_rect(pr, UiKit.DIGIT_PLATE_EDGE, false, 1.0)
## ⑤ 路径点迹 + 焦点高亮(悬停态覆盖层,不参与命中)——宿主时序器在其后插雾层
func draw_overlay_under(canvas: CanvasItem, rect: Rect2) -> void:
if dot_visible:
var ds := 6.0
canvas.draw_rect(Rect2(Vector2((rect.size.x - ds) * 0.5, (rect.size.x - ds) * 0.5), Vector2(ds, ds)), UiKit.HERO_BLUE, true)
if hl_visible:
# 悬停高亮减淡(金框太明显——绘制时 55% alpha,hl_color 属性与探针断言面不动)
canvas.draw_rect(Rect2(1, 1, rect.size.x - 2.0, rect.size.x - 2.0), Color(hl_color, 0.55), false, 2.0) # 2px 内描边
## ⑦ 中立移除退色层(退色降档:暗幕压顶全层覆盖,暗淡记忆态同族呈现语义;
## 顶层于迷雾之上——移除格不再参与迷雾语义。本实现移除集=未揭示危险格(无数字无封印印))
func draw_removed_dim(canvas: CanvasItem, rect: Rect2) -> void:
if removed_dim:
canvas.draw_rect(rect, Color(0.12, 0.10, 0.09, 0.62), true)
## 文字位绘制单点(宿主迷雾记忆态复用——同公式同字号;水平居中 + 垂直居中,基线 = (格高 − 字高)/2 + 上伸部)
func draw_centered_text(canvas: CanvasItem, rect: Rect2, text: String, col: Color, fs: int) -> void:
var f := font if font != null else ThemeDB.fallback_font
if f == null:
return
var asc := f.get_ascent(fs)
var desc := f.get_descent(fs)
canvas.draw_string(f, Vector2(0.0, (rect.size.x - (asc + desc)) * 0.5 + asc), text,
HORIZONTAL_ALIGNMENT_CENTER, rect.size.x, fs, col)
# 徽章绘制:右下角金(文字贴角内缩 2–3px)
func _draw_badge(canvas: CanvasItem, rect: Rect2, g: String) -> void:
var f := font if font != null else ThemeDB.fallback_font
if f == null:
return
var fs := maxi(10, int(rect.size.x * 0.28))
var sz := f.get_string_size(g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs)
canvas.draw_string(f, Vector2(rect.size.x - sz.x - 2.0, rect.size.x - 3.0), g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, UiKit.GOLD)
@@ -0,0 +1 @@
uid://c60e3su2qd1yq
@@ -21,8 +21,8 @@ var _board_variant_section: VBoxContainer # 地形变异常驻角标(空态
var _board_variant_list: VBoxContainer
func _init() -> void:
custom_minimum_size = Vector2(240, 0)
func _init(layout: BattleLayoutTable) -> void:
custom_minimum_size = Vector2(layout.info_panel_w, 0)
clip_contents = true
# 主面板穿石板戏服(纹理本色;数据框仍素面 §7.1;缺位回退 flat)
var sb := UiKit.panel_skinned(Vector4(8, 6, 8, 6))
+4 -4
View File
@@ -478,9 +478,9 @@ func _room_color(node: Dictionary, ctx: Dictionary) -> Color:
## 选路停点(候选非空)非候选通道降透明——金岔路从结构网分离(干线共线重叠缺陷的
## 视觉主因:不同源通道共用同一垂直干线,金边与石色边焊接成「整列可走」假象)
func _draw_passage_layer(canvas: Control, ctx: Dictionary) -> void:
var nodes: Array = ctx["nodes"]
var pos: Dictionary = ctx["pos"]
var cand_ids: Array = ctx["cand_ids"]
var nodes: Array = ctx["nodes"] # type-hint: ignore(ctx 束 Variant 出值——布局缓存数组未参数化)
var pos: Dictionary = ctx["pos"] # type-hint: ignore(同上——节点 id → 坐标)
var cand_ids: Array = ctx["cand_ids"] # type-hint: ignore(同上——候选 id 集)
var dim_passages: bool = not cand_ids.is_empty()
for node in nodes:
var from: Vector2 = pos[node]
@@ -496,7 +496,7 @@ func _draw_passage_layer(canvas: Control, ctx: Dictionary) -> void:
## 房间方块域:底色随行进态+Boss/候选描边+图标/字形+编号+类型文字标签
func _draw_room(canvas: Control, node: Dictionary, ctx: Dictionary) -> void:
var pos: Dictionary = ctx["pos"]
var pos: Dictionary = ctx["pos"] # type-hint: ignore(ctx 束 Variant 出值——节点 id → 坐标)
var p: Vector2 = pos[node]
var t := String(node.get("type", ""))
var nid := _id_of(node)
+142 -194
View File
@@ -2,81 +2,42 @@
## 站桩本体:B 区主角/敌人无框立绘+底座+血条+顺位/意图徽标的世界精灵(Node2D)。
## 站桩形态为定案——禁回卡片式;立绘素材缺位回退字形占位;击杀呈现禁瞬消(灰化塌陷)。
## 无 class_name——经 preload 引用;壳层坐标换算单点在 BattleStageView.world_to_shell()。
## 内部状态件(u4 子域拆件——契约 9 职责语句「站桩演出状态」):单一变化原因=站桩呈现与演出,
## 成员共同表达站桩的行进态/演出计数/资产挂点。外部直访冻结成员(box/font/slot_row/has_world_hp_bar)
## 与源码锚承载成员(_intent_charge——theme 源锚句所在行逐字保留)住宿主。
## 内部状态两件(u4 拆件——st=身份命值态 standee_vitals、mo=资产演出态 standee_motion,_init 自建);
## 外部直访冻结成员(box/font/slot_row/has_world_hp_bar)与源码锚承载成员(_intent_charge——theme
## 源锚句所在行逐字保留)与三个演出探针计数器住宿主。
extends Node2D
const CHARGE_SKILL_IDS := ["heavy"]
# 护甲类型图标占位——形状+色相双通道(art-bible):轻甲=◇ 灰石中性 / 魔法护甲=◆ 小人蓝 /
# 重甲=▣ 深金(避开 Boss 专属紫赤与妨害紫)。
# 护甲类型图标三键(light/ward/heavy)——终稿 = 三枚 24×24 PNG(UiKit.TEX_ARMOR_*,art-bible §5.3
# 工单:形状承载身份、色相不承载身份;挂载底色 = 名字行素底 HP_BAR_BACK)。字形占位仅作素材缺位
# 回退(与立绘缺位回退链同构——素材不入 git,缺位不炸)。
const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"}
# 名行/底座纵向预算余量(盒底−1px「声明值刚好=容器值」系高危写法;
# ≥8px 硬余量 + 字形下探预算 ⇒ 12;取值不属色板,不进 ui_kit)
const STANDEE_FOOT_MARGIN := 12.0
const DEATH_SHARD_N := 5 # 碎块数(§8.4 冻结值)
const HIT_SPARK_N := 6 # 命中火花数(打击瞬间可读性一拍)
const _VITALS := preload("res://presentation/views/standee_vitals.gd")
const _MOTION := preload("res://presentation/views/standee_motion.gd")
## 站桩演出状态(全部内部态单点——含 setter 触发宿主重绘语义,_owner 回引保持逐字)
class _StandeeState extends RefCounted:
var _owner: Node2D = null
var _kind := "enemy"
var _portrait_sprite: Sprite2D = null # 资产挂点(null = 未落位,字形在位)
var _portrait_tex: Texture2D = null # 落位纹理引用(set_portrait_texture 装载;字形回退判据)
# 主角三态切换:state 名 → 纹理(battle_stage_view 组合点注册);缺态=保持现纹理
# (缺位回退链延伸——三态素材缺任一不炸不空)。敌站桩不注册不消费。
var _hero_tex := {}
var _hero_state: int = 0
var _state_revert_tw: Tween
var _order := 1
var _intent_glyph := ""
var _intent_col := Color.WHITE
var _intent_visible := false
var _intent_scale := 1.0: # 意图弹入动画参数
set(v):
_intent_scale = v
_owner.queue_redraw()
var _glyph_rot := 0.0: # 立绘前倾角(贴图态无此动画)
set(v):
_glyph_rot = v
_owner.queue_redraw()
var _display_name := ""
var _enemy_id := ""
var _armor := "light"
var _dead := false
var _hp_max := 1.0
var _hp_cur := 0 # 血量数字显示面(HUD框架 验收「血量数字可读」)
var hp_value := 1.0: # 血条值 0..1(直角递减——tween_property 目标,逐行刷新重启不累积)
set(v):
hp_value = clampf(v, 0.0, 1.0)
_owner.queue_redraw()
var _hp_tween: Tween
var _hp_flash_tw: Tween
var _death_shards := 0 # 探针出口计数(碎块节点回收后仍保留读数)
var _hit_sparks := 0 # 探针出口计数(命中火花累计)
var _flash_count := 0 # 探针出口计数(受击闪白触发次数——主角路由断言面)
var st := _StandeeState.new()
var st: _VITALS # 身份与命值态件实例(名字沿用——外部路径串 "st.hp_value" 与约 40 处引用零改)
var mo: _MOTION # 资产与演出态件实例(_init 自建)
var box := Vector2(150, 176) # 站桩盒尺寸(实际高随壳层 row 布局注入;外部直访冻结)
var font: Font = null # 世界字体槽位(null = ThemeDB.fallback_font——world.world_font 注入;外部直访冻结)
var slot_row: StatusSlotRow # 状态效果唯一展示面 = 壳层伴随件(挂 BattleStageView 壳层,本体进世界;外部直访冻结)
var _intent_charge := false # 蓄力档(pid ∈ CHARGE_SKILL_IDS——意图三档 ③ 分支判据;源码锚承载成员住宿主)
# 血条存在性探针出口(世界直角血条常在;外部直访冻结)
var has_world_hp_bar := true
# 演出探针计数器(写入方=宿主演出函数、读取方=宿主 fx_probe 出口——仪器跟着驱动它的函数走)
var _death_shards := 0 # 探针出口计数(碎块节点回收后仍保留读数)
var _hit_sparks := 0 # 探针出口计数(命中火花累计)
var _flash_count := 0 # 探针出口计数(受击闪白触发次数——主角路由断言面)
func _init(kind: String = "enemy") -> void:
st._owner = self
st._hero_state = HeroState.IDLE
st = _VITALS.new(self)
mo = _MOTION.new(self)
st._kind = kind
enum HeroState { IDLE, RESET, ATTACK, HIT } # 主角三态+复位哨兵(外部 String 契约=register/set 形参零变化)
const _HS_NAME := {HeroState.IDLE: "idle", HeroState.RESET: "reset", HeroState.ATTACK: "attack", HeroState.HIT: "hit"}
const _HS_VALUE := {"idle": HeroState.IDLE, "reset": HeroState.RESET, "attack": HeroState.ATTACK, "hit": HeroState.HIT}
mo._hero_state = _MOTION.HeroState.IDLE
func _efont() -> Font:
@@ -94,12 +55,23 @@ func enemy_id() -> String:
# 意图徽标探针出口
func intent_probe() -> Dictionary:
return {"visible": st._intent_visible, "text": st._intent_glyph}
return {"visible": mo._intent_visible, "text": mo._intent_glyph}
# 名字行探针出口
# 名字行探针出口(ui_probe 呈现口径 + 本批 V6/V8 断言面:armor_icon = 绑定纹理路径〔缺位回退字形〕、
# 几何 = 素底牌矩形与文字基线、素底色——V6 判据 = baseline_y < plinth_top_y;
# name_font_size = 名字行字号档实读(迭代 46 卡B——提档 FS_HINT 14 的几何单点出口))
func name_line_probe() -> Dictionary:
return {"name": st._display_name, "armor_icon": String(ARMOR_ICONS.get(st._armor, "◇"))}
var f := _efont()
var tex := UiKit.load_tex(_armor_tex_path(st._armor))
var plate := _name_plate_rect(f, tex)
return {"name": st._display_name,
"armor_icon": _armor_tex_path(st._armor) if tex != null else String(ARMOR_ICONS.get(st._armor, "◇")),
"armor_tex_bound": tex != null,
"plate": plate, "baseline_y": _name_baseline(plate).y,
"plinth_top_y": _pedestal_rect().position.y,
"plate_color": UiKit.HP_BAR_BACK,
"name_font_size": UiKit.FS_HINT}
# ---- setter 面(presenter/组合根零改排) ----
@@ -111,7 +83,7 @@ func set_data(d: Dictionary) -> void:
st._enemy_id = String(d.get("enemy_id", ""))
st._armor = String(d.get("armor_type", "light"))
st._hp_max = float(maxi(1, int(d.get("max_hp", 1))))
_set_hp(int(d.get("hp", 0)))
st.apply_hp(int(d.get("hp", 0)))
_set_order(int(d.get("order", 1)))
set_pending(String(d.get("pending_type", "")), String(d.get("pending_skill", "")))
set_effects(d.get("effects", []))
@@ -122,21 +94,11 @@ func set_data(d: Dictionary) -> void:
func set_hero(hp: int, max_hp: int) -> void:
st._hp_max = float(maxi(1, max_hp))
_set_hp(hp)
st.apply_hp(hp)
if st._dead and hp > 0:
_revive() # 新战斗开场 HP 回正——主角死亡残位随之复活(敌侧 set_data 同款语义)
# 血条短 tween 递减(直角递减占位);逐行刷新时重启 tween 不累积
func _set_hp(hp: int) -> void:
st._hp_cur = clampi(hp, 0, int(st._hp_max))
var target := float(st._hp_cur) / st._hp_max
if st._hp_tween != null and st._hp_tween.is_valid():
st._hp_tween.kill()
st._hp_tween = create_tween()
st._hp_tween.tween_property(self, "st.hp_value", target, UiKit.motion(0.15))
# 主角/敌方共用——状态效果唯一展示面(壳层伴随件转发;空态 = 图标行隐藏不占位)
func set_effects(effects: Array) -> void:
if slot_row == null:
@@ -175,12 +137,12 @@ func _set_order(i: int) -> void:
func set_pending(ptype: String, pid: String) -> void:
if st._dead:
_intent_charge = false
st._intent_visible = false
mo._intent_visible = false
queue_redraw()
return
if ptype == "" or ptype == "basic":
_intent_charge = false
st._intent_visible = false # 普通攻击零表示(完全不可预测——§2.4)
mo._intent_visible = false # 普通攻击零表示(完全不可预测——§2.4)
queue_redraw()
return
var glyph := "⚔" # 攻击系(damage)
@@ -194,60 +156,15 @@ func set_pending(ptype: String, pid: String) -> void:
_intent_charge = pid in CHARGE_SKILL_IDS
if _intent_charge:
col = UiKit.GOLD_INK # 重击类蓄力档(深金墨——浅垫牌上 ≥4.5:1)
st._intent_glyph = glyph
st._intent_col = col
st._intent_visible = true
_play_intent_motion()
mo._intent_glyph = glyph
mo._intent_col = col
mo._intent_visible = true
mo.play_intent_motion(_intent_charge)
# 意图三档动作动画(§4.6.4 三档:①普通攻击=零表示〔set_pending 早退〕
# ②特殊准备=徽标弹入+二次弹跳(高举语义,运动手段 §2)③重击/硬化=缓推蓄力+定格微顿+回落;
# 三档运动签名互异,灰度/剪影层可辨)。摇定时一次性短动画,随后静帧保持。
func _play_intent_motion() -> void:
var tw := create_tween()
tw.tween_property(self, "st._intent_scale", 1.6, UiKit.motion(0.10))
tw.tween_property(self, "st._intent_scale", 1.0, UiKit.motion(0.16))
tw.parallel().tween_property(self, "st._glyph_rot", 4.0, UiKit.motion(0.10))
tw.tween_property(self, "st._glyph_rot", 0.0, UiKit.motion(0.14))
if _intent_charge:
tw.tween_property(self, "st._intent_scale", 1.22, UiKit.motion(0.28)).set_trans(Tween.TRANS_SINE) # ③蓄力缓推
tw.tween_interval(UiKit.motion(0.40)) # 定格微顿
tw.tween_property(self, "st._intent_scale", 1.0, UiKit.motion(0.18))
else:
tw.tween_property(self, "st._intent_scale", 1.26, UiKit.motion(0.11)) # ②二次弹跳(高举)
tw.tween_property(self, "st._intent_scale", 1.0, UiKit.motion(0.13))
# 资产挂点:tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形。
# flip = 立绘朝向翻转(素材基准朝左时传 true 呈现朝右)。盒高 96px = 48px 立绘源整数 2×
# (圣经 §8.2 禁非整数缩放)——源换代时随源重算。
# 资产挂点转调壳(签名外形零变——box 由宿主传入件内)
func set_portrait_texture(tex: Texture2D, flip: bool = false) -> void:
if tex == null:
if st._portrait_sprite != null:
st._portrait_sprite.visible = false
queue_redraw()
return
st._portrait_tex = tex
if st._portrait_sprite == null:
st._portrait_sprite = Sprite2D.new()
st._portrait_sprite.centered = false
st._portrait_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST # 像素帧放大防糊
add_child(st._portrait_sprite)
st._portrait_sprite.texture = tex
st._portrait_sprite.flip_h = flip
st._portrait_sprite.visible = true
_place_portrait()
func _place_portrait() -> void:
# 立绘盒 = (0,44)-(box.x, box.y-36);48 源 ×2 整数档居中
if st._portrait_sprite == null or st._portrait_tex == null:
return
var box_h := box.y - 44.0 - 36.0
var w := st._portrait_tex.get_width() * 2.0
var h := st._portrait_tex.get_height() * 2.0
st._portrait_sprite.scale = Vector2(2.0, 2.0) # 整数 2×(C5)
st._portrait_sprite.position = Vector2((box.x - w) * 0.5, 44.0 + (box_h - h) * 0.5)
mo.set_portrait_texture(tex, flip, box)
func mark_dead() -> void:
@@ -261,8 +178,8 @@ func mark_dead() -> void:
tw.tween_property(self, "rotation_degrees", 0.0, UiKit.motion(0.08))
_spawn_death_shards() # 碎裂退场(§4.6.4 语义硬规则:灰化+碎裂禁瞬间消失;§8.4 v2 冻结 5 块 0.45s)
_intent_charge = false
st._intent_visible = false # 死亡残位意图退位
_set_hp(0)
mo._intent_visible = false # 死亡残位意图退位
st.apply_hp(0)
if slot_row != null:
slot_row.visible = false # 状态行随 mark_dead 同拍隐藏
@@ -272,7 +189,7 @@ func mark_dead() -> void:
# 死亡碎块弹飞:立绘盒中心五块碎石抛物线散落(程序化色块——确定性散布,禁全局随机)
func _spawn_death_shards() -> void:
st._death_shards = DEATH_SHARD_N
_death_shards = DEATH_SHARD_N
var center := _portrait_rect().get_center()
for i in DEATH_SHARD_N:
_spawn_death_shard(i, center)
@@ -298,7 +215,7 @@ func _spawn_death_shard(i: int, center: Vector2) -> void:
# 演出探针出口(fx_probe——死亡碎裂/命中火花/闪白计数断言面)
func fx_probe() -> Dictionary:
return {"death_shards": st._death_shards, "hit_sparks": st._hit_sparks, "flash_count": st._flash_count}
return {"death_shards": _death_shards, "hit_sparks": _hit_sparks, "flash_count": _flash_count}
# 命中瞬间打击特效:立绘盒中心六枚短火花放射(程序化色块,确定性方向
@@ -312,7 +229,7 @@ func play_hit_impact() -> void:
## 单火花域(循环体提函数——放射弹道+渐隐+回收)
func _spawn_hit_spark(i: int, center: Vector2) -> void:
st._hit_sparks += 1
_hit_sparks += 1
var spark := ColorRect.new()
spark.color = UiKit.GOLD_LIT if i % 2 == 0 else UiKit.WARM
spark.size = Vector2(4, 3)
@@ -333,7 +250,7 @@ func is_dead() -> bool:
# 受击闪白(结算演出占位——§2.2 受击反馈;强度时长维持现行)
func flash_hit() -> void:
st._flash_count += 1
_flash_count += 1
if st._hp_flash_tw != null and st._hp_flash_tw.is_valid():
st._hp_flash_tw.kill()
st._hp_flash_tw = create_tween()
@@ -346,8 +263,7 @@ func _revive() -> void:
modulate = Color(1, 1, 1)
rotation_degrees = 0.0
if st._kind == "hero":
st._hero_state = HeroState.RESET # 强制回 idle 态(绕过同态早退)
set_hero_state("idle")
mo.reset_state()
if slot_row != null:
slot_row.visible = true
queue_redraw()
@@ -365,45 +281,24 @@ func _portrait_glyph() -> String:
# 立绘落位探针出口(站桩纹理非空断言面;缺位回退链保持的对照面)
func portrait_probe() -> Dictionary:
return {"has_tex": st._portrait_sprite != null and st._portrait_sprite.texture != null}
return {"has_tex": mo._portrait_sprite != null and mo._portrait_sprite.texture != null}
# ---- 主角三态切换(攻击演出→attack/受击→hit/结算回 idle;§8.4 帧数档挂点) ----
# 组合点注册(battle_stage_view _init——UiKit 常量 load_tex,缺位 null 不注册=该态保持现纹理)
# 组合点注册转调壳(签名外形零变)
func register_hero_state(state: String, tex: Texture2D) -> void:
if tex != null:
st._hero_tex[state] = tex
mo.register_hero_state(state, tex)
# 状态切换:现纹理换注册态(attack/hit);未注册态名或缺位 = 保持现纹理不炸。
# 非 idle 态自动短驻回归 idle(0.35s——演出一拍即回,结算收尾另有显式 idle 兜底)。
# 状态切换转调壳(签名外形零变——is_hero 由宿主 vitals 参判传入,早退序逐字在件内)
func set_hero_state(state: String) -> void:
var target: int = _HS_VALUE.get(state, -1)
if st._kind != "hero" or target == st._hero_state:
return
if not st._hero_tex.has(state) and state != "idle":
return
st._hero_state = target
if st._portrait_sprite != null and st._hero_tex.has(state):
st._portrait_sprite.texture = st._hero_tex[state]
if st._state_revert_tw != null and st._state_revert_tw.is_valid():
st._state_revert_tw.kill()
if state != "idle":
st._state_revert_tw = create_tween()
st._state_revert_tw.tween_interval(UiKit.motion(0.35))
st._state_revert_tw.tween_callback(_settle_hero_idle)
mo.apply_hero_state(state, st._kind == "hero")
func _settle_hero_idle() -> void:
st._hero_state = HeroState.IDLE
if st._portrait_sprite != null and st._hero_tex.has("idle"):
st._portrait_sprite.texture = st._hero_tex["idle"]
# 三态探针出口(fx_probe——当前态名+注册态集合)
# 三态探针出口(fx_probe——当前态名+注册态集合;读件投影外形零变)
func hero_state_probe() -> Dictionary:
return {"state": _HS_NAME.get(st._hero_state, "idle"), "registered": st._hero_tex.keys()}
return mo.state_probe()
func _portrait_color() -> Color:
@@ -420,6 +315,37 @@ func _pedestal_rect() -> Rect2:
return Rect2(18.0, box.y - 18.0, box.x - 36.0, 12.0)
# 护甲图标纹理路径(甲档键 → UiKit.TEX_ARMOR_*——显式 match 单点,禁动态拼键取常量;
# 未知键回退轻甲路径,由 load_tex exists 守卫接住)
func _armor_tex_path(armor: String) -> String:
match armor:
"ward":
return UiKit.TEX_ARMOR_WARD
"heavy":
return UiKit.TEX_ARMOR_HEAVY
_:
return UiKit.TEX_ARMOR_LIGHT
# 名字行素底牌矩形(_draw 与 name_line_probe 共用几何——单一来源防两处漂移):
# 牌高 26 = 护甲图标 24px(甲类 1× 禁缩放)+ 上下各 1px;牌底沿 = 地台顶(box.y-18)——
# 文字禁直接压地台(§6.1/§7.2 素底落法);牌宽 = 行宽(图标+间距+名字)+ 8,水平居中
func _name_plate_rect(f: Font, tex: Texture2D) -> Rect2:
var icon_w := 24.0
if tex == null:
icon_w = f.get_string_size(String(ARMOR_ICONS.get(st._armor, "◇")),
HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT).x
var name_w := f.get_string_size(st._display_name, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT).x
var total_w := icon_w + 3.0 + name_w
return Rect2((box.x - total_w - 8.0) * 0.5, box.y - 18.0 - 26.0, total_w + 8.0, 26.0)
# 名字行文字基线(牌内几何单点——图标 24px 垂直居中牌内〔中心=牌顶+13〕,14px 字形基线=中心+5
# (0.35h 规则按字号档推导 14×0.35≈4.9→5,T0 实测校验——迭代 46 卡B §7.4 提档))
func _name_baseline(plate: Rect2) -> Vector2:
return plate.position + Vector2(4.0, 18.0)
# 绘制内容矩形(世界局部;ui_struct_probe ⑥standee「不出框+下余量≥8px」断言读数面——
# 最低绘制元素 = 底座下缘)
func content_rect() -> Rect2:
@@ -438,8 +364,9 @@ func _draw() -> void:
## 地面域:椭圆落影(圣经 §6.1 地台落影压脚:方向稿椭圆 (0,0,0,70),压在地台
## 上缘周围;24 段多边形近似椭圆,中心=台面上方 6px、横半径=台宽一半)
## + 石板地台(v3.5——方向稿取色:板身/描边/顶缘受光线 2px——取色单点收 UiKit.STAND_STONE 族
## ;几何沿用现行底座位〔margin 18/高 12——按方向稿对齐〕)
## + 石板地台(§6.1 近景地台独立件:TEX_STAGE_PLAZA 源 48×12 九宫格横向拉伸、顶缘受光线 2px
## 不随拉伸变形、跨三关恒定;素材缺位回退程序化三段绘制〔板身 STAND_STONE/描边 PLAZA_EDGE/
## 顶缘线 BRICK——art-bible §4.1 换色合并①②改指目标色,几何同源 _pedestal_rect〕)
func _draw_ground(f: Font) -> void:
var sh_center := Vector2(box.x * 0.5, box.y - 24.0)
var sh_rx := (box.x - 36.0) * 0.5
@@ -450,26 +377,41 @@ func _draw_ground(f: Font) -> void:
pts.append(sh_center + Vector2(cos(ang) * sh_rx, sin(ang) * sh_ry))
draw_colored_polygon(pts, Color(0, 0, 0, 0.27))
var ped := _pedestal_rect()
draw_rect(ped, UiKit.STAND_STONE, true)
draw_rect(ped, UiKit.STAND_STONE_EDGE, false, 1.0)
draw_rect(Rect2(ped.position.x, ped.position.y, ped.size.x, 2.0), UiKit.STAND_STONE_RIM, true)
var plaza := UiKit.load_tex(UiKit.TEX_STAGE_PLAZA)
if plaza != null:
_draw_plinth_stretched(plaza, ped)
else:
draw_rect(ped, UiKit.STAND_STONE, true)
draw_rect(ped, UiKit.PLAZA_EDGE, false, 1.0)
draw_rect(Rect2(ped.position.x, ped.position.y, ped.size.x, 2.0), UiKit.BRICK, true)
## 血条域(直角递减):敌附顺位徽标(GOLD_BRIGHT ①②③);血条几何左 8 / 右 -8;
## 血量数字「当前/最大」居中压条(白字+深描边双通道——11px 于 10px 条带垂直居中;
## 数字=目标值即时刷新,条 tween 只管填充宽度;死残位 0/max 照常可读)
## 地台九宫格横向拉伸域(循环体提函数——左右 4px 端头切片 + 中段拉伸;纵向恒 12px 源档,
## 顶缘受光线 2px 落纵向不缩放区,工单 §5.2 工艺要点)
func _draw_plinth_stretched(tex: Texture2D, ped: Rect2) -> void:
draw_texture_rect_region(tex, Rect2(ped.position, Vector2(4.0, 12.0)), Rect2(0, 0, 4, 12))
draw_texture_rect_region(tex, Rect2(ped.position + Vector2(4.0, 0.0),
Vector2(ped.size.x - 8.0, 12.0)), Rect2(4, 0, 40, 12))
draw_texture_rect_region(tex, Rect2(ped.position + Vector2(ped.size.x - 4.0, 0.0),
Vector2(4.0, 12.0)), Rect2(44, 0, 4, 12))
## 血条域(直角递减):敌附顺位徽标(GOLD_BRIGHT ①②③,字号 FS_HINT——art-bible §7.4 清单外
## 11–13px 提档归档);血条几何左 8 / 右 -8;血条填充 = 危险红 HP_BAR_FILL(§4.2 语义物件——
## 不因敌我分色);条底 = HP_BAR_TRACK;血量数字「当前/最大」居中压条(白字+深描边双通道——11px 于
## 10px 条带垂直居中;数字=目标值即时刷新,条 tween 只管填充宽度;死残位 0/max 照常可读)
func _draw_hp_bar(f: Font) -> void:
var bar_x := 8.0
if st._kind == "enemy":
var order_txt := String("①②③"[clampi(st._order - 1, 0, 2)])
f.draw_string(get_canvas_item(), Vector2(bar_x, 12), order_txt,
HORIZONTAL_ALIGNMENT_LEFT, -1, 13, UiKit.GOLD_BRIGHT)
HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT, UiKit.GOLD_BRIGHT)
bar_x += 20.0
var bar_w := box.x - 8.0 - bar_x
draw_rect(Rect2(bar_x, 2, bar_w, 10), Color(0.10, 0.10, 0.11)) # 条底(引擎默认主题深底近似平移)
draw_rect(Rect2(bar_x, 2, bar_w, 10), UiKit.HP_BAR_TRACK) # 条底(值单点收 ui_kit)
var fill_w := bar_w * st.hp_value
if fill_w > 0.0:
draw_rect(Rect2(bar_x, 2, fill_w, 10), Color(0.41, 0.61, 0.91)) # 直角填充(默认主题蓝近似——直角递减)
draw_rect(Rect2(bar_x, 2, fill_w, 10), UiKit.HP_BAR_FILL) # 直角填充(危险红——§4.2 血条显式句)
var hp_txt := "%d/%d" % [st._hp_cur, int(st._hp_max)]
var hts: Vector2 = f.get_string_size(hp_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 11)
# 对比度垫牌(白 α.95 直压蓝条 2.67:1 违 §7.2;
@@ -482,7 +424,7 @@ func _draw_hp_bar(f: Font) -> void:
## 立绘域:贴图在位 = Sprite2D(set_portrait_texture 落位);缺位 = 字形回退
func _draw_portrait(f: Font) -> void:
if st._portrait_sprite != null and st._portrait_sprite.texture != null and st._portrait_sprite.visible:
if mo._portrait_sprite != null and mo._portrait_sprite.texture != null and mo._portrait_sprite.visible:
return
var prect := _portrait_rect()
var glyph := _portrait_glyph()
@@ -490,7 +432,7 @@ func _draw_portrait(f: Font) -> void:
# 坐标系注记:draw_set_transform 原点已置于 center——draw_string 的
# pos 是**变换后的局部坐标**,须传相对 center 的偏移(-w/2 水平居中、+0.35h 基线下移);
# 传绝对 center 会成双重偏移、把字形画到盒外。
draw_set_transform(center, deg_to_rad(st._glyph_rot), Vector2.ONE)
draw_set_transform(center, deg_to_rad(mo._glyph_rot), Vector2.ONE)
var ts := f.get_string_size(glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 42)
f.draw_string(get_canvas_item(), Vector2(-ts.x * 0.5, ts.y * 0.35), glyph,
HORIZONTAL_ALIGNMENT_LEFT, -1, 42, _portrait_color())
@@ -499,33 +441,39 @@ func _draw_portrait(f: Font) -> void:
## 意图徽标域(敌 only——立绘上半部居中悬浮,CENTER_TOP 74..98 等价):弹入 scale 动画
func _draw_intent_badge(f: Font) -> void:
if not st._intent_visible:
if not mo._intent_visible:
return
var icenter := Vector2(box.x * 0.5, 86.0)
draw_set_transform(icenter, 0.0, Vector2(st._intent_scale, st._intent_scale))
var its := f.get_string_size(st._intent_glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 15)
draw_set_transform(icenter, 0.0, Vector2(mo._intent_scale, mo._intent_scale))
var its := f.get_string_size(mo._intent_glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 15)
# 对比度垫牌(四色墨直压立绘纹理/暗舞台底 1.5-3.3:1 违 §7.2
# 「文字禁直压纹理底」;垫 PANEL_DATA 素牌后 atk 5.3/buf 4.8/debuff 4.7/charge 4.9 全 ≥4.5:1;
# atk/debuff 零换墨、buf 换 HERO_BLUE_DEEP、charge 换 GOLD_INK;随弹入 transform 同步缩放)
draw_rect(Rect2(-its.x * 0.5 - 4.0, -its.y * 0.75, its.x + 8.0, its.y * 1.1), UiKit.PANEL_DATA, true)
f.draw_string(get_canvas_item(), Vector2(-its.x * 0.5, its.y * 0.35), st._intent_glyph,
HORIZONTAL_ALIGNMENT_LEFT, -1, 15, st._intent_col) # 局部坐标(同上注记——双重偏移修复)
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
f.draw_string(get_canvas_item(), Vector2(-its.x * 0.5, its.y * 0.35), mo._intent_glyph,
HORIZONTAL_ALIGNMENT_LEFT, -1, 15, mo._intent_col) # 局部坐标(同上注记——双重偏移修复)
## 底座下 护甲类型图标+名字 常驻行(敌 only——AC 数值不展示,类型图标化)
## 名字行域(敌 only——AC 数值不展示,类型图标化):整行垫 HP_BAR_BACK 素底牌(§6.1/§7.2
## 「文字禁直接压地台」素底落法——牌底沿 = 地台顶,几何单点 _name_plate_rect);护甲图标 =
## 24×24 PNG(甲类 1×,缺位回退字形,色随甲档亮档族);名字 = 暖白(暗底双色墨制)
func _draw_armor_name_line(f: Font) -> void:
var armor_txt := String(ARMOR_ICONS.get(st._armor, "◇"))
var col: Color = UiKit.WARM_FAINT # 轻甲 = 暖白弱化(v3.5 暗底双色墨制)
match st._armor:
"ward":
col = UiKit.HERO_BLUE_BRIGHT # 魔法护甲 = 小人蓝亮档
"heavy":
col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档
var name_w := f.get_string_size(st._display_name, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x
var armor_w := f.get_string_size(armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x
var total_w := armor_w + 3.0 + name_w
var baseline := Vector2((box.x - total_w) * 0.5, box.y - 1.0 - STANDEE_FOOT_MARGIN) # 纵向余量纪律
f.draw_string(get_canvas_item(), baseline, armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12, col)
f.draw_string(get_canvas_item(), baseline + Vector2(armor_w + 3.0, 0), st._display_name,
HORIZONTAL_ALIGNMENT_LEFT, -1, 12, UiKit.WARM) # v3.5 暗底双色墨制
var tex := UiKit.load_tex(_armor_tex_path(st._armor))
var plate := _name_plate_rect(f, tex)
draw_rect(plate, UiKit.HP_BAR_BACK, true)
var baseline := _name_baseline(plate)
var icon_w := 24.0
if tex != null:
draw_texture_rect(tex, Rect2(baseline.x, plate.position.y + 1.0, 24.0, 24.0), false)
else:
var armor_txt := String(ARMOR_ICONS.get(st._armor, "◇"))
icon_w = f.get_string_size(armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT).x
var col: Color = UiKit.WARM_FAINT # 轻甲 = 暖白弱化(v3.5 暗底双色墨制)
match st._armor:
"ward":
col = UiKit.HERO_BLUE_BRIGHT # 魔法护甲 = 小人蓝亮档
"heavy":
col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档
f.draw_string(get_canvas_item(), baseline, armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT, col)
f.draw_string(get_canvas_item(), baseline + Vector2(icon_w + 3.0, 0.0), st._display_name,
HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT, UiKit.WARM) # v3.5 暗底双色墨制
+128
View File
@@ -0,0 +1,128 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 站桩资产与演出态件(StandeeBody 拆件——单一变化原因=美术素材落位规则与演出编排设计):
## 站桩的资产挂点与一次性演出——立绘精灵落位与换装、主角三态注册与 0.35s 短驻回归、意图徽标弹入动画。
## 无 class_name(类名缓存面纪律)——经宿主 standee_body preload 引用(mo 实例);
## 两个属性 setter 经 _owner 回引触发宿主重绘语义逐字;tween 经 _owner 创建(同一 Node 生命绑定)。
extends RefCounted
enum HeroState { IDLE, RESET, ATTACK, HIT } # 主角三态+复位哨兵(外部 String 契约=register/set 形参零变化)
const _HS_NAME := {HeroState.IDLE: "idle", HeroState.RESET: "reset", HeroState.ATTACK: "attack", HeroState.HIT: "hit"}
const _HS_VALUE := {"idle": HeroState.IDLE, "reset": HeroState.RESET, "attack": HeroState.ATTACK, "hit": HeroState.HIT}
var _owner: Node2D = null
var _portrait_sprite: Sprite2D = null # 资产挂点(null = 未落位,字形在位)
var _portrait_tex: Texture2D = null # 落位纹理引用(set_portrait_texture 装载;字形回退判据)
# 主角三态切换:state 名 → 纹理(battle_stage_view 组合点注册);缺态=保持现纹理
# (缺位回退链延伸——三态素材缺任一不炸不空)。敌站桩不注册不消费。
var _hero_tex := {}
var _hero_state: int = 0
var _state_revert_tw: Tween
var _intent_glyph := ""
var _intent_col := Color.WHITE
var _intent_visible := false
var _intent_scale := 1.0: # 意图弹入动画参数
set(v):
_intent_scale = v
_owner.queue_redraw()
var _glyph_rot := 0.0: # 立绘前倾角(贴图态无此动画)
set(v):
_glyph_rot = v
_owner.queue_redraw()
func _init(owner: Node2D) -> void:
_owner = owner
# 资产挂点:tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形。
# flip = 立绘朝向翻转(素材基准朝左时传 true 呈现朝右)。盒高 96px = 48px 立绘源整数 2×
# (圣经 §8.2 禁非整数缩放)——源换代时随源重算。(box = 宿主冻结 var,由宿主传入)
func set_portrait_texture(tex: Texture2D, flip: bool, box: Vector2) -> void:
if tex == null:
if _portrait_sprite != null:
_portrait_sprite.visible = false
_owner.queue_redraw()
return
_portrait_tex = tex
if _portrait_sprite == null:
_portrait_sprite = Sprite2D.new()
_portrait_sprite.centered = false
_portrait_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST # 像素帧放大防糊
_owner.add_child(_portrait_sprite)
_portrait_sprite.texture = tex
_portrait_sprite.flip_h = flip
_portrait_sprite.visible = true
_place_portrait(box)
func _place_portrait(box: Vector2) -> void:
# 立绘盒 = (0,44)-(box.x, box.y-36);48 源 ×2 整数档居中
if _portrait_sprite == null or _portrait_tex == null:
return
var box_h := box.y - 44.0 - 36.0
var w := _portrait_tex.get_width() * 2.0
var h := _portrait_tex.get_height() * 2.0
_portrait_sprite.scale = Vector2(2.0, 2.0) # 整数 2×(C5)
_portrait_sprite.position = Vector2((box.x - w) * 0.5, 44.0 + (box_h - h) * 0.5)
# 组合点注册(battle_stage_view _init——UiKit 常量 load_tex,缺位 null 不注册=该态保持现纹理)
func register_hero_state(state: String, tex: Texture2D) -> void:
if tex != null:
_hero_tex[state] = tex
# 状态切换:现纹理换注册态(attack/hit);未注册态名或缺位 = 保持现纹理不炸。
# 非 idle 态自动短驻回归 idle(0.35s——演出一拍即回,结算收尾另有显式 idle 兜底)。
#(is_hero = 宿主 vitals._kind == "hero" 参判传入——三连早退条件序逐字)
func apply_hero_state(state: String, is_hero: bool) -> void:
var target: int = _HS_VALUE.get(state, -1)
if not is_hero or target == _hero_state:
return
if not _hero_tex.has(state) and state != "idle":
return
_hero_state = target
if _portrait_sprite != null and _hero_tex.has(state):
_portrait_sprite.texture = _hero_tex[state]
if _state_revert_tw != null and _state_revert_tw.is_valid():
_state_revert_tw.kill()
if state != "idle":
_state_revert_tw = _owner.create_tween()
_state_revert_tw.tween_interval(UiKit.motion(0.35))
_state_revert_tw.tween_callback(_settle_hero_idle)
func _settle_hero_idle() -> void:
_hero_state = HeroState.IDLE
if _portrait_sprite != null and _hero_tex.has("idle"):
_portrait_sprite.texture = _hero_tex["idle"]
# 复位强制绕同态早退两拍封装(原 _revive 内 hero 分支——先置 RESET 哨兵再回 idle)
func reset_state() -> void:
_hero_state = HeroState.RESET # 强制回 idle 态(绕过同态早退)
apply_hero_state("idle", true)
# 意图三档动作动画(§4.6.4 三档:①普通攻击=零表示〔set_pending 早退〕
# ②特殊准备=徽标弹入+二次弹跳(高举语义,运动手段 §2)③重击/硬化=缓推蓄力+定格微顿+回落;
# 三档运动签名互异,灰度/剪影层可辨)。摇定时一次性短动画,随后静帧保持。
#(charge = 宿主 _intent_charge 分档布尔参判传入)
func play_intent_motion(charge: bool) -> void:
var tw := _owner.create_tween()
tw.tween_property(self, "_intent_scale", 1.6, UiKit.motion(0.10))
tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.16))
tw.parallel().tween_property(self, "_glyph_rot", 4.0, UiKit.motion(0.10))
tw.tween_property(self, "_glyph_rot", 0.0, UiKit.motion(0.14))
if charge:
tw.tween_property(self, "_intent_scale", 1.22, UiKit.motion(0.28)).set_trans(Tween.TRANS_SINE) # ③蓄力缓推
tw.tween_interval(UiKit.motion(0.40)) # 定格微顿
tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.18))
else:
tw.tween_property(self, "_intent_scale", 1.26, UiKit.motion(0.11)) # ②二次弹跳(高举)
tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.13))
# 三态探针出口(fx_probe——当前态名+注册态集合;宿主 hero_state_probe 读件投影,外形零变)
func state_probe() -> Dictionary:
return {"state": _HS_NAME.get(_hero_state, "idle"), "registered": _hero_tex.keys()}
@@ -0,0 +1 @@
uid://cdfo2jgp2w7ei
+36
View File
@@ -0,0 +1,36 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 站桩身份与命值态件(StandeeBody 拆件——单一变化原因=战斗结算快照投影的字段形态):
## 站桩是谁、活没活、血量几何的唯一账面——身份五元组(种类/名/id/护甲/顺位)+存活位+血量账与两条血量系 tween 槽。
## 无 class_name(类名缓存面纪律)——经宿主 standee_body preload 引用(st 实例名沿用);
## hp_value 属性 setter 经 _owner 回引触发宿主重绘语义逐字。
extends RefCounted
var _owner: Node2D = null
var _kind := "enemy"
var _display_name := ""
var _enemy_id := ""
var _armor := "light"
var _order := 1
var _dead := false
var _hp_max := 1.0
var _hp_cur := 0 # 血量数字显示面(HUD框架 验收「血量数字可读」)
var hp_value := 1.0: # 血条值 0..1(直角递减——tween_property 目标,逐行刷新重启不累积)
set(v):
hp_value = clampf(v, 0.0, 1.0)
_owner.queue_redraw()
var _hp_tween: Tween
var _hp_flash_tw: Tween
func _init(owner: Node2D) -> void:
_owner = owner
# 血条短 tween 递减(直角递减占位);逐行刷新时重启 tween 不累积
func apply_hp(hp: int) -> void:
_hp_cur = clampi(hp, 0, int(_hp_max))
var target := float(_hp_cur) / _hp_max
if _hp_tween != null and _hp_tween.is_valid():
_hp_tween.kill()
_hp_tween = _owner.create_tween()
_hp_tween.tween_property(self, "hp_value", target, UiKit.motion(0.15))
@@ -0,0 +1 @@
uid://bjd7t5nmepbey
+12 -4
View File
@@ -185,12 +185,13 @@ const TIP_BG := Color(0.16, 0.12, 0.09, 0.96) # tooltip 深底
const STAGE_BG := Color8(62, 52, 42) # B 区舞台暗底(方向稿 render_battle_screen——battle_stage_view 面板底)
const BEAM_LIGHT := Color8(255, 236, 200) # B 区顶光柱暖光(用时乘 alpha 渐隐——battle_stage_view 光柱层)
const STAND_STONE := Color8(150, 130, 100) # 站桩石板地台·板身(standee_body 方向稿取色)
const STAND_STONE_EDGE := Color8(90, 76, 58) # 站桩石板地台·描边
const STAND_STONE_RIM := Color8(196, 172, 132) # 站桩石板地台·顶缘受光线
const PLAZA_EDGE := Color8(76, 64, 56) # 小石台描边(art-bible §4.1 换色合并②合并目标——贴图已落此值,缺位回退同值)
const CAMP_HEADER_BAND := Color8(110, 69, 38) # 营地柜台头带深棕 #6E4526(4-camp.png 方向稿取色;对齐 demo --wood-deep)
const CAMP_ROW_PLATE := Color8(240, 230, 206) # 营地行板暖米(同上)
const CAMP_BADGE_PANEL := Color8(40, 32, 24) # 营地钥石徽暗底(同上)
const HP_BAR_BACK := Color8(40, 34, 32) # 迷你血条深底 #282220(兼血数垫牌)
const HP_BAR_BACK := Color8(40, 34, 32) # 迷你血条深底 #282220(兼血数垫牌;站桩名字行素底同源——§6.1/§7.2 文字禁压地台)
const HP_BAR_FILL := Color8(168, 50, 31) # 血条填充·危险红 #A8321F(§4.2 语义物件显式落法——不因敌我分色、不因关卡换族;蓝族 #689CE8 已登记违例退役)
const HP_BAR_TRACK := Color(0.10, 0.10, 0.11) # 血条条底(视图原裸值收口单点——非色板新增,现渲染值原样入册)
# BBCode 十六进制(RichTextLabel 用;与上同值)
const HEX_GOLD := "f0a828"
@@ -244,6 +245,13 @@ const TEX_ENEMY_DRIDER := "res://assets/art/character/character_enemy_drider.png
const TEX_ENEMY_DUERGAR := "res://assets/art/character/character_enemy_duergar.png" # 灰矮人
const TEX_ENEMY_HOOK_HORROR := "res://assets/art/character/character_enemy_hook_horror.png" # 钩爪恐魔
const TEX_ENEMY_DUERGAR_KING := "res://assets/art/character/character_enemy_duergar_king.png" # 灰矮人石君王
# 近景地台独立件(§6.1——源 48×12 九宫格横向拉伸、顶缘受光线 2px 不随拉伸变形;跨三关恒定不吃 wall_ambient tint)
const TEX_STAGE_PLAZA := "res://assets/art/scene/scene_stage_plaza.png"
# 护甲类型图标三键(art-bible §5.3 工单:对象键 = 甲档变体;源 24×24 显示 24px = 1× 甲类整数倍;
# 缺位 load_tex→null→字形回退,冒烟安全)
const TEX_ARMOR_LIGHT := "res://assets/ui/icons/ui_armor_light.png"
const TEX_ARMOR_WARD := "res://assets/ui/icons/ui_armor_ward.png"
const TEX_ARMOR_HEAVY := "res://assets/ui/icons/ui_armor_heavy.png"
# 框材组纹理槽(♻ 复用件量化改色入包,产物 = tools/quantize_ui_skin.py,
# 全 CC0;缺位 = load_tex null → 程序化回退原路径,冒烟安全)
@@ -466,7 +474,7 @@ const PANEL_DEFAULT := true # modal_shell 面板哨兵(非 StyleBox 实例
# 面板哨兵解析(六参收拢五参——三态:null=无面板〔原 with_panel=false〕/PANEL_DEFAULT=默认样式盒面板
# 〔原 with_panel=true+panel_sb=null〕/StyleBox 实例=自定义样式盒面板〔原双参形态〕;
# 判别用 is StyleBox——StyleBox 与布尔哨兵直接 != 属异类操作数违例)
static func _resolve_shell_panel(panel_sb) -> PanelContainer:
static func _resolve_shell_panel(panel_sb) -> PanelContainer: # type-hint: ignore(三态哨兵 bool/null/StyleBox 异构形参)
if panel_sb == null:
return null
var panel := PanelContainer.new()
+1 -1
View File
@@ -136,7 +136,7 @@ func _ready() -> void:
# ---- ⑥ 主角受击/死亡路由(battle_stage 主角走完整呈现链的断言) ----
var db2 = load("res://core/config_db.gd").new()
if db2.load_tables("res://data/tables", false):
var stage2 = BattleStageView.new(db2)
var stage2 = BattleStageView.new(db2, db2.presentation.battle_layout)
add_child(stage2)
var flash_before: int = int(stage2.hero_standee.fx_probe().get("flash_count", 0))
stage2.on_step_row_fx({"type": "damage", "target": "hero", "killed": false})
+3 -3
View File
@@ -37,14 +37,14 @@ func _ready() -> void:
# 等关卡画面出图(choices_presented 异步到达——最多 3s 轮询)
for i in 60:
await get_tree().process_frame
if main.level_screen != null and main.level_screen.visible:
if main.ui.modals.level_screen != null and main.ui.modals.level_screen.visible:
break
if main.level_screen == null or not main.level_screen.visible:
if main.ui.modals.level_screen == null or not main.ui.modals.level_screen.visible:
_fail("关卡画面未出现(函数直调中间态路径)")
return
for i in 5:
await get_tree().process_frame
main.level_screen.press_candidate(0)
main.ui.modals.level_screen.press_candidate(0)
for i in 20:
await get_tree().process_frame
var st = main.run_scope.settlement
+6 -5
View File
@@ -23,12 +23,12 @@ func _ready() -> void:
add_child(main)
await get_tree().process_frame
ProbeUiUtil.isolate_save(main, "smoke_layout_probe", true, false, true)
main.debug_overlay.seed_edit.text = "42" # 种子钉死(确定性禁依赖默认随机分支)
main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(确定性禁依赖默认随机分支)
main._start_adventure_from_menu()
for i in range(8):
await get_tree().process_frame
# 进战斗屏——三区(B 战斗画面/C 角色信息/D 扫雷图)同屏常驻,菜单屏不构成断言面
main.level_screen.press_candidate(0)
main.ui.modals.level_screen.press_candidate(0)
for i in range(20):
await get_tree().process_frame
for vp in VIEWPORTS:
@@ -57,8 +57,9 @@ func _check_one(label: String, main: Node) -> void:
if vr.size != Vector2(VIEWPORTS[0]["size"]) and vr.size.y < 1.0:
_problems.append("[%s] 逻辑视口退化 %s" % [label, vr.size])
var rects := {}
var battle_ui: Object = main.get("ui").battle # 注册面战区域容器(字段名与三区 key 同名——合同键即字段名)
for key in ["stage", "info_panel", "board_zone"]:
var ctl: Control = main.get(key)
var ctl: Control = battle_ui.get(key)
if ctl == null:
_problems.append("[%s] %s = null(三区未装配)" % [label, key])
return
@@ -73,7 +74,7 @@ func _check_one(label: String, main: Node) -> void:
# battle_root 内层根下的全幅 ColorRect)这一缺陷类不在本探针拦截面内,探针对此仍全绿。
# 要拦它得改为递归遍历全子树找重叠,那是另一套判据(且极易误报容器父子关系),
# 本探针不设此判据。
var bv: Control = main.board_view
var bv: Control = main.ui.battle.board_zone.board_view
if bv == null:
_problems.append("[%s] board_view = null" % label)
return
@@ -89,6 +90,6 @@ func _check_one(label: String, main: Node) -> void:
if rects[keys[i]].intersects(rects[keys[j]]):
_problems.append("[%s] 顶层区重叠 %s×%s(%s ∩ %s)"
% [label, keys[i], keys[j], rects[keys[i]], rects[keys[j]]])
var cell: float = main.board_view.get("cell_size")
var cell: float = main.ui.battle.board_zone.board_view.get("cell_size")
if cell < MIN_CELL:
_problems.append("[%s] 格子边长 %.1fpx < 可读门槛 %.0fpx" % [label, cell, MIN_CELL])
+1 -1
View File
@@ -130,7 +130,7 @@ static func open_camp_via_savesel(main, tag: String) -> bool:
if FileAccess.file_exists(sp):
DirAccess.remove_absolute(sp)
main._open_save_select()
var new_btn: Button = main.save_select_overlay._new_btns.get(1)
var new_btn: Button = main.ui.modals.save_select_overlay._new_btns.get(1)
if new_btn == null:
return false
new_btn.pressed.emit() # 真实按钮信号路径 → _on_save_select_new → create_slot+open_camp
+21 -21
View File
@@ -50,10 +50,10 @@ func _ready() -> void:
# 其余任意两屏同时为真=互斥失守。
func _screen_seals() -> Dictionary:
return {
"菜单": main._start_overlay != null and main._start_overlay.visible,
"营地": main.camp_overlay != null and main.camp_overlay.visible,
"菜单": main.ui.screens.start_overlay != null and main.ui.screens.start_overlay.visible,
"营地": main.ui.modals.camp_overlay != null and main.ui.modals.camp_overlay.visible,
"冒险": main.run_scope != null and main.run_scope.is_adventure,
"终局": main._end_overlay != null and main._end_overlay.visible,
"终局": main.ui.modals.end_overlay != null and main.ui.modals.end_overlay.visible,
}
@@ -101,7 +101,7 @@ func _frames(n: int = 2) -> void:
func _step_boot_menu() -> void:
_expect_exclusive("①boot", "菜单")
_expect_adventure_seal("①boot", false)
if main.debug_overlay != null and main.debug_overlay.visible:
if main.ui.modals.debug_overlay != null and main.ui.modals.debug_overlay.visible:
_problems.append("①boot:调试浮窗 boot 自动开(菜单优先失守)")
@@ -117,10 +117,10 @@ func _step_menu_to_camp() -> void:
# ---- ③ 营地 → 菜单(回退路径:营地关闭发自覆盖层自身信号)----
func _step_camp_back_to_menu() -> void:
if main.camp_overlay == null:
if main.ui.modals.camp_overlay == null:
_problems.append("③营地→菜单:营地覆盖层句柄缺失")
return
main.camp_overlay.closed.emit()
main.ui.modals.camp_overlay.closed.emit()
await _frames()
main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点)
await _frames()
@@ -133,22 +133,22 @@ func _step_camp_back_to_menu() -> void:
func _step_save_select_open_back() -> void:
ProbeUiUtil.isolate_save(main, "smoke_screen_flow_savesel", false, false, true)
main.save_manager.slots.attach_slot_template("user://smoke_screen_flow_savesel_slot_%d.json")
var esb: Button = ProbeUiUtil.find_button(main._start_overlay, "进入存档", "prefix")
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
if esb == null:
_problems.append("[savesel] mn3 进入存档钮缺失(卡② 交付物)")
return
esb.pressed.emit() # 真实按钮信号路径 → _open_save_select(卡② mn3 点击链——卡③ 直调中间态就此闭环)
await get_tree().process_frame
var seals := _screen_seals()
if not main.save_select_overlay.visible or not seals_check_single(seals):
_problems.append("[savesel] sl10 开层后屏级互斥失守(选档=%s)" % str(main.save_select_overlay.visible))
var back_btn: Button = ProbeUiUtil.find_button(main.save_select_overlay, "返回", "prefix")
if not main.ui.modals.save_select_overlay.visible or not seals_check_single(seals):
_problems.append("[savesel] sl10 开层后屏级互斥失守(选档=%s)" % str(main.ui.modals.save_select_overlay.visible))
var back_btn: Button = ProbeUiUtil.find_button(main.ui.modals.save_select_overlay, "返回", "prefix")
if back_btn == null:
_problems.append("[savesel] sl10 返回钮缺失(页壳顶条)")
else:
back_btn.pressed.emit()
await get_tree().process_frame
if main.save_select_overlay.visible or not main._start_overlay.visible:
if main.ui.modals.save_select_overlay.visible or not main.ui.screens.start_overlay.visible:
_problems.append("[savesel] sl10 返回后选档未隐藏或开始层未恢复(关闭链失守)")
# ESC 语境:再开选档→合成 ESC 键→层不关、流程零扰动(ESC 不属选档语境——零实现断言)
main._open_save_select()
@@ -159,7 +159,7 @@ func _step_save_select_open_back() -> void:
ev.pressed = true
main.get_viewport().push_input(ev, true)
await get_tree().process_frame
if not main.save_select_overlay.visible:
if not main.ui.modals.save_select_overlay.visible:
_problems.append("[savesel] sl10 ESC 关闭了选档层(选档语境 ESC 应不响应——卡⑤ 范围外)")
main._on_save_select_closed()
await get_tree().process_frame
@@ -171,7 +171,7 @@ func seals_check_single(f: Dictionary) -> bool:
for k in f:
if bool(f[k]):
on += 1
if main.save_select_overlay.visible:
if main.ui.modals.save_select_overlay.visible:
on += 1 # 选档自身计入(调用点已开层)
return on == 1
@@ -184,25 +184,25 @@ func _step_menu_to_adventure() -> void:
_expect_adventure_seal("④菜单→冒险", true)
if main.run_scope != null and main.run_scope.adventure == null:
_problems.append("④菜单→冒险:冒险实例未建立(入口停点未达)")
if main.level_screen != null and not main.level_screen.visible:
if main.ui.modals.level_screen != null and not main.ui.modals.level_screen.visible:
_problems.append("④菜单→冒险:入口停点未落在关卡地图屏(level_screen 不可见)")
# ---- ⑤ 冒险内屏切换:关卡地图屏 ⇄ 战斗屏(屏内两态,屏级仍独占)----
func _step_level_to_battle_and_back() -> void:
if not main.level_screen.visible:
if not main.ui.modals.level_screen.visible:
_problems.append("⑤冒险内切换:前置不在关卡地图屏")
return
main._exit_level_screen()
await _frames()
if main.level_screen.visible:
if main.ui.modals.level_screen.visible:
_problems.append("⑤冒险内切换:退出关卡屏后 level_screen 仍可见")
if main._battle_root != null and not main._battle_root.visible:
if main.ui.screens.battle_root != null and not main.ui.screens.battle_root.visible:
_problems.append("⑤冒险内切换:战斗屏未接手(battle_root 不可见)")
_expect_exclusive("⑤冒险内切换(关卡→战斗)", "冒险")
main._enter_level_screen()
await _frames()
if not main.level_screen.visible:
if not main.ui.modals.level_screen.visible:
_problems.append("⑤冒险内切换:回到关卡地图屏失败")
_expect_exclusive("⑤冒险内切换(战斗→关卡)", "冒险")
@@ -215,16 +215,16 @@ func _step_abandon_to_end() -> void:
main._on_abandon_requested()
await _frames(4)
_expect_exclusive("⑥放弃→终局", "终局")
if main._end_overlay != null and not main._end_overlay.visible:
if main.ui.modals.end_overlay != null and not main.ui.modals.end_overlay.visible:
_problems.append("⑥放弃→终局:终局覆盖层未开")
# ---- ⑦ 终局 → 菜单(终局出口返回开始界面)----
func _step_end_back_to_menu() -> void:
if main._end_overlay == null or not main._end_overlay.visible:
if main.ui.modals.end_overlay == null or not main.ui.modals.end_overlay.visible:
_problems.append("⑦终局→菜单:前置不在终局")
return
main._end_overlay.continue_pressed.emit()
main.ui.modals.end_overlay.continue_pressed.emit()
await _frames(3)
_expect_exclusive("⑦终局→菜单", "菜单")
if main.run_scope != null and main.run_scope.is_adventure:
Loaded 100 of 191 files, more files were not shown because too many files have changed in this diff. Show more