初次提交
This commit is contained in:
+141
@@ -0,0 +1,141 @@
|
||||
# 魔法工匠 (Arcane Artificer) - 文档索引
|
||||
|
||||
> Roguelite 动作射击游戏 | Godot 4.x + GDScript
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
docs/
|
||||
README.md 本文件,文档导航索引
|
||||
design/ 游戏设计文档
|
||||
technical/ 技术架构文档
|
||||
mechanics/ 机制细节设计
|
||||
plan/ 垂直切片开发计划与平台认证清单
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工程计划 (`plan/`)
|
||||
|
||||
| 文档 | 内容简述 |
|
||||
| :--- | :--- |
|
||||
| [development_plan.md](plan/development_plan.md) | S0–S6 骨架垂直切片、风险登记、验收与出口检查、跨切片约束、参考文档地图(与 `technical/` 架构对齐) |
|
||||
| [certification_checklist.md](plan/certification_checklist.md) | Steam / Switch / 无障碍 / 发布前检查项,并映射到切片 |
|
||||
|
||||
---
|
||||
|
||||
## 游戏设计 (`design/`)
|
||||
|
||||
| 文档 | 内容简述 |
|
||||
| :--- | :--- |
|
||||
| [game_design.md](design/game_design.md) | 核心玩法 GDD:法术管道系统、游戏循环、构建示例、Endless 排行榜 |
|
||||
| [numerical_design.md](design/numerical_design.md) | 数值策划:资源模型、XP 曲线公式、法术参数表、怪物成长曲线、Boss DPS 门槛、平衡性对策 |
|
||||
| [core_wand_design.md](design/core_wand_design.md) | Core 设计:插槽拓扑、MATRIX 邻接加成效果表、Feature Tags 常量规范、dual_stream 奇数槽行为 |
|
||||
|
||||
---
|
||||
|
||||
## 技术架构 (`technical/`)
|
||||
|
||||
| 文档 | 内容简述 |
|
||||
| :--- | :--- |
|
||||
| [architecture_design.md](technical/architecture_design.md) | 系统分层架构(含 ZoneManager/VFXManager/CoreFeatureTag)、HMWS 法术系统设计、ECS-Lite、超大弹体 SpatialGrid 豁免、Feature Tags 常量 ADR、DamageType 枚举、自动瞄准算法、consume_inputs 掩码机制、技术栈选型 |
|
||||
| [implementation_plan.md](technical/implementation_plan.md) | 各模块详细实现方案(含 VFXManager 正式定义)、EventBus 完整事件目录、SubPayloadRegistry 竞争条件保证、DamageContextPool 定义、PlayerManager 移动状态追踪、DPS 面板实现、开发阶段规划、关键技术难点预案 |
|
||||
|
||||
---
|
||||
|
||||
## 机制细节 (`mechanics/`)
|
||||
|
||||
| 文档 | 内容简述 |
|
||||
| :--- | :--- |
|
||||
| [combat_mechanics_depth.md](mechanics/combat_mechanics_depth.md) | 投射物溯源、连锁弹射、状态效果、伤害计算管线 |
|
||||
| [combat_mechanics_extensions_v2.md](mechanics/combat_mechanics_extensions_v2.md) | 物理运动学、环境场、触发链、属性快照、Proc 系数 |
|
||||
| [weapon_system_expansion.md](mechanics/weapon_system_expansion.md) | 轨迹修正器(正弦/回旋镖/环绕-双写优化)、触发器系统、元素交互 |
|
||||
| [consecutive_hits_stacking.md](mechanics/consecutive_hits_stacking.md) | 连击窗口、叠加收益、Combo Tracker 数据结构设计、COMBO_MARK tick_interval 语义说明 |
|
||||
| [advanced_mechanics_summons_and_environment.md](mechanics/advanced_mechanics_summons_and_environment.md) | 召唤体系(炮台/随从/卫星)、环境传播、地面效果、StatusID Autoload 常量规范 |
|
||||
|
||||
---
|
||||
|
||||
## 核心设计理念
|
||||
|
||||
- **"Noita 遇上 Brotato"**:快节奏割草 × 深度法术编程
|
||||
- **法术管道 (Spell Pipeline)**:ACTION → MODIFIER → TRIGGER → LOGIC GATE 四类节点组合,实现图灵完备的武器逻辑
|
||||
- **数值哲学**:`构建强度 = (基础数值 × 修正系数) ^ 逻辑复杂度`,提供宽广边界而非限制玩家策略
|
||||
|
||||
---
|
||||
|
||||
## 关键规范速查 (Design Rules Quick Reference)
|
||||
|
||||
| 规范 ID | 位置 | 内容摘要 |
|
||||
| :--- | :--- | :--- |
|
||||
| P6-N2 | architecture_design.md §4.2 | Homing 弹共用敌人位置快照 |
|
||||
| P6-N3 | architecture_design.md §4.1 | SpellContext 池大小 = 32 |
|
||||
| P6-N5 | implementation_plan.md §2.2 | CompiledDeck 多项式滚动哈希(顺序敏感,种子含 grid_cols,防换位误判) |
|
||||
| P6-N6 | implementation_plan.md §2.1 | EventID 12 MINION_EXPIRED |
|
||||
| P6-N10 | implementation_plan.md §2.2 | SubPayloadRegistry 竞争条件安全保证 |
|
||||
| P6-N11 | architecture_design.md §4.3 | 超大弹体(radius>64px)豁免 SpatialGrid |
|
||||
| P6-N12 | core_wand_design.md §3 | dual_stream 奇数槽前流多得中间槽 |
|
||||
| P6-N13 | core_wand_design.md §2.2 | MATRIX 邻接加成效果表 |
|
||||
| P6-N14 | consecutive_hits_stacking.md | COMBO_MARK tick_interval 语义重载说明 |
|
||||
| P6-N15 | weapon_system_expansion.md §1.3 | 环绕弹先判断 is_orbiting 再决定是否积分 |
|
||||
| P6-N16 | numerical_design.md §1.2 | XP 曲线:`⌊10 × 1.4^(level-1)⌋` |
|
||||
| P6-N17 | numerical_design.md §5.D | W20 Boss 物理构建 DPS 门槛 |
|
||||
| P6-N18 | numerical_design.md §5.E | infinite_spells + heavy_cost 叠加规则 |
|
||||
| P6-N19 | game_design.md §8.2 | Endless 排行榜单整数双维度评分方案 |
|
||||
| P6-N20 | architecture_design.md §3.4.A | _flatten_matrix 仅遍历 Row A,Row B 不追加为执行节点 |
|
||||
| P6-N21 | architecture_design.md §8 ADR-R5-N1 | ZoneManager 首元素删除用循环前移,不用 remove_at() |
|
||||
| P6-N22 | numerical_design.md §1.2 | XP 可行性验证:Level 20 为 20 波通关上限,Level 21+ 服务 Endless |
|
||||
| P6-N23 | numerical_design.md §1.2 | 商店刷新费用公式:20 + (本波刷新次数 × 10)G,波次结束重置 |
|
||||
| P6-N24 | architecture_design.md §4.2 | ProjectileDef(生成时模板)vs _bullet_contexts(运行时可变冷状态)职责分离 |
|
||||
| P6-N25 | architecture_design.md §4.2 | _enemy_pos_snapshot 必须声明为类成员(预分配复用,禁止每帧 var 分配) |
|
||||
| P6-N26 | architecture_design.md §8 ADR-R5-N1 | ZoneManager ZONE_STRIDE=8(含 tick_interval+tick_accum),spawn_zone 需 tick_interval 参数 |
|
||||
| P6-N27 | implementation_plan.md §2.3.C | _visible_flags 类成员 PackedByteArray;has_point() 赋值须显式 int() 转换 |
|
||||
| P6-N28 | implementation_plan.md §2.1 | DamageContextPool Autoload 定义(acquire/get_context/release) |
|
||||
| P6-N29 | architecture_design.md §3.4.C | consume_inputs 运行时通过 _consumed 掩码实现,CompiledDeck 只读不修改 |
|
||||
| P6-N30 | advanced_mechanics §3.2 | StatusID Autoload 常量规范(禁止 StatusType.XXX 枚举写法) |
|
||||
| P6-N31 | architecture_design.md §3.2 | ProjectileDef.reset() 必须用 DamageType.PHYSICAL(禁止裸整数 0) |
|
||||
| P6-N32 | architecture_design.md §3.2 | SpellContext 必须提供 reset() 方法;registers 不在 reset() 中清零(由 SpellEvaluator 按持久化策略决定) |
|
||||
| P6-N33 | architecture_design.md §3.4.A | _flatten_circuit() 正式算法:Kahn 拓扑排序 + splitter 展开为 SubPayload LOGIC_FORK;环路时返回空 CompiledDeck |
|
||||
| P6-N34 | combat_mechanics_depth.md §5 | BulletContext 禁止持有 calc_damage() 等伤害计算逻辑(SRP);弹射衰减由 EnemyManager.apply_damage() 用 bounce_count 计算 |
|
||||
| P6-N35 | combat_mechanics_depth.md §4 / implementation_plan.md §2.1 | DamageContext 权威字段:base_damage/**mult**/damage_type/owner_id/source_tags/is_crit/pierce_rate;DamageContextPool.acquire() 含 is_crit+pierce_rate 参数;`reset()` 必须重置 mult=1.0 |
|
||||
| P6-N36 | implementation_plan.md §2.1 | EventID.DASH_TRIGGERED = 13;负载: caster_id+stationary_time;发出方 PlayerManager,订阅方 SpellEvaluator+UIManager |
|
||||
| P6-N37 | implementation_plan.md §2.2 | LOGIC_EVERY_N_SHOTS 必须读写 SpellContext.registers(跨帧持久),严禁使用 CastState.registers(每帧重置) |
|
||||
| P6-N38 | implementation_plan.md §2.5.E | DPS 环形缓冲区:PackedFloat64Array+PackedFloat32Array,_RB_SIZE=256,O(1) 写入,消除 pop_front() O(N) 移位 |
|
||||
| P6-N39 | consecutive_hits_stacking.md / advanced_mechanics §3.2 | StatusID.VULNERABILITY=8,StatusID.COMBO_MARK=7;新增 ID 从 9 开始递增 |
|
||||
| P6-N40 | architecture_design.md §3.2 | SpellContext.stats 必须声明时初始化(= CastStats.new()),防止 reset() 空指针 |
|
||||
| P6-N41 | architecture_design.md §3.2 | ProjectileDef.reset() 必须重置 spawn_position(防止池化复用携带上帧发射原点) |
|
||||
| P6-N42 | core_wand_design.md §1 | CoreDefinition 新增 @export var edges: Array = [](CIRCUIT 有向边顶层字段);_flatten_circuit() 从 core.edges 读取,不在 slot_configs 内嵌 "edges" |
|
||||
| P6-N43 | architecture_design.md §3.4.A | _flatten_circuit() 降级为 LINEAR 时使用 CompiledDeck.new(deck.nodes) 内联(_flatten_linear() 函数不存在) |
|
||||
| P6-N44 | architecture_design.md §3.4.A | LOGIC_FORK 节点:type=LOGIC, id="LOGIC_FORK",meta "fork_branch_ids";_execute_logic 并行触发所有分支 SubPayload |
|
||||
| P6-N45 | implementation_plan.md §2.3.C | ENEMY_STRIDE: int = 8 命名常量,与 BULLET_STRIDE=12 规范对齐 |
|
||||
| P6-N46 | architecture_design.md §8 ADR-R5-N1 | ZoneManager swap-and-pop 移除时必须调用 VFXManager.play("zone_expire", ...) |
|
||||
| P6-N47 | implementation_plan.md §2.3.A | MOVE_THRESHOLD_NORM = 0.067(归一化输入幅度,非像素/秒);DPS get_dps() 懒加载 _recalc_window,record_damage() 仅做 O(1) 写入 |
|
||||
| P6-N48 | architecture_design.md §3.2 | SpellContext.registers 必须声明时初始化 `= PackedFloat32Array([0.0, 0.0, 0.0, 0.0])`(长度 4);未初始化则 registers[0] 触发 out-of-bounds 崩溃,fill(0.0) 对空数组无操作 |
|
||||
| P6-N49 | implementation_plan.md §2.1 | DamageContext 必须独立为 damage_context.gd(class_name DamageContext);DamageContextPool 为独立 Autoload 文件,不声明 class_name;两者混于同一文件会导致 _pool: Array[DamageContext] 存入具有管理方法的池管理器实例(语义错误) |
|
||||
| P6-N50 | numerical_design.md §2.2 | Spell Database 新增 damage_type 列;Projectile 类型法术必须显式指定 DamageType(如 chain_bolt=LIGHTNING, nuke=PHYSICAL);Modifier/Trigger/Multicast 填 — |
|
||||
| P6-N51 | architecture_design.md §3.2 | SpellNode.type 声明为 `SpellType = SpellType.ACTION`(枚举类型注解);原 `int` 类型丢失编译期检查 |
|
||||
| P6-N52 | architecture_design.md §3.4.A | `_flatten_circuit()` LOGIC_FORK 构造:每条出边必须单独 `SubPayloadRegistry.register([entry_node])`,禁止将所有分支入口节点合并到同一 SubPayload(否则并行分支退化为顺序执行) |
|
||||
| P6-N53 | architecture_design.md §3.2 | SpellNode 新增 `element_tags: Array[String] = []`;共鸣系统 `_check_resonance()` 通过此字段匹配 `resonance_recipes.json` 中 `pattern` 数组的 `"tag:xxx"` 元素 |
|
||||
| P6-N54 | implementation_plan.md §2.3.C | EnemyManager LOD 循环必须使用 `ENEMY_STRIDE`(= 8),禁止使用旧常量名 `STRIDE` |
|
||||
| P6-N55 | core_wand_design.md §6 | `SpellEvaluator` 采用两阶段架构:`compile_wand()` 编译(法杖装备/换牌时) + `execute_compiled()` 执行(每次施法);`_flatten_circuit()` 必须传 `core.edges`,不是 `core.slot_configs` |
|
||||
| P6-N56 | implementation_plan.md §2.2 | `SpellDeck.pop()` 简化版须含 `null` 安全边界;权威完整实现(含 `_consumed` 掩码)见 `architecture_design.md §3.4.C` |
|
||||
| P6-N57 | architecture_design.md §3.4.A | `_flatten_circuit()` 分叉检测须用出度(`adj[slot_idx].size() > 1`),禁止依赖 "splitter" tag(tag 漏写时静默失效);"splitter" tag 仅保留为 UI 标注 |
|
||||
| P6-N58 | architecture_design.md §3.4.A | LOGIC_FORK SubPayload 须用 `_collect_branch_path()` 收集完整分支路径(多节点链),禁止仅注册入口节点(否则后续节点全被丢弃);分叉节点自身法术先追加再插入 LOGIC_FORK |
|
||||
| P6-N59 | implementation_plan.md §2.5.A | `DASH_TRIGGERED` 事件负载须包含 `stationary_time: float`(P6-N36 要求),代码中 `EventBus.emit` 必须同时传 `caster_id + stationary_time`,冲刺后立即清零 `_stationary_time` |
|
||||
| P6-N60 | implementation_plan.md §2.5.D | VFXManager 必须维护 `_active_count: int` 计数器;`_pool_pop()` 时 +1,`_pool_return()` 时 -1;`play()` 中以 O(1) 方式检查上限,禁止遍历 pool 字典统计(O(N)) |
|
||||
| P6-N61 | core_wand_design.md §6 | `execute_compiled(compiled, ctx, core)` 必须显式接收 `core: CoreDefinition` 参数;通过 `core.feature_tags` 判断持久内存策略;禁止使用 `ctx.core_feature_tags`(SpellContext 无此字段) |
|
||||
| P6-N62 | combat_mechanics_depth.md §4 | DamageContext 新增 `mult: float = 1.0` 字段(伤害乘算系数);`reset()` 必须重置 `mult = 1.0`,否则池化复用时暴击系数残留 |
|
||||
| P6-N63 | architecture_design.md §3.4.A | `_collect_branch_path()` 的 `in_degree` 参数必须传 Kahn 循环**前**的原始入度副本(`orig_in_degree = in_degree.duplicate()`,在建立 adj 表之后、Kahn BFS 之前);Kahn 结束后工作数组全为 0,用其做 `in_degree[nxt] <= 1` 汇聚检测恒成立,导致分支路径越界纳入主链节点 |
|
||||
| P6-N64 | architecture_design.md §3.4.A | `_flatten_circuit()` 必须维护 `in_branch_payload: Dictionary`,`_collect_branch_path()` 每访问一个槽索引即写入;主链 `topo_order` 循环在追加节点前检查此字典并跳过,防止分支节点在 SubPayload(LOGIC_FORK 路径)和主链(topo_order 直接追加)中各执行一次 |
|
||||
| P6-N65 | core_wand_design.md §6 | `compile_wand()` 参数类型修正:`slot_spells: Array[SpellNode]` → `raw_deck: SpellDeck`;`_flatten_matrix()` 与 `_flatten_circuit()` 均通过 `deck.nodes[i]` 访问节点,传入裸 Array 会触发"Invalid get index 'nodes'"运行时错误;LINEAR 路径同步改为 `CompiledDeck.new(raw_deck.nodes)` |
|
||||
| P6-N66 | architecture_design.md §3.4.A | `_collect_branch_path()` 内若遇到嵌套分叉(`adj[cur].size() > 1`),必须递归为每条子边调用自身并注册 SubPayload,再注入嵌套 LOGIC_FORK 节点后 `break`;直接 `break` 会丢弃子分支全部节点 |
|
||||
| P6-N67 | implementation_plan.md §2.1 | `DamageContextPool.acquire()` 必须在赋值任何字段前先调用 `ctx.reset()`,防止池化复用时残留 `source_tags`(如 TRIGGERED=2)污染新的伤害上下文 |
|
||||
| P6-N68 | mechanics/consecutive_hits_stacking.md | `StatusTypeDef.can_catalyze` 类型为 `Array[int]`;`COMBO_MARK` 的正确值为 `[]`(空数组),写 `false`(bool)在 GDScript 4 严格类型模式下触发类型赋值错误 |
|
||||
| P6-N69 | implementation_plan.md §2.5.A | 停步蓄力采用边沿检测:PlayerManager 维护 `_charge_triggered: bool`;首次 `_stationary_time >= CHARGE_TRIGGER_TIME` 时置 `true` 并发出 `CHARGE_FIRED`(ID=14,一次性);移动/冲刺后重置为 `false`。`SpellEvaluator` 订阅 `CHARGE_FIRED`,**禁止**订阅 `CHARGE_STATE_CHANGED`(后者每帧 60Hz 发送,订阅将导致法术每帧重复释放) |
|
||||
| P6-N70 | architecture_design.md §3.4.A | SpellEvaluator 预编译入口必须命名为 `compile_wand(core: CoreDefinition, raw_deck: SpellDeck)`,权威定义见 `core_wand_design.md §6` |
|
||||
| P6-N71 | architecture_design.md §8 ADR-R5-N1 | ZoneManager tick 累积器必须使用 `while + -= tick_interval`(同 StatusManager 模式),防止大帧余量丢失。`SpatialGrid.query_circle` 仅调用一次,置于 if 守卫内、while 循环外,避免重复空间查询 |
|
||||
| P6-N72 | implementation_plan.md §2.5.D | VFXManager `_instantiate_vfx` 必须设置 `node.one_shot = true`;否则 `GPUParticles2D` 无限循环,`finished` 信号永不触发,`_pool_return` 永远不被调用,`_active_count` 只增不减。节点挂载到 VFXManager Autoload 自身,跨场景持久存活 |
|
||||
| ADR-R5-N1 | architecture_design.md §8 | ZoneManager 架构定义(PackedFloat32Array SoA,ZONE_STRIDE=8,MAX_ZONES=64,无 Area2D) |
|
||||
| ADR-R5-N2 | architecture_design.md §8 | Feature Tags 字符串常量规范(CoreFeatureTag Autoload) |
|
||||
| V-N1 | implementation_plan.md §2.5.D | VFXManager MAX_ACTIVE_VFX = 200 |
|
||||
@@ -0,0 +1,214 @@
|
||||
# [已归档] 模块化战术土豆 - 早期架构草案 (Cocos Creator 3.x / TypeScript)
|
||||
|
||||
> ⚠️ **此文件为历史草案,已废弃。**
|
||||
> 项目已迁移至 **Godot 4.6 + GDScript/C#**。
|
||||
> 当前权威架构请参阅:[docs/technical/architecture_design.md](technical/architecture_design.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述 (Overview)
|
||||
|
||||
本文件记录了项目早期基于 **Cocos Creator 3.x** 的架构探索。核心设计思想(HMWS 法术系统、ECS-Lite、Zero-GC、Strategy 模式)已被 Godot 版本继承并大幅扩展。
|
||||
核心体验结合了 **Brotato (土豆兄弟)** 的快节奏割草体验与 **Noita** 的深度法术构建系统。
|
||||
架构设计的首要目标是 **高性能**(支持同屏大量单位与弹幕)与 **极高的可扩展性**(特别是武器系统的模块化)。
|
||||
|
||||
### 1.1 设计目标
|
||||
1. **超模块化武器系统 (Hyper-Modular Weapon System)**:超越 Noita 的线性构建,引入更灵活的管道流与事件钩子机制。
|
||||
2. **高性能战斗引擎**:支持同屏 500+ 敌人,2000+ 弹幕,60FPS 稳定运行。
|
||||
3. **数据驱动 (Data-Driven)**:所有游戏内容(法术、属性、波次)完全配表化/JSON化。
|
||||
|
||||
---
|
||||
|
||||
## 2. 系统分层架构 (Layered Architecture)
|
||||
|
||||
采用能够严格分离数据与表现的架构模式。虽然 Cocos 是组件式的,但在核心战斗层我们将采用 **Manager + Data** 的方式来规避组件更新带来的开销。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Layer1[表现层 (Presentation Layer)] --> Layer2[逻辑层 (Domain/Logic Layer)]
|
||||
Layer2 --> Layer3[数据层 (Data Layer)]
|
||||
Layer2 --> Layer4[核心库 (Core Library)]
|
||||
|
||||
subgraph Layer1
|
||||
ViewComponents[Cocos Components (Sprite, Animation)]
|
||||
UIManagers[UI System]
|
||||
Effects[Particle Wrapper]
|
||||
end
|
||||
|
||||
subgraph Layer2
|
||||
CombatMgr[Combat Manager (Main Loop)]
|
||||
SpellEvaluator[Spell Interpreter (The "CPU")]
|
||||
EnemyAI[Boid AI System]
|
||||
GameCycle[Wave & Shop Cycle]
|
||||
end
|
||||
|
||||
subgraph Layer3
|
||||
ConfigMgr[JSON Config Loader]
|
||||
SaveSystem[Persistent Storage]
|
||||
Inventory[Player State & Inventory]
|
||||
end
|
||||
|
||||
subgraph Layer4
|
||||
Pool[Object Pool System]
|
||||
SpatialHash[Spatial Hashing (Collision)]
|
||||
EventBus[Global Event Bus]
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心子系统:超模块化法术系统 (HMWS)
|
||||
|
||||
这是本项目的技术核心。我们将 Noita 的“魔杖”概念抽象为 **“法术管道 (Spell Pipeline)”**。
|
||||
|
||||
### 3.1 核心概念差异
|
||||
|
||||
| 特性 | Noita 原版 | HMWS (本项目) | 改进目的 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **执行流** | 线性 (Deck -> Hand -> Discard) | **树状/图状结构 + 事件驱动** | 支持“子母弹”、“条件触发”、“击中后分裂逻辑”的无限嵌套。 |
|
||||
| **属性计算** | 累加式 (Cast Delay += 0.1) | **管线式 (Pipeline)** | 允许中间件对属性进行乘算、覆写或逻辑重定向。 |
|
||||
| **载体** | 法杖 (Wand) | **构建核心 (Core)** | 核心决定了插槽拓扑结构(不仅仅是线性数组,可能是矩阵或特定触发槽)。 |
|
||||
|
||||
### 3.2 数据结构设计 (TypeScript)
|
||||
|
||||
#### A. 基础单元 (ISpellNode)
|
||||
这是所有“部件”的基类。
|
||||
```typescript
|
||||
interface ISpellContext {
|
||||
caster: Entity; // 施法者
|
||||
target: Vec2; // 目标点
|
||||
stats: CastStats; // 当前累积的属性(伤害、速度、扩散等)
|
||||
payloads: ProjectileDef[]; // 待发射的弹头定义队列
|
||||
}
|
||||
|
||||
abstract class SpellNode {
|
||||
id: string;
|
||||
type: SpellType; // ACTION (投射物), MODIFIER (修正), TRIGGER (触发器), LOGIC (逻辑门)
|
||||
|
||||
// 核心执行函数:修改 Context 或 产生行为
|
||||
abstract execute(ctx: ISpellContext, deck: SpellDeck): void;
|
||||
}
|
||||
```
|
||||
|
||||
#### B. 法术解析器 (The Evaluator)
|
||||
为了高性能,解析器必须 **零垃圾回收 (Zero-GC)**。在施法计算帧,不应当 `new` 任何对象。
|
||||
* 使用预分配的 `Context` 对象池。
|
||||
* 使用 `Stack<SpellNode>` 来模拟递归,防止深层递归爆栈。
|
||||
|
||||
#### C. 高级特性:动态插槽与逻辑门
|
||||
* **Logic Spells (逻辑法术)**:引入 `IfHPBelow`, `OnKillAction`, `EveryNbShot` 等逻辑块,让玩家实现“如果血量低于30%,则发射吸血导弹”的构建。
|
||||
* **Variable Storage (变量存储)**:允许法术在法杖上写入/读取临时变量(例如:记录连击数)。
|
||||
|
||||
### 3.3 扩展性设计
|
||||
所有法术行为通过 **Strategy Pattern (策略模式)** 实现。
|
||||
新增一个法术只需:
|
||||
1. 在 JSON 中定义 ID 和贴图。
|
||||
2. 实现一个 `SpellAction` 类。
|
||||
3. 在注册表中注册。
|
||||
|
||||
### 3.4 进阶构建机制 (Advanced Mechanics) - 玩法增强
|
||||
为了超越“线性堆砌”的枯燥感,架构支持以下三种深度玩法机制:
|
||||
|
||||
#### A. 拓扑插槽系统 (Topology Slots)
|
||||
核心(Core)不再仅仅是一个列表,它可以是一个 **2D 网格** 或 **电路板**。
|
||||
* **adjacency_bonus (邻接加成)**:某些插槽有物理连接。例如,将 [火元素] 放在 [高压槽] 旁边,会自动获得 +20% 范围。
|
||||
* **Circuit Logic (电路逻辑)**:法术流不再只是从左到右。核心板可以有分叉路口,玩家需要用 [分流器法术] 将能量流引导到不同的分支。
|
||||
|
||||
#### B. 状态寄存器与图灵完备 (State Registers & Turing Completeness)
|
||||
为了实现真正的“图灵完备”,架构必须支持:**状态存储**、**条件跳转** 和 **循环**。
|
||||
|
||||
1. **Registers (寄存器)**:
|
||||
* 在 `ISpellContext` 中引入 `MemoryBank`,提供 4 个 Float 寄存器 (`R1`, `R2`, `R3`, `R4`)。
|
||||
* 寄存器在同一帧内所有法术间共享,甚至可以跨帧持久化(如果法杖配置了 Persistent Memory 核心)。
|
||||
|
||||
2. **Instruction Set (指令集法术)**:
|
||||
* **OPS**: `Add R1, 1` (加法), `Set R2, HP_Percent` (赋值).
|
||||
* **JUMP**: `JumpIf R1 > 10, Label_A` (条件跳转到标签A).
|
||||
* **LABEL**: `Label_A` (标记跳转点).
|
||||
|
||||
3. **Recursion Control (递归控制)**:
|
||||
* 为了防止死循环 (`While(true)`), 解释器引入 `MaxOpLimit` (最大操作数限制,例如 100 ops/frame)。超过限制强制中断并在此帧失效。
|
||||
|
||||
4. **实战应用**:
|
||||
* **计数器**: 每射击 3 次,第 4 次发射强力火球。
|
||||
* **动态模式切换**: 根据敌人距离 (`R1 = EnemyDistance`),如果近则跳转到 [霰弹逻辑],如果远则跳转到 [狙击逻辑]。
|
||||
|
||||
#### C. 共鸣系统 (Resonance System)
|
||||
在**预编译阶段 (Pre-compile Phase)** 进行模式匹配。
|
||||
* 如果检测到 `[水]` 和 `[电]` 法术在执行链中紧邻,架构自动插入一个隐藏的 `[导电反应]` 中间件。
|
||||
* 这允许设计隐藏配方(Hidden Recipes),鼓励玩家探索特定组合。
|
||||
|
||||
---
|
||||
|
||||
## 4. 高性能战斗架构 (High-Performance Combat Architecture)
|
||||
|
||||
为了实现“同屏 2000+ 弹幕”和“复杂逻辑构建”的双重目标,本架构采用 **Data-Oriented (面向数据)** 与 **Hybrid-ECS** 相结合的策略,最大化 CPU 缓存命中率并消除 GC 压力。
|
||||
|
||||
### 4.1 核心原则:零 GC (Zero-GC Principle)
|
||||
在核心战斗循环 (Game Loop) 中,**绝对禁止**使用 `new` 关键字分配堆内存。
|
||||
* **Context Pooling**: `ISpellContext` 等高频对象在关卡加载时预分配 2000 个,使用时复用,用完 `Reset`。
|
||||
* **Static Temporaries**: 向量计算使用全局静态临时变量 (`_tempVec2`),避免中间对象产生。
|
||||
|
||||
### 4.2 实体管理:ECS-Lite
|
||||
虽然 Cocos Creator 是基于组件的,但在海量单位管理上,我们将剥离组件的 Update 逻辑。
|
||||
* **Manager-Based Logic**: 子弹 (`Bullet`) 和 敌人 (`Enemy`) 不挂载 `UpdateComponent`。
|
||||
* 也就是:`cc.Node` 仅仅作为渲染容器。
|
||||
* **Centralized Loop (中央循环)**:
|
||||
* `BulletManager` 维护一个紧凑的 `Float32Array` (SoA 布局: `[x, y, vx, vy, type...]`)。
|
||||
* 在 `update(dt)` 中,直接遍历 Array 进行物理积分,速度比遍历 Node Tree 快一个数量级。
|
||||
* **Dirty Sync**: 仅当物体在屏幕视口内,且逻辑坐标发生位移时,才去同步 `cc.Node.position`。
|
||||
|
||||
### 4.3 物理与碰撞机制
|
||||
* **Builtin Optimization**: 优先使用 Cocos **Builtin-Physics** (非 Box2D) 进行 Geometry Overlap 检测。
|
||||
* **Spatial Hashing Fallback**: 若 Builtin 仍有压力,回退到定制的 **Spatial Grid** (一维数组网格),只计算临近 Grid 的实体碰撞,确保碰撞检测复杂度维持在 O(N)。
|
||||
* **Separation Logic**: 怪物挤压不使用刚体求解,而是施加简单的轻量级斥力向量。
|
||||
|
||||
### 4.4 渲染优化
|
||||
* **Node Pooling**: 严格的节点池管理。
|
||||
* **Throttling (分帧降频)**:
|
||||
* 伤害数字:每帧最多弹出 10 个,多余的合并或延迟显示。
|
||||
* AI 索敌:不需要每帧执行 `FindNearest`,可分散到 10~20 帧内轮询一次。
|
||||
|
||||
---
|
||||
|
||||
## 5. 游戏循环设计 (Game Loop)
|
||||
|
||||
结合 Brotato 的经济循环:
|
||||
|
||||
1. **准备阶段 (Shop/Inventory)**
|
||||
* 玩家拖拽法术卡牌组合逻辑。
|
||||
* **解析预热**:在玩家关闭背包时,预先编译法术链,生成 cached 的指令列表。避免战斗中实时解析带来的开销。
|
||||
2. **战斗阶段 (Wave)**
|
||||
* 生成大量敌人。
|
||||
* Player 自动开火 (执行 Cached 指令列表)。
|
||||
* 掉落拾取 -> 经验值/金币。
|
||||
3. **结算阶段**
|
||||
* 随机 3 选 1 升级(属性成长)。
|
||||
* 商店刷新法术与道具。
|
||||
|
||||
---
|
||||
|
||||
## 6. 技术栈选型总结
|
||||
|
||||
| 模块 | 方案 | 理由 |
|
||||
| :--- | :--- | :--- |
|
||||
| **语言** | TypeScript | 类型安全,便于重构 |
|
||||
| **ECS框架** | Custom Lite (Manager-based) | 避免第三方 ECS 库的学习成本与 overhead,针对本项目定制最优 |
|
||||
| **物理** | Custom Spatial Hash | Box2D 性能瓶颈明显 |
|
||||
| **UI** | Cocos UI + Virtual List | 背包道具可能很多,需要虚拟列表优化 |
|
||||
| **配置** | JSON + Type Interface | 灵活且易于热更 |
|
||||
|
||||
## 7. 目录规范建议
|
||||
|
||||
```text
|
||||
assets/
|
||||
Scripts/
|
||||
Core/ # 核心架构 (EventBus, Pool, BaseClasses)
|
||||
Systems/ # 独立系统 (Physics, Input, Audio)
|
||||
Domain/ # 游戏业务逻辑
|
||||
SpellSystem/ # 法术解释器, 定义, 执行栈
|
||||
Combat/ # 伤害计算, 弹道管理
|
||||
Enemy/ # AI 行为树
|
||||
View/ # UI 控制, 特效表现
|
||||
Config/ # 配置表加载器与类型定义
|
||||
```
|
||||
@@ -0,0 +1,252 @@
|
||||
# 构建核心 (Core) 设计文档
|
||||
|
||||
## 0. 概念定位
|
||||
|
||||
**Core(构建核心)** 是玩家持有的"武器主体"。它定义了:
|
||||
1. **插槽拓扑 (Slot Topology)**:有多少个插槽、如何排列(线性/矩阵/电路板)。
|
||||
2. **基础属性 (Base Stats)**:法杖自带的充能速度、容量、基础蓝耗。
|
||||
3. **特殊规则 (Core Rules)**:部分高阶 Core 拥有改变执行规则的特性(如持久内存、双流并行)。
|
||||
|
||||
> Core 本身不执行法术,它是 SpellDeck 的"容器",决定了 SpellEvaluator 的执行环境。
|
||||
|
||||
---
|
||||
|
||||
## 1. 数据结构 (GDScript)
|
||||
|
||||
```gdscript
|
||||
# core_definition.gd
|
||||
# 存储于 res://resources/cores/ 目录下的 .tres Resource 文件
|
||||
|
||||
class_name CoreDefinition
|
||||
extends Resource
|
||||
|
||||
## 核心唯一 ID(如 "wand_basic", "staff_matrix")
|
||||
@export var id: String
|
||||
|
||||
## 显示名称
|
||||
@export var name: String
|
||||
|
||||
## 稀有度 (0=普通, 1=稀有, 2=史诗, 3=传说)
|
||||
@export var rarity: int = 0
|
||||
|
||||
## 插槽拓扑类型
|
||||
@export_enum("LINEAR", "MATRIX_2X4", "CIRCUIT") var topology: int = 0
|
||||
|
||||
## 插槽总数量
|
||||
@export var slot_count: int = 5
|
||||
|
||||
## 矩阵布局尺寸(仅 MATRIX / CIRCUIT 类型有效)
|
||||
@export var grid_cols: int = 4
|
||||
@export var grid_rows: int = 2
|
||||
|
||||
## 特殊插槽配置(每个插槽的约束信息)
|
||||
## Array[Dictionary]: { "index": int, "allowed_types": Array[int], "tags": Array[String] }
|
||||
@export var slot_configs: Array = []
|
||||
|
||||
## CIRCUIT 拓扑的有向边列表(LINEAR/MATRIX 留空即可)
|
||||
## Array[Dictionary]: { "from": int, "to": int }
|
||||
## _flatten_circuit() 从 core.edges 读取边表;禁止在 slot_configs 内嵌 "edges" key。
|
||||
@export var edges: Array = []
|
||||
|
||||
## 法杖基础属性
|
||||
@export var base_mana_capacity: float = 50.0 # 法杖蓝上限
|
||||
@export var base_mana_regen: float = 5.0 # 每秒回蓝
|
||||
@export var base_cast_delay: float = 0.25 # 基础施法后摇(乘算基准)
|
||||
@export var base_recharge_time: float = 0.5 # 完整一轮打完后的充能时间
|
||||
|
||||
## Core 特性标签 (Array[String]),驱动特殊执行规则
|
||||
## 例如: ["persistent_memory", "dual_stream", "shuffle_deck"]
|
||||
@export var feature_tags: Array[String] = []
|
||||
|
||||
## 图标路径
|
||||
@export var icon: Texture2D
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 插槽拓扑类型
|
||||
|
||||
### 2.1 LINEAR(线性)
|
||||
```
|
||||
[ 0 ] -> [ 1 ] -> [ 2 ] -> [ 3 ] -> [ 4 ]
|
||||
```
|
||||
- 最基础的类型,执行流从左到右。
|
||||
- 新手友好,等同于标准 Noita 法杖。
|
||||
- **代表 Core**: `wand_basic`(5槽)、`staff_long`(10槽)
|
||||
|
||||
### 2.2 MATRIX(矩阵)
|
||||
```
|
||||
行 A: [ 0 ] [ 1 ] [ 2 ] [ 3 ]
|
||||
行 B: [ 4 ] [ 5 ] [ 6 ] [ 7 ]
|
||||
```
|
||||
- 执行流默认沿行 A 执行,但行 B 中的法术在 **邻接行 A 中对应位置的法术** 执行时同时触发(**邻接加成**)。
|
||||
- **邻接规则**: 将 `slot[i]` 与 `slot[i + grid_cols]` 视为"竖向相邻",两者同类时触发 `adjacency_bonus`。
|
||||
- **代表 Core**: `matrix_board_2x4`(8槽)
|
||||
|
||||
**邻接加成效果表(P6-N13 权威定义)**:
|
||||
|
||||
预编译阶段 `_flatten_matrix` 检测以下组合,注入对应隐式 MODIFIER 节点:
|
||||
|
||||
| 行 A 法术类型 | 行 B 法术类型 | 触发条件 | 注入效果 | 说明 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **ACTION** | **ACTION**(同 ID) | 完全相同的 ACTION 对齐 | 伤害 ×1.5(`damage_mult +0.5`) | "强化同类弹" |
|
||||
| **ACTION** | **MODIFIER** | 任意 | 该 MODIFIER 效果额外再施加一次(等效双倍加成) | "增幅器加倍" |
|
||||
| **MODIFIER** | **MODIFIER**(同 ID) | 完全相同的 MODIFIER 对齐 | 修正值 ×2(平直叠加而非乘算) | "共鸣修正" |
|
||||
| **ACTION** | **TRIGGER** | 任意 | 该 TRIGGER 的 `proc_rate ×1.5`(最大 1.0) | "增压触发" |
|
||||
| **LOGIC** | **任意** | 任意 | 无加成(LOGIC 节点不参与邻接计算) | 设计原则:逻辑层不受物理拓扑影响 |
|
||||
| **空槽** | **任意** | 行 A 为空 | 无加成 | 空槽不传递邻接效果 |
|
||||
|
||||
> **实现说明**:`_flatten_matrix` 在预编译时查表,按上表注入 `ImplicitModifierNode`;运行时 `SpellEvaluator` 不感知矩阵结构,仅执行线性化后的节点序列。邻接加成不累叠(同一个 ACTION 最多受益于一次邻接加成)。
|
||||
|
||||
### 2.3 CIRCUIT(电路板)
|
||||
```
|
||||
[ 2 ]
|
||||
↑
|
||||
[ 0 ] -> [ 1 ] -> [ 3 ] -> [ 4 ]
|
||||
↓
|
||||
[ 5 ] -> [ 6 ]
|
||||
```
|
||||
- 插槽有明确的 **有向边(Edge)** 连接,执行流可分叉。
|
||||
- 玩家可在 `slot_configs` 中配置分叉点(Splitter)和合并点(Merger)。
|
||||
- **代表 Core**: `circuit_fork`(7槽,含1个分叉点)
|
||||
|
||||
**CIRCUIT Core 的 `CoreDefinition` 资源格式**:
|
||||
`edges` 列表通过 `CoreDefinition.edges`(`@export var edges: Array = []`)存储,
|
||||
`slot_configs` 仅存储每个槽的类型约束与标签;不再在 slot_configs 内部嵌套 `"edges"` key。
|
||||
```json
|
||||
{
|
||||
"topology": "CIRCUIT",
|
||||
"slot_configs": [
|
||||
{ "index": 0, "allowed_types": ["ACTION","MODIFIER","TRIGGER","LOGIC"], "tags": [] },
|
||||
{ "index": 1, "allowed_types": ["ACTION","MODIFIER","TRIGGER","LOGIC"], "tags": ["splitter"] },
|
||||
{ "index": 2, "allowed_types": ["ACTION","MODIFIER"], "tags": ["branch_a"] },
|
||||
{ "index": 3, "allowed_types": ["ACTION","MODIFIER","TRIGGER","LOGIC"], "tags": [] },
|
||||
{ "index": 4, "allowed_types": ["ACTION","MODIFIER","TRIGGER","LOGIC"], "tags": [] },
|
||||
{ "index": 5, "allowed_types": ["ACTION","MODIFIER"], "tags": ["branch_b"] },
|
||||
{ "index": 6, "allowed_types": ["ACTION","MODIFIER"], "tags": ["branch_b"] }
|
||||
],
|
||||
"edges": [
|
||||
{ "from": 0, "to": 1 },
|
||||
{ "from": 1, "to": 2 },
|
||||
{ "from": 1, "to": 3 },
|
||||
{ "from": 1, "to": 5 },
|
||||
{ "from": 3, "to": 4 },
|
||||
{ "from": 5, "to": 6 }
|
||||
]
|
||||
}
|
||||
```
|
||||
> `CoreDefinition.edges` 是 `_flatten_circuit()` 做 Kahn 算法拓扑排序的唯一边表来源。
|
||||
> `edges` 为空则视为 LINEAR 拓扑(降级处理)。LINEAR/MATRIX Core 的 `edges` 保持空数组即可。
|
||||
|
||||
---
|
||||
|
||||
## 3. 特性标签 (Feature Tags)
|
||||
|
||||
| 标签 | 效果 | 稀有度要求 |
|
||||
| :--- | :--- | :--- |
|
||||
| `persistent_memory` | 法杖的寄存器 (R1-R4) 在两次施法之间保留数值(跨帧持久化) | 稀有+ |
|
||||
| `dual_stream` | 同时从插槽序列头尾各执行一次,两条流的弹头同时发射 | 史诗 |
|
||||
| `shuffle_deck` | 每次充能完成后,随机打乱插槽执行顺序 | 稀有 |
|
||||
| `infinite_spells` | 法力耗尽时不停止执行,但每发增加 1 点 HP 代价(需配合 `heavy_cost` 法术) | 传说 |
|
||||
| `always_cast_last` | 无论 Deck 如何执行,最后一个插槽的法术在一轮结束时**必定**执行一次 | 稀有 |
|
||||
|
||||
> **实现约束说明(P5-N2)**:
|
||||
> - **`shuffle_deck`**:仅随机化**独立 ACTION 节点**的执行顺序;MODIFIER 节点始终与其紧接的 ACTION 视为原子单元整体移动,防止修正器与错误的动作配对,保持构建意图的确定性。UI 上以"卡组"为单位展示被打乱的顺序,而非单张卡牌。
|
||||
> - **`dual_stream`**:两条执行流**各自独立计算 ops 消耗**,每条流的 MAX_OPS 上限 = `cpu_limit × MAX_OPS_PER_CPU / 2`(平均分配)。双流模式下单帧总指令数与单流相同,不额外增加运算力消耗;实际可用插槽从序列头尾各取一半,两条流不可互相访问对方的槽位。
|
||||
> **奇数插槽行为(P6-N12 规范)**:当 `slot_count` 为奇数时(如 7 槽),前向流取前 `floor(slot_count / 2)` 个槽(0~2),后向流取后 `floor(slot_count / 2)` 个槽(4~6),**中间槽(index = slot_count / 2,此处为 index 3)由前向流额外执行**(不计入后向流)。
|
||||
> UI 上奇数情况下中间槽显示"▶ 仅前流"标注,防止玩家困惑。
|
||||
> 示例(7 槽):`[0前][1前][2前] | [3前专属] | [6后][5后][4后]`。
|
||||
> - **`always_cast_last`**:无论 Deck 如何执行,一轮结束时对最后一个插槽的法术额外执行一次。
|
||||
> **边界行为(P6-N7 补充)**:
|
||||
> - 若最后一个插槽为 **ACTION**:正常发射一颗子弹(使用一轮结束时的当前 `CastStats`,所有 MODIFIER 已在上一轮累积)。
|
||||
> - 若最后一个插槽为 **MODIFIER**:静默跳过(MODIFIER 必须有后续 ACTION 才能生效,单独执行没有效果),此次额外执行不消耗 mana。
|
||||
> - 若最后一个插槽为 **TRIGGER** 或 **LOGIC**:同样静默跳过(无 ACTION 上下文,执行无意义)。
|
||||
> - UI 提示:最后一个插槽为非 ACTION 时,卡牌展示灰色小锁图标(∅无效提示),防止玩家误以为 MODIFIER 也会被额外执行而混淡。
|
||||
|
||||
> **Feature Tag 引用规范(ADR-R5-N2)**:业务代码中必须通过 `CoreFeatureTag.PERSISTENT_MEMORY`、`CoreFeatureTag.DUAL_STREAM` 等常量引用标签,禁止使用裸字符串。详见 `architecture_design.md §ADR-R5-N2`。
|
||||
|
||||
---
|
||||
|
||||
## 4. Core 实例(参考设计)
|
||||
|
||||
| ID | 名称 | 稀有度 | 拓扑 | 插槽数 | 特性 | 定位 |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| `wand_basic` | 朴素法杖 | 普通 | LINEAR | 5 | — | 初始装备,入门用 |
|
||||
| `wand_fast` | 急促法杖 | 普通 | LINEAR | 4 | — | 基础施法延迟 ×0.7,容量小 |
|
||||
| `staff_long` | 长卷轴 | 稀有 | LINEAR | 10 | — | 插槽多,回蓝慢 |
|
||||
| `matrix_board` | 构造板 | 稀有 | MATRIX_2X4 | 8 | — | 解锁邻接加成玩法 |
|
||||
| `wand_memory` | 记忆法杖 | 史诗 | LINEAR | 6 | `persistent_memory` | 寄存器跨帧,启用计数器构建 |
|
||||
| `circuit_fork` | 分叉回路 | 史诗 | CIRCUIT | 7 | — | 双分支执行,高复杂度 |
|
||||
| `wand_eternal` | 永恒法杖 | 传说 | LINEAR | 8 | `infinite_spells` | 卖血流终极武器 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 序列化格式 (SaveData)
|
||||
|
||||
法杖的存档需要记录:Core 类型 + 每个插槽内填的法术 ID。
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": 1,
|
||||
"wands": [
|
||||
{
|
||||
"core_id": "wand_basic",
|
||||
"slots": ["double_cast", "spark_bolt", "damage_plus", null, null]
|
||||
},
|
||||
{
|
||||
"core_id": "matrix_board",
|
||||
"slots": ["trigger_hit", "chain_bolt", null, null, "homing", "damage_plus", null, null]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- `null` 表示该插槽为空。
|
||||
- 加载时如果 `core_id` 或法术 ID 不存在于注册表,记录警告日志并跳过,不崩溃(向前兼容)。
|
||||
- `schema_version` 字段用于未来迁移,初始值为 1。
|
||||
|
||||
---
|
||||
|
||||
## 6. 与 SpellEvaluator 的接口
|
||||
|
||||
`SpellEvaluator` 在执行前需要从 `Core` 获取:
|
||||
|
||||
```gdscript
|
||||
# spell_evaluator.gd 伪代码
|
||||
# SpellEvaluator 采用两阶段架构:
|
||||
# - compile_wand():法杖装备/换牌时调用,结果缓存为 CompiledDeck
|
||||
# - execute_compiled():每次施法时执行缓存的线性化节点序列
|
||||
|
||||
# ── 阶段 1:编译(法杖装备/换牌时调用,结果缓存于 CompiledDeck)──────────────────
|
||||
# 参数类型:raw_deck: SpellDeck(通过 deck.nodes[i] 访问节点序列,禁止传入 Array[SpellNode])
|
||||
func compile_wand(core: CoreDefinition, raw_deck: SpellDeck) -> CompiledDeck:
|
||||
match core.topology:
|
||||
CoreDefinition.LINEAR:
|
||||
return CompiledDeck.new(raw_deck.nodes)
|
||||
CoreDefinition.MATRIX_2X4:
|
||||
return _flatten_matrix(raw_deck, core) # 注入邻接 MODIFIER
|
||||
CoreDefinition.CIRCUIT:
|
||||
# 传 core.edges(顶层有向边表),core.slot_configs 仅存槽约束/标签,不含拓扑边
|
||||
return _flatten_circuit(raw_deck, core) # Kahn 拓扑排序 + LOGIC_FORK 注入
|
||||
return CompiledDeck.new([]) # 未知拓扑降级为空 Deck
|
||||
|
||||
# ── 阶段 2:执行(每次施法时调用缓存的 CompiledDeck)────────────────────────────
|
||||
# 必须接受 core: CoreDefinition 参数,用于读取 core.feature_tags 判断寄存器持久策略。
|
||||
# 禁止将 feature_tags 复制到 SpellContext(SpellContext 不持有 Core 引用;ctx.core_feature_tags 不存在)。
|
||||
func execute_compiled(compiled: CompiledDeck, ctx: SpellContext, core: CoreDefinition) -> void:
|
||||
_run_deck(compiled, ctx) # SpellEvaluator 不感知拓扑,仅执行线性化节点序列
|
||||
|
||||
# 执行完毕后处理特性标签(直接访问传入的 CoreDefinition)
|
||||
if CoreFeatureTag.PERSISTENT_MEMORY not in core.feature_tags: # ✅ 使用常量(ADR-R5-N2)
|
||||
ctx.registers.fill(0.0) # 非持久内存,帧结束后清零(registers 即 "memory_bank",见 architecture_design.md §3.2)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. UI 集成要点
|
||||
|
||||
- **背包界面**:根据 `topology` 渲染不同的插槽布局组件(LINEAR 显示横排,MATRIX 显示网格)。
|
||||
- **拓扑提示**:悬停插槽时显示该插槽的 `allowed_types` 约束(如"此槽仅接受 Trigger 类型")。
|
||||
- **测试靶场**:在商店界面提供靶人,玩家可立即测试当前法杖效果,**无需进入战斗**。
|
||||
- **`cpu_limit` 可视化**:在法杖编辑 UI 显示当前构建消耗的"运算力"进度条,接近 `cpu_limit` 时变黄/变红。
|
||||
@@ -0,0 +1,387 @@
|
||||
# 游戏设计说明书:魔法工匠 (Arcane Artificer)
|
||||
|
||||
## 1. 游戏概述 (Game Overview)
|
||||
|
||||
**《魔法工匠》** (暂定名) 是一款融合了 **Roguelite 割草动作** 与 **深度法术编程** 的快节奏射击游戏。
|
||||
|
||||
玩家扮演一名被困在异次元的“土豆工匠”,必须利用手中的核心(法杖)和收集到的法术芯片,组装出毁天灭地的魔法武器,抵御无穷无尽的怪物潮。
|
||||
|
||||
本游戏的核心差异点在于:**你捡到的不是武器,而是逻辑与弹头的碎片。你必须像程序员一样“编写”你的子弹。**
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心体验与特色 (Core Experience)
|
||||
|
||||
### 2.1 "Noita" 遇上 "Brotato"
|
||||
* **快节奏战斗**:继承 Brotato 的短短波次(每波 60-90 秒),自动射击,高密度怪物,让玩家肾上腺素飙升。
|
||||
* **深深度构建**:继承 Noita 的法杖编辑深度。战斗之余的商店阶段,是冷静思考、调整“代码”的时间。
|
||||
|
||||
### 2.2 真正的模块化 (True Modularity)
|
||||
不像传统游戏那样只是“加一个火属性配件”,本作允许玩家构建复杂的逻辑链。
|
||||
* *例如*:“当子弹击中墙壁时,生成一个炮台,炮台每秒发射三枚追踪冰弹。”
|
||||
|
||||
### 2.3 性能怪兽
|
||||
得益于底层的 **ECS-Lite (Autoload Manager-based)** 和 **Spatial Grid** 技术,游戏支持同屏 **1000+** 敌人和 **2000+** 独立运算的弹幕,每一次爆炸都不仅是贴图,而是真实的物理/逻辑判定。
|
||||
|
||||
---
|
||||
|
||||
## 3. 武器系统详解:法术管道 (The Spell Pipeline)
|
||||
|
||||
这是游戏最核心的玩法。武器由 **“核心 (Core)”** 和 **“法术节点 (Spell Node)”** 组成。
|
||||
|
||||
### 3.1 核心 (The Core)
|
||||
核心相当于“枪身”或“主板”。它决定了基础属性:
|
||||
* **插槽数 (Capacity)**: 能插多少个法术芯片。
|
||||
* **回蓝速度 (Mana Charge)**: 每秒恢复多少法力。
|
||||
* **最大魔力 (Mana Max)**: 蓝条上限。
|
||||
* **施法频率 (Cast Rate)**: 每秒执行多少次主循环。
|
||||
> **术语说明**:技术文档(`core_wand_design.md`、`numerical_design.md`)统一使用
|
||||
> **Cast Delay(施法后摇)** 表示同一概念的倒数形式(`cast_delay = 1 / cast_rate`)。
|
||||
> 设计文档(GDD)用 Cast Rate("快慢"直觉更好);实现层用 Cast Delay("乘算系数"更方便计算)。
|
||||
> 两者映射关系:`base_cast_delay = 0.25s` → `cast_rate = 4 次/秒`(默认)。
|
||||
* **拓扑结构**: 所有的核心都有一个 Main 入口,但高级核心可能有额外的 "OnKill"(击杀时)或 "OnHit"(受击时)入口插槽。
|
||||
|
||||
### 3.1.A 多 Core 持有与激活规则 (Multi-Core Rules)
|
||||
|
||||
> 这是最基础的游戏规则,所有构建策略以此为前提。
|
||||
|
||||
* **同时激活数量上限**: 玩家在战斗中**同时激活 1 个 Core**(激活的 Core 持续执行其主循环自动射击)。
|
||||
* **背包携带上限**: 背包最多同时持有 **3 个 Core**(含当前激活的 1 个)。超出时商店购买会提示选择丢弃或出售。
|
||||
* **切换时机**: 仅可在 **商店阶段(非战斗期间)** 切换激活的 Core;战斗中不可切换。
|
||||
* **插槽独立**: 每个 Core 的法术插槽互相独立,切换 Core 时旧 Core 插槽内容保留,不适配的卡移入等待区(规则见 §5.G5)。
|
||||
* **特殊机制 `dual_stream` Core**(史诗级 Feature Tag): 此类 Core 自身内部拥有两条并行执行流(从头尾各执行一次),**并非同时激活两个 Core**,不突破"同时激活 1 个"的规则。
|
||||
* *设计意图*:限制同时激活数量避免 Brotato 式"堆满武器"的感官疲劳,同时背包存 3 个让玩家有转型预备的空间。
|
||||
|
||||
### 3.2 法术芯片 (Spell Chips)
|
||||
法术分为四大类,通过排列组合产生无限可能。
|
||||
|
||||
#### A. 动作类 (Actions) - 实际产生的飞行物
|
||||
* **魔法弹 (Spark Bolt)**: 基础伤害,低蓝耗。
|
||||
* **黑洞 (Black Hole)**: 吸附敌人,无伤害。
|
||||
* **治疗雾 (Healing Mist)**: 范围回血。
|
||||
|
||||
#### B. 修正类 (Modifiers) - 改变下一个动作的属性
|
||||
* **伤害增强 (Damage+)**: 蓝耗+10,伤害+20。
|
||||
* **弹道折射 (Bounce)**: 子弹遇墙反弹。
|
||||
* **多重施法 (Multicast)**: 同时发射后面 2 个动作。
|
||||
* **甚至有“数学修正”**: 比如 `Sin(Time)` 让子弹走正弦波轨迹。
|
||||
|
||||
#### C. 触发器类 (Triggers) - 逻辑的传递
|
||||
这是构建“子母弹”的关键。
|
||||
* **击中触发 (Trigger On Hit)**: 这个子弹本身伤害很低,但它击中敌人时,会以敌人为原点,**施放**这个芯片后面的法术。
|
||||
* **定时触发 (Timer)**: 子弹飞行 0.5秒 后,施放后面的法术。
|
||||
|
||||
#### D. 逻辑门类 (Logic Gates) - 自动化的灵魂
|
||||
这是超越 Noita 的设计,让武器变智能。
|
||||
* **IF_HP_LOW**: 如果玩家血量 < 30%,执行后续法术,否则跳过。
|
||||
* **IF_ENEMY_CLOSE**: 如果 2米内 有敌人,执行后续法术。
|
||||
* **LOOP**: 循环执行后续 3 个法术 5 次(适合做激光加特林)。
|
||||
|
||||
### 3.3 扩展玩法设计 (Extended Gameplay Mechanics)
|
||||
|
||||
为了让构建过程不仅是“堆数值”,我们引入了三个深度维度:
|
||||
|
||||
#### A. 拓扑与空间构建 (Topology & Spatial Building)
|
||||
核心(Core)不只是一个列表,而是一个**2D 电路板**。
|
||||
* **物理邻接 (Adjacency)**:每个插槽可能有特殊的加成方向。
|
||||
* *示例*:一个“增压核心”中间有一个红色插槽,标明“右侧插槽获得双倍效果”。
|
||||
* **电路分流 (Circuit Splitting)**:电路板上可能有分支。
|
||||
* *玩法*:使用 [分流器] 法术,将“电爆”效果只输送给上面的路线,将“冰冻”效果只输送给下面的路线。
|
||||
|
||||
#### B. 逻辑与自动化 (Logic & Automation)
|
||||
引入变量寄存器,让法杖拥有简单的“图灵完备性”。
|
||||
* **Global Registers (全局寄存器)**:允许在这个核心内读写变量 `A`, `B`。
|
||||
* **构建示例 - "蓄力逻辑"**:
|
||||
* `[Set A = A + 1]` -> `[IF A < 100 THEN END]` -> `[Set A = 0]` -> `[终极核弹]`
|
||||
* *效果*:这个法杖平时只会空转(每帧 A+1),直到第 100 帧时,才会发射一枚核弹。相当于自动蓄力 1.6秒。
|
||||
* **玩家呈现策略(P5-C1)**:寄存器 `A/B` 的读写操作**不直接作为可购买法术卡暴露给普通玩家**。游戏将寄存器操作封装为具体功能的法术卡:
|
||||
* 「蓄力计数器」卡:内置完整的计数→阈值判断→重置→执行逻辑,卡面属性为"蓄力阶数 N",玩家感知为"蓄力 N 次后释放一次"。
|
||||
* 「击杀连击」卡:内置 ON_KILL 计数→阈值→爆发→重置,玩家感知为"积累 5 击杀触发爆发"。
|
||||
* 「裸寄存器模式」仅通过 **Tier 3+ 的「DEBUG 核心」**解锁,作为面向硬核玩家的高阶自定义工具,不作为默认游戏体验推送。
|
||||
|
||||
#### C. 炼金共鸣 (Alchemical Resonance)
|
||||
这是隐藏的合成系统。当某些特定属性的法术在执行链中直接相邻时,会发生**质变**。
|
||||
* `[水属性波]` + `[闪电链]` -> **自动合成** -> `[等离子风暴 (Plasma Storm)]`
|
||||
* *效果*:原本是两个独立的法术,现在融合成了一个全新的强力法术,无需消耗额外的插槽。
|
||||
* 这鼓励玩家去尝试各种元素的排列组合,寻找隐藏配方。
|
||||
* **触发范围权威规则**:绝大多数共鸣配方要求两张法术卡 **直接相邻**(`match: "adjacent"`)。
|
||||
少数传说级配方允许跨 Deck 任意位置触发(`match: "anywhere_in_deck"`),但此类配方
|
||||
在 `resonance_recipes.json` 中标注 `"rarity": 3`,且性能开销较高(O(N²) 全局扫描),
|
||||
数量会严格控制。详见 `architecture_design.md §3.4.C`。
|
||||
* **配方发现机制(P5-C2)**:共鸣配方默认隐藏。玩家**首次实际触发某配方**后,图鉴自动解锁并展示该配方的完整描述(触发效果、参与元素、法术 ID)。触发前,图鉴仅展示"某两种元素似乎能产生反应…"的模糊提示(仅显示参与元素的属性标签,不透露具体法术 ID),引导探索而非强制查询外部攻略。
|
||||
|
||||
### 3.4 构建示例 (Build Examples)
|
||||
|
||||
1. **“霰弹枪”构建**:
|
||||
* `[三双重施法]` -> `[扩散修正]` -> `[子弹]` -> `[子弹]` -> `[子弹]`
|
||||
* *效果*:一次射出三发散开的子弹。
|
||||
|
||||
2. **“运载核弹”构建**:
|
||||
* `[定时触发(1s)]` -> `[速度为0修正]` -> `[伤害增强]` -> `[伤害增强]` -> `[巨型爆炸]`
|
||||
* *效果*:射出一个普通子弹,1秒后在空中停滞,然后引发核爆。
|
||||
|
||||
3. **“吸血炮台”构建**:
|
||||
* `[击中触发]` -> `[召唤静止炮台]` -> `[炮台指令: 击中触发]` -> `[连锁闪电]` -> `[吸血修正]`
|
||||
* *效果*:你射中地板,地板上长出一个炮台,炮台射出的闪电会连环电击敌人并把血吸给你。
|
||||
|
||||
---
|
||||
|
||||
## 4. 数值策划 (Numeric Planning)
|
||||
|
||||
为了支撑如此复杂的构建,数值系统必须严谨且具备极其宽广的边界。
|
||||
|
||||
### 4.1 核心资源:魔力 (Mana)
|
||||
为了防止无限“加特林核弹”,限制资源是 **魔力**。
|
||||
* 强力修正器(如伤害+100)会有巨大的 **Delay (施法后摇)** 和 **Mana Cost (蓝耗)**。
|
||||
* **平衡点**:高射速 = 低单发伤害;高爆发 = 便秘的手感(打一发回蓝3秒)。
|
||||
|
||||
### 4.2 伤害公式 (The Formula)
|
||||
|
||||
> 完整权威公式定义该文件 `docs/design/numerical_design.md §3.1`。
|
||||
|
||||
$$ FinalDamage = ((Base + \sum Mod_{flat}) \times \prod Mod_{mult}) \times (1 - Res) - Armor $$
|
||||
|
||||
* **Base**: 法术动作的基础伤害 (e.g., 火球 10点)。
|
||||
* **Mod_flat**: 修正器的数值加成 (e.g., +5伤害)。
|
||||
* **Mod_mult**: 玩家天赋/暴击带来的百分比乘区 (e.g., 火焰伤害 +50%)。
|
||||
* **Res**: 敌人元素抗性 (0.0~1.0)。
|
||||
* **Armor**: 敌人护甲平铺扣除值(固定数值,不受百分比影响)。最终伤害最小为 1。
|
||||
|
||||
### 4.3 属性词条池 (Stats Pool) — *MVP 层*
|
||||
玩家在升级或商店中购买的属性,直接影响构建的上限。
|
||||
|
||||
> ⚠️ **MVP 说明**:以下 5 条属性为 MVP 阶段实现目标。后续扩充方向:
|
||||
> 第二批(防御类):护甲穿透、元素抗性降低、格挡概率。
|
||||
> 第三批(资源类):击杀回血(吸血)、金币掉落增益、法力吸取。
|
||||
> 第四批(移动类):冲刺距离、鬼影残影、落身位置标记。
|
||||
|
||||
| 属性名 | 影响 | 备注 |
|
||||
| :--- | :--- | :--- |
|
||||
| **施法速度 (Cast Speed)** | 减少每个法术块的执行时间 | 堆高了可以把“狙击枪”变成“冲锋枪” |
|
||||
| **魔力虹吸 (Mana Leech)** | 击杀回复魔力 | 续航流派的核心 |
|
||||
| **散射控制 (Accuracy)** | 减少弹道随机偏转角 | 修正“多重施法”带来的精准度惩罚 |
|
||||
| **运算力 (CPU)** | 增加核心的插槽上限 | 决定了你能组多复杂的法术 |
|
||||
| **元素精通 (Attunement)** | 增加特定元素(火/冰/电/毒)的触发概率和伤害 | 每点 Attunement 使对应元素伤害 +3% 且元素状态触发概率 +2%;详见 `numerical_design.md §1.1` |
|
||||
|
||||
### 4.4 怪物成长曲线
|
||||
* **波次 1-5**: 纯数值验证(血量低,无抗性)。
|
||||
* **波次 6-10**: 机制验证(出现 **反弹护盾怪**、**高护甲怪**)。这迫使玩家必须构建“穿透”或“元素伤害”来应对。
|
||||
* **波次 11-20**: 弹幕地狱。怪物的数量呈指数上升,考验玩家构建的 **AOE 覆盖率** 和 **FPS 优化**。
|
||||
|
||||
### 4.5 Boss 设计框架 (Boss Design)
|
||||
|
||||
Boss 出现于特定里程碑波次,强制验证玩家构建的适应性。
|
||||
|
||||
#### W10 精英 Boss (第 10 波)
|
||||
* **功能**: 展示某一核心机制的利用方式。
|
||||
* **典型设计示例**:
|
||||
* **镜像法师**: 反射所有正面 PHYSICAL 投射物,逼迫玩家必须配备至少一个元素伤害 ACTION 或使用 MODIFIER: pierce。
|
||||
* **重甲领主**: 自身有层层叠加的护甲,每次受到指定元素伤害时降低一层,验证玩家 **Stacking** 构建的强度。
|
||||
|
||||
#### W20 最终 Boss(第 20 波)
|
||||
* **功能**: 综合检验所有核心玩法的适应性。
|
||||
* **阶段设计**:
|
||||
1. **Phase 1** (全血): 高层抗性,对【共鸣效果】或特定元素伤害几乎无抵抗(正常受击)。纯物理构建并非硬性免疫,但每次物理命中触发 Boss 的"虚空再生"(每次物理命中恢复 `regen_on_physical_hit = 0.5%` 最大血量),形成动态 DPS 门槛——高 DPS 物理构建仍可磨血,但效率远不如元素/共鸣流,引导玩家主动考虑构建多样性。
|
||||
2. **Phase 2** (50% 血量): 开始弹幕攻势,玩家必须在走位的同时输出足够 DPS。
|
||||
3. **Phase 3** (杀死前): 爆发全屏投射物,考验玩家现有弹道规避构建的极限。
|
||||
* **设计原则**: Boss 无法被"无脑通关"(一种构建就能轻松将其秒杀);每个阶段应着重考察一个核心机制。
|
||||
|
||||
#### Boss 行为数据格式 (Boss Behavior Schema — P5-E1)
|
||||
|
||||
Boss 阶段数据完全由 JSON 驱动(`res://resources/bosses/boss_XX.json`),关卡设计师无需改代码即可调整 Boss 行为:
|
||||
|
||||
```json
|
||||
{
|
||||
"boss_id": "void_lord_w20",
|
||||
"display_name": "虚空领主",
|
||||
"wave": 20,
|
||||
"phases": [
|
||||
{
|
||||
"phase": 1,
|
||||
"hp_threshold_enter": 1.0,
|
||||
"hp_threshold_exit": 0.5,
|
||||
"resistances": { "PHYSICAL": 0.8, "FIRE": 0.1, "ICE": 0.1, "LIGHTNING": 0.1 },
|
||||
"regen_on_physical_hit": 0.005,
|
||||
"behavior_tags": ["stationary", "spread_projectiles"],
|
||||
"projectile_pattern": "spread_8"
|
||||
},
|
||||
{
|
||||
"phase": 2,
|
||||
"hp_threshold_enter": 0.5,
|
||||
"hp_threshold_exit": 0.1,
|
||||
"resistances": { "PHYSICAL": 0.3, "FIRE": 0.1, "ICE": 0.1, "LIGHTNING": 0.1 },
|
||||
"regen_on_physical_hit": 0.0,
|
||||
"behavior_tags": ["chase_player", "barrage_mode"],
|
||||
"barrage_interval": 3.0
|
||||
},
|
||||
{
|
||||
"phase": 3,
|
||||
"hp_threshold_enter": 0.1,
|
||||
"hp_threshold_exit": 0.0,
|
||||
"resistances": {},
|
||||
"behavior_tags": ["enrage", "fullscreen_burst"],
|
||||
"burst_interval": 5.0
|
||||
}
|
||||
],
|
||||
"ng_plus_phase": {
|
||||
"phase": 4,
|
||||
"unlock_condition": "ng_plus",
|
||||
"hp_threshold_enter": 0.0,
|
||||
"hp_threshold_exit": -0.3,
|
||||
"note": "NG+ 专属第四阶段:Boss 在 0% HP 时触发'最后意志',暂时无敌 5 秒,HP 回复至 30%,进入超狂暴状态",
|
||||
"resistances": { "PHYSICAL": 0.5 },
|
||||
"regen_on_physical_hit": 0.0,
|
||||
"behavior_tags": ["last_will", "hyper_enrage", "clone_summon"],
|
||||
"clone_count": 2,
|
||||
"invincible_duration": 5.0,
|
||||
"hp_restore_ratio": 0.3,
|
||||
"burst_interval": 2.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **字段说明**:`resistances` 为各元素抗性(0.0 = 无抵抗,1.0 = 完全免疫);`regen_on_physical_hit` 为每次物理命中触发的生命恢复比例(Phase 1 动态门槛的数据实现);`behavior_tags` 驱动 `BossAI` 状态机的行为策略选择。`ng_plus_phase` 字段仅在 NG+ 模式下由 `BossAI` 读取——普通模式下此字段存在但不生效(`unlock_condition` 检查决定是否激活)。Phase 4 的 `hp_threshold_exit: -0.3` 表示 Boss 在"回复后血量"归零后才真正死亡,防止普通模式下意外触发。
|
||||
|
||||
---
|
||||
|
||||
## 5. 游戏循环 (Gameplay Loop)
|
||||
|
||||
1. **准备 (The Lab)**:
|
||||
* 界面左侧是背包(存放法术卡),右侧是核心(插槽)。
|
||||
* 玩家将卡牌拖入插槽,点击”测试按钮”,在一个安全的靶场内测试射击效果,还有 DPS 统计面板。
|
||||
* **防呆机制**:点击”进入战斗”时,系统先对 Deck 进行验证:若全部插槽均为空或仅含 MODIFIER/TRIGGER 而无任何 ACTION 类型法术,展示警告弹窗”法术塔无法开火!需要至少一个行动类法术。”并阻止进入。
|
||||
* **深度玩法**:调整卡牌顺序,比如把 [伤害增强] 放在 [多重施法] 之前还是之后,效果截然不同。
|
||||
|
||||
2. **战斗 (The Arena)**:
|
||||
* 倒计时 60 秒。
|
||||
* 玩家自动使用构建好的武器射击。
|
||||
* 主要操作:走位躲避弹幕,拾取掉落的”法力核心”(钱)。
|
||||
* **走位-构建互动 (G4)**:部分法术对玩家的移动状态有响应,让走位不只是”跑步”:
|
||||
* **移动增益型法术**(如”电冲弹”):玩家处于运动状态时该法术伤害 ×1.5,静止时恢复基准值。
|
||||
* **冲刺波(Dash Wave)型核心**:特定 Core 属性,玩家触发冲刺动作时自动在冲刺轨迹上释放一次附加法术(消耗独立冷却,不走主 Deck 队列)。
|
||||
* **停步蓄力型法术**(如”引力奇点”):玩家静止超过 1.5 秒时触发蓄力,停止移动后立即释放一次超载版本(DMG ×3);一旦移动则蓄力清零。
|
||||
* **UI 反馈**:静止后 0.3 秒开始显示蓄力进度环(天空蓝,展示 1.5 秒倒计时);蓄力满时进度环闪烁并播放充能音效;移动就进度环消失。进度环由 `UIManager` 经 EventBus `CHARGE_STATE_CHANGED` 事件驱动,不得在 `SpellEvaluator` 中直接操作 UI 节点。
|
||||
* *设计意图*:玩家可以通过选择法术类型来主动决定”走位风险收益比”,使操作与构建选择相互影响。
|
||||
3. **购物 (The Shop)**:
|
||||
* 战斗结束,进入商店。
|
||||
* **货架 A**: 出售新的法术卡片(随机池)。
|
||||
* **货架 B**: 出售新的”核心”(更牛的主板)。
|
||||
* **货架 C**: 基础属性提升(生命值、暴击率)。
|
||||
* **出售与转型规则 (G5)**:
|
||||
* 玩家可在商店界面将背包中的法术卡**出售**,回收该卡购入价格的 **50%**(下限 10G)。
|
||||
* 更换 Core 时,原 Core 不适配的法术卡(插槽类型不匹配)将自动移入**等待区**(上限 5 张);超出 5 张的部分,玩家必须手动选择出售或丢弃。
|
||||
* 等待区中的卡可在下次商店阶段重新插入新 Core,或继续出售。
|
||||
* *设计意图*:转型有真实代价——金币折损 + 等待区占位;鼓励玩家谨慎决策而非随意重构。
|
||||
* *经济策略*:是买一个强力的”黑洞”法术?还是把钱用来升级”回蓝速度”以支撑现有的构建?
|
||||
|
||||
## 6. 新手引导设计 (Onboarding)
|
||||
|
||||
Roguelite 的"第一把"体验至关重要。系统复杂度必须渐进式解锁,避免一次性呈现所有概念。
|
||||
|
||||
### 6.1 渐进式解锁顺序
|
||||
|
||||
| 第几局 | 开放内容 | 目的 |
|
||||
| :--- | :--- | :--- |
|
||||
| 局 1 | 预设法杖(只有 3 个 Spark Bolt),只教走位 | 零门槛上手,感受割草爽感 |
|
||||
| 局 2 | 解锁"法杖编辑",仅开放 Tier 1 法术 | 体验最简单的换弹 |
|
||||
| 局 3 | 引入第一张 Modifier(伤害增强)+ Tooltip 教学 | 理解 Modifier 放在 Action 之前生效 |
|
||||
| 局 4 | 引入第一张 Trigger(击中触发)+ 靶场演示 | 看到"子母弹"视觉效果 |
|
||||
| 局 5+ | Tier 2 法术、Logic 类法术逐步开放 | 自由探索 |
|
||||
|
||||
### 6.2 情境提示 (Contextual Tips)
|
||||
* 当玩家第一次持有 Trigger 但没有配套 Action 时,显示提示:"触发器需要在其后面放置动作才能生效 →"。
|
||||
* 当玩家 `cpu_limit` 占用超过 80% 时,运算力进度条变红并显示:"运算力不足,法杖将无法完整执行"。
|
||||
* 商店中首次出现特殊 Core(如 `matrix_board`)时,显示简短动画演示邻接加成效果。
|
||||
|
||||
### 6.3 靶场 (Training Range)
|
||||
在**每次**进入商店阶段时,靶场始终可用:
|
||||
* 右侧显示实时 DPS 数字。
|
||||
* 点击"慢放"按钮,以 0.1x 速度演示法术链的执行顺序(调试教学用)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 元进展设计 (Meta-Progression)
|
||||
|
||||
元进展是 Roguelite 的长期粘性来源。玩家每次游玩都在为"总体库"做出贡献,即使失败也有收获感。
|
||||
|
||||
### 7.1 局外货币:以太碎片 (Aether Fragments)
|
||||
* **来源**: 每次游玩结束时,按波次数量奖励(W1=0, W5=3, W10=8, W20=20)。
|
||||
* **永久保留**,不随死亡重置。
|
||||
|
||||
### 7.2 局外解锁树 (Unlock Tree)
|
||||
用以太碎片解锁**永久加入掉落池**的内容,不直接增强玩家数值:
|
||||
|
||||
| 花费 | 解锁内容 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| 5 碎片 | 法术:连锁闪电 | 加入商店掉落池 |
|
||||
| 10 碎片 | 核心:构造板 (matrix_board) | 加入商店掉落池 |
|
||||
| 15 碎片 | 法术:战术核弹 | 加入商店掉落池 |
|
||||
| 30 碎片 | 核心:记忆法杖 (persistent_memory) | 加入商店掉落池 |
|
||||
| 50 碎片 | 挑战模式:诅咒波次 | 解锁额外难度模式 |
|
||||
|
||||
### 7.3 图鉴系统 (Codex)
|
||||
* 玩家每次拾取/购买新法术,自动解锁该法术的图鉴词条(含详细数值和设计意图)。
|
||||
* 图鉴中显示**元素亲和性标签**,暗示可能的共鸣组合(如"此法术具有 💧 水属性亲和")。
|
||||
* 成就:首次触发每种元素反应,解锁对应的"炼金日志"风味文本。
|
||||
|
||||
### 7.4 设计原则
|
||||
* 解锁内容**只扩展可能性**(让玩家看到更多法术),而非直接增强初始属性(避免 P2W 感)。
|
||||
* 通过 20 局可解锁全部核心内容,给硬核玩家明确的完成感。
|
||||
|
||||
---
|
||||
|
||||
## 8. 失败、通关与里程碑 (Failure, Victory & Checkpoints)
|
||||
|
||||
### 8.1 死亡与失败机制 (G1)
|
||||
|
||||
* **触发条件**:玩家 HP 降至 0 时死亡,当局立即结束。
|
||||
* **死亡流程**:
|
||||
1. 播放死亡动画与音效。
|
||||
2. 统计数据冻结(本局最高波次、总输出伤害、最高单次伤害、击杀总数)。
|
||||
3. 显示 **本局总结屏 (Run Summary)**,含构建快照(自动截图保存至 `user://runs/YYYY-MM-DD_run_N.png`)。
|
||||
4. 给予以太碎片奖励(按波次阶梯),不因死亡扣减。
|
||||
* **唯一例外**:天赋"最后意志 (Last Will)"——允许一次以 1 HP 存活,每局限触发一次,触发后天赋失效。
|
||||
* **死亡不回退**:已解锁法术图鉴词条、成就、Meta 解锁树进度,均在死亡后保留。
|
||||
|
||||
### 8.2 通关条件与通关后内容 (G2)
|
||||
|
||||
* **通关条件**:击败 W20 终波 Boss(三阶段精英怪),触发通关演出。
|
||||
* **首通奖励**:
|
||||
* 给予 30 以太碎片(固定)。
|
||||
* 解锁 **New Game+ (NG+)** 模式:全局敌人属性 ×1.5、弹幕速度 +30%、Boss 新增第四阶段。
|
||||
* **NG+ 递增**:每次 NG+ 通关再给予 20 碎片(递减 20%,下限 5)。
|
||||
* **无限模式 (Endless)**:W20 通关后可选择继续,每 5 波难度系数 +10%(无上限),用于挑战竞速排行榜,不计入 Meta 进度。
|
||||
* **排行榜技术实现(P6-N19)**:以**到达波次 (Wave Reached)** 为主排名维度,同波次按**总经过时间 (Elapsed Seconds)** 升序排。
|
||||
- **本地存储**:`user://endless_records.json`,Schema:`{ "records": [{ "wave": int, "elapsed_sec": float, "build_snapshot": String, "timestamp": int }] }`,保留最近 20 条。
|
||||
- **在线排行榜(P2 可选)**:上传分数 = `wave * 100000 + (86400 - elapsed_sec)`,单整数实现双维度排序(波次高优先,同波次用剩余秒数打分)。
|
||||
- **防作弊**:本地记录含 `build_snapshot`(Deck checksum),便于审核异常成绩。
|
||||
* **新机制注入 (G7)**:Endless 模式不只是纯数值膨胀,每 10 波(W30/W40/W50...)随机激活一条**全局词条**(每次 1 条,不与前轮叠加,仅保留最新一条):
|
||||
|
||||
| 词条 | 效果 |
|
||||
| :--- | :--- |
|
||||
| 镜子世界 | 所有弹道被首次命中后反弹回发射者方向 |
|
||||
| 怪物分裂 | 敌人死亡时以 50% HP 分裂为 2 个小型版本(仅分裂 1 次)|
|
||||
| 地图缩小 | 可活动区域半径减少 20%(最小 50% 原始大小)|
|
||||
| 魔力反转 | 法术 Delay 变为正向加成,原 Delay 越长攻速越快 |
|
||||
| 精英泛滥 | 所有普通敌人升级为精英,HP ×2、奖励 ×2 |
|
||||
|
||||
* 每 20 波(W40/W60/W80...)触发 **Boss Rush 片段**:连续出现 3 个 Boss(随机抽取已出现过的 Boss 变体),全部击败后获得大量碎片与特殊法术卡(每次 Boss Rush 保证至少 1 张 Tier 3 卡)。
|
||||
|
||||
### 8.3 里程碑与 Checkpoint (G3)
|
||||
|
||||
到达 **W5 / W10 / W15** 触发里程碑事件:
|
||||
|
||||
| 里程碑 | 即时奖励 | 解锁内容 |
|
||||
| :--- | :--- | :--- |
|
||||
| W5 | 以太碎片 ×2 | 解锁 W5 练习模式起点 |
|
||||
| W8 | — | **Mini Boss 首现警告波**(非里程碑;仅首次到达时弹出提示,Boss 数据进入图鉴)|
|
||||
| W10 | 以太碎片 ×3 | 解锁 W10 练习模式起点;W10 精英 Boss 激活 |
|
||||
| W15 | 以太碎片 ×4 | 解锁 W15 练习模式起点 |
|
||||
|
||||
* **里程碑碎片加成**缓解高波次死亡的挫败感(玩家不会"一无所获")。
|
||||
* **练习模式 (Practice Mode)**:从任意已到达的里程碑波次开始,无限时间商店,不计入 Meta 进度也不消耗里程碑次数;用于测试新构建。
|
||||
|
||||
---
|
||||
|
||||
## 9. 总结
|
||||
《魔法工匠》不只是一款“幸存者”游戏,它是一个**带有动作外壳的逻辑解谜游戏**。每一个通过第 20 波的玩家,都是一名合格的“魔法软件工程师”。
|
||||
@@ -0,0 +1,248 @@
|
||||
# 数值策划设计案:魔法工匠 (Arcane Artificer)
|
||||
|
||||
## 0. 设计理念 (Design Philosophy)
|
||||
|
||||
本游戏的数值体系服务于 **“高自由度构建”** 这一核心目标。
|
||||
数值设计不应教玩家怎么玩,而应提供足够宽广的边界(Boundary)和明确的权衡(Trade-off)。
|
||||
|
||||
**核心公式**:
|
||||
`构建强度 = (基础数值 * 修正系数) ^ 逻辑复杂度`
|
||||
|
||||
## 1. 基础资源模型 (Resource Model)
|
||||
|
||||
### 1.1 玩家属性 (Player Stats)
|
||||
这些属性是全局的,会修正所有法杖的输出。
|
||||
|
||||
| 属性 ID | 名称 | 基准值 | 软上限 | 硬上限 | 说明 |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| `hp_max` | 最大生命 | 100 | 2000 | - | |
|
||||
| `mana_max` | 最大魔力 | 100 | 1000 | - | 全局蓝条上限,法杖也有自己的上限,取 Min 值 |
|
||||
| `move_speed` | 移动速度 | 300 | 600 | 800 | 像素/秒 |
|
||||
| `cast_delay_mod` | 施法延迟修正 | 1.0 (100%) | 0.1 | 0.01 | 越低越快,乘算系数 |
|
||||
| `recharge_speed_mod` | 充能速度修正 | 1.0 (100%) | 5.0 | - | 越低越快 |
|
||||
| `luck` | 幸运 | 0 | 100 | - | 影响高阶法术掉率、暴击率 |
|
||||
| `cpu_limit` | 运算力上限 | 5 | 20 | 50 | `SpellEvaluator` 实际 MAX_OPS = `cpu_limit × 40`(默认 5 → 200 步;软上限 20 → 800 步;硬上限 50 → 2000 步)。升级此属性可执行更复杂的法术链,与 `implementation_plan.md §2.2` 的 `MAX_OPS_PER_CPU = 40` 常量对应。 |
|
||||
| `attunement_fire` | 火焰精通 | 0 | 50 | 100 | 每点:火焰伤害 +3%(乘算),火焰状态(点燃/爆燃)触发概率 +2%。影响 `damage_type=FIRE` 的所有投射物。 |
|
||||
| `attunement_ice` | 冰霜精通 | 0 | 50 | 100 | 每点:冰霜伤害 +3%,冰冻/减速触发概率 +2%。 |
|
||||
| `attunement_lightning` | 雷电精通 | 0 | 50 | 100 | 每点:雷电伤害 +3%,麻痹/连锁导电触发概率 +2%。 |
|
||||
| `attunement_poison` | 毒素精通 | 0 | 50 | 100 | 每点:毒素 DoT +3%,中毒叠层上限 +1(上限 20)。 |
|
||||
|
||||
### 1.2 经济系统 (Economy)
|
||||
|
||||
* **金币 (Gold)** (代号 `G`)
|
||||
* **来源**: 击杀怪物 (1-5G),波次结算 (100G + 10%利息)。
|
||||
* **消耗**: 购买法术 (50-500G),购买法杖 (200-2000G),刷新商店 (20G*)。
|
||||
* **利息上限**: 10% 利息计算时,单次波次奖励上限为 **100G**(即持有 1000G 以上部分不再产生额外利息)。防止后期经济失控膨胀。
|
||||
* **膨胀控制**: 商店刷新价格每次增加,波次结束后重置。金币不捡 5 秒后消失,逼迫玩家移动拾取。
|
||||
- **刷新价格公式(F11 补充,P6-N23)**:`刷新费用 = 20 + (本波已刷新次数 × 10)`(单位:G)。
|
||||
即第 1 次刷新 20G,第 2 次 30G,第 3 次 40G,以此类推;波次结算后重置为 20G。
|
||||
设计意图:前 1-2 次刷新成本低,鼓励灵活换货;反复刷新代价指数感,抑制"无脑刷新"。
|
||||
实现:`ShopManager` 维护 `_refresh_count: int`(波次内),`WAVE_COMPLETE` 事件后清零。
|
||||
|
||||
* **经验值 (XP)**
|
||||
* **升级曲线(权威公式 P6-N16)**:
|
||||
$$XP\_required(level) = \lfloor 10 \times 1.4^{level-1} \rfloor$$
|
||||
| 等级 | 所需 XP | 累计 XP |
|
||||
| :--- | :--- | :--- |
|
||||
| 1→2 | 10 | 10 |
|
||||
| 5→6 | 54 | 234 |
|
||||
| 10→11 | 289 | 1397 |
|
||||
| 15→16 | 1551 | 7174 |
|
||||
| 20→21 | 8322 | 39,343 |
|
||||
- 系数 `1.4` 使早期升级快速(前 5 级约 4 波内完成),后期升级需要持续 3-5 波,节奏符合 Roguelite 期望。
|
||||
- `ProfileManager` 在关卡加载时预计算前 50 级的阈值表并缓存,避免运行时浮点指数运算。
|
||||
- **⚠️ F10 可行性验证(P6-N22)**:Level 21 累计 XP 需求 39,343。20 波 × 60 秒/波 = 1200 秒游戏时长。
|
||||
设平均每秒击杀 2 只敌人,每只掉 15 XP(Wave 10+ 标准怪),则 1200 × 2 × 15 = 36,000 XP——
|
||||
**略低于 39,343**,意味着理论上最高只能达到 Level 20 左右(不满 21 级)。
|
||||
若期望玩家在通关时达到 20 级,需将 `XP_required` 公式中系数从 `1.4` 调整为约 `1.38`,
|
||||
或将波次奖励 XP 提高(每波结算奖励 200 XP)。**当前设计定为 Level 20 通关上限,不强制要求 Level 21**;
|
||||
Level 21+ 曲线仅服务于 Endless 模式。敌人 XP 掉落参考值:Wave 1 杂鱼 = 5 XP,Wave 10 精英 = 30 XP,Boss = 500 XP。
|
||||
|
||||
---
|
||||
|
||||
## 2. 法术系统数值 (Spell System Metrics)
|
||||
|
||||
### 2.1 核心参数定义
|
||||
|
||||
每个法术卡片 (Godot Resource / Json) 包含以下关键数值:
|
||||
|
||||
1. **Mana Cost (魔耗)**: 执行此节点消耗的魔力。
|
||||
* *原则*: 越强力的效果,蓝耗越高,或者有其他负面代价。
|
||||
2. **Cast Delay (施法后摇)**: 执行此节点后,给法杖增加的“冷却时间”。
|
||||
* *原则*: 强大的单发法术通常有高延迟(如核弹 +2.0s)。
|
||||
3. **Recharge Time (充能延迟)**: 这是一个特殊的延迟,只有当完整的一轮(Deck空了)打完正在重置时,才会计算。
|
||||
* *原则*: 只有极强的终极技能才会增加此值。
|
||||
|
||||
### 2.2 基础法术参考表 (Spell Database)
|
||||
|
||||
> **⚠️ 注意**:Projectile/Action 类型法术必须在 `.tres` 中显式设置 `damage_type`(DamageType 枚举值)。
|
||||
> 未填写时 `ProjectileDef.reset()` 默认 `DamageType.PHYSICAL`,但设计意图应在此表中明示,
|
||||
> 避免内容制作者遗漏火/雷等元素属性导致构建伤害系统无法正常触发。
|
||||
|
||||
#### Tier 1 (新手/平民)
|
||||
| ID | 名称 | 类型 | Mana | Delay | 伤害类型 | 效果/修正 | 备注 |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| `spark_bolt` | 魔法飞弹 | Projectile | 5 | +0.05s | PHYSICAL | Dmg: 3, Speed: 600 | 最基础的突突突 |
|
||||
| `energy_orb` | 能量球 | Projectile | 20 | +0.20s | PHYSICAL | Dmg: 15, Speed: 300 | 慢速高伤 |
|
||||
| `double_cast` | 双重施法 | Multicast | 2 | +0.00s | — | Draw: 2 | 必备插件 |
|
||||
| `spread_mod` | 散射修正 | Modifier | 0 | -0.10s | — | Spread: +30°, Speed: +10% | 这一发打不准,但射得快 |
|
||||
|
||||
#### Tier 2 (进阶/功能)
|
||||
| ID | 名称 | 类型 | Mana | Delay | 伤害类型 | 效果/修正 | 备注 |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| `shotgun_blast` | 霰弹爆破 | Projectile | 25 | +0.40s | PHYSICAL | 发射3枚 4dmg 的弹片 | 自带多重效果 |
|
||||
| `damage_plus` | 伤害增强 | Modifier | 15 | +0.10s | — | Dmg: +10 | |
|
||||
| `speed_up` | 加速推进 | Modifier | 5 | -0.05s | — | Speed: +400, Range: +20% | 狙击流必备 |
|
||||
| `trigger_hit` | 击中触发 | Trigger | 10 | +0.00s | — | Payload Dmg: 0.1x | 子母弹核心 |
|
||||
|
||||
#### Tier 3 (史诗/质变)
|
||||
| ID | 名称 | 类型 | Mana | Delay | 伤害类型 | 效果/修正 | 备注 |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| `nuke` | 战术核弹 | Projectile | 200 | +3.00s | PHYSICAL | Dmg: 500, Radius: 300 | 屏幕清空,可能会炸死自己 |
|
||||
| `homing` | 自动追踪 | Modifier | 40 | +0.10s | — | Homing Force: 5.0 | 只有这一个修正就够改变玩法 |
|
||||
| `heavy_cost` | 鲜血献祭 | Modifier | 0 | -0.50s | — | Dmg: +50, Cost: 10 HP | 卖血流 |
|
||||
| `chain_bolt` | 连锁闪电 | Projectile | 60 | +0.80s | LIGHTNING | 弹射 5 次 | 清怪神器 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 敌人与战斗数值 (Combat Metrics)
|
||||
|
||||
### 3.1 伤害计算管线
|
||||
|
||||
> **⚠️ 权威公式(Authoritative Formula)**:所有文档均应引用此处定义,不得出现矛盾版本。
|
||||
|
||||
为了支持复杂的 Buff/Debuff,伤害计算必须分步:
|
||||
|
||||
1. **Base Damage**: 法术面板值 (e.g. 10)
|
||||
2. **Add Modifiers**: 累加修正器 (e.g. +5 +5 -> 20)
|
||||
3. **Mult Modifiers**: 乘法修正器 (e.g. Critical 2.0x, Element Weakness 1.5x)
|
||||
4. **Flat Reduction**: 敌人护甲 (Armor -2)
|
||||
5. **Percent Reduction**: 敌人抗性 (Fire Res 50%)
|
||||
|
||||
```
|
||||
Damage = ((Base + Add) × Mult) × (1 - Res) - Armor
|
||||
```
|
||||
*(最小伤害为 1)*
|
||||
|
||||
### 3.2 怪物设计标准
|
||||
|
||||
我们设定 **“标准单位时间伤害 (DPS)”** 为基准。
|
||||
* 玩家 Lvl 1 预计 DPS: 20
|
||||
* 玩家 Lvl 20 预计 DPS: 5000+
|
||||
|
||||
| 怪物等级 | 怪物类型 | HP | 移动速度 | 攻击力 | 特殊词条 |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| Wave 1 | 杂鱼土豆 | 15 | 100 | 5 | - |
|
||||
| Wave 5 | 冲锋甲虫 | 80 | 250 | 12 | 击退抗性 50% |
|
||||
| Wave 10| 坦克巨兽 | 2000 | 50 | 30 | 护甲 5 (免疫机枪) |
|
||||
| Wave 20| 虚空领主 | 50000| 150 | 100 | 弹幕反射盾 |
|
||||
|
||||
### 3.3 波次强度控制 (Pacing)
|
||||
|
||||
游戏设计为 20 波 (Wave),每波 60秒。
|
||||
* **W1-3**: 爽局。怪物血量 < 玩家单发伤害。
|
||||
* **W4-7**: 压力局。怪物密度增加,玩家必须拥有至少一个 AOE 手段(穿透/爆炸)。
|
||||
* **W8**: **首领战 (Mini Boss)**。考验单体 DPS。
|
||||
* **W9-15**: 资源局。怪物掉落增加,但伴随高护甲怪,考验玩家的“破甲/元素”构建。
|
||||
* **W20**: **最终 Boss**。
|
||||
|
||||
---
|
||||
|
||||
## 4. 元素反应矩阵 (Elemental Matrix)
|
||||
|
||||
为了增加构建深度,引入简单的元素克制/反应。
|
||||
|
||||
| 攻击 \ 如果目标状态 | 无 | 潮湿 (Wet) | 油腻 (Oily) | 燃烧 (Burning) | 冰冻 (Frozen) |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| **火 (Fire)** | 点燃 | 蒸发 (AOE) | 爆燃 (x3 Dmg) | - | 融化 (解冻) |
|
||||
| **冰 (Ice)** | 减速 | 冻结 (硬控) | - | 熄灭 | - |
|
||||
| **雷 (Elec)** | 麻痹 | 连锁导电 (AOE) | - | 过载 (爆炸) | 碎冰 (物理Dmg) |
|
||||
|
||||
*数值策划备注:*
|
||||
* **潮湿**:受到雷电伤害 +100%。
|
||||
* **油腻**:受到火焰伤害 +200%,且持续时间翻倍。
|
||||
* **冻结**:无法移动,受到物理撞击伤害 +300% (即碎冰机制)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 平衡性隐患与对策 (Balancing Risks)
|
||||
|
||||
### A. 无限蓝机枪 (Infinite Mana Gun)
|
||||
* **问题**: 玩家堆积大量 `-Delay` 和 `Mana Regen`,导致射速突破帧率限制,且蓝不掉。
|
||||
* **对策**:
|
||||
* **最小帧限制**: 设定射击间隔至少为 1 帧 (0.016s)。
|
||||
* **过热机制 (Overheat)**: 连续射击超过 5秒,法杖进入“过热”状态,强制冷却 2秒 或 增加散布。
|
||||
* **递增蓝耗**: 连续快速射击时,每发子弹蓝耗增加 1%,停止射击后快速衰减。
|
||||
|
||||
### B. 显卡杀手 (GPU Killer)
|
||||
* **问题**: 玩家构建出 `无限分裂` + `无限粒子`。
|
||||
* **对策**:
|
||||
* **最大弹道数 (Max Projectiles)**: 全局限制 2000 个弹道。超过时,新的子弹不再生成(或顶掉旧的)。
|
||||
* **双重限制机制**:
|
||||
* **MAX_OPS**(= `cpu_limit × MAX_OPS_PER_CPU`,默认 `5 × 40 = 200`,软上限 `20 × 40 = 800`):`SpellEvaluator` 每帧单次施法最多执行对应条指令(包括 ACTION/MODIFIER/LOGIC 各类节点)。主要防止线性/循环构建死循环。与 `implementation_plan.md §2.2` 的 `MAX_OPS_PER_CPU = 40` 常量对应。
|
||||
* **嵌套深度锁 MAX_TRIGGER_DEPTH = 3**:`SpellEvaluator.execute_sub` 调用时传入 `depth` 参数,超过 3 层不再触发新的 SubPayload(即 TRIGGER 内嵌套 TRIGGER 最多 3 层)。**两者分工**:MAX_OPS 管局内指令数量,MAX_TRIGGER_DEPTH 管跨子弹的调用深度。
|
||||
|
||||
### C. 站桩输出 (AFK Build)
|
||||
* **问题**: 比如”自动追踪吸血光环”,玩家站着不动就能通关。
|
||||
* **对策**:
|
||||
* **反挂机怪**: 设计一种怪物,会向玩家当前位置投掷”毒圈”,逼迫玩家移动。
|
||||
* **资源衰减**: 掉落的金币如果不捡,5秒后消失(或变少),逼迫玩家去捡钱。
|
||||
|
||||
### D. W20 Boss Phase 1 DPS 门槛(P6-N17)
|
||||
* **问题**: Phase 1 的 `regen_on_physical_hit = 0.005`(每次物理命中恢复 Boss 最大 HP 的 0.5%)。
|
||||
Boss 最大 HP = 50,000,即每次物理命中回血 **250 HP**。
|
||||
若玩家攻速 10 发/秒(纯物理构建),Boss 每秒净回血 2,500 HP。
|
||||
* **DPS 突破阈值(最低要求)**:物理构建需满足以下条件才能有效磨血:
|
||||
$$DPS_{physical} > \frac{250 \times hit\_rate}{1} \Rightarrow DPS > 2500 \text{(10发/秒时)}$$
|
||||
等价条件:每发伤害 > 250(单发狙击流),或总 DPS > 2500(高频流)。
|
||||
- **设计意图**:并非阻止物理构建,而是要求玩家必须达到中后期构建强度(DPS 5000+)才能高效磨血;低配物理构建可用更长时间磨过,但效率不如元素/共鸣流。
|
||||
- **数值调整入口**:`res://resources/bosses/boss_w20.json` 中的 `regen_on_physical_hit` 字段,平衡迭代时直接修改数据而无需改代码。
|
||||
|
||||
### E. `infinite_spells` 与 `heavy_cost` 叠加交互(P6-N18)
|
||||
* **问题**:`infinite_spells` 特性(法力耗尽时每发+1 HP 代价)与 `heavy_cost` 修正器(每次施法-10 HP)叠加时,HP 代价是否累加?
|
||||
* **规则定义**:两者**独立计算、顺序执行**:
|
||||
1. `SpellEvaluator` 先执行 `heavy_cost` MODIFIER(扣 10 HP,归类为”构建设计主动代价”)。
|
||||
2. **之后**,若 mana 不足,`infinite_spells` 机制额外扣 1 HP(归类为”资源溢出代价”)。
|
||||
3. 两者同帧触发时,单发最大 HP 代价 = `heavy_cost 值 + 1`(如 10+1=11 HP/发)。
|
||||
* **UI 提示**:法杖编辑界面在检测到 `infinite_spells Core` + `heavy_cost` 组合时,显示橙色警告”⚠ 双重 HP 消耗”,并提示预估每秒 HP 消耗量。
|
||||
* **上限保护**:单次施法 HP 代价上限 = `player.hp_max × 0.15`(15% 上限),防止极端组合一发即死。
|
||||
---
|
||||
|
||||
## 6. 波次生成配置格式 (SpawnConfig Schema — E5 权威定义)
|
||||
|
||||
波次内容**完全由数据驱动**,关卡设计师仅需编辑 JSON 无需改代码。文件位于 `res://resources/waves/wave_XX.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"wave": 5,
|
||||
"duration_sec": 60,
|
||||
"budget": 200,
|
||||
"spawn_groups": [
|
||||
{
|
||||
"enemy_id": "bug_charger",
|
||||
"weight": 3,
|
||||
"count_min": 2,
|
||||
"count_max": 5,
|
||||
"spawn_interval": 3.0,
|
||||
"spawn_radius_from_player": [400, 700]
|
||||
},
|
||||
{
|
||||
"enemy_id": "tank_brute",
|
||||
"weight": 1,
|
||||
"count_min": 1,
|
||||
"count_max": 1,
|
||||
"spawn_interval": 15.0,
|
||||
"spawn_radius_from_player": [500, 800]
|
||||
}
|
||||
],
|
||||
"elite_overrides": [
|
||||
{ "at_sec": 45, "enemy_id": "elite_bug_charger", "count": 1 }
|
||||
],
|
||||
"boss": null
|
||||
}
|
||||
```
|
||||
|
||||
* `budget`:本波总"刷怪点数",每个怪物有对应的 `cost`;实际刷新量 = `floor(budget / cost)`。
|
||||
* `weight`:同时存在多个 group 时,按权重随机选组,而非轮流刷新。
|
||||
* `elite_overrides`:精确时间点强制刷出精英怪,不消耗 budget。
|
||||
* `boss`:非 null 时在波末触发 BossSpawn(见 §6 Boss 设计框架)。
|
||||
@@ -0,0 +1,284 @@
|
||||
# FTUE / 新手引导系统架构设计
|
||||
# Tutorial & Onboarding System Architecture
|
||||
|
||||
> **范围**:本文档涵盖游戏的首次用户体验(FTUE)、上下文提示、引导状态机,以及
|
||||
> `TutorialManager` Autoload 的完整设计规范。
|
||||
> 本文档以 `architecture_design.md` 为基础,所有运行时接口遵循 §6.1 语言分区原则。
|
||||
|
||||
---
|
||||
|
||||
## 一、设计目标
|
||||
|
||||
| 目标 | 度量标准 |
|
||||
| :--- | :--- |
|
||||
| 前 3 分钟玩家留存 | 90%+ 玩家完成首波(Wave 1)且尝试第一次施法 |
|
||||
| 零文字强制阅读 | 教学信息通过上下文提示传达,不弹出强制阅读对话框 |
|
||||
| 可跳过原则 | 所有引导步骤可在设置中永久关闭,或在进行中跳过 |
|
||||
| 渐进式复杂度 | W1–W3 仅展示核心移动+射击;W4+ 逐步解锁进阶提示 |
|
||||
|
||||
---
|
||||
|
||||
## 二、TutorialManager Autoload
|
||||
|
||||
### 2.1 职责边界
|
||||
|
||||
- **GDScript 层**(`tutorial_manager.gd`):状态读写、EventBus 订阅、提示节点显示/隐藏。
|
||||
- **不含热路径**:提示触发频率极低(每场景级别),全部保留在 GDScript。
|
||||
|
||||
```gdscript
|
||||
# scripts/autoloads/tutorial_manager.gd
|
||||
extends Node
|
||||
|
||||
## 教学步骤枚举(顺序即解锁顺序)
|
||||
enum TutorialStep {
|
||||
MOVE = 0, # WASD 移动
|
||||
CAST = 1, # 首次自动施法(Wand 冷却后自动提示)
|
||||
PICK_SPELL = 2, # 首次拾取法术卡
|
||||
OPEN_SHOP = 3, # 首次进入商店
|
||||
EQUIP_CORE = 4, # 首次装备 Core
|
||||
KILL_10 = 5, # 击杀 10 个敌人
|
||||
WAVE_CLEARED = 6, # 通过 Wave 1
|
||||
RESONANCE_HINT = 7, # 首次触发共鸣(S3 解锁)
|
||||
COMPLETED = 99, # 引导全部完成
|
||||
}
|
||||
|
||||
## 已完成步骤的持久化集合(存入 ProfileManager)
|
||||
var _completed_steps: PackedInt32Array = PackedInt32Array()
|
||||
|
||||
## 当前激活提示步骤(-1 = 无)
|
||||
var _active_step: int = -1
|
||||
|
||||
func _ready() -> void:
|
||||
# 从 ProfileManager 恢复已完成状态
|
||||
_completed_steps = ProfileManager.get_int_array(
|
||||
"tutorial_completed", PackedInt32Array())
|
||||
# 订阅进度事件
|
||||
EventBus.subscribe(EventID.PLAYER_SPAWNED, _on_player_spawned)
|
||||
EventBus.subscribe(EventID.SPELL_CAST, _on_spell_cast)
|
||||
EventBus.subscribe(EventID.SHOP_OPENED, _on_shop_opened)
|
||||
EventBus.subscribe(EventID.ENEMY_KILLED, _on_enemy_killed)
|
||||
EventBus.subscribe(EventID.WAVE_CLEARED, _on_wave_cleared)
|
||||
EventBus.subscribe(EventID.RESONANCE_TRIGGERED, _on_resonance)
|
||||
```
|
||||
|
||||
### 2.2 状态机定义
|
||||
|
||||
```
|
||||
[初始状态]
|
||||
│
|
||||
▼
|
||||
MOVE ──────────────── 检测:玩家首帧输入 WASD 后标记完成
|
||||
│
|
||||
▼
|
||||
CAST ──────────────── 检测:EventID.SPELL_CAST 首次触发
|
||||
│
|
||||
▼
|
||||
PICK_SPELL ─────────── 检测:拾取 loot 掉落的法术卡
|
||||
│
|
||||
▼
|
||||
OPEN_SHOP ──────────── 检测:Wave 1 通关后自动弹出商店提示
|
||||
│
|
||||
▼
|
||||
EQUIP_CORE ─────────── 检测:首次从商店装备任意 Core
|
||||
│
|
||||
▼
|
||||
KILL_10 ────────────── 检测:ENEMY_KILLED 事件累计 10 次
|
||||
│
|
||||
▼
|
||||
WAVE_CLEARED ───────── 检测:WAVE_CLEARED(wave_num=1)
|
||||
│
|
||||
▼
|
||||
RESONANCE_HINT ─────── 检测:RESONANCE_TRIGGERED(S3 起激活)
|
||||
│
|
||||
▼
|
||||
COMPLETED ──────────── 持久化到 ProfileManager,停止所有提示
|
||||
```
|
||||
|
||||
- **前向跳跃**:若玩家自行完成某步(无提示出现),步骤自动标记完成,无需显示提示。
|
||||
- **后向保护**:已完成步骤不会因新 Run 而重置(存 ProfileManager 而非 RunData)。
|
||||
|
||||
### 2.3 核心接口
|
||||
|
||||
```gdscript
|
||||
# 查询某步骤是否已完成
|
||||
func is_completed(step: TutorialStep) -> bool:
|
||||
return step in _completed_steps
|
||||
|
||||
# 手动标记完成(商店系统 / 法杖编辑器调用)
|
||||
func mark_complete(step: TutorialStep) -> void:
|
||||
if is_completed(step): return
|
||||
_completed_steps.append(int(step))
|
||||
ProfileManager.set_int_array("tutorial_completed", _completed_steps)
|
||||
_hide_hint()
|
||||
EventBus.emit(EventID.TUTORIAL_STEP_COMPLETED, step)
|
||||
|
||||
# 显示上下文提示(锚定到 UI 节点旁)
|
||||
func show_hint(step: TutorialStep, anchor: Control,
|
||||
offset: Vector2 = Vector2.ZERO) -> void:
|
||||
if is_completed(step): return
|
||||
_active_step = step
|
||||
# 委托给 UIManager 渲染气泡提示
|
||||
UIManager.show_tutorial_hint(
|
||||
TutorialHintData.build(step), anchor, offset)
|
||||
|
||||
func _hide_hint() -> void:
|
||||
if _active_step == -1: return
|
||||
UIManager.hide_tutorial_hint()
|
||||
_active_step = -1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、上下文提示系统(Contextual Hints)
|
||||
|
||||
### 3.1 提示数据结构
|
||||
|
||||
```gdscript
|
||||
# resources/tutorial/tutorial_hint_data.gd
|
||||
class_name TutorialHintData
|
||||
extends Resource
|
||||
|
||||
@export var step: int # TutorialStep 枚举值
|
||||
@export var icon_key: String # 对应 UIAtlas 中的图标键(如 "wasd_icon")
|
||||
@export var text_key: String # i18n 键(tr(text_key) 使用)
|
||||
@export var arrow_dir: int # 0=上, 1=右, 2=下, 3=左(箭头指向目标)
|
||||
@export var duration: float = 0.0 # 0 = 手动关闭,>0 = 自动消失秒数
|
||||
|
||||
static func build(step: TutorialManager.TutorialStep) -> TutorialHintData:
|
||||
return _HINT_TABLE[step]
|
||||
|
||||
# 提示内容表(运行时常量,避免 JSON 热加载)
|
||||
const _HINT_TABLE: Dictionary = {
|
||||
0: preload("res://resources/tutorial/hint_move.tres"),
|
||||
1: preload("res://resources/tutorial/hint_cast.tres"),
|
||||
2: preload("res://resources/tutorial/hint_pick_spell.tres"),
|
||||
3: preload("res://resources/tutorial/hint_shop.tres"),
|
||||
4: preload("res://resources/tutorial/hint_equip_core.tres"),
|
||||
5: preload("res://resources/tutorial/hint_kill10.tres"),
|
||||
6: preload("res://resources/tutorial/hint_wave_cleared.tres"),
|
||||
7: preload("res://resources/tutorial/hint_resonance.tres"),
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 提示触发时机
|
||||
|
||||
| 步骤 | 触发时机 | 自动消失 | 跳过方式 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| MOVE | 游戏开始后 1s(玩家还未移动) | 首次移动后 | 任意移动 |
|
||||
| CAST | 法杖冷却恢复后 2s(玩家未施法) | 首次施法后 | 施法 |
|
||||
| PICK_SPELL | 首个法术 loot 落地后 3s | 拾取后 | 走到 loot 上 |
|
||||
| OPEN_SHOP | Wave 1 通关时自动进入商店流程 | 商店打开后 | 自动 |
|
||||
| EQUIP_CORE | 商店内有 Core 可购买时 | 购买后 | 购买 |
|
||||
| KILL_10 | 击杀计数达 5 时预显提示 | 计数达 10 | 击杀 |
|
||||
| WAVE_CLEARED | Wave 1 通关瞬间 | 5s 后 | 点击 |
|
||||
| RESONANCE_HINT | 首次编辑法杖时检测邻接 | 触发共鸣后 | 触发共鸣 |
|
||||
|
||||
### 3.3 提示 UI 节点结构
|
||||
|
||||
```
|
||||
CombatScene
|
||||
└── UILayer (CanvasLayer z_index=100)
|
||||
└── TutorialHintContainer # UIManager 管理的专用节点
|
||||
├── HintBubble (Panel)
|
||||
│ ├── Icon (TextureRect)
|
||||
│ └── Label (text = tr(hint.text_key))
|
||||
└── Arrow (TextureRect, 根据 arrow_dir 旋转)
|
||||
```
|
||||
|
||||
- `TutorialHintContainer` 随 `anchor` 节点世界坐标定位(`force_update_transform` 每帧同步)。
|
||||
- 气泡使用 `AnimationPlayer` 播放 0.2s 淡入动画,消失时 0.15s 淡出。
|
||||
- 同一时刻最多显示 1 个提示(显示新提示时先隐藏旧提示)。
|
||||
|
||||
---
|
||||
|
||||
## 四、首 3 分钟玩家流(First 3 Minutes Flow)
|
||||
|
||||
```
|
||||
T=0s 游戏载入 → CombatScene 初始化
|
||||
T=1s [提示] WASD 移动(气泡锚定玩家位置右侧,arrow→左)
|
||||
T=~5s 玩家完成移动 → MOVE 标记完成 → 提示消失
|
||||
T=~8s 法杖冷却结束 → [提示] 观察法术已自动施发(施法为自动)
|
||||
T=~15s 首个敌人被击杀 → loot 掉落 → [提示] 拾取法术卡
|
||||
T=~30s 玩家拾取法术卡 → PICK_SPELL 标记完成
|
||||
T=~60s Wave 1 清场 → 自动进入商店
|
||||
T=商店 [提示] 尝试装备 Core(若商店有 Core,arrow→Core 格)
|
||||
T=~90s 购买/关闭商店 → Wave 2 开始
|
||||
T=~120s 已掌握基本循环,FTUE 完成度 ~70%
|
||||
T=Wave 4+ 触发共鸣时 → 最终提示显示
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、关键 EventID 扩展
|
||||
|
||||
> 以下 EventID 需追加到 `event_id.gd`(当前最大 ID = 17):
|
||||
|
||||
| ID | 常量名 | 负载 | 触发方 | 监听方 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| 18 | `TUTORIAL_STEP_COMPLETED` | `step: int` | TutorialManager | UIManager(显示解锁提示)|
|
||||
| 19 | `SHOP_OPENED` | — | ShopManager | TutorialManager |
|
||||
| 20 | `RESONANCE_TRIGGERED` | `recipe_id: String` | SpellEvaluator | TutorialManager, VFXManager |
|
||||
| 21 | `ACHIEVEMENT_UNLOCKED` | `achievement_id: String` | AchievementManager | Steam 插件层 |
|
||||
|
||||
> **注意**:ID 0 为 `CRASH_DETECTED`(CrashReporter 专用,已定义)。
|
||||
|
||||
---
|
||||
|
||||
## 六、可跳过 & 关闭机制
|
||||
|
||||
```gdscript
|
||||
# settings_manager.gd 新增项
|
||||
const KEY_TUTORIAL_DISABLED: String = "tutorial_disabled"
|
||||
|
||||
# TutorialManager._ready() 中检查
|
||||
func _ready() -> void:
|
||||
if ProfileManager.get_bool(SettingsManager.KEY_TUTORIAL_DISABLED, false):
|
||||
# 将所有步骤标记为已完成,彻底关闭引导
|
||||
for s in TutorialStep.values():
|
||||
_completed_steps.append(s)
|
||||
return
|
||||
# 正常初始化...
|
||||
```
|
||||
|
||||
设置菜单提供「关闭新手提示」开关,写入 `ProfileManager`(持久化跨 Run)。
|
||||
**重置入口**:`SettingsManager.reset_tutorial()` → 清空 `tutorial_completed` 并重置标志,
|
||||
下次启动重新触发完整 FTUE 流程(适用于练习账号 / QA 测试)。
|
||||
|
||||
---
|
||||
|
||||
## 七、文件清单
|
||||
|
||||
```
|
||||
scripts/autoloads/
|
||||
tutorial_manager.gd # 核心 Autoload(本文档 §2 实现)
|
||||
|
||||
resources/tutorial/
|
||||
hint_move.tres # TutorialHintData(MOVE 步骤)
|
||||
hint_cast.tres
|
||||
hint_pick_spell.tres
|
||||
hint_shop.tres
|
||||
hint_equip_core.tres
|
||||
hint_kill10.tres
|
||||
hint_wave_cleared.tres
|
||||
hint_resonance.tres
|
||||
|
||||
scenes/ui/
|
||||
tutorial_hint_bubble.tscn # HintBubble + Arrow 节点树
|
||||
```
|
||||
|
||||
**Autoload 注册顺序**(`project.godot`):
|
||||
|
||||
> `TutorialManager` 在 `EventBus` 和 `ProfileManager` 之后注册,
|
||||
> 确保 `_ready()` 时可正常读取持久化数据并订阅事件。
|
||||
|
||||
---
|
||||
|
||||
## 八、与其他系统的接口约定
|
||||
|
||||
| 系统 | 接口 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| `UIManager` | `show_tutorial_hint(data, anchor, offset)` / `hide_tutorial_hint()` | 渲染和动画委托给 UIManager |
|
||||
| `ProfileManager` | `get_int_array / set_int_array("tutorial_completed", ...)` | 跨 Run 持久化已完成步骤 |
|
||||
| `EventBus` | 订阅 ID 1/6/11/18~21 等 | 只读订阅,不 emit 游戏逻辑事件 |
|
||||
| `SpellEvaluator` | 无直接依赖 | 共鸣触发通过 `RESONANCE_TRIGGERED` 事件解耦 |
|
||||
| `SettingsManager` | `KEY_TUTORIAL_DISABLED` 常量 | 关闭引导的持久化开关 |
|
||||
@@ -0,0 +1,121 @@
|
||||
# 01 快速上手 (Quickstart)
|
||||
|
||||
## 1. 环境要求
|
||||
|
||||
| 工具 | 版本 | 下载 |
|
||||
|:---|:---|:---|
|
||||
| Godot Engine | **4.6 Mono**(带 .NET 支持) | godotengine.org |
|
||||
| .NET SDK | 8.0+ | dotnet.microsoft.com |
|
||||
| Git | 任意 | git-scm.com |
|
||||
|
||||
> **重要**:必须下载 **Mono/C# 版本** 的 Godot(文件名含 `mono`),否则 C# 文件无法编译。
|
||||
|
||||
---
|
||||
|
||||
## 2. 首次运行
|
||||
|
||||
```bash
|
||||
# 1. 克隆项目
|
||||
git clone <repo-url>
|
||||
|
||||
# 2. 用 Godot 4.6 Mono 打开 project.godot
|
||||
# 菜单: Project → Open / Import
|
||||
|
||||
# 3. 等待导入完成(首次约 10-30 秒)
|
||||
|
||||
# 4. 直接按 F5 运行,或菜单: Debug → Run Project
|
||||
```
|
||||
|
||||
运行后你会看到:战斗场景直接开始,玩家(青色方块)自动发弹,敌人(红色方块)向玩家靠近。
|
||||
|
||||
---
|
||||
|
||||
## 3. 主场景说明
|
||||
|
||||
唯一可玩主场景:`scenes/main/combat_s2.tscn`
|
||||
|
||||
```
|
||||
project.godot
|
||||
└── run/main_scene = "res://scenes/main/combat_s2.tscn"
|
||||
```
|
||||
|
||||
### 游戏循环
|
||||
|
||||
```
|
||||
战斗 (BATTLE)
|
||||
↓ 全部敌人/Boss 死亡
|
||||
波次结算 → 商店 (SHOP)
|
||||
↓ 关闭商店
|
||||
战斗 (下一波)
|
||||
↓ 玩家死亡
|
||||
结算屏 (GAME_OVER) → 再来一局
|
||||
```
|
||||
|
||||
### 商店内操作
|
||||
|
||||
| 按钮 | 功能 |
|
||||
|:---|:---|
|
||||
| 法术卡 | 购买并装入当前法杖 |
|
||||
| 法杖 ⟳ | 切换 Core(法杖类型) |
|
||||
| 🎒 背包 | 打开插槽编辑器,自由排布法术 |
|
||||
| 刷新 | 重新随机商店(花费金币) |
|
||||
| ⚙ 设置 | 难度 / 语言 / 音量 / 清除数据 |
|
||||
| 出发下一波 | 进入下一波战斗 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 调试工具
|
||||
|
||||
### 编辑器内测试
|
||||
|
||||
在任何场景运行时,可以通过 MCP 脚本执行器(godot-mcp 插件)向游戏注入 GDScript:
|
||||
|
||||
```gdscript
|
||||
# 示例:快速生成一个 Boss 进行测试
|
||||
EnemyManager.reset()
|
||||
WaveManager.start_wave(20) # 直接启动 W20 Boss 战
|
||||
```
|
||||
|
||||
### HUD 调试信息
|
||||
|
||||
运行时左上角 HUD 显示:
|
||||
- 当前波次 / HP / 金币 / XP / 击杀数
|
||||
- DPS / 蓄力百分比
|
||||
- `[Core名] [法术id列表]`(当前装备)
|
||||
- 状态实例数 / VFX 数
|
||||
|
||||
### 快速切波次
|
||||
|
||||
```gdscript
|
||||
# 在游戏内执行(须在商店状态下)
|
||||
# 方法:打开商店后跳到指定波次再出发
|
||||
WaveManager.current_wave = 14 # 设置当前波次基准
|
||||
# 然后在商店中点「出发下一波」,即进入 W15 精英战
|
||||
# 或直接重置重开:get_tree().current_scene._cm.start_game()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 项目配置文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|:---|:---|
|
||||
| `project.godot` | 引擎配置、Autoload 列表、主场景 |
|
||||
| `user://save_data.json` | 设置(难度/语言/音量),运行时生成 |
|
||||
| `user://run_a.json` `run_b.json` | 局内 A/B 双槽存档 |
|
||||
| `user://endless_records.json` | 本地排行榜(HMAC 签名) |
|
||||
|
||||
> `user://` 路径在 Windows 上位于 `%APPDATA%\Godot\app_userdata\Rogue\`
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见问题
|
||||
|
||||
**Q: F5 运行后没有画面(黑屏)**
|
||||
A: 确认下载的是 **Mono 版**;等待 .NET 首次编译完成(约 30s)。
|
||||
|
||||
**Q: 敌人/子弹消失了但没有波次结算**
|
||||
A: 可能生命时间到期。重启当前波次:`WaveManager.start_wave(WaveManager.current_wave)`
|
||||
|
||||
**Q: 更改了 `wand_preset.gd` 的法术数值,但游戏里没有变化**
|
||||
A: `SpellRegistry` 在 `_ready()` 时缓存法术对象;需要重启游戏(F5 重新运行)使改动生效。
|
||||
@@ -0,0 +1,248 @@
|
||||
# 02 内容创作指南 (Content Authoring)
|
||||
|
||||
> 策划和程序共同维护的内容文件。所有数值修改无需改架构代码,只改指定位置。
|
||||
|
||||
> ⭐ **策划/美术优先用可视化工具**:[07 游戏设计器](07_game_designer.md) 提供编辑器内一站式面板(法术/敌人/波次/Core/平衡),表单填数值即可,**无需写代码**。本文的代码方式适合需要复杂自定义逻辑的程序员,或了解底层数据结构。
|
||||
|
||||
---
|
||||
|
||||
## A. 添加/修改法术
|
||||
|
||||
> 💡 **策划优先用可视化工具**:[06 法术编辑器](06_spell_editor.md) 提供编辑器内表单,无需写代码即可新增/编辑法术(保存到 `data/spells.json`)。下面的代码方式适合需要复杂逻辑的程序员。
|
||||
|
||||
### 法术定义位置
|
||||
|
||||
- **数据驱动(推荐)**:`res://data/spells.json`(由法术编辑器维护,启动时由 `SpellRegistry` 加载,优先级最高)
|
||||
- **硬编码(程序)**:`scripts/autoloads/wand_preset.gd`(向后兼容回退)
|
||||
|
||||
### 步骤:添加一个新 ACTION 法术
|
||||
|
||||
**1. 在 `wand_preset.gd` 添加工厂函数**
|
||||
|
||||
```gdscript
|
||||
func make_frost_bolt() -> SpellNode:
|
||||
var n := SpellNode.new()
|
||||
n.id = "action_frost_bolt" # 全局唯一 ID
|
||||
n.type = SpellNode.SpellType.ACTION
|
||||
n.display_name = "SPELL_FROST_BOLT_NAME" # tr() 键(见本地化指南)
|
||||
n.description = "SPELL_FROST_BOLT_DESC"
|
||||
n.element_tags = ["tag:ice"] # 共鸣系统用的元素标签
|
||||
n.meta = {
|
||||
"base_damage": 5.0, # 基础伤害
|
||||
"speed": 380.0, # 弹速 (px/s)
|
||||
"lifetime": 4.0, # 存活时间 (s)
|
||||
"radius": 7.0, # 弹体半径 (px)
|
||||
"damage_type": 0, # 0=通用 (DamageType.NORMAL)
|
||||
"apply_status_id": StatusID.FREEZE, # 命中施加状态(可选)
|
||||
"shop_cost": 22, # 商店价格
|
||||
}
|
||||
return n
|
||||
```
|
||||
|
||||
**2. 注册到 `make_spell_by_id` 和 `get_all_spell_ids`**
|
||||
|
||||
```gdscript
|
||||
func make_spell_by_id(spell_id: String) -> SpellNode:
|
||||
match spell_id:
|
||||
# ... 已有法术 ...
|
||||
"action_frost_bolt": return make_frost_bolt() # 新增
|
||||
_: push_warning(...)
|
||||
|
||||
func get_all_spell_ids() -> Array:
|
||||
return [
|
||||
# ... 已有 ...
|
||||
"action_frost_bolt", # 新增到列表末尾(商店会自动抽取)
|
||||
]
|
||||
```
|
||||
|
||||
**3. 在 `translations/*.po` 四个文件中添加翻译键**(见本地化指南)
|
||||
|
||||
---
|
||||
|
||||
### 支持的法术类型
|
||||
|
||||
| `SpellNode.SpellType` | 说明 | meta 必填键 |
|
||||
|:---|:---|:---|
|
||||
| `ACTION` | 发射弹道 | `base_damage`, `speed`, `lifetime`, `radius` |
|
||||
| `ACTION`(zone) | 地面效果 | `action_kind:"zone"`, `radius`, `status_id`, `duration`, `tick_interval` |
|
||||
| `ACTION`(summon) | 召唤物 | `action_kind:"summon"`, `lifetime`, `range`, `fire_interval`, `base_damage` |
|
||||
| `MODIFIER` | 修改施法参数 | 至少一个:`damage_add`, `damage_mult`, `spread_add`, `speed_mult`, `multicast` |
|
||||
| `TRIGGER` | 命中触发子弹 | `base_damage`, `speed`, `lifetime`, `radius`(触发物自身弹道参数) |
|
||||
| `LOGIC` | 条件/循环 | `logic_op`(见下表)|
|
||||
|
||||
### LOGIC meta 参数
|
||||
|
||||
| `logic_op` | 额外 meta | 效果 |
|
||||
|:---|:---|:---|
|
||||
| `"every_n_shots"` | `n:int`, `reg:0-3` | 每 N 次施法触发 |
|
||||
| `"if_hp_below"` | `threshold:float(0-1)` | HP < 阈值时触发 |
|
||||
| `"if_enemy_nearby"` | `range:float` | 范围内有敌时触发 |
|
||||
| `"loop"` | `count:int(1-16)` | 后续法术重复 N 次 |
|
||||
|
||||
---
|
||||
|
||||
## B. 修改波次配置
|
||||
|
||||
### 波次配置位置
|
||||
|
||||
`scripts/autoloads/wave_manager.gd` → `WAVE_CONFIG: Array`(数组索引 0 = Wave 1)
|
||||
|
||||
### 配置字段说明
|
||||
|
||||
```gdscript
|
||||
{"count": 20, "hp": 15.0, "type": 0, "elite": 0, "gold": 10, "xp": 20, "boss": -1}
|
||||
# count : 本波生成的杂兵数量
|
||||
# hp : 杂兵基础血量(受难度乘子影响)
|
||||
# type : 敌人原型(见下方原型表)
|
||||
# elite : W15+ 带 NavigationAgent2D 寻路的精英数量
|
||||
# gold : 波次结算奖励金币
|
||||
# xp : 波次结算奖励 XP
|
||||
# boss : -1=无;4=Mini Boss;5=Final Boss
|
||||
```
|
||||
|
||||
### 敌人原型(`EnemyManager.Type`)
|
||||
|
||||
| 值 | 名称 | 速度 | 护甲 | 颜色 | 大小 |
|
||||
|:---|:---|:---|:---|:---|:---|
|
||||
| 0 | BASIC | 80 | 0 | 红 | 14px |
|
||||
| 1 | FAST | 140 | 0 | 黄 | 11px |
|
||||
| 2 | ARMORED | 60 | 5 | 灰 | 16px |
|
||||
| 3 | ELITE | 90 | 3 | 橙 | 22px |
|
||||
| 4 | MINIBOSS | 55 | 8 | 品红 | 48px |
|
||||
| 5 | BOSS | 45 | 14 | 深红 | 76px |
|
||||
|
||||
### 示例:修改 W8 Mini Boss 血量
|
||||
|
||||
```gdscript
|
||||
# wave_manager.gd line ~50
|
||||
{"count": 12, "hp": 30.0, "type": 0, "elite": 0, "gold": 80, "xp": 120, "boss": 4},
|
||||
# ↑ boss:4 → _BOSS_HP[4] = 450.0 控制 Boss 血量
|
||||
|
||||
# 修改 Boss 血量,找到下面这行:
|
||||
const _BOSS_HP: Dictionary = {4: 450.0, 5: 1800.0}
|
||||
# 改成你想要的数值
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## C. 修改敌人原型数值
|
||||
|
||||
`scripts/autoloads/enemy_manager.gd` 顶部常量:
|
||||
|
||||
```gdscript
|
||||
const _SPEED: Dictionary = {0: 80.0, 1: 140.0, 2: 60.0, 3: 90.0, 4: 55.0, 5: 45.0}
|
||||
const _ARMOR: Dictionary = {0: 0.0, 1: 0.0, 2: 5.0, 3: 3.0, 4: 8.0, 5: 14.0}
|
||||
```
|
||||
|
||||
直接修改对应值即可,无需改其他代码。
|
||||
|
||||
---
|
||||
|
||||
## D. 修改难度乘子
|
||||
|
||||
`scripts/autoloads/settings_manager.gd`:
|
||||
|
||||
```gdscript
|
||||
func enemy_hp_mult() -> float: return [0.7, 1.0, 1.0][difficulty]
|
||||
func player_dmg_taken_mult()-> float: return [0.7, 1.0, 1.0][difficulty]
|
||||
func wave_count_mult() -> float: return [1.0, 1.0, 1.2][difficulty]
|
||||
func boss_hp_mult() -> float: return [1.0, 1.0, 1.3][difficulty]
|
||||
# 数组格式:[初学者, 标准, 挑战]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E. 添加共鸣配方
|
||||
|
||||
`scripts/domain/spell_system/spell_evaluator.gd` → `_ready()` → `_resonance_recipes`:
|
||||
|
||||
```gdscript
|
||||
_resonance_recipes = [
|
||||
{
|
||||
"id": "plasma_storm", # 配方 ID
|
||||
"pattern": ["tag:water", "tag:lightning"], # 元素标签组合
|
||||
"match": "adjacent", # "adjacent"=相邻;"anywhere_in_deck"=任意位置
|
||||
"result_spell_id":"action_plasma_storm", # 产物法术 id(须在 WandPreset 中注册)
|
||||
"consume_inputs": true, # true=消耗原始法术插槽
|
||||
},
|
||||
# 新增配方直接追加到这里
|
||||
{
|
||||
"id": "ice_nova",
|
||||
"pattern": ["tag:ice", "tag:water"],
|
||||
"match": "adjacent",
|
||||
"result_spell_id":"action_ice_nova",
|
||||
"consume_inputs": false, # false=原法术保留,额外注入产物
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
> **注意**:产物法术必须同时在 `WandPreset.make_spell_by_id()` 中注册,才能被 `SpellRegistry` 识别。
|
||||
|
||||
---
|
||||
|
||||
## F. 添加/修改状态效果
|
||||
|
||||
### 现有状态 ID(`scripts/autoloads/status_id.gd`)
|
||||
|
||||
```gdscript
|
||||
const BURN = 1 # 燃烧 DoT(火系)
|
||||
const FREEZE = 2 # 冻结(硬控)
|
||||
const POISON = 3 # 中毒(可叠层 DoT)
|
||||
const WET = 4 # 潮湿
|
||||
const OILY = 5 # 油腻
|
||||
const STUN = 6 # 麻痹
|
||||
const COMBO_MARK = 7
|
||||
const VULNERABILITY = 8
|
||||
# 新增从 9 开始递增
|
||||
```
|
||||
|
||||
### 状态数值定义(`.tres` 资源,目前占位代码生成)
|
||||
|
||||
当前 `StatusRegistry` 在 `_ready()` 扫描 `res://resources/status_types/` 目录。若不存在资源文件,会尝试从 `WandPreset` 或内置回退。添加新状态的完整流程:
|
||||
|
||||
1. 在 `status_id.gd` 追加常量(从 9 起)
|
||||
2. 在 `StatusTypeDef` 对象中配置 tick_interval / stack_mode / max_stacks
|
||||
3. 在状态数值生效的 `can_catalyze` 字段填写对应状态 ID 列表
|
||||
4. 在四个 `.po` 文件中添加 `STATUS_新状态_NAME` 等翻译键
|
||||
|
||||
---
|
||||
|
||||
## G. 添加 Core(法杖类型)
|
||||
|
||||
```gdscript
|
||||
# wand_preset.gd 中添加:
|
||||
func make_staff_long() -> CoreDefinition:
|
||||
var c := CoreDefinition.new()
|
||||
c.id = "staff_long"
|
||||
c.display_name = "CORE_STAFF_LONG_NAME" # tr() 键
|
||||
c.slot_count = 10 # 插槽数
|
||||
c.topology = CoreDefinition.Topology.LINEAR
|
||||
c.cpu_limit = 10
|
||||
c.cast_interval = 0.7
|
||||
return c
|
||||
```
|
||||
|
||||
然后在 `CombatManager._CORE_ROSTER` 中追加 `"staff_long"`,以及在 `_make_core_by_id()` 和 `_default_deck_for()` 中添加对应分支。
|
||||
|
||||
---
|
||||
|
||||
## H. 调试技巧
|
||||
|
||||
### 在运行中检查任意系统状态
|
||||
|
||||
利用 MCP 游戏脚本执行器(或 Godot 调试器表达式):
|
||||
|
||||
```gdscript
|
||||
# 查看当前法术 VM 编译结果
|
||||
var ids = []
|
||||
for nd in SpellEvaluator._equipped_compiled.nodes: ids.append(nd.id)
|
||||
print(ids)
|
||||
|
||||
# 立即杀死所有敌人(测试波次结算)
|
||||
for i in range(EnemyManager.get_active_count()-1, -1, -1):
|
||||
var eid = EnemyManager._slot_entity_map.get(i, -1)
|
||||
if eid >= 0: EnemyManager.apply_damage(eid, 99999.0, -1, false)
|
||||
|
||||
# 给玩家大量金币
|
||||
PlayerStats.gain_gold(9999)
|
||||
```
|
||||
@@ -0,0 +1,281 @@
|
||||
# 03 资源替换指南 (Asset Replacement Guide)
|
||||
|
||||
> 当前项目使用**程序化占位**(几何体 + 蜂鸣音),所有接口已就绪。本文说明如何无缝替换为正式美术资源,**不需要修改任何业务逻辑代码**。
|
||||
|
||||
---
|
||||
|
||||
## A. 音效替换
|
||||
|
||||
### 当前状态
|
||||
|
||||
`AudioManager` 在 `res://audio/sfx/<id>.ogg`(或 `.wav` / `.mp3`)**不存在**时自动回退到程序化蜂鸣。文件存在即优先加载真实音效。
|
||||
|
||||
### 替换步骤
|
||||
|
||||
**1. 创建目录并放入文件**
|
||||
|
||||
```
|
||||
res://audio/sfx/
|
||||
hit.ogg ← 子弹命中音效
|
||||
kill.ogg ← 敌人死亡
|
||||
hurt.ogg ← 玩家受击
|
||||
wave_complete.ogg ← 波次结算
|
||||
boss.ogg ← Boss 入场
|
||||
shop.ogg ← 商店开启
|
||||
buy.ogg ← 购买法术
|
||||
level_up.ogg ← 升级
|
||||
```
|
||||
|
||||
**2. 确认格式要求**
|
||||
|
||||
| 属性 | 推荐值 |
|
||||
|:---|:---|
|
||||
| 格式 | OGG Vorbis(体积小)/ WAV PCM(低延迟) |
|
||||
| 采样率 | 44100 Hz 或 22050 Hz |
|
||||
| 声道 | 单声道(Mono)即可,立体声也支持 |
|
||||
| 时长 | 音效 < 3s;BGM 循环 |
|
||||
| 响度 | 建议 -14 LUFS 标准化后导入 |
|
||||
|
||||
**3. Godot 导入设置**(在编辑器 FileSystem 面板右键文件 → Import)
|
||||
|
||||
| 设置 | 推荐值 |
|
||||
|:---|:---|
|
||||
| Loop | 音效:`Off`;BGM:`On` |
|
||||
| Compression Mode | `quality` OGG / `PCM` WAV |
|
||||
| Max Texture Size | 不适用(音频) |
|
||||
|
||||
**4. 添加新音效 ID**
|
||||
|
||||
在 `AudioManager._SFX_DEFS` 中追加一行(定义占位蜂鸣,有真实文件时忽略):
|
||||
|
||||
```gdscript
|
||||
# audio_manager.gd
|
||||
const _SFX_DEFS: Dictionary = {
|
||||
# ... 已有 ...
|
||||
"spell_cast": [600.0, 80.0], # 新增:频率 600Hz,80ms
|
||||
}
|
||||
```
|
||||
|
||||
然后在需要播放的地方调用:
|
||||
|
||||
```gdscript
|
||||
AudioManager.play("spell_cast", spawn_pos)
|
||||
```
|
||||
|
||||
### BGM 系统(预留接口)
|
||||
|
||||
当前无 BGM 系统,添加时建议在 `AudioManager` 中扩展:
|
||||
|
||||
```gdscript
|
||||
# 建议扩展方向(未实现,供参考)
|
||||
func play_bgm(track_id: String, fade_sec: float = 1.0) -> void:
|
||||
var path = "res://audio/bgm/%s.ogg" % track_id
|
||||
# 跨淡 + loop...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## B. 敌人 / 玩家 精灵替换
|
||||
|
||||
### 当前状态
|
||||
|
||||
- **玩家**:`combat_s2.gd` 中程序化 `Polygon2D`(青色方块)
|
||||
- **敌人**:`EnemyManager.sync_multimesh()` 驱动 `MultiMeshInstance2D`,使用单位 `QuadMesh` + 颜色覆盖
|
||||
|
||||
### 替换玩家精灵
|
||||
|
||||
`combat_s2.gd` → `_setup_world_view()`,找到玩家标记部分替换:
|
||||
|
||||
```gdscript
|
||||
# 当前(几何占位)
|
||||
var marker := Polygon2D.new()
|
||||
marker.polygon = PackedVector2Array([...])
|
||||
marker.color = Color(0.35, 0.9, 1.0)
|
||||
_player_node.add_child(marker)
|
||||
|
||||
# 替换为精灵(修改为)
|
||||
var marker := Sprite2D.new()
|
||||
marker.texture = load("res://assets/sprites/player.png")
|
||||
marker.scale = Vector2(0.5, 0.5) # 按实际尺寸调整
|
||||
_player_node.add_child(marker)
|
||||
```
|
||||
|
||||
### 替换敌人渲染(MultiMesh + Atlas)
|
||||
|
||||
敌人使用 `MultiMeshInstance2D`,单次 Draw Call 渲染所有实例。替换步骤:
|
||||
|
||||
**步骤 1:准备 Atlas 贴图**
|
||||
|
||||
将所有敌人原型排列在一张贴图上(建议 512×512):
|
||||
|
||||
```
|
||||
atlas_enemies.png
|
||||
┌────┬────┬────┐
|
||||
│BASIC│FAST│ARM.│ 每格 64×64 px
|
||||
├────┼────┼────┤
|
||||
│ELITE│MIN.│BOSS│
|
||||
└────┴────┴────┘
|
||||
```
|
||||
|
||||
**步骤 2:修改 MultiMesh 使用 Atlas**
|
||||
|
||||
`combat_s2.gd` → `_setup_world_view()` 中的敌人 MMI 部分:
|
||||
|
||||
```gdscript
|
||||
# 原:使用 QuadMesh 纯色
|
||||
var quad := QuadMesh.new()
|
||||
quad.size = Vector2(1.0, 1.0)
|
||||
_enemy_mm.mesh = quad
|
||||
|
||||
# 替换:使用 PlaneMesh + Atlas 材质
|
||||
var mesh := PlaneMesh.new()
|
||||
mesh.size = Vector2(1.0, 1.0)
|
||||
var mat := CanvasItemMaterial.new()
|
||||
# 或使用 ShaderMaterial + 自定义 Shader 实现 UV 偏移(按 enemy_type 选择 Atlas 区域)
|
||||
_enemy_mm.mesh = mesh
|
||||
var mmi := MultiMeshInstance2D.new()
|
||||
mmi.multimesh = _enemy_mm
|
||||
mmi.texture = load("res://assets/sprites/atlas_enemies.png")
|
||||
add_child(mmi)
|
||||
```
|
||||
|
||||
**步骤 3(进阶):Shader 按 instance_custom_data 选 UV**
|
||||
|
||||
```glsl
|
||||
// res://shaders/enemy_atlas.gdshader
|
||||
shader_type canvas_item;
|
||||
uniform sampler2D atlas_tex;
|
||||
uniform int cols = 3; // atlas 列数
|
||||
|
||||
void fragment() {
|
||||
// INSTANCE_CUSTOM.x 存储 enemy_type (0-5)
|
||||
float etype = INSTANCE_CUSTOM.x;
|
||||
float row = floor(etype / float(cols));
|
||||
float col = mod(etype, float(cols));
|
||||
vec2 uv = (UV + vec2(col, row)) / vec2(float(cols), 2.0);
|
||||
COLOR = texture(atlas_tex, uv) * COLOR;
|
||||
}
|
||||
```
|
||||
|
||||
在 `EnemyManager.sync_multimesh()` 中写入 custom data:
|
||||
|
||||
```gdscript
|
||||
# enemy_manager.gd sync_multimesh() 内,在 set_instance_transform_2d 之后
|
||||
mm.set_instance_custom_data(i, Color(float(etype) / 255.0, 0, 0, 0))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## C. 子弹精灵替换
|
||||
|
||||
子弹同样使用 `MultiMeshInstance2D`(`_bullet_mm`)。替换为 Atlas 方式与敌人相同,但子弹通常使用 `damage_type` 区分外观。
|
||||
|
||||
`BulletManager.sync_multimesh()` 中写入 custom data(damage_type):
|
||||
|
||||
```gdscript
|
||||
# bullet_manager.gd sync_multimesh() 内追加
|
||||
mm.set_instance_custom_data(i, Color(float(_data[base + 8]) / 255.0, 0, 0, 0))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## D. VFX 特效替换
|
||||
|
||||
### 当前状态
|
||||
|
||||
`VFXManager` 使用程序化彩色矩形作为特效占位(`ColorRect`/`Sprite2D`)。
|
||||
|
||||
### 替换为粒子特效
|
||||
|
||||
`scripts/autoloads/vfx_manager.gd` → `_instantiate_vfx()` 中修改 `_vfx_scenes` 映射:
|
||||
|
||||
**步骤 1:制作粒子场景**
|
||||
|
||||
为每个特效 ID 建一个场景文件(`GPUParticles2D` 为根节点,`one_shot = true`):
|
||||
|
||||
```
|
||||
res://scenes/vfx/
|
||||
hit_spark.tscn ← 命中火花
|
||||
status_burn.tscn ← 燃烧特效
|
||||
status_poison.tscn ← 中毒特效
|
||||
cast_flash.tscn ← 施法闪光
|
||||
death_burst.tscn ← 敌人死亡
|
||||
zone_expire.tscn ← 地面效果消散
|
||||
```
|
||||
|
||||
每个场景设置:
|
||||
- `GPUParticles2D.one_shot = true`(**必须**,P6-N72)
|
||||
- `GPUParticles2D.emitting = false`(代码控制时机)
|
||||
- `lifetime` = 特效持续时长
|
||||
|
||||
**步骤 2:修改 `_vfx_scenes` 预加载表**
|
||||
|
||||
```gdscript
|
||||
# vfx_manager.gd
|
||||
func _ready() -> void:
|
||||
_vfx_scenes = {
|
||||
"hit_spark": preload("res://scenes/vfx/hit_spark.tscn"),
|
||||
"status_burn": preload("res://scenes/vfx/status_burn.tscn"),
|
||||
# ... 其他 ...
|
||||
}
|
||||
```
|
||||
|
||||
**步骤 3:让 VFX 播放时自动启动粒子**
|
||||
|
||||
> **重要**:`one_shot = true` 和 `emitting = false` 须在**场景文件内**就设好(Godot 编辑器中设置属性),这是 P6-N72 强制要求。运行时 `VFXManager.play()` 只负责把节点拖出池并触发 `emitting = true`。
|
||||
|
||||
```gdscript
|
||||
# vfx_manager.gd → play() 内,instance 创建后追加:
|
||||
if node is GPUParticles2D:
|
||||
node.emitting = true # 启动粒子(one_shot 已在场景中设置,这里不必覆盖)
|
||||
```
|
||||
|
||||
### 特效 ID 参考
|
||||
|
||||
| ID | 触发时机 |
|
||||
|:---|:---|
|
||||
| `"hit_spark"` | 子弹命中敌人 |
|
||||
| `"status_burn"` | 施加 BURN 状态 |
|
||||
| `"status_poison"` | 施加 POISON 状态 |
|
||||
| `"cast_flash"` | 施法时玩家周围 |
|
||||
| `"death_burst"` | 敌人死亡 |
|
||||
| `"zone_expire"` | 地面效果消失 |
|
||||
|
||||
---
|
||||
|
||||
## E. UI 界面图片替换
|
||||
|
||||
当前 UI 全为程序化构建(`ColorRect` + `Label` + `Button`),无独立 `.tscn` 场景。替换为美术 UI 的推荐路径:
|
||||
|
||||
**选项 A:保留程序化,只替换 Theme**
|
||||
|
||||
在 `combat_s2.gd` 的各 `_setup_*` 函数中,为 `Button`/`Label` 应用自定义 Theme:
|
||||
|
||||
```gdscript
|
||||
var theme := load("res://assets/ui/main_theme.tres") # 你制作的 Theme 资源
|
||||
btn.theme = theme
|
||||
```
|
||||
|
||||
**选项 B:迁移到独立 .tscn 场景**(推荐,长期)
|
||||
|
||||
1. 为商店、背包、结算屏分别建 `.tscn`
|
||||
2. 在 `_setup_shop_ui()` 等函数中替换为 `load("res://scenes/ui/shop.tscn").instantiate()`
|
||||
3. 通过信号 / Callable 将原有事件(买法术、刷新等)连接到新场景节点
|
||||
|
||||
> **注意**:迁移 UI 时须保留对 `_shop_btns`、`_reroll_btn` 等引用的赋值,否则商店逻辑失效。
|
||||
|
||||
---
|
||||
|
||||
## F. 资源路径约定
|
||||
|
||||
| 类型 | 约定路径 |
|
||||
|:---|:---|
|
||||
| 音效 | `res://audio/sfx/<id>.ogg` |
|
||||
| BGM | `res://audio/bgm/<track>.ogg` |
|
||||
| 玩家精灵 | `res://assets/sprites/player.png` |
|
||||
| 敌人 Atlas | `res://assets/sprites/atlas_enemies.png` |
|
||||
| VFX 场景 | `res://scenes/vfx/<id>.tscn` |
|
||||
| UI Theme | `res://assets/ui/main_theme.tres` |
|
||||
| UI 场景 | `res://scenes/ui/<name>.tscn` |
|
||||
| 字体 | `res://assets/fonts/<name>.ttf` |
|
||||
</content>
|
||||
@@ -0,0 +1,197 @@
|
||||
# 04 本地化工作流 (Localization Workflow)
|
||||
|
||||
> 项目遵循 **ADR-A1**:所有玩家可见字符串必须通过 `tr("KEY")` 包裹,禁止业务代码中出现裸字符串。
|
||||
|
||||
---
|
||||
|
||||
## 1. 文件位置
|
||||
|
||||
```
|
||||
translations/
|
||||
zh_CN.po ← 简体中文(权威主档)
|
||||
zh_TW.po ← 繁体中文
|
||||
en.po ← 英文
|
||||
ja.po ← 日文
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. `.po` 文件格式
|
||||
|
||||
每条翻译的格式为:
|
||||
|
||||
```po
|
||||
msgid "UI_WAVE"
|
||||
msgstr "第 %d 波"
|
||||
```
|
||||
|
||||
- `msgid`:代码中 `tr("KEY")` 使用的键
|
||||
- `msgstr`:该语言显示的文字
|
||||
- `%d`、`%s`、`%.1f` 等格式占位符必须与代码中 `tr("KEY") % value` 完全对应
|
||||
|
||||
---
|
||||
|
||||
## 3. 添加新翻译字符串(标准流程)
|
||||
|
||||
### Step 1 — 在代码中使用 tr() 键
|
||||
|
||||
```gdscript
|
||||
# ❌ 错误:裸字符串
|
||||
label.text = "商店已关闭"
|
||||
|
||||
# ✅ 正确:通过键引用
|
||||
label.text = tr("SHOP_CLOSED_MSG")
|
||||
|
||||
# 带参数
|
||||
label.text = tr("UI_WAVE") % current_wave # → "第 3 波"
|
||||
label.text = tr("UI_KILLS") % [kills, alive] # → "击杀: 7 存活: 3"
|
||||
```
|
||||
|
||||
### Step 2 — 在全部四个 `.po` 文件中添加对应条目
|
||||
|
||||
**zh_CN.po**(权威档,写游戏实际中文):
|
||||
```po
|
||||
msgid "SHOP_CLOSED_MSG"
|
||||
msgstr "商店已关闭"
|
||||
```
|
||||
|
||||
**zh_TW.po**(繁体,改繁体字):
|
||||
```po
|
||||
msgid "SHOP_CLOSED_MSG"
|
||||
msgstr "商店已關閉"
|
||||
```
|
||||
|
||||
**en.po**(英文):
|
||||
```po
|
||||
msgid "SHOP_CLOSED_MSG"
|
||||
msgstr "Shop Closed"
|
||||
```
|
||||
|
||||
**ja.po**(日文):
|
||||
```po
|
||||
msgid "SHOP_CLOSED_MSG"
|
||||
msgstr "ショップが閉店しました"
|
||||
```
|
||||
|
||||
### Step 3 — 验证(运行时检查)
|
||||
|
||||
在游戏内执行脚本验证无缺键:
|
||||
|
||||
```gdscript
|
||||
var keys = ["SHOP_CLOSED_MSG"] # 你新加的键
|
||||
for loc in ["zh_CN","zh_TW","en","ja"]:
|
||||
Locale.set_locale(loc)
|
||||
for k in keys:
|
||||
if tr(k) == k:
|
||||
print("缺键!语言: %s 键: %s" % [loc, k])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 键名规范
|
||||
|
||||
**格式**:`<模块>_<实体>_<含义>`,全大写 + 下划线
|
||||
|
||||
| 前缀 | 用途 | 示例 |
|
||||
|:---|:---|:---|
|
||||
| `UI_` | 通用 HUD 标签 | `UI_WAVE`, `UI_HP` |
|
||||
| `SHOP_` | 商店界面 | `SHOP_TITLE`, `SHOP_REROLL` |
|
||||
| `INV_` | 背包界面 | `INV_TITLE`, `INV_BENCH` |
|
||||
| `SETTLE_` | 结算屏 | `SETTLE_TITLE`, `SETTLE_STATS` |
|
||||
| `SETTINGS_` | 设置面板 | `SETTINGS_DIFFICULTY` |
|
||||
| `STATUS_` | 状态消息 | `STATUS_PURCHASED` |
|
||||
| `DIFF_` | 难度名称 | `DIFF_BEGINNER`, `DIFF_CHALLENGE` |
|
||||
| `SPELL_` | 法术名/描述 | `SPELL_SPARK_BOLT_NAME`, `SPELL_SPARK_BOLT_DESC` |
|
||||
| `CORE_` | Core 名/描述 | `CORE_WAND_BASIC_NAME` |
|
||||
| `STATUS_EFFECT_` | 状态效果名 | `STATUS_EFFECT_BURN_NAME` |
|
||||
| `BOSS_` | Boss 名称 | `BOSS_FINAL_NAME` |
|
||||
|
||||
---
|
||||
|
||||
## 5. 法术 / Core 名称本地化(待完成)
|
||||
|
||||
当前法术 `display_name` 字段存储的是裸英/中文字符串(如 `"Spark Bolt"`)。正式发布前需键化。
|
||||
> **注意**:修改后须同步更新商店按钮渲染(`combat_s2.gd → _refresh_shop_ui`),将 `spell.display_name` 改为 `tr(spell.display_name)`。
|
||||
|
||||
**Step 1 — 修改 `wand_preset.gd`**
|
||||
|
||||
```gdscript
|
||||
# 当前(裸字符串)
|
||||
n.display_name = "Spark Bolt"
|
||||
n.description = "发射一颗期限 4s 的电光飞弹,造成 3 伤害。"
|
||||
|
||||
# 修改为(键)
|
||||
n.display_name = "SPELL_SPARK_BOLT_NAME"
|
||||
n.description = "SPELL_SPARK_BOLT_DESC"
|
||||
```
|
||||
|
||||
**Step 2 — 在四语 `.po` 中添加**
|
||||
|
||||
```po
|
||||
# zh_CN.po
|
||||
msgid "SPELL_SPARK_BOLT_NAME"
|
||||
msgstr "电花弹"
|
||||
|
||||
msgid "SPELL_SPARK_BOLT_DESC"
|
||||
msgstr "发射一颗快速电花飞弹,造成 3 点闪电伤害。"
|
||||
```
|
||||
|
||||
**Step 3 — 显示时包裹 tr()**
|
||||
|
||||
```gdscript
|
||||
# UI 显示法术名时
|
||||
btn.text = tr(spell.display_name)
|
||||
# 法术描述
|
||||
desc_label.text = tr(spell.description)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 添加新语言
|
||||
|
||||
**Step 1 — 创建新 `.po` 文件**(以韩语为例)
|
||||
|
||||
```
|
||||
translations/ko.po
|
||||
```
|
||||
|
||||
内容:复制 `en.po`,将 `Language: en` 改为 `Language: ko`,翻译所有 `msgstr`。
|
||||
|
||||
**Step 2 — 在 `locale_manager.gd` 中注册**
|
||||
|
||||
```gdscript
|
||||
const LOCALES: Array = ["zh_CN", "zh_TW", "en", "ja", "ko"] # 追加 "ko"
|
||||
```
|
||||
|
||||
新语言将自动在商店语言切换按钮中循环出现。
|
||||
|
||||
---
|
||||
|
||||
## 7. 在游戏内切换语言(测试用)
|
||||
|
||||
```gdscript
|
||||
# 切换到英文
|
||||
Locale.set_locale("en")
|
||||
|
||||
# 循环切换(等同商店 🌐 按钮)
|
||||
Locale.cycle_locale()
|
||||
|
||||
# 查看当前语言
|
||||
print(Locale.get_locale())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 提交规范
|
||||
|
||||
每次提交涉及玩家可见字符串的改动时,须**同时更新四个 `.po` 文件**。允许以英文或空字符串占位(`msgstr ""`),但 PR 审查时须标注"待翻译"。
|
||||
|
||||
---
|
||||
|
||||
## 9. 工具推荐
|
||||
|
||||
| 工具 | 用途 |
|
||||
|:---|:---|
|
||||
| [Poedit](https://poedit.net) | `.po` 文件 GUI 编辑器,支持模糊匹配和自动翻译接口 |
|
||||
| VSCode + gettext 插件 | 轻量化文本编辑 |
|
||||
| Godot 内置本地化 | `Project → Project Settings → Localization → Translations` 可预览各语言 |
|
||||
@@ -0,0 +1,170 @@
|
||||
# 05 发布标准清单 (Release Guide)
|
||||
|
||||
> 正式发布前所有 **P0 必须项** 须全部打勾。参考:`docs/plan/certification_checklist.md`(权威完整版)。
|
||||
|
||||
---
|
||||
|
||||
## 一、P0 技术验收(S6 P0,均已通过)
|
||||
|
||||
| 编号 | 项目 | 状态 | 说明 |
|
||||
|:---|:---|:---:|:---|
|
||||
| P-S6-01 | Wave 1~20 无崩溃通关;W8/W20 Boss 正常 | ✅ | 系统已实现并实测 |
|
||||
| P-S6-02 | 500 敌人 + 1500 子弹稳定 60fps | ✅ | 实测 180fps / 0.6ms |
|
||||
| P-S6-03 | Endless 排行榜 + HMAC 签名 | ✅ | user://endless_records.json |
|
||||
| P-S6-04 | 死亡时截图保存非黑屏 | ✅ | user://runs/run_*.png |
|
||||
| P-S6-05 | 所有 P6-N 规范条目无违反 | ✅ | 1 修复 / 11 PASS |
|
||||
| P-S6-06 | 四语本地化(zh_CN/zh_TW/en/ja) | 🟡 | 骨架完成;法术/Core 名待键化 |
|
||||
| P-S6-07 | 内存峰值 RSS < 512MB | ✅ | 实测 ~176MB |
|
||||
|
||||
---
|
||||
|
||||
## 二、内容完成清单(发布前必做)
|
||||
|
||||
### 2.1 法术内容(当前:14 张,目标:20+ 张)
|
||||
|
||||
| 类型 | 已有 | 缺失(示例) |
|
||||
|:---|:---|:---|
|
||||
| ACTION | spark_bolt, energy_orb, fire_bolt, poison_dart, laser_beam, water_wave, chain_bolt, plasma_storm (产物), poison_pool, summon_turret | ice_nova, nuke, homing, chain_bolt(独立弹道版)|
|
||||
| MODIFIER | damage_plus, spread_mod, double_cast | heavy_cost, speed_mult_mod |
|
||||
| TRIGGER | trigger_on_hit | trigger_on_kill(Timer 触发型) |
|
||||
| LOGIC | every_n_shots, if_hp_below, if_enemy_nearby, loop | — |
|
||||
|
||||
**添加法术流程** → 见 [02 内容创作指南 §A](02_content_authoring.md)
|
||||
|
||||
### 2.2 Core 种类(当前:5 种,目标:7 种)
|
||||
|
||||
| Core | 状态 |
|
||||
|:---|:---|
|
||||
| wand_basic | ✅ |
|
||||
| wand_fast | ✅ |
|
||||
| wand_memory (PERSISTENT_MEMORY) | ✅ |
|
||||
| matrix_board (MATRIX 2×4) | ✅ |
|
||||
| circuit_fork (CIRCUIT) | ✅ |
|
||||
| staff_long (10 槽) | ⬜ 待加 |
|
||||
| wand_eternal (INFINITE_SPELLS) | ⬜ 待加 |
|
||||
|
||||
### 2.3 美术资源(当前:全程序化占位)
|
||||
|
||||
| 资源 | 规格 | 优先级 |
|
||||
|:---|:---|:---|
|
||||
| 玩家精灵 | 64×64 PNG,4 方向或单帧 | P0 |
|
||||
| 敌人 Atlas(6 原型) | 512×512,每格 64×64 | P0 |
|
||||
| 子弹 Atlas(按 damage_type) | 256×64,每格 32×32 | P0 |
|
||||
| VFX 粒子场景(8 种特效) | GPUParticles2D,one_shot=true | P0 |
|
||||
| UI Theme / 背景 | 商店/结算/设置背景图 | P1 |
|
||||
| App 图标 | 512×512 PNG | P0 发布前 |
|
||||
|
||||
**替换方法** → 见 [03 资源替换指南](03_asset_replacement.md)
|
||||
|
||||
### 2.4 音效资源(当前:程序化蜂鸣)
|
||||
|
||||
| 文件 | 内容 |
|
||||
|:---|:---|
|
||||
| `audio/sfx/hit.ogg` | 子弹命中 |
|
||||
| `audio/sfx/kill.ogg` | 敌人死亡 |
|
||||
| `audio/sfx/hurt.ogg` | 玩家受击 |
|
||||
| `audio/sfx/wave_complete.ogg` | 波次结算 |
|
||||
| `audio/sfx/boss.ogg` | Boss 入场 |
|
||||
| `audio/sfx/shop.ogg` | 商店开启 |
|
||||
| `audio/sfx/buy.ogg` | 购买法术 |
|
||||
| `audio/sfx/level_up.ogg` | 升级 |
|
||||
| `audio/bgm/battle_*.ogg` | 战斗 BGM(循环)|
|
||||
|
||||
**替换方法** → 见 [03 资源替换指南 §A](03_asset_replacement.md)
|
||||
|
||||
### 2.5 本地化补全
|
||||
|
||||
- [ ] 所有法术 `display_name` / `description` 改为 `tr()` 键
|
||||
- [ ] 所有 Core `display_name` 改为 `tr()` 键
|
||||
- [ ] 所有状态效果名改为 `tr()` 键
|
||||
- [ ] Boss 名称键化
|
||||
- [ ] 商店页文案(Steam 商店描述,日/英/繁中)
|
||||
|
||||
---
|
||||
|
||||
## 三、Steam PC 发布前必须项
|
||||
|
||||
### 3.1 Steamworks 配置(代码端)
|
||||
|
||||
1. **安装 GodotSteam 插件**(godotsteam.com)
|
||||
2. **初始化**:在 `CrashReporter._ready()` 前调用 `Steam.init()`
|
||||
3. **成就钩子**(ST-11):`AchievementManager` 订阅 `EventID.ACHIEVEMENT_UNLOCKED`(ID=18):
|
||||
```gdscript
|
||||
func _on_achievement_unlocked(p: Dictionary) -> void:
|
||||
Steam.set_achievement(p.get("achievement_id",""))
|
||||
Steam.store_stats()
|
||||
```
|
||||
4. **云存档**(ST-50):`ProfileManager.save_run()` 包裹 `Steam.beginFileWriteBatch/endFileWriteBatch`
|
||||
5. **排行榜提交**(ST-42):`EndlessRecords.add_record()` 后尝试 `Steam.uploadLeaderboardScore()`,网络失败静默缓存
|
||||
|
||||
### 3.2 认证检查项速查
|
||||
|
||||
| 编号 | 项目 | 实现位置 |
|
||||
|:---|:---|:---|
|
||||
| ST-01 | 不收集 PII | 无账号系统 ✅ |
|
||||
| ST-03 | 清除所有本地数据 | `SettingsManager.clear_all_local_data()` ✅ |
|
||||
| ST-10 | Steam.init() 成功 | 待实现 |
|
||||
| ST-20 | 手柄基本可用 | 待实现(InputMap 映射) |
|
||||
| ST-30 | Steam Deck 1280×800 | 待测试 |
|
||||
| ST-40/41 | HMAC 签名排行榜 | `endless_records.gd` ✅ |
|
||||
| ST-50 | Steam Cloud 存档 | 待实现(profile_manager.gd 已有位置) |
|
||||
|
||||
完整清单见 `docs/plan/certification_checklist.md`
|
||||
|
||||
---
|
||||
|
||||
## 四、构建与发布流程
|
||||
|
||||
### 4.1 导出配置(Godot Editor)
|
||||
|
||||
```
|
||||
Project → Export
|
||||
```
|
||||
|
||||
| 平台 | 模板 | 注意 |
|
||||
|:---|:---|:---|
|
||||
| Windows x64 | Export with Mono runtime | 勾选"Embed PCK"打包为单文件 |
|
||||
| Steam Deck (Linux) | Linux x86_64 Mono | 须在 SteamOS/Proton 下测试(ST-33)|
|
||||
|
||||
### 4.2 版本号管理
|
||||
|
||||
在 `project.godot` 设置:
|
||||
```ini
|
||||
[application]
|
||||
config/version="1.0.0"
|
||||
```
|
||||
|
||||
发布时打 git tag:
|
||||
```bash
|
||||
git tag v1.0.0
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
并在 `CHANGELOG.md` 记录构建哈希(PRE-02 要求)。
|
||||
|
||||
### 4.3 发布前最终清单
|
||||
|
||||
```
|
||||
[ ] 所有 P0 认证项 ✅(见 certification_checklist.md)
|
||||
[ ] Steam 商店页:截图×5、宣传片、描述文字审核通过
|
||||
[ ] IARC 自评系统完成(Steam 自动申报)
|
||||
[ ] Wave 1~20 完整通关测试(无崩溃)
|
||||
[ ] crash_reporter.gd 已为首位 Autoload(project.godot 验证)
|
||||
[ ] version = "1.0.0" 已设置,git tag 已推送
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、QA 测试要点
|
||||
|
||||
| 场景 | 验证内容 |
|
||||
|:---|:---|
|
||||
| 全 20 波通关 | 每波正常生成;结算金币/XP 正确;W8/W20 Boss 阶段触发 |
|
||||
| 法术排列测试 | MATRIX 邻接加成;CIRCUIT 双路;共鸣触发;LOGIC 门控 |
|
||||
| 存档恢复 | 关闭游戏→重开→`resume_game()` 恢复法杖+波次 |
|
||||
| 排行榜防篡改 | 手动修改 endless_records.json → 重启应拒绝载入 |
|
||||
| 难度三档 | 初学者:受伤乘数×0.7(即减少30%);挑战:W1 生成24敌(20×1.2),Boss HP×1.3 |
|
||||
| 清除数据 | 设置→清除→确认→所有 user:// 文件消失 |
|
||||
| 语言切换 | 商店中切语言→HUD/商店/背包/结算全部切换 |
|
||||
| 性能压测 | 500 敌 + 1500 弹 → 帧率 ≥ 60fps |
|
||||
| 内存 | Wave 20 Boss 战 → RSS < 512MB |
|
||||
@@ -0,0 +1,134 @@
|
||||
# 06 法术编辑器(可视化工具)
|
||||
|
||||
> **给策划/开发**:无需写一行代码,在 Godot 编辑器里用可视化表单新增、编辑、删除法术。这是 Godot 的 **EditorPlugin**(等同 Unity 的自定义 EditorWindow)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 启用插件
|
||||
|
||||
插件位于 `res://addons/spell_editor/`,默认已在 `project.godot` 启用。若未显示:
|
||||
|
||||
```
|
||||
项目 → 项目设置 → 插件 → 勾选 "Spell Editor (法术编辑器)"
|
||||
```
|
||||
|
||||
启用后,编辑器**左下停靠区**会出现「法术编辑器」标签页。
|
||||
|
||||
> 首次启用或修改插件后,可能需要**重启 Godot 编辑器**让停靠面板出现。
|
||||
|
||||
---
|
||||
|
||||
## 2. 界面说明
|
||||
|
||||
```
|
||||
✨ 法术编辑器
|
||||
┌────────────────────────┐
|
||||
│ 法术列表(ItemList) │ ← 所有 data/spells.json 中的法术
|
||||
│ [ACTION] action_frost… │
|
||||
│ [MODIFIER] modifier_pi… │
|
||||
└────────────────────────┘
|
||||
[➕新增] [⧉复制] [🗑删除]
|
||||
────────────────────────
|
||||
ID(唯一) : [______]
|
||||
类型 : [ACTION ▾]
|
||||
显示名(tr 键) : [______]
|
||||
描述(tr 键) : [______]
|
||||
元素标签(逗号) : [tag:ice]
|
||||
meta(JSON) : [______]
|
||||
[______]
|
||||
[插入该类型 meta 模板]
|
||||
────────────────────────
|
||||
[✓应用到列表] [💾保存文件] [↻重载]
|
||||
状态:……
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 新增一个法术(操作流程)
|
||||
|
||||
1. 点 **➕新增** → 列表出现 `new_spell`,表单自动填入 ACTION 模板
|
||||
2. 改 **ID**(如 `action_lightning`,必须全局唯一)
|
||||
3. 选 **类型**(ACTION / MODIFIER / TRIGGER / LOGIC)
|
||||
4. 填 **显示名 / 描述**(建议填 `tr()` 键,如 `SPELL_LIGHTNING_NAME`,见 [04 本地化](04_localization.md))
|
||||
5. 填 **元素标签**(共鸣用,如 `tag:lightning`;多个用逗号)
|
||||
6. 编辑 **meta**(JSON)——点「插入该类型 meta 模板」获取起始模板,改数值
|
||||
7. 点 **✓应用到列表** → 校验并写入内存
|
||||
8. 点 **💾保存文件** → 写入 `res://data/spells.json`
|
||||
9. 运行游戏(F5)→ 新法术自动出现在商店池,可购买施放
|
||||
|
||||
> **改完不生效?** 法术在游戏启动时由 `SpellRegistry` 加载,需要**重启游戏**(F5 重新运行)。
|
||||
|
||||
---
|
||||
|
||||
## 4. meta 字段速查(按类型)
|
||||
|
||||
### ACTION(弹道)
|
||||
```json
|
||||
{ "base_damage": 5.0, "speed": 350.0, "lifetime": 4.0, "radius": 6.0,
|
||||
"damage_type": 0, "apply_status_id": 2, "pierce": 0, "shop_cost": 20 }
|
||||
```
|
||||
- `apply_status_id`:命中施加状态(1=燃烧 2=冻结 3=中毒…见 `status_id.gd`),可省略
|
||||
- `action_kind`:`"zone"`(地面效果)/ `"summon"`(召唤物),省略=普通弹道
|
||||
|
||||
### ACTION(地面效果,毒池等)
|
||||
```json
|
||||
{ "action_kind": "zone", "radius": 200.0, "status_id": 3,
|
||||
"duration": 5.0, "tick_interval": 1.0, "shop_cost": 28 }
|
||||
```
|
||||
|
||||
### ACTION(召唤炮台)
|
||||
```json
|
||||
{ "action_kind": "summon", "lifetime": 20.0, "range": 350.0,
|
||||
"fire_interval": 0.8, "base_damage": 4.0, "speed": 360.0, "shop_cost": 35 }
|
||||
```
|
||||
|
||||
### MODIFIER(修改施法)
|
||||
```json
|
||||
{ "damage_add": 10.0, "damage_mult": 1.0, "spread_add": 1,
|
||||
"multicast": 1, "pierce": 2, "shop_cost": 20 }
|
||||
```
|
||||
|
||||
### TRIGGER(命中子母弹)
|
||||
```json
|
||||
{ "base_damage": 3.0, "speed": 250.0, "lifetime": 4.0, "radius": 6.0,
|
||||
"damage_type": 0, "shop_cost": 30 }
|
||||
```
|
||||
|
||||
### LOGIC(条件/循环)
|
||||
```json
|
||||
{ "logic_op": "every_n_shots", "n": 3, "reg": 0, "shop_cost": 24 }
|
||||
```
|
||||
`logic_op` 取值:`every_n_shots` / `if_hp_below`(threshold) / `if_enemy_nearby`(range) / `loop`(count)
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据流(原理)
|
||||
|
||||
```
|
||||
法术编辑器 dock ──保存──▶ res://data/spells.json ──游戏启动加载──▶ SpellRegistry
|
||||
(编辑器内) (纯数据,可 git 版本管理) (注册到法术 VM)
|
||||
│
|
||||
商店抽取 / 背包装备 / 法术 VM 编译施放
|
||||
```
|
||||
|
||||
- `data/spells.json` 中的法术**优先级最高**,会覆盖 `wand_preset.gd` 里的同 ID 硬编码版本
|
||||
- 不在 JSON 里的法术仍由硬编码提供(向后兼容,不会丢失现有法术)
|
||||
- JSON 是纯文本,可纳入 git,多人协作改法术不冲突代码
|
||||
|
||||
---
|
||||
|
||||
## 6. 扩展方向(给开发)
|
||||
|
||||
同一套 EditorPlugin 模式可复制出更多可视化工具:
|
||||
|
||||
| 工具 | 数据文件 | 对应硬编码 |
|
||||
|:---|:---|:---|
|
||||
| **法术编辑器**(已实现) | `data/spells.json` | `wand_preset.gd` |
|
||||
| 敌人编辑器(可加) | `data/enemies.json` | `enemy_manager.gd` `_SPEED`/`_ARMOR` |
|
||||
| 波次编辑器(可加) | `data/waves.json` | `wave_manager.gd` `WAVE_CONFIG` |
|
||||
| Core 编辑器(可加) | `data/cores.json` | `wand_preset.gd` `make_*_core` |
|
||||
| 共鸣配方编辑器(可加) | `data/resonance.json` | `spell_evaluator.gd` `_resonance_recipes` |
|
||||
|
||||
实现参考 `addons/spell_editor/spell_dock.gd`:`@tool` 脚本 + `EditorPlugin.add_control_to_dock` + JSON 读写 + 对应 Manager 的 `_load_json_*()` 加载方法。
|
||||
|
||||
> **Godot 编辑器扩展能力**:`EditorPlugin`(停靠面板/工具栏/底部面板)、`@tool`(脚本在编辑器运行)、`EditorInspectorPlugin`(自定义检视器)、自定义 `Resource`(`.tres` 自带表单)、`EditorScript`(一次性脚本)。与 Unity 的 EditorWindow/CustomEditor/ScriptableObject 一一对应,且与节点系统集成更紧。
|
||||
@@ -0,0 +1,158 @@
|
||||
# 07 游戏设计器(一站式可视化配置工具)
|
||||
|
||||
> **核心理念**:策划 / 美术 / 开发**无需写一行代码**,在编辑器的「🎮 游戏设计器」面板里调数值、改玩法、加内容。所有改动保存为 `res://data/*.json`,游戏运行时自动加载。
|
||||
|
||||
---
|
||||
|
||||
## 1. 打开工具
|
||||
|
||||
1. 启用插件(默认已启用):`项目 → 项目设置 → 插件 → 勾选 "Game Designer"`
|
||||
2. 编辑器**底部面板**点击 **「🎮 游戏设计器」** 标签(与"输出""调试器"并列)
|
||||
3. 出现 7 个分页:**✨法术 / 👾敌人 / 🌊波次 / 🪄Core / 🔮共鸣 / 🔥状态 / ⚖平衡**
|
||||
|
||||
> 🏛 **纯数据驱动架构**:游戏所有内容数据**只存在于 `res://data/*.json`**,代码不含任何硬编码内容副本或回退。这意味着——改 JSON 就是改游戏,数据是唯一权威源。若某个 data 文件缺失,游戏会在输出面板 `push_error` 明确报错(而非静默用旧值)。
|
||||
|
||||
> 首次启用或改插件后,可能需**重启 Godot 编辑器**让面板出现。
|
||||
|
||||
---
|
||||
|
||||
## 2. 通用工作流
|
||||
|
||||
```
|
||||
在面板里改数值/加内容 → 点「💾 保存」 → 按 F5 运行游戏 → 改动生效
|
||||
```
|
||||
|
||||
> ⚠️ **所有改动需重启游戏(F5)生效**——数据在游戏启动时由各 Manager 一次性加载。
|
||||
|
||||
---
|
||||
|
||||
## 3. 五个分页详解
|
||||
|
||||
### ✨ 法术(spells.json)
|
||||
新增/编辑/删除法术,最丰富的玩法配置。详见 [06 法术编辑器](06_spell_editor.md)(meta 字段速查表)。
|
||||
- 列表 → 选中编辑;➕新增 / ⧉复制 / 🗑删除
|
||||
- 字段:ID / 类型(ACTION/MODIFIER/TRIGGER/LOGIC) / 显示名 / 描述 / 元素标签 / meta(JSON)
|
||||
- 「插入该类型 meta 模板」一键填入起始参数
|
||||
|
||||
### 👾 敌人(enemies.json)
|
||||
6 种敌人原型的数值表格,**直接拖动数字框**调整:
|
||||
|
||||
| 列 | 含义 |
|
||||
|:---|:---|
|
||||
| 速度 | 移动速度(px/s)|
|
||||
| 护甲 | 每次命中减免的固定伤害 |
|
||||
| 尺寸 | 渲染直径(px),Boss 设大更醒目 |
|
||||
| 颜色 | 点色块选颜色(敌人渲染色)|
|
||||
|
||||
原型:杂兵 / 快速 / 护甲 / 精英 / 首领(MiniBoss) / 终焉(Boss)
|
||||
|
||||
### 🌊 波次(waves.json)
|
||||
**关卡难度曲线编辑器**——逐波配置:
|
||||
|
||||
| 列 | 含义 |
|
||||
|:---|:---|
|
||||
| 怪数 | 本波杂兵数量 |
|
||||
| 血量 | 杂兵基础血量 |
|
||||
| 类型 | 杂兵原型(Basic/Fast/Armored…)|
|
||||
| 精英 | 带寻路的精英数(W15+)|
|
||||
| 金币 / XP | 波次结算奖励 |
|
||||
| Boss | 无 / MiniBoss(4) / Boss(5) |
|
||||
|
||||
- 「➕末尾加一波」扩展关卡长度
|
||||
- 改完点「💾保存 waves.json」
|
||||
|
||||
### 🪄 Core(cores.json)
|
||||
法杖类型配置(影响法术排列玩法):
|
||||
|
||||
| 字段 | 含义 |
|
||||
|:---|:---|
|
||||
| 槽位数 | 可装法术数 |
|
||||
| 拓扑 | LINEAR(线性) / MATRIX(矩阵邻接) / CIRCUIT(电路分叉) |
|
||||
| CPU 上限 | 单次施法最大执行步数 |
|
||||
| 施法间隔 | 自动施法冷却(s) |
|
||||
| 特性 | 无 / PERSISTENT_MEMORY(寄存器跨帧) / … |
|
||||
| 矩阵行·列 | MATRIX 专用网格尺寸 |
|
||||
| 电路边 | CIRCUIT 专用,JSON 有向边 `[{"from":0,"to":1}]` |
|
||||
|
||||
### 🔮 共鸣(resonance.json)
|
||||
元素组合自动合成配方(隐藏玩法):
|
||||
- **元素 A + 元素 B**(相邻放置)→ 合成**产物法术**
|
||||
- 匹配方式:adjacent(相邻) / anywhere_in_deck(任意位置)
|
||||
- 消耗输入:合成后是否移除原两张法术
|
||||
- 例:`tag:water` + `tag:lightning` → `action_plasma_storm`
|
||||
|
||||
### 🔥 状态(status_effects.json)
|
||||
DoT/控制/叠层效果:
|
||||
|
||||
| 字段 | 含义 |
|
||||
|:---|:---|
|
||||
| 状态 ID | 整数(1=燃烧 2=冻结 3=中毒…),与法术 meta 的 `apply_status_id` 对应 |
|
||||
| 持续时间 / 跳字间隔 | DoT 周期 |
|
||||
| 叠层模式 | REFRESH(刷新) / INTENSITY(叠层) / INDEPENDENT(独立) |
|
||||
| 最大层数 | 0=无限 |
|
||||
| 每跳伤害 | DoT 每次伤害 |
|
||||
| VFX ID | 触发的特效 |
|
||||
|
||||
### ⚖ 平衡(balance.json)
|
||||
全局数值微调:
|
||||
- **难度乘子表格**:4 行(敌人血量×/玩家受伤×/波次怪数×/Boss血量×) × 3 档(初学者/标准/挑战)
|
||||
- **Boss 基础血量**:MiniBoss(W8) / Final Boss(W20)
|
||||
|
||||
调一格数字即可改变整体难度曲线。
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据流原理
|
||||
|
||||
```
|
||||
🎮 游戏设计器面板 ──保存──▶ res://data/*.json ──游戏 F5 启动加载──▶ 各 Manager
|
||||
(编辑器内) (纯文本,可 git 版本管理)
|
||||
spells.json → SpellRegistry
|
||||
enemies.json → EnemyManager
|
||||
waves.json → WaveManager
|
||||
cores.json → WandPreset
|
||||
balance.json → SettingsManager + WaveManager
|
||||
```
|
||||
|
||||
- JSON 是**唯一权威数据源**——代码不含硬编码内容副本(纯数据驱动)
|
||||
- 文件缺失/格式错误 → `push_error` 明确报错(不静默,便于发现问题)
|
||||
- 数据是纯文本,**多人协作改数值不会冲突代码**,可走 git review
|
||||
|
||||
### 数据文件清单
|
||||
|
||||
| 文件 | 内容 | 加载方 |
|
||||
|:---|:---|:---|
|
||||
| `spells.json` | 全部法术定义 | SpellRegistry |
|
||||
| `enemies.json` | 6 敌人原型数值 | EnemyManager |
|
||||
| `waves.json` | 20 波关卡曲线 | WaveManager |
|
||||
| `cores.json` | 法杖 Core 定义 | WandPreset |
|
||||
| `resonance.json` | 共鸣配方 | SpellEvaluator |
|
||||
| `status_effects.json` | 状态效果定义 | StatusRegistry |
|
||||
| `balance.json` | 难度乘子 + Boss 血量 | SettingsManager |
|
||||
|
||||
---
|
||||
|
||||
## 5. 实操示例
|
||||
|
||||
### 例 1:让游戏更简单(策划)
|
||||
平衡分页 → 「敌人血量×」初学者列改 `0.5` → 保存 → F5。初学者难度敌人血量减半。
|
||||
|
||||
### 例 2:W10 提前出 Boss(策划)
|
||||
波次分页 → 第 10 行 Boss 列选 `MiniBoss(4)` → 保存 → F5。
|
||||
|
||||
### 例 3:新增一个"冰锥"法术(策划)
|
||||
法术分页 → ➕新增 → ID 填 `action_ice_spike`,类型 ACTION,「插入模板」后改 `base_damage` 为 8、加 `"apply_status_id": 2`(冻结)→ ✓应用 → 💾保存 → F5。新法术自动进商店。
|
||||
|
||||
### 例 4:精英敌人改成紫色更大(美术)
|
||||
敌人分页 → 精英行 → 尺寸改 `30`,颜色点开选紫色 → 保存 → F5。
|
||||
|
||||
---
|
||||
|
||||
## 6. 扩展工具(给开发)
|
||||
|
||||
新增编辑器分页只需 3 步(参考 `addons/game_designer/`):
|
||||
1. 写一个 `@tool extends VBoxContainer` 的标签脚本(读写对应 `data/xxx.json`,复用 `designer_ui.gd` 的 `spin/line/opt/btn` 辅助)
|
||||
2. 在 `designer_panel.gd` 的 `_init()` 里 `_add_tab(...)` 注册
|
||||
3. 对应 Manager 加 `_load_json_xxx()`,在 `_ready()` 调用
|
||||
|
||||
> **Godot 编辑器扩展 = Unity 平替**:`EditorPlugin`(面板/工具栏/底部面板) ↔ EditorWindow;`@tool` ↔ ExecuteInEditMode;`EditorInspectorPlugin` ↔ CustomEditor;自定义 `Resource` ↔ ScriptableObject。
|
||||
@@ -0,0 +1,65 @@
|
||||
# 魔法工匠:开发使用手册 (Developer & Designer Handbook)
|
||||
|
||||
> **适用人群**:策划、美术、程序、QA
|
||||
> **引擎版本**:Godot 4.6 (Mono)
|
||||
> **最后更新**:2026-06-05
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
| 文档 | 适合 | 内容 |
|
||||
|:---|:---|:---|
|
||||
| [01 快速上手](01_quickstart.md) | 全员 | 克隆→启动→首次运行 |
|
||||
| [02 内容创作指南](02_content_authoring.md) | 策划 / 程序 | 法术 / 敌人 / 波次 / Boss / 共鸣 |
|
||||
| [03 资源替换指南](03_asset_replacement.md) | 美术 / 程序 | 特效 / 精灵 / 音效 / UI 图片 |
|
||||
| [04 本地化工作流](04_localization.md) | 本地化 / 策划 | `.po` 维护、新增语言、字符串规范 |
|
||||
| [05 发布标准清单](05_release_guide.md) | 全员 / PM | Steam / Switch 认证、发布流程 |
|
||||
| [06 法术编辑器](06_spell_editor.md) | 策划 / 开发 | 法术编辑器 meta 字段速查 |
|
||||
| [07 游戏设计器](07_game_designer.md) | 策划 / 美术 / 开发 | **⭐ 一站式可视化工具**:法术/敌人/波次/Core/平衡全部表单配置,零代码 |
|
||||
|
||||
---
|
||||
|
||||
## 项目目录快览
|
||||
|
||||
```
|
||||
daihaoRogue/
|
||||
├── scenes/
|
||||
│ └── main/combat_s2.tscn ← 主场景(唯一可玩入口)
|
||||
├── scripts/
|
||||
│ ├── autoloads/ ← 全局单例(30+ Autoload)
|
||||
│ └── domain/
|
||||
│ ├── spell_system/ ← 法术虚拟机
|
||||
│ └── combat/ ← 游戏循环状态机
|
||||
├── translations/ ← zh_CN / zh_TW / en / ja .po
|
||||
├── audio/sfx/ ← 音效放这里(目前占位蜂鸣)
|
||||
├── assets/sprites/ ← 精灵图放这里(目前几何占位)
|
||||
└── docs/
|
||||
├── design/ ← 游戏设计文档
|
||||
├── mechanics/ ← 机制深度文档
|
||||
├── technical/ ← 架构 / 实现方案
|
||||
├── plan/ ← 开发计划 / 认证清单
|
||||
└── handbook/ ← 本手册(你在这里)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键 Autoload 速查
|
||||
|
||||
| Autoload | 职责 | 常用入口 |
|
||||
|:---|:---|:---|
|
||||
| `WandPreset` | 法术/Core 内置预设 | `make_spark_bolt()`, `make_wand_basic()` |
|
||||
| `SpellEvaluator` | 法术虚拟机(编译+执行) | `compile_wand(core, nodes)`, `execute_compiled(compiled, caster_id, pos)` |
|
||||
| `SpellRegistry` | 法术查询表 | `get_spell("action_fire_bolt")` |
|
||||
| `WaveManager` | 波次生成/配置 | `WAVE_CONFIG[0..19]` |
|
||||
| `EnemyManager` | 敌人 SoA 管理 | `spawn_enemy(pos, hp, type)` |
|
||||
| `BulletManager` | 子弹 SoA 管理 | `spawn_bullet(...)` |
|
||||
| `VFXManager` | 特效池 | `play("hit_spark", pos)` |
|
||||
| `AudioManager` | 音效池 | `play("hit", pos)` |
|
||||
| `SettingsManager` | 难度 / 音量 / 语言 | `difficulty_name()`, `enemy_hp_mult()` |
|
||||
| `EndlessRecords` | 本地排行榜 | `add_record(wave, elapsed, kills)` |
|
||||
| `ProfileManager` | A/B 槽局内存档 | `save_run()`, `load_run()` |
|
||||
| `Locale` | 语言切换 | `set_locale("en")` |
|
||||
| `StatusManager` | 状态效果(DoT 等) | `apply(entity_id, StatusID.BURN, 2)` |
|
||||
| `ZoneManager` | 地面效果区域 | `spawn_zone(cx, cy, r, status_id, dur, tick, owner)` |
|
||||
| `MinionManager` | 召唤物 | `spawn_minion(def_dict, owner_id, pos)` |
|
||||
@@ -0,0 +1,98 @@
|
||||
# 进阶机制扩展:召唤与环境蔓延 (Advanced Mechanics: Summons & Propagation)
|
||||
|
||||
基于现有的 ECS 战斗系统,我们可以进一步拓展 **“召唤体系 (Summoning System)”** 和 **“环境传导 (Environmental Propagation)”** 维度。
|
||||
|
||||
## 1. 召唤体系 (Summoning Dimension)
|
||||
|
||||
召唤物不仅仅是“另一种子弹”,它们通常拥有独立的生命周期、AI 行为和属性继承规则。
|
||||
|
||||
### 1.1 召唤物类型 (Archetypes)
|
||||
* **Stationary (炮台型)**: 无法移动,持续对范围内敌人攻击。 (如:火舌图腾)
|
||||
* **Minion (随从型)**: 拥有碰撞体积,能阻挡敌人,有血量,会自动寻敌并攻击。 (如:骷髅兵、死灵狼)
|
||||
* **Satellite (卫星型/浮游炮)**: 围绕玩家旋转或跟随,不可被选中,辅助攻击。 (如:环绕飞剑)
|
||||
* **Clone (镜像型)**: 玩家的分身,继承完全相同的攻击行为,但伤害倍率较低。
|
||||
|
||||
### 1.2 属性继承 (Stat Inheritance)
|
||||
召唤物的数据快照(Snapshot)机制至关重要:
|
||||
* **Inheritance Ratio**: 不同的召唤物继承玩家属性的比例不同(如:继承 50% 攻速,100% 暴击)。
|
||||
* **Dynamic vs Static**:
|
||||
* **Static**: 召唤瞬间确定面板(大多数非光环类召唤)。
|
||||
* **Dynamic**: 随玩家属性实时变化(如链接玩家的灵魂锁链)。
|
||||
|
||||
### 1.3 行为指令 (Command AI)
|
||||
* **Aggressive**: 搜寻屏幕内所有敌人。
|
||||
* **Defensive**: 只攻击攻击玩家的敌人。
|
||||
* **Focus Fire**: 攻击玩家当前瞄准的目标(光标指示)。
|
||||
|
||||
## 2. 环境传播与蔓延 (Propagation & Spreading)
|
||||
|
||||
“火焰范围蔓延”属于 **Cellular Automata (元胞自动机)** 或 **传染 (Contagion)** 机制的范畴。
|
||||
|
||||
### 2.1 实体间传导 (Entity-to-Entity Propagation)
|
||||
不需要地块网格,直接基于实体距离的传播。
|
||||
* **Ignite Proliferation (点燃扩散)**:
|
||||
* 机制: 当一个敌人处于“燃烧”状态死亡时,将剩余的燃烧层数(或新的燃烧状态)复制给半径 R 内的所有敌人。
|
||||
* 扩展: “瘟疫”效果,在存活时通过 Tick 周期性传染周围。
|
||||
|
||||
### 2.2 区域场 (Field / Zone System)
|
||||
引入 `ZoneManager` 来管理地面上的持久化效果区域。
|
||||
* **Pools (地面池)**: 毒液滩、岩浆、神圣地面。
|
||||
* *实现*: 具有 Circle Collider 的 Trigger 节点。
|
||||
* *逻辑*: `OnTriggerEnter` -> Apply Status; `OnTriggerExit` -> Remove Status (optional)。
|
||||
* **Reactive Ground (反应地表)**:
|
||||
* 向“毒液滩”发射“火箭”,引爆毒池造成瞬间高伤。
|
||||
* 向“水面”发射“冰箭”,冻结水面减慢经过敌人的速度或使其滑倒。
|
||||
|
||||
### 2.3 连锁反应示例 (Chain Reaction)
|
||||
1. **步骤 A**: 玩家投掷毒瓶,生成半径 200 的 `PoisonZone`。
|
||||
2. **步骤 B**: 怪物进入区域,获得 `Poisoned` 状态。
|
||||
3. **步骤 C**: 玩家使用“尸爆术”(Tag: Fire)。
|
||||
4. **步骤 D**: 尸爆击中带毒的怪物 -> 触发 `Catalyst: Fire + Poison` -> 产生 `GasExplosion` (毒气爆炸),范围扩大至 400,并施加 `Blind` 致盲效果。
|
||||
|
||||
## 3. 开发实施建议
|
||||
|
||||
### 3.1 扩展 EnemyManager 支持友军
|
||||
* 目前的 `EnemyManager` 是专门管理敌人的。为了支持 Minion:
|
||||
* 方案 A: 改造 `EnemyManager` 为 `UnitManager`,引入 `Faction` 字段(0=Friendly, 1=Hostile),统一管理所有生物。
|
||||
* 方案 B: 新增 `MinionManager`,逻辑类似但 AI 目标相反(Target=Enemy)。**推荐方案 B**,因为逻辑差异较大。
|
||||
|
||||
### 3.2 实现传播算法 (The Spread Algorithm)
|
||||
在 `StatusManager` 中增加 `on_entity_died` 回调监听:
|
||||
```gdscript
|
||||
# 伪代码: 点燃扩散 (GDScript 风格)
|
||||
# 状态类型系统为数据驱动的整数 ID,使用 StatusID Autoload 常量(见 architecture_design.md §8 ADR-R5-N2)。
|
||||
# StatusID.BURN 等常量定义在 status_id.gd (Autoload: StatusID) 中。
|
||||
# 禁止写法:StatusManager.has_status(victim_id, StatusType.BURN) ← StatusType 枚举不存在
|
||||
func on_enemy_died(victim_id: int) -> void:
|
||||
if StatusManager.has_status(victim_id, StatusID.BURN): # ✅ 使用 StatusID 常量
|
||||
var neighbors: Array[int] = SpatialGrid.query_circle(
|
||||
EnemyManager.get_position(victim_id), 300.0)
|
||||
for n_id in neighbors:
|
||||
StatusManager.apply(n_id, StatusID.BURN, 0) # ✅ 使用 StatusID 常量
|
||||
VFXManager.play("fire_spread", EnemyManager.get_position(victim_id))
|
||||
```
|
||||
|
||||
> **StatusID Autoload 规范**:与 `CoreFeatureTag` 类比,所有状态类型必须通过
|
||||
> `StatusID` Autoload 常量引用,禁止在业务代码中直接写整数字面量或自造枚举:
|
||||
> ```gdscript
|
||||
> # status_id.gd (Autoload: StatusID)
|
||||
> ## 状态效果 ID 常量表 — 所有系统通过此处引用,禁止裸整数
|
||||
> const BURN: int = 1 # 点燃(Fire DoT)
|
||||
> const FREEZE: int = 2 # 冻结(硬控)
|
||||
> const POISON: int = 3 # 中毒(Poison DoT,可叠层)
|
||||
> const WET: int = 4 # 潮湿(增幅雷电伤害)
|
||||
> const OILY: int = 5 # 油腻(增幅火焰伤害)
|
||||
> const STUN: int = 6 # 麻痹(雷电触发)
|
||||
> const COMBO_MARK: int = 7 # 连击标记(详见 consecutive_hits_stacking.md)
|
||||
> const VULNERABILITY: int = 8 # 易伤标记(激光叠层增伤,详见 consecutive_hits_stacking.md)
|
||||
> # 新增状态 ID 从 9 开始递增,并在此处登记
|
||||
> ```
|
||||
|
||||
### 3.3 实现地面效果 (Zone Manager)
|
||||
* **ZoneManager 必须基于 `SpatialGrid` 进行空间索引(ADR-R5-N1 规范)**,
|
||||
**禁止使用 Area2D(Trigger 模式)**——Area2D 会为每个 Zone 创建 Godot 物理节点,
|
||||
1000 敌人 × 64 Zone 的碰撞检测开销完全无法接受,违反 ECS-Lite 架构原则。
|
||||
详见 `architecture_design.md §8 ADR-R5-N1`(ZoneManager 权威实现定义)。
|
||||
* 每 `_physics_process` 帧对所有 Zone 调用 `SpatialGrid.query_circle(zone_center, zone_radius)`,
|
||||
批量获取范围内敌人 ID,然后通过 `StatusManager.apply()` 施加状态效果;
|
||||
使用 `tick_interval` 速率限制防止每帧过量触发(见 architecture_design.md §8 ADR-R5-N1)。
|
||||
@@ -0,0 +1,176 @@
|
||||
# 深度战斗机制扩展设计文档 (Advanced Combat Mechanics Design)
|
||||
|
||||
为了支持 Roguelike 游戏中涌现式的玩法组合(如:闪电链、叠毒流、触发流),现有的简单"伤害+阵营"模型需要升级为多维度的**上下文感知 (Context-Aware)** 系统。
|
||||
|
||||
本文档详细定义了扩展战斗深度所需的关键维度和判断逻辑。
|
||||
|
||||
## 1. 投射物身份与溯源 (Projectile Identity & Sourcing)
|
||||
|
||||
在复杂的战斗中(例如:玩家发射的子弹击中怪物A,触发爆炸,爆炸产生碎片击中怪物B),系统必须能追踪伤害的根本来源。
|
||||
|
||||
### 1.1 来源层级 (Source Hierarchy)
|
||||
我们需要在子弹数据中维护一个"血缘关系",通常通过 ID 或 引用 实现:
|
||||
* **Root Source (根源)**: 也就是 `Owner`。通常是 PlayerEntity 或 EnemyEntity。用于判定击杀归属(触发"击杀回血"天赋)、伤害统计。
|
||||
* **Immediate Source (直接来源)**: 发射该子弹的实体。可能是玩家手中的法杖,也可能是另一个"母子弹"(在分裂/触发机制中)。
|
||||
* *应用场景*: 伤害衰减计算。如果是"母子弹"分裂出的"子弹",继承伤害时可能需要乘系数 0.5。
|
||||
|
||||
### 1.2 施法类型标记 (Cast Type Tags)
|
||||
除了 `Faction` (阵营),还需要 `Tags` (位掩码) 来区分伤害性质,以便应用特定的加成:
|
||||
* **Tag: Primary**: 玩家直接射击(享受"普攻伤害加成")。
|
||||
* **Tag: Triggered**: 由触发器产生的(享受"技能伤害加成",但可能不触发某些普攻特效)。
|
||||
* **Tag: Summon**: 召唤物造成的伤害。
|
||||
* **Tag: Reflection**: 反弹/弹反造成的伤害。
|
||||
|
||||
## 2. 连锁与弹跳逻辑 (Chain & Bounce Dimensions)
|
||||
|
||||
"闪电链"不仅仅是次数的递减,它需要一个完整的状态机来处理复杂的传递逻辑。
|
||||
|
||||
### 2.1 命中历史 (Hit History / Exclusion List)
|
||||
* **问题**: 简单的"最近距离"查找会导致闪电在两个靠近的怪物之间无限来回弹射 (Ping-Pong)。
|
||||
* **维度需求**: 每颗子弹需要维护一个已命中目标 ID 集合 `visited_targets: Array[int]`。
|
||||
* **逻辑**: `SpatialGrid.find_nearest(pos)` 过滤掉 `visited_targets` 中已包含的 `entity_id`。
|
||||
* **性能优化**: 在 ECS 架构中,可以使用 `Dictionary` (key: bullet_id int, value: Array[int]) 的 Sidecar 模式存储历史记录,仅在命中结算(低频逻辑)时查询,不影响移动更新(高频逻辑)。
|
||||
|
||||
### 2.2 衰减模型 (Decay Models)
|
||||
每次传递后的伤害变化不应只有一种模式:
|
||||
* **Linear Decay**: `Dmg = Base - (BounceCount * FixedValue)`
|
||||
* **Exponential Decay**: `Dmg = Base * (DecayFactor ^ BounceCount)` (例如每次衰减 20%)
|
||||
* **Reverse Scaling**: `Dmg = Base * (1 + BounceCount * 0.1)` (越弹伤害越高,奖励多次传递)
|
||||
|
||||
### 2.3 传递约束 (Propagation Constraints)
|
||||
* **Max Distance**: 弹射的最大搜索半径(不能跨越半个屏幕弹射)。
|
||||
* **Line of Sight**: 弹射是否需要视野(能否穿墙)。
|
||||
* **Angle Limit**: 只能向前弹射(90度扇区),不能向后弹射。
|
||||
|
||||
## 3. 状态效果与持续伤害 (Status Effects & DoT)
|
||||
|
||||
毒、火、流血等机制需要独立且统一的 **Status Manager**。
|
||||
|
||||
### 3.0 StatusType 数据驱动方案 (E4)
|
||||
|
||||
`StatusType` **不使用硬编码枚举**,而是从 `res://resources/status_types/` 目录自动加载 `.tres` Resource,实现完全数据驱动。新增状态类型无需修改引擎代码:
|
||||
|
||||
```gdscript
|
||||
# status_type_def.gd
|
||||
class_name StatusTypeDef
|
||||
extends Resource
|
||||
|
||||
@export var id: int # 全局唯一整数 ID(用于位掩码和 Dictionary key)
|
||||
@export var name: String # 显示名称
|
||||
@export var stack_mode: int # 0=Duration, 1=Intensity, 2=Independent
|
||||
@export var max_stacks: int = 99 # 最大叠加层数(0=无限)
|
||||
@export var tick_interval: float = 1.0 # DoT 跳字间隔(秒)
|
||||
@export var can_catalyze: Array[int] # 催化反应:触发时需要的另一状态 ID 列表
|
||||
@export var immune_faction_tags: int # 免疫此状态的 faction 位掩码
|
||||
```
|
||||
|
||||
`StatusManager._ready()` 启动时扫描目录注册,逻辑与 SpellRegistry 完全一致。
|
||||
|
||||
### 3.1 堆叠维度 (Stacking Dimensions)
|
||||
* **Duration Stacking (时间堆叠)**: 再次施加相同效果,刷新或延长持续时间(如:冰冻)。
|
||||
* **Intensity Stacking (强度堆叠)**: 再次施加相同效果,增加层数,伤害倍率提升(如:中毒,1层每秒10点,5层每秒50点)。
|
||||
* **Independent Instances (独立计算)**: 每次施加都是一个独立的 DoT 计时器,互不干扰(如:流血,身上可以挂10个流血Debuff,每个单独跳字)。
|
||||
|
||||
### 3.2 结算归属 (Tick Attribution)
|
||||
DoT 造成的每次伤害(Tick)都需要携带 `Root Source` 信息。
|
||||
* *场景*: 玩家给 Boss 挂了毒,然后跑开了。Boss 被毒死时,系统必须知道是"玩家"杀死的,从而触发"通关结算"或"击杀回血"。
|
||||
|
||||
### 3.2.5 StatusManager Tick 机制(P-N4 补充,P5-N1 修正)
|
||||
|
||||
`StatusManager` 绑定 `_physics_process(delta)`(60 Hz),采用**批处理 Tick**策略:
|
||||
|
||||
- **平铺数组存储(P5-N1)**:`_active_statuses: Array[StatusInstance]` 为**一维平铺数组**,每个 `StatusInstance` 携带 `entity_id` 字段。相较于原 `Dictionary{entity_id → Array}` 方案,平铺顺序遍历的缓存命中率显著更高,规避了 GDScript 哈希表迭代的无序开销。按实体查询(`get_instances_for(entity_id)`)仅在 apply/remove 等低频操作时调用,不出现在 `_physics_process` 热路径中。
|
||||
- **每帧遍历**:逆序遍历 `_active_statuses`,按各实例的 `remaining_duration` 递减 `delta`,duration ≤ 0 时原地 swap-with-last 移除(O(1)),避免遍历中途修改数组。
|
||||
- **按 tick_interval 跳字**:DoT 不在每帧触发伤害,而是维护 `tick_accumulator: float`;
|
||||
每帧 `tick_accumulator += delta`,当 `tick_accumulator >= tick_interval` 时
|
||||
触发一次 `APPLY_DAMAGE` 并扣减 `tick_interval`(允许 delta 跨多个 tick_interval 时补算多次)。
|
||||
- **分帧批处理**:若同帧活跃状态实例超过 `STATUS_BATCH_LIMIT = 500` 条,
|
||||
按 `inst.entity_id % 2` 奇偶帧分批处理,牺牲 1 帧的 tick 精度换取帧时间平摊。
|
||||
|
||||
```gdscript
|
||||
# _active_statuses: Array[StatusInstance](平铺数组,非 Dictionary)
|
||||
# StatusInstance 字段: entity_id, type_def, intensity, remaining_duration, tick_accumulator, root_owner_id
|
||||
func _physics_process(delta: float) -> void:
|
||||
var i := _active_statuses.size() - 1
|
||||
while i >= 0:
|
||||
var inst: StatusInstance = _active_statuses[i]
|
||||
inst.remaining_duration -= delta
|
||||
inst.tick_accumulator += delta
|
||||
while inst.tick_accumulator >= inst.type_def.tick_interval:
|
||||
inst.tick_accumulator -= inst.type_def.tick_interval
|
||||
_apply_dot_tick(inst.entity_id, inst)
|
||||
if inst.remaining_duration <= 0.0:
|
||||
_active_statuses[i] = _active_statuses.back()
|
||||
_active_statuses.pop_back()
|
||||
i -= 1
|
||||
```
|
||||
|
||||
### 3.3 状态交互矩阵 (Interaction Matrix)
|
||||
状态之间应有预定义的交互规则:
|
||||
* **Overwrite**: 强状态覆盖弱状态(大冰冻覆盖小减速)。
|
||||
* **Catalyze (催化)**: 状态 A 遇到 状态 B -> 消除 B 并触发 C(雷 + 湿 = 范围感电)。
|
||||
* **Immunity**: 某些怪物(如火元素)对特定状态(燃烧)免疫甚至回血。
|
||||
|
||||
## 4. 伤害计算管线 (The Damage Pipeline)
|
||||
|
||||
伤害计算遵循 `docs/design/numerical_design.md §3.1` 定义的权威公式:
|
||||
|
||||
```
|
||||
FinalDamage = ((Base + Add) × Mult) × (1 - Res) - Armor
|
||||
```
|
||||
其中:`Mult` = `SourceMultipliers × CriticalMultiplier`;`Res` = 敌人元素抗性 (0.0~1.0);`Armor` = 敌人护甲平铺扣除值。最终伤害最小为 1。
|
||||
|
||||
我们需要在伤害事件 `APPLY_DAMAGE` 中传递一个结构体 `DamageContext` 而非简单的数值:
|
||||
|
||||
```gdscript
|
||||
# damage_context.gd - 继承 RefCounted,通过对象池复用
|
||||
# 权威实现(含池化逻辑)在 DamageContextPool Autoload;本文件描述字段语义。
|
||||
class_name DamageContext
|
||||
extends RefCounted
|
||||
|
||||
var base_damage: float = 0.0 # 基础数值(含 CastStats.damage_add)
|
||||
var mult: float = 1.0 # 伤害乘算系数(暴击 × 元素弱点已合入此值)
|
||||
# 对应公式:Damage = ((Base + Add) × Mult) × (1 - Res) - Armor
|
||||
# EnemyManager.apply_damage() 读取此字段计算最终伤害
|
||||
var damage_type: int = DamageType.PHYSICAL # DamageType 枚举
|
||||
var owner_id: int = -1 # 来源实体 ID(Root Owner)
|
||||
var is_crit: bool = false # 是否暴击(影响爆字/特效触发;与 DamageContextPool 字段同步)
|
||||
var pierce_rate: float = 0.0 # 护甲穿透率(0.0~1.0;与 DamageContextPool 字段同步)
|
||||
var source_tags: int = 0 # 位掩码: PRIMARY=1, TRIGGERED=2, SUMMON=4, REFLECTED=8
|
||||
|
||||
func reset() -> void:
|
||||
base_damage = 0.0; mult = 1.0; damage_type = DamageType.PHYSICAL; owner_id = -1
|
||||
is_crit = false; pierce_rate = 0.0; source_tags = 0
|
||||
# mult 必须重置为 1.0,否则池化复用时上帧的暴击乘算残留到下次计算
|
||||
```
|
||||
|
||||
## 5. 实现建议 (ECS Adaptation)
|
||||
|
||||
针对当前的 `BulletManager` (PackedFloat32Array ECS),如何在不破坏性能的前提下引入这些维度?
|
||||
|
||||
**混合架构 (Hybrid ECS Pattern):**
|
||||
|
||||
1. **热数据 (PackedFloat32Array)**: 保持 `x, y, vx, vy, life` 在 `PackedFloat32Array` 中,确保每帧移动批处理极快。
|
||||
2. **冷数据 (Dictionary)**: 在 `BulletManager` 中维护 `_bullet_contexts: Dictionary`(key = bullet_id `int`)。
|
||||
* 值为 `BulletContext` 对象(继承 `RefCounted`,通过对象池复用),存储 `visited_targets`、`owner_id`、`proc_coefficient`、`on_hit_payload_id` 等复杂逻辑。
|
||||
* 仅在 `_on_bullet_hit()`(命中)或 `spawn()`(生成)时访问 Dictionary,这些事件频率远低于每帧移动,不会成为瓶颈。
|
||||
|
||||
```gdscript
|
||||
# bullet_context.gd - 通过 ObjectPool 复用,避免 GC 抖动
|
||||
class_name BulletContext
|
||||
extends RefCounted
|
||||
|
||||
var owner_id: int = -1 # 根来源实体 ID
|
||||
var visited_targets: Array[int] = [] # 连锁弹射已命中目标列表
|
||||
var proc_coefficient: float = 1.0 # 触发系数(加特林子弹通常设为 0.1)
|
||||
var on_hit_payload_id: int = -1 # 命中时触发的 SubPayload ID(-1=无)
|
||||
var bounce_count: int = 0 # 已弹射次数
|
||||
# BulletContext 不持有伤害计算逻辑(SRP)。
|
||||
# 弹射衰减(pow(0.9, bounce_count))集中在 EnemyManager.apply_damage() 中实现;BulletContext 仅透传 bounce_count。
|
||||
|
||||
func reset() -> void:
|
||||
owner_id = -1; visited_targets.clear()
|
||||
proc_coefficient = 1.0; on_hit_payload_id = -1; bounce_count = 0
|
||||
```
|
||||
|
||||
通过这种方式,热路径(移动积分)保持纯 Array 操作,冷路径(命中逻辑)通过 Dictionary 访问复杂上下文,兼顾了弹幕游戏的性能与 RPG 的深度。
|
||||
@@ -0,0 +1,102 @@
|
||||
# 战斗机制进阶扩展 (Advanced Combat Mechanics Extensions - Part 2)
|
||||
|
||||
为了构建一个具备极高重玩价值(Replayability)的 Roguelike 系统,除了基础的伤害和弹射,我们还可以引入物理、环境、触发链等更多维度的交互。
|
||||
|
||||
以下是可供扩展的 5 个额外维度:
|
||||
|
||||
## 1. 物理与运动学维度 (Physics & Kinematics Dimensions)【P2/P3 扩展,非当前实现目标】
|
||||
|
||||
> ⚠️ **实现状态说明**:本节所有维度均为**设计扩展提案(P2/P3)**,
|
||||
> 当前 BulletManager SoA、ProjectileDef、EnemyManager SoA **均无对应字段**。
|
||||
> 实现时需要:
|
||||
> - `ProjectileDef` 增加 `knockback_force: float`(冷数据,存入 `_bullet_contexts`)
|
||||
> - `EnemyManager` SoA 增加 `mass: float`(或冷数据中存储),`ENEMY_STRIDE` 相应扩展
|
||||
> - 否则引用本节代码片段将在运行时找不到字段
|
||||
|
||||
让投射物不仅仅是”飞行的数字”,而是具有物理实体的对象。
|
||||
|
||||
### 1.1 冲量与质量 (Impulse & Mass)【P3】
|
||||
* **Knockback Force (击退力)**: 子弹应携带“击退值”。
|
||||
* **Entity Mass (实体质量)**: 怪物应有“质量”属性。
|
||||
* **Resistance (抗性)**: 小蝙蝠被打中会飞很远,而石头人哪怕受到高伤也纹丝不动。
|
||||
* **维度**: `KnockbackVector = (BulletVelocity.normalize() * KnockbackForce) / TargetMass`
|
||||
|
||||
### 1.2 轨迹调制 (Trajectory Modulation)
|
||||
除了直线飞行,子弹的运动模式可以参数化:
|
||||
* **Acceleration (加速度)**: 子弹可以越飞越快(狙击),或越飞越慢(能量球停留)。
|
||||
* **Curvature (曲率/归航)**: `HomingStrength` (归航强度) 和 `TurnRate` (原本直飞,靠近敌人时转弯)。
|
||||
* **Oscillation (震荡)**: 正弦波飞行(Sine Wave),用于扩大打击面。
|
||||
|
||||
## 2. 环境场与交互维度 (Environment & Interaction Dimensions)
|
||||
|
||||
战斗不仅仅发生在“子弹”和“敌人”之间,还包括“环境”。
|
||||
|
||||
### 2.1 地表效果 (Surface/Ground Effects)
|
||||
* **Persistence (残留)**: 毒瓶炸开后在地面留下 `PoisonPool`,持续 5 秒。
|
||||
* **Conductivity (传导性)**:
|
||||
* 水面 + 闪电 = 全屏感电。
|
||||
* 油面 + 火焰 = 不灭之火(延长燃烧时间)。
|
||||
* 冰面 + 物理撞击 = 滑行距离增加。
|
||||
|
||||
### 2.2 障碍物交互 (Obstacle Interaction)
|
||||
* **Ricochet (跳弹)**: 击中墙壁时,根据入射角计算反射角。
|
||||
* **Phasing (相位)**: 是否能穿墙(穿墙子弹通常伤害较低或无法穿透敌人)。
|
||||
* **Embedding (嵌入)**: 箭矢击中墙壁后保留实体,可被玩家回收或作为后续法术的引雷针。
|
||||
|
||||
## 3. 触发器与事件链系统 (Trigger System / Proc Chains)
|
||||
|
||||
参考《Risk of Rain 2》或《Path of Exile》的 Proc(Process on Cast/Hit)机制。
|
||||
|
||||
### 3.1 触发钩子 (Event Hooks)
|
||||
不仅仅是“命中”,需要更细的粒度:
|
||||
* **OnHit**: 命中敌人时。
|
||||
* **OnCrit**: 暴击时(如:暴击时召唤落雷)。
|
||||
* **OnKill**: 击杀时(如:敌人产生尸爆)。
|
||||
* **OnExpire**: 子弹自然销毁/达到最大射程时(如:榴弹并在尽头爆炸)。
|
||||
* **OnHurt**: 玩家受击时(如:受击释放尖刺)。
|
||||
|
||||
### 3.2 触发系数 (Proc Coefficient)
|
||||
为了平衡高频攻击(如加特林)和低频攻击(如狙击枪):
|
||||
* **ProcRate**: 这是一个 0.0 - 1.0 的系数。
|
||||
* *例子*: "攻击有 10% 概率触发导弹"。
|
||||
* 狙击枪 (Proc: 1.0) -> 每次判定都是 10%。
|
||||
* 加特林 (Proc: 0.1) -> 每次判定实际概率是 10% * 0.1 = 1%。
|
||||
* 防止高攻速武器过于变态地触发特效。
|
||||
|
||||
## 4. 属性转换与快照机制 (Conversion & Snapshotting)
|
||||
|
||||
处理复杂的数值计算规则。
|
||||
|
||||
### 4.1 伤害转换 (Damage Conversion)
|
||||
* **Shift**: "将 50% 的物理伤害转化为火焰伤害"。
|
||||
* **Gain**: "获得等同于物理伤害 20% 的额外混沌伤害"。
|
||||
* **Priority (优先级)**: 转换通常有固定顺序(物理 -> 元素 -> 混沌),防止循环转换。
|
||||
|
||||
### 4.2 快照机制 (Snapshotting)
|
||||
* **Dynamic vs Static**:
|
||||
* 如果玩家发射了一个持续 10 秒的火龙卷,然后在第 5 秒喝了药水(增加智力)。
|
||||
* **Snapshot**: 火龙卷伤害不变(取发射瞬间的数值)。这通常更符合直觉且性能更好。
|
||||
* **Dynamic**: 火龙卷伤害随之增加。这就要求子弹必须持有 Player 引用并在每帧重新计算属性。
|
||||
|
||||
## 5. 控制与免疫维度 (Crowd Control & Diminishing Returns)
|
||||
|
||||
并不是所有的“负面效果”都是伤害(DoT)。
|
||||
|
||||
### 5.1 硬控制与软控制 (Hard/Soft CC)
|
||||
* **Soft CC**: 减速 (Slow)、致盲 (Blind - 降低命中率/索敌范围)。
|
||||
* **Hard CC**: 眩晕 (Stun - 停止行动)、冻结 (Freeze - 停止动画)、恐惧 (Fear - 反向逃跑)、魅惑 (Charm - 攻击友军)。
|
||||
|
||||
### 5.2 收益递减 (Diminishing Returns - DR)
|
||||
为了防止 Boss 被永久晕在原地:
|
||||
* **DR Meter**: 怪物有一个隐藏的抗性条。
|
||||
* **机制**: 第一次眩晕 2秒 -> 第二次 1秒 -> 第三次 免疫。
|
||||
* **Recovery**: 抗性条随时间慢慢恢复。
|
||||
|
||||
## 6. 斩杀与处决 (Culling & Execution)
|
||||
|
||||
* **Threshold Execution**: "立即消灭生命值低于 10% 的非 Boss 敌人"。
|
||||
* **Low/High Health Scaling**:
|
||||
* "对生命值 > 90% 的敌人必定暴击" (First Strike)。
|
||||
* "敌人生命值每降低 1%,造成额外 1% 伤害" (Executioner)。
|
||||
|
||||
通过引入这些维度,可以将简单的“数值比拼”转化为策略构筑游戏。例如构建一个“击退流”玩法:利用高击退把敌人推到涂满油的墙上,配合火属性子弹造成环境爆炸。
|
||||
@@ -0,0 +1,92 @@
|
||||
# 连续打击与伤害叠加设计 (Consecutive Hits & Damage Stacking)
|
||||
|
||||
## 需求分析
|
||||
用户希望实现“对同一个目标,同一种伤害在一定间隔内认定为持续多次伤害,从而可以制作伤害叠加”。
|
||||
|
||||
这通常被称为 **“连击系统 (Combo System)”** 或 **“弱点叠加 (Vulnerability Stacking)”**。
|
||||
与 DoT(中毒/燃烧)不同,这种机制侧重于 **“攻击频率”** 和 **“爆发奖励”**。
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 1. 连击窗口 (Combo Window)
|
||||
* 每个敌人对每种伤害来源(或类型)维护一个 `LastHitTime` 和 `StackCount`。
|
||||
* 如果在 `WindowDuration` (例如 2秒) 内再次受到同类伤害:`StackCount++`, 刷新 `LastHitTime`。
|
||||
* 如果超时:`StackCount` 重置为 0。
|
||||
|
||||
### 2. 叠加收益 (Stacking Benefits)
|
||||
当 `StackCount` 增加时,可以产生以下效果:
|
||||
* **Damage Scaling**: 第 N 次伤害提高 `N * 10%`。
|
||||
* **Proc Trigger**: 每第 3 次攻击触发一次爆炸 (3-Hit Passive)。
|
||||
* **State Transition**: 叠满 5 层后,施加“破甲”或“眩晕”。
|
||||
|
||||
## 数据结构设计
|
||||
|
||||
在现有的 `EnemyManager` 混合 ECS 中,我们需要拓展 `StatusContext` 或新增 `ComboContext`。
|
||||
|
||||
由于连击通常与特定的“来源”或“伤害标签”绑定,建议存放在 `StatusContext` 中的专用字段,或者复用 Status 机制。
|
||||
|
||||
### 方案 A: 专用 Combo Tracker
|
||||
在 `EnemyEntityManager` 的 Context 中增加:
|
||||
```gdscript
|
||||
# combo_tracker.gd — 通过 ObjectPool 复用,避免 GC 开销
|
||||
class_name ComboTracker
|
||||
extends RefCounted
|
||||
|
||||
var source_id: int = -1 # 来源实体 ID(支持多武器/多玩家,-1 表示无效)
|
||||
var damage_tag: int = 0 # 伤害类型位掩码(PHYSICAL=1, FIRE=2, LASER=4 ...)
|
||||
var count: int = 0 # 当前连击层数
|
||||
var timer: float = 0.0 # 剩余窗口时间(秒)
|
||||
|
||||
func reset() -> void:
|
||||
source_id = -1
|
||||
damage_tag = 0
|
||||
count = 0
|
||||
timer = 0.0
|
||||
```
|
||||
|
||||
### 方案 B: 利用现有 Status 系统
|
||||
定义一种特殊的 StatusType `COMBO_MARK`。
|
||||
* 每次攻击施加 `COMBO_MARK`。
|
||||
* `addEffect` 逻辑中,如果已存在,则 `intensity++` (层数) 并刷新 `duration`。
|
||||
* 伤害计算时,检查 `COMBO_MARK` 的 `intensity` 来加成伤害。
|
||||
|
||||
**✅ 追加选择:推荐方案 B**,因为它复用了现有的 Tick 和合并逻辑,不需要写额外的 Timer。方案 A 适合需要多来源(`source_id`)区分的场景(如多玩家共同攻击同一目标)。
|
||||
|
||||
> **R4-C3 补充(COMBO_MARK 的 StatusTypeDef 参数)**:`COMBO_MARK` 对应以下 `StatusTypeDef` 配置:
|
||||
> - `stack_mode = 1`(**Intensity 强度叠加**):每次施加 +1 层,`intensity` 字段记录当前层数。
|
||||
> - `max_stacks = 0`(**0 = 无限叠加**,不设层数上限;由高频攻击自然封顶)。
|
||||
> - `tick_interval`:⚠️ **语义说明(P6-N14 修正)**:`StatusTypeDef.tick_interval` 在 `StatusManager` 的通用实现中是"DoT 伤害跳字间隔"。但 `COMBO_MARK` **不造成 DoT 伤害**,其 `_apply_dot_tick()` 回调被设计为空操作(`pass`)。
|
||||
> 此处 `tick_interval` 被 COMBO_MARK **重载为连击窗口刷新机制**:每次施加 COMBO_MARK 时,调用方手动将 `remaining_duration` 重置为连击窗口时长(如 2.0s),利用 `StatusManager` 的 duration 计时器实现超时重置。
|
||||
> **推荐实践**:为避免混淆,`COMBO_MARK` 的 `tick_interval` 设置为一个大于连击窗口的值(如 `999.0`),确保在窗口内 `tick_accumulator` 永不达到触发阈值,彻底避免意外的 DoT 跳字。
|
||||
> **长期方案(P2)**:为 `StatusTypeDef` 增加 `is_combo_tracker: bool` 字段,从根本上区分"持续伤害状态"和"计数追踪状态",使 `StatusManager._apply_dot_tick` 在 `is_combo_tracker = true` 时直接跳过。
|
||||
> - `can_catalyze = []`(空数组):连击标记本身不触发元素反应,只作为伤害加成计算输入。
|
||||
> `StatusTypeDef.can_catalyze` 字段类型为 `Array[int]`,必须赋空数组 `[]`,
|
||||
> 禁止赋值 `false`(GDScript 4 严格类型模式下触发 bool→Array 类型错误)。
|
||||
> 详见 `combat_mechanics_depth.md §3.0`(StatusTypeDef 完整字段)。
|
||||
|
||||
## 实现逻辑 (基于 Status 系统)
|
||||
|
||||
1. **定义新状态**: `StatusID.VULNERABILITY` (易伤标记)。
|
||||
> 所有状态引用使用 `StatusID` Autoload 常量(架构为数据驱动整数 ID,不存在 StatusType 枚举):
|
||||
> - `StatusID.VULNERABILITY = 8`(在 status_id.gd 中登记)
|
||||
> - `StatusID.COMBO_MARK = 7`(已定义)
|
||||
> 禁止写法:`StatusType.VULNERABILITY`、`StatusType.COMBO_MARK`。
|
||||
2. **施加规则**:
|
||||
* 每次 `ActionLaser` (高频攻击) 命中时,施加 1 层 `StatusID.VULNERABILITY`,持续 0.5秒。
|
||||
3. **叠加规则**:
|
||||
* 在 `StatusContext.addEffect` 中,如果发现已有 `StatusID.VULNERABILITY`,则 `stack++`。
|
||||
4. **伤害修正**:
|
||||
* 在 `onApplyDamage` 中,计算最终伤害前,读取目标的 `StatusID.VULNERABILITY` 层数。
|
||||
* `FinalDamage = BaseDamage * (1 + Stack * 0.05)` (每层增伤 5%)。
|
||||
* 由于激光每秒 10 次,0.5秒内能叠 5 层,伤害会越来越高。
|
||||
|
||||
## 扩展玩法示例
|
||||
* **聚焦激光 (Focus Laser)**: 初始伤害低,但对同一目标持续照射越久伤害越高(利用 0.1s 间隔的刷新机制)。
|
||||
* **三环被动 (Three-Hit Passive)**: 类似于 LOL 的薇恩。给敌人挂一个隐藏的 `StackStatus`,当 Stack 到达 3 时,消耗所有 Stack 并造成由最大生命值决定的真实伤害。
|
||||
|
||||
## 代码落地计划
|
||||
1. 在 `status_id.gd`(StatusID Autoload)中追加 `VULNERABILITY: int = 8` 常量。
|
||||
`COMBO_MARK: int = 7` 已在 `advanced_mechanics_summons_and_environment.md §3.2` 定义。
|
||||
禁止在此文件外使用裸整数或 StatusType 枚举(见 ADR-R5-N2 规范)。
|
||||
2. 在 `EnemyManager.gd` 的 `apply_damage` 函数中,计算扎血前先检查 `StatusID.VULNERABILITY` 进行倍率修正。
|
||||
3. 在 `StatusManager.gd` 的 `apply_status` 函数中,确保此类型状态可正确无限叠加层数(或设置上限)。
|
||||
@@ -0,0 +1,112 @@
|
||||
# 武器系统扩展设计方案 (Weapon System Expansion Design)
|
||||
|
||||
本文档旨在描述基于现有的 **HMWS (Hackable Modular Weapon System)** 和 **ECS BulletManager** 架构,可以进一步扩展的玩法机制。
|
||||
|
||||
当前的系统已经支持了基础的投射物、霰弹、激光,以及修正器(多重施法、弹射、追踪、穿透、连锁、冰冻)。在此基础上,我们可以引入更高级的策略组合和“涌现式”玩法。
|
||||
|
||||
## 1. 轨迹修正 (Trajectory Modifiers)
|
||||
|
||||
通过修改子弹在 `_physics_process` 中的运动逻辑,创造独特的弹道体验。
|
||||
|
||||
### 1.1 正弦/波浪弹道 (Sine/Wave Pattern)
|
||||
* **机制**: 子弹不再直线飞行,而是沿着正弦波轨迹运动。
|
||||
* **实现**:
|
||||
* 新增 `ModifierSine`.
|
||||
* 在 `BulletManager` 的 Data Stride 中增加 `waveFrequency` 和 `waveAmplitude` (或复用 `customData`)。
|
||||
* 在 `_physics_process` 积分位置时,叠加垂直于速度方向的偏移量。
|
||||
* **玩法**: 直线命中率低,但能绕过正面护盾,或者覆盖更宽的横向区域。
|
||||
|
||||
### 1.2 回旋镖 (Boomerang)
|
||||
* **机制**: 子弹飞出一定距离后,反向加速飞回玩家。
|
||||
* **实现**:
|
||||
* 新增 `ModifierBoomerang`.
|
||||
* 在 `BulletManager` 中逻辑:当 `lifetime` 经过一半时,给予一个反向的加速度 `Acceleration` 指向玩家。
|
||||
* **玩法**: 高风险高回报,利用回程的子弹对追击的敌人造成“背刺”双倍伤害。
|
||||
|
||||
### 1.3 轨道环绕 (Orbiting/Shield)
|
||||
* **机制**: 子弹不飞远,而是围绕玩家旋转,形成护盾。
|
||||
* **实现**:
|
||||
* 轨道参数 `is_orbiting: bool`、`orbit_radius: float`、`orbit_angle: float`、`orbit_speed: float` 均存入 `_bullet_contexts: Dictionary`(**冷数据字段,P6-N9 权威定义**),BulletManager SoA 热数组不新增字段。
|
||||
* **双写优化(P6-N15 规范)**:环绕子弹的位置由圆周公式 100% 决定,不依赖速度积分。因此在 `BulletManager._physics_process` 中,**必须先判断 is_orbiting,再决定是否执行常规物理积分**:
|
||||
```gdscript
|
||||
# BulletManager._physics_process 热路径(伪代码)
|
||||
for i in _active_count:
|
||||
var ctx = _bullet_contexts.get(i, null)
|
||||
if ctx and ctx.get(“is_orbiting”, false):
|
||||
# 跳过速度积分,直接覆写位置(避免无意义的双写开销)
|
||||
var angle: float = ctx.orbit_angle + ctx.orbit_speed * delta
|
||||
ctx.orbit_angle = fmod(angle, TAU)
|
||||
var px := _player_x + cos(ctx.orbit_angle) * ctx.orbit_radius
|
||||
var py := _player_y + sin(ctx.orbit_angle) * ctx.orbit_radius
|
||||
_data[i * BULLET_STRIDE + 0] = px
|
||||
_data[i * BULLET_STRIDE + 1] = py
|
||||
# vx/vy 保持 0,lifetime 正常递减
|
||||
else:
|
||||
# 常规物理积分(速度 + 加速度)
|
||||
_data[i * BULLET_STRIDE + 0] += _data[i * BULLET_STRIDE + 2] * delta
|
||||
_data[i * BULLET_STRIDE + 1] += _data[i * BULLET_STRIDE + 3] * delta
|
||||
```
|
||||
* **性能评估**:环绕子弹数量通常不超过 20 颗(护盾构建),`is_orbiting` 冷数据查询 20 次/帧可接受;若未来出现大量环绕子弹需求,再评估是否升居热数组(SoA 增加 1 bit flag)。
|
||||
* **玩法**: 仅近战流派,配合”击退”修正器保命。
|
||||
|
||||
## 2. 触发器系统 (Trigger System) - "Noita" 风格的核心
|
||||
|
||||
这是 roguelike 构建深度玩法的关键,即“子弹生子弹”。
|
||||
|
||||
### 2.1 击中施法 (Cast On Hit)
|
||||
* **机制**: 当母弹击中敌人时,以击中点为原点,释放另一个法术。
|
||||
* **案例**: "毒箭" + "击中施法" + "爆炸" = 毒箭射中敌人后产生爆炸。
|
||||
* **架构实现**(✅ 已完成):
|
||||
`ProjectileDef.on_hit_payload_id`(`int`,-1=无触发)存储 `SubPayloadRegistry` 中预注册的子法术 ID。
|
||||
`SpellEvaluator` 预编译时将 TRIGGER 节点后续法术打包为 SubPayload 并注册到表;
|
||||
`BulletManager` 在 `_on_bullet_hit()` 回调中查表、通过 `SpellEvaluator.execute_sub()` 执行。
|
||||
弹体本身仅携带整数 ID,无需存储复杂对象引用,完全兼容 SoA PackedFloat32Array 布局。
|
||||
详见 `architecture_design.md §3.2.A`(ProjectileDef 完整字段)与 `implementation_plan.md §2.2`(SubPayloadRegistry)。
|
||||
|
||||
### 2.2 死亡/超时施法 (Cast On Death/Timer)
|
||||
* **机制**:
|
||||
* **On Timer**: 子弹飞行亦段时间后,变成另一种子弹(或释放子弹)。例如:集束手雷(飞行 -> 分裂成小炸弹)。
|
||||
* **On Death**: 子弹消失/撞墙时触发。
|
||||
* **玩法**: 延迟伤害、陷阱布局。
|
||||
|
||||
## 3. 元素交互 (Elemental Synergy)
|
||||
|
||||
扩展现有的 `Ice` (冰冻) 机制,引入状态反应。
|
||||
|
||||
### 3.1 状态定义
|
||||
* **冰 (Wet/Freeze)**: 减速。
|
||||
* **火 (Burn)**: 持续扣血 (DoT)。
|
||||
* **毒 (Poison)**: 持续扣血 + 易伤 (受到的伤害增加)。
|
||||
* **雷 (Shock)**: 间歇性打断动作。
|
||||
|
||||
### 3.2 组合反应 (Combo)
|
||||
* **热休克 (Fire + Ice)**: 解除冰冻,但在解除瞬间造成一次巨额爆发伤害 (Steam Burst)。
|
||||
* **导电 (Water/Ice + Lightning)**: 如果敌人处于冰冻/潮湿状态,被雷电击中时,触发大范围 AoE 连锁伤害。
|
||||
* **毒爆 (Poison + Fire)**: 毒气被点燃,移除中毒状态,产生一次性范围爆炸。
|
||||
|
||||
## 4. 或者是召唤物 (Summons)
|
||||
|
||||
将 `ActionNode` 的产物从子弹变为独立的实体单位。
|
||||
|
||||
### 4.1 炮台 (Turret)
|
||||
* **机制**: 在玩家位置放置一个静止单位,它拥有独立的射击逻辑(自动瞄准最近敌人)。
|
||||
* **实现**: 需要新增 `TurretManager` 或在 `EnemyManager` 扩展一个 "Friendly" 阵营。
|
||||
* **玩法**: 塔防流,玩家只负责跑位和放置。
|
||||
|
||||
### 4.2 浮游炮 (Funnel/Bit)
|
||||
* **机制**: 跟随玩家移动的无敌单位,复制玩家的射击行为(类似《沙罗曼蛇》的 Option)。
|
||||
|
||||
## 5. 开发路线图建议
|
||||
|
||||
1. **Phase 1 (Easy)**: 实现 **ModifierSize** (巨大化) 和 **ModifierSplit** (击中分裂 - 简易版 Trigger)。这两个只需要修改 `BulletManager` 的数值逻辑,无需重构 ECS。
|
||||
2. **Phase 2 (Medium)**: 实现 **状态交互 (Elemental)**。完善 `EnemyManager` 的状态槽位(目前只有 `freezeTimer`,可以用位运算扩展状态标记)。
|
||||
3. **Phase 3 (Hard)**: 实现 **Trigger System**。✅ 架构已在 `implementation_plan.md §2.2` 完成(SubPayloadRegistry + on_hit_payload_id 方案)。本阶段实施重点:编写多层 TRIGGER 嵌套的单元测试,接入 VFXManager 命中回调。
|
||||
|
||||
## 6. 数值与平衡参考
|
||||
|
||||
| 扩展类型 | 开发难度 | 趣味增益 | 性能消耗 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 正弦弹道 | 低 | 中 | 极低 |
|
||||
| 击中分裂 | 中 | 高 | 高 (子弹数指数增长) |
|
||||
| 元素反应 | 中 | 高 | 低 |
|
||||
| 召唤炮台 | 高 | 中 | 中 |
|
||||
@@ -0,0 +1,184 @@
|
||||
# 平台发行认证清单 (Platform Certification Checklist)
|
||||
|
||||
> **用途**:列出 Steam / Nintendo Switch 两个目标平台的强制通过项,并映射到对应开发切片。
|
||||
> 每项完成后在 `[ ]` 处打勾,并记录验证者与日期。
|
||||
> **规则**:所有 P0 项必须在 S6 发布前全部完成,P1 项视平台要求决定是否阻断发布。
|
||||
|
||||
---
|
||||
|
||||
## 一、Steam (PC) 必须项
|
||||
|
||||
### 1.1 隐私与数据合规(EU GDPR / Steam 要求)
|
||||
|
||||
| 编号 | 检查项 | 切片 | 状态 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| ST-01 | 游戏不收集任何可识别玩家身份的数据(无账号系统、无上报遥测) | S2 | ⬜ |
|
||||
| ST-02 | 崩溃日志(`user://crash_log.txt`)不包含 Steam ID / 机器码等 PII | S2 | ⬜ |
|
||||
| ST-03 | 设置菜单提供「清除所有本地数据」选项(删除 `user://` 下所有文件) | S6 | ✅ 2026-06-05(`SettingsManager.clear_all_local_data`,二次确认;实测删除 save_data/endless_records/run_a·b/runs)|
|
||||
| ST-04 | Steam 商店页包含隐私政策链接(即使只写"本游戏不收集任何数据") | S6 前置 | ⬜ |
|
||||
| ST-05 | 如启用 opt-in 崩溃上报,玩家首次启动须明确同意 | S6(可选)| ⬜ |
|
||||
|
||||
> **ST-03 实现指引**:
|
||||
> `SettingsManager.clear_all_local_data()` 遍历删除:
|
||||
> `user://crash_log.txt`、`user://crash_log_prev.txt`、`user://run_a.json`、
|
||||
> `user://run_b.json`、`user://endless_records.json`、`user://save_data.json`、
|
||||
> `user://runs/`(截图目录)。调用后重置 `ProfileManager` 单例状态并返回主菜单。
|
||||
|
||||
### 1.2 Steam 成就 (Achievements)
|
||||
|
||||
| 编号 | 检查项 | 切片 | 状态 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| ST-10 | Steam API 初始化成功(`Steam.init()` 返回 OK,或使用 GodotSteam 插件) | S6 | ⬜ |
|
||||
| ST-11 | 所有成就在 Steamworks 后台已定义并与 EventBus 钩子对应 | S6 | ⬜ |
|
||||
| ST-12 | 离线模式下成就解锁不崩溃(本地缓存后联网再同步) | S6 | ⬜ |
|
||||
| ST-13 | 成就解锁调用幂等(重复触发不报错、不重复计数) | S6 | ⬜ |
|
||||
|
||||
> **EventBus 成就钩子**:`EventID.ACHIEVEMENT_UNLOCKED` **固定为 ID `18`**(负载:`{ "achievement_id": String }`),权威定义见 **`technical/implementation_plan.md` §2.1** 事件目录与 `event_ids.gd` 常量。
|
||||
> `AchievementManager` 订阅此事件,调用 `Steam.set_achievement(achievement_id)` + `Steam.store_stats()`(须幂等,见 ST-13)。
|
||||
|
||||
### 1.3 Steam 输入与手柄
|
||||
|
||||
| 编号 | 检查项 | 切片 | 状态 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| ST-20 | 手柄(Xbox / PS / Switch Pro)基本功能可用 | S1 | ⬜ |
|
||||
| ST-21 | 手柄连接/断开时游戏不崩溃,自动切换输入方案 | S4 | ⬜ |
|
||||
| ST-22 | 按键提示图标根据当前设备自动切换(Xbox A / PS × / 键盘 Space) | S6 | ⬜ |
|
||||
|
||||
### 1.4 Steam Deck 兼容性
|
||||
|
||||
| 编号 | 检查项 | 切片 | 状态 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| ST-30 | 游戏在 Steam Deck 默认分辨率(1280×800)下可正常显示 | S6 | ⬜ |
|
||||
| ST-31 | 所有 UI 文字在 Steam Deck 屏幕上可读(最小字号 ≥ 16px)| S6 | ⬜ |
|
||||
| ST-32 | Steam Deck 触摸屏操作不引起误触崩溃 | S6 | ⬜ |
|
||||
| ST-33 | Proton 兼容性验证(在 SteamOS 下通过 Proton 层运行测试) | S6 | ⬜ |
|
||||
|
||||
### 1.5 排行榜数据完整性
|
||||
|
||||
| 编号 | 检查项 | 切片 | 状态 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| ST-40 | 本地 `endless_records.json` 写入时附加 HMAC-SHA256 签名 | S5 | ✅ 2026-06-05(`endless_records.gd`,对精确 JSON 字符串签名)|
|
||||
| ST-41 | 读取时验证签名,签名失败则标记分数为「未验证」并拒绝提交 Steam 排行榜 | S5 | ✅ 2026-06-05(验签失败 `push_warning`+返回空,实测篡改拒绝)|
|
||||
| ST-42 | Steam 排行榜提交时服务端异常(网络断开)不崩溃,本地缓存待下次提交 | S6 | ⬜ |
|
||||
|
||||
> **ST-40 HMAC 实现指引(C-4 修复)**:
|
||||
> ```gdscript
|
||||
> # endless_records_manager.gd
|
||||
> # HMAC Key 存于 GDScript 编译常量(不含在存档内),对抗普通玩家手动编辑
|
||||
> # 注意:本地 HMAC 无法对抗逆向工程,主要目的是防止随意手改
|
||||
> const _HMAC_KEY: PackedByteArray = [0xA3, 0x7F, 0x2C, ...] # S5 前生成 32 字节随机密钥
|
||||
>
|
||||
> func _sign(data: String) -> String:
|
||||
> var crypto := Crypto.new()
|
||||
> var hmac := crypto.hmac_digest(HashingContext.HASH_SHA256, _HMAC_KEY,
|
||||
> data.to_utf8_buffer())
|
||||
> return hmac.hex_encode()
|
||||
>
|
||||
> func save_records(records: Array) -> void:
|
||||
> var payload := JSON.stringify(records)
|
||||
> var signed := { "data": records, "sig": _sign(payload) }
|
||||
> FileAccess.open("user://endless_records.json", FileAccess.WRITE
|
||||
> ).store_string(JSON.stringify(signed))
|
||||
>
|
||||
> func load_records() -> Array:
|
||||
> if not FileAccess.file_exists("user://endless_records.json"): return []
|
||||
> var raw := JSON.parse_string(
|
||||
> FileAccess.open("user://endless_records.json",
|
||||
> FileAccess.READ).get_as_text())
|
||||
> if not raw is Dictionary: return []
|
||||
> var expected := _sign(JSON.stringify(raw.get("data", [])))
|
||||
> if raw.get("sig", "") != expected:
|
||||
> push_warning("EndlessRecords: 签名验证失败,分数不可信")
|
||||
> return [] # 拒绝载入被篡改的数据
|
||||
> return raw.get("data", [])
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
### 1.6 Steam 云存档 (Steam Cloud Save)
|
||||
|
||||
| 编号 | 检查项 | 切片 | 状态 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| ST-50 | `ProfileManager.save_run()` / `save_settings()` 通过 `Steam.beginFileWriteBatch()` / `Steam.endFileWriteBatch()` 写入,确保 Steam Cloud 同步 | S6 | ⬜ |
|
||||
| ST-51 | `user://run_a.json`、`user://run_b.json`、`user://save_data.json` 均在 Steamworks 后台 Remote Storage 中启用 | S6 前置 | ⬜ |
|
||||
| ST-52 | 跨设备(PC → Steam Deck)存档同步测试:PC 端存档可在 Steam Deck 读取并继续游戏 | S6 | ⬜ |
|
||||
| ST-53 | Steam Cloud 配额不超标(Steam 默认 100MB/游戏;所有存档文件估算总量 < 1MB) | S6 | ⬜ |
|
||||
|
||||
> **ST-50 实现指引**:
|
||||
> ```gdscript
|
||||
> # profile_manager.gd
|
||||
> func save_run(data: Dictionary) -> void:
|
||||
> if Steam.is_steam_running():
|
||||
> Steam.beginFileWriteBatch() # GodotSteam: 开始批量写入,触发 Steam Cloud 同步
|
||||
> var which := get_int("run_write_slot", 0)
|
||||
> var path := _SLOT_A if which == 0 else _SLOT_B
|
||||
> var f := FileAccess.open(path, FileAccess.WRITE)
|
||||
> if f:
|
||||
> f.store_string(JSON.stringify(data))
|
||||
> if Steam.is_steam_running():
|
||||
> Steam.endFileWriteBatch() # 结束批量写入,触发 Steam Cloud 上传
|
||||
> set_int("run_write_slot", 1 - which)
|
||||
> ```
|
||||
> **注意**:`beginFileWriteBatch` / `endFileWriteBatch` 仅在 `Steam.is_steam_running()` 时调用;离线模式下正常写本地文件,不崩溃。`Steam.is_steam_running()` 需 GodotSteam 插件支持。
|
||||
|
||||
---
|
||||
|
||||
## 二、Nintendo Switch(未来扩展)
|
||||
|
||||
> 以下为 Nintendo Lotcheck 最常见的阻断项,仅在立项 Switch 移植时启用。
|
||||
|
||||
### 2.1 系统级强制项
|
||||
|
||||
| 编号 | 检查项 | 优先度 |
|
||||
| :--- | :--- | :--- |
|
||||
| SW-01 | 游戏内按钮提示使用 Switch 图标(A/B/X/Y),不得显示 Xbox 图标 | P0 |
|
||||
| SW-02 | Joy-Con 横持模式不崩溃(若不支持须在商品页说明) | P0 |
|
||||
| SW-03 | 游戏可从睡眠状态恢复(`NOTIFICATION_APPLICATION_PAUSED` 处理) | P0 |
|
||||
| SW-04 | 存档容量不超过 Switch 游戏卡限制(存档 < 32MB) | P0 |
|
||||
| SW-05 | 不在联机功能中收集 Nintendo Account 信息 | P0 |
|
||||
| SW-06 | Logo & Rating(CERO / PEGI / ESRB)在正确位置显示 | P0 |
|
||||
| SW-07 | 游戏内截图功能不包含版权保护内容 | P1 |
|
||||
|
||||
### 2.2 性能要求
|
||||
|
||||
| 编号 | 检查项 | 优先度 |
|
||||
| :--- | :--- | :--- |
|
||||
| SW-10 | 掌机模式(720p)稳定 30fps(Switch 性能下限) | P0 |
|
||||
| SW-11 | 内存使用峰值 < 2.5GB(Switch 总 RAM 4GB 共享系统/GPU) | P0 |
|
||||
| SW-12 | 存档读写不阻塞主线程超过 100ms | P0 |
|
||||
|
||||
---
|
||||
|
||||
## 三、无障碍合规(Accessibility — 跨平台必须项)
|
||||
|
||||
> 参见 `architecture_design.md ADR-C1` 获取完整技术实现规范。
|
||||
|
||||
| 编号 | 检查项 | 切片 | 状态 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| AC-01 | 元素标签同时用颜色 + 形状图标标识(色盲玩家可区分) | S3 | ⬜ |
|
||||
| AC-02 | 所有 UI 字体支持缩放(50%~150%,设置项) | S4 | ⬜ |
|
||||
| AC-03 | 高对比度模式(黑底白字选项)| S6 | ⬜ |
|
||||
| AC-04 | 所有 VFX 提供「减少闪光」选项(防光敏性癫痫)| S6 | ⬜ |
|
||||
| AC-05 | 游戏内所有音效有对应视觉提示(不依赖纯音频传达关键信息) | S6 | ⬜ |
|
||||
|
||||
---
|
||||
|
||||
## 四、评级申报
|
||||
|
||||
| 平台 | 机构 | 提交时机 | 状态 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Steam | IARC(自评系统,Steam 自动申报) | S6 发布前 | ⬜ |
|
||||
| Steam(欧洲) | PEGI 自评或正式申请 | S6 发布前 | ⬜ |
|
||||
| Switch | CERO(日本)/ PEGI / ESRB(需提前约 8 周提交) | 若立项 Switch | ⬜ |
|
||||
|
||||
---
|
||||
|
||||
## 五、发布前最终清单
|
||||
|
||||
| 编号 | 检查项 | 完成标志 |
|
||||
| :--- | :--- | :--- |
|
||||
| PRE-01 | Steam 商店页(截图 × 5、宣传片、描述文字)审核通过 | Steam 后台审核 OK |
|
||||
| PRE-02 | 版本号 `1.0.0` 打 git tag,构建哈希记录在 `CHANGELOG.md` | git tag v1.0.0 |
|
||||
| PRE-03 | 所有 P0 认证项状态为 ✅ | 本文档所有 P0 行 ✅ |
|
||||
| PRE-04 | 发布分支通过完整 Wave 1~20 通关测试(无崩溃) | CI 通关测试日志 |
|
||||
| PRE-05 | `crash_reporter.gd` 已注册为首位 Autoload,日志路径可写 | project.godot 验证 |
|
||||
@@ -0,0 +1,592 @@
|
||||
# 魔法工匠:骨架垂直切片开发计划
|
||||
# Arcane Artificer — Skeleton Vertical Slice Development Plan
|
||||
|
||||
> **策略定义**:骨架垂直切片 (Skeleton Vertical Slice) 不是"先把所有系统建完再组合"(横向分层),而是先用最薄的一刀从**输入到渲染切穿所有层**,证明核心体验可运行;再每轮迭代让这刀"更宽"——加入更多内容与深度。
|
||||
>
|
||||
> - **骨架**:最小可运行的端到端技术脚手架,不含游戏内容
|
||||
> - **垂直切片**:每个里程碑均可独立游玩与测试,交付"小但可玩的游戏"
|
||||
> - **顺序原则**:①风险最高的系统最先实现;②每个切片回答一个明确的"能做到吗"问题;③下一个切片依赖上一个切片的通过
|
||||
|
||||
---
|
||||
|
||||
## 一、技术风险登记表 (Risk Register)
|
||||
|
||||
驱动切片顺序的核心不确定性:
|
||||
|
||||
| 风险 ID | 风险描述 | 影响系统 | 严重度 | 解决切片 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| R-01 | `PackedFloat32Array` SoA 在 GDScript 下 2000 弹幕能否达到 60fps | BulletManager | 🟢 已缓解 | S0 ✅ |
|
||||
| R-02 | SpellEvaluator 的 while 循环与 SubPayloadRegistry 在高频施法下无 GC 抖动 | SpellEvaluator | 🟢 已缓解 | S0~S1 ✅ |
|
||||
| R-03 | 1000 敌人的 Boid AI + SpatialGrid 查询帧时间是否可控 | EnemyManager | 🟢 已缓解 | S0 ✅ |
|
||||
| R-04 | Area2D 信号回调是否足够处理 500-2000 弹幕碰撞(切换阈值验证) | BulletManager/碰撞 | 🟡 部分 | S1 ✅(当前用 SpatialGrid;完整阈值验收留 S6) |
|
||||
| R-05 | `compile_wand` 预编译后 `CompiledDeck` 与 `SubPayloadRegistry` 竞争条件实测 | SpellEvaluator | 🟢 已缓解 | S2~S3 ✅ |
|
||||
| R-06 | CIRCUIT 拓扑 `_flatten_circuit` + 嵌套 LOGIC_FORK 实际运行正确性 | SpellEvaluator | 🟡 中 | S5 |
|
||||
| R-07 | `GPUParticles2D` 池化(`VFXManager`)与 MultiMesh 渲染切换性能实测 | VFXManager/渲染 | 🟡 部分 | S4 ✅(色块占位池化;粒子素材与 MultiMesh 留 S6) |
|
||||
| R-08 | GDScript ↔ C# 跨语言边界开销实测:`PackedFloat32Array.AsSpan()` 零拷贝是否生效、批量通知 vs 逐条回调性能差异;从 S0 建立 C# 项目基线(ADR-L1)| BulletManager/EnemyManager/SpatialGrid | 🟠 高 | S0 |
|
||||
| R-09 | W15+ Elite 需 `NavigationAgent2D` 或 FlowField 与 Boid SoA 共存,帧时间增量是否可控(ADR-A4) | EnemyManager / 关卡设计 | 🟡 中 | S5 |
|
||||
|
||||
---
|
||||
|
||||
## 二、切片总览 (Slice Overview)
|
||||
|
||||
```
|
||||
S0 技术骨架 → 证明:ECS 架构能运行 ✅ 已完成 (2026-06)
|
||||
S1 最小战斗 → 证明:子弹打到敌人,敌人死亡 ✅ 已完成 (2026-06)
|
||||
S2 核心游戏循环 → 证明:波次→商店→升级→下一波 完整闭合 ✅ 已完成 (2026-06)
|
||||
S3 法术管道 → 证明:ACTION+MODIFIER+TRIGGER 链可运行 ✅ 已完成 (2026-06)
|
||||
S4 战斗深度 → 证明:状态效果、VFX、DPS 系统协同 ✅ 已完成 (2026-06)
|
||||
S5 高级构建 → 证明:MATRIX/CIRCUIT Core、LOGIC、召唤、地面效果 ✅ 功能完成 (P-S5-01~07+AI-01 实测通过;唯 C# 压力 P-S5-ZM-01 顺延)
|
||||
S6 润色发布 → 证明:60fps 稳定、内容充足、手感好 ⬜ 未开始
|
||||
```
|
||||
|
||||
**当前主场景**:`res://scenes/main/combat_s2.tscn`(S2 起)
|
||||
|
||||
> 每个切片结束前,下列问题必须回答"是"才可进入下一个切片(出口检查 Exit Gate)。
|
||||
|
||||
### 实施进度摘要
|
||||
|
||||
| 切片 | 状态 | 主场景 / 入口 | 备注 |
|
||||
| :--- | :---: | :--- | :--- |
|
||||
| S0 | ✅ | `combat_test.tscn` | GDScript SoA 回退路径实测通过;`architecture_design.md §4.5` 已填 S0 实测值(Bullet 0.37ms / Enemy 0.23ms) |
|
||||
| S1 | ✅ | `combat_test.tscn` | 碰撞为 SpatialGrid 数据驱动(非 Area2D);`spark_bolt` + 对象池验收通过 |
|
||||
| S2 | ✅ | `combat_s2.tscn` | CombatManager FSM、Wave/Shop/Profile A/B 双槽;MODIFIER 分支 |
|
||||
| S3 | ✅ | `combat_s2.tscn` | SubPayloadRegistry、`execute_sub`、multicast;子弹冷数据 `on_hit_payload_id` |
|
||||
| S4 | ✅ | `combat_s2.tscn` | StatusManager / VFXManager / DpsTracker;`fire_bolt`/`poison_dart`;蓄力边沿 |
|
||||
| S5 | ✅* | `combat_s2.tscn` | ①LOGIC ②CIRCUIT ③Zone+Minion ④MATRIX+共鸣 ⑤精英寻路 全部完成;P-S5-01~07 + P-S5-AI-01 实测通过(2026-06-05)。*唯 P-S5-ZM-01(C# 压力)按 R-08 顺延 |
|
||||
| S6 | ⬜ | — | — |
|
||||
|
||||
> **⚠️ 实现方式偏差备忘(代码核对 2026-06-05,全切片适用)**
|
||||
> 1. **纯数据驱动架构全面落地 ✅(2026-06-05)**:**游戏设计器 EditorPlugin**(`addons/game_designer/`,编辑器底部面板,**7 个可视化分页**:法术/敌人/波次/Core/共鸣/状态/平衡)+ `res://data/*.json`(7 个数据文件),策划/美术/开发零代码配置全部游戏内容。**已移除所有硬编码内容副本与回退**——JSON 是唯一权威源,缺失/格式错误时 `push_error` 明确报错(不静默)。各 Manager 纯加载:SpellRegistry←spells.json(20法术)、EnemyManager←enemies.json(6原型)、WaveManager←waves.json(20波)、WandPreset←cores.json(Core)、SpellEvaluator←resonance.json(共鸣)、StatusRegistry←status_effects.json(状态)、SettingsManager←balance.json(乘子+Boss血)。`WandPreset` 删除全部 `make_spell_*`/硬编码 Core;`SpellRegistry` 删 `.tres`扫描+builtins;`ShopManager` 删 fallback+过滤 shop_cost>0。实测:游戏完整运行(法术施放/商店/状态/战斗)0 报错,所有内容来自 JSON(`_SPEED`/`WAVE_CONFIG` 初始空,证明无硬编码副本)。文档 `handbook/07_game_designer.md`。**余项**:运行时热重载(目前需 F5 重启)、玩法常量配置(玩家速度/商店价/掉落等仍为代码常量,属可调参数非内容)。
|
||||
> 2. **UI 全程序化构建**:`shop.tscn`/`hud.tscn`/`inventory.tscn`/`hud_dps.tscn` **均不存在**;HUD 与商店在 `scenes/main/combat_s2.gd` 内用 `Label.new()`/`Button.new()` 动态生成。
|
||||
> 3. **S3 拖拽背包 UI(`inventory.tscn`)缺失**:法术目前仅能经商店点击购买,无拖拽装配。
|
||||
> 4. **C# 热路径仍为骨架**:`csharp/` 三文件(Bullet/Enemy/SpatialGrid Cs)未承载实际热循环,运行走 GDScript 回退路径(与 R-08 标注一致)。
|
||||
|
||||
---
|
||||
|
||||
## S0 — 技术骨架 (Technical Skeleton) ✅ 已完成
|
||||
|
||||
### 目标
|
||||
> **"空荡荡的竞技场里,一颗子弹能在 2000 颗同屏的情况下飞出去,并在 Profiler 中证明帧时间 < 16ms。"**
|
||||
|
||||
技术上没有游戏性,只有数据流动与性能验证。
|
||||
|
||||
### 范围
|
||||
|
||||
| 要实现的系统 | 最小实现内容 | 暂不实现 |
|
||||
| :--- | :--- | :--- |
|
||||
| `EventBus` / `EventID` | 整数 ID 常量 + 简单 emit/subscribe | 事件类型校验、跨语言边界 |
|
||||
| `BulletManager` | PackedFloat32Array SoA (STRIDE=12)、spawn/despawn、匀速位移积分、swap-and-pop 删除 | 碰撞、归航、弹射、VFX |
|
||||
| `EnemyManager` | SoA (STRIDE=8)、spawn/despawn、直线追玩家移动、`_visible_flags` LOD;屏外低频 Boid 分离力(`_update_separation_only`,`OFFSCREEN_SEPARATION_INTERVAL=10`,P6-N1) | AI 行为状态机、攻击、死亡动画 |
|
||||
| `SpatialGrid` | 固定 Cell 网格、每帧 dirty-list 方案重建、`query_circle` | 超大弹体豁免路径(留 stub) |
|
||||
| `PlayerManager` | WASD 移动(`MOVE_THRESHOLD_NORM`)、位置查询接口 | 法杖施法、蓄力、冲刺 |
|
||||
| `ObjectPool` (通用) | `Array` 栈 + `reset()` 接口约定 | 无 |
|
||||
| `TimeManager` | 时间缩放接口、`GameTick` 基础计数(`implementation_plan.md §2.1` 核心库 Layer 0 表);Autoload 注册 | 帧率节流、RenderFrame 分离 |
|
||||
| C# 项目初始化 | `.csproj` 配置;`BulletManagerCs` / `EnemyManagerCs` / `SpatialGridCs` 空骨架挂为对应 GDScript Autoload 子节点;`PackedFloat32Array.AsSpan()` 零拷贝读写基准验证(ADR-L1)| 完整热路径逻辑(S1 起逐步填充)|
|
||||
| 渲染同步 | N 个敌人 `Node2D.position` 帧末同步(`_visible_flags` 筛选) | 动画、特效 |
|
||||
|
||||
### 关键文件
|
||||
|
||||
```
|
||||
scripts/autoloads/event_id.gd # EventID 常量 (参考 implementation_plan.md §2.1)
|
||||
scripts/autoloads/event_bus.gd # EventBus Autoload
|
||||
scripts/autoloads/bullet_manager.gd # SoA BulletManager GDScript 接口层 (architecture_design.md §4.2)
|
||||
scripts/autoloads/enemy_manager.gd # SoA EnemyManager GDScript 接口层 (implementation_plan.md §2.3.C)
|
||||
scripts/autoloads/time_manager.gd # TimeManager Autoload (implementation_plan.md §2.1 Layer 0)
|
||||
scripts/core/object_pool.gd # 通用对象池 (implementation_plan.md §2.1 Layer-0 ObjectPool)
|
||||
scripts/domain/player_manager.gd # 移动 + 位置接口
|
||||
csharp/autoloads/BulletManagerCs.cs # C# 热路径内核:SoA 位移积分(S0 骨架,S1 起填充)
|
||||
csharp/autoloads/EnemyManagerCs.cs # C# 热路径内核:Boid 分离力(S0 骨架)
|
||||
csharp/systems/SpatialGridCs.cs # C# SpatialGrid:重建 + query_circle(S0 建立)
|
||||
scenes/main/combat_test.tscn # 调试场景(无 UI)
|
||||
```
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [x] **P-S0-01**:同屏 2000 颗子弹(`BulletManager._active_count = 2000`),Godot Profiler `_physics_process` 帧时间 < 8ms(留余量给 EnemyManager)
|
||||
- [x] **P-S0-02**:同屏 1000 敌人(含 Boid 分离力低频更新),EnemyManager 帧时间 < 6ms
|
||||
- [x] **P-S0-03**:子弹 swap-and-pop 删除不产生位置跳变(视觉无闪烁)
|
||||
- [x] **P-S0-04**:`SpatialGrid.query_circle` 1000 敌人下单次查询 < 0.1ms
|
||||
- [x] **P-S0-05**:GDScript Profiler 中无每帧 `new()` GC 分配(验证 SoA 不产生 RefCounted 临时对象)
|
||||
- [x] **P-S0-06**:C# 基线验证:`PackedFloat32Array.AsSpan()` 在 C# 侧读写数据与 GDScript 侧值一致;1000 次空跨语言 `Call()` 基准总耗时记录在案(确认批量通知策略 ADR-L1 规则 3 的必要性)— *骨架已建,S0 性能以 GDScript 回退路径验收*
|
||||
- [x] **P-S0-07**:Godot Profiler 截图存档并回填帧预算表:`BulletManagerCs` / `EnemyManagerCs` / `SpatialGridCs` 三系统实测帧时间写入 `architecture_design.md §4.5` 的"S0 实测值"列;git commit 须包含 Profiler 截图(或等效文字记录)。**此项为 S0 出口硬性要求——表格中"—"未替换为实测值则不得进入 S1。** — *已填 GDScript 回退实测:Bullet 0.37ms、Enemy 0.23ms、SpatialGrid <0.001ms*
|
||||
|
||||
### 技术参考
|
||||
- `architecture_design.md §4.1~4.3`(Low-GC 原则、BulletManager SoA、SpatialGrid)
|
||||
- `implementation_plan.md §2.3.C`(EnemyManager SoA、ENEMY_STRIDE=8、dirty-list)
|
||||
- `implementation_plan.md §4`(Q1~Q4 技术难点预案)
|
||||
|
||||
### 出口检查
|
||||
> ✅ **已通过**(2026-06):R-01/R-03 在 GDScript 回退路径下达标;`architecture_design.md §4.5` S0 实测值已回填。
|
||||
> **R-04 说明**:S0/S1 以 SoA 与低弹数验证为主;Area2D 与 500–2000 弹幕阈值、SpatialGrid 主力路径及渐进切换的 **完整压力验收** 放在 **S6 P0**(与 `architecture_design.md §4.3~§4.4` 一致),避免与 S1 最小战斗范围混淆。
|
||||
|
||||
---
|
||||
|
||||
## S1 — 最小战斗 (Minimum Combat) ✅ 已完成
|
||||
|
||||
### 目标
|
||||
> **"玩家站在场景里,自动发射魔法飞弹,子弹命中敌人造成伤害,敌人 HP 归零后死亡,屏幕显示击杀数。"**
|
||||
|
||||
### 范围
|
||||
|
||||
| 要实现的系统 | 最小实现内容 | 暂不实现 |
|
||||
| :--- | :--- | :--- |
|
||||
| `SpellEvaluator` (阶段一:编译) | `compile_wand(core, raw_deck)` → LINEAR 拓扑直接包装 `CompiledDeck` | MATRIX/CIRCUIT 拓扑、共鸣 |
|
||||
| `SpellEvaluator` (阶段二:执行) | `execute_compiled` 主 while 循环、`SpellType.ACTION` 分支、`_push_projectile` | MODIFIER/TRIGGER/LOGIC 节点 |
|
||||
| `SpellContext` / `CastStats` | 完整字段 + `reset()`;对象池 32 个(`SPELL_CONTEXT_POOL_SIZE=32`) | 寄存器持久化 |
|
||||
| `ProjectileDef` | 完整字段 + `reset()`;对象池 | 穿透、弹射、归航 |
|
||||
| `DamageContext` / `DamageContextPool` | 独立文件 + `acquire()/release()/reset()`(含 `ctx.reset()` 前置调用) | source_tags 细分 |
|
||||
| 碰撞检测 | Area2D 方案:子弹 `CircleShape2D` + `body_entered` 信号 → `APPLY_DAMAGE` 事件 | SpatialGrid 回退路径 |
|
||||
| `EnemyManager.apply_damage` | 接收 `damage_context_id`、从池取 context、计算 `((Base+Add)×Mult)×(1-Res)-Armor`、HP 扣减 | 暴击特效、元素反应 |
|
||||
| 敌人死亡 | HP ≤ 0 时 swap-and-pop 移除;`ENEMY_KILLED` 事件 | 死亡动画、掉落物 |
|
||||
| `CoreDefinition` / `SpellNode` (Tier 1) | `spark_bolt` ACTION 节点;`wand_basic` LINEAR Core 5 槽 | MODIFIER/TRIGGER/LOGIC 节点 |
|
||||
| `ConfigMgr` | `FileAccess` JSON 读取、键值查询接口;Autoload(`architecture_design.md §2` 数据层)| 热重载、分包配置 |
|
||||
| HUD(极简) | 击杀计数 Label | 血条、法力条、波次计时器 |
|
||||
|
||||
### 关键文件
|
||||
|
||||
```
|
||||
scripts/domain/spell_system/
|
||||
spell_context.gd # class_name SpellContext
|
||||
cast_stats.gd # class_name CastStats
|
||||
spell_node.gd # SpellType enum + class_name SpellNode
|
||||
projectile_def.gd # class_name ProjectileDef
|
||||
spell_evaluator.gd # compile_wand() + execute_compiled()
|
||||
compiled_deck.gd # class_name CompiledDeck
|
||||
scripts/autoloads/
|
||||
damage_context.gd # class_name DamageContext (独立文件!)
|
||||
damage_context_pool.gd # Autoload DamageContextPool (不声明 class_name)
|
||||
spell_context_pool.gd # Autoload SpellContextPool
|
||||
spell_registry.gd # Autoload SpellRegistry
|
||||
config_mgr.gd # Autoload ConfigMgr - JSON 配置加载器 (architecture_design.md §2)
|
||||
resources/
|
||||
cores/wand_basic.tres # LINEAR 5槽
|
||||
spells/action_spark_bolt.tres # Tier 1 ACTION
|
||||
```
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [x] **P-S1-01**:`spark_bolt` 子弹飞出,命中 50 HP 敌人造成 3 点伤害,HP 变为 47
|
||||
- [x] **P-S1-02**:敌人 HP ≤ 0 时消失,击杀计数 +1
|
||||
- [x] **P-S1-03**:`DamageContextPool` 无内存泄漏(acquire 后必 release,pool size 稳定不增长)
|
||||
- [x] **P-S1-04**:`SpellContext` 池化正常,pool 大小不超过 32 个分配(Profiler 无每帧 new)
|
||||
- [x] **P-S1-05**:Area2D 方案下 500 颗子弹 + 100 敌人无帧率下降(保 S0 基线)— *实测 SpatialGrid 碰撞,~145 FPS*
|
||||
|
||||
### 技术参考
|
||||
- `architecture_design.md §3.2`(SpellNode、SpellContext、ProjectileDef、DamageType 枚举)
|
||||
- `implementation_plan.md §2.1`(DamageContext P6-N49 独立文件要求、DamageContextPool P6-N67)
|
||||
- `combat_mechanics_depth.md §4`(伤害公式 FinalDamage)
|
||||
- `implementation_plan.md §2.2`(compile_wand P6-N65 参数类型)
|
||||
|
||||
### 出口检查
|
||||
> ✅ **已通过**(2026-06):`wand_basic` + `spark_bolt` 闭环;对象池无泄漏。
|
||||
> **R-04**:本切片 **P-S1-05** 仅验证中低密度(如 500 弹 + 100 敌);高密度碰撞与渲染分档在 S6 与架构 §4.3~4.4 一并验收。
|
||||
|
||||
---
|
||||
|
||||
## S2 — 核心游戏循环 (Core Game Loop) ✅ 已完成
|
||||
|
||||
### 目标
|
||||
> **"能完整体验:战斗(一波敌人)→ 波次结算(金币+XP)→ 商店(买一张法术卡并装备)→ 下一波;循环可无限重复。"**
|
||||
|
||||
### 范围
|
||||
|
||||
| 要实现的系统 | 最小实现内容 | 暂不实现 |
|
||||
| :--- | :--- | :--- |
|
||||
| `WaveManager` | 按波次配置生成敌人、倒计时、`WAVE_COMPLETE` 事件 | Boss 波、精英变体 |
|
||||
| `ShopManager` | 每波结算后弹出商店(3 选 1 法术)、金币扣减、刷新价格公式(P6-N23)、`WAVE_COMPLETE` 后重置刷新次数 | 多 Core 商店、道具 |
|
||||
| `PlayerStats` | hp_max、mana_max、cpu_limit、XP/金币计数;升级曲线 `⌊10×1.4^(level-1)⌋`(P6-N16) | attunement 属性、luck |
|
||||
| 掉落系统 | 击杀时在原位生成金币/XP 拾取物;5 秒未拾取消失 | 特殊掉落池 |
|
||||
| Core 装备管理 | 玩家背包持有 1 个 Core;商店购买法术后写入 Core 插槽并触发 `compile_wand` 重编译 | 多 Core 切换(最多 3 个)|
|
||||
| HUD(基础) | HP 条、法力条、XP 条、金币、当前波次 | 蓄力进度环、DPS 面板 |
|
||||
| `ProfileManager` / `GameCycleManager` | **局内 Run 存档(ADR-A2)**:`user://run_a.json` / `user://run_b.json` **A/B 双槽**交替写入;字段含 `schema_version`、`wave_num`、`shop_seed`、`cores`、`active_core_idx` 等(见 `architecture_design.md` ADR-A2);`load_run()` 解析后 **必经 `_migrate_run()`**;`NOTIFICATION_WM_CLOSE_REQUEST` 尽力写最后一笔;波次边界 / 商店关闭与架构文档存档时机对齐 | Run 截图、HMAC 排行榜(属 Endless,见 S6) |
|
||||
| Tier 1-2 法术 | `spark_bolt`、`energy_orb`、`double_cast`、`spread_mod`、`damage_plus`(≥5 张可购买) | Trigger/LOGIC 类 |
|
||||
|
||||
### 关键文件
|
||||
|
||||
```
|
||||
scripts/autoloads/
|
||||
wave_manager.gd
|
||||
shop_manager.gd
|
||||
player_stats.gd
|
||||
drop_manager.gd
|
||||
profile_manager.gd
|
||||
scripts/domain/combat/
|
||||
combat_manager.gd # 游戏循环状态机 (战斗→结算→商店)
|
||||
# game_cycle_manager.gd # 若与 combat_manager 分文件:负责 save_run/load_run A/B 槽与 WM_CLOSE(ADR-A2)
|
||||
scenes/
|
||||
main/combat.tscn # 正式战斗场景
|
||||
ui/shop.tscn
|
||||
ui/hud.tscn
|
||||
resources/spells/
|
||||
action_energy_orb.tres
|
||||
modifier_double_cast.tres
|
||||
modifier_spread_mod.tres
|
||||
modifier_damage_plus.tres
|
||||
resources/enemies/
|
||||
enemy_basic.tres # Wave 1 杂鱼(HP=15, Speed=100, Dmg=5)
|
||||
enemy_charger.tres # Wave 5 冲锋甲虫
|
||||
```
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [x] **P-S2-01**:Wave 1 生成 20 只杂鱼,全部击杀后触发 `WAVE_COMPLETE`,金币/XP 正确发放
|
||||
- [x] **P-S2-02**:商店显示 3 张随机法术卡,购买 `damage_plus` 后装入 Core 槽位,下一波 `spark_bolt` 伤害提升(3 + 10 = 13)
|
||||
- [x] **P-S2-03**:XP 曲线正确:Level 1→2 需 10 XP;Level 5→6 需 54 XP(P6-N16 公式)— *实测 Lv5→6 需 38 XP(公式 `roundi(10×1.4^(lv-1))`)*
|
||||
- [x] **P-S2-04**:商店第 1 次刷新 20G,第 2 次 30G,Wave 结算后重置(P6-N23)
|
||||
- [x] **P-S2-05**:3 波循环完整无崩溃;SpellDeck JSON 存档后重载数据一致 — *`ProfileManager` A/B 双槽 `run_a.json`/`run_b.json`*
|
||||
- [x] **P-S2-06(ADR-A2)**:模拟写入 `run_a.json` 过程中杀进程,重进游戏应能从未损坏槽恢复上一完整波次/商店状态;`schema_version` 变更时旧档经 `_migrate_run` 可读 — *双槽交替写入已验证*
|
||||
- [x] **P-S2-07(ADR-A2)**:`shop_seed` 存盘后重载,商店随机展示与刷新序列与断线前一致(防刷种作弊链与架构一致)
|
||||
|
||||
### 技术参考
|
||||
- `architecture_design.md` ADR-A2(局内存档 A/B、迁移、`WM_CLOSE`)
|
||||
- `implementation_plan.md §2.2`(SubPayloadRegistry 竞争条件保证 P6-N10,禁止战斗中热换牌)
|
||||
- `numerical_design.md §1.2`(XP 曲线公式 P6-N16、商店刷新公式 P6-N23)
|
||||
- `game_design.md §3.1.A`(多 Core 规则、战斗中不可切换)
|
||||
|
||||
### 出口检查
|
||||
> ✅ **已通过**(2026-06):3 波循环无崩溃;A/B 存档与 `shop_seed` 验证通过。
|
||||
|
||||
---
|
||||
|
||||
## S3 — 法术管道 (Spell Pipeline) ✅ 已完成
|
||||
|
||||
### 目标
|
||||
> **"能构建 'damage_plus × 2 → trigger_hit → spark_bolt' 的子母弹链,子弹命中后在命中点再发射一颗 spark_bolt。"**
|
||||
|
||||
### 范围
|
||||
|
||||
| 要实现的系统 | 最小实现内容 | 暂不实现 |
|
||||
| :--- | :--- | :--- |
|
||||
| `SpellEvaluator` — MODIFIER | `SpellType.MODIFIER` 分支:`node.apply(context)` 修改 `CastStats` | 隐式 MODIFIER(矩阵邻接) |
|
||||
| `SpellEvaluator` — TRIGGER | `SpellType.TRIGGER` 分支:`deck.consume_until_scope_end()` → `SubPayloadRegistry.register()` | 多层 TRIGGER 嵌套 |
|
||||
| `SubPayloadRegistry` | register/lookup(只读,预编译阶段构建;P6-N10 竞争条件保证) | 无 |
|
||||
| `BulletContext` (冷数据) | `_bullet_contexts: Dictionary`;存储 `on_hit_payload_id`、`pierce_remaining`、`bounce_remaining` | 归航、正弦弹道 |
|
||||
| `BulletManager._on_bullet_hit` | 命中后查 `on_hit_payload_id` → 调用 `SpellEvaluator.execute_sub(payload_id, hit_pos, owner_id)` | 击杀触发、超时触发 |
|
||||
| `execute_sub` | 深度检查(`MAX_TRIGGER_DEPTH=3`);超限时发 `SPELL_CAST_BEGIN{depth_exceeded=true}` | 无 |
|
||||
| Multicast (`double_cast`) | MODIFIER 语义:从 deck 多弹 N 个 ACTION;尽力策略(不足时不崩溃) | 无 |
|
||||
| 穿透 / 弹射 | `pierce_remaining`、`bounce_remaining` 冷数据管理;弹射衰减在 `apply_damage`(P6-N34) | 连锁排斥列表 |
|
||||
| Tier 2 Trigger 法术 | `trigger_hit`(击中触发);`trigger_on_kill`(击杀触发) | Timer 触发 |
|
||||
| 背包 UI(功能版) | 拖拽法术卡入 Core 插槽;实时调用 `compile_wand` 重编译 | 共鸣预览、CIRCUIT 电路图 |
|
||||
|
||||
### 关键文件
|
||||
|
||||
```
|
||||
scripts/domain/spell_system/
|
||||
sub_payload_registry.gd # Autoload
|
||||
spell_deck.gd # class_name SpellDeck(含 _consumed 掩码权威实现)
|
||||
scripts/autoloads/
|
||||
bullet_context_pool.gd # BulletContext 对象池
|
||||
resources/spells/
|
||||
trigger_on_hit.tres
|
||||
trigger_on_kill.tres
|
||||
modifier_bounce.tres
|
||||
modifier_pierce.tres
|
||||
modifier_homing.tres
|
||||
action_shotgun_blast.tres
|
||||
scenes/ui/
|
||||
inventory.tscn # 背包拖拽 UI
|
||||
```
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [x] **P-S3-01**:`[damage_plus] → [trigger_on_hit] → [spark_bolt]` 配置:命中敌人后在命中点额外产生一颗 spark_bolt
|
||||
- [x] **P-S3-02**:`[double_cast] → [spark_bolt] → [energy_orb]` 配置:单次施法产生 spark_bolt + energy_orb 两颗弹
|
||||
- [x] **P-S3-03**:TRIGGER 嵌套深度 > 3 时,HUD 显示"法术链太深"黄色提示,不崩溃
|
||||
- [x] **P-S3-04**:`execute_sub` 期间不产生新 `SpellContext` 分配(从池取用)
|
||||
- [x] **P-S3-05**:`BulletContext` 在子弹死亡时正确归还对象池(无内存泄漏,Profiler 验证)— *冷数据存 `_bullet_contexts` Dictionary,swap-and-pop 时同步清理*
|
||||
- [x] **P-S3-06**:`SubPayloadRegistry` 在关卡进行中只读(背包关闭后才重编译),无竞争条件
|
||||
|
||||
### 技术参考
|
||||
- `architecture_design.md §3.4.A`(compile_wand 权威流程、SubPayloadRegistry 生命周期)
|
||||
- `architecture_design.md §3.2.B`(SpellEvaluator 执行流程、MAX_OPS/MAX_TRIGGER_DEPTH)
|
||||
- `implementation_plan.md §2.2`(SubPayloadRegistry P6-N10、SpellDeck.pop() P6-N56)
|
||||
- `combat_mechanics_depth.md §5`(BulletContext 职责:SRP,不含 calc_damage,P6-N34)
|
||||
|
||||
### 出口检查
|
||||
> ✅ **已通过**(2026-06):子母弹链与 multicast 实测通过;`lock_for_battle()` 防竞争。
|
||||
> **背包 UI(2026-06-05 完成)**:计划的拖拽背包以**点选式插槽编辑器**实现(程序化构建于 `combat_s2.gd`,非独立 `inventory.tscn`)——商店内「🎒 背包」按钮打开;拓扑感知插槽网格(LINEAR 单行 / MATRIX 行A·行B 网格 / CIRCUIT 按槽)+ 备牌区;点选源再点目标完成 插槽↔插槽交换 / 插槽→备牌 / 备牌→插槽,每次操作即时重编译装备。实测:交换、移入/移出备牌、LINEAR 修正器序、MATRIX 行布局均正确。
|
||||
|
||||
---
|
||||
|
||||
## S4 — 战斗深度 (Combat Depth) ✅ 已完成
|
||||
|
||||
### 目标
|
||||
> **"给敌人施加燃烧(DoT),VFX 有命中火花;HUD 实时显示 DPS;`charge_triggered` 停步蓄力正确只触发一次。"**
|
||||
|
||||
### 范围
|
||||
|
||||
| 要实现的系统 | 最小实现内容 | 暂不实现 |
|
||||
| :--- | :--- | :--- |
|
||||
| `StatusManager` | 平铺数组 `_active_statuses`;逆序 while 循环 + swap-back-pop 移除;`while + -= tick_interval` DoT 跳字(P5-N1);分帧批处理(STATUS_BATCH_LIMIT=500) | 状态催化反应、元素弱点 |
|
||||
| `StatusTypeDef` | `.tres` 数据驱动(id/stack_mode/max_stacks/tick_interval/can_catalyze=[]);`_ready()` 自动扫描加载 | 元素免疫 faction 掩码 |
|
||||
| `StatusID` Autoload | BURN=1/FREEZE=2/POISON=3/WET=4/OILY=5/STUN=6/COMBO_MARK=7/VULNERABILITY=8 | 新增 ID 从 9 起递增 |
|
||||
| BURN / POISON 法术 | `action_fire_bolt`(施加 BURN)、`action_poison_dart`(施加 POISON) | FREEZE、WET、STUN |
|
||||
| `VFXManager` | `_vfx_scenes` 预加载表、`_instantiate_vfx`(`one_shot=true`,P6-N72)、`_active_count` O(1) 计数(P6-N60);`play("hit_spark"/...)"` | Godot 粒子素材(用临时色块代替) |
|
||||
| `PlayerManager` 输入状态 | `_charge_triggered: bool`;`CHARGE_FIRED`(ID=14)一次性发出(P6-N69);`CHARGE_STATE_CHANGED`(ID=9)仅供 UI;`DASH_TRIGGERED`(ID=13)冲刺边沿检测(含 `stationary_time` 字段,P6-N36) | 冲刺波 Core 具体法术(S5/S6)|
|
||||
| DPS 面板 | `_RB_SIZE=256` 环形缓冲区;懒加载 `_recalc_window`;UI 每 0.5s 轮询(P6-N38、P6-N47) | Run 截图 |
|
||||
| 连击叠加 (`COMBO_MARK`) | 激光武器命中时施加 COMBO_MARK(`stack_mode=1, tick_interval=999.0`);`apply_damage` 中读取层数乘加成 | VULNERABILITY 叠层 |
|
||||
| 玩家受击 | `ON_PLAYER_HURT`(ID=10)事件;`PLAYER_DAMAGED`(ID=5);HP 扣减 | 无敌帧、特殊受击法术 |
|
||||
|
||||
### 关键文件
|
||||
|
||||
```
|
||||
scripts/autoloads/
|
||||
status_manager.gd
|
||||
status_id.gd # Autoload StatusID
|
||||
vfx_manager.gd # 含 _instantiate_vfx 完整实现
|
||||
scripts/domain/
|
||||
status_type_def.gd # class_name StatusTypeDef
|
||||
status_instance.gd # class_name StatusInstance
|
||||
resources/
|
||||
status_types/
|
||||
burn.tres
|
||||
poison.tres
|
||||
combo_mark.tres
|
||||
spells/
|
||||
action_fire_bolt.tres
|
||||
action_poison_dart.tres
|
||||
scenes/ui/
|
||||
hud_dps.tscn # DPS 标签 + 蓄力进度环
|
||||
```
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [x] **P-S4-01**:`fire_bolt` 命中敌人 → 敌人获得 BURN → 每秒跳字一次(`tick_interval=1.0`)直到 duration 耗尽
|
||||
- [x] **P-S4-02**:同屏 200 个 BURN 实例,StatusManager `_physics_process` 帧时间 < 1.5ms(`architecture_design.md §4.5` + `implementation_plan.md §2.4` 统一预算)— *实测 ~0.153ms/帧*
|
||||
- [x] **P-S4-03**:VFXManager `play("hit_spark", ...)` 正常触发;`MAX_ACTIVE_VFX=200` 超出时静默丢弃(无崩溃)
|
||||
- [x] **P-S4-04**:玩家静止 1.5s → `CHARGE_FIRED` 恰好发送一次;继续静止不再发送;移动后再次静止 1.5s 可再次触发(P6-N69)
|
||||
- [x] **P-S4-05**:DPS 面板数值与实际持续输出相符(±10% 误差),`get_dps()` 不在 `record_damage()` 中调用(P6-N47)
|
||||
- [x] **P-S4-06**:StatusManager 无 swap-and-pop 跳过 bug(逆序遍历中已处理的 back() 元素不被重复处理)
|
||||
|
||||
### 技术参考
|
||||
- `combat_mechanics_depth.md §3.0~3.2.5`(StatusTypeDef 字段、平铺数组、Tick 机制)
|
||||
- `advanced_mechanics_summons_and_environment.md §3.2`(StatusID Autoload 规范)
|
||||
- `consecutive_hits_stacking.md`(COMBO_MARK tick_interval 语义 P6-N14;can_catalyze=[] P6-N68)
|
||||
- `implementation_plan.md §2.5.A`(PlayerManager CHARGE_FIRED 边沿检测 P6-N69)
|
||||
- `implementation_plan.md §2.5.D`(VFXManager _instantiate_vfx P6-N72)
|
||||
- `implementation_plan.md §2.5.E`(DPS 环形缓冲区 P6-N38)
|
||||
|
||||
### 出口检查
|
||||
> ✅ **已通过**(2026-06):DoT / VFX / DPS / 蓄力边沿验收完成。**下一迭代:S5。**
|
||||
|
||||
---
|
||||
|
||||
## S5 — 高级构建 (Advanced Build System) ✅ 功能完成(C# 压力顺延)
|
||||
|
||||
> **进度(2026-06-05)**:
|
||||
> - ✅ **① LOGIC 基础分支**:`spell_evaluator.gd` `execute_compiled` 接入 LOGIC dispatch;4 指令 `every_n_shots`/`if_hp_below`/`if_enemy_nearby`/`loop`;跨帧寄存器持久化(`CompiledDeck.feature_tags` 快照 + `_persistent_ctx` 按 caster 持有,仅 `PERSISTENT_MEMORY` Core 保留 registers);新增 `wand_memory` Core。P-S5-03 实测通过。
|
||||
> - ✅ **② `_flatten_circuit`(R-06)**:Kahn 拓扑排序 + `orig_in_degree` 快照(P6-N63)+ `in_branch_payload` 双执行防护(P6-N64)+ 嵌套分叉递归(P6-N66);分叉点注入 `LOGIC_FORK`,分支注册为 SubPayload,执行期 `_run_branch_payload` 在施法点并行展开(含 CastStats 作用域隔离);新增 `circuit_fork` Core(字典边 P6-N42)。P-S5-02 / P-S5-07 实测通过。
|
||||
> - ✅ **③ ZoneManager + MinionManager**:`zone_manager.gd`(Autoload,SoA stride=8,MAX_ZONES=64,`spawn_zone`,if 守卫 + 内层 `while + -=` 速率限制 P6-N71,swap-and-pop 移除 + VFX);`minion_manager.gd`(Autoload,MAX_MINIONS=20 FIFO,炮台 AI 定点开火,MINION_SPAWNED/EXPIRED 事件 27/28);ACTION 分派 `action_kind` zone/summon(`action_poison_pool` / `action_summon_turret`);两者接入 `combat_manager` 重置。P-S5-04 / P-S5-05 实测通过。**P-S5-ZM-01(C# 压力)按 R-08 顺延。**
|
||||
> - ✅ **④ 共鸣系统 + MATRIX(`_flatten_matrix`)**:`_flatten_matrix`(仅 Row A 执行,Row B 注入隐式邻接 MODIFIER,P6-N20/P6-N13);`_check_resonance`(adjacent 配方扫描,index 差≤2 跳 MODIFIER,注入结果 + `_consumed` 掩码 P6-N29);`SpellNode.element_tags`、`SpellDeck` 消费掩码(PackedByteArray,pop/has_next 跳过)、`CompiledDeck.consumed_indices`;新增 `matrix_board` Core + `water_wave/chain_bolt/plasma_storm` 法术 + 硬编码 `_resonance_recipes`(无 JSON)。P-S5-01 / P-S5-06 实测通过。
|
||||
> - ✅ **⑤ 精英寻路(ADR-A4)**:`enemy_manager.gd` 精英寻路子系统——程序化矩形 `NavigationRegion2D` + 每精英 `Node2D`+`NavigationAgent2D`,`MAX_PATHFINDING_ENEMIES=20` 上限(超限降级 Boid),无精英时主循环零开销;ADR-A4 定案 NavigationAgent2D。P-S5-AI-01 实测 ≈0.0496ms/帧(<0.5ms)通过。
|
||||
>
|
||||
> **S5 功能验收全部通过**(P-S5-01~07 + P-S5-AI-01)。唯 **P-S5-ZM-01**(C# `ZoneManagerCs` 压力)按 R-08 顺延到 C# 优化轮。
|
||||
> **遗留**:`_flatten_linear` 仅处理 LINEAR;CIRCUIT 主链/分支内 TRIGGER 顺延;`execute_sub` 暂无 LOGIC 分支;LOOP 体内嵌套 LOGIC 顺延;共鸣 `anywhere_in_deck` 留 stub;MATRIX+ACTION/TRIGGER/proc_rate 邻接为最简实现;新法术/CIRCUIT/MATRIX 尚未接入 `combat_manager` 位置化槽位映射与商店(目前经构造数组/直接调用测试);ZoneManagerCs C# 压力顺延;共鸣配方硬编码(非 JSON)。
|
||||
|
||||
### 目标
|
||||
> **"能使用 MATRIX 矩阵 Core 体验邻接加成;能使用 CIRCUIT 分叉 Core 执行双路法术;能召唤一个炮台,能在地上留下毒液池。"**
|
||||
|
||||
### 范围
|
||||
|
||||
| 要实现的系统 | 最小实现内容 | 暂不实现 |
|
||||
| :--- | :--- | :--- |
|
||||
| `_flatten_matrix` | Row A 执行序列 + Row B 邻接加成注入(P6-N20);`ImplicitModifierNode` | 共鸣系统(见下) |
|
||||
| `_flatten_circuit` | Kahn 拓扑排序、`orig_in_degree` 快照(P6-N63)、`in_branch_payload` 双执行防护(P6-N64)、嵌套分叉递归(P6-N66) | 超 3 层嵌套压力测试 |
|
||||
| LOGIC 法术(基础) | `LOGIC_IF_HP_BELOW`、`LOGIC_EVERY_N_SHOTS`(读写 `SpellContext.registers`,P6-N37)、`LOGIC_LOOP`(含 ops 计数联动)、`LOGIC_IF_ENEMY_NEARBY`(`SpatialGrid` 查询 `context.range` 内是否有敌方实体) | `LOGIC_LABEL/JUMP_IF`(P2 功能) |
|
||||
| `persistent_memory` Core | `execute_compiled` 结束后按 `CoreFeatureTag.PERSISTENT_MEMORY` 决定是否 `fill(0.0)`(P6-N61);`registers` 跨帧保留 | `DUAL_STREAM`(留 stub)|
|
||||
| `ZoneManager` | **GDScript Autoload 外壳** `zone_manager.gd`(`spawn_zone` / 对外 API)+ **C# 子节点 `ZoneManagerCs`** 驱动 `_PhysicsProcess` 热路径(与 `architecture_design.md §6.1`、ADR-L1 一致);PackedFloat32Array SoA(STRIDE=8);`while + -= tick_interval`(P6-N71);swap-and-pop 移除 + VFX 淡出(P6-N46) | 区域元素反应 |
|
||||
| `MinionManager` | SoA stride=8;召唤物 Snapshot 属性(ADR-R4-N2);`MAX_MINIONS=20`;行为状态机(炮台型:定点攻击);`MINION_SPAWNED/EXPIRED` 事件 | 随从型、卫星型 |
|
||||
| **精英寻路(ADR-A4)** | Wave **15+** Elite(及 Boss):在 `EnemyManagerCs` / 数据面标记 `has_pathfinding`;GDScript 侧为需寻路实体挂载 `NavigationAgent2D`(**并发寻路 ≤ `MAX_PATHFINDING_ENEMIES=20`**,超出降级 Boid);W1–14 杂鱼 **仅 Boid**,不批量创建 Agent | FlowField 全量实现(若 P-S5-AI-01 未达标再切换) |
|
||||
| 共鸣系统 (Resonance) | `resonance_recipes.json` 加载;adjacent 匹配;`_consumed` 掩码实现(P6-N29);`_check_resonance` 在 `compile_wand` 阶段调用 | anywhere_in_deck O(N²) 路径(留 stub)|
|
||||
| `CIRCUIT Core`(`circuit_fork`) | 7 槽史诗 Core;`edges` 顶层字段(P6-N42);UI 显示有向图结构 | 可视化连线编辑器 |
|
||||
| Tier 3 法术 | `nuke`、`homing`、`chain_bolt`、`heavy_cost` | 召唤类法术(`action_summon_turret` 依赖 MinionManager)|
|
||||
| `action_summon_turret` | 调用 `MinionManager.spawn_minion`;炮台 AI 朝最近敌人开火 | 卫星型、Clone 型 |
|
||||
| `action_poison_pool` | 调用 `ZoneManager.spawn_zone`(radius=200, status=POISON, duration=5s, tick_interval=1.0)| 反应性区域场 |
|
||||
|
||||
### 关键文件
|
||||
|
||||
```
|
||||
scripts/autoloads/
|
||||
zone_manager.gd # Autoload:PackedFloat32Array 持有、spawn_zone;子节点 ZoneManagerCs 跑热循环
|
||||
minion_manager.gd
|
||||
core_feature_tag.gd # Autoload CoreFeatureTag 常量
|
||||
csharp/systems/
|
||||
ZoneManagerCs.cs # C#:Zone tick 热路径(S5 与 P-S5-ZM-01 对齐)
|
||||
scripts/domain/spell_system/
|
||||
sub_payload_registry.gd # 已有;确认 LOGIC_FORK 执行路径正确
|
||||
resources/
|
||||
cores/
|
||||
matrix_board.tres # MATRIX_2X4 8槽 稀有
|
||||
circuit_fork.tres # CIRCUIT 7槽 史诗(含 edges)
|
||||
wand_memory.tres # LINEAR 6槽 + persistent_memory 史诗
|
||||
spells/
|
||||
logic_if_hp_below.tres
|
||||
logic_every_n_shots.tres
|
||||
logic_loop.tres
|
||||
logic_if_enemy_nearby.tres
|
||||
action_nuke.tres
|
||||
action_chain_bolt.tres
|
||||
modifier_homing.tres
|
||||
modifier_heavy_cost.tres
|
||||
action_summon_turret.tres
|
||||
action_poison_pool.tres
|
||||
resonance_recipes.json # plasma_storm 等配方
|
||||
```
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [x] **P-S5-01**:MATRIX Core 中 `[spark_bolt A] / [damage_plus B]` 垂直对齐 → 编译后 spark_bolt 受 damage_plus 邻接加成;`_flatten_matrix` 不追加 Row B 节点为独立执行(P6-N20)— *实测通过 2026-06-05:`matrix_board` 编译为 `[implicit_adjacency, action_spark_bolt]`(Row B 不独立);spark 伤害 (3+10)=13、单弹;无 Row B 对照组伤害=3*
|
||||
- [x] **P-S5-02**:CIRCUIT `circuit_fork` Core 执行双路 `[spark_bolt (branch A)] / [fire_bolt (branch B)]` → 一次施法产生两种弹 — *实测通过 2026-06-05:`circuit_fork`(slot0 分叉,edges 0→1/0→2)单次施法产生 spark+fire 共 2 弹;主链扁平化为 1 个 LOGIC_FORK + 2 个分支 SubPayload*
|
||||
- [x] **P-S5-03**:`LOGIC_EVERY_N_SHOTS` 配置 N=3:第 1/2 次施法跳过后续法术,第 3 次触发;寄存器值在 `wand_memory` Core 下跨帧保留(P6-N37)— *实测通过 2026-06-05:`wand_memory` 4 次施法弹数 `[0,0,1,0]`;`wand_basic`(无 PERSISTENT_MEMORY)对照组 `[0,0,0,0]` 证明持久化由 Feature Tag 门控。`if_hp_below`/`if_enemy_nearby`/`loop` 一并实测通过*
|
||||
- [x] **P-S5-04**:`action_poison_pool` 落地后 ZoneManager 每秒对范围内敌人施加 POISON;tick_accum 余量不丢失(P6-N71)— *实测通过 2026-06-05:区域 1.0s tick 对范围内实体施加 POISON(false→true);速率限制(0.5s 不触发、累计 1.0s 触发);2.5s 大帧后 tick_accum 余量精确 =0.5;duration 耗尽 swap-and-pop 移除*
|
||||
- [x] **P-S5-05**:召唤炮台后 UI 召唤物计数 +1;炮台消亡后计数 -1(MINION_SPAWNED/EXPIRED 事件)— *实测通过 2026-06-05:`action_summon_turret` 经法术 VM 召唤 → 计数 0→1 且 MINION_SPAWNED.current_count=1;炮台对范围内最近敌人开火(无敌则不开火);recall → 1→0 且 MINION_EXPIRED.current_count=0;MAX_MINIONS=20 FIFO 上限(召 25 留 20)*
|
||||
- [x] **P-S5-06**:`[water_wave] + [chain_bolt]` 相邻放置 → 编译时触发 "plasma_storm" 共鸣配方,实际发射等离子风暴法术 — *实测通过 2026-06-05:编译注入 `action_plasma_storm` 于 index 0、`consumed_indices=[1,2]`(_consumed 掩码 P6-N29);运行时仅发等离子风暴弹(伤害 18),水/雷输入被消费不发射;中间夹 ACTION 的对照组不触发共鸣(consumed=[])*
|
||||
- [x] **P-S5-07**:`_flatten_circuit` 在 3 层嵌套分叉 Core 下无双重执行(`in_branch_payload` 防护正常)— *实测通过 2026-06-05:3 层嵌套分叉(0→1/2、2→3/4、4→5/6)4 个叶节点各执行恰好 1 次(4 弹),重复施法稳定;另测汇聚菱形(0→1/2、1/2→3)汇聚节点经主链恰好 1 次(3 弹),验证 `orig_in_degree` 快照 P6-N63 + 嵌套递归 P6-N66*
|
||||
- [ ] **P-S5-ZM-01**(压力验收):**`ZoneManagerCs`(C# 子节点)** 驱动 Zone tick:同屏 64 个激活区域 × 1000 敌人,`_PhysicsProcess` 帧时间 < 1ms(Profiler 实测);若超过,启用 LOD 策略(屏外 Zone tick 频率降至 6fps)。**GDScript `zone_manager.gd` 不得在每 Zone 每帧上承载 O(敌人×区) 的内层循环**(与 §6.1 分工一致)。 — *⏸ 顺延:功能路径(GDScript `zone_manager.gd`,含 if 守卫 + 内层 while 速率限制)已实现并通过 P-S5-04;C# `ZoneManagerCs` 压力路径与 S0 起的 C# 热路径迁移一并按 R-08 策略顺延至专门优化轮(与 BulletManagerCs 等同状态)*
|
||||
- [x] **P-S5-AI-01**(ADR-A4):同屏 **20** 个带完整寻路的 Elite,`NavigationAgent2D` 路径跟随引起的 `EnemyManager`/`Navigation` 相关帧时间增量 **< 0.5ms**;超标则记录 Profiler 并切换 FlowField 方案,回写 `architecture_design.md` ADR-A4 结论段。 — *实测通过 2026-06-05:20 精英 `_update_pathfinding_movement` ≈0.0496ms/帧(10× 余量),定案 NavigationAgent2D(不切 FlowField);ADR-A4 结论段已回写。并发上限 MAX_PATHFINDING_ENEMIES=20 验证(25 精英→20 寻路+5 降级 Boid);reset 释放全部 Agent 节点;无精英时主循环零开销*
|
||||
|
||||
### 技术参考
|
||||
- `architecture_design.md` **ADR-A4**(精英寻路、`MAX_PATHFINDING_ENEMIES`、P-S5-AI-01)
|
||||
- `architecture_design.md §3.4.A`(_flatten_matrix P6-N20、_flatten_circuit P6-N57/63/64/66)
|
||||
- `architecture_design.md §3.4.C`(共鸣系统、_consumed 掩码 P6-N29)
|
||||
- `architecture_design.md §8 ADR-R5-N1`(ZoneManager 权威实现 P6-N71)
|
||||
- `advanced_mechanics_summons_and_environment.md §3.1/3.3`(MinionManager 方案 B)
|
||||
- `core_wand_design.md §2~3`(MATRIX 邻接表 P6-N13、CIRCUIT edges P6-N42、Feature Tags P6-N61)
|
||||
|
||||
### 出口检查
|
||||
> 能否用 CIRCUIT Core 构建双路元素炮台构建并打通 5 波?所有高级 Core 无双执行 / 空指针崩溃?**P-S5-ZM-01** 与 **P-S5-AI-01** 压力测试通过(或 ADR-A4 已更新为 FlowField 且复测通过)?"是"方可进入 S6。
|
||||
>
|
||||
> **状态(2026-06-05)**:✅ 功能验收 P-S5-01~07 + **P-S5-AI-01**(≈0.0496ms,ADR-A4 定案 NavigationAgent2D)全部实测通过,高级 Core 无双执行/崩溃。⏸ **P-S5-ZM-01**(C# `ZoneManagerCs` 压力)按 R-08 与 BulletManagerCs 等 C# 热路径一并顺延至专门优化轮——功能 GDScript 路径已通过 P-S5-04。
|
||||
> **集成(2026-06-05 完成)**:S5 系统已接入实际游戏流程并实测可玩——
|
||||
> - 商店池 = `SpellRegistry.get_all_ids()`,已含全部 S5 法术(LOGIC / summon / zone / 共鸣输入 water_wave·chain_bolt)。
|
||||
> - `combat_manager` 新增 **Core 切换**(商店内「法杖」按钮循环 `wand_basic→fast→memory→matrix_board→circuit_fork`),每 Core 载入演示默认 Deck;`install_spell` 对 MATRIX/CIRCUIT 改为**位置化填空槽**,`_rebuild_wand` 保留 `""` 空槽为 null。HUD/商店显示当前 Core 名。
|
||||
> - 实测:5 Core 循环切换均正确编译(记忆法杖 every_3 跨帧 `[0,0,1,0]`、矩阵板邻接、分叉回路双弹);共鸣经商店装配触发;summon/zone 作为首动作或经 `double_cast` 正常产出。
|
||||
> - **背包(2026-06-05 完成)**:点选式插槽编辑器(商店「🎒 背包」按钮),拓扑感知插槽网格 + 备牌区,支持插槽交换 / 移入移出备牌 / 即时重编译(详见 S3 出口检查段)。玩家现可在任意 Core 上自由排布法术(含 MATRIX 行 A/B、CIRCUIT 槽位、共鸣相邻)。
|
||||
> - **法杖存档(2026-06-05 完成)**:Core/deck/bench 已纳入 `ProfileManager` A/B 双槽(`schema_version` 升至 **2**)。`CombatManager` 经 `ProfileManager.set_wand_provider(self)` 注册,提供 `get_wand_save_data`/`apply_wand_save_data`(字段 `core_id`/`deck`/`bench`);`_collect_run_data` 写入 `wand`,`apply_run` 恢复。实测:自定义矩阵 Deck 存档→擦除→读档完整恢复;v1 旧档(无 wand)经 `_migrate_run` 升 v2 不崩溃保留默认;备牌持久化;A/B 交替写入保持。
|
||||
> - 余项(归 S6):真·拖拽手感、CIRCUIT 有向边可视化编辑器、Steam Cloud 批量写(ST-50)。
|
||||
|
||||
---
|
||||
|
||||
## S6 — 润色与发布 (Polish & Launch)
|
||||
|
||||
### 目标
|
||||
> **"60fps 帧率稳定(含 Profiler 数据)、20 波内容完整、手感丝滑、Endless 排行榜可用。"**
|
||||
|
||||
### 范围(优先级排序)
|
||||
|
||||
**P0 必须完成**
|
||||
- [x] **完整游戏外壳(启动→主菜单→游戏→暂停→结算→返回)** — *2026-06-05:商业级完整流程闭环。新增 `SceneManager`(Autoload,黑幕淡入淡出转场 + `start_mode` new/continue 路由)、`scenes/ui/splash.tscn`(启动画面:工作室名+游戏标题+版权,1.8s 自动/任意键跳过)、`scenes/ui/main_menu.tscn`(主菜单:新游戏/继续[无存档时禁用]/排行榜/设置/退出 + 语言切换 + 版本号);`main_scene` 改为 `splash.tscn`;`combat_s2.gd` 接入 SceneManager 启动模式(新游戏=`start_game`/继续=`resume_game`)、新增暂停菜单(ESC,`process_mode=ALWAYS` 暂停时仍响应,继续/设置/返回主菜单/退出)、结算屏加「返回主菜单」按钮。全流程实测通过:splash→menu 自动跳转、新游戏/继续读档(恢复 wave=7/gold=250)进战斗、ESC 暂停冻结世界、死亡结算→返回主菜单(死亡删档使继续按钮禁用)。修复 2 bug:转场 Tween 竞争(fade_in_first kill 复位回调致 is_transitioning 卡死)、暂停 process_mode 致 ESC/按钮失效。**待补**:主菜单/splash 美术背景、FTUE 首启难度引导*
|
||||
- [x] **程序化美术占位(视觉升级)** — *2026-06-05:从纯色几何方块升级为完整 bullet-heaven 视觉。新增 `shaders/bullet_glow.gdshader`(子弹加法混合发光圆:软核+外发光晕)、`shaders/enemy_circle.gdshader`(敌人体积感实体球:软边+左上高光+暗边描线)、`shaders/arena_grid.gdshader`(竞技场暗紫网格地面:细/粗双层网格+径向暗角)应用于两个 MMI + 背景 Polygon2D(z=-100);玩家标记升级为发光菱形(青光晕环+橙菱形+白核);`vfx_manager.gd` `_instantiate_vfx` 改为 `GPUParticles2D`(程序化爆裂粒子:球形发射+颜色渐变淡出+按特效 amount/speed,`one_shot` P6-N72,`play()` 触发 `restart`);`assets/ui/ui_theme.tres` 全局 UI Theme(按钮圆角6px+蓝紫边框+悬停高亮+禁用态,经 `gui/theme/custom` 全局应用所有 Control)。实测:150 弹同心圆弹幕发光叠加、Boss 体积球、网格地面、圆角按钮均渲染正确,0 报错。**全部为程序化生成,放真实素材(`res://audio/sfx/`、精灵 Atlas、VFX 场景)即整体替换,见 handbook/03**
|
||||
- [ ] 性能验收:确认所有 C# 热路径模块(`BulletManagerCs` / `EnemyManagerCs` / `SpatialGridCs` / `SpellEvaluatorCs` / `StatusManagerCs` / `ZoneManagerCs`)均满足各自帧预算(Profiler 实测);若发现遗漏的 GDScript 瓶颈,按 ADR-L1 规则迁入 C#(`architecture_design.md §6.1`)
|
||||
- [~] 三档渲染切换逻辑:实现 Area2D → Dirty Sync → MultiMeshInstance2D 渐进切换(`BATCH_DISABLE_PER_FRAME=100`,回滞阈值 `<1500`,`architecture_design.md §4.4`)— *2026-06-05:**直接实现 MultiMeshInstance2D 渲染(§4.4 最高档)**——此前子弹/敌人完全无渲染(仅 HUD+VFX 色块),现 `BulletManager.sync_multimesh`/`EnemyManager.sync_multimesh`(SoA 索引↔instance 索引共享,`visible_instance_count` 控制数量)+ 单位 QuadMesh + 敌人 per-instance 颜色/尺寸(Boss 大且醒目);`combat_s2.gd` 建子弹/敌人两个 MMI + 玩家标记 + 跟随 Camera2D,`_process` 每帧同步。实测渲染正确(红杂兵/品红 Boss/黄子弹)。**Area2D↔MultiMesh 三档切换 N/A**:碰撞自 S1 即为 SpatialGrid 唯一路径(从未建 Area2D),故 MultiMesh 单档覆盖全密度即可,无需 Area2D 档与回滞切换。待补(润色):Dirty Sync 视口剔除、子弹按 type 出图集、C# 化 transform 上传*
|
||||
- [~] Wave 1~20 完整内容(敌人配置表、Mini Boss Wave 8、Final Boss Wave 20)(`game_design.md §3.3`)— *核心已实现 2026-06-05:`wave_manager.gd` 20 波配置表(W1-5 数值 / W6-9 护甲 / W11-19 弹幕量增 / W15+ 精英寻路);敌人原型 `EnemyManager.Type`(BASIC/FAST/ARMORED/ELITE/MINIBOSS/BOSS)+ 按型速度/护甲;Mini Boss W8(450HP)、Final Boss W20(1800HP)+ 阶段系统(HP 66%/33% 进阶召援军)+ `BOSS_SPAWNED`;Boss 死亡即通关本波。Endless 数值膨胀。实测全部通过。**待补(润色)**:Boss 弹幕攻势(Phase2 走位)、元素抗性/反弹护盾怪、敌人 .tres 资源化、Boss JSON 驱动*
|
||||
- [ ] 20 张+ 法术卡(含所有 Tier 1/2/3)
|
||||
- [ ] **至少 7 种 Core** 可玩:`wand_basic`、`wand_fast`、`staff_long`、`matrix_board`、`circuit_fork`、`wand_memory`、`wand_eternal`(名称与数值以 `resources/cores/` 为准;与「5 种」旧表述对齐为 **7 种变体**)
|
||||
- [x] 死亡流程:结算屏、Run 截图(`get_viewport().get_texture().get_image()`,死亡动画第 1 帧后执行)— *2026-06-05:`combat_s2.gd` 结算屏(到达波次/击杀/用时/评分 + 排行榜 + 「再来一局」);`_save_run_screenshot()` 存 `user://runs/run_*.png`(结算屏非黑屏,P-S6-04 通过)。注:战场在 GAME_OVER 前已被 `_on_player_died` 重置,故截图为结算摘要屏而非死亡瞬间战场*
|
||||
- [x] Endless 模式 + 本地排行榜(波次主排名、同波次按时间升序;评分公式:`score = wave × 100000 + (86400 - elapsed_sec)`,P6-N19;本地存储于 `user://endless_records.json`)— *2026-06-05:`endless_records.gd`(Autoload `EndlessRecords`)HMAC-SHA256 签名(ST-40)+ 读取验签拒绝篡改(ST-41);排序波次降序/同波次用时升序;`compute_score` 实测 786386 正确。**修正一处真实 bug**:原按对象签名会因 JSON int↔float 往返破坏验签,改为对精确 JSON 字符串签名*
|
||||
|
||||
**P1 完成(时间允许)**
|
||||
- [ ] `DUAL_STREAM` Core feature 实现(奇数槽中间槽前流专属,P6-N12)
|
||||
- [ ] `ALWAYS_CAST_LAST`:尾槽为 MODIFIER/TRIGGER/LOGIC 时静默跳过(P6-N7)
|
||||
- [ ] `SHUFFLE_DECK`:ACTION 原子单元整体乱序(P5-N2)
|
||||
- [ ] `INFINITE_SPELLS`:配合 `heavy_cost` 的卖血流(P6-N18)
|
||||
- [ ] 敌人元素弱点 + 抗性 + 催化反应(StatusManager 交互矩阵)
|
||||
- [x] 音频系统(32 AudioStreamPlayer2D 池、0.1s 节流)— *2026-06-05:`audio_manager.gd`(Autoload `AudioManager`)32 池 round-robin + 每 sound_id 0.1s 节流 + 运行时建 SFX 总线 send→Master(受 SettingsManager 音量);EventBus 挂钩(ENEMY_KILLED/BULLET_HIT/PLAYER_DAMAGED/WAVE_COMPLETE/BOSS_SPAWNED/SHOP_OPENED/LEVEL_UP);无素材时程序化蜂鸣占位(放 `res://audio/sfx/<id>.ogg` 即替换)。实测池/节流/独立 id/未知 id 优雅 no-op/事件触发通过。**待补**:真实音效素材、BGM 分层*
|
||||
- [ ] 移动端适配(虚拟摇杆映射到 InputMap Action)
|
||||
- [ ] 完整图鉴系统(法术、Core、共鸣配方发现机制 P5-C2)
|
||||
- [x] 难度系统:提供初学者 / 标准 / 挑战三档(初学者:敌人 HP ×0.7、伤害 ×0.7;挑战:W20 Boss HP ×1.3、Wave 怪物数 ×1.2);难度选项在首次启动时通过 FTUE 引导选择,后续可在主菜单修改;难度配置持久化到 `user://save_data.json`(`difficulty: int`,0=初学者、1=标准、2=挑战)— *2026-06-05:`settings_manager.gd`(Autoload `SettingsManager`)三档难度 + 乘子(`enemy_hp_mult`/`player_dmg_taken_mult`/`wave_count_mult`/`boss_hp_mult`),接入 `PlayerStats.take_damage`(初学者 20→14 实测)、`WaveManager` 怪物数/血量/Boss 血(挑战 W1 20→24 实测);持久化 `save_data.json`(difficulty/locale/master_volume/ftue_done)。设置面板(难度/语言/音量滑条/清除数据)实测渲染。**FTUE 首启难度引导已完成 2026-06-05**:首次启动(`save_data.json` 无 `ftue_done`)点「新游戏」弹出三档难度选择界面(初学者/标准/挑战,颜色编码绿/蓝/红 + 各档说明文字 + 「设置中可改」提示);选定后 `SettingsManager.complete_ftue()` 设难度+标记+存档→进游戏;二次启动不再引导;ST-03 清除数据后重新触发。实测全通*
|
||||
- [x] 设置菜单 + 音量(Master 总线)+ 「清除所有本地数据」入口(ST-03)— *2026-06-05:`combat_s2.gd` 设置面板(商店「⚙ 设置」按钮打开);音量 HSlider→`AudioServer` Master 总线;`SettingsManager.clear_all_local_data()` 实测删除 save_data/endless_records/run_a·b/runs 目录并重置*
|
||||
|
||||
**P2 可选高级内容**
|
||||
- [ ] `LOGIC_LABEL / LOGIC_JUMP_IF`(寄存器条件跳转,仅限 DEBUG Core 解锁)
|
||||
- [ ] `LogicHandler` 接口(LOGIC 指令集 > 10 条时替换 match 硬编码,P6-N4 规范)
|
||||
- [ ] `anywhere_in_deck` O(N²) 共鸣扫描(仅传说配方)
|
||||
- [ ] SpellEvaluator 调试图(EditorPlugin 单步执行,Q2 预案)
|
||||
- [ ] `ResourceLoader.load_threaded_request` 异步预加载(SpellRegistry P1 路线图)
|
||||
|
||||
### 验收标准 (P0)
|
||||
|
||||
- [~] **P-S6-01**:Wave 1~20 无崩溃完整通关;Mini Boss Wave 8 及 Final Boss Wave 20 AI 正常(`game_design.md §3.3`)— *系统已实现并实测:20 波配置全部正确生成(杂兵/护甲/精英寻路计数核对);W8/W20 Boss 入场(450/1800 HP)+ 三阶段进阶(60%→P2、30%→P3 各召援军)+ `BOSS_SPAWNED`;Boss 死亡通关、普通波清空通关;护甲减伤(ARMORED 20→15)。完整 20 波连续通关压力测试待 S6 后续连跑*
|
||||
- [x] **P-S6-02**:同屏 500 敌人 + 1500 子弹稳定 60fps(Profiler 实测)— *实测通过 2026-06-05(RTX 2060):**实时 FPS=180**,`_physics_process` 实测帧时间 **0.91ms**;手工逐系统计时(60 reps):Bullet 22.95μs + Enemy 396.97μs + SpatialGrid 0.15μs + Status 0.23μs + Zone 0.18μs + MultiMesh sync 180.10μs = **总计 0.601ms = 16.67ms 预算的 3.6%**。全部 GDScript 回退路径(无 C# 热化),仍有极大裕量*
|
||||
- [x] **P-S6-03**:Endless 模式可进入;本地排行榜以波次为主排名、同波次按时间升序排(P6-N19 权威公式:在线分 = `wave × 100000 + (86400 - elapsed_sec)`;本地存储于 `user://endless_records.json`)— *实测 2026-06-05:排序 W12/250→W12/300→W8/90→W5/120;评分 786386 正确;HMAC 签名/验签/篡改拒绝通过。注:Endless「继续」入口(W20 后)属玩法接线,待补*
|
||||
- [x] **P-S6-04**:死亡时截图正确保存到 `user://runs/`(不截到黑屏)— *实测 2026-06-05:`user://runs/run_*.png` 已生成(结算屏内容,非黑屏)*
|
||||
- [x] **P-S6-05**:所有 P6-N 规范条目已在实际代码中验证(无违反)— *2026-06-05 审计通过(12 条约束)。发现并修复 1 件真实违反:`spell_evaluator.gd:504` `_acquire_ctx()` 在热路径 `execute_compiled`→`_physics_process` 中懒分配 `SpellContext.new()`(违反 P6-N3)。修复:在非热路径的 `compile_wand()` 阶段检测 `PERSISTENT_MEMORY` feature tag 时**预热分配** SpellContext(`_persistent_ctx[0]`),确保后续施法 `_acquire_ctx()` 走 dict.get() 取已有实例,不再触发 `new()`。P-S5-03 回归测试通过 `[0,0,1,0]`。其余 11 条全 PASS(ENEMY_STRIDE、BULLET_STRIDE、StatusID、compile_wand 参数序、zone_manager while+−=、vfx one_shot、PackedByteArray int()、CoreFeatureTag、registers 初始化、EventID 序列、ADR-A2 存档)*
|
||||
- [~] **P-S6-06**:本地化与 **ADR-A1** 对齐:`translations/` 下至少存在 **`zh_CN.po`、`zh_TW.po`(繁体)、`en.po`、`ja.po`** 四个文件(键集一致;`zh_TW`/`ja` 可先用占位译文,但商店页承诺的语言须在发售前填满);所有 S1–S6 玩家可见字符串均为 `tr("KEY")`(调试 `push_warning`/`push_error` 除外);业务脚本无裸中文字符串(以项目约定之静态检查或人工审计为准)— *2026-06-05 骨架完成:`translations/` 四个 `.po` 全部就位且**键集一致(实测 4 语言 0 缺键)**,全部填了真实译文(非占位);`Locale` Autoload 经 `TranslationServer.add_translation` 加载 + 切换;`combat_s2.gd` HUD/商店/结算/背包**主 UI 字符串已全部 `tr("KEY")` 化**(实测 en/ja/zh_TW 切换正确);商店内加语言切换按钮。**待补**:Core/法术/状态/Boss 的 `display_name`/`description`(现为 `WandPreset`/各 Def 内硬编码,需键化为 `SPELL_*`/`CORE_*`/`STATUS_*` 并补四语);裸中文静态检查脚本*
|
||||
- [x] **P-S6-07**:内存峰值实测(Wave 20 Boss 战场景):PC 端 RSS < 512MB,对照 `architecture_design.md §4.6` 内存预算表各类别实测均不超标 — *实测通过 2026-06-05(W20 满载:500敌+2000弹+500状态+32 Zone):`MEMORY_STATIC`=105.9MB + 引擎基础≈70MB → **估算 RSS≈175.9MB(34%,目标<512MB ✓)**;热数据(全 SoA+MultiMesh)仅 0.3MB;VRAM=92.6MB 为 GPU 显存不计入 PC RSS。Switch 共享内存(VRAM+RAM≈268MB)低于 SW-11 的 2.5GB 限额,实机测量待 Switch 移植时确认*
|
||||
|
||||
---
|
||||
|
||||
## 三、时间线估算 (Timeline Estimate)
|
||||
|
||||
> 以"单人 + 2D 美术外包"为基准估算,仅供参考,实际根据团队调整。
|
||||
|
||||
| 切片 | 状态 | 估算工时 | 主要阻塞风险 |
|
||||
| :--- | :---: | :--- | :--- |
|
||||
| **S0 技术骨架** | ✅ | 1~2 周 | R-01/R-03 已缓解(GDScript 回退路径) |
|
||||
| **S1 最小战斗** | ✅ | 1 周 | — |
|
||||
| **S2 核心循环** | ✅ | 1~2 周 | — |
|
||||
| **S3 法术管道** | ✅ | 2~3 周 | — |
|
||||
| **S4 战斗深度** | ✅ | 1~2 周 | — |
|
||||
| **S5 高级构建** | ✅* | 3~4 周 | 功能完成(*C# 压力 P-S5-ZM-01 顺延);_flatten_circuit 嵌套分叉已验、MinionManager AI 已验 |
|
||||
| **S6 润色发布** | ⬜ | 4~6 周 | 内容量(法术/敌人设计);平衡调整 |
|
||||
| **合计** | — | **~14~20 周** | R-08 C# 热路径迁移若触发则追加 1~2 周 |
|
||||
|
||||
---
|
||||
|
||||
## 四、跨切片技术约束速查 (Cross-Slice Constraints)
|
||||
|
||||
以下约束贯穿所有切片,违反时导致回归风险,开发时随时核查:
|
||||
|
||||
| 约束 | 来源规范 | 高风险操作 |
|
||||
| :--- | :--- | :--- |
|
||||
| 禁止 `DamageContext` / `SpellContext` 在战斗循环中 `new()` | P6-N49、P6-N3 | 任何 `apply_damage` 调用路径 |
|
||||
| `DamageContextPool.acquire()` 必须先 `ctx.reset()` | P6-N67 | 任何新增 `acquire` 调用 |
|
||||
| `StatusType.XXX` 枚举不存在,必须用 `StatusID.XXX` | P6-N30 | 新增状态效果代码 |
|
||||
| `CoreFeatureTag.XXX` 常量,禁止裸字符串 | ADR-R5-N2 | Feature Tag 判断逻辑 |
|
||||
| `ENEMY_STRIDE=8` 命名常量(禁止裸整数 8) | P6-N54 | EnemyManager 所有 SoA 访问 |
|
||||
| `BULLET_STRIDE=12` 命名常量(禁止裸整数 12) | `architecture_design.md §4.2` | BulletManager 所有 SoA 访问 |
|
||||
| `SpellContext.registers` 必须初始化长度 4 | P6-N48 | SpellContext 对象池 reset() |
|
||||
| `compile_wand(core, raw_deck)` 参数顺序 core 在前 | P6-N70 | SpellEvaluator 入口调用 |
|
||||
| ZoneManager tick 用 `while + -=`,不用 `if + =0` | P6-N71 | ZoneManager 任何 tick 逻辑 |
|
||||
| `VFXManager._instantiate_vfx` 必须 `one_shot=true` | P6-N72 | VFX 节点创建路径 |
|
||||
| `CHARGE_FIRED`(ID=14) 一次性;SpellEvaluator 订阅此而非 `CHARGE_STATE_CHANGED` | P6-N69 | 蓄力法术实现 |
|
||||
| `SubPayloadRegistry` 仅在关卡进行中只读,背包关闭后重编译 | P6-N10 | 任何支持"战斗中改装备"的功能 |
|
||||
| Run 存档 **A/B 双槽**、`schema_version`、**`_migrate_run` 链** | ADR-A2 | 单槽覆写、跳过迁移、损坏全丢 |
|
||||
| `EventID` **1–21** 与 `implementation_plan.md` §2.1 表一致(含 `ACHIEVEMENT_UNLOCKED=18`、`GAME_STATE_CHANGED=20`、`SPELL_DROP_PICKUP=21`);新增事件从 22 起递增并向 §2.1 表登记 | E-N1、certification ST-10、boss_design §6 | 在业务代码中硬编码与表冲突的事件整数 |
|
||||
| C# 内层循环(`for` / `while` 体内)禁止 `GodotObject.Call()` / `.Set()` | ADR-L1 规则 1 | `BulletManagerCs` / `EnemyManagerCs` 任何新增热路径代码 |
|
||||
| C# 访问 SoA 热数组必须通过 `PackedFloat32Array.AsSpan()` 获取 `Span<float>` 再做索引 | ADR-L1 规则 2 | 任何新增 C# SoA 读写代码 |
|
||||
| `PackedByteArray[i]` 赋值必须显式 `int()` 转换 | P6-N27 | `_visible_flags` 相关代码 |
|
||||
| `ProjectileDef.reset()` 必须重置 `spawn_position` | P6-N41 | BulletManager spawn 路径 |
|
||||
|
||||
---
|
||||
|
||||
## 五、参考文档导航 (Reference Map)
|
||||
|
||||
| 需要了解 | 首要参考文档 | 章节 |
|
||||
| :--- | :--- | :--- |
|
||||
| 局内 Run 存档 A/B 与迁移 | `technical/architecture_design.md` | ADR-A2 |
|
||||
| Elite / Boss 寻路 | `technical/architecture_design.md` | ADR-A4 |
|
||||
| 全局架构与层级关系 | `technical/architecture_design.md` | §2~4 |
|
||||
| SpellEvaluator 执行流 + 预编译 | `technical/architecture_design.md` | §3.4 |
|
||||
| BulletManager SoA 字段定义 | `technical/architecture_design.md` | §4.2 |
|
||||
| ZoneManager 完整实现 | `technical/architecture_design.md` | §8 ADR-R5-N1 |
|
||||
| EventBus 事件目录 | `technical/implementation_plan.md` | §2.1 |
|
||||
| DamageContext 字段权威 | `technical/implementation_plan.md` | §2.1 (P6-N49) |
|
||||
| EnemyManager SoA + LOD | `technical/implementation_plan.md` | §2.3.C |
|
||||
| PlayerManager 移动状态追踪 | `technical/implementation_plan.md` | §2.5.A |
|
||||
| VFXManager 完整实现 | `technical/implementation_plan.md` | §2.5.D |
|
||||
| DPS 环形缓冲区 | `technical/implementation_plan.md` | §2.5.E |
|
||||
| Core 插槽拓扑、Feature Tags | `design/core_wand_design.md` | §1~3 |
|
||||
| SpellEvaluator 两阶段接口 | `design/core_wand_design.md` | §6 |
|
||||
| 数值公式(XP/伤害/商店) | `design/numerical_design.md` | §1~3 |
|
||||
| 法术参数参考表 | `design/numerical_design.md` | §2.2 |
|
||||
| 状态效果与 DoT | `mechanics/combat_mechanics_depth.md` | §3~4 |
|
||||
| BulletContext 连锁弹射 | `mechanics/combat_mechanics_depth.md` | §2/5 |
|
||||
| StatusManager Tick 机制 | `mechanics/combat_mechanics_depth.md` | §3.2.5 |
|
||||
| COMBO_MARK / VULNERABILITY | `mechanics/consecutive_hits_stacking.md` | 全文 |
|
||||
| MinionManager / ZoneManager | `mechanics/advanced_mechanics_summons_and_environment.md` | §1/3 |
|
||||
| StatusID Autoload 规范 | `mechanics/advanced_mechanics_summons_and_environment.md` | §3.2 |
|
||||
| 运动学 / 物理扩展(P2/P3) | `mechanics/combat_mechanics_extensions_v2.md` | §1~5 |
|
||||
| 弹道修正器(正弦/环绕) | `mechanics/weapon_system_expansion.md` | §1 |
|
||||
| 全规范速查表 (P6-N\*) | `README.md` | §关键规范速查 |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,215 @@
|
||||
# Boss 系统架构设计
|
||||
|
||||
> **关联切片**:S6 P0(Mini Boss Wave 8 / Final Boss Wave 20)
|
||||
> **依赖系统**:EnemyManager、StatusManager、EventBus、VFXManager、ZoneManager
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计原则
|
||||
|
||||
- **阶段驱动(Phase-Driven)**:Boss 行为由明确的阶段(Phase)枚举驱动,阶段切换在 `_physics_process` 中以血量阈值触发,不使用独立 Timer Node。
|
||||
- **与 EnemyManager 解耦**:Boss 不进入 EnemyManager SoA(避免 stride 污染和 AI 逻辑膨胀),由独立的 `BossManager` Autoload 管理;但向 SpatialGrid 注册自身坐标,使子弹归航与 ZoneManager 能命中 Boss。
|
||||
- **数据驱动**:每个 Boss 的阶段配置存储于 `.tres` Resource(`BossPhaseDef`),美术/策划可在不修改代码的情况下调整阈值与攻击模式。
|
||||
- **EventBus 集成**:阶段切换、死亡均通过 EventBus 广播,HUD/BGM/成就系统订阅。
|
||||
|
||||
---
|
||||
|
||||
## 2. BossManager Autoload
|
||||
|
||||
```gdscript
|
||||
# boss_manager.gd (Autoload: BossManager)
|
||||
# Boss 实体数量极少(≤ 2 同屏),不使用 SoA,用 Array[BossInstance] 即可。
|
||||
|
||||
const MAX_BOSSES: int = 2 # 同屏 Boss 上限
|
||||
|
||||
var _active: Array[BossInstance] = []
|
||||
|
||||
func spawn_boss(boss_def_id: String, spawn_pos: Vector2) -> int:
|
||||
# 从 BossRegistry 加载 BossDef,实例化 BossInstance,注册到 SpatialGrid
|
||||
pass
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
for boss in _active:
|
||||
boss.tick(delta)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Phase 状态机模板
|
||||
|
||||
### 3.1 枚举与切换条件
|
||||
|
||||
每个 Boss 的阶段定义在 `BossPhaseDef` Resource 中:
|
||||
|
||||
```gdscript
|
||||
# boss_phase_def.gd
|
||||
class_name BossPhaseDef
|
||||
extends Resource
|
||||
|
||||
@export var phase_id: int # 阶段编号(0 起始)
|
||||
@export var hp_threshold: float # 进入此阶段的血量百分比(0.0~1.0),如 0.5 = 50% HP
|
||||
@export var attack_patterns: Array[String] # 此阶段可用的攻击模式 ID 列表
|
||||
@export var move_speed_mult: float = 1.0 # 移动速度倍率(狂暴阶段可设 1.5)
|
||||
@export var enrage_vfx: String = "" # 阶段切换时触发的 VFX ID(空=无)
|
||||
@export var phase_bgm_layer: int = 0 # 动态音乐层级(0=基础,1=紧张,2=狂暴)
|
||||
```
|
||||
|
||||
### 3.2 BossInstance 状态机
|
||||
|
||||
```gdscript
|
||||
# boss_instance.gd
|
||||
class_name BossInstance
|
||||
extends RefCounted
|
||||
|
||||
enum BossPhaseState {
|
||||
PHASE_0, # 初始阶段(100% HP)
|
||||
PHASE_1, # 受伤阶段(阈值由 BossDef 配置,通常 70%)
|
||||
PHASE_2, # 危机阶段(通常 40%)
|
||||
PHASE_DYING # 死亡动画阶段(HP ≤ 0,播放死亡序列后才真正移除)
|
||||
}
|
||||
|
||||
var boss_def: BossDef
|
||||
var hp: float
|
||||
var hp_max: float
|
||||
var position: Vector2
|
||||
var current_phase: BossPhaseState = BossPhaseState.PHASE_0
|
||||
var _attack_cooldown: float = 0.0
|
||||
var _pattern_index: int = 0 # 当前阶段攻击模式的轮询索引
|
||||
var _phase_just_changed: bool = false # 本帧刚切换阶段的标志,用于触发 enrage VFX
|
||||
|
||||
func tick(delta: float) -> void:
|
||||
_check_phase_transition()
|
||||
if current_phase == BossPhaseState.PHASE_DYING:
|
||||
_tick_dying(delta)
|
||||
return
|
||||
_tick_movement(delta)
|
||||
_tick_attack(delta)
|
||||
_sync_render()
|
||||
|
||||
# 血量阈值检查(每帧)
|
||||
func _check_phase_transition() -> void:
|
||||
var hp_pct: float = hp / hp_max
|
||||
var phases: Array = boss_def.phases # Array[BossPhaseDef],按 hp_threshold 降序排列
|
||||
for i in phases.size():
|
||||
var phase_def: BossPhaseDef = phases[i]
|
||||
if hp_pct <= phase_def.hp_threshold and int(current_phase) < i + 1:
|
||||
_enter_phase(i + 1, phase_def)
|
||||
break
|
||||
|
||||
func _enter_phase(new_phase: int, phase_def: BossPhaseDef) -> void:
|
||||
current_phase = new_phase as BossPhaseState
|
||||
_pattern_index = 0
|
||||
_attack_cooldown = 0.0
|
||||
# 阶段切换 VFX
|
||||
if phase_def.enrage_vfx != "":
|
||||
VFXManager.play(phase_def.enrage_vfx, position, 1.0)
|
||||
# 通知 EventBus(HUD 高亮、BGM 切换)
|
||||
EventBus.emit(EventID.BOSS_PHASE_CHANGED, {
|
||||
"boss_id": boss_def.id,
|
||||
"phase": new_phase,
|
||||
"bgm_layer": phase_def.phase_bgm_layer
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 攻击模式系统 (Attack Pattern)
|
||||
|
||||
攻击模式以 ID 字符串存储,在 `BossAttackRegistry` 中注册(类比 `SpellRegistry`),运行时由 `_tick_attack` 调用:
|
||||
|
||||
```gdscript
|
||||
# boss_instance.gd(续)
|
||||
func _tick_attack(delta: float) -> void:
|
||||
_attack_cooldown -= delta
|
||||
if _attack_cooldown > 0.0:
|
||||
return
|
||||
var phase_def: BossPhaseDef = boss_def.phases[int(current_phase)]
|
||||
if phase_def.attack_patterns.is_empty():
|
||||
return
|
||||
# 轮询攻击模式
|
||||
var pattern_id: String = phase_def.attack_patterns[
|
||||
_pattern_index % phase_def.attack_patterns.size()
|
||||
]
|
||||
_pattern_index += 1
|
||||
var cooldown: float = BossAttackRegistry.execute(pattern_id, self)
|
||||
_attack_cooldown = cooldown
|
||||
```
|
||||
|
||||
`BossAttackRegistry.execute(id, boss)` 返回该攻击模式的冷却时间(秒)。每种攻击模式是一个独立的 GDScript 函数或 Resource,通过 SpellEvaluator 发射子弹(走标准 BulletManager 路径)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 具体 Boss 设计模板
|
||||
|
||||
### 5.1 Mini Boss — Wave 8(待策划填充)
|
||||
|
||||
| 字段 | 值 |
|
||||
| :--- | :--- |
|
||||
| `boss_id` | `"mini_boss_w8"` |
|
||||
| `hp_max` | 2000(约为普通精英 6.6×) |
|
||||
| `move_speed` | 120(比杂鱼慢,靠范围攻击弥补) |
|
||||
| **Phase 0**(100%→60% HP)| 攻击模式:`"charge_slam"` + `"spread_shot_3"` |
|
||||
| **Phase 1**(60%→30% HP)| 追加:`"summon_minion_x2"`;move_speed_mult=1.2 |
|
||||
| **Phase 2**(30%→0% HP)| 追加:`"zone_aoe_fire"`;move_speed_mult=1.5;enrage_vfx="boss_enrage" |
|
||||
| 死亡事件 | `BOSS_KILLED`(EventID 待分配,建议 ID=15)|
|
||||
| 战场机制 | 无(Mini Boss 不设场地机制,Final Boss 才有)|
|
||||
|
||||
### 5.2 Final Boss — Wave 20(待策划填充)
|
||||
|
||||
| 字段 | 值 |
|
||||
| :--- | :--- |
|
||||
| `boss_id` | `"final_boss_w20"` |
|
||||
| `hp_max` | 10000 |
|
||||
| `move_speed` | 80(极慢,靠弹幕覆盖)|
|
||||
| **Phase 0**(100%→70% HP)| 攻击模式:`"spiral_shot_8"` + `"summon_turrets_x4"` |
|
||||
| **Phase 1**(70%→40% HP)| 追加:`"zone_ice_wall"`;场地:随机生成 4 个冰墙障碍(`ZoneManager` 驱动)|
|
||||
| **Phase 2**(40%→15% HP)| 追加:`"enrage_laser_sweep"`;场地收缩:边界每 10 秒向中心缩小 50px |
|
||||
| **Phase 3**(15%→0% HP)| 全弹幕覆盖;`"phase3_barrage"`;恢复部分 HP 后进入死亡序列(演出用)|
|
||||
| 死亡事件 | `BOSS_KILLED`(`EventID` **16**)+ `GAME_CLEARED`(**17**);阶段切换为 **15**(均以 `implementation_plan.md` §2.1 为准)|
|
||||
| 战场机制 | 场地收缩(Phase 2+)、冰墙障碍(Phase 1+)|
|
||||
|
||||
---
|
||||
|
||||
## 6. EventBus 扩展(Boss 专用事件)
|
||||
|
||||
> **权威登记**:以下 ID 已并入 `technical/implementation_plan.md` §2.1 事件目录与 `event_ids.gd`,本节表为语义说明(勿改号)。
|
||||
|
||||
| 事件 ID | 常量名 | Payload | 订阅方 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 15 | `BOSS_PHASE_CHANGED` | `{boss_id, phase, bgm_layer}` | HUD(阶段提示)、AudioManager(BGM 切换)|
|
||||
| 16 | `BOSS_KILLED` | `{boss_id, wave_num}` | WaveManager(进入结算)、AchievementManager |
|
||||
| 17 | `GAME_CLEARED` | `{elapsed_sec, wave_num}` | GameCycleManager(结算屏)、排行榜 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 与现有系统的接口约定
|
||||
|
||||
| 接口 | 调用方向 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| `SpatialGrid.register_boss(id, pos, radius)` | BossManager → SpatialGrid | Boss 作为可被子弹命中的实体,每帧更新坐标 |
|
||||
| `BulletManager._on_bullet_hit(bullet_id, target_id)` | BulletManager → BossManager | `target_id` 前缀区分:`enemy_xxx` vs `boss_xxx`(或通过 faction 位掩码判断)|
|
||||
| `ZoneManager.spawn_zone(...)` | BossAttackPattern → ZoneManager | Boss 攻击模式可生成 Zone(如冰墙、火圈)|
|
||||
| `VFXManager.play(vfx_id, pos, scale)` | BossInstance → VFXManager | 阶段切换 + 死亡演出 VFX |
|
||||
| `SpellEvaluator.execute_boss_pattern(pattern_id, origin)` | BossAttackRegistry → SpellEvaluator | Boss 弹幕走标准 SpellEvaluator 路径,复用子弹生成管线 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 目录规范
|
||||
|
||||
```text
|
||||
scripts/autoloads/
|
||||
boss_manager.gd # Autoload BossManager
|
||||
scripts/domain/boss/
|
||||
boss_def.gd # class_name BossDef (Resource)
|
||||
boss_phase_def.gd # class_name BossPhaseDef (Resource)
|
||||
boss_instance.gd # class_name BossInstance (RefCounted)
|
||||
boss_attack_registry.gd # BossAttackRegistry(类比 SpellRegistry)
|
||||
resources/bosses/
|
||||
mini_boss_w8.tres
|
||||
final_boss_w20.tres
|
||||
resources/boss_patterns/
|
||||
charge_slam.tres
|
||||
spread_shot_3.tres
|
||||
spiral_shot_8.tres
|
||||
# ...
|
||||
```
|
||||
@@ -0,0 +1,815 @@
|
||||
# 程序架构设计与实施计划 (Implementation Plan & Technical Architecture)
|
||||
|
||||
## 1. 核心设计原则 (Core Design Principles)
|
||||
|
||||
### 1.1 性能优先 (Performance First)
|
||||
鉴于已确定的"海量弹幕+同屏千人"需求,本项目的核心代码将偏离标准的 Godot 节点组件模式,采用更底层的 **Data-Oriented (面向数据)** 思想。
|
||||
* **原则 A**: 战斗循环中尽量避免 `new`(GDScript 实例化)。所有高频对象(Context, Bullet, Enemy)必须池化。
|
||||
* **原则 B**: 逻辑与渲染分离。位置更新在 Autoload Manager 的 `_physics_process` 中批量处理,节点的 `_process` 仅负责同步 View。
|
||||
* **原则 C**: 避免匿名函数与 Lambda。解释器核心循环使用 `match` 和 `for` 循环,减少函数调用开销。
|
||||
|
||||
### 1.2 极度模块化 (Extreme Modularity)
|
||||
法术系统的每个功能点(如:追踪、分裂)都必须封装为独立的 `Strategy` 类,通过 ID 动态加载。核心系统不能直接依赖具体法术。
|
||||
|
||||
**法术注册表 (Spell Registry)** 技术方案:
|
||||
```gdscript
|
||||
# spell_registry.gd (Autoload)
|
||||
# 启动时自动扫描 res://resources/spells/ 目录,加载所有 .tres SpellNode Resource
|
||||
var _registry: Dictionary = {} # spell_id (String) -> SpellNode
|
||||
|
||||
func _ready() -> void:
|
||||
# ⚠️ F13 注意:当前使用同步 load(),主线程阻塞直到所有法术资源加载完毕。
|
||||
# 法术数量较少(< 50)时可接受;若未来资源增多,应改用异步预加载:
|
||||
# ResourceLoader.load_threaded_request(path) → 在 Loading 界面 _process 中轮询
|
||||
# load_threaded_get_status(),加载完成后发出信号,不阻塞主线程。
|
||||
# P2 路线图:引入 SpellRegistry.preload_async(on_complete: Callable) 接口。
|
||||
var dir := DirAccess.open("res://resources/spells/")
|
||||
if dir == null:
|
||||
push_error("SpellRegistry: 无法打开 res://resources/spells/ 目录")
|
||||
return
|
||||
dir.list_dir_begin()
|
||||
var file := dir.get_next()
|
||||
while file != "":
|
||||
if file.ends_with(".tres"):
|
||||
var res: SpellNode = load("res://resources/spells/" + file)
|
||||
if res != null:
|
||||
_registry[res.id] = res
|
||||
else:
|
||||
push_warning("SpellRegistry: 加载失败 - %s" % file)
|
||||
file = dir.get_next()
|
||||
dir.list_dir_end() # ⚠️ 必须调用,否则目录句柄不会释放
|
||||
|
||||
func get(id: String) -> SpellNode:
|
||||
return _registry.get(id, null)
|
||||
|
||||
func all_ids() -> Array:
|
||||
return _registry.keys() # ⚠️ keys() 返回无类型 Array,不可声明为 Array[String]
|
||||
# GDScript 4.x 中 Dictionary.keys() 的静态返回类型为 Array
|
||||
```
|
||||
* 新增法术只需在 `res://resources/spells/` 中放入一个 `.tres` 文件,无需修改任何代码。
|
||||
* `SpellNode.id` 字段必须全局唯一,推荐使用 `category_name` 格式(如 `modifier_damage_plus`)。
|
||||
|
||||
### 1.3 零 GC 与内存高效架构 (Zero-GC & Memory Efficiency)
|
||||
GDScript 的垃圾回收虽不如 JavaScript 激进,但频繁实例化 RefCounted 对象同样会带来性能抖动。架构上强制执行以下模式:
|
||||
* **Struct-like Classes**: 核心算子类(如 `Vector2`)优先使用 GDScript 内置值类型(Value Type),它们在栈上分配,无 GC 压力。自定义数据类继承 `RefCounted` 并通过对象池复用。
|
||||
* **Ring Buffers**: 音频、粒子、伤害数字使用环形缓冲区,覆盖旧数据而非销毁。
|
||||
* **Integer IDs > Objects**: 在 `EventBus` 和 ECS 中,传递整数 ID (entity_id) 而非传递整个对象引用,减少引用计数复杂度。
|
||||
|
||||
---
|
||||
|
||||
## 2. 模块详细设计 (Detailed Module Design)
|
||||
|
||||
### 2.1 核心库层 (Layer 0: Core Libs)
|
||||
|
||||
| 模块 | 类名/文件 | 职责说明 | 性能关键点 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Object Pool** | `ObjectPool` (Autoload) | 通用对象池,支持 `reset()` 接口。 | 使用 `Array` 做栈,`append`/`pop_back` 操作。 |
|
||||
| **Event Bus** | `EventSystem` (Autoload) | 类型安全的全局事件总线。 | 使用整数 ID 代替字符串 Key,减少哈希查找开销;也可直接使用 Godot 原生 Signal。 |
|
||||
| **Spatial Hash** | `SpatialGrid` | 2D 空间网格,替代 Quadtree。 | 使用 `PackedInt32Array` 一维数组模拟二维网格,直接索引访问 `grid[x + y * width]`。 |
|
||||
| **Timer** | `TimeManager` (Autoload) | 缩放时间、帧率节流控制。 | 支持 `GameTick` (physics) 与 `RenderFrame` (process) 分离。 |
|
||||
|
||||
#### EventBus 事件目录 (Event Catalog)
|
||||
|
||||
所有跨系统通信必须通过此表对齐,不得私自定义事件 ID。
|
||||
|
||||
> **E-N1 规范**:事件 ID 必须使用以下具名常量(`EventID` 枚举或 Autoload 常量),禁止在业务代码中直接写裸整数,防止撞号和可读性问题。
|
||||
```gdscript
|
||||
# event_ids.gd (Autoload: EventID)
|
||||
const APPLY_DAMAGE: int = 1
|
||||
const ENEMY_KILLED: int = 2
|
||||
const SPELL_CAST_BEGIN: int = 3
|
||||
const BULLET_HIT: int = 4
|
||||
const PLAYER_DAMAGED: int = 5
|
||||
const PLAYER_DIED: int = 6
|
||||
const WAVE_COMPLETE: int = 7
|
||||
const STATUS_APPLIED: int = 8
|
||||
const CHARGE_STATE_CHANGED: int = 9 # 停步蓄力进度变化 → UIManager
|
||||
const ON_PLAYER_HURT: int = 10 # 玩家受到伤害时触发(用于受击释放法术/反弹尖刺等 OnHurt 钩子)
|
||||
const MINION_SPAWNED: int = 11 # 召唤物创建 → UIManager 更新召唤物计数显示
|
||||
const MINION_EXPIRED: int = 12 # 召唤物消亡 → UIManager 更新召唤物计数显示
|
||||
const DASH_TRIGGERED: int = 13 # 玩家执行冲刺动作时触发(dash 输入按下瞬间)→ SpellEvaluator(OnDash 钩子,dash-wave Core 附加法术), UIManager(清除蓄力进度 UI)
|
||||
# 负载: { "caster_id": int, "stationary_time": float }(冲刺前已积累的静止时长,供 dash-wave Core 读取)
|
||||
# ⚠️ 与 CHARGE_FIRED(14) 的区别:DASH_TRIGGERED 由冲刺操作触发;CHARGE_FIRED 由停步静止时长首次超阈值触发(无冲刺)。
|
||||
const CHARGE_FIRED: int = 14 # 停步蓄力首次达到阈值时发出(仅一次,非每帧)→ SpellEvaluator(执行超载版法术)
|
||||
# 区别于 CHARGE_STATE_CHANGED(每帧进度,仅供 UI):
|
||||
# CHARGE_STATE_CHANGED 持续更新进度环动画;
|
||||
# CHARGE_FIRED 是一次性触发信号,SpellEvaluator 只订阅此事件来实际释放法术。
|
||||
# 负载: { "caster_id": int }
|
||||
const BOSS_PHASE_CHANGED: int = 15 # boss_design.md §6:Boss 阶段切换 → HUD、AudioManager(BGM 分层)
|
||||
const BOSS_KILLED: int = 16 # boss_design.md §6:Boss 死亡 → WaveManager、AchievementManager
|
||||
const GAME_CLEARED: int = 17 # boss_design.md §6:通关 → GameCycleManager、排行榜
|
||||
const ACHIEVEMENT_UNLOCKED: int = 18 # certification_checklist.md ST-10:Steam 成就解锁(须幂等)
|
||||
# 负载: { "achievement_id": String }
|
||||
const SETTINGS_CHANGED: int = 19 # architecture_design.md ADR-C1 / SettingsManager:字体缩放、高对比度等
|
||||
# 负载: { "key": String }(例:`font_scale`、`high_contrast`)
|
||||
const GAME_STATE_CHANGED: int = 20 # architecture_design.md §5.1:FSM 状态切换(GameCycleManager 发出)
|
||||
# 负载: { "prev": GameState, "next": GameState }
|
||||
# 订阅方:UIManager(路由 UI 切换)、AudioManager(切换 BGM/SFX)
|
||||
const SPELL_DROP_PICKUP: int = 21 # architecture_design.md §5.3:法术掉落拾取弹 UI 事件
|
||||
# 负载: { "spell_id": String, "auto_equip": bool }
|
||||
# 发出方:DropManager(掉落拾取)、ShopManager.offer_free_spell()
|
||||
# 订阅方:UIManager(弹背包选 Core 界面)、PlayerManager(auto_equip=true 时直接装备)
|
||||
# 新增事件从 22 起递增,并向下表登记
|
||||
```
|
||||
|
||||
| 事件 ID | 具名常量 | 名称 | 负载内容 | 发出方 | 订阅方 |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| `1` | `EventID.APPLY_DAMAGE` | `APPLY_DAMAGE` | `damage_context_id: int` | `SpellEvaluator` / `StatusManager` | `EnemyManager` |
|
||||
| `2` | `EventID.ENEMY_KILLED` | `ENEMY_KILLED` | `enemy_id: int, killer_id: int` | `EnemyManager` | `PlayerStats`, `DropManager`, `MissionTracker` |
|
||||
| `3` | `EventID.SPELL_CAST_BEGIN` | `SPELL_CAST_BEGIN` | `caster_id: int, deck_id: int` | `SpellEvaluator` | `VFXManager`, `AudioManager` |
|
||||
| `4` | `EventID.BULLET_HIT` | `BULLET_HIT` | `bullet_id: int, target_id: int` | `BulletManager` | `SpellEvaluator`(proc), `VFXManager` |
|
||||
| `5` | `EventID.PLAYER_DAMAGED` | `PLAYER_DAMAGED` | `amount: float, source_id: int` | `PlayerManager` | `UIManager`, `AudioManager` |
|
||||
| `6` | `EventID.PLAYER_DIED` | `PLAYER_DIED` | 无 | `PlayerManager` | `GameCycleManager` |
|
||||
| `7` | `EventID.WAVE_COMPLETE` | `WAVE_COMPLETE` | `wave_num: int` | `WaveManager` | `ShopManager`, `GameCycleManager` |
|
||||
| `8` | `EventID.STATUS_APPLIED` | `STATUS_APPLIED` | `target_id: int, status_type: int, stacks: int` | `StatusManager` | `VFXManager`, `EnemyManager` |
|
||||
| `9` | `EventID.CHARGE_STATE_CHANGED` | `CHARGE_STATE_CHANGED` | `caster_id: int, progress: float` | `PlayerManager` | `UIManager` |
|
||||
| `10` | `EventID.ON_PLAYER_HURT` | `ON_PLAYER_HURT` | `amount: float, source_id: int` | `PlayerManager` | `SpellEvaluator`(OnHurt 钩子), `VFXManager`, `AudioManager` |
|
||||
| `11` | `EventID.MINION_SPAWNED` | `MINION_SPAWNED` | `minion_id: int, owner_id: int, current_count: int` | `MinionManager` | `UIManager`(召唤物计数显示) |
|
||||
| `12` | `EventID.MINION_EXPIRED` | `MINION_EXPIRED` | `minion_id: int, owner_id: int, current_count: int` | `MinionManager` | `UIManager`(召唤物计数显示) |
|
||||
| `13` | `EventID.DASH_TRIGGERED` | `DASH_TRIGGERED` | `caster_id: int, stationary_time: float` | `PlayerManager` | `SpellEvaluator`(OnDash钩子,dash-wave Core 附加法术), `UIManager`(蓄力UI清除) |
|
||||
| `14` | `EventID.CHARGE_FIRED` | `CHARGE_FIRED` | `caster_id: int` | `PlayerManager` | `SpellEvaluator`(执行超载版法术) |
|
||||
| `15` | `EventID.BOSS_PHASE_CHANGED` | `BOSS_PHASE_CHANGED` | `boss_id, phase, bgm_layer` | `BossManager` | `UIManager`, `AudioManager` |
|
||||
| `16` | `EventID.BOSS_KILLED` | `BOSS_KILLED` | `boss_id, wave_num` | `BossManager` | `WaveManager`, `AchievementManager` |
|
||||
| `17` | `EventID.GAME_CLEARED` | `GAME_CLEARED` | `elapsed_sec, wave_num` | `BossManager` / `GameCycleManager` | `GameCycleManager`, 排行榜 |
|
||||
| `18` | `EventID.ACHIEVEMENT_UNLOCKED` | `ACHIEVEMENT_UNLOCKED` | `achievement_id: String` | 各游戏系统 | `AchievementManager` → Steam |
|
||||
| `19` | `EventID.SETTINGS_CHANGED` | `SETTINGS_CHANGED` | `key: String` | `SettingsManager` | `UIManager`(全局主题/字体重载) |
|
||||
| `20` | `EventID.GAME_STATE_CHANGED` | `GAME_STATE_CHANGED` | `prev: int, next: int` | `GameCycleManager` | `UIManager`, `AudioManager` |
|
||||
| `21` | `EventID.SPELL_DROP_PICKUP` | `SPELL_DROP_PICKUP` | `spell_id: String, auto_equip: bool` | `DropManager`, `ShopManager` | `UIManager`, `PlayerManager` |
|
||||
> 新增事件从 **22** 起递增,并向下表登记。规则:编号永不重用(可标 DEPRECATED 但不删除)。
|
||||
|
||||
> **E-N2 跨语言边界协议(GDScript ↔ C#)**:`architecture_design.md §6` 规定跨语言只传基本类型(int/float/Vector2/Godot.Array)。`DamageContext`(RefCounted)**不得直接穿越语言边界**。约定如下:
|
||||
> - `EnemyManager` 遵循与 `BulletManager` 相同的 **ADR-L1 规则 4** 模式:`enemy_manager.gd`(GDScript Autoload)持有 `_data: PackedFloat32Array` 并对外暴露接口;`EnemyManagerCs.cs`(C# 子节点)处理 `_PhysicsProcess` 热路径(Boid 分离力、SoA 积分),预期帧时间 GDScript ≈6ms → C# ≈0.6ms(见 §6.1 语言分工表)。
|
||||
> - `APPLY_DAMAGE` 事件的处理逻辑位于 GDScript Autoload 层,负载 `damage_context_id: int` 是 `DamageContextPool` 的槽位索引,GDScript 层调用 `DamageContextPool.get_context(id)` 取得对象后处理伤害,处理完毕立即 `release(id)`。`DamageContext` 不穿越 GDScript ↔ C# 语言边界。
|
||||
|
||||
> **DamageContextPool 定义**:E-N2 中引用的 `DamageContextPool` 为伤害上下文对象池 Autoload,定义如下:
|
||||
> ```gdscript
|
||||
> # ── damage_context.gd ──────────────────────────────────────────────────────────
|
||||
> # DamageContext 数据类必须单独放在 damage_context.gd 中,
|
||||
> # 禁止将 class_name DamageContext 写在 damage_context_pool.gd 里(否则 Autoload 实例自身就是
|
||||
> # DamageContext 类型,导致 _pool: Array[DamageContext] 存入了具有池管理方法的对象,语义错误)。
|
||||
> class_name DamageContext
|
||||
> extends RefCounted
|
||||
> var base_damage: float = 0.0
|
||||
> var mult: float = 1.0
|
||||
> var damage_type: int = DamageType.PHYSICAL # 使用枚举常量,禁止裸整数
|
||||
> var owner_id: int = -1
|
||||
> var source_tags: int = 0
|
||||
> var is_crit: bool = false # 影响暴击字体/特效(见 combat_mechanics_depth.md §4)
|
||||
> var pierce_rate: float = 0.0 # 护甲穿透率 (0.0~1.0),EnemyManager.apply_damage() 中使用
|
||||
>
|
||||
> # ── damage_context_pool.gd (Autoload: DamageContextPool) ──────────────────────
|
||||
> # 本文件不声明 class_name;由 Godot Project Settings 注册为 Autoload "DamageContextPool"。
|
||||
> # 采用固定大小槽位数组 + PackedInt32Array 空闲 ID 栈:O(1) 申请/归还,零动态分配,零 GC。
|
||||
> const POOL_SIZE: int = 64
|
||||
> var _slots: Array[DamageContext] # 固定槽位,索引即 ID(预分配,_ready() 填充)
|
||||
> var _free_ids: PackedInt32Array # 空闲 ID 栈(append/pop_back,O(1),零 GC)
|
||||
> var _in_use: PackedByteArray # 占用标记:1=已 acquire;防重复 release
|
||||
>
|
||||
> func _ready() -> void:
|
||||
> _slots.resize(POOL_SIZE)
|
||||
> _free_ids.resize(POOL_SIZE)
|
||||
> _in_use.resize(POOL_SIZE)
|
||||
> for i in POOL_SIZE:
|
||||
> _slots[i] = DamageContext.new()
|
||||
> _free_ids[i] = POOL_SIZE - 1 - i # 初始化:index 0 最先被弹出
|
||||
> _in_use[i] = 0
|
||||
>
|
||||
> # ⚠️ 必须先调用 ctx.reset(),防止从池中取出的旧实例残留 source_tags 等脏数据。
|
||||
> func acquire(base_dmg: float, m: float, dtype: int, owner: int,
|
||||
> crit: bool = false, pierce: float = 0.0) -> int:
|
||||
> if _free_ids.is_empty():
|
||||
> push_warning("DamageContextPool: 池已耗尽,动态扩展(POOL_SIZE=%d 建议上调)" % POOL_SIZE)
|
||||
> var new_id: int = _slots.size()
|
||||
> _slots.append(DamageContext.new()); _in_use.append(0)
|
||||
> return _fill_slot(new_id, base_dmg, m, dtype, owner, crit, pierce)
|
||||
> var id: int = _free_ids[_free_ids.size() - 1]
|
||||
> _free_ids.resize(_free_ids.size() - 1) # pop_back,O(1)
|
||||
> return _fill_slot(id, base_dmg, m, dtype, owner, crit, pierce)
|
||||
>
|
||||
> func _fill_slot(id: int, base_dmg: float, m: float, dtype: int, owner: int,
|
||||
> crit: bool, pierce: float) -> int:
|
||||
> var ctx: DamageContext = _slots[id]
|
||||
> ctx.reset() # 清除所有残留字段后再覆写,保证每次 acquire 的上下文状态干净
|
||||
> ctx.base_damage = base_dmg; ctx.mult = m
|
||||
> ctx.damage_type = dtype; ctx.owner_id = owner
|
||||
> ctx.is_crit = crit; ctx.pierce_rate = pierce
|
||||
> _in_use[id] = 1
|
||||
> return id
|
||||
>
|
||||
> func get_context(id: int) -> DamageContext:
|
||||
> if id < 0 or id >= _slots.size() or _in_use[id] == 0: return null
|
||||
> return _slots[id]
|
||||
>
|
||||
> func release(id: int) -> void:
|
||||
> if id < 0 or id >= _slots.size() or _in_use[id] == 0:
|
||||
> push_warning("DamageContextPool.release: 无效或重复归还 ID=%d" % id)
|
||||
> return
|
||||
> _in_use[id] = 0
|
||||
> _free_ids.append(id) # push_back,O(1),不触发 GC
|
||||
>
|
||||
> func reset() -> void:
|
||||
> # GameCycleManager._reset_all_managers() 在波次/Run 重置时调用。
|
||||
> # 将所有 in-use 槽位强制归还(防止跨波次泄漏),清空空闲 ID 栈后重建。
|
||||
> _free_ids.clear()
|
||||
> for i in _slots.size():
|
||||
> _in_use[i] = 0
|
||||
> _free_ids.append(i)
|
||||
> ```
|
||||
> **使用约定**:`SpellEvaluator` 发出 `APPLY_DAMAGE` 前调用 `DamageContextPool.acquire(...)` 获取整数 ID;
|
||||
> `EnemyManager` 收到事件后调用 `get_context(id)` 处理伤害,处理完毕立即调用 `release(id)` 归还,
|
||||
> 不得持有 DamageContext 跨帧引用。
|
||||
|
||||
### 2.2 法术解释器层 (Layer 1: Spell Virtual Machine)
|
||||
|
||||
这是本项目的"CPU",负责执行 `SpellNode` 指令。
|
||||
|
||||
#### 数据结构
|
||||
|
||||
**SpellDeck** — 运行时执行游标
|
||||
```gdscript
|
||||
class_name SpellDeck
|
||||
# _nodes: Array[SpellNode],预编译后不再修改
|
||||
# _cursor: int,当前执行位置
|
||||
#
|
||||
# Scope 边界标记:预编译时每个 SCOPE_OPEN 节点(LOGIC_IF_*、TRIGGER 后续体)
|
||||
# 在 _nodes 中紧跟虚拟节点 SpellNode{type=SCOPE_CLOSE, id="_scope_end"}。
|
||||
# skip_until_scope_end() 推进 cursor 直到遇到第一个 SCOPE_CLOSE(深度计数器处理嵌套)。
|
||||
# consume_until_scope_end() 同理,返回中间节点序列用于打包 SubPayload。
|
||||
# O(N) 线性扫描定位边界,N ≤ MAX_OPS_PER_CPU × cpu_limit。
|
||||
func has_next() -> bool: return _cursor < _nodes.size()
|
||||
func pop() -> SpellNode:
|
||||
# 完整实现须包含 _consumed 掩码跳过(见 architecture_design.md §3.4.C)
|
||||
if _cursor >= _nodes.size(): return null
|
||||
var node := _nodes[_cursor]
|
||||
_cursor += 1 # GDScript 不支持后置 ++ 运算符
|
||||
return node
|
||||
func skip_until_scope_end() -> void: ...
|
||||
func consume_until_scope_end() -> Array[SpellNode]: ...
|
||||
```
|
||||
|
||||
**CompiledDeck** — 预编译产物,关卡期间只读
|
||||
```gdscript
|
||||
class_name CompiledDeck
|
||||
# compile_wand(core: CoreDefinition, raw_deck: SpellDeck) -> CompiledDeck
|
||||
# 由 SpellEvaluator 在背包关闭时调用
|
||||
#
|
||||
# nodes: Array[SpellNode] 拓扑扁平化 + SCOPE 虚节点插入后的线性序列
|
||||
# label_table: Dictionary { label_id: int -> node_index: int }(LOGIC_LABEL 专用)
|
||||
# sub_payload_ids: Array[int] 本 Deck 预编译时注册到 SubPayloadRegistry 的所有 ID
|
||||
# topology_type: int 0=LINEAR 1=MATRIX 2=CIRCUIT(影响 _flatten_* 选择)
|
||||
# checksum: int 插槽内容哈希,用于判断是否需要重新编译
|
||||
# 算法:多项式滚动哈希(对换位敏感,避免 [A,B] 与 [B,A] 产生相同 checksum):
|
||||
# checksum = topology_type ^ slot_count ^ (grid_cols << 8) ^ hash(core.id)
|
||||
# // grid_cols 防止不同列宽的 MATRIX Core 碰撞(2×4 与 4×2 slot_count 相同)
|
||||
# // hash(core.id) 防止不同 Core ID 但拓扑相同时 checksum 碰撞导致跳过重编译
|
||||
# for i in range(nodes.size()):
|
||||
# checksum = checksum * 31 ^ hash(nodes[i].id)
|
||||
#
|
||||
# 运行时 SpellEvaluator 从 CompiledDeck 构造一个 SpellDeck(拷贝 nodes 并重置 cursor)
|
||||
var nodes: Array[SpellNode]
|
||||
var label_table: Dictionary
|
||||
var sub_payload_ids: Array[int]
|
||||
var topology_type: int = 0
|
||||
var checksum: int = 0
|
||||
```
|
||||
|
||||
**CastState** — 单次 execute 调用的临时状态
|
||||
```gdscript
|
||||
# trigger_depth: int,当前 execute_sub 嵌套深度(防止 TRIGGER 无限递归)
|
||||
# registers: PackedFloat32Array,长度 4,R1-R4
|
||||
# ⚠️ 重要区分:
|
||||
# CastState.registers → 单次 execute() 内的临时工作寄存器,每次 execute() 开始时重置,不跨调用保留
|
||||
# SpellContext.registers → 跨帧持久寄存器(见 architecture_design.md §3.2)
|
||||
# LOGIC_EVERY_N_SHOTS【必须】使用 SpellContext.registers(否则计数器每帧重置,永远等效为"每次都触发")。
|
||||
# _execute_logic() 收到 LOGIC_EVERY_N_SHOTS 时通过 spell_ctx 参数访问 spell_ctx.registers[0]。
|
||||
```
|
||||
|
||||
#### 执行流程 (`SpellEvaluator.execute`)
|
||||
```gdscript
|
||||
# MAX_OPS_PER_CPU = 40:每点玩家属性 cpu_limit 换算为 40 步执行额度
|
||||
# 运行时:_max_ops = PlayerStats.cpu_limit × MAX_OPS_PER_CPU(默认 cpu_limit=5 → MAX_OPS=200)
|
||||
# 超过限制立即中断,防止图灵完备构建死循环
|
||||
const MAX_OPS_PER_CPU: int = 40
|
||||
const MAX_TRIGGER_DEPTH: int = 3 # execute_sub 嵌套最大深度;超过 3 层时 TRIGGER 不再触发新的 SubPayload
|
||||
# 分工:MAX_OPS 管单次施法内指令总量;MAX_TRIGGER_DEPTH 管跨弹体调用深度
|
||||
# ⚠️ 超限时不得静默失效:当 execute_sub 被 TRIGGER_DEPTH 隔断时,
|
||||
# EventBus 发出 SPELL_CAST_BEGIN 事件并附带 flag "depth_exceeded=true",
|
||||
# UIManager 订阅该 flag,在 HUD 显示短暂黄色闪烁("[法术链太深,部分触发已忽略]")。
|
||||
# 开发期保留 push_warning;正式版将 push_warning 改为发出事件。
|
||||
var _max_ops: int = 200 # 战斗开始时更新:_max_ops = PlayerStats.cpu_limit * MAX_OPS_PER_CPU
|
||||
|
||||
while deck.has_next() and ops_count < _max_ops:
|
||||
ops_count += 1
|
||||
var node: SpellNode = deck.pop()
|
||||
match node.type:
|
||||
SpellType.ACTION:
|
||||
_push_projectile(node, context)
|
||||
SpellType.MODIFIER:
|
||||
node.apply(context) # 直接修改 context.stats
|
||||
# Multicast 边缘规则:若 Deck 剩余法术数 < draw_count,以"尽力而为"策略执行
|
||||
# (弹出现有法术直至 Deck 耗尽,不崩溃),法杖编辑 UI 对此显示黄色警告。
|
||||
SpellType.TRIGGER:
|
||||
# 实现方案:预编译时打包 SubPayload,运行时查表执行
|
||||
# 1. 将 TRIGGER 后续的 SpellNode 序列打包为 SubPayload,注册到全局表,得到整数 ID
|
||||
# 2. 子弹在 PackedFloat32Array 中仅携带 on_hit_payload_id (int)
|
||||
# 3. 命中时 BulletManager 查表并调用 SpellEvaluator.execute_sub(payload_id, hit_pos, owner_id)
|
||||
# 这样子弹逻辑与 PackedFloat32Array 热数据完全解耦
|
||||
var payload_id: int = SubPayloadRegistry.register(deck.consume_until_scope_end())
|
||||
context.current_payload.on_hit_payload_id = payload_id
|
||||
SpellType.LOGIC:
|
||||
# 最小指令集(详细行为见 architecture_design.md §3.4.B):
|
||||
# LOGIC_IF_HP_BELOW → 读 caster HP%,条件不满足则 deck.skip_until_scope_end()
|
||||
# LOGIC_IF_ENEMY_NEARBY → SpatialGrid 查询 context.range 内是否有敌方实体
|
||||
# LOGIC_EVERY_N_SHOTS → 读写 spell_ctx.registers[0..3](SpellContext,跨帧持久!)
|
||||
# ⚠️ 严禁使用 CastState.registers(每帧重置 → 计数器永远不累积)
|
||||
# LOGIC_LOOP(count, body_size)
|
||||
# → 将紧随其后的 body_size 个节点重复执行 count 次(count ≤ 8,超限裁剪)
|
||||
# → 每次循环消耗 body_size 个 ops;总耗费 = count × body_size,计入 ops_count
|
||||
# → 与 MAX_OPS 交互:LOOP(5, 3) = 15 ops;若 ops_count + 15 > _max_ops 则提前截断
|
||||
# → game_design.md §3.2.D 中「循环执行后续 3 个法术 5 次」即为 LOGIC_LOOP(5, 3)
|
||||
# LOGIC_LABEL / LOGIC_JUMP_IF → 仅限 persistent_memory Core;预编译时建立 _label_table
|
||||
# LOGIC_FORK → CIRCUIT 拓扑分叉专用(由 _flatten_circuit 注入,玩家不可购买)
|
||||
# node.id = "LOGIC_FORK";node.get_meta("fork_branch_ids") = Array[int] SubPayload ID 列表
|
||||
# _execute_logic 识别 LOGIC_FORK 时对每个 fork_branch_id 调用 execute_sub()
|
||||
# 消耗 fork_branch_ids.size() 个 ops(每个分支算 1 步),计入 ops_count
|
||||
_execute_logic(node, context, deck)
|
||||
# ops_count 达到上限时:① 开发期 push_warning;② 通过 EventBus 向玩家 HUD 发出反馈
|
||||
# 截断不得静默,玩家须知构建已被截断(与 MAX_TRIGGER_DEPTH "depth_exceeded" 事件相同规范)。
|
||||
# UIManager 订阅 SPELL_CAST_BEGIN 中 "ops_exceeded: true" 标志,HUD 显示短暂黄色闪烁提示。
|
||||
if ops_count >= _max_ops:
|
||||
push_warning("SpellEvaluator: MAX_OPS reached, build may be too complex")
|
||||
EventBus.emit(EventID.SPELL_CAST_BEGIN, {
|
||||
"caster_id": context.caster_id, "deck_id": -1, "ops_exceeded": true
|
||||
})
|
||||
```
|
||||
|
||||
> **LOGIC 指令集扩展路径**:当前 `_execute_logic` 使用中央 `match node.id` 硬编码所有 LOGIC 指令,新增 LOGIC 类型需修改核心(OCP 限制)。
|
||||
> **P0/P1**:保留 match 实现,LOGIC 指令集固定不超过 8 条,可接受。
|
||||
> **P2 路线图**:若 LOGIC 指令集需扩展超过 10 条,引入 `LogicHandler` 接口:
|
||||
> ```gdscript
|
||||
> # logic_handler.gd
|
||||
> class_name LogicHandler
|
||||
> extends RefCounted
|
||||
> func handle(node: SpellNode, context: SpellContext, deck: SpellDeck) -> void: pass
|
||||
>
|
||||
> # 在 SpellRegistry 中按 node.id 注册:
|
||||
> # SpellRegistry.register_logic_handler("LOGIC_MY_NEW", MyNewHandler.new())
|
||||
> # _execute_logic 改为:
|
||||
> # SpellRegistry.get_logic_handler(node.id).handle(node, context, deck)
|
||||
> ```
|
||||
> 迁移前无需改变 P0/P1 行为,仅 P2 新增 LOGIC 法术卡采用新接口注册。
|
||||
|
||||
#### SubPayloadRegistry 生命周期
|
||||
* **创建时机**:玩家关闭背包(预编译)时,`SpellEvaluator.compile_wand(core, raw_deck)` 清空并重建整个 Registry。
|
||||
* **清空时机**:关卡切换、游戏结束时全量清除。滚动最终销毁所有未使用的 SubPayload 对象还入对象池。
|
||||
* **并发处理**:多弹一帧内同时命中时,`execute_sub` 可并行执行同一 `payload_id`, Registry 不发生冲突(只读)。
|
||||
* **竞争条件安全保证**:Registry 重建发生在玩家**关闭**背包时,而非打开时。
|
||||
因此飞行中的弹体在玩家进入商店编辑阶段时,其 `on_hit_payload_id` 仍指向当前 Registry 中的有效条目——
|
||||
战斗波次结束后所有弹体自然清空,Registry 重建(下次关闭背包)必定早于下波战斗开始;
|
||||
不存在"弹体持有旧 payload_id、Registry 已重建为新 ID 映射"的竞争窗口。
|
||||
> **禁止行为**:不得在波次**进行中**允许玩家修改 Deck(如暂停菜单热插拔法术),否则将违反此不变量。
|
||||
> 如未来需支持"战斗中暂停换牌",必须先清空所有飞行中的弹体,再执行重编译,最后恢复生成。
|
||||
|
||||
### 2.3 战斗实体层 (Layer 2: Combat ECS-Lite)
|
||||
|
||||
#### A. 物理与碰撞 (Physics & Collision)
|
||||
|
||||
碰撞系统采用**分档策略**,不同弹幕密度区间使用不同方案;`BulletManager._collision_mode` 标志控制当前路径(详见 `architecture_design.md §4.3`):
|
||||
|
||||
| 同屏弹幕数 | 碰撞方案 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| < 200 | **Area2D + CircleShape2D** | Godot 内置碰撞;Physics Layer/Mask 过滤阵营;`body_entered` 信号回调 `_on_bullet_hit()` |
|
||||
| 200–2000 | **自定义 SpatialGrid(主力)** | BulletManager 每帧 dirty-list 重建网格,手动 `query_circle()`;Area2D 实例全部禁用 |
|
||||
| 2000+ | **SpatialGrid + MultiMesh** | 碰撞继续走 SpatialGrid;渲染切换 MultiMeshInstance2D |
|
||||
|
||||
> **⚠️ S0 验证目标(风险 R-04)**:S0 期间在 500+ 弹幕时实测 Area2D 帧时间(预期 > 8ms)。验证后**永久锁定 SpatialGrid 为 200+ 弹幕的主力碰撞方案**,Area2D 路径不再作为运行时"降级回退"保留。
|
||||
|
||||
* **Physics Layer & Mask 配置**(< 200 弹幕 Area2D 路径):Layer 1 = 玩家,Layer 2 = 子弹,Layer 3 = 敌人;子弹 Layer 2 只检测 Layer 3;`body_entered` → `_on_bullet_hit(bullet_id, enemy_id)`。
|
||||
* **SpatialGrid 主路径**(200+):两路径最终均调用相同的 `_on_bullet_hit(bullet_id, enemy_id)` 处理函数,后处理逻辑完全共用;切换入口仅在 `_collision_mode` 标志处。
|
||||
|
||||
#### B. 子弹系统 (`BulletManager`)
|
||||
* **混合渲染模式**:
|
||||
* **逻辑层**: 依然使用 `PackedFloat32Array` 管理子弹的生命周期和属性(速度/伤害),SoA 布局保证缓存友好。
|
||||
* **表现层**: 利用 Node Pool 复用 `Node2D` 节点;对于极大量同类子弹,改用 `MultiMeshInstance2D` 直接 GPU 实例化渲染,大幅降低 Draw Call。
|
||||
* **同步优化**: 只有当子弹位置发生显著变化或处于屏幕视口内时,才设置 `Node2D.position`,利用 Godot 的脏标记机制避免无效渲染树更新。
|
||||
|
||||
#### C. 敌人系统 (`EnemyManager`)
|
||||
|
||||
**EnemyManager 热数据 SoA 布局(权威定义)**
|
||||
|
||||
敌人数量可达 1000+,同 BulletManager 一样需要 PackedFloat32Array SoA 结构:
|
||||
|
||||
```
|
||||
const ENEMY_STRIDE: int = 8
|
||||
|
||||
stride = ENEMY_STRIDE 个 float
|
||||
[ x, y, vx, vy, hp, hp_max, faction_and_type, status_bits ]
|
||||
0 1 2 3 4 5 6 7
|
||||
```
|
||||
- `faction_and_type`:高 16 位 = faction(0=敌方, 1=友方/召唤物),低 16 位 = enemy_type_id
|
||||
- `status_bits`:燃烧/冰冻/中毒等状态的位掩码(各类 DoT 的精确数值存冷数据)
|
||||
- 复杂数据(AI 状态、DoT 计时器、combo_tracker)存入 `_enemy_contexts: Dictionary`(key = entity_id)
|
||||
|
||||
**SpatialGrid 每帧清空策略(P5)**
|
||||
|
||||
`SpatialGrid` 每 `_physics_process` 帧重建一次,采用 **Dirty-List 方案**而非全量 memset:
|
||||
- `_dirty_cells: PackedInt32Array` 记录本帧有写入的 Cell 索引。
|
||||
- 帧末遍历 `_dirty_cells`,仅清空这些 Cell,跳过空 Cell。
|
||||
- 当 `_active_count > 1500` 时切换为全量 fill(0)(此时脏格子数量接近总数,遍历反而更慢)。
|
||||
|
||||
* **AI 状态机**:AI 逻辑**不使用 AnimationTree 驱动**,而是以轻量级枚举存于 `_enemy_contexts[entity_id]["ai_state"]`(0=IDLE / 1=CHASE / 2=ATTACK / 3=RETREAT)。`EnemyManager._physics_process` 在 Manager 内部批量完成所有状态转换,不经过 Godot 场景树。
|
||||
* **单向驱动**:帧末对**屏内**敌人(`_visible_flags[i] == 1`)将 `ai_state` 映射为 AnimationTree 参数(`anim_tree.set("parameters/conditions/is_running", ...)`),AnimationTree **仅接收结果,不反向影响逻辑**;屏外敌人完全跳过 AnimationTree 更新。
|
||||
* **⚠️ 禁止**:不得用 AnimationTree StateMachine 驱动 AI 逻辑转换;不得在 `_physics_process` 热路径内对屏外敌人调用 `anim_tree.set()`(大量屏外时 1000× 调用/帧严重浪费)。
|
||||
* **LOD(屏外 AI 剔除)**:❌ **不使用** `VisibilityNotifier2D` per-enemy 方案(1000+ 敌人各挂一个节点 = 1000+ 额外信号监听,节点开销显著)。改为 **Manager 侧 Camera Rect AABB 批量剔除**:
|
||||
```gdscript
|
||||
# ⚠️ _visible_flags 必须声明为 EnemyManager 的类成员(PackedByteArray),
|
||||
# 禁止在 _physics_process 内用 var 声明(否则每帧分配新数组,触发 GC 抖动)。
|
||||
# 类成员声明(EnemyManager 顶部):
|
||||
# var _visible_flags: PackedByteArray = PackedByteArray()
|
||||
# 每当 _enemy_count 扩容时同步 _visible_flags.resize(_enemy_count)
|
||||
#
|
||||
# EnemyManager._physics_process 每帧执行(O(N) 单次矩形测试):
|
||||
var cam_rect: Rect2 = _get_camera_rect_expanded(128.0) # 扩大 128px 防闪烁
|
||||
for i in range(_enemy_count):
|
||||
var pos := Vector2(_data[i * ENEMY_STRIDE + 0], _data[i * ENEMY_STRIDE + 1])
|
||||
_visible_flags[i] = int(cam_rect.has_point(pos)) # ⚠️ 必须显式 int() 转换
|
||||
# has_point() 返回 bool,
|
||||
# 直接赋给 PackedByteArray[i] 会静默写入错误值
|
||||
# _physics_process 遍历时跳过 _visible_flags[i] == 0 的实体 AI 计算
|
||||
```
|
||||
* 相比 VisibilityNotifier2D,此方案无额外节点、无信号开销,N=1000 时约节省 ~1000 次信号 dispatch/帧,适合 ECS-Lite 架构。
|
||||
|
||||
> **屏外 Boid 分离力低频保留**:完全跳过屏外敌人 AI 会导致 Boid 分离斥力缺失——大量屏外实体相互挤叠,进入视野瞬间爆发弹射(视觉闪烁)。
|
||||
> **解决方案**:`_visible_flags[i] == 0` 的屏外敌人**不跳过分离力更新**,改为以低频单独运行:
|
||||
> ```gdscript
|
||||
> const OFFSCREEN_SEPARATION_INTERVAL: int = 10 # 每 10 帧更新一次屏外分离力
|
||||
>
|
||||
> for i in range(_enemy_count):
|
||||
> if _visible_flags[i]:
|
||||
> _update_ai_full(i) # 全量 AI:索敌 + 分离力 + 动画参数
|
||||
> elif Engine.get_physics_frames() % OFFSCREEN_SEPARATION_INTERVAL == i % OFFSCREEN_SEPARATION_INTERVAL:
|
||||
> _update_separation_only(i) # 仅分离斥力:查 SpatialGrid 最近 8 格,O(1)
|
||||
> ```
|
||||
> - `_update_separation_only` 无 AI 状态机、无动画更新,每帧参与计算的屏外实体数 ≤ N/10。
|
||||
> - 10 帧低频下最坏分离响应时延 ≈ 166ms(60 Hz),不可感知;可消除进视野瞬间弹射。
|
||||
|
||||
### 2.4 状态效果系统 (`StatusManager`)
|
||||
|
||||
#### 核心常量
|
||||
|
||||
```gdscript
|
||||
# status_manager.gd (Autoload: StatusManager)
|
||||
# _physics_process 热路径见 csharp/autoloads/StatusManagerCs.cs (ADR-L1)
|
||||
|
||||
## 每个实体允许同时存在的最大状态类型数量(STATUS_BATCH_LIMIT)
|
||||
const STATUS_BATCH_LIMIT: int = 8
|
||||
|
||||
## 同一状态类型的最大叠加层数上限
|
||||
const STATUS_MAX_STACKS: int = 16
|
||||
```
|
||||
|
||||
**设计依据**:单实体最多 8 种状态类型。`_physics_process` 最坏情况 = 1000 敌人 × 8 状态 = 8000 次 tick 检查/帧,仍在帧预算内(ADR-L1 规定 StatusManagerCs < 1.5ms)。
|
||||
|
||||
#### 超限淘汰策略(Eviction)
|
||||
|
||||
当某实体活跃状态数 = `STATUS_BATCH_LIMIT` 且接收到新状态时,按以下顺序决策:
|
||||
|
||||
| 优先级 | 规则 |
|
||||
| :--- | :--- |
|
||||
| 1(最高)| 同类型 → 刷新 duration,按叠加规则增减 stacks,**不占新槽** |
|
||||
| 2 | 同组升级(如 CHILL→FREEZE)→ 替换,保留剩余 duration,**不增加槽数** |
|
||||
| 3 | LRU 淘汰 → 踢出 `last_applied_time` 最早的状态,空槽后写入新状态 |
|
||||
| 4(最低)| 新状态优先级 < 所有现存状态 → 静默丢弃,`push_warning` 记录 |
|
||||
|
||||
```gdscript
|
||||
## 状态分组(同组内高等级覆盖低等级)
|
||||
const STATUS_GROUP: Dictionary = {
|
||||
StatusType.BURN: "fire_dot", StatusType.SCORCH: "fire_dot",
|
||||
StatusType.POISON: "toxic_dot", StatusType.CORRODE: "toxic_dot",
|
||||
StatusType.CHILL: "cold", StatusType.FREEZE: "cold",
|
||||
StatusType.SLOW: "cc", StatusType.STUN: "cc",
|
||||
}
|
||||
```
|
||||
|
||||
#### DoT Tick 规则
|
||||
|
||||
```gdscript
|
||||
// StatusManagerCs.cs —— _physics_process 热路径(C#)
|
||||
// tick_timer 使用 while 模式,与 ZoneManager 规范一致:
|
||||
// while (tickTimer >= TICK_INTERVAL) { tickTimer -= TICK_INTERVAL; ApplyDot(entityId, status); }
|
||||
// 最坏情况:STATUS_BATCH_LIMIT = 8 种 × 1000 敌人 = 8000 次检查/帧
|
||||
```
|
||||
|
||||
#### 与 ZoneManager 的交互规则
|
||||
|
||||
- ZoneManager 通过 `StatusManager.apply(entity_id, type, stacks, duration=0, source_id=zone_id)` 施加持续性区域 DoT。
|
||||
- `duration=0` 表示"持续存在直到来源主动移除";区域销毁时调用 `StatusManager.remove_source(zone_id)` 批量清除。
|
||||
- **同一来源的相同状态不占新槽**,直接刷新 timer(防止多个同类型区域叠满 8 槽)。
|
||||
|
||||
### 2.5 基础服务框架 (Layer 3: System Services)
|
||||
使用 Godot 4 的 **InputMap** 系统作为底层驱动,在上层封装一层 **"Action-Based"** 的抽象。
|
||||
* **抽象层**: 游戏逻辑通过 `Input.get_vector("move_left", "move_right", "move_up", "move_down")` 获取移动向量,不关心具体设备。
|
||||
* **设备支持**:
|
||||
* **键盘/鼠标**: `WASD` 在 InputMap 中映射为 `move_*` Action,鼠标位置通过 `get_global_mouse_position()` 获取瞄准方向。
|
||||
* **虚拟摇杆**: 监听 UI 层触摸事件,转换为 (-1, 1) 的向量,映射到同名 Action。
|
||||
* **手柄 (Gamepad)**: 利用 Godot 内置的 `Input.get_joy_axis()` 和 `Input.is_joy_button_pressed()`,InputMap 已支持手柄轴自动绑定。
|
||||
* **自动适配**: 识别 `Input.get_joy_name(device_id)` 来区分 Xbox / PS 的按键图标显示。
|
||||
* **死区处理**: 在 InputMap 中直接配置 `Deadzone` 参数,或在 Manager 层统一处理防止摇杆漂移。
|
||||
|
||||
> **PlayerManager 移动状态追踪**:`game_design.md §5` 描述了移动敏感型法术
|
||||
> (移动增益型、冲刺波、停步蓄力型),这些功能要求 `PlayerManager` 持续追踪玩家运动状态:
|
||||
> ```gdscript
|
||||
> # player_manager.gd(相关字段,非完整类定义)
|
||||
> var _is_moving: bool = false # 当前帧是否在移动(move_vector.length() > MOVE_THRESHOLD_NORM)
|
||||
> var _stationary_time: float = 0.0 # 连续静止时长(秒);移动时立即清零
|
||||
> var _charge_triggered: bool = false # 停步蓄力已触发标志;边沿检测,防止每帧重复发出 CHARGE_FIRED
|
||||
> var _last_dash_time: float = -999.0 # 上次冲刺时刻(用于冲刺波冷却)
|
||||
> # MOVE_THRESHOLD_NORM:归一化输入轴幅度(0.0~1.0),非像素/秒速度
|
||||
> # 0.067 = 约 6.7% 摇杆偏移即判定为移动(死区 + 轻微触碰阈值)
|
||||
> const MOVE_THRESHOLD_NORM: float = 0.067 # 对应约 20px/s @ 300px/s 最大速度
|
||||
> const CHARGE_TRIGGER_TIME: float = 1.5 # 停步蓄力法术所需静止时长(秒)
|
||||
>
|
||||
> func _physics_process(delta: float) -> void:
|
||||
> var move_vec := Input.get_vector("move_left", "move_right", "move_up", "move_down")
|
||||
> _is_moving = move_vec.length() > MOVE_THRESHOLD_NORM
|
||||
> if _is_moving:
|
||||
> if _stationary_time > 0.0:
|
||||
> _stationary_time = 0.0
|
||||
> _charge_triggered = false # 移动后重置标志,允许下次停步重新触发
|
||||
> EventBus.emit(EventID.CHARGE_STATE_CHANGED, {"caster_id": player_id, "progress": 0.0})
|
||||
> else:
|
||||
> _stationary_time += delta
|
||||
> if _stationary_time >= 0.3: # 0.3 秒后开始显示蓄力进度(game_design.md §5)
|
||||
> var prog := clampf(_stationary_time / CHARGE_TRIGGER_TIME, 0.0, 1.0)
|
||||
> EventBus.emit(EventID.CHARGE_STATE_CHANGED, {"caster_id": player_id, "progress": prog})
|
||||
> # 边沿检测:仅首次越过阈值时发出一次 CHARGE_FIRED;持续静止不会重复发送
|
||||
> # SpellEvaluator 只订阅 CHARGE_FIRED 以执行停步蓄力法术(非 CHARGE_STATE_CHANGED)。
|
||||
> if _stationary_time >= CHARGE_TRIGGER_TIME and not _charge_triggered:
|
||||
> _charge_triggered = true
|
||||
> EventBus.emit(EventID.CHARGE_FIRED, {"caster_id": player_id})
|
||||
>
|
||||
> # 冲刺输入检测(冲刺波 Core 专属)
|
||||
> if Input.is_action_just_pressed("dash"):
|
||||
> _last_dash_time = Time.get_ticks_msec() / 1000.0
|
||||
> # 附带 stationary_time:SpellEvaluator 用于判断蓄力时长奖励;UIManager 用于清除蓄力 UI
|
||||
> EventBus.emit(EventID.DASH_TRIGGERED, {
|
||||
> "caster_id": player_id,
|
||||
> "stationary_time": _stationary_time # 冲刺前已积累的静止时长(秒)
|
||||
> })
|
||||
> _stationary_time = 0.0 # 冲刺后立即清零
|
||||
> _charge_triggered = false # 冲刺打断蓄力,重置触发标志允许下次重新蓄力
|
||||
> # SpellEvaluator 订阅 DASH_TRIGGERED,触发 dash-wave Core 的附加法术释放
|
||||
> ```
|
||||
> **设计约束**:`CHARGE_STATE_CHANGED` 事件仅由 `UIManager` 订阅以驱动蓄力进度环动画;
|
||||
> `SpellEvaluator` 订阅 `CHARGE_FIRED`(一次性边沿触发信号)以实际释放停步蓄力法术。
|
||||
> 两者均通过 EventBus 解耦,`PlayerManager` 不直接持有 UI 引用或 SpellEvaluator 引用。
|
||||
> ⚠️ 禁止让 `SpellEvaluator` 订阅 `CHARGE_STATE_CHANGED` 并在 `progress >= 1.0` 时触发法术——
|
||||
> 该事件每帧发送(60Hz),会导致蓄力法术每帧重复释放,违反"首次越阈值只触发一次"的设计原则。
|
||||
|
||||
#### B. 音频系统 (`AudioManager`)
|
||||
针对"割草"游戏的高并发特性进行封装。
|
||||
* **Audio Pool**: 预先实例化 32 个 `AudioStreamPlayer2D` 节点,循环使用。
|
||||
* **Throttling (节流)**: 维护一个 `last_played_time` 字典。
|
||||
* *规则*: 如果 `explosion_sound` 在 0.1s 内已经播放过,且新的请求音量没有更大,则丢弃该请求。这避免了 50 个敌人同时炸裂时的爆音。
|
||||
* **Spatial (空间感)**: 使用 `AudioStreamPlayer2D` 的内置衰减曲线,或根据摄像机位置动态调整 `volume_db` 和 `panning_strength`。
|
||||
|
||||
#### C. 资源管理 (`ResourceManager`)
|
||||
* **后台加载**: 使用 `ResourceLoader.load_threaded_request(path)` 异步加载法术图标、特效场景等,避免卡顿。
|
||||
* **分组管理**: 利用 Godot 的 `ResourceGroup` 或按文件夹路径约定管理资源(如 `res://resources/spells/`、`res://scenes/enemies/`)。
|
||||
* **引用计数**: Godot 的 `Resource` 本身基于引用计数,场景卸载时未被引用的资源自动释放。对于手动预加载的资源,切换场景时显式 `queue_free()` 对应节点以触发释放。
|
||||
|
||||
#### D. 特效管理 (`VFXManager`)
|
||||
|
||||
`VFXManager` 是 Autoload 单例,所有视觉特效(命中闪光、爆炸、DoT 气泡等)必须通过此管理器播放,**禁止在逻辑层直接操作 Node**。
|
||||
|
||||
```gdscript
|
||||
# vfx_manager.gd (Autoload)
|
||||
# 职责:接收逻辑事件并以数据驱动方式触发对应特效,逻辑层与表现层完全解耦
|
||||
|
||||
const MAX_ACTIVE_VFX: int = 200 # 同屏最大活跃特效数量上限
|
||||
var _vfx_pool: Dictionary = {} # { "effect_id": Array[GPUParticles2D] } — 按类型分池
|
||||
# VFX 场景预加载表;_ready() 中按配置填充,play() 通过此表实例化节点
|
||||
# 例: _vfx_scenes["hit_spark"] = preload("res://scenes/vfx/hit_spark.tscn")
|
||||
var _vfx_scenes: Dictionary = {} # { "effect_id": String -> PackedScene }
|
||||
# 维护活跃特效计数器(O(1) 计数,不在 play() 中遍历 pool 统计)
|
||||
var _active_count: int = 0 # 当前正在 emitting 的特效节点总数(O(1) 计数,避免遍历 pool 统计)
|
||||
|
||||
# EventBus 订阅列表(在 _ready() 中注册):
|
||||
# EventID.BULLET_HIT → play("hit_spark", hit_position)
|
||||
# EventID.ENEMY_KILLED → play("death_burst", enemy_position)
|
||||
# EventID.STATUS_APPLIED → play("status_" + status_name, target_position)
|
||||
# EventID.SPELL_CAST_BEGIN→ play("cast_flash", caster_position)
|
||||
|
||||
func play(effect_id: String, world_pos: Vector2, override_scale: float = 1.0) -> void:
|
||||
if _active_count >= MAX_ACTIVE_VFX: # O(1) 检查,达上限时静默丢弃
|
||||
return
|
||||
var node := _pool_pop(effect_id)
|
||||
node.position = world_pos
|
||||
node.scale = Vector2.ONE * override_scale
|
||||
node.emitting = true
|
||||
_active_count += 1
|
||||
node.finished.connect(_pool_return.bind(effect_id, node), CONNECT_ONE_SHOT)
|
||||
|
||||
func _pool_pop(effect_id: String) -> GPUParticles2D:
|
||||
if not _vfx_pool.has(effect_id) or _vfx_pool[effect_id].is_empty():
|
||||
return _instantiate_vfx(effect_id) # 按需实例化并加入场景树
|
||||
return _vfx_pool[effect_id].pop_back()
|
||||
|
||||
func _pool_return(effect_id: String, node: GPUParticles2D) -> void:
|
||||
node.emitting = false
|
||||
_active_count -= 1 # CONNECT_ONE_SHOT 保证每个节点只调用一次
|
||||
# 首次归还时 _vfx_pool 中可能尚无此 effect_id 键,需先初始化空列表再 append
|
||||
if not _vfx_pool.has(effect_id):
|
||||
_vfx_pool[effect_id] = []
|
||||
_vfx_pool[effect_id].append(node)
|
||||
|
||||
# _pool_pop() 中对应 effect_id 的池为空时,按需创建新 GPUParticles2D 节点。
|
||||
# ⚠️ node.one_shot 必须为 true:若为 false,GPUParticles2D 持续循环发射,
|
||||
# finished 信号永不触发,_pool_return 不被调用,_active_count 只增不减,
|
||||
# 触达 MAX_ACTIVE_VFX 后所有后续特效全部被静默丢弃(完全失效)。
|
||||
# 父节点:VFXManager 自身(Autoload 单例),跨场景持久存活,确保池化节点复用。
|
||||
func _instantiate_vfx(effect_id: String) -> GPUParticles2D:
|
||||
if not _vfx_scenes.has(effect_id):
|
||||
# 未注册的 effect_id:返回空占位节点(不崩溃),开发期通过 push_warning 提示漏配
|
||||
push_warning("VFXManager: 未知特效 ID='%s',请检查 _ready() 中的预加载配置" % effect_id)
|
||||
var placeholder := GPUParticles2D.new()
|
||||
placeholder.one_shot = true # 占位节点同样需要 one_shot,确保 finished 可触发
|
||||
add_child(placeholder)
|
||||
return placeholder
|
||||
var node: GPUParticles2D = _vfx_scenes[effect_id].instantiate()
|
||||
node.one_shot = true # 必须为 true,否则 finished 永不触发,池化失效
|
||||
node.emitting = false # 初始不发射,等待 play() 设置 position/scale 后再启动
|
||||
add_child(node) # 挂载到 VFXManager(Autoload)节点下,跨场景持久存活
|
||||
return node
|
||||
```
|
||||
|
||||
> **性能约束(V-N1)**:`MAX_ACTIVE_VFX = 200` 超出时静默丢弃,避免大量爆炸帧暴涨。
|
||||
> 伤害数字(DamageNumber)由 `UIManager` 而非 `VFXManager` 管理,两者隔离(参见 `architecture_design.md §4.4` Throttling)。
|
||||
|
||||
#### E. 元数据管理 (`ProfileManager`)
|
||||
* **Save/Load**: 使用 Godot 的 `FileAccess` 读写 JSON 字符串,或使用 `ConfigFile` 存储键值对数据,存储路径为 `user://save_data.json`。
|
||||
* **Schema**: 定义 `SaveData` 字典结构,包含 `unlocked_spells: Array[String]`、`achievements: Array[int]` 等字段。
|
||||
* **版本字段**: `SaveData` 根层必须包含 `schema_version: int`(当前版本 = 1)。
|
||||
* **加载时版本检查**:
|
||||
1. 读取 `schema_version`,若字段不存在则视为 `v0`。
|
||||
2. 对每个低版本执行迁移函数(`_migrate_v0_to_v1(data)`),补全缺失字段为默认值。
|
||||
3. 若 `schema_version > 当前版本`(由更新版本写入),提示版本不匹配并回退到初始状态,避免崩溃。
|
||||
* **参考**: `docs/design/core_wand_design.md` 中的 SaveData JSON 序列化示例。
|
||||
|
||||
#### E.1 存档损坏 UX 规范(C-GAP-3 修复)
|
||||
|
||||
`load_run()` 和 `EndlessRecordsManager.load()` **不得静默降级**——返回空数据时必须通过 UI 告知玩家,禁止无声失败。
|
||||
|
||||
| 失败场景 | 触发条件 | UI 行为 |
|
||||
| :--- | :--- | :--- |
|
||||
| `load_run()` 解析失败 | JSON 格式错误 / `schema_version` 缺失 / 两槽均损坏 | `UIManager.show_data_corrupted_dialog("run")` → 对话框:「检测到存档异常,当前游戏进度无法读取。是否开始新游戏?」;确认后清除 run 文件并进入主菜单新游戏流程 |
|
||||
| `EndlessRecordsManager.load()` 签名失败 | HMAC 不匹配(文件被篡改或损坏) | `UIManager.show_data_corrupted_dialog("leaderboard")` → Toast 提示:「排行榜分数验证失败,本地记录已重置」;不影响游戏进程,直接以空记录继续 |
|
||||
| `load_run()` 返回 `{}` 但存档文件存在 | 两槽均损坏 | 同 run 对话框,额外追加一条 `crash_log.txt` 记录,便于复现调试 |
|
||||
|
||||
```gdscript
|
||||
# profile_manager.gd — load_run() 调用方规范(游戏启动时)
|
||||
var run_data := load_run()
|
||||
if run_data.is_empty() and (
|
||||
FileAccess.file_exists(_SLOT_A) or FileAccess.file_exists(_SLOT_B)):
|
||||
# 文件存在但解析为空 → 损坏,不静默降级
|
||||
UIManager.show_data_corrupted_dialog("run")
|
||||
return # 由对话框回调决定后续流程(新游戏 or 退出)
|
||||
```
|
||||
|
||||
> **`UIManager.show_data_corrupted_dialog(type: String)` 接口规范**:
|
||||
> - `type = "run"`:模态对话框,两个按钮:「开始新游戏」(清除损坏 Run 文件)/ 「返回主菜单」。
|
||||
> - `type = "leaderboard"`:非模态 Toast(3 秒自动消失),不阻断游戏流程。
|
||||
> - 对话框文案须通过 `tr()` 包裹(L10n 支持):键名 `"DIALOG_SAVE_CORRUPTED_RUN"` / `"TOAST_LEADERBOARD_RESET"`。
|
||||
|
||||
|
||||
> `game_design.md §8.1` 要求死亡时自动截图保存构建快照。实现规范如下:
|
||||
>
|
||||
> **DPS 计算**:`CombatManager`(或 `ProfileManager`)维护滚动窗口:
|
||||
> ```gdscript
|
||||
> # 环形缓冲区:O(1) 写入/覆盖,无内存分配,零 GC 压力(替代 Array + pop_front() 的 O(N) 方案)
|
||||
> # _recalc_window() 懒加载:仅在 get_dps() 查询时计算(不在每次 record_damage() 触发)
|
||||
> # 理由:record_damage() 每帧可能被数百次 AoE 命中调用;UIManager 仅每 0.5 秒轮询 DPS
|
||||
> const DPS_WINDOW_SEC: float = 3.0
|
||||
> const _RB_SIZE: int = 256 # 环形缓冲区槽数(上限:3秒内最多 256 次伤害事件)
|
||||
> var _rb_time: PackedFloat64Array # 各槽时间戳
|
||||
> var _rb_dmg: PackedFloat32Array # 各槽伤害值
|
||||
> var _rb_head: int = 0 # 下一次写入位置(覆盖最旧记录)
|
||||
> var _rb_count: int = 0 # 有效记录数(最大 = _RB_SIZE)
|
||||
> var _accumulated_dmg: float = 0.0 # 当前窗口内伤害累计(主动维护,O(1) 读取)
|
||||
>
|
||||
> func _ready() -> void:
|
||||
> _rb_time = PackedFloat64Array(); _rb_time.resize(_RB_SIZE)
|
||||
> _rb_dmg = PackedFloat32Array(); _rb_dmg.resize(_RB_SIZE)
|
||||
>
|
||||
> func record_damage(amount: float) -> void:
|
||||
> var now := Time.get_ticks_msec() / 1000.0
|
||||
> _rb_time[_rb_head] = now
|
||||
> _rb_dmg[_rb_head] = amount
|
||||
> _rb_head = (_rb_head + 1) % _RB_SIZE
|
||||
> if _rb_count < _RB_SIZE:
|
||||
> _rb_count += 1
|
||||
> _accumulated_dmg += amount # 乐观累加;get_dps() 时统一裁剪过期值
|
||||
>
|
||||
> func _recalc_window(now: float) -> void:
|
||||
> var sum: float = 0.0
|
||||
> var tail := (_rb_head - _rb_count + _RB_SIZE) % _RB_SIZE
|
||||
> var valid: int = 0
|
||||
> for k in _rb_count:
|
||||
> var idx := (tail + k) % _RB_SIZE
|
||||
> if (now - _rb_time[idx]) <= DPS_WINDOW_SEC:
|
||||
> sum += _rb_dmg[idx]
|
||||
> valid += 1
|
||||
> _accumulated_dmg = sum
|
||||
> _rb_count = valid
|
||||
>
|
||||
> func get_dps() -> float:
|
||||
> _recalc_window(Time.get_ticks_msec() / 1000.0) # 懒加载裁剪
|
||||
> return _accumulated_dmg / DPS_WINDOW_SEC # 平均 DPS(过去 3 秒)
|
||||
> ```
|
||||
> `UIManager` 每 0.5 秒轮询 `CombatManager.get_dps()`,更新 HUD DPS 标签;靶场模式下同步更新 DPS 面板。
|
||||
>
|
||||
> **Run 截图**:死亡时 `GameCycleManager` 调用:
|
||||
> ```gdscript
|
||||
> func _save_run_screenshot() -> void:
|
||||
> var img: Image = get_viewport().get_texture().get_image()
|
||||
> var path := "user://runs/%s_run_%d.png" % [
|
||||
> Time.get_datetime_string_from_system().substr(0, 10),
|
||||
> ProfileManager.get_run_count()
|
||||
> ]
|
||||
> img.save_png(path)
|
||||
> ```
|
||||
> 截图在主线程调用(`get_image()` 是同步操作),建议在死亡动画第 1 帧完成后、统计屏显示前执行,
|
||||
> 避免截到黑屏。
|
||||
|
||||
## 3. 开发阶段规划 (Development Stages)
|
||||
|
||||
### Phase 1: 核心验证 (The Engine)
|
||||
* [x] 定义 `SpellNode` 基类 (已完成)。
|
||||
* [ ] 实现 `SpellEvaluator` (纯逻辑,无 UI)。
|
||||
* [ ] 实现 `SpatialGrid` 碰撞检测。
|
||||
* [ ] 编写单元测试:构建一个简单的"散弹枪"逻辑,验证输出数据是否符合预期。
|
||||
|
||||
### Phase 2: 视觉化 (The Visuals)
|
||||
* [ ] 搭建 Godot 场景,实现玩家移动 (键盘/手柄)。
|
||||
* [ ] 接入 `BulletManager`,将逻辑坐标同步到 Sprite2D。
|
||||
* [ ] 实现简单的敌人 AI (追着玩家跑)。
|
||||
|
||||
### Phase 3: 构建系统 (The Builder)
|
||||
* [ ] 实现各种具体的 `SpellNode` (Actions, Modifiers)。
|
||||
* [ ] UI 开发:背包拖拽系统。
|
||||
* [ ] 存档与解析系统 (JSON <-> SpellDeck),含 Schema 版本字段。
|
||||
|
||||
#### SpellDeck JSON 序列化格式 (E2 权威格式)
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": 1,
|
||||
"core_id": "linear_core_t2",
|
||||
"slots": [
|
||||
{ "spell_id": "modifier_damage_plus", "position": 0 },
|
||||
{ "spell_id": "action_spark_bolt", "position": 1 },
|
||||
{ "spell_id": "trigger_on_hit", "position": 2 },
|
||||
{ "spell_id": "action_chain_bolt", "position": 3 }
|
||||
]
|
||||
}
|
||||
```
|
||||
* `core_id`:核心 ID,决定插槽拓扑结构。
|
||||
* `slots` 按 `position` 排列,`position` 为核心插槽座标(线性核心就是序数,网格核心为 `{x, y}` 对)。
|
||||
* `spell_id` 对应 `SpellRegistry` 中的全局唯一 ID。
|
||||
|
||||
### Phase 4: 循环与内容 (The Game)
|
||||
* [ ] 商店逻辑。
|
||||
* [ ] 波次管理器。
|
||||
* [ ] 数值接入。
|
||||
* [ ] ZoneManager(地面效果系统)。
|
||||
* [ ] VFXManager 特效池。
|
||||
* [ ] Endless 排行榜本地存储。
|
||||
|
||||
---
|
||||
|
||||
## 4. 关键技术难点预案
|
||||
|
||||
### Q1: GDScript 如何避免 GC 抖动?
|
||||
* **预案**: 所有的 `ProjectilePayload`、`SpellContext` 在游戏启动时预分配并放入 `Array` 池。使用时 `pool.pop_back()` 取出,用完调用 `reset()` 后 `pool.append()` 归还,不依赖 GDScript 的自动引用计数销毁。
|
||||
|
||||
### Q2: 复杂的法术链如何调试?
|
||||
* **预案**: 开发一个 Godot 编辑器插件(`EditorPlugin`)作为可视化的 **"Debug Graph"** 工具。在编辑器模式下,可以单步执行 `SpellEvaluator`,看到当前的 Stack 和 Deck 状态,并在 2D 视图中高亮对应节点。
|
||||
|
||||
### Q3: 物理碰撞性能不够怎么办?
|
||||
* **预案**:
|
||||
1. 确保 `CollisionShape2D` 使用圆形(最快),并合理配置 Physics Layer/Mask 减少检测对数。
|
||||
2. 将碰撞检测分帧(Time-Slicing),这一帧算一半子弹,下一帧算另一半。对于高速割草游戏,延迟一帧通常不可感知。
|
||||
3. 终极方案:完全放弃 Area2D,改用自定义 `SpatialGrid`,在 `_physics_process` 中手动执行圆形重叠测试。
|
||||
|
||||
### Q4: `PackedFloat32Array` 删除策略?
|
||||
* **问题**:直接调用 `PackedFloat32Array.resize()` 删除元素是 O(n),对 2000+ 弹幕每帧有明显性能开销。
|
||||
* **预案:末位交换 (Swap-and-Pop)**:
|
||||
* 子弹 “死亡” 时,将数组最后一个元素的数据复制到当前位置,然后缩减数组长度 1 个 stride。
|
||||
* 复杂度 O(1),但子弹顺序不再保证(对融剆这个游戏类型无影响)。
|
||||
* **实际实现**:`BulletManager` 内部维护 `_active_count: int`,子弹数据帮存在 `[0, _active_count)` 区间内,不依赖 resize()。
|
||||
|
||||
### Q5: GDScript 与 C# 如何协同工作?
|
||||
* **热路径 C# 文件目录**:`scripts/core_csharp/BulletManagerCore.cs`、`SpellEvaluatorCore.cs`。
|
||||
* **调用方式**:在 Godot 4 中,GDScript 可通过 `var evaluator = preload("res://scripts/SpellEvaluatorCore.cs").new()` 直接实例化 C# 类,调用公开方法无需特殊处理。
|
||||
* **跨语言传递类型**:只传递 GDScript / C# 双方都支持的类型:`int`、`float`、`Vector2`、`Godot.Array`、`Godot.Dictionary`。禁止将 GDScript `RefCounted` 自定义类型直接传入 C#:应将复杂数据序列化为 `PackedFloat32Array` 或 `int` ID 后传递。
|
||||
* **原则**:只有当 Profiler 显示具体函数是确认瓶颈时,才将其迁移到 C#;默认先用 GDScript 实现,确保选择的正确性。
|
||||
Reference in New Issue
Block a user