feat(slice): 敌人技能池批收编——废确定性行为环改技能池+开轮摇号(冒烟 263 项全绿)
- 机制:每敌种技能池(池模板/实例池/技能行三层),开轮摇号定拍(behavior_roll 第五流, 每敌子流 scope=spawn_index×1000000+摇号序,单敌轨迹独立复放);两道闸 = 全技能各自 冷却(普攻恒 0 锁死)+ 保底 M=4;死亡即不动手改摇定作废不进账;拍结构保留 - 数据:新增 skills.tres+skill_table.gd(五行+保底阈值)/skill_lottery.gd 独立抽签器; enemy.tres 六行 pattern→pool(9/1·6/2/2·6/2/2 provisional);status_effects 七→九行 (empower/fortify——enemy_defense 敌方防御池新词汇);settlement effect_pool_map 增两键 - core:rng_service 增流+open_scope(salt 0x85EBCA7758C24E15,黄金向量 m=42 scope0= 426657203655691839);unit_container 重写(实例池/冷却/待发/保底);battle_orchestrator 开轮摇号编排+三乘法链+施放行 cast+敌方防御池乘区;config_db 挂技能表+池跨表校验 - 呈现:快照 pending_* 三字段+敌方增益投影(beat/ring 拆除);敌卡摇定动画位(普攻零 表示/攻击系⚔/增益系✦);明细具名技能+施放行;状态槽九行全映射 - smoke 211→263:六项验收(回放逐位/单敌独立复放/EV 对锚/死亡作废/空池不可能/加载拒绝) - 文档:敌人系统 v0.5/战斗结算 v0.11/增益状态 v0.5/难度曲线与Boss v0.6/战斗表现 v0.3/ HUD框架 v0.4/battle.md 改型注记/registry(skill 新条目+四条目修订+术语七词条)/ 06-待办清单收编;active.md entry 60 - EV 实测(×5000 拍):地精 1.1056(保底闸抬升,登记难度曲线重锚)/熊地精 0.9998/酋长 1.0236
This commit is contained in:
1 parent
d76a023fa9
commit
8ca3da669b
35 files changed
+1309
-356
No files matched your search
@@ -479,3 +479,25 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s
|
||||
**表实一致注记(对 registry reward_table 条目)**:十一行逐字段一致;两处切片级补全登记——① 负向 heal/blast 行 registry 未定 reward_id,切片取语义名 `heal_backlash` / `ambush_double`;② blast 负向加层行 effect_id 恒空(registry flat 行口径)+ power 校验例外(power 不消费——伏击加层为独立兑付形态,见战斗结算 GDD 兑付形态表)。
|
||||
|
||||
**待拍板(本批登记)**:① 关卡奖励点节点内容(功能道具渠道 P1——归 关卡节点地图 定稿批);② 已开格类型徽章与极性视觉(批 2——纯呈现);③ "剩余未探索安全区域"计数呈现口径(切片现无该计数器,随批 2)。
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-16 敌人技能池批(批 1+2+3 合并落地——设计权威 docs/proposal-skill-pool.md v1.0;文档面 v0.5/v0.11/v0.5/v0.6/v0.3/v0.4 同批)
|
||||
|
||||
**机制**:确定性出招剧本(行为环:拍枚举/环指针/同种锁步/heavy_multiplier)整体退役,改技能池 + 开轮摇号——每轮轮次开始排除冷却中技能、按行序逐敌摇定本拍动作(behavior_roll 封闭流,每敌子流 scope = spawn_index×1000000+摇号序),战斗段按技能行兑现(伤害技 = 三乘法链〔基线×倍率→d20 掷点→甲效→伏击后置→池乘区→max(1,·)〕,增益技 = 施放行 cast + 状态授予〔0 伤害不掷不折不兑伏击〕);两道闸 = 全技能各自冷却(普攻恒 0 锁死)+ 保底 M=4(连续 4 拍执行普攻 → 下拍必出特殊技、不受冷却限制);死亡即不动手改"摇定后兑现前死亡 → 作废不进账";拍结构保留(每结算每存活敌恰行动一次/行序/先作用后推进)。
|
||||
|
||||
**数据面**:新增 `skills.tres` + `skill_table.gd`(五行:普攻/突刺×1.5 cd2/蓄力 cd3/重击×2 cd3/硬化 cd4;pity_threshold=4;普攻倍率/冷却域校验锁死);`enemy.tres` 六行 pattern/heavy_multiplier → pool(地精 9/1 · 熊地精 6/2/2 · 酋长 6/2/2,全部 provisional);`status_effects.tres` 七→九行(empower 蓄力 +100 enemy_output 池 / fortify 硬化 +100 **enemy_defense 敌方防御池新词汇**——duration 2 = 1 个有效增益拍〔先兑付后衰减含施放当轮〕);`settlement.tres` effect_pool_map 增 empower/fortify 两键。
|
||||
|
||||
**core 面**:`rng_service.gd` 增 behavior_roll 流(salt 0x85EBCA7758C24E15,黄金向量 m=42 scope0=426657203655691839 见冒烟;open_scope 显式 scope 取号门 + BEHAVIOR_SUBSTREAM_STRIDE);新增 `core/skill_lottery.gd`(独立抽签器——输入可摇条目+子流、输出 skill_id,D9 封装纪律);`unit_container.gd` 重写(实例池快照/冷却递减/待发动作/保底计数;query_beat/query_attack/advance/steps/pattern 退役);`battle_orchestrator.gd` 开轮摇号编排 `_roll_round_actions()`(首轮同样摇、轮内刷新不重摇)+ 战斗段技能行兑现 + 主角攻击增敌方防御池乘区(Σ三分量后 ×(100+clamp(enemy_defense))/100);`status_container.apply_effect` 增 duration_override(技能行 buff_duration 覆盖);`config_db.gd` 挂技能表 + validate_skill_pool_links 跨表校验(悬空致命/每池恰一普攻/增益 ≤1)。
|
||||
|
||||
**呈现面(批 2)**:`presentation_snapshot` 敌人数组字段定稿(beat/ring_size/ring_index 拆除 → pending_skill/pending_type/pending_name + effects 敌方增益投影;池与权重不公开);`enemy_card_view` 环点拆除(护甲行常驻)、意图徽标 = 摇定动画占位(普攻零表示 —/攻击系 ⚔/增益系 ✦);`battle_log_view` 施放行 ✦ 格式化、伤害行具名技能;`status_slot_row` 图标键补 empower ⇧ / fortify ▣(九行全映射);`settlement_snapshot` 敌方 beat 键 → pending_skill;damage_probe 拍名重建改 pending_skill 口径。
|
||||
|
||||
**smoke(211→263 项)**:行为环断言全部改型(行相位 cast、fresh 池采样、削弱触底/AC12 采样改多种子扫描);新增 `_smoke_skill_pool` 六项验收(proposal §8):数据面(池模板/普攻锁死/空池不可能)、同种子回放逐位复现 + 单敌轨迹独立复放(D3 多实例逐轮记录 vs 只喂该敌子流的独立重放器逐位对照)、EV 抽样对锚(×5000 拍)、死亡打断作废(D2 特殊技种子 + hero_atk 999 先手击杀零行动行 + 不进账)、加载校验拒绝(悬空/池构成/普攻锁死/倍率域/权重域/空池)。
|
||||
|
||||
**验证(本批实跑)**:冒烟 **263 项检查 0 失败**;四探针 ui_probe / ui_tune_probe / ui_layout_probe / ui_click_probe(种子 7、23)全 PASS;damage_probe(boss/level 全 PASS 含施放行)、defense_scan、monte_carlo 工具零脚本错误。
|
||||
|
||||
**EV 抽样读数(×5000 拍,含闸长期速率 vs 静态锚)**:地精 1.1056(静态 1.05——保底闸把突刺率从自然 10% 抬到 ~22%,登记难度曲线重锚)/ 熊地精 0.9998(结构守恒恒 1)/ 酋长 1.0236。
|
||||
|
||||
**文档/registry 同批(批 3)**:敌人系统 v0.5(技能池章节重写)/ 战斗结算 v0.11(随机口径三处/三乘法链/施放行/敌方防御池/AC 措辞四处)/ 增益状态 v0.5(九行 + 敌方防御池;顺手清偿 v0.4 挂起的 sapped 文档同步)/ 难度曲线与Boss v0.6(曲线纪律/Boss 专属池/技能池投值表 + EV 实测登记)/ 战斗表现 v0.3 / HUD框架 v0.4 / 数据配置(敌人表措辞 + 技能表行)/ 03-内容规模 / 02-系统清单 / 关卡节点地图 / ux battle.md(改型注记)/ architecture.md(单位容器 API 面)/ registry(rng/skill 新条目/enemy/status_effect/settlement/术语七词条/修订记录)/ 06-待办清单(改型"单拍行为环体验验证"、收编关闭"行为环废除与蓄力拍退役"、新增 P1 五项 + P2 技能图鉴)。
|
||||
|
||||
**实现决策登记(提案未定处的自行拍板,按封装纪律报告)**:① 保底 M 载体 = 技能表全局单字段 pity_threshold(不分敌种,难度曲线单旋钮);② 冷却"施放后连续 N 拍不可摇中"的实现 = 摇号按当前冷却值排除、递减在其后(与提案"递减→排除"的可摇集合逐拍恒等);③ 增益持续与技能冷却两计时器 = apply_effect duration_override 通道;④ 敌方防御池乘区落点 = 主角攻击 Σ三分量后(hero_offense 现行走普通分量直乘的切片现状不动——登记遗留);⑤ 摇号器类名 SkillLottery(core/skill_lottery.gd)。
|
||||
@@ -4,8 +4,9 @@
|
||||
# 战斗结算编排中枢:轮次生命周期唯一 owner · 五段结算(授予→增益/减益→战斗→衰减→判定——2026-09-15
|
||||
# 格面资源批,道具窗口/道具段随可选类删除下线)· 授予段极性路由(「动作 × 格型」矩阵:开安全格/标中旗
|
||||
# = 正向,开雷格/标错旗 = 负向;结算队列 + 抵消过滤——标旗后被撤旗以轮末在场旗为基线不判)·
|
||||
# 10 类行协议可回放步骤流(零 await;随机消费 = 开战遭遇骰初始化随机 + 护甲等级掷点回合内随机——
|
||||
# 均走封闭命名流,2026-09-16 护甲等级掷点批)· 高光双落(完美排雷 ×2 / 标中旗 highlight)·
|
||||
# 10 类行协议可回放步骤流(零 await;随机消费三处皆封闭流——开战遭遇骰〔初始化随机〕+ 技能摇号
|
||||
# behavior_roll〔回合内、开轮时点、步骤流之外——敌人技能池批 2026-09-16〕+ 护甲等级掷点〔回合内、
|
||||
# 步骤流之内〕;同种子同输入逐位复现)· 高光双落(完美排雷 ×2 / 标中旗 highlight)·
|
||||
# Waiting 相位结算触发(不依赖事件到达序)。
|
||||
# 切片登记:① 事件总线载体 = 直连信号(实现期 ADR 项,provisional)② Waiting 相位刷新通道 2026-09-15 用户拍板整体
|
||||
# 拆除(仅 Active 挖掘期受理)③ 效果行协议 = 授予段逐 grant 行 + 增益/减益段续存实例组合行。
|
||||
@@ -22,7 +23,7 @@ signal settlement_snapshot(snap: Dictionary) # 每结算末尾的全量状态
|
||||
# 明细流水行型协议(战斗结算 Core Rule;10 类——2026-09-15 格面资源批由 15 类收拢,
|
||||
# 清单改动须同步 battle_log_view 的 _format_row 与本文档头注):
|
||||
# 效果行 effect · 伤害行 damage · 治疗行 heal · 溢出未生效行 overflow ·
|
||||
# 奖励未生效行 reward_ineffective · 标旗未中行 flag_miss · 蓄力行 charge · 衰减行 decay ·
|
||||
# 奖励未生效行 reward_ineffective · 标旗未中行 flag_miss · 施放行 cast(敌人技能池批由蓄力行 charge 改型)· 衰减行 decay ·
|
||||
# 判定行 verdict · 高光行 highlight
|
||||
# 行来源断言豁免集合(行型层)——判定段 / 衰减段本就无棋盘来源,勿一刀切误炸
|
||||
const SOURCE_EXEMPT_TYPES := ["decay", "verdict"]
|
||||
@@ -122,6 +123,7 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
_fatal("行动经济 首轮 notify_round_start 被拒")
|
||||
return false
|
||||
round_index = 1
|
||||
_roll_round_actions() # 开轮摇号(敌人技能池批——首轮同样摇;先于 round_started,挖掘期动画位可读)
|
||||
round_started.emit(round_index)
|
||||
return true
|
||||
|
||||
@@ -218,7 +220,7 @@ func _settle() -> void:
|
||||
# ② 增益/减益段(续存实例组合行;本结算新授予的已在授予段逐 grant 出行——增益状态 义务⑦)
|
||||
_emit_composition_effects("hero", ri)
|
||||
_emit_composition_effects("enemy", ri)
|
||||
# ③ 战斗段(调用序钉死 query_attack → apply_damage → advance;hero hp=0 短路)
|
||||
# ③ 战斗段(调用序:掷点判定 → apply_damage → commit_action;hero hp=0 短路)
|
||||
_combat_segment(ri)
|
||||
# ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减)
|
||||
var k: int = _status_effects.decay_round()
|
||||
@@ -396,6 +398,26 @@ func _grant_clear_identity(snap: Dictionary, ri: int, suffix: String) -> bool:
|
||||
return _apply_flat_row(row, "clear_reward", Vector2i(-1, -1), ri, "clear_reward_h2", suffix) == "redeemed"
|
||||
|
||||
|
||||
# ---- 开轮摇号(敌人技能池批——共识点 B:技能选择前置到轮次开始,数值计算留结算段)----
|
||||
# 每实例:保底判定(连续执行普攻 ≥ M → 必出特殊技、特殊技内按权重、不受冷却限制)→ 排除冷却中技能
|
||||
# (中签率 = 权重 ÷ 可摇权重合计——份额归一)→ 按行序逐敌从 behavior_roll 独立子流摇定本拍动作。
|
||||
# 随机消费只在换算层(本方法 + SkillLottery);单位容器收确定值(零随机纪律——armor_roll 先例同构)。
|
||||
# 子流 scope = spawn_index × BEHAVIOR_SUBSTREAM_STRIDE + 单敌摇号序(registry 派生向量)——
|
||||
# 单敌出招轨迹可独立复放、敌间互不穿插。摇定后、兑现前死亡 → 本拍技能作废(死亡即不动手)。
|
||||
func _roll_round_actions() -> void:
|
||||
var pity_m: int = int(db.skill.pity_threshold)
|
||||
for u in _enemy_system.query_alive("enemy"):
|
||||
var entries: Array = _enemy_system.rollable_skills(u)
|
||||
if _enemy_system.basic_streak(u) >= pity_m:
|
||||
var specials: Array = _enemy_system.special_skills(u)
|
||||
if not specials.is_empty():
|
||||
entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合
|
||||
var scope: int = int(u.get("handle_id", 0)) * RngService.BEHAVIOR_SUBSTREAM_STRIDE + int(u.get("roll_seq", 0))
|
||||
var stream: RandomNumberGenerator = rng.open_scope("behavior_roll", scope)
|
||||
_enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream))
|
||||
_enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记)
|
||||
|
||||
|
||||
# ---- 战斗段 ----
|
||||
|
||||
func _combat_segment(ri: int) -> void:
|
||||
@@ -433,33 +455,50 @@ func _combat_segment(ri: int) -> void:
|
||||
payload["tier"] = discount
|
||||
if crit:
|
||||
payload["crit"] = true
|
||||
# 增益池乘区·敌方防御池(敌人技能池批——硬化技载体):Σ三分量后 ×(100+clamp(enemy_defense))/100。
|
||||
# 四池对称:敌打主角 = hero_defense×enemy_output(_resolve_damage);主角打敌 = hero_offense×enemy_defense
|
||||
# (hero_offense 现行走普通分量 attack_up 直乘的切片现状——登记遗留,本批不动)。
|
||||
d_hero = DamageMath.pool_damage(d_hero, 0, _clamped_pool("enemy_defense"))
|
||||
var d_landed: int = maxi(int(db.settlement.damage_floor), d_hero)
|
||||
_enemy_system.apply_damage(hero_target, d_landed)
|
||||
payload["killed"] = not hero_target.alive # 击杀标记在扣血后取(原语义不变)
|
||||
_emit_row("damage", "hero_attack", Vector2i(-1, -1), ri, _attack_label("主角攻击", crit, discount), d_landed, payload)
|
||||
# 敌方行动(敌人技能池批——本拍动作开轮已摇定,此处按技能行兑现;行序 = 生成序)。
|
||||
# 死亡即不动手:摇定后、兑现前死亡的敌不在存活序中——本拍技能作废,不 commit、不进冷却。
|
||||
for u in _enemy_system.query_alive("enemy"):
|
||||
if int(_enemy_system.hero_unit().hp) <= 0:
|
||||
break # hero hp=0 立即短路(死尸挨打行是归因噪音——用户拍板)
|
||||
var beat: String = _enemy_system.query_beat(u)
|
||||
var atk: Dictionary = _enemy_system.query_attack(u, L, _m_amb) # 调用序①兑付读值
|
||||
if beat == "charge":
|
||||
_emit_row("charge", "enemy_action", Vector2i(-1, -1), ri, "%s 蓄力(本轮踩雷白喂)" % u.display_name, 0, {"target": "hero"})
|
||||
else:
|
||||
# 基线部分吃掷点判定(含暴击)+ 伏击附加(必中全额)→ 合计 → 增益池乘区 → max(1, ·)(伤害下限)
|
||||
var roll_read_e: Dictionary = _armor_roll()
|
||||
var threshold_e: int = _defender_ac(_enemy_system.hero_unit()) + int(db.settlement.attack_bonus)
|
||||
var crit_e: bool = bool(roll_read_e.crit)
|
||||
var discount_e: int = _roll_discount(int(roll_read_e.roll), threshold_e)
|
||||
var rolled_base: int = DamageMath.rolled_component(float(int(atk.baseline)), discount_e, crit_e, int(db.settlement.crit_multiplier), 100)
|
||||
var d_tick := _resolve_damage(rolled_base + int(atk.attach))
|
||||
d_tick = maxi(int(db.settlement.damage_floor), d_tick)
|
||||
_enemy_system.apply_damage(_enemy_system.hero_unit(), d_tick) # 调用序②受击上报
|
||||
var payload_e: Dictionary = {"target": "hero", "baseline": int(atk.baseline), "attach": int(atk.attach),
|
||||
"roll": int(roll_read_e.roll), "ac": threshold_e, "tier": discount_e}
|
||||
if crit_e:
|
||||
payload_e["crit"] = true
|
||||
_emit_row("damage", "enemy_action", Vector2i(-1, -1), ri, _attack_label("%s 攻击" % u.display_name, crit_e, discount_e), d_tick, payload_e)
|
||||
_enemy_system.advance(u) # 调用序③推进——每结算每存活敌恰一次(hero 不在推进域)
|
||||
var skill_id := _enemy_system.pending_action(u)
|
||||
var srow: Dictionary = db.skill_row(skill_id)
|
||||
if srow.is_empty():
|
||||
_fatal("敌实例待发动作悬空: %s" % skill_id)
|
||||
continue
|
||||
if String(srow.type) == "buff":
|
||||
# 增益技 = 0 伤害施放行:不兑付伏击(旧蓄力拍"零兑付"语义由施放行继承——本轮喂的伏击
|
||||
# 逢施放拍不兑现的读怪奖励保留);状态授予即数值兑现,技能冷却与增益持续两计时器分账。
|
||||
var dur: int = int(srow.get("buff_duration", 1))
|
||||
_status_effects.apply_effect(String(srow.buff_ref), {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": ri}, dur)
|
||||
_emit_row("cast", "enemy_action", Vector2i(-1, -1), ri, "%s 施放 %s" % [u.display_name, String(srow.display_name)], 0, {"skill_id": skill_id, "effect_id": String(srow.buff_ref), "duration": dur})
|
||||
_enemy_system.commit_action(u)
|
||||
continue
|
||||
# 伤害技(含普攻——倍率恒 1):三乘法链 ① A = 攻击基线 × 技能倍率(判定对象 = 技能基础伤害分量)
|
||||
# → ② d20 vs 主角 AC(nat20 暴击 ≠ 技能重击,两层严格区分)→ ③ ×甲效(敌打主角无甲轴恒中性)
|
||||
# → 伏击附加(后置加法、必中全额、不吃 AC)→ 增益池乘区 → 伤害下限 max(1,·)。
|
||||
var baseline: int = int(round(float(u.atk) * float(srow.get("damage_mult", 1.0))))
|
||||
var roll_read_e: Dictionary = _armor_roll()
|
||||
var threshold_e: int = _defender_ac(_enemy_system.hero_unit()) + int(db.settlement.attack_bonus)
|
||||
var crit_e: bool = bool(roll_read_e.crit)
|
||||
var discount_e: int = _roll_discount(int(roll_read_e.roll), threshold_e)
|
||||
var rolled_base: int = DamageMath.rolled_component(float(baseline), discount_e, crit_e, int(db.settlement.crit_multiplier), 100)
|
||||
var d_tick := _resolve_damage(rolled_base + L * _m_amb)
|
||||
d_tick = maxi(int(db.settlement.damage_floor), d_tick)
|
||||
_enemy_system.apply_damage(_enemy_system.hero_unit(), d_tick)
|
||||
var payload_e: Dictionary = {"target": "hero", "skill_id": skill_id, "baseline": baseline, "attach": L * _m_amb,
|
||||
"roll": int(roll_read_e.roll), "ac": threshold_e, "tier": discount_e}
|
||||
if crit_e:
|
||||
payload_e["crit"] = true
|
||||
_emit_row("damage", "enemy_action", Vector2i(-1, -1), ri, _attack_label("%s %s" % [u.display_name, String(srow.display_name)], crit_e, discount_e), d_tick, payload_e)
|
||||
_enemy_system.commit_action(u) # 调用序③执行上报——技能进冷却 + 保底计数(每结算每存活敌恰一次)
|
||||
|
||||
|
||||
# 攻击行显示名(掷点档位随行文本可读——2026-09-16 护甲等级掷点批;零值行语义已退役,重档折扣 = "被压制")
|
||||
@@ -569,7 +608,7 @@ func _emit_composition_effects(side: String, ri: int) -> void:
|
||||
func _emit_settlement_snapshot(verdict: String) -> void:
|
||||
var enemies: Array = []
|
||||
for u in _enemy_system.query_alive("enemy"):
|
||||
enemies.append({"id": String(u.enemy_id), "hp": int(u.hp), "max_hp": int(u.max_hp), "atk": int(u.atk), "beat": query_beat_of(u), "armor": String(u.get("armor_type", "light")), "armor_class": int(u.get("armor_class", 0))})
|
||||
enemies.append({"id": String(u.enemy_id), "hp": int(u.hp), "max_hp": int(u.max_hp), "atk": int(u.atk), "pending_skill": _enemy_system.pending_action(u), "armor": String(u.get("armor_type", "light")), "armor_class": int(u.get("armor_class", 0))})
|
||||
var buffs: Array = []
|
||||
for eff in _status_effects.query_active("hero"):
|
||||
buffs.append({"id": eff.effect_id, "side": "hero", "layers": eff.layers, "left": eff.remaining_settlements})
|
||||
@@ -606,10 +645,6 @@ func _emit_settlement_snapshot(verdict: String) -> void:
|
||||
})
|
||||
|
||||
|
||||
func query_beat_of(u: Dictionary) -> String:
|
||||
return _enemy_system.query_beat(u)
|
||||
|
||||
|
||||
# ---- 出口分发与战斗结束序列 ----
|
||||
|
||||
func _exit_distribution() -> void:
|
||||
@@ -627,6 +662,7 @@ func _exit_distribution() -> void:
|
||||
_reset_round_facts()
|
||||
_refresh_used_this_round = 0
|
||||
round_index += 1
|
||||
_roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道)
|
||||
round_started.emit(round_index)
|
||||
|
||||
|
||||
@@ -669,16 +705,24 @@ func _battle_over(result: String) -> void:
|
||||
# 协作对象(_enemy_system/_status_effects/_reward_economy)私有化后,跨层读取一律经本快照或下列命名面(diagnostic_snapshot 同款模式,
|
||||
# 禁再链式穿透 settlement.enemy_system.hero 一类路径)。载荷 schema 成文(code-conventions 2.1):
|
||||
# round / battle_result / phase / ap / rap / can_request_refresh / hero{hp,max_hp,atk,alive,armor_class} /
|
||||
# enemies[{order,display_name,hp,max_hp,enemy_id,armor_type,armor_class,beat,ring_size,ring_index,ambush_layers,alive}] /
|
||||
# enemies[{order,display_name,hp,max_hp,enemy_id,armor_type,armor_class,pending_skill,pending_type,pending_name,effects[],ambush_layers,alive}] /
|
||||
# (敌人技能池批批 2 定稿:beat/ring_size/ring_index 拆除 → pending_* 三字段 + 敌方增益 effects 投影;池与权重不公开)
|
||||
# ambush_layers / hero_effects[](增益状态 query_active 投影)
|
||||
func presentation_snapshot() -> Dictionary:
|
||||
var enemies: Array = []
|
||||
var all: Array = _enemy_system.all_enemies()
|
||||
var ambush: int = _status_effects.pool_layers("ambush")
|
||||
# 敌方增益投影(side-wide 状态——蓄力/硬化/削弱/暴走等 percent 行;伏击单独走 ambush_layers)
|
||||
var enemy_effects: Array = []
|
||||
for eff in _status_effects.query_active("enemy"):
|
||||
var erow: Dictionary = db.status_effect_row(eff.effect_id)
|
||||
if not erow.is_empty() and String(erow.unit) == "percent" and String(eff.effect_id) != "ambush":
|
||||
enemy_effects.append({"effect_id": eff.effect_id, "display_name": String(eff.display_name), "layers": int(eff.layers)})
|
||||
for i in all.size():
|
||||
var u: Dictionary = all[i]
|
||||
var alive: bool = bool(u.get("alive", false))
|
||||
var ring: Array = u.get("pattern", [])
|
||||
var pending: String = String(u.get("pending_skill", "")) if alive else ""
|
||||
var pending_row: Dictionary = db.skill_row(pending)
|
||||
enemies.append({
|
||||
"order": i + 1,
|
||||
"display_name": String(u.display_name),
|
||||
@@ -686,9 +730,12 @@ func presentation_snapshot() -> Dictionary:
|
||||
"enemy_id": String(u.enemy_id),
|
||||
"armor_type": String(u.get("armor_type", "light")),
|
||||
"armor_class": int(u.get("armor_class", 0)),
|
||||
"beat": String(_enemy_system.query_beat(u)) if alive else "",
|
||||
"ring_size": ring.size(),
|
||||
"ring_index": int(u.get("steps", 0)) % maxi(ring.size(), 1),
|
||||
# 敌人技能池批(批 2 定稿):摇定动作三字段——普通攻击零表示 / 特殊技类型级动画占位。
|
||||
# 池与权重不公开(proposal D4/D5)——实例池的权重/冷却不投影,实例池可见面 = 摇定结果本身。
|
||||
"pending_skill": pending,
|
||||
"pending_type": String(pending_row.get("type", "")) if not pending_row.is_empty() else "",
|
||||
"pending_name": String(pending_row.get("display_name", "")) if not pending_row.is_empty() else "",
|
||||
"effects": enemy_effects,
|
||||
"ambush_layers": ambush,
|
||||
"alive": alive,
|
||||
})
|
||||
|
||||
@@ -14,6 +14,7 @@ const TABLE_MANIFEST := {
|
||||
"status_effects": "status_effects.tres",
|
||||
"board_gen": "board_gen.tres",
|
||||
"enemy": "enemy.tres",
|
||||
"skill": "skills.tres",
|
||||
"hero": "hero.tres",
|
||||
"reward": "reward.tres",
|
||||
"settlement": "settlement.tres",
|
||||
@@ -26,6 +27,7 @@ var variation: VariationTable
|
||||
var status_effects: StatusEffectTable
|
||||
var board_gen: BoardGenTable
|
||||
var enemy: EnemyTable
|
||||
var skill: SkillTable
|
||||
var hero: HeroTable
|
||||
var reward: RewardTable
|
||||
var settlement: SettlementTable
|
||||
@@ -95,6 +97,7 @@ func load_tables(root: String, collect_only: bool) -> bool:
|
||||
status_effects = loaded.get("status_effects")
|
||||
board_gen = loaded.get("board_gen")
|
||||
enemy = loaded.get("enemy")
|
||||
skill = loaded.get("skill")
|
||||
hero = loaded.get("hero")
|
||||
reward = loaded.get("reward")
|
||||
settlement = loaded.get("settlement")
|
||||
@@ -150,6 +153,39 @@ func _cross_validate(loaded: Dictionary) -> void:
|
||||
push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 board_gen 无精确行" % [li, t, key])
|
||||
if not en_keys.has(key):
|
||||
push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 enemy 无精确行" % [li, t, key])
|
||||
# ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验)
|
||||
var skt = loaded.get("skill")
|
||||
if skt != null and en != null:
|
||||
for e in validate_skill_pool_links(skt, en):
|
||||
push_diag("error", e.category, e.message)
|
||||
|
||||
|
||||
# 池模板 × 技能行跨表校验(加载期致命;返回错误列表——ConfigDB 诊断通道与冒烟直验共用):
|
||||
# skill_id 悬空致命 + 池构成纪律(每池恰一行普攻、增益技 ≤1——proposal §2.1 结构保证)。
|
||||
func validate_skill_pool_links(skill_table, enemy_table) -> Array:
|
||||
var errs: Array = []
|
||||
var skill_types := {}
|
||||
for row in skill_table.rows:
|
||||
skill_types[String(row.get("skill_id", ""))] = String(row.get("type", ""))
|
||||
for row in enemy_table.rows:
|
||||
var eid: String = String(row.get("enemy_id", ""))
|
||||
var basic_n := 0
|
||||
var buff_n := 0
|
||||
for entry in row.get("pool", []):
|
||||
var sid: String = String(entry.get("skill_id", ""))
|
||||
if not skill_types.has(sid):
|
||||
errs.append({"category": "dangling_skill", "message": "EnemyTable %s 池引用不存在的 skill_id: %s(悬空致命)" % [eid, sid]})
|
||||
continue
|
||||
match skill_types[sid]:
|
||||
"basic":
|
||||
basic_n += 1
|
||||
"buff":
|
||||
buff_n += 1
|
||||
if basic_n != 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池普攻行数 ≠ 1 (%d)——普攻冷却恒 0 = 空池不可能的结构前提" % [eid, basic_n]})
|
||||
if buff_n > 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池增益技 > 1 (%d)——每池增益 ≤1 纪律(纠偏三)" % [eid, buff_n]})
|
||||
return errs
|
||||
|
||||
|
||||
# ---- R1 难度行解析(floor + warning 四元组去重;请求越域致命)----
|
||||
@@ -259,6 +295,11 @@ func status_effect_row(effect_id: String) -> Dictionary:
|
||||
return status_effects.row_by_id(effect_id)
|
||||
|
||||
|
||||
# 技能行单点读取(敌人技能池批——单位容器执行上报 / 战斗结算兑现的消费面)
|
||||
func skill_row(skill_id: String) -> Dictionary:
|
||||
return skill.row_by_id(skill_id)
|
||||
|
||||
|
||||
# ---- 数据配置 EC8:debug 热重载门(战斗结算 唯一调用方;切片实现 = 门标志 + 拒绝语义)----
|
||||
|
||||
func set_reload_gate(gated: bool) -> void:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# 取号语义:全新实例首个 open_stream = scope 0(地图战斗资源 预推进术语的登记口径)。
|
||||
extends Node
|
||||
|
||||
const STREAM_NAMES := ["board_gen", "reward_roll", "encounter", "armor_roll"] # 2026-09-12 增 encounter(遭遇随机化批);2026-09-16 增 armor_roll(护甲等级掷点批,用户拍板——回合内随机、逐次掷逐次取号,registry rng_service 条目同步)
|
||||
const STREAM_NAMES := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll"] # 2026-09-12 增 encounter(遭遇随机化批);2026-09-16 增 armor_roll(护甲等级掷点批)+ behavior_roll(敌人技能池批——开轮摇号,每敌派生独立子流,registry rng_service 条目同步)
|
||||
|
||||
# 分段表达式书写(数据配置 R2 实现级规约——禁 ≥2^63 完整字面量)
|
||||
const GOLDEN_RATIO := (0x9E3779B9 << 32) | 0x7F4A7C15 # 0x9E3779B97F4A7C15
|
||||
@@ -15,6 +15,10 @@ const SALT_BOARD_GEN := (0xA0761D64 << 32) | 0x78BD642F # 0xA0761D6478BD642F
|
||||
const SALT_REWARD_ROLL := (0xE7037ED1 << 32) | 0xA0B428DB # 0xE7037ED1A0B428DB
|
||||
const SALT_ENCOUNTER := (0x8EBC6AF0 << 32) | 0x9C88C6E3 # 0x8EBC6AF09C88C6E3(遭遇骰——registry stream_seed_derivation 权威值;旧值 0x5DEECE66D<<32 超 int64 被截断,黄金向量漂移)
|
||||
const SALT_ARMOR_ROLL := (0xC2B2AE3D << 32) | 0x27D4EB2F # 0xC2B2AE3D27D4EB2F(护甲掷点流——互异奇常数 ≠ 乘数;黄金向量见冒烟/registry 同批登记)
|
||||
const SALT_BEHAVIOR_ROLL := (0x85EBCA77 << 32) | 0x58C24E15 # 0x85EBCA7758C24E15(技能摇号流——互异奇常数 ≠ 乘数;黄金向量见冒烟/registry 同批登记)
|
||||
# behavior_roll 派生向量(registry 同批登记):子流 scope = spawn_index × STRIDE + 单敌摇号序(0 起)。
|
||||
# 每敌消费自己的 scope 段 → 单敌出招轨迹可独立复放、敌间互不穿插(proposal 约束二)。
|
||||
const BEHAVIOR_SUBSTREAM_STRIDE := 1000000
|
||||
const OUT_MASK := 0x7FFFFFFFFFFFFFFF
|
||||
const INT64_MIN := -9223372036854775808
|
||||
|
||||
@@ -23,7 +27,7 @@ const INT64_MIN := -9223372036854775808
|
||||
var _master_seed: int = -1 # -1 = 未播种(取数即致命,EC7)
|
||||
var master_seed: int:
|
||||
get: return _master_seed
|
||||
var _counters := {"board_gen": 0, "reward_roll": 0, "encounter": 0, "armor_roll": 0}
|
||||
var _counters := {"board_gen": 0, "reward_roll": 0, "encounter": 0, "armor_roll": 0, "behavior_roll": 0}
|
||||
|
||||
|
||||
func _salt_for(stream: String) -> int:
|
||||
@@ -36,6 +40,8 @@ func _salt_for(stream: String) -> int:
|
||||
return SALT_ENCOUNTER
|
||||
"armor_roll":
|
||||
return SALT_ARMOR_ROLL
|
||||
"behavior_roll":
|
||||
return SALT_BEHAVIOR_ROLL
|
||||
push_error("[RngService] 未知名流: %s" % stream)
|
||||
assert(false, "未知名流(流集合封闭,EC12)")
|
||||
return 0
|
||||
@@ -57,7 +63,7 @@ func set_master_seed(seed_val: int) -> void:
|
||||
if seed_val < 0:
|
||||
seed_val = -seed_val
|
||||
push_warning("[RngService] master_seed 负输入 abs 归一化: %d" % seed_val)
|
||||
assert(_counters["board_gen"] == 0 and _counters["reward_roll"] == 0 and _counters["encounter"] == 0 and _counters["armor_roll"] == 0, "冒险中途重播种会击穿同冒险无碰撞前提(仅许全部流计数器全零时调用)")
|
||||
assert(_counters["board_gen"] == 0 and _counters["reward_roll"] == 0 and _counters["encounter"] == 0 and _counters["armor_roll"] == 0 and _counters["behavior_roll"] == 0, "冒险中途重播种会击穿同冒险无碰撞前提(仅许全部流计数器全零时调用)")
|
||||
_master_seed = seed_val
|
||||
_counters["board_gen"] = 0
|
||||
_counters["reward_roll"] = 0
|
||||
@@ -69,6 +75,7 @@ func new_adventure(seed_val: int) -> void:
|
||||
_counters["reward_roll"] = 0
|
||||
_counters["encounter"] = 0
|
||||
_counters["armor_roll"] = 0
|
||||
_counters["behavior_roll"] = 0
|
||||
set_master_seed(seed_val)
|
||||
|
||||
|
||||
@@ -87,6 +94,23 @@ func open_stream(stream: String) -> RandomNumberGenerator:
|
||||
return rg
|
||||
|
||||
|
||||
# 显式 scope 取号门(behavior_roll 每敌子流专用——armor_roll 式顺序取号不适用:子流 scope =
|
||||
# spawn_index × BEHAVIOR_SUBSTREAM_STRIDE + 单敌摇号序,见派生向量注记)。计数器仍单调 +1
|
||||
# (总摇号次数可观测);"首个 open_stream = scope 0"语义不变(不同流不同门,互不干扰)。
|
||||
func open_scope(stream: String, scope: int) -> RandomNumberGenerator:
|
||||
if not STREAM_NAMES.has(stream):
|
||||
push_error("[RngService] 未知名流: %s" % stream)
|
||||
assert(false, "未知名流(EC12)")
|
||||
assert(scope >= 0, "负 scope 与 scope+2^63 经输出掩码别名(禁止)")
|
||||
if _master_seed < 0:
|
||||
push_error("[RngService] 未设 master_seed 即取数(EC7)")
|
||||
assert(false, "未播种流取数致命(EC7)")
|
||||
_counters[stream] = _counters[stream] + 1
|
||||
var rg := RandomNumberGenerator.new()
|
||||
rg.seed = derive_at(stream, scope)
|
||||
return rg
|
||||
|
||||
|
||||
# 只读计数器(存档序列化 快照契约的观测面)
|
||||
func counter(stream: String) -> int:
|
||||
return _counters[stream]
|
||||
@@ -0,0 +1,28 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-16
|
||||
# 敌人系统·技能摇号器(proposal-skill-pool D9 封装纪律):独立服务类——输入 实例池可摇条目 + 随机子流,
|
||||
# 输出 skill_id。权重份额归一:中签率 = 该技能权重 ÷ 可摇权重合计(排除谁,谁的份额按比例摊给其余技能)。
|
||||
# 不碰血量、不碰表、不碰容器——过滤与冷却状态由调用方(战斗结算换算层)注入确定值。
|
||||
# 零随机纪律:随机消费只在子流参数(behavior_roll 派生流),本类无自建随机源。
|
||||
# 实现 GDD:design/gdd/systems/战斗循环/敌人系统.md(registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name SkillLottery
|
||||
extends RefCounted
|
||||
|
||||
|
||||
# 权重摇号(整数份额,行序累计走查——同份额前行优先,确定性);entries = [{skill_id, weight}](非空)
|
||||
static func draw(entries: Array, stream: RandomNumberGenerator) -> String:
|
||||
assert(not entries.is_empty(), "[技能摇号] 空可摇集合(普攻冷却恒 0 → 池恒非空——结构违例即致命)")
|
||||
var total := 0
|
||||
for e in entries:
|
||||
var w: int = int(e.get("weight", 0))
|
||||
assert(w > 0, "[技能摇号] 池条目权重须 > 0: %s" % str(e))
|
||||
total += w
|
||||
assert(total > 0, "[技能摇号] 可摇权重合计为 0")
|
||||
var r: int = stream.randi_range(1, total)
|
||||
var acc := 0
|
||||
for e in entries:
|
||||
acc += int(e.weight)
|
||||
if r <= acc:
|
||||
return String(e.skill_id)
|
||||
return String(entries[entries.size() - 1].skill_id) # 数值防御分支(不可达——r ≤ total 恒有落点)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dvj4nqsa6juu
|
||||
@@ -26,8 +26,10 @@ func notify_battle_start() -> bool:
|
||||
return true
|
||||
|
||||
|
||||
# 唯一授予入口(挖掘期零直写——战斗结算 结算编排期统一调用)
|
||||
func apply_effect(effect_id: String, source: Dictionary) -> bool:
|
||||
# 唯一授予入口(挖掘期零直写——战斗结算 结算编排期统一调用)。
|
||||
# duration_override(敌人技能池批——增益技施放):技能行 buff_duration 覆盖状态行时长(-1 = 状态表值)。
|
||||
# 施放是持续量的 owner(proposal §2.2 字段词汇表"buff_duration 增益持续回合数");其他授予路径传缺省。
|
||||
func apply_effect(effect_id: String, source: Dictionary, duration_override: int = -1) -> bool:
|
||||
if not _open:
|
||||
push_warning("[增益状态] 域闭授予(EC1)")
|
||||
return false
|
||||
@@ -36,7 +38,7 @@ func apply_effect(effect_id: String, source: Dictionary) -> bool:
|
||||
push_warning("[增益状态] 未知 effect_id(EC2): %s" % effect_id)
|
||||
return false
|
||||
var policy: String = row.stack_policy
|
||||
var dur: int = int(row.duration_settlements)
|
||||
var dur: int = int(row.duration_settlements) if duration_override < 1 else duration_override
|
||||
var max_stack: int = int(row.max_stack)
|
||||
# 授予时快照(抗表重载——EC9)
|
||||
var snap_m: int = int(row.magnitude)
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# 敌人系统·战斗单位容器:hero/enemy 统一受伤死亡接口 · 行为环确定性轮转(死亡即不动手/先作用后推进)·
|
||||
# 行值快照抗热重载 · side-wide 伏击池读值由调用方注入(敌人系统 无池写面)· 编排被动零事件。
|
||||
# 切片登记接口漂移:heal() 为切片新增(战斗结算 治疗 clamp 需要写入口;敌人系统 API 封闭枚举原本无治疗面——
|
||||
# 正式侧应走 敌人系统 GDD 修订,切片以头注登记)。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/b2-enemy-system.md(系统 ID B2·敌人系统——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# Date: 2026-09-16
|
||||
# 敌人系统·战斗单位容器:hero/enemy 统一受伤死亡接口 · 实例池状态机(敌人技能池批——2026-09-16 用户拍板,
|
||||
# 设计权威 docs/proposal-skill-pool.md v1.0)· 行值快照抗热重载 · 编排被动零事件。
|
||||
# 实例池三层对象(proposal §2.2):定义层技能行(SkillTable)· 定义层池模板(EnemyTable.pool)·
|
||||
# 本类持实例层——spawn 把模板快照为各实例池(冷却全零),此后冷却递减/待发动作/保底计数各自独立。
|
||||
# 零随机零事件纪律(proposal 约束二):随机消费只在 战斗结算换算层抽签器(SkillLottery + behavior_roll 子流);
|
||||
# 本类只收确定结果(set_pending_action)并管理确定性状态(冷却递减、执行上报)。旧行为环
|
||||
# (pattern 环数组 + steps 指针 + heavy_multiplier)随技能池整体退役——拍结构保留(每结算每存活敌恰行动一次、
|
||||
# 行序、先作用后推进),"轮到什么"改由开轮摇号决定(死亡即不动手:摇定后兑现前死亡 → 本拍作废、不 commit)。
|
||||
# 切片登记接口漂移:heal() 为切片新增(战斗结算 治疗 clamp 需要写入口);spawn 签名 +3 参数(E4/区间批/
|
||||
# 遭遇骰——正式侧 GDD/registry 修订随对应批)。
|
||||
# 实现 GDD:design/gdd/systems/战斗循环/敌人系统.md(registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name UnitContainer
|
||||
extends RefCounted
|
||||
|
||||
var db
|
||||
# 单位容器私有(E3 封装收口 2026-09-13):hero/enemies 原始数组不再外露——读取经 hero_unit()/
|
||||
# all_enemies() 投影面(死亡残位查询包含在内),写入只经 apply_damage/heal/advance/spawn/despawn。
|
||||
# all_enemies() 投影面(死亡残位查询包含在内),写入只经 apply_damage/heal/set_pending_action/commit_action/
|
||||
# tick_cooldowns/spawn/despawn。
|
||||
# 句柄语义不变:投影返回的 Dictionary 引用即单位句柄(同 query_alive 先例)。
|
||||
var _hero: Dictionary = {} # {hp, max_hp, atk, is_hero: true}
|
||||
var _enemies: Array = [] # 行主序生成序;元素 = 单位 Dictionary
|
||||
@@ -25,9 +32,8 @@ func _init(db_ref) -> void:
|
||||
|
||||
# E4 收口(2026-09-13):加成值经参数显式传入(atk_override/max_hp_override,-1 = 表值兜底)——
|
||||
# 此前调用方写 db.hero 表值再由本类读表 = 共享可变单例传参(双写叠加 6→18→54 教训根源)。
|
||||
# 接口漂移登记:spawn 签名 +2 参数(敌人系统 GDD API 封闭枚举——正式侧 GDD/registry 修订批随 ADR-0009)。
|
||||
# enemy_atk_override(2026-09-14 敌方攻击区间批):战斗结算 遭遇骰同流的 spawn 期整战持久 roll 结果
|
||||
# (-1 = 表值定值路径;≥0 = 行值替换后再乘关卡 atk_mult——修饰语义不变)。敌人系统 零随机纪律保持。
|
||||
# enemy_atk_override(2026-09-14 敌方攻击区间批):遭遇骰同流的 spawn 期整战持久 roll 结果
|
||||
# (-1 = 表值定值路径;≥0 = 行值替换后再乘关卡 atk_mult)。敌人系统 零随机纪律保持。
|
||||
func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override: int = -1,
|
||||
atk_override: int = -1, max_hp_override: int = -1, enemy_atk_override: int = -1) -> bool:
|
||||
if _spawned:
|
||||
@@ -45,7 +51,7 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override:
|
||||
return false
|
||||
# max_hp = 生效上限(非入参 hero_hp)——2026-09-11 关卡扩展批修复:跨战斗携带 HP 时上限不得压扁
|
||||
# (恢复点/治疗包的满血守卫与治疗上限随之失效——切片登记接口语义澄清)
|
||||
_hero = {"hp": hero_hp, "max_hp": eff_max_hp, "atk": eff_atk, "is_hero": true, "alive": true, "display_name": "探险者", "steps": 0, "pattern": [], "enemy_id": "hero"}
|
||||
_hero = {"hp": hero_hp, "max_hp": eff_max_hp, "atk": eff_atk, "is_hero": true, "alive": true, "display_name": "探险者", "enemy_id": "hero"}
|
||||
_enemies.clear() # 难度曲线与Boss DifficultyTable 修饰(2026-09-12 结构位兑现):施加于行值、行值快照之前——MVP 空表 = 恒 1.0 无行为(AC-10)
|
||||
var mod: Dictionary = db.resolve_difficulty_modifiers(level_index, node_depth)
|
||||
var hp_mult: float = float(mod.get("hp_mult", 1.0))
|
||||
@@ -57,6 +63,14 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override:
|
||||
return false
|
||||
for i in range(count):
|
||||
var eff_row_atk: int = enemy_atk_override if enemy_atk_override >= 0 else int(row.atk)
|
||||
# 实例池快照(proposal §2.2:开战把模板快照一份,冷却全零——与"生成拿到当时的表值快照"同一条快照纪律)
|
||||
var pool_snap: Array = []
|
||||
for entry in row.get("pool", []):
|
||||
pool_snap.append({
|
||||
"skill_id": String(entry.get("skill_id", "")),
|
||||
"weight": int(entry.get("weight", 0)),
|
||||
"cooldown_left": 0,
|
||||
})
|
||||
_enemies.append({
|
||||
"handle_id": i,
|
||||
"enemy_id": String(row.enemy_id),
|
||||
@@ -64,12 +78,13 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override:
|
||||
"hp": maxi(1, int(round(float(row.hp) * hp_mult))),
|
||||
"max_hp": maxi(1, int(round(float(row.hp) * hp_mult))),
|
||||
"atk": maxi(0, int(round(float(eff_row_atk) * atk_mult))),
|
||||
"heavy_multiplier": int(row.heavy_multiplier),
|
||||
"pattern": Array(row.pattern), # 行值快照(拷贝)
|
||||
"pool": pool_snap, # 实例池(模板快照 + 冷却状态——独立摇号互不干涉)
|
||||
"pending_skill": "", # 本拍摇定动作(开轮摇号写入;兑现前死亡 = 作废不 commit)
|
||||
"roll_seq": 0, # 单敌摇号序(behavior_roll 子流 scope 派生向量——registry 登记)
|
||||
"basic_streak": 0, # 保底计数:连续执行普攻的拍数(执行上报驱动)
|
||||
"advantage_per_round_cap": int(row.advantage_per_round_cap),
|
||||
"armor_type": String(row.get("armor_type", "light")), # 护甲类型快照(克制矩阵消费面——战斗结算·战斗段;2026-09-15 命名统一批:light/ward/heavy)
|
||||
"armor_class": int(row.get("armor_class", 0)), # 护甲等级快照(掷点判定阈值基数——战斗结算·战斗段;2026-09-16 护甲等级掷点批)
|
||||
"steps": 0,
|
||||
"alive": true,
|
||||
"is_hero": false,
|
||||
})
|
||||
@@ -91,30 +106,92 @@ func query_alive(side: String) -> Array:
|
||||
return out
|
||||
|
||||
|
||||
# 当前拍(Dead 句柄保留只读可查——冻结不变式观测量;hero 句柄拒绝)
|
||||
func query_beat(u: Dictionary) -> String:
|
||||
# ---- 实例池状态机(敌人技能池批——全部确定性状态,随机在 战斗结算换算层)----
|
||||
|
||||
# 可摇集合:冷却已尽的池条目投影 [{skill_id, weight}]。普通攻击冷却恒 0 → 恒非空(空池不可能的结构前提,
|
||||
# 加载校验"每池恰一行普攻"兜底)。hero/失效句柄拒绝。
|
||||
func rollable_skills(u: Dictionary) -> Array:
|
||||
if u.is_empty() or _despawned or u.get("is_hero", false):
|
||||
_diag("rollable_skills 失效句柄")
|
||||
return []
|
||||
var out: Array = []
|
||||
for e in u.pool:
|
||||
if int(e.cooldown_left) <= 0:
|
||||
out.append({"skill_id": String(e.skill_id), "weight": int(e.weight)})
|
||||
return out
|
||||
|
||||
|
||||
# 特殊技集合(保底专用——proposal §2.1:必出特殊技、特殊技内按权重、不受冷却限制):type ≠ 普攻的全部池条目。
|
||||
func special_skills(u: Dictionary) -> Array:
|
||||
if u.is_empty() or _despawned or u.get("is_hero", false):
|
||||
_diag("special_skills 失效句柄")
|
||||
return []
|
||||
var out: Array = []
|
||||
for e in u.pool:
|
||||
var srow: Dictionary = db.skill_row(String(e.skill_id))
|
||||
if not srow.is_empty() and String(srow.type) != "basic":
|
||||
out.append({"skill_id": String(e.skill_id), "weight": int(e.weight)})
|
||||
return out
|
||||
|
||||
|
||||
# 摇定结果写入(战斗结算换算层调用——确定值;roll_seq 递增 = 子流 scope 派生向量的取号序)
|
||||
func set_pending_action(u: Dictionary, skill_id: String) -> void:
|
||||
if u.is_empty() or _despawned or u.get("is_hero", false):
|
||||
_diag("set_pending_action 失效句柄")
|
||||
return
|
||||
u.pending_skill = skill_id
|
||||
u.roll_seq = int(u.roll_seq) + 1
|
||||
|
||||
|
||||
# 本拍摇定动作(未摇 = "";死亡残位保留可查——作废观测量)
|
||||
func pending_action(u: Dictionary) -> String:
|
||||
if u.is_empty() or _despawned:
|
||||
_diag("query_beat 失效句柄")
|
||||
_diag("pending_action 失效句柄")
|
||||
return ""
|
||||
if u.get("is_hero", false):
|
||||
_diag("hero 无行为环(环接口对主角句柄拒绝)")
|
||||
return ""
|
||||
var p: Array = u.pattern
|
||||
return String(p[u.steps % p.size()])
|
||||
return String(u.get("pending_skill", ""))
|
||||
|
||||
|
||||
# F2 分量供给面:{baseline, attach, total};L/m_amb 由调用方注入(判别探针,不校验游戏态域)
|
||||
func query_attack(u: Dictionary, L: int, m_amb: int) -> Dictionary:
|
||||
if u.is_empty() or _despawned or not u.alive:
|
||||
_diag("query_attack 失效/死亡句柄")
|
||||
return {"baseline": 0, "attach": 0, "total": 0}
|
||||
var beat := query_beat(u)
|
||||
if beat == "charge":
|
||||
return {"baseline": 0, "attach": 0, "total": 0} # charge 拍不读伏击池——空拍零兑付
|
||||
var baseline: int = u.atk
|
||||
if beat == "heavy":
|
||||
baseline = u.atk * int(u.heavy_multiplier)
|
||||
return {"baseline": baseline, "attach": L * m_amb, "total": baseline + L * m_amb}
|
||||
# 保底计数读数(连续执行普攻拍数——战斗结算开轮摇号时与 SkillTable.pity_threshold 比较)
|
||||
func basic_streak(u: Dictionary) -> int:
|
||||
if u.is_empty() or _despawned:
|
||||
_diag("basic_streak 失效句柄")
|
||||
return 0
|
||||
return int(u.get("basic_streak", 0))
|
||||
|
||||
|
||||
# 执行上报(战斗段兑现时调用):该技能进冷却(施放后连续 N 拍不可摇中——含增溢技,技能冷却与增益持续
|
||||
# 是两个计时器互不相干)+ 保底计数更新(普攻 +1 / 特殊技归零)。摇定后兑现前死亡不经过本方法
|
||||
# = 作废不进账(无冷却无计数——死亡即不动手)。
|
||||
func commit_action(u: Dictionary) -> void:
|
||||
if u.is_empty() or _despawned or u.get("is_hero", false):
|
||||
_diag("commit_action 失效句柄")
|
||||
return
|
||||
var sid := String(u.get("pending_skill", ""))
|
||||
if sid == "":
|
||||
_diag("commit_action 无待发动作(兑现序违例)")
|
||||
return
|
||||
var srow: Dictionary = db.skill_row(sid)
|
||||
var cd: int = int(srow.get("cooldown_beats", 0)) if not srow.is_empty() else 0
|
||||
for e in u.pool:
|
||||
if String(e.skill_id) == sid:
|
||||
e.cooldown_left = cd
|
||||
if not srow.is_empty() and String(srow.type) == "basic":
|
||||
u.basic_streak = int(u.basic_streak) + 1
|
||||
else:
|
||||
u.basic_streak = 0
|
||||
u.pending_skill = ""
|
||||
|
||||
|
||||
# 开轮冷却递减(战斗结算开轮摇号编排调用,摇号读值之后全体恰一次):floor 0。
|
||||
# 时序口径 = 摇号按当前 cooldown_left 排除(>0 冷却中),递减在其后——等价实现"施放后连续 N 拍
|
||||
# 不可摇中"(施放拍置 N → 后续 N 个开轮各排除一次);与 proposal §2.1"冷却递减 → 排除"的
|
||||
# 可摇集合逐拍恒等(init N+1 的同构变换)。
|
||||
func tick_cooldowns() -> void:
|
||||
if _despawned:
|
||||
return
|
||||
for u in _enemies:
|
||||
for e in u.pool:
|
||||
e.cooldown_left = maxi(0, int(e.cooldown_left) - 1)
|
||||
|
||||
|
||||
# 受击上报:hp 递减 floor 0 → Dead;n ≤ 0 拒绝
|
||||
@@ -141,20 +218,6 @@ func heal(u: Dictionary, n: int) -> int:
|
||||
return actual
|
||||
|
||||
|
||||
# 推进:存活才推进;false 两级——预期冻结(无诊断)/ 失效句柄(诊断);hero 拒绝
|
||||
func advance(u: Dictionary) -> bool:
|
||||
if u.is_empty() or _despawned:
|
||||
_diag("advance 失效句柄")
|
||||
return false
|
||||
if u.get("is_hero", false):
|
||||
_diag("hero 不在推进域(主角无行为环)")
|
||||
return false
|
||||
if not u.alive:
|
||||
return false # 轮到前被杀——预期冻结(死亡即不动手),无诊断
|
||||
u.steps += 1
|
||||
return true
|
||||
|
||||
|
||||
# 固定顺位:行主序生成序最前存活敌;死亡后顺延;空 → null
|
||||
func default_target():
|
||||
for u in _enemies:
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# 数据配置 EnemyTable / HeroTable schema(敌人系统 回填值)。EnemyTable 键 (L,D,enemy_id),每行集恰一敌种;
|
||||
# HeroTable 单行全局(Rule 4 豁免)。依据 design/archive/gdd-2026-09-14/b2-enemy-system.md Schema 节。
|
||||
# HeroTable 单行全局(Rule 4 豁免)。依据 design/gdd/systems/战斗循环/敌人系统.md Schema 节。
|
||||
# 敌人技能池批(2026-09-16 用户拍板,设计权威 docs/proposal-skill-pool.md v1.0):pattern 行为环数组与
|
||||
# heavy_multiplier 退役,改池模板 pool = [{skill_id, weight}](定义层——实例池快照与冷却状态在 单位容器)。
|
||||
# 池构成纪律(每池恰一行普攻、增益 ≤1、skill_id 悬空致命)需跨表视野,校验在 ConfigDB 跨表通道。
|
||||
class_name EnemyTable
|
||||
extends Resource
|
||||
|
||||
@export var rows: Array = [] # Array[Dictionary]
|
||||
|
||||
const PATTERN_MVP := ["attack", "charge", "heavy"]
|
||||
const PATTERN_RESERVED := ["guard", "boost", "hinder", "summon"]
|
||||
# 护甲类型封闭枚举(攻击类型 × 护甲克制批——克制矩阵本体在 SettlementTable;
|
||||
# 2026-09-15 命名统一批:位置键 {none, a, b} → 语义键 {light, ward, heavy} = 轻甲 / 魔法护甲 / 重甲)
|
||||
const ARMOR_TYPES := ["light", "ward", "heavy"]
|
||||
@@ -64,17 +65,27 @@ func validate() -> Array:
|
||||
errs.append({"category": "domain", "message": "%s atk 区间越域 [0,999] 且 min≤max: %d..%d" % [eid, amin, amax]})
|
||||
if atk != int((amin + amax) / 2.0):
|
||||
errs.append({"category": "cross_field", "message": "%s atk(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [eid, atk, (amin + amax) / 2]})
|
||||
var hm: int = int(row.get("heavy_multiplier", 0))
|
||||
if hm < 1 or hm > 16:
|
||||
errs.append({"category": "domain", "message": "%s heavy_multiplier 越域 [1,16]" % eid})
|
||||
var pattern: Array = row.get("pattern", [])
|
||||
if pattern.size() < 1 or pattern.size() > 8:
|
||||
errs.append({"category": "domain", "message": "%s pattern 环长越域 [1,8]" % eid})
|
||||
for beat in pattern:
|
||||
if PATTERN_RESERVED.has(beat):
|
||||
errs.append({"category": "enum", "message": "%s pattern 预留值拒绝(MVP 校验域): %s" % [eid, beat]})
|
||||
elif not PATTERN_MVP.has(beat):
|
||||
errs.append({"category": "enum", "message": "%s pattern 未知枚举: %s" % [eid, beat]})
|
||||
# 池模板(敌人技能池批——单表结构校验;跨表构成纪律在 ConfigDB):
|
||||
# 非空池 + 条目 {skill_id 非空, weight > 0} + 池内 skill_id 不重复。
|
||||
# 普攻冷却恒 0(SkillTable schema 锁死)→ 可摇集合恒非空,"空池不可能"由此结构保证。
|
||||
var pool: Array = row.get("pool", [])
|
||||
if pool.is_empty():
|
||||
errs.append({"category": "domain", "message": "%s pool 空池(禁——技能池行必带池模板)" % eid})
|
||||
var pool_ids := {}
|
||||
for entry_v in pool:
|
||||
if not (entry_v is Dictionary):
|
||||
errs.append({"category": "domain", "message": "%s pool 条目非字典: %s" % [eid, str(entry_v)]})
|
||||
continue
|
||||
var entry: Dictionary = entry_v
|
||||
var sid: String = String(entry.get("skill_id", ""))
|
||||
if sid == "":
|
||||
errs.append({"category": "domain", "message": "%s pool 条目 skill_id 空" % eid})
|
||||
elif pool_ids.has(sid):
|
||||
errs.append({"category": "dup_key", "message": "%s pool 内 skill_id 重复: %s" % [eid, sid]})
|
||||
pool_ids[sid] = true
|
||||
var w: int = int(entry.get("weight", 0))
|
||||
if w <= 0 or w > 9999:
|
||||
errs.append({"category": "domain", "message": "%s pool 权重越域 (0,9999]: %d" % [eid, w]})
|
||||
var cap: int = int(row.get("advantage_per_round_cap", -1))
|
||||
if cap < 0 or cap > 16:
|
||||
errs.append({"category": "domain", "message": "%s advantage_per_round_cap 越域 [0,16]" % eid})
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-16
|
||||
# 数据配置 SkillTable schema(敌人技能池批——定义层"技能行",敌人系统 回填值;行 = Dictionary,键 skill_id;Rule 4 豁免)。
|
||||
# 设计权威:docs/proposal-skill-pool.md v1.0 §2.2 字段词汇表。
|
||||
# 三层对象:技能行(本表,多敌种共享)· 池模板(EnemyTable.pool:{skill_id, weight})· 实例池(单位容器 spawn 快照)。
|
||||
# 池构成纪律的加载校验在 ConfigDB 跨表通道(每池恰一行普攻、增益 ≤1、skill_id 悬空致命——本表无池视野)。
|
||||
class_name SkillTable
|
||||
extends Resource
|
||||
|
||||
@export var rows: Array = [] # Array[Dictionary]
|
||||
# 保底 M(provisional 归 难度曲线与Boss):连续 M 拍执行的都是普通攻击 → 下拍必出特殊技
|
||||
# (特殊技内按权重、不受冷却限制——proposal §2.1 两道闸之二)。全局单旋钮(不分敌种)。
|
||||
@export var pity_threshold: int = 4
|
||||
|
||||
const SKILL_TYPES := ["basic", "damage", "buff"] # 普通攻击 / 伤害 / 增益(封闭枚举——新类型走文档修订)
|
||||
const MULT_DOMAIN_MIN := 1.1 # damage_mult 域 [1.1, 16] provisional(proposal §2.2)
|
||||
const MULT_DOMAIN_MAX := 16.0
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array = []
|
||||
if rows.is_empty():
|
||||
errs.append({"category": "empty_table", "message": "SkillTable 为空"})
|
||||
return errs
|
||||
var seen := {}
|
||||
var has_basic := false
|
||||
for row in rows:
|
||||
var sid: String = row.get("skill_id", "")
|
||||
if sid == "":
|
||||
errs.append({"category": "domain", "message": "skill_id 缺失/空"})
|
||||
continue
|
||||
if seen.has(sid):
|
||||
errs.append({"category": "dup_key", "message": "skill_id 重复: %s" % sid})
|
||||
seen[sid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % sid})
|
||||
var type: String = row.get("type", "")
|
||||
if not SKILL_TYPES.has(type):
|
||||
errs.append({"category": "enum", "message": "%s type 未知枚举: %s(域 %s)" % [sid, type, str(SKILL_TYPES)]})
|
||||
continue
|
||||
# 冷却域(全类型):施放后连续 N 拍不可摇中
|
||||
var cd: int = int(row.get("cooldown_beats", -1))
|
||||
if cd < 0 or cd > 99:
|
||||
errs.append({"category": "domain", "message": "%s cooldown_beats 越域 [0,99]: %d" % [sid, cd]})
|
||||
# 普通攻击两条域校验锁死特殊性:倍率恒 1、冷却恒 0——想配的就是伤害技(proposal §2.2)
|
||||
if type == "basic":
|
||||
has_basic = true
|
||||
if row.has("damage_mult") and not is_equal_approx(float(row.damage_mult), 1.0):
|
||||
errs.append({"category": "cross_field", "message": "%s 普攻 damage_mult ≠ 1(禁配——要配就是伤害技): %s" % [sid, str(row.damage_mult)]})
|
||||
if row.has("cooldown_beats") and cd != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 普攻 cooldown_beats ≠ 0(禁配——池恒非空的结构前提): %d" % [sid, cd]})
|
||||
if row.has("buff_ref") or row.has("buff_duration"):
|
||||
errs.append({"category": "cross_field", "message": "%s 普攻不带增益字段" % sid})
|
||||
# 伤害技:存倍率不存固定值——伤害 = 施放者攻击基线 × 技能倍率(三乘法链第一乘)
|
||||
if type == "damage":
|
||||
if not row.has("damage_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 伤害技缺 damage_mult(存倍率不存固定值)" % sid})
|
||||
else:
|
||||
var m: float = float(row.damage_mult)
|
||||
if m < MULT_DOMAIN_MIN or m > MULT_DOMAIN_MAX:
|
||||
errs.append({"category": "domain", "message": "%s damage_mult 越域 [%.1f, %.1f]: %s" % [sid, MULT_DOMAIN_MIN, MULT_DOMAIN_MAX, str(m)]})
|
||||
if row.has("buff_ref") or row.has("buff_duration"):
|
||||
errs.append({"category": "cross_field", "message": "%s 伤害技不带增益字段" % sid})
|
||||
# 增益技:0 伤害动作(无倍率),效果引用 + 持续回合;悬空引用由 ConfigDB 跨表校验兜底
|
||||
if type == "buff":
|
||||
if row.has("damage_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 增益技为 0 伤害动作,不带 damage_mult" % sid})
|
||||
if String(row.get("buff_ref", "")) == "":
|
||||
errs.append({"category": "cross_field", "message": "%s 增益技缺 buff_ref(效果引用)" % sid})
|
||||
var bd: int = int(row.get("buff_duration", 0))
|
||||
if bd < 1 or bd > 99:
|
||||
errs.append({"category": "domain", "message": "%s buff_duration 越域 [1,99]: %d" % [sid, bd]})
|
||||
if not has_basic:
|
||||
errs.append({"category": "structural", "message": "SkillTable 无普攻行(basic)——池构成纪律的结构前提"})
|
||||
if pity_threshold < 1 or pity_threshold > 99:
|
||||
errs.append({"category": "domain", "message": "pity_threshold 越域 [1,99]: %d" % pity_threshold})
|
||||
return errs
|
||||
|
||||
|
||||
func row_by_id(skill_id: String) -> Dictionary:
|
||||
for row in rows:
|
||||
if row.get("skill_id", "") == skill_id:
|
||||
return row
|
||||
return {}
|
||||
@@ -0,0 +1 @@
|
||||
uid://b05eabwqt7yer
|
||||
@@ -15,8 +15,13 @@ rows = [{
|
||||
"atk": 7,
|
||||
"atk_min": 4,
|
||||
"atk_max": 10,
|
||||
"heavy_multiplier": 2,
|
||||
"pattern": ["attack"],
|
||||
"pool": [{
|
||||
"skill_id": "attack",
|
||||
"weight": 9
|
||||
}, {
|
||||
"skill_id": "lunge",
|
||||
"weight": 1
|
||||
}],
|
||||
"advantage_per_round_cap": 4,
|
||||
"armor_type": "light",
|
||||
"armor_class": 0
|
||||
@@ -31,8 +36,13 @@ rows = [{
|
||||
"atk": 5,
|
||||
"atk_min": 2,
|
||||
"atk_max": 8,
|
||||
"heavy_multiplier": 2,
|
||||
"pattern": ["attack"],
|
||||
"pool": [{
|
||||
"skill_id": "attack",
|
||||
"weight": 9
|
||||
}, {
|
||||
"skill_id": "lunge",
|
||||
"weight": 1
|
||||
}],
|
||||
"advantage_per_round_cap": 4,
|
||||
"armor_type": "light",
|
||||
"armor_class": 0
|
||||
@@ -46,8 +56,16 @@ rows = [{
|
||||
"atk": 9,
|
||||
"atk_min": 6,
|
||||
"atk_max": 12,
|
||||
"heavy_multiplier": 2,
|
||||
"pattern": ["attack", "charge", "heavy"],
|
||||
"pool": [{
|
||||
"skill_id": "attack",
|
||||
"weight": 6
|
||||
}, {
|
||||
"skill_id": "charge",
|
||||
"weight": 2
|
||||
}, {
|
||||
"skill_id": "heavy",
|
||||
"weight": 2
|
||||
}],
|
||||
"advantage_per_round_cap": 4,
|
||||
"armor_type": "ward",
|
||||
"armor_class": 0
|
||||
@@ -62,8 +80,16 @@ rows = [{
|
||||
"atk": 6,
|
||||
"atk_min": 3,
|
||||
"atk_max": 9,
|
||||
"heavy_multiplier": 2,
|
||||
"pattern": ["attack", "charge", "heavy"],
|
||||
"pool": [{
|
||||
"skill_id": "attack",
|
||||
"weight": 6
|
||||
}, {
|
||||
"skill_id": "charge",
|
||||
"weight": 2
|
||||
}, {
|
||||
"skill_id": "heavy",
|
||||
"weight": 2
|
||||
}],
|
||||
"advantage_per_round_cap": 4,
|
||||
"armor_type": "ward",
|
||||
"armor_class": 0
|
||||
@@ -77,8 +103,16 @@ rows = [{
|
||||
"atk": 9,
|
||||
"atk_min": 6,
|
||||
"atk_max": 12,
|
||||
"heavy_multiplier": 2,
|
||||
"pattern": ["attack", "attack", "charge", "heavy"],
|
||||
"pool": [{
|
||||
"skill_id": "attack",
|
||||
"weight": 6
|
||||
}, {
|
||||
"skill_id": "heavy",
|
||||
"weight": 2
|
||||
}, {
|
||||
"skill_id": "harden",
|
||||
"weight": 2
|
||||
}],
|
||||
"advantage_per_round_cap": 4,
|
||||
"armor_type": "heavy",
|
||||
"armor_class": 0
|
||||
@@ -92,8 +126,16 @@ rows = [{
|
||||
"atk": 7,
|
||||
"atk_min": 4,
|
||||
"atk_max": 10,
|
||||
"heavy_multiplier": 2,
|
||||
"pattern": ["attack", "charge", "heavy"],
|
||||
"pool": [{
|
||||
"skill_id": "attack",
|
||||
"weight": 6
|
||||
}, {
|
||||
"skill_id": "charge",
|
||||
"weight": 2
|
||||
}, {
|
||||
"skill_id": "heavy",
|
||||
"weight": 2
|
||||
}],
|
||||
"advantage_per_round_cap": 4,
|
||||
"armor_type": "ward",
|
||||
"armor_class": 0
|
||||
@@ -122,3 +164,11 @@ rows = [{
|
||||
; 护甲等级掷点批(2026-09-16 用户拍板):armor_class 字段入列(缺省 0 向后兼容)——防守方掷点判定阈值基数,
|
||||
; 与 armor_type(克制属性轴)两概念严格分离、不做映射。首关六行暂定 0(正式行值归怪物/敌人系统设定批——
|
||||
; 登记待办:第一关怪物 AC 相对较低,值待怪物设定投值;投值所有权归 难度曲线与Boss)。
|
||||
|
||||
; 敌人技能池批(2026-09-16 用户拍板——设计权威 docs/proposal-skill-pool.md v1.0):
|
||||
; pattern 行为环数组 + heavy_multiplier 整体退役,改池模板 pool = [{skill_id, weight}](技能行本体在 skills.tres;
|
||||
; 实例池快照与冷却状态在 单位容器 spawn)。建议池(全部 provisional,投值归 难度曲线与Boss;EV 锚 = proposal §2.5):
|
||||
; 地精 普攻 9/突刺 1(每拍 EV 1.05——突刺倍率 >1 必然抬 EV,+5% 偏移显式登记不硬凑);
|
||||
; 熊地精 普攻 6/蓄力 2/重击 2(EV 1.00 中性);食人魔酋长 普攻 6/重击 2/硬化 2(EV 1.00 中性;
|
||||
; 硬化 = Boss 池独占增益技——增益 ≤1 纪律下旧蓄力拍退役不进 Boss 池)。
|
||||
; 含闸长期速率(冷却/保底)EV 锚按实施批 EV 抽样复核登记(proposal §2.5)。
|
||||
@@ -22,7 +22,9 @@ effect_pool_map = {
|
||||
"ambush": "flat_attach",
|
||||
"enrage": "enemy_output",
|
||||
"off_guard": "hero_defense",
|
||||
"sapped": "hero_offense"
|
||||
"sapped": "hero_offense",
|
||||
"empower": "enemy_output",
|
||||
"fortify": "enemy_defense"
|
||||
}
|
||||
pool_clamp_lo = -100
|
||||
pool_clamp_hi = 100
|
||||
@@ -88,3 +90,8 @@ armor_roll_stream = "armor_roll"
|
||||
; EV 参考数(按判定式复算):AC10 期望减免 21.25% / AC11 25% / AC12 28.75%;微调旋钮 = bounds/discounts,不动 AC 值。
|
||||
; effect_pool_map 同批删 defense_up 键(减伤百分比行整体退役、无双轨)——ac_up 是 AC 阈值轴效果,不入任何池;
|
||||
; off_guard(hero_defense 池)保留不动,池结构与乘区公式零改动。
|
||||
; 敌人技能池批(2026-09-16 用户拍板):effect_pool_map 增 empower→enemy_output(蓄力技载体——敌方输出 ×2
|
||||
; 经既有 enemy_output 乘区兑付)与 fortify→enemy_defense(硬化技载体——敌方防御池新词汇)。
|
||||
; enemy_defense 池乘区消费面 = 主角攻击落地段(Σ三分量后 ×(100+clamp(enemy_defense))/100)——
|
||||
; 四池对称乘区:敌打主角 = hero_defense×enemy_output,主角打敌 = hero_offense×enemy_defense
|
||||
; (hero_offense 现行走主角普通分量 attack_up 直乘、切片未改——登记遗留)。clamp 域 [-100,+100] 共用。
|
||||
@@ -0,0 +1,50 @@
|
||||
[gd_resource type="Resource" script_class="SkillTable" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/schemas/skill_table.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
pity_threshold = 4
|
||||
rows = [{
|
||||
"skill_id": "attack",
|
||||
"display_name": "普通攻击",
|
||||
"type": "basic",
|
||||
"cooldown_beats": 0
|
||||
}, {
|
||||
"skill_id": "lunge",
|
||||
"display_name": "突刺",
|
||||
"type": "damage",
|
||||
"damage_mult": 1.5,
|
||||
"cooldown_beats": 2
|
||||
}, {
|
||||
"skill_id": "charge",
|
||||
"display_name": "蓄力",
|
||||
"type": "buff",
|
||||
"buff_ref": "empower",
|
||||
"buff_duration": 2,
|
||||
"cooldown_beats": 3
|
||||
}, {
|
||||
"skill_id": "heavy",
|
||||
"display_name": "重击",
|
||||
"type": "damage",
|
||||
"damage_mult": 2.0,
|
||||
"cooldown_beats": 3
|
||||
}, {
|
||||
"skill_id": "harden",
|
||||
"display_name": "硬化",
|
||||
"type": "buff",
|
||||
"buff_ref": "fortify",
|
||||
"buff_duration": 2,
|
||||
"cooldown_beats": 4
|
||||
}]
|
||||
|
||||
; 敌人技能池批(2026-09-16 用户拍板——设计权威 docs/proposal-skill-pool.md v1.0,全值 provisional 投值归 难度曲线与Boss):
|
||||
; 五行 = 普通攻击 / 突刺(新行:伤害 ×1.5 冷却 2)/ 蓄力(增益技)/ 重击 / 硬化(新行:独占增益技——Boss 专属池载体)。
|
||||
; 普通攻击两条域校验锁死:倍率恒 1、冷却恒 0(schema 拒绝配置)——池恒非空的结构前提。
|
||||
; 蓄力 = 纯"攻击力增加"增益技(0 伤害、施加 empower 状态),与重击无任何组合语义(纠偏二——旧"蓄力→猛击"
|
||||
; 固定配对随行为环退役);增益与倍率同拍只是两层乘法共存(×2 遇 ×2 = ×4)。
|
||||
; buff_duration = 2 的口径:容器时限纲 = remaining_settlements(先兑付后衰减、授予当轮衰减段即 −1)——
|
||||
; 施放发生在第 N 结算战斗段,需存活到第 N+1 结算战斗段才兑付("下一拍"),故持续 1 个有效结算 ⇔ 表值 2
|
||||
; (提案"攻击×2 持续 1 结算"的容器语义落地;有效增益拍恰 1 拍,归 增益状态/难度曲线 复核)。
|
||||
; 硬化 = 自防增益(fortify 状态 → 敌方防御池),Boss 池独占——增益 ≤1 纪律下旧蓄力拍不进 Boss 池(proposal §2.5)。
|
||||
; 保底 M = pity_threshold = 4:连续 4 拍执行普攻 → 下拍必出特殊技(特殊技内按权重、不受冷却限制)。
|
||||
@@ -67,6 +67,24 @@ rows = [{
|
||||
"duration_settlements": 1,
|
||||
"stack_policy": "stack",
|
||||
"max_stack": 16
|
||||
}, {
|
||||
"effect_id": "empower",
|
||||
"display_name": "蓄力",
|
||||
"target_side": "enemy",
|
||||
"magnitude": 100,
|
||||
"unit": "percent",
|
||||
"duration_settlements": 2,
|
||||
"stack_policy": "refresh",
|
||||
"max_stack": 1
|
||||
}, {
|
||||
"effect_id": "fortify",
|
||||
"display_name": "硬化",
|
||||
"target_side": "enemy",
|
||||
"magnitude": 100,
|
||||
"unit": "percent",
|
||||
"duration_settlements": 2,
|
||||
"stack_policy": "refresh",
|
||||
"max_stack": 1
|
||||
}]
|
||||
|
||||
; 减伤墙调参批(2026-09-13,playtest-2026-09-12 报告 Q1 触发——defense_scan 实测墙轮 39%、
|
||||
@@ -82,3 +100,10 @@ rows = [{
|
||||
; unit=ac 新量纲(AC 阈值轴效果,不入 effect_pool_map 任何池),消费面 = 战斗结算掷点判定
|
||||
; (防守方 AC = 主角表值 armor_class + Σac 型效果层数×幅度);off_guard(失衡 +25 percent,hero_defense 池)
|
||||
; 保留不动,其余六行零改动——状态表仍七行(一行换装)。
|
||||
; 敌人技能池批(2026-09-16 用户拍板——增益技新状态行,七行→九行;敌方增益走既有增益状态容器):
|
||||
; empower(蓄力:敌方攻击 +100 percent,enemy_output 池——蓄力技 buff_ref 载体)与
|
||||
; fortify(硬化:敌方防御 +100 percent,enemy_defense 池——敌方防御池新词汇载体,Boss 池独占硬化技)。
|
||||
; 两行 duration = 2 的口径:先兑付后衰减含施放当轮 → 表值 2 ⇔ 有效增益拍 1 拍(施放后下一结算兑付,
|
||||
; 提案"持续 1 结算"的容器语义落地——详见 skills.tres 批注);refresh ×1 层(冷却闸下不可能自叠加)。
|
||||
; enemy 侧池语义 = side-wide(registry 既有口径):蓄力 +100 入 enemy_output 与 weaken/enrage 共池 clamp;
|
||||
; 硬化 +100 入 enemy_defense 新池(乘区消费面 = 战斗结算 主角攻击落地段)。值全部 provisional 归 难度曲线与Boss。
|
||||
@@ -5,7 +5,7 @@
|
||||
# 静态无状态——呈现不持久化任何核心句柄。
|
||||
# E1 封装收口(2026-09-13):核心状态读取统一经 战斗结算.presentation_snapshot() 成文快照面——
|
||||
# 禁再链式穿透 settlement.enemy_system.hero / settlement.enemy_system.enemies / settlement.status_effects.query_active 一类路径(战斗结算 协作对象已私有化,
|
||||
# 敌人含死亡残位的投影由快照 enemies[] 承载,含 alive/beat/ring 字段)。
|
||||
# 敌人含死亡残位的投影由快照 enemies[] 承载,含 alive/pending_*/effects 字段——2026-09-16 敌人技能池批批 2 定稿)。
|
||||
# 更新触发注记(battle.md Data Requirements):旗/拔/移动成功反馈零事件外发(排雷玩法 发射面封闭)——
|
||||
# 经 ActionOutcome 返回值命令式驱动;AP/RAP 无推送信号——组合根每次调用返回后重查;
|
||||
# 增益状态 纯被动容器零信号——状态栏刷新绑 战斗结算 结算编排完成时点。
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
# highlight 金框〔flag_hit/clear_reward_h2/perfect_broken〕、击杀标记)。
|
||||
# Date: 2026-09-12(基础 = 2026-09-11 13 类行协议格式化版;2026-09-15 格面资源批 15→10 类:
|
||||
# 删 item / consumed_ineffective / use_rejected / window_open / window_skip 五类——可选类道具与窗口下线,
|
||||
# 战斗结算 行协议清单同步;2026-09-16 护甲等级掷点批:伤害行补掷点读数/档位随行标注 + nat20 金框高亮)
|
||||
# 战斗结算 行协议清单同步;2026-09-16 护甲等级掷点批:伤害行补掷点读数/档位随行标注 + nat20 金框高亮;
|
||||
# 同日 敌人技能池批:施放行 = 蓄力行 charge 改型〔增益技 0 伤害施放、具名技能〕)
|
||||
# 高内聚边界:战斗结算 步骤流 → 玩家文案的全部格式化规则内聚于此;来源文案映射来自 SettlementTable。
|
||||
class_name BattleLogView
|
||||
extends PanelContainer
|
||||
@@ -200,8 +201,9 @@ func _format_row(row: Dictionary, agg_count := -1) -> String:
|
||||
"flag_miss":
|
||||
var body := "✗ %s%s" % [name_, "(本板完美排雷已破——归因此旗)" if hl == "perfect_broken" else ""]
|
||||
return prefix + (_gold_frame(body) if hl == "perfect_broken" else "[color=#9a8a72]%s[/color]" % body)
|
||||
"charge":
|
||||
return prefix + "[color=#9a8a72]%s%s[/color]" % [name_, suffix]
|
||||
"cast":
|
||||
# 施放行(敌人技能池批——蓄力行 charge 改型):增益技 0 伤害施放,具名技能
|
||||
return prefix + "[color=#b09a72]✦ %s%s[/color]" % [name_, suffix]
|
||||
"decay":
|
||||
return prefix + "[color=#8a9ab0]状态衰减 ×%d[/color]" % v
|
||||
"verdict":
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 实现 GDD:design/ux/battle.md(B 区敌人卡 ×1–3——敌像占位、血条、顺位徽标、意图徽标〔常驻:
|
||||
# 挖掘期全程可见,映射 = enemy_id 恒定、环点 = 拍数直觉编码,敌人系统 数据源 = query_beat 当前拍〕、
|
||||
# 实现 GDD:design/ux/battle.md(B 区敌人卡 ×1–3——敌像占位、血条、顺位徽标、意图徽标〔常驻:挖掘期全程可见〕、
|
||||
# 状态槽〔敌方 side 池口径:伏击层数全体共享单值各卡同值——战斗结算 流水经 增益状态 投影〕)。
|
||||
# Date: 2026-09-12
|
||||
# Date: 2026-09-12(2026-09-16 敌人技能池批改型:行为环环点拆除〔proposal §2.4〕——
|
||||
# 意图徽标语义 = 本拍摇定结果的动画占位:普通攻击零表示 / 特殊技类型级提示(攻击系 ⚔ / 增益系 ✦),
|
||||
# 池与权重不公开(D4/D5);环点行退役,护甲类型 + AC 常驻;敌方增益(蓄力/硬化等)入状态行)
|
||||
# 敌人死亡呈现(敌人系统 资产下限同等 blocking):血条归零 → 卡体灰化 + 碎裂退场,禁瞬消;
|
||||
# 卡体保留灰化残位——定长卡槽,死亡动画不回流布局。
|
||||
# 意图线索映射目录(敌人系统×4① 成文占位——资产定稿归 战斗表现,本处按 enemy_id 程序化占位)。
|
||||
@@ -10,8 +11,14 @@ class_name EnemyCardView
|
||||
extends PanelContainer
|
||||
|
||||
const COL_INK := Color8(51, 38, 27)
|
||||
const COL_BEAT := {"attack": Color8(198, 61, 34), "charge": Color8(90, 108, 140), "heavy": Color8(143, 47, 106)}
|
||||
const BEAT_NAMES := {"attack": "攻击", "charge": "蓄力", "heavy": "重击"}
|
||||
# 摇定动作呈现映射(敌人技能池批 §2.4 三档可见性):键 = pending_type(快照投影)。
|
||||
# 普通攻击零表示("—" 中性色——完全不可预测);伤害特殊技 = 攻击系 ⚔(朱红);
|
||||
# 增益特殊技 = 防御/增益系 ✦(蓝)。nat20 暴击无前置表示(掷点层,与技能重击严格区分)。
|
||||
const PENDING_STYLE := {
|
||||
"basic": ["—", Color8(120, 106, 88)],
|
||||
"damage": ["⚔", Color8(198, 61, 34)],
|
||||
"buff": ["✦", Color8(46, 110, 142)],
|
||||
}
|
||||
# 护甲类型呈现(攻击类型×护甲克制批——可读性前提:玩家须能看出该用穿刺还是魔法打它)
|
||||
const ARMOR_NAMES := {"light": "轻甲", "ward": "魔法护甲", "heavy": "重甲"}
|
||||
|
||||
@@ -19,10 +26,10 @@ var _portrait: Label
|
||||
var _name_label: Label
|
||||
var _hp_bar: ProgressBar
|
||||
var _intent_badge: Label
|
||||
var _ring: Label
|
||||
var _armor_line: Label
|
||||
var _status_label: Label
|
||||
var _armor := "light" # 护甲类型快照(set_data 写入、set_intent 渲染——环点行常驻标签)
|
||||
var _armor_class := 0 # 护甲等级快照(2026-09-16 护甲等级掷点批——环点行常驻;与护甲类型两轴分立)
|
||||
var _armor := "light" # 护甲类型快照(set_data 写入、set_pending 渲染——护甲行常驻标签)
|
||||
var _armor_class := 0 # 护甲等级快照(2026-09-16 护甲等级掷点批——护甲行常驻;与护甲类型两轴分立)
|
||||
var _dead := false
|
||||
|
||||
|
||||
@@ -60,13 +67,14 @@ func _init() -> void:
|
||||
_intent_badge = UiKit.label("意图 —", 13, true)
|
||||
_intent_badge.add_theme_color_override("font_color", COL_INK)
|
||||
col.add_child(_intent_badge)
|
||||
_ring = UiKit.label("", 12, false)
|
||||
col.add_child(_ring)
|
||||
_armor_line = UiKit.label("", 12, false)
|
||||
col.add_child(_armor_line)
|
||||
_status_label = _fixed_label(30)
|
||||
col.add_child(_status_label)
|
||||
|
||||
|
||||
# data = 增益状态/敌人系统 投影:{order, display_name, hp, max_hp, enemy_id, beat, ring_size, ring_index, ambush_layers}
|
||||
# data = 战斗结算 presentation_snapshot 投影:{order, display_name, hp, max_hp, enemy_id,
|
||||
# pending_skill/pending_type/pending_name(摇定动作三字段), effects[](敌方增益), ambush_layers}
|
||||
func set_data(d: Dictionary) -> void:
|
||||
_name_label.text = "%s" % String(d.get("display_name", "敌人"))
|
||||
_hp_bar.max_value = maxi(1, int(d.get("max_hp", 1)))
|
||||
@@ -74,9 +82,15 @@ func set_data(d: Dictionary) -> void:
|
||||
_set_order(int(d.get("order", 1)))
|
||||
_armor = String(d.get("armor_type", "light"))
|
||||
_armor_class = int(d.get("armor_class", 0))
|
||||
set_intent(String(d.get("beat", "")), int(d.get("ring_size", 0)), int(d.get("ring_index", 0)))
|
||||
set_pending(String(d.get("pending_type", "")), String(d.get("pending_name", "")))
|
||||
# 状态行:伏击池(flat 共享池,单独通道)+ 敌方增益 percent 状态(蓄力/硬化/削弱/暴走——side-wide)
|
||||
var parts: Array = []
|
||||
var ambush: int = int(d.get("ambush_layers", 0))
|
||||
_status_label.text = "伏击池 ×%d(全体共享)" % ambush if ambush > 0 else ""
|
||||
if ambush > 0:
|
||||
parts.append("伏击池 ×%d(全体共享)" % ambush)
|
||||
for eff in d.get("effects", []):
|
||||
parts.append("%s ×%d" % [String(eff.get("display_name", "")), int(eff.get("layers", 1))])
|
||||
_status_label.text = " · ".join(parts)
|
||||
_portrait.text = _portrait_glyph(String(d.get("enemy_id", "")))
|
||||
if _dead and int(d.get("hp", 0)) > 0:
|
||||
_revive()
|
||||
@@ -88,29 +102,20 @@ func _set_order(i: int) -> void:
|
||||
_name_label.text = "%s %s" % [String("①②③"[clampi(i - 1, 0, 2)]), base]
|
||||
|
||||
|
||||
# beat = 敌人系统 行为环词表原键(attack/charge/heavy,战斗结算 query_beat 直出——presenter 不翻译);
|
||||
# 名(BEAT_NAMES)/色(COL_BEAT)/重击标记(▼)渲染映射唯一定义处 = 本类(code-review 2026-09-12:
|
||||
# 双翻译曾使色相与重击通道永久失效)。信号/入参契约:set_data 每结算行刷新、set_intent 每拍刷新。
|
||||
func set_intent(beat: String, ring_size: int, ring_index: int) -> void:
|
||||
# 摇定动作渲染(敌人技能池批——presenter 不翻译,渲染映射唯一定义处归本类〔code-review 2026-09-12 先例〕):
|
||||
# pending_type = 战斗结算 快照投影(basic/damage/buff;"" = 未摇定/死亡残位)。
|
||||
# 信号/入参契约:set_data 每结算行刷新、set_pending 每轮开轮刷新(开轮摇号时点)。
|
||||
func set_pending(ptype: String, pname: String) -> void:
|
||||
if _dead:
|
||||
return
|
||||
var label := String(BEAT_NAMES.get(beat, beat))
|
||||
# 强度档位编码:重击 = 垂直延伸体量(占位:▼)+ 色相双通道(色弱冗余 = 文字常驻)
|
||||
var beat_col: Color = COL_BEAT.get(beat, Color8(90, 78, 66))
|
||||
_intent_badge.add_theme_color_override("font_color", beat_col)
|
||||
if label == "":
|
||||
_intent_badge.text = "意图:—"
|
||||
var style: Array = PENDING_STYLE.get(ptype, ["—", Color8(120, 106, 88)])
|
||||
_intent_badge.add_theme_color_override("font_color", style[1])
|
||||
if ptype == "" or ptype == "basic":
|
||||
_intent_badge.text = "意图:—" # 普通攻击零表示(不可预测——§2.4)
|
||||
else:
|
||||
_intent_badge.text = "意图:" + label + ("▼" if beat == "heavy" else "")
|
||||
# 环点 = 拍数直觉编码(● 已到拍 / ○ 未到)+ 护甲类型标签(克制对策的可读前提——恒常驻非悬停)
|
||||
# + 护甲等级 AC(掷点折扣阈值——2026-09-16 护甲等级掷点批;与护甲类型两轴分立可读)
|
||||
if ring_size > 0:
|
||||
var dots := ""
|
||||
for i in ring_size:
|
||||
dots += "●" if i == ring_index else "○"
|
||||
_ring.text = "%s · AC %d · 环 %s(%d 拍)" % [_armor_tag(_armor), _armor_class, dots, ring_size]
|
||||
else:
|
||||
_ring.text = "%s · AC %d" % [_armor_tag(_armor), _armor_class]
|
||||
_intent_badge.text = "意图:%s %s" % [String(style[0]), pname] # 特殊技类型级占位(纯提醒,不可撤销)
|
||||
# 护甲行(环点拆除——护甲类型 + AC 恒常驻,克制对策与掷点阈值的可读前提)
|
||||
_armor_line.text = "%s · AC %d" % [_armor_tag(_armor), _armor_class]
|
||||
|
||||
|
||||
func _armor_tag(armor: String) -> String:
|
||||
@@ -130,7 +135,7 @@ func mark_dead() -> void:
|
||||
_portrait.text = "✕"
|
||||
_intent_badge.text = "—— 击破 ——"
|
||||
_intent_badge.add_theme_color_override("font_color", Color8(120, 110, 98))
|
||||
_ring.text = ""
|
||||
_armor_line.text = ""
|
||||
_status_label.text = ""
|
||||
_hp_bar.value = 0
|
||||
|
||||
|
||||
@@ -10,10 +10,11 @@ extends HBoxContainer
|
||||
|
||||
const MAX_SLOTS := 4
|
||||
# 极性/语义映射(数据源 = SettlementTable.effect_pool_map 效果语义 + StatusEffectTable display_name):
|
||||
# 益 = ▲/◆/▼(金/蓝族);害 = ⚡/⚔/✕/☾(旗朱族)——形状+色相双通道,文字名常驻第三通道。
|
||||
# 状态表七行全映射(attack_up/ac_up/weaken/ambush/enrage/off_guard/sapped)——HUD框架 图标键契约
|
||||
# "一键一枚专属纹样,查不到的键立即断言 + ? 占位 + 诊断"(2026-09-15 格面资源批补 sapped 键;
|
||||
# 2026-09-16 护甲等级掷点批:defense_up → ac_up 换装)。
|
||||
# 益 = ▲/◆/▼/◇(金/蓝族);害 = ⚡/⚔/✕/☾/⇧/▣(旗朱族;⇧/▣ = 敌方增益威胁纹样)——形状+色相双通道,
|
||||
# 文字名常驻第三通道。状态表九行全映射(attack_up/ac_up/weaken/ambush/enrage/off_guard/sapped/empower/fortify)
|
||||
# ——HUD框架 图标键契约"一键一枚专属纹样,查不到的键立即断言 + ? 占位 + 诊断"
|
||||
# (2026-09-15 格面资源批补 sapped;2026-09-16 护甲等级掷点批 defense_up → ac_up 换装;
|
||||
# 同日 敌人技能池批补 empower/fortify 敌方增益两键——敌方增益显示 = 敌卡状态行文本 + 本图标键词汇)。
|
||||
const EFFECT_ICON := {
|
||||
"attack_up": ["▲", Color8(176, 122, 20), "益"],
|
||||
"ac_up": ["◇", Color8(46, 110, 142), "益"],
|
||||
@@ -22,6 +23,8 @@ const EFFECT_ICON := {
|
||||
"enrage": ["⚔", Color8(198, 61, 34), "害"],
|
||||
"off_guard": ["✕", Color8(198, 61, 34), "害"],
|
||||
"sapped": ["☾", Color8(198, 61, 34), "害"],
|
||||
"empower": ["⇧", Color8(198, 61, 34), "害"],
|
||||
"fortify": ["▣", Color8(120, 106, 88), "害"],
|
||||
}
|
||||
const EFFECT_SEMANTIC := {
|
||||
"attack_up": "主角攻击加成(hero_offense 池加算)",
|
||||
@@ -31,6 +34,8 @@ const EFFECT_SEMANTIC := {
|
||||
"enrage": "敌方输出加成(enemy_output 池加算——attack 型负向)",
|
||||
"off_guard": "主角承伤加深(hero_defense 池加算——defense 型负向)",
|
||||
"sapped": "主角输出降低(hero_offense 池加算——weaken 型负向)",
|
||||
"empower": "敌方输出 ×2(enemy_output 池加算——蓄力技施放;持续 1 个有效结算)",
|
||||
"fortify": "敌方防御 +100(enemy_defense 池——硬化技施放,主角攻击折半;持续 1 个有效结算)",
|
||||
}
|
||||
const SOURCE_NAMES := {"safe_cell": "安全格", "flag_hit": "标中旗", "mine_hit": "踩雷", "clear_reward": "清图奖励"}
|
||||
# 未知图标键的占位纹样(HUD框架:占位也要可见,禁静默空图标)
|
||||
|
||||
@@ -199,8 +199,8 @@ func _analyze_round(db, math, rows: Array, beats: Dictionary) -> Dictionary:
|
||||
var ticks: Array = []
|
||||
for row in rows:
|
||||
var t := String(row.get("type", ""))
|
||||
if t == "charge":
|
||||
lines.append("拍=charge(蓄力拍零兑付——设计语义,不读池)")
|
||||
if t == "cast":
|
||||
lines.append("拍=cast(施放行零兑付——增益技不读池,技能池批由蓄力行改型)")
|
||||
elif t == "damage" and String(row.get("source_class", "")) == "enemy_action":
|
||||
var baseline: int = int(row.get("baseline", 0))
|
||||
var attach: int = int(row.get("attach", 0))
|
||||
@@ -224,19 +224,15 @@ func _analyze_round(db, math, rows: Array, beats: Dictionary) -> Dictionary:
|
||||
return {"pd": pd, "pe": pe, "grants": grants, "lines": lines, "ticks": ticks}
|
||||
|
||||
|
||||
# 拍名重建:round r 的已打拍 = 快照 r-1 的敌人 beat 字段(快照取在 advance 之后 = 下一拍预付);
|
||||
# R1 = 行 pattern 首元素(steps 从 0 起)。快照 enemies 空时(敌死亡轮)回退 "?"——该轮敌已无行动行。
|
||||
# 拍名重建(敌人技能池批):pending_skill 字段 = 该结算开轮摇定的动作 id(快照取在结算末尾)——
|
||||
# round r 的拍 = 快照 r 的 enemies[0].pending_skill。快照 enemies 空时(敌死亡轮)回退 "?"——该轮敌已无行动行。
|
||||
func _played_beats(run: Dictionary, db) -> Dictionary:
|
||||
var beats := {}
|
||||
var erow: Dictionary = db.resolve_enemy_rowset(1, 4)
|
||||
var pat: Array = erow.get("pattern", [])
|
||||
if pat.size() > 0:
|
||||
beats[1] = String(pat[0])
|
||||
for r in range(2, run.snaps.size() + 1):
|
||||
var s: Dictionary = run.snaps[r - 2]
|
||||
for r in range(1, run.snaps.size() + 1):
|
||||
var s: Dictionary = run.snaps[r - 1]
|
||||
var en: Array = s.get("enemies", [])
|
||||
if en.size() > 0:
|
||||
beats[r] = String(en[0].get("beat", "?"))
|
||||
beats[r] = String(en[0].get("pending_skill", "?"))
|
||||
return beats
|
||||
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ func _initialize() -> void:
|
||||
check(rng.derive_at("reward_roll", 0) == 7422916049646135537, "R2 向量 reward_roll m=42 scope=0")
|
||||
check(rng.derive_at("encounter", 0) == 1061841193803499209, "R2 向量 encounter m=42 scope=0(registry salt_encounter=0x8EBC6AF09C88C6E3 权威值)")
|
||||
check(rng.derive_at("armor_roll", 0) == 4806095330042243845, "R2 向量 armor_roll m=42 scope=0(护甲等级掷点批 2026-09-16——registry salt_armor_roll=0xC2B2AE3D27D4EB2F 权威值)")
|
||||
check(rng.derive_at("behavior_roll", 0) == 426657203655691839, "R2 向量 behavior_roll m=42 scope=0(敌人技能池批 2026-09-16——registry salt_behavior_roll=0x85EBCA7758C24E15 权威值)")
|
||||
check(rng.derive_at("board_gen", 0) == rng.derive_at("board_gen", 0), "derive_at 纯函数恒等")
|
||||
# scope 取号语义:首个 open_stream = scope 0;计数器单调
|
||||
var s0 = rng.open_stream("board_gen")
|
||||
@@ -83,6 +84,7 @@ func _run_core_smoke(db, rng) -> void:
|
||||
_smoke_negative_dictionary()
|
||||
_smoke_resource_surface()
|
||||
_smoke_settlement_segments()
|
||||
_smoke_skill_pool()
|
||||
|
||||
|
||||
# ---- 地图战斗资源·身份面单元冒烟(格面资源批:roll 恰一次 / 雷安同池 / 幂等 / 清图批尾)----
|
||||
@@ -346,7 +348,8 @@ func _first_hero_attack_rows(seed_val: int) -> Dictionary:
|
||||
return {}
|
||||
|
||||
|
||||
# 削弱 ×2(池 enemy_output 触底 ×0)下的敌方攻击伤害行采样(D2 熊地精——首拍攻击)
|
||||
# 削弱 ×2(池 enemy_output 触底 ×0)下的敌方攻击伤害行采样(D2 熊地精)。
|
||||
# 敌人技能池批:开轮摇号下首拍可能是施放行(零伤害)——逐轮重授削弱并停手,直到采到伤害行为止。
|
||||
func _weaken_zero_round_rows() -> Array:
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(20260916)
|
||||
@@ -356,36 +359,47 @@ func _weaken_zero_round_rows() -> Array:
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)}):
|
||||
return rows
|
||||
# 测试通道:直授削弱 ×2(来源格取真实坐标——行来源断言要求;时效 1 当轮结算内有效)
|
||||
for i in range(2):
|
||||
settlement._status_effects.apply_effect("weaken", {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": 1})
|
||||
settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var out: Array = []
|
||||
for row in rows:
|
||||
if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "enemy_action":
|
||||
out.append(row)
|
||||
var guard := 8
|
||||
while out.is_empty() and guard > 0 and settlement.battle_result == "":
|
||||
guard -= 1
|
||||
rows.clear()
|
||||
for i in range(2):
|
||||
settlement._status_effects.apply_effect("weaken", {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index})
|
||||
settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
for row in rows:
|
||||
if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "enemy_action":
|
||||
out.append(row)
|
||||
return out
|
||||
|
||||
|
||||
# AC12 折扣档采样:每停手轮前直授 ac_up ×2(时效 1 逐轮衰减)——Boss 行攻击/重击拍逐拍判定留档
|
||||
# AC12 折扣档采样:每停手轮前直授 ac_up ×2(时效 1 逐轮衰减)——敌拍逐拍判定留档。
|
||||
# 敌人技能池批:拍序改为摇号制,原 AC 批定影种子不再保证档位多样性——扫描种子取首个 ≥2 档样本集。
|
||||
func _ac12_tier_samples() -> Array:
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(20260917)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)}):
|
||||
return []
|
||||
var guard := 12
|
||||
while settlement.battle_result == "" and guard > 0:
|
||||
guard -= 1
|
||||
for i in range(2):
|
||||
settlement._status_effects.apply_effect("ac_up", {"source_class": "safe_cell", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index})
|
||||
settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var tiers: Array = []
|
||||
for row in rows:
|
||||
if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "enemy_action":
|
||||
tiers.append(int(row.get("tier", -1)))
|
||||
return tiers
|
||||
for seed_offset in range(20):
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(20260917 + seed_offset)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)}):
|
||||
continue
|
||||
var guard := 12
|
||||
while settlement.battle_result == "" and guard > 0:
|
||||
guard -= 1
|
||||
for i in range(2):
|
||||
settlement._status_effects.apply_effect("ac_up", {"source_class": "safe_cell", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index})
|
||||
settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var tiers: Array = []
|
||||
for row in rows:
|
||||
if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "enemy_action":
|
||||
tiers.append(int(row.get("tier", -1)))
|
||||
var distinct := {}
|
||||
for t in tiers:
|
||||
distinct[int(t)] = true
|
||||
if tiers.size() >= 3 and distinct.size() >= 2:
|
||||
return tiers
|
||||
return []
|
||||
|
||||
|
||||
func _smoke_board_generator() -> void:
|
||||
@@ -960,12 +974,12 @@ func _smoke_attack_chain_fresh_pool() -> void:
|
||||
ticks.append({"baseline": int(row.baseline), "attach": int(row.attach), "roll": int(row.roll),
|
||||
"tier": int(row.tier), "crit": bool(row.get("crit", false)), "tick": int(row.value)}))
|
||||
check(settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "fresh 池回归战斗启动(Boss 行)")
|
||||
# 停手轮:零挖掘零旗 → 零授予 → 池恒空;Boss pattern 前 4 拍 = attack/attack/charge/heavy
|
||||
for i in range(4):
|
||||
# 停手轮:零挖掘零旗 → 零授予 → 池恒空;Boss 拍 = 开轮摇号结果(伤害技/普攻兑付伤害行、硬化施放行不计)
|
||||
for i in range(6):
|
||||
if settlement.battle_result != "":
|
||||
break
|
||||
settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
check(ticks.size() >= 2, "非蓄力攻击拍采样充足(%d 拍——charge 拍零兑付为设计语义不计)" % ticks.size())
|
||||
check(ticks.size() >= 2, "非施放攻击拍采样充足(%d 拍——增益施放行零兑付为设计语义不计)" % ticks.size())
|
||||
var all_full := true
|
||||
for t in ticks:
|
||||
var base_factor: float = float(t.tier) * (float(_db.settlement.crit_multiplier) / 100.0 if t.crit else 1.0)
|
||||
@@ -1383,7 +1397,7 @@ func _row_phase(row: Dictionary, ri: int) -> int:
|
||||
return 3
|
||||
"verdict":
|
||||
return 4
|
||||
"charge":
|
||||
"cast":
|
||||
return 2
|
||||
"damage":
|
||||
if String(row.get("source_class", "")) in ["hero_attack", "enemy_action"]:
|
||||
@@ -1473,3 +1487,285 @@ func check_silent(cond: bool) -> void:
|
||||
if not cond:
|
||||
_failures += 1
|
||||
print(" [FAIL] (silent check)")
|
||||
|
||||
|
||||
# ==================== 敌人技能池批(2026-09-16 用户拍板——开轮摇号出招系统) ====================
|
||||
# 六项验收断言(docs/proposal-skill-pool.md §8 批 1):同种子回放逐位复现 / 单敌轨迹独立复放 /
|
||||
# EV 抽样对锚 / 死亡打断作废 / 空池不可能 / 加载校验拒绝。
|
||||
|
||||
func _smoke_skill_pool() -> void:
|
||||
print("--- 敌人技能池(开轮摇号 · 冷却/保底两闸 · 三乘法链 · 独立子流)---")
|
||||
_smoke_skill_pool_data()
|
||||
_smoke_skill_pool_replay_independence()
|
||||
_smoke_skill_pool_ev_anchor()
|
||||
_smoke_skill_pool_death_void()
|
||||
_smoke_skill_pool_validation_reject()
|
||||
|
||||
|
||||
# ① 数据面:技能表行集 + 普攻锁死 + 建议池行值 + 空池不可能(结构断言)
|
||||
func _smoke_skill_pool_data() -> void:
|
||||
check(_db.skill.rows.size() == 5, "技能表五行(普攻/突刺/蓄力/重击/硬化)")
|
||||
var basic: Dictionary = _db.skill.row_by_id("attack")
|
||||
check(not basic.is_empty() and String(basic.type) == "basic" and int(basic.cooldown_beats) == 0
|
||||
and (not basic.has("damage_mult") or is_equal_approx(float(basic.damage_mult), 1.0)),
|
||||
"普攻行:倍率恒 1 · 冷却恒 0(域校验锁死——池恒非空的结构前提)")
|
||||
var lunge: Dictionary = _db.skill.row_by_id("lunge")
|
||||
check(not lunge.is_empty() and is_equal_approx(float(lunge.damage_mult), 1.5) and int(lunge.cooldown_beats) == 2,
|
||||
"突刺行(伤害 ×1.5 · 冷却 2——提案 §2.5 建议池新行)")
|
||||
var charge_row: Dictionary = _db.skill.row_by_id("charge")
|
||||
check(not charge_row.is_empty() and String(charge_row.buff_ref) == "empower",
|
||||
"蓄力行 = 纯攻击增益技(0 伤害、buff_ref → empower——与重击无组合语义)")
|
||||
var expect_pools := {
|
||||
"goblin": {"attack": 9, "lunge": 1},
|
||||
"bugbear": {"attack": 6, "charge": 2, "heavy": 2},
|
||||
"ogre_chief": {"attack": 6, "heavy": 2, "harden": 2},
|
||||
}
|
||||
var pools_ok := true
|
||||
for row in _db.enemy.rows:
|
||||
var eid := String(row.enemy_id)
|
||||
if not expect_pools.has(eid):
|
||||
pools_ok = false
|
||||
continue
|
||||
var got := {}
|
||||
for entry in row.pool:
|
||||
got[String(entry.skill_id)] = int(entry.weight)
|
||||
if got != expect_pools[eid]:
|
||||
pools_ok = false
|
||||
check(pools_ok, "六行池模板 = 建议池行值(地精 9/1 · 熊地精 6/2/2 · 酋长 6/2/2——全部 provisional)")
|
||||
var basic_count_ok := true
|
||||
for row in _db.enemy.rows:
|
||||
var n := 0
|
||||
for entry in row.pool:
|
||||
var srow: Dictionary = _db.skill_row(String(entry.skill_id))
|
||||
if not srow.is_empty() and String(srow.type) == "basic":
|
||||
n += 1
|
||||
if n != 1:
|
||||
basic_count_ok = false
|
||||
check(basic_count_ok, "每池恰一行普攻(空池不可能的加载校验同款)")
|
||||
var rollable_ok := true
|
||||
for depth in [0, 1, 2, 3, 4]:
|
||||
var es = load("res://core/unit_container.gd").new(_db)
|
||||
es.spawn(1, int(depth), 50)
|
||||
for u in es.all_enemies():
|
||||
if es.rollable_skills(u).is_empty():
|
||||
rollable_ok = false
|
||||
check(rollable_ok, "全深度实例 spawn 后可摇集合恒非空(普攻恒冷却 0——容器级断言)")
|
||||
# 状态表新行 + 敌方防御池收编
|
||||
var empower: Dictionary = _db.status_effect_row("empower")
|
||||
var fortify: Dictionary = _db.status_effect_row("fortify")
|
||||
check(not empower.is_empty() and String(empower.target_side) == "enemy" and int(empower.magnitude) == 100,
|
||||
"蓄力状态行 empower(enemy +100 percent——敌方输出 ×2 走既有 enemy_output 池)")
|
||||
check(not fortify.is_empty() and String(fortify.target_side) == "enemy" and int(fortify.magnitude) == 100,
|
||||
"硬化状态行 fortify(enemy +100 percent——敌方防御池新词汇载体)")
|
||||
check(String(_db.settlement.effect_pool_map.get("empower", "")) == "enemy_output" \
|
||||
and String(_db.settlement.effect_pool_map.get("fortify", "")) == "enemy_defense",
|
||||
"effect_pool_map 收编 empower/fortify(敌方防御池 enemy_defense 入表)")
|
||||
|
||||
|
||||
# ② 同种子回放逐位复现 + ③ 单敌轨迹独立复放(D3 熊地精双雄——多实例同种)
|
||||
func _smoke_skill_pool_replay_independence() -> void:
|
||||
var seed_val := -1
|
||||
for s in range(1, 60):
|
||||
var seqs := _record_pool_trajectories(s, 1)
|
||||
if not seqs.is_empty() and int(seqs.count) >= 2:
|
||||
seed_val = s
|
||||
break
|
||||
check(seed_val > 0, "多实例种子可达(seed=%d——D3 遭遇 ≥2 实例)" % seed_val)
|
||||
var rounds := 10
|
||||
var run1 := _record_pool_trajectories(seed_val, rounds)
|
||||
var run2 := _record_pool_trajectories(seed_val, rounds)
|
||||
check(not run1.is_empty() and JSON.stringify(run1.seqs) == JSON.stringify(run2.seqs),
|
||||
"同种子回放:各实例摇定动作序列逐位相同(×%d 轮 × %d 实例)" % [rounds, int(run1.count)])
|
||||
var indep_ok := true
|
||||
var detail := ""
|
||||
var units: Array = _db.resolve_enemy_rowset(1, 3).pool
|
||||
for i in range(int(run1.count)):
|
||||
var replayed := _replay_single_trajectory(seed_val, units, rounds, i)
|
||||
if JSON.stringify(replayed) != JSON.stringify(run1.seqs[i]):
|
||||
indep_ok = false
|
||||
detail += " #%d" % i
|
||||
check(indep_ok, "单敌轨迹独立复放(只喂该敌子流 scope=spawn_index×%d+序 即可逐位重建%s——互不穿插)" % [load("res://core/rng_service.gd").BEHAVIOR_SUBSTREAM_STRIDE, detail])
|
||||
|
||||
|
||||
# 多敌战逐轮记录各实例摇定动作(hero_atk=1/hp=300 构造全员存活满 N 轮——轨迹长度 = 轮数)
|
||||
func _record_pool_trajectories(seed_val: int, rounds: int) -> Dictionary:
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(seed_val)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 3, "hero_hp": 300, "hero_max_hp": 300, "hero_atk": 1}):
|
||||
return {}
|
||||
var seqs: Array = []
|
||||
for u in settlement._enemy_system.all_enemies():
|
||||
seqs.append([])
|
||||
for r in rounds:
|
||||
var handles: Array = settlement._enemy_system.all_enemies()
|
||||
for i in handles.size():
|
||||
seqs[i].append(settlement._enemy_system.pending_action(handles[i]))
|
||||
settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
if settlement.battle_result != "":
|
||||
return {}
|
||||
return {"count": seqs.size(), "seqs": seqs}
|
||||
|
||||
|
||||
# 独立重放器(镜像 开轮摇号 + 容器冷却/保底规则——只消费目标实例自己的子流 scope 段)
|
||||
func _replay_single_trajectory(seed_val: int, pool_template: Array, rounds: int, spawn_index: int) -> Array:
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(seed_val)
|
||||
var stride: int = load("res://core/rng_service.gd").BEHAVIOR_SUBSTREAM_STRIDE
|
||||
var pool: Array = []
|
||||
for entry in pool_template:
|
||||
pool.append({"skill_id": String(entry.skill_id), "weight": int(entry.weight), "cooldown_left": 0})
|
||||
var streak := 0
|
||||
var pity_m: int = int(_db.skill.pity_threshold)
|
||||
var out: Array = []
|
||||
for k in rounds:
|
||||
var entries: Array = []
|
||||
for e in pool:
|
||||
if int(e.cooldown_left) <= 0:
|
||||
entries.append({"skill_id": e.skill_id, "weight": e.weight})
|
||||
if streak >= pity_m:
|
||||
var specials: Array = []
|
||||
for e in pool:
|
||||
var sr: Dictionary = _db.skill_row(String(e.skill_id))
|
||||
if not sr.is_empty() and String(sr.type) != "basic":
|
||||
specials.append({"skill_id": e.skill_id, "weight": e.weight})
|
||||
if not specials.is_empty():
|
||||
entries = specials
|
||||
var sid := SkillLottery.draw(entries, rng.open_scope("behavior_roll", spawn_index * stride + k))
|
||||
out.append(sid)
|
||||
var srow: Dictionary = _db.skill_row(sid)
|
||||
for e in pool:
|
||||
e.cooldown_left = maxi(0, int(e.cooldown_left) - 1) # 开轮递减(摇号读值之后——与容器同序)
|
||||
if String(e.skill_id) == sid:
|
||||
e.cooldown_left = int(srow.cooldown_beats) # 执行上报置冷却(战斗段时点)
|
||||
streak = streak + 1 if String(srow.type) == "basic" else 0
|
||||
return out
|
||||
|
||||
|
||||
# ④ EV 抽样对锚(含闸长期速率 vs 静态锚——proposal §2.5;精确重锚归 难度曲线与Boss)。
|
||||
# 带宽口径:熊地精/酋长 EV 结构性恒 1(普攻 1 + 重击 2 与蓄力/硬化 0 加权守恒)→ 窄带;
|
||||
# 地精突刺池受保底闸抬升(唯一特殊技 w=1/10,保底 M=4 把突刺率从自然 10% 抬到 ~20%+)→ 宽带登记。
|
||||
func _smoke_skill_pool_ev_anchor() -> void:
|
||||
var bands := {"goblin": [1.00, 1.20], "bugbear": [0.97, 1.03], "ogre_chief": [0.97, 1.03]}
|
||||
var species := ["goblin", "bugbear", "ogre_chief"]
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(20260921)
|
||||
var stride: int = load("res://core/rng_service.gd").BEHAVIOR_SUBSTREAM_STRIDE
|
||||
var n_beats := 5000
|
||||
var pity_m: int = int(_db.skill.pity_threshold)
|
||||
for si in species.size():
|
||||
var eid: String = species[si]
|
||||
var template: Array = []
|
||||
for row in _db.enemy.rows:
|
||||
if String(row.enemy_id) == eid:
|
||||
template = row.pool
|
||||
break
|
||||
var pool: Array = []
|
||||
for entry in template:
|
||||
pool.append({"skill_id": String(entry.skill_id), "weight": int(entry.weight), "cooldown_left": 0})
|
||||
var streak := 0
|
||||
var ev_sum := 0.0
|
||||
for k in n_beats:
|
||||
var entries: Array = []
|
||||
for e in pool:
|
||||
if int(e.cooldown_left) <= 0:
|
||||
entries.append({"skill_id": e.skill_id, "weight": e.weight})
|
||||
if streak >= pity_m:
|
||||
var specials: Array = []
|
||||
for e in pool:
|
||||
var sr: Dictionary = _db.skill_row(String(e.skill_id))
|
||||
if not sr.is_empty() and String(sr.type) != "basic":
|
||||
specials.append({"skill_id": e.skill_id, "weight": e.weight})
|
||||
if not specials.is_empty():
|
||||
entries = specials
|
||||
var sid := SkillLottery.draw(entries, rng.open_scope("behavior_roll", si * stride + k))
|
||||
var srow: Dictionary = _db.skill_row(sid)
|
||||
var mult := 0.0
|
||||
if String(srow.type) == "basic":
|
||||
mult = 1.0
|
||||
elif String(srow.type) == "damage":
|
||||
mult = float(srow.get("damage_mult", 1.0))
|
||||
ev_sum += mult
|
||||
for e in pool:
|
||||
e.cooldown_left = maxi(0, int(e.cooldown_left) - 1)
|
||||
if String(e.skill_id) == sid:
|
||||
e.cooldown_left = int(srow.cooldown_beats)
|
||||
streak = streak + 1 if String(srow.type) == "basic" else 0
|
||||
var measured: float = ev_sum / float(n_beats)
|
||||
var band: Array = bands[eid]
|
||||
var in_band: bool = measured >= float(band[0]) and measured <= float(band[1])
|
||||
check(in_band, "EV 抽样对锚 %s(实测 %.4f ∈ [%.2f, %.2f] 含闸长期速率,×%d 拍)" % [eid, measured, float(band[0]), float(band[1]), n_beats])
|
||||
|
||||
|
||||
# ⑤ 死亡打断作废(摇定后、兑现前死亡 → 本拍技能作废、不动手、不进账)
|
||||
func _smoke_skill_pool_death_void() -> void:
|
||||
var void_seed := -1
|
||||
for s in range(1, 400):
|
||||
var rg = load("res://core/rng_service.gd").new()
|
||||
rg.set_master_seed(s)
|
||||
var erow: Dictionary = _db.resolve_enemy_rowset(1, 2)
|
||||
var entries: Array = []
|
||||
for entry in erow.pool:
|
||||
entries.append({"skill_id": String(entry.skill_id), "weight": int(entry.weight)})
|
||||
var sid := SkillLottery.draw(entries, rg.open_scope("behavior_roll", 0))
|
||||
if String(_db.skill_row(sid).type) != "basic":
|
||||
void_seed = s
|
||||
break
|
||||
check(void_seed > 0, "死亡打断种子可达(seed=%d——D2 首拍摇中特殊技)" % void_seed)
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(void_seed)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
check(settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": 50, "hero_atk": 999}),
|
||||
"死亡打断战斗启动(hero_atk 999——先手必杀 40HP 熊地精)")
|
||||
var u: Dictionary = settlement._enemy_system.all_enemies()[0] # 句柄预捕获(胜利 despawn 后容器已清,字典引用仍可读)
|
||||
var pending: String = settlement._enemy_system.pending_action(u)
|
||||
check(pending != "", "开轮已摇定本拍动作(待发 = %s)" % pending)
|
||||
settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var enemy_rows := 0
|
||||
for row in rows:
|
||||
if String(row.get("source_class", "")) == "enemy_action":
|
||||
enemy_rows += 1
|
||||
check(enemy_rows == 0, "先手击杀 → 摇定技能作废(第 1 轮零敌方行动行——死亡即不动手)")
|
||||
var cd_sum := 0
|
||||
for e in u.pool:
|
||||
cd_sum += int(e.cooldown_left)
|
||||
check(cd_sum == 0 and int(u.basic_streak) == 0,
|
||||
"作废不进账(全池冷却 0 · 保底计数 0——执行上报未发生)")
|
||||
|
||||
|
||||
# ⑥ 加载校验拒绝(单表域校验 + 跨表池构成纪律)
|
||||
func _smoke_skill_pool_validation_reject() -> void:
|
||||
var skill_table = load("res://data/schemas/skill_table.gd").new()
|
||||
var enemy_table = load("res://data/schemas/enemy_table.gd").new()
|
||||
enemy_table.rows = [{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "ghost", "weight": 1}]}]
|
||||
check(_has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "dangling_skill"),
|
||||
"加载校验拒绝:池条目 skill_id 悬空致命")
|
||||
skill_table.rows = [{"skill_id": "a", "display_name": "a", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2}]
|
||||
check(_has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition"),
|
||||
"加载校验拒绝:池普攻行数 ≠ 1(空池不可能前提)")
|
||||
skill_table.rows = [
|
||||
{"skill_id": "b1", "display_name": "b1", "type": "buff", "buff_ref": "empower", "buff_duration": 2, "cooldown_beats": 3},
|
||||
{"skill_id": "b2", "display_name": "b2", "type": "buff", "buff_ref": "fortify", "buff_duration": 2, "cooldown_beats": 4}]
|
||||
enemy_table.rows = [{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "b1", "weight": 1}, {"skill_id": "b2", "weight": 1}]}]
|
||||
check(_has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition"),
|
||||
"加载校验拒绝:池增益技 > 1(每池增益 ≤1 纪律)")
|
||||
skill_table.rows = [{"skill_id": "atk", "display_name": "a", "type": "basic", "damage_mult": 2.0, "cooldown_beats": 0}]
|
||||
check(not skill_table.validate().is_empty(), "加载校验拒绝:普攻配倍率 ≠ 1(域校验锁死)")
|
||||
skill_table.rows = [{"skill_id": "atk", "display_name": "a", "type": "basic", "cooldown_beats": 2}]
|
||||
check(not skill_table.validate().is_empty(), "加载校验拒绝:普攻配冷却 ≠ 0(域校验锁死)")
|
||||
skill_table.rows = [{"skill_id": "d", "display_name": "d", "type": "damage", "damage_mult": 30.0, "cooldown_beats": 1}]
|
||||
check(not skill_table.validate().is_empty(), "加载校验拒绝:damage_mult 越域 [1.1,16]")
|
||||
var et2 = load("res://data/schemas/enemy_table.gd").new()
|
||||
et2.rows = [{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "a", "weight": 0}]}]
|
||||
check(not et2.validate().is_empty(), "加载校验拒绝:池权重 ≤ 0")
|
||||
et2.rows = [{"enemy_id": "x", "display_name": "x", "pool": []}]
|
||||
check(not et2.validate().is_empty(), "加载校验拒绝:空池(禁)")
|
||||
|
||||
|
||||
func _has_err_category(errs: Array, category: String) -> bool:
|
||||
for e in errs:
|
||||
if String(e.get("category", "")) == category:
|
||||
return true
|
||||
return false
|
||||
Reference in new issue
Block a user