This commit is contained in:
2026-04-27 14:53:11 +08:00
commit 6fc7796933
12 changed files with 1761 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
# ADR-001: Three-Layer Architecture & Code Isolation
**Status**: Accepted
**Date**: 2026-04-27
**Engine**: Unity 2022.3.62f3c1
**GDD Requirements Addressed**: All L0/L1/L2 system boundaries from systems-index.md
---
## Context
夜裔的架构要求在 L0(纯 C# 核心逻辑)、L1(Unity 适配层)、L2(Unity 表现层)之间建立严格的代码隔离。L0 必须零 UnityEngine 依赖以支持独立测试和未来复用。外部开发者需要清晰的物理边界来理解什么代码属于哪一层。
## Decision
### Directory Structure
```
Assets/Scripts/
Core/ # L0 — Zero UnityEngine dependency
Core.asmdef
Combat/ # BloodEnergyEconomy, CombatLogic, FormSwitchSM, AttackStyle
Enemy/ # EnemyAILogic, BossAILogic, WaveManagerLogic
Player/ # DeathRespawnRules
Progression/ # SkillTreeRules
Persistence/ # SaveLoadLogic
Meta/ # ScoreCalculator
Geometry/ # Vector3, Shape, MathUtil (pure C#)
Adapters/ # L1 — Thin MonoBehaviour wrappers
Adapters.asmdef
InputAdapter.cs, CameraController.cs, LevelLoader.cs,
AudioPlayer.cs, VFXSpawner.cs
Presentation/ # L2 — UI Rendering
Presentation.asmdef
HUDManager.cs, HUDState.cs, MenuSystem.cs
```
### asmdef Reference Chain
| Assembly | References | Prohibited |
|----------|-----------|------------|
| Core | (none) | All UnityEngine packages |
| Adapters | Core | Presentation |
| Presentation | Core, Adapters | — |
| Core.Tests | Core | Unity (NUnit standalone) |
| Adapter.Tests | Core, Adapters | — |
### Enforcement Rules
1. `using UnityEngine;` in any L0 file → violation
2. `MonoBehaviour` inheritance in L0 → violation
3. L1 classes may NOT contain game rule logic — forwarding and subscription only
4. asmdef reference chain is strictly one-way: Core ← Adapters ← Presentation
## Consequences
- L0 compiles and tests via `dotnet test` + NUnit without Unity, enabling fast feedback
- External developers cannot accidentally put game logic in MonoBehaviour — asmdef enforces it
- If the project migrates engines or adds server-side, L0 is directly reusable
- Tradeoff: small systems (e.g., ScoreCalculator) pay the adapter abstraction cost
+42
View File
@@ -0,0 +1,42 @@
# ADR-002: Event Bus & Cross-Layer Communication Pattern
**Status**: Accepted
**Date**: 2026-04-27
**Engine**: Unity 2022.3.62f3c1
**GDD Requirements Addressed**: All L0→L1 communication from systems-index.md
---
## Context
Architecture requires L0 to never call L1 or L2. All upward communication must go through an event mechanism. Need to choose: C# built-in `event` vs. a dedicated EventBus class. This decision affects every L0 module's interface design and memory safety.
## Decision
**Use C# built-in `event` + `Action<T>`. No dedicated EventBus class.**
Rationale:
- L0 has a fixed set of 10 modules — direct `event` declarations are manageable without a central registry
- C# `event` provides compile-time type safety; parameter mismatches are caught at build time, not runtime
- Zero external dependencies — no third-party messaging library
- Each L0 module's events are part of its public API contract, visible and self-documenting for external developers
**Subscription lifecycle**:
```
L1/L2 subscribe to L0 events in OnEnable()
L1/L2 unsubscribe in OnDisable()
(NOT OnDestroy — avoids lost subscriptions on disable/re-enable cycles)
```
**Naming convention**: `On[What]` — e.g., `OnFormChanged`, `OnEnergyChanged`, `OnWaveStart`
**Parameter convention**: Use lightweight structs or primitive types. Never pass mutable L0 internal object references to subscribers (prevents L1 from mutating L0 state).
**Hot path exception**: Attack resolution and damage calculation use per-frame method calls (`CombatLogic.ResolveAttacks()`), not events. Events are reserved for low-frequency state transition notifications.
## Consequences
- Each L0 module's event surface is explicitly visible, making inter-system communication self-documenting
- No centralized message registry reduces coupling
- L1 developers MUST follow OnEnable/OnDisable subscription discipline — leaks or null refs otherwise
- If system count grows beyond ~20, a refactor to a dedicated EventBus may be warranted
+43
View File
@@ -0,0 +1,43 @@
# ADR-003: Pure C# Geometry & Math Library
**Status**: Accepted
**Date**: 2026-04-27
**Engine**: Unity 2022.3.62f3c1
**GDD Requirements Addressed**: Combat Logic (hit detection), Form Switch SM (attack style shapes), Enemy AI (distance/range checks)
---
## Context
L0 must not reference `UnityEngine`, but Combat Logic requires geometric intersection tests (circle vs rect vs sector), Form Switch SM needs distance calculations, and all systems need basic math utilities. A self-contained geometry/math library is required.
## Decision
**Build a minimal geometry library implementing only what Nightborn's combat systems actually need.**
### Type Inventory
| Type | Fields | Methods | Consumer |
|------|--------|---------|----------|
| `Vector3` | `float X, Y, Z` | `Distance(Vector3)`, `Normalize()`, `Length`, `Dot`, `Cross`, `+`, `-`, `*` | All modules |
| `Circle` | `Vector3 Center`, `float Radius` | `Intersects(Circle)`, `Intersects(Rect)`, `Contains(Vector3)` | Combat (Mist form AOE) |
| `Rect` | `Vector3 Center`, `float Width`, `float Height`, `float Rotation` | `Intersects(Circle)`, `Intersects(Rect)`, `Contains(Vector3)` | Combat (Wolf form dash) |
| `Sector` | `Vector3 Origin`, `float Radius`, `float Angle`, `float Direction` | `Intersects(Circle)`, `Contains(Vector3)` | Combat (Human form parry fan) |
| `MathUtil` | — | `Clamp`, `Lerp`, `InverseLerp`, `Remap`, `Approximately` | All modules |
### Explicitly Out of Scope
- Full 3D geometry library — Nightborn is top-down/isometric, needs XZ plane only
- Matrix operations — use `System.Numerics.Matrix4x4` (.NET Standard 2.1 built-in)
- Physics collision engine — only static intersection tests between geometric shapes
### Why Not System.Numerics.Vector3
`System.Numerics.Vector3` is available but its field names and method surface overlap with Unity's Vector3. Self-building avoids confusion for external developers juggling two Vector3 types. If SIMD performance becomes needed later, switching to System.Numerics is a drop-in replacement.
## Consequences
- Combat hit detection is fully self-contained and testable without Unity
- Geometric types precisely match game needs (Sector directly addresses Human form's parry shape detection)
- Tradeoff: maintaining a micro math library (~500 lines maximum)
- If complex physics is needed later, re-evaluate against third-party libraries
+90
View File
@@ -0,0 +1,90 @@
# ADR-004: Form Switch State Machine Design
**Status**: Accepted
**Date**: 2026-04-27
**Engine**: Unity 2022.3.62f3c1
**GDD Requirements Addressed**: Form Switch State Machine (systems-index.md #3)
---
## Context
Form switching is Nightborn's core operation — every switch is a risky commitment action. The state machine must balance "responsive feel" against "decisions have weight." It must also support future switch-speed upgrades from the Skill Tree.
## Decision
### Four-Phase Linear State Machine
```
RequestSwitch(Wolf)
Idle ─────────────────────────────→ Windup
↑ │
│ timer >= recovery │ timer >= windup
│ │
│ ┌─────────────────────────────┐ │
│ │ Hit during Windup → │←──│
│ │ OnSwitchInterrupted │ │
│ └─────────────────────────────┘ ↓
│ Switching (i-frames)
Recovery ←───────────────────────────┘
timer >= switch
```
### Phase Parameters (baseline, modifiable by Skill Tree)
| Phase | Duration | Can Act | Interruptible | Can Re-Switch |
|-------|----------|---------|---------------|---------------|
| **Idle** | — | Full | — | — |
| **Windup** | 0.25s | Cannot attack | Yes (hit → lose Blood Energy) | No |
| **Switching** | 0.10s | Cannot act | No (i-frames) | No |
| **Recovery** | 0.15s | Can attack/move | No | No |
**Total switch time**: 0.50s. Danger window: 0.25s (Windup).
### Interrupt Rules
- Hit during Windup → `OnSwitchInterrupted` → Blood Energy already spent, switch fails → return to Idle
- This IS the "switched at the wrong time" punishment — core to the Switch is Commitment pillar
### Post-Switch Cooldown
After Recovery ends, an additional 0.3s cooldown before another switch is allowed (minimum 0.8s between switches) — prevents switch spam and preserves rhythmic feel.
### Implementation
```csharp
// L0 — Pure C#, zero Unity
public class FormSwitchStateMachine
{
public FormType CurrentForm { get; private set; } = FormType.Human;
public SwitchPhase Phase { get; private set; } = SwitchPhase.Idle;
public float PhaseTimer { get; private set; }
// Tunable (Skill Tree can modify)
public float WindupDuration = 0.25f;
public float SwitchDuration = 0.10f;
public float RecoveryDuration = 0.15f;
public float CooldownDuration = 0.3f;
public SwitchResult RequestSwitch(FormType target, BloodEnergyEconomy energy);
public void Update(float deltaTime);
public void Interrupt();
public event Action<FormType, FormType> OnFormChanged;
public event Action<SwitchPhase> OnPhaseChanged;
public event Action<FormType> OnSwitchInterrupted;
}
```
### Why a Simple Linear FSM
- 4 phases progress sequentially — no Hierarchical FSM or Behavior Tree needed
- Only Idle accepts input — logic is straightforward for external developers
- If a future form needs a different flow, it can subclass with its own state machine
## Consequences
- Switch feel is controlled by exactly 4 parameters (WindupDuration, SwitchDuration, RecoveryDuration, CooldownDuration) — tuning is centralized
- Interrupt during Windup creates clear "wrong time to switch" punishment, reinforcing Switch is Commitment
- Skill Tree can directly modify these 4 parameters for switch-speed upgrades
- Windup duration (0.25s) is the critical feel parameter — MUST be validated through prototype, likely iterating between 0.15s–0.35s
+84
View File
@@ -0,0 +1,84 @@
# ADR-005: Enemy AI Decision Model
**Status**: Accepted
**Date**: 2026-04-27
**Engine**: Unity 2022.3.62f3c1
**GDD Requirements Addressed**: Enemy AI Logic, Boss AI Logic (systems-index.md #4, #5)
---
## Context
Nightborn's enemy AI doesn't need complex tactical reasoning, but has a special requirement: each enemy type must strongly favor a specific form, creating "I need to switch" pressure. MVP scope is 3-5 enemy types. The AI model must be simple to implement, produce predictable behavior, and be easily understood and tested by external developers.
## Decision
**Use lightweight per-type Finite State Machines. Reject Behavior Trees and Utility AI.**
### Base FSM Structure (shared by all enemy types)
```
Idle ──→ Chase ──→ Attack ──→ Cooldown
↑ │ │
└────────────────────┴──────────┘
If too far, return to Chase
```
### Per-Type Differentiation in the Attack State
| Enemy Type | Attack Behavior | Encouraged Form | Why |
|------------|----------------|-----------------|-----|
| **Swarm** | Many simultaneous small-area hits | Mist | Mist's circular AOE clears groups at once |
| **Brute** | Slow, high-damage single strike | Human | Human's parry window is clear and precise against it |
| **Stalker** | Fast rush + point-blank flurry | Wolf | Wolf's rectangular dash collides for a stagger counter |
| **Shooter** | Ranged projectile barrage from periphery | Mist/Wolf | Mist phases through to close, or Wolf bursts to eliminate |
| **Boss** | Multi-phase mixed patterns | All forms | Phase transitions require form adaptation |
### Why Not Behavior Trees
BTs excel at complex decision chains (e.g., FPS cover-shooter AI). Nightborn enemies need only 4 states. The tree's maintenance cost exceeds its value.
### Why Not Utility AI
Scoring systems produce "optimal" behavior but aren't predictable enough — players can't learn enemy patterns, violating the "Battlefield is Information" pillar.
### Implementation
```csharp
// L0 — Pure C#, returns one AICommand per frame
public enum EnemyStateType { Idle, Chase, Attack, Cooldown }
public class EnemyAILogic
{
public EnemyTypeConfig Config;
public AICommand ComputeAction(EnemyState self, PlayerState player)
{
TransitionState(self, player);
return CurrentState switch
{
Idle => ScanForPlayer(player),
Chase => MoveToward(player),
Attack => ExecuteAttack(self.AttackPattern, player),
Cooldown => WaitAndReset(self.CooldownTimer),
};
}
// Per-type override for unique transition logic
protected virtual void TransitionState(EnemyState self, PlayerState player) { }
}
```
### Boss Extension
Boss AI extends the base FSM with:
- `BossPhase` enum (additional state dimension)
- Phase transition conditions (health thresholds, timer triggers)
- `OnPhaseChanged` event for L1 VFX/Audio synchronization
## Consequences
- External developers can understand an enemy's behavior from `EnemyTypeConfig` alone — no need to parse behavior trees
- Enemy behavior is predictable and learnable — satisfies "Battlefield is Information" pillar
- Each enemy type's Attack pattern naturally points to one form counter — reinforces "Form is Tactics"
- If future enemies need more intelligence (cover, coordination), the FSM base can be extended without replacement
+405
View File
@@ -0,0 +1,405 @@
# 夜裔 (Nightborn) — Master Architecture
## Document Status
- **Version**: 1.0
- **Last Updated**: 2026-04-27
- **Engine**: Unity 2022.3.62f3c1 (URP 14.0.12)
- **Language**: C# (.NET Standard 2.1)
- **GDDs Covered**: game-concept.md, systems-index.md
- **ADRs Referenced**: None yet — see Required ADRs section
- **Review Mode**: Lean (no director sign-off)
- **Architecture**: L0 Pure C# / L1 Unity Adapter / L2 Unity Presentation
---
## Engine Knowledge Gap Summary
| Risk Level | Domains | Implication |
|------------|---------|-------------|
| **LOW** | All domains | Unity 2022.3 LTS is within LLM training data (cutoff May 2025). No engine knowledge gaps. |
| **Note** | New Input System | Project uses Input System package (not legacy Input Manager), consistent with best practices |
| **Note** | URP 14.x | Render pipeline is stable and well-documented |
---
## Architecture Principles
These are non-negotiable technical rules derived from the game pillars and design concept.
1. **L0 is pure** — No `UnityEngine` reference in any L0 source file. L0 compiles as a standalone .NET library.
2. **L0 doesn't know it's in Unity** — L0 communicates only via C# primitives and events. It never calls up.
3. **L1 is thin** — MonoBehaviour classes in L1 contain only Unity lifecycle wiring and event forwarding. No game logic.
4. **Events go up, methods go down** — L0 emits events (L1/L2 subscribe). L1 calls methods on L0. Never the reverse.
5. **Data owned by L0, rendered by L1/L2** — All game state lives in L0. L1 and L2 are stateless regarding game rules.
---
## System Layer Map
```
┌──────────────────────────────────────────────────────────┐
│ L2: Unity 表现层 │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ UI/HUD │ │ Menu System │ │
│ │ Canvas │ │ Canvas │ │
│ └──────┬──────┘ └──────┬───────┘ │
│ │ │ │
│ └───────┬────────┘ │
│ │ 只读L0状态 │
├─────────────────┼────────────────────────────────────────┤
│ L1: Unity 适配层 │
│ ┌──────────────┐ ┌──────────┐ ┌───────────┐ │
│ │Input Adapter │ │ Camera │ │Level │ │
│ │InputSystem │ │ Ctrl │ │Loader │ │
│ └──────┬───────┘ └────┬─────┘ └─────┬─────┘ │
│ ┌──────┴───────┐ ┌────┴─────┐ │
│ │ VFX Spawner │ │ Audio │ │
│ │ ParticleSys │ │ Player │ │
│ └──────┬───────┘ └────┬─────┘ │
│ │ │ │
│ └──────┬───────┘ │
│ │ 方法调用↓ 事件订阅↑ │
├────────────────┼────────────────────────────────────────────┤
│ L0: 纯C# 核心逻辑层 (零Unity依赖) │
│ ┌──────────────┐ ┌──────────┐ ┌──────────────┐ │
│ │Blood Energy │ │ Combat │ │Form Switch │ │
│ │Economy │ │ Logic │ │State Machine │ │
│ └──────┬───────┘ └────┬─────┘ └──────┬───────┘ │
│ ┌──────┴───────┐ ┌────┴─────┐ ┌──────┴───────┐ │
│ │Enemy AI │ │Boss AI │ │Wave Manager │ │
│ │Logic │ │Logic │ │Logic │ │
│ └──────────────┘ └──────────┘ └──────────────┘ │
│ ┌──────────────┐ ┌──────────┐ ┌──────────────┐ │
│ │Death/Respawn │ │Skill Tree│ │Save/Load │ │
│ │Rules │ │Rules │ │Logic │ │
│ └──────────────┘ └──────────┘ └──────────────┘ │
│ ┌──────────────┐ │
│ │Score Calc │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### Communication Rules
| Direction | Allowed | Mechanism |
|-----------|---------|-----------|
| L0 → L0 | ✓ | Direct method call, C# event |
| L0 → L1 | ✗ | L0 does NOT call L1. L0 emits C# events; L1 subscribes. |
| L0 → L2 | ✗ | L2 reads L0 state through L1 or direct POCO access |
| L1 → L0 | ✓ | Method calls (e.g., `combat.Attack()`), property reads |
| L1 → L2 | ✓ | Method calls (e.g., `hud.Refresh(state)`) |
| L2 → L0 | ✓ | Read-only access to L0 public properties and state aggregates |
---
## Module Ownership
### L0 — Pure C# Logic Layer
| Module | Owns | Exposes | Consumes | Unity Deps |
|--------|------|---------|----------|-----------|
| **Blood Energy Economy** | Current/max energy, gain/spend rules, decay formula | `float Current`, `float Max`, `void Add(float)`, `bool Spend(float)`, `event OnEnergyChanged` | — | Zero |
| **Combat Logic** | Hit detection (geometry intersection), damage formula, crit rules | `DamageResult Attack(AttackData)`, `bool HitTest(Shape, Shape)` | — (uses own geometry lib) | Zero |
| **Form Switch SM** | Current form, switch state machine, windup timer, switch conditions | `FormType CurrentForm`, `SwitchResult RequestSwitch(FormType)`, `AttackStyle GetAttackStyle(FormType)`, `event OnFormChanged` | Blood Energy (check/spend) | Zero |
| **Enemy AI Logic** | Per-type behavior rules, target selection, attack decisions | `AICommand Update(EnemyState, PlayerState)` | Combat Logic (damage calc) | Zero |
| **Boss AI Logic** | Phase logic, phase transition conditions, phase-specific behaviors | `BossCommand Update(BossState, PlayerState)`, `event OnPhaseChanged` | Enemy AI, Combat Logic | Zero |
| **Wave Manager Logic** | Wave composition data, spawn timers, wave state | `WaveComposition GetNextWave()`, `bool IsWaveComplete()`, `event OnWaveStart`, `event OnWaveBreak` | Enemy AI (types) | Zero |
| **Death/Respawn Rules** | Death trigger, checkpoint state, respawn logic | `bool IsDead(float)`, `Vector3 RespawnPoint`, `event OnDeath`, `event OnRespawn` | Combat Logic (health) | Zero |
| **Skill Tree Rules** | Unlock conditions, upgrade formulas, form synergy effects | `bool CanUnlock(SkillNode)`, `void Unlock(SkillNode)` | Form Switch SM | Zero |
| **Save/Load Logic** | Serialization format, file paths, version migration | `void Save(GameState)`, `GameState Load()`, `bool SaveExists(int)` | Skill Tree Rules | Zero |
| **Score Calculator** | Score formula, combo tracking, per-wave stats | `int Score`, `float StyleRank`, `void OnKill(KillData)` | Combat Logic, Wave Manager | Zero |
### L1 — Unity Adapter Layer
| Module | Owns | Exposes | Consumes | Key Unity APIs |
|--------|------|---------|----------|---------------|
| **Input Adapter** | Input Action bindings, action map config | `event OnAttack`, `event OnSwitch(FormType)`, `Vector2 MoveInput` | — | `InputAction`, `PlayerInput` |
| **Camera Controller** | Camera component, follow logic, shake | `void SetTarget(Vector3)`, `void Shake(float)` | L0 player position (read) | `Camera`, `Transform` |
| **Level Loader** | Scene refs, load progress | `void LoadLevel(string)`, `event OnLevelLoaded` | — | `SceneManager`, `AsyncOperation` |
| **Audio Player** | AudioSource pool, clip mappings | `void Play(SfxType)`, `void SetMusic(MusicTrack)` | L0 events (subscribes) | `AudioSource`, `AudioMixer` |
| **VFX Spawner** | ParticleSystem pool, geometric preset library | `void Spawn(VfxType, Vector3)`, `void SetFormColor(FormType)` | L0 events (subscribes) | `ParticleSystem`, `ObjectPool<T>` |
### L2 — Unity Presentation Layer
| Module | Owns | Exposes | Consumes | Key Unity APIs |
|--------|------|---------|----------|---------------|
| **UI/HUD** | Canvas, form indicator, blood energy bar, wave info | `void Refresh(HUDState)` | L0 state (read only) | `Canvas`, `UI.Image`, `TMP_Text` |
| **Menu System** | Main menu, pause panel, skill tree UI, settings | `void ShowMainMenu()`, `void ShowPause()` | Input Adapter, Level Loader, Save/Load | `Canvas`, `UI.Button` |
---
## Data Flow
### Combat Frame Path
```
Unity Update (L1)
→ Input Adapter reads Input System → fires C# events
→ L1 calls L0: CombatSystem.Attack(), FormSwitchSM.Update()
→ L0 resolves: hit tests, damage, AI decisions, state machine ticks
→ L0 fires events: OnHit, OnFormChanged, OnEnergyChanged
→ L1 subscribers: VFX.Spawn(), Audio.Play()
→ L1 reads L0 state: Camera.SetTarget(), HUD.Refresh(state)
→ Unity renders to screen
```
### Form Switch Event Chain
```
Input → OnSwitch(Wolf) event
→ FormSwitchSM.RequestSwitch(Wolf)
→ BloodEnergy.Spend(cost)
→ Windup timer starts
→ Windup completes → OnFormChanged(Human→Wolf)
→ VFX: switch particle burst
→ Audio: switch SFX
→ HUD: color update
→ Enemy AI: re-evaluate threat priority
```
### Initialization Order
```
1. Unity Awake: Level Loader → Input Adapter → Camera → Audio → VFX
2. L0 bootstrap (triggered by L1):
Blood Energy → Combat Logic → Form Switch SM → Enemy AI → Wave Manager
3. L2 HUD subscribes to L0 events
4. Game start signal
```
---
## API Boundaries
### Core L0 Interfaces
```csharp
// Blood Energy Economy
public class BloodEnergyEconomy {
public float Current { get; }
public float Max { get; }
public float GainRate { get; set; }
public void Add(float amount);
public bool CanSpend(float amount);
public bool Spend(float amount);
public void Reset();
public event Action<float,float> OnEnergyChanged;
// Invariant: 0 ≤ Current ≤ Max
}
// Combat Logic
public struct AttackData {
public Vector3 Origin;
public Shape HitShape;
public float Damage;
public float KnockbackForce;
public FormType SourceForm;
}
public struct DamageResult {
public int TargetId;
public float FinalDamage;
public bool IsCritical;
public Vector3 HitPoint;
public bool IsKillingBlow;
}
public class CombatLogic {
public static bool TestHit(Shape a, Shape b);
public DamageResult CalculateDamage(AttackData atk, EnemyDefenseData def);
public List<DamageResult> ResolveAttacks(List<AttackData> atks, List<EnemyState> enemies);
}
// Form Switch State Machine
public enum FormType { Human, Wolf, Mist }
public enum SwitchPhase { Idle, Windup, Switching, Recovery }
public enum SwitchResult { Success, InsufficientEnergy, OnCooldown, InvalidTarget }
public struct AttackStyle {
public float Range, BaseDamage, AttackSpeed, AreaSize;
public Shape AttackShape;
public int MaxTargets;
}
public class FormSwitchStateMachine {
public FormType CurrentForm { get; }
public SwitchPhase Phase { get; }
public SwitchResult RequestSwitch(FormType target);
public AttackStyle GetAttackStyle(FormType form);
public void Update(float deltaTime);
public event Action<FormType,FormType> OnFormChanged;
public event Action<SwitchPhase> OnPhaseChanged;
}
// Enemy AI Logic
public struct AICommand {
public CommandType Type; // Move, Attack, Ability, Flee
public Vector3 TargetPosition;
public int TargetId, AbilityId;
}
public class EnemyAILogic {
public AICommand ComputeAction(EnemyState self, PlayerState player);
}
// Wave Manager Logic
public struct WaveComposition {
public List<EnemySpawnEntry> Enemies;
public float DelayBetweenGroups;
public string[] SpecialConditions;
}
public class WaveManagerLogic {
public WaveState CurrentState { get; }
public int CurrentWave { get; }
public WaveComposition GetNextWave();
public void OnEnemyKilled(int enemyId);
public void Update(float deltaTime);
public event Action<WaveComposition> OnWaveStart;
public event Action<float> OnWaveBreak;
public event Action OnAllWavesComplete;
}
```
### L1 Adapter Interfaces
```csharp
// Input Adapter — sole owner of Unity Input System interaction
public class InputAdapter : MonoBehaviour {
public event Action OnAttackPressed, OnAttackReleased, OnDodgePressed, OnPausePressed;
public event Action<FormType> OnSwitchPressed;
public Vector2 MoveInput { get; }
}
// VFX Spawner — subscribes to L0 events, drives Unity ParticleSystem
public class VFXSpawner : MonoBehaviour {
public void PlayFormSwitchEffect(FormType from, FormType to, Vector3 pos);
public void PlayHitEffect(Vector3 pos, float damage);
public void PlayKillEffect(Vector3 pos);
public void PlayBossPhaseEffect(Vector3 pos);
}
```
### L2 HUD Interface
```csharp
// HUDState — pure data aggregate of L0 state for rendering
public struct HUDState {
public float BloodEnergyCurrent, BloodEnergyMax;
public FormType CurrentForm;
public float PlayerHealth;
public int CurrentWave, TotalWaves;
public float WaveProgress;
public int StyleRank;
public float BossHealth;
}
public class HUDManager : MonoBehaviour {
public void Refresh(HUDState state); // called every frame by L1
}
```
---
## ADR Audit
| Status | Detail |
|--------|--------|
| Existing ADRs found | 0 |
| Traceable TRs from GDDs | 0 (no per-system GDDs authored) |
| Architecture decisions in this doc | All API boundaries, layer map, data flow, communication rules |
No existing ADRs to audit. All architectural decisions in this document require formal ADR records.
---
## Required ADRs
### Must create before coding (Foundation)
| ADR ID | Title | Covers | Priority |
|--------|-------|--------|----------|
| ADR-001 | Three-Layer Architecture & Code Isolation | L0/L1/L2 directory layout, asmdef configuration, cross-layer reference prohibition | BLOCKING |
| ADR-002 | Event Bus & Communication Pattern | C# event as sole L0→L1 mechanism, subscription lifecycle, no direct L0→Unity calls | BLOCKING |
| ADR-003 | Pure C# Geometry & Math Library | Custom Vector3, Shape hierarchy, Math utilities — zero UnityEngine dependency | BLOCKING |
### Should create before relevant system (Core)
| ADR ID | Title | Covers | Priority |
|--------|-------|--------|----------|
| ADR-004 | Form Switch State Machine Design | State transitions, timing parameters, windup/invuln/recovery phases, interrupt behavior | HIGH |
| ADR-005 | Enemy AI Decision Model | Behavior tree vs state machine vs utility AI — choice and rationale | HIGH |
| ADR-006 | Wave Composition Data Format | WaveConfig structure, ScriptableObject vs JSON, editor tooling | MEDIUM |
| ADR-007 | Serialization Format & Save Strategy | JSON vs binary, version migration policy, save slot management | MEDIUM |
### Can defer to implementation (Feature)
| ADR ID | Title | Covers | Priority |
|--------|-------|--------|----------|
| ADR-008 | Skill Tree Data Structure | Node graph representation, unlock dependencies, synergy trigger conditions | LOW |
| ADR-009 | Score & Style Formula | Kill weighting, combo decay, wave speed bonus, rank thresholds | LOW |
---
## Project Directory Structure
```
Assets/
├── Scripts/
│ ├── Core/ # L0 — Pure C# (no UnityEngine)
│ │ ├── Core.asmdef # asmdef: no Unity refs
│ │ ├── Combat/
│ │ │ ├── BloodEnergyEconomy.cs
│ │ │ ├── CombatLogic.cs
│ │ │ ├── FormSwitchStateMachine.cs
│ │ │ └── AttackStyle.cs
│ │ ├── Enemy/
│ │ │ ├── EnemyAILogic.cs
│ │ │ ├── BossAILogic.cs
│ │ │ └── WaveManagerLogic.cs
│ │ ├── Player/
│ │ │ └── DeathRespawnRules.cs
│ │ ├── Progression/
│ │ │ └── SkillTreeRules.cs
│ │ ├── Persistence/
│ │ │ └── SaveLoadLogic.cs
│ │ ├── Meta/
│ │ │ └── ScoreCalculator.cs
│ │ └── Geometry/ # ADR-003 — Pure C# math types
│ │ ├── Vector3.cs
│ │ ├── Shape.cs
│ │ └── MathUtil.cs
│ ├── Adapters/ # L1 — MonoBehaviour wrappers
│ │ ├── Adapters.asmdef # asmdef: refs Core + Unity
│ │ ├── InputAdapter.cs
│ │ ├── CameraController.cs
│ │ ├── LevelLoader.cs
│ │ ├── AudioPlayer.cs
│ │ └── VFXSpawner.cs
│ └── Presentation/ # L2 — UI & Rendering
│ ├── Presentation.asmdef # asmdef: refs Core + Adapters + Unity
│ ├── HUDManager.cs
│ ├── HUDState.cs
│ └── MenuSystem.cs
├── Scenes/ # Unity场景文件
├── Settings/ # URP质量配置
└── Tests/
├── Core.Tests/ # L0 单元测试 (NUnit, 无Unity)
│ ├── Core.Tests.asmdef
│ ├── BloodEnergyEconomyTests.cs
│ ├── CombatLogicTests.cs
│ └── FormSwitchSMTests.cs
└── Adapter.Tests/ # L1 集成测试 (Unity Test Framework)
└── Adapter.Tests.asmdef
```
### asmdef 配置规则
| 程序集 | 引用 | 禁止引用 |
|--------|------|----------|
| `Core.asmdef` | (none) | 不可引用任何 Unity 包 |
| `Adapters.asmdef` | Core | 不可引用 Presentation |
| `Presentation.asmdef` | Core, Adapters | — |
| `Core.Tests.asmdef` | Core | 不可引用 Unity (NUnit standalone) |
| `Adapter.Tests.asmdef` | Core, Adapters | — |
---
## Open Questions
- 是否需要专用的事件总线类管理 L0 事件的订阅/取消,还是直接使用 C# `event` + `Action`?—— ADR-002 中决定
- 敌人的 WaveConfig 用 ScriptableObject (方便策划编辑) 还是纯 JSON (更便携)?—— ADR-006 中决定
- 是否需要支持中途存档(战斗中途保存并恢复)还是仅关卡间存档?—— 在 Save/Load GDD 中决定
- L0 的三个 asmdef 子模块(Combat / Enemy / Player)是否需要独立 asmdef 而非一个 Core.asmdef?—— 当前一个 Core.asmdef 即可,复杂度不足以拆分