init
This commit is contained in:
commit
fb2252f688
|
|
@ -0,0 +1,38 @@
|
|||
# Build outputs
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
out/
|
||||
|
||||
# User-specific files
|
||||
*.user
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# IDE folders
|
||||
.vs/
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# NuGet
|
||||
*.nupkg
|
||||
*.snupkg
|
||||
packages/
|
||||
!**/packages/build/
|
||||
|
||||
# Test results
|
||||
TestResults/
|
||||
*.trx
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Local tooling/session artifacts
|
||||
.omx/
|
||||
.omc/
|
||||
.claude/
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build Commands
|
||||
|
||||
This is a Unity project that **cannot build standalone** outside of Unity Editor. The `.sln` references UnityEngine and UnityGameFramework which are only available in the Unity Editor environment. Use Unity's build system instead.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Three-Layer Structure (per `docs/LayeredArchitectureDesign.md`)
|
||||
|
||||
- **L0 (Domain)**: Pure C# business logic with no Unity dependencies. Contains enums, constants, data structures, CombatNode domain, PlayerInventory, InventoryGeneration, UI use cases.
|
||||
- **L1 (Infrastructure)**: Glue layer bridging L0 and Unity. Implements L0 interfaces, holds L0 service instances, manages Unity lifecycle.
|
||||
- **L2 (Presentation)**: Unity MonoBehaviour classes, UGuiForm implementations, Entity Logic (Player, Enemy, Tower).
|
||||
|
||||
### Key Domain Boundaries
|
||||
|
||||
| Domain | Key Files | Responsibility |
|
||||
|--------|-----------|----------------|
|
||||
| **CombatScheduler** | `CombatScheduler.cs`, `CombatStates/*` | State machine managing combat phases |
|
||||
| **EnemyManager** | `EnemyManager.cs`, `EnemySpawnDirector.cs`, `EnemyLifecycleTracker.cs` | Enemy spawning and lifecycle |
|
||||
| **PlayerInventory** | `PlayerInventoryComponent.cs`, `PlayerInventoryTowerAssemblyService.cs` | Backpack, trading, tower assembly |
|
||||
| **InventoryGeneration** | `InventoryGenerationComponent.cs`, `DropPoolRoller.cs`, `ShopGoodsBuilder.cs` | Drops, shop goods, rewards |
|
||||
| **MapEntity** | `MapEntity.cs`, `MapTopologyService.cs`, `TowerPlacementService.cs` | Map orchestration, grid/path, tower placement |
|
||||
|
||||
### Combat State Machine Flow
|
||||
|
||||
`Loading → RunningPhase → WaitingForPhaseEnd → Settlement → RewardSelection(可选) → FinishForm → WaitingForReturn`
|
||||
|
||||
### Dual-Currency System
|
||||
|
||||
- **Coin**: Single-combat internal currency (`CombatRunResourceStore.CurrentCoin`)
|
||||
- **Gold**: Cross-combat persistent currency (`PlayerInventoryComponent.Gold`)
|
||||
|
||||
### Tag System
|
||||
|
||||
Three-table configuration: `Tag.txt`, `RarityTagBudget.txt`, `TagConfig.txt`. Current shipped 7 tags: Fire, Ice, Crit, Execution, Shatter, Inferno, AbsoluteZero.
|
||||
|
||||
## Service Naming Conventions
|
||||
|
||||
- `Scheduler`: State machine boundary only
|
||||
- `Manager`: Facade/aggregate entry for subdomain
|
||||
- `Coordinator`: Cross-state orchestration
|
||||
- `Calculator`: Pure computation
|
||||
- `Session`: Single lifecycle object
|
||||
- `Bridge`: Framework boundary adapter
|
||||
- `Runtime`: Mutable state carrier
|
||||
- `Resolver`: Mapping/lookup/resolution
|
||||
|
||||
## Key Invariants
|
||||
|
||||
1. All combat state transitions go through `CombatScheduler.ChangeState(...)`
|
||||
2. `EnemyManager` only reports events, never directly calls state transitions
|
||||
3. `CombatRunResourceStore` is sole source of truth for in-combat Coin/Gold/BaseHp
|
||||
4. `EnemyLifecycleTracker` is sole source for `AliveEnemyCount` and `HasAliveBoss`
|
||||
5. `MapEntity` accesses combat context via `MapData` + Events only
|
||||
6. Tag generation uses `InventoryGenerationRandomContext` for reproducibility
|
||||
|
||||
## Documentation
|
||||
|
||||
- `docs/LayeredArchitectureDesign.md` - Three-layer architecture
|
||||
- `docs/CombatNodeArchitecture.md` - Combat scheduler and state machine
|
||||
- `docs/MapEntityArchitecture.md` - Map orchestration services
|
||||
- `docs/TagSystemDesign.md` - Tag system rules
|
||||
- `docs/GameDesign.md` - High-level game design
|
||||
- `docs/MVP-Scope.md` - Current MVP scope
|
||||
|
|
@ -0,0 +1,611 @@
|
|||
# GeometryTD 三层拆分迁移 TODO
|
||||
|
||||
最后更新:2026-04-30
|
||||
|
||||
> **2026-04-30 第一波完成**:Definition/Enum + Event
|
||||
|
||||
## 概述
|
||||
|
||||
| 指标 | 数量 | 状态 |
|
||||
|------|------|------|
|
||||
| 总文件数 | 457 | - |
|
||||
| L0 (Domain) 可直接迁移 | ~180 | **第一波已完成 62 个文件** |
|
||||
| L1 (Infrastructure) 需重构 | ~80 | - |
|
||||
| L2 (Presentation) Unity 依赖 | ~197 | - |
|
||||
|
||||
## 项目骨架
|
||||
|
||||
```
|
||||
src/
|
||||
├── GeometryTD.Domain/ # L0 - 纯净 C#
|
||||
├── GeometryTD.Infrastructure/ # L1 - Unity 胶水层
|
||||
├── GeometryTD.Presentation/ # L2 - Unity 表现层
|
||||
└── Geometry-Tower-Defense-Base.sln
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 阶段一:L0 直接迁移(无需修改)
|
||||
|
||||
以下文件可直接复制到 `GeometryTD.Domain/`,无任何修改。
|
||||
|
||||
### Definition/Enum(27 个文件)
|
||||
|
||||
- [x] `Definition/Enum/AttackMethodType.cs`
|
||||
- [x] `Definition/Enum/AttackPropertyType.cs`
|
||||
- [x] `Definition/Enum/CampType.cs`
|
||||
- [x] `Definition/Enum/CombatSelectActionType.cs` ⚠️ **已修改** - 从 `UI/Combat/Context/` 迁移
|
||||
- [x] `Definition/Enum/EntryType.cs`
|
||||
- [x] `Definition/Enum/EventEffectType.cs`
|
||||
- [x] `Definition/Enum/EventRequirementType.cs`
|
||||
- [x] `Definition/Enum/InventoryTagSourceType.cs`
|
||||
- [x] `Definition/Enum/LevelThemeType.cs`
|
||||
- [x] `Definition/Enum/LevelVictoryType.cs`
|
||||
- [x] `Definition/Enum/PhaseEndType.cs`
|
||||
- [x] `Definition/Enum/ProcedureMainCombatEntryBlockReason.cs`
|
||||
- [x] `Definition/Enum/ProcedureMainFlowPhase.cs`
|
||||
- [x] `Definition/Enum/ProcedureMainRunAdvanceResult.cs`
|
||||
- [x] `Definition/Enum/ProcedureMainRunCompletionResult.cs`
|
||||
- [x] `Definition/Enum/RarityType.cs`
|
||||
- [x] `Definition/Enum/RelationType.cs`
|
||||
- [x] `Definition/Enum/RepoItemClickActionType.cs`
|
||||
- [x] `Definition/Enum/RunNodeCompletionStatus.cs` ⚠️ **已修改** - 从 `Procedure/ProcedureMain/RunModel.cs` 迁移枚举
|
||||
- [x] `Definition/Enum/RunNodeStatus.cs` ⚠️ **已修改** - 从 `Procedure/ProcedureMain/RunModel.cs` 迁移枚举
|
||||
- [x] `Definition/Enum/RunNodeType.cs` ⚠️ **已修改** - 从 `Procedure/ProcedureMain/RunModel.cs` 迁移枚举
|
||||
- [x] `Definition/Enum/SceneType.cs`
|
||||
- [x] `Definition/Enum/TagCategory.cs`
|
||||
- [x] `Definition/Enum/TagTriggerPhase.cs`
|
||||
- [x] `Definition/Enum/TagType.cs`
|
||||
- [x] `Definition/Enum/TowerCompSlotType.cs`
|
||||
- [x] `Definition/Enum/UIFormType.cs`
|
||||
|
||||
> ⚠️ 注:原 TODO 统计 25 个 Enum 文件,实际源文件 23 个。第一波新增 4 个枚举从其他层迁移:CombatSelectActionType(UI层)、RunNodeType/RunNodeStatus/RunNodeCompletionStatus(Procedure层)
|
||||
|
||||
### Definition/DataStruct(8 个文件)
|
||||
|
||||
- [ ] `Definition/DataStruct/BackpackInventoryData.cs`
|
||||
- [ ] `Definition/DataStruct/BuildInfo.cs`
|
||||
- [ ] `Definition/DataStruct/EventItem.cs`
|
||||
- [ ] `Definition/DataStruct/EventOption.cs`
|
||||
- [ ] `Definition/DataStruct/ImpactData.cs`
|
||||
- [ ] `Definition/DataStruct/TowerCompItemData.cs`
|
||||
- [ ] `Definition/DataStruct/TowerStatsData.cs`
|
||||
- [ ] `Definition/DataStruct/VersionInfo.cs`
|
||||
|
||||
### Definition/ 其他
|
||||
|
||||
- [ ] `Definition/CombatParticipantTowerValidation.cs`
|
||||
- [ ] `Definition/CombatParticipantTowerValidationText.cs`
|
||||
- [ ] `Definition/InventoryRarityRuleService.cs`
|
||||
- [ ] `Definition/ParticipantTowerAssignResult.cs`
|
||||
|
||||
### Event(35 个文件)
|
||||
|
||||
- [x] `Event/Combat/CombatBaseHpChangedEventArgs.cs`
|
||||
- [x] `Event/Combat/CombatCoinChangedEventArgs.cs`
|
||||
- [x] `Event/Combat/CombatDebugFailEventArgs.cs`
|
||||
- [x] `Event/Combat/CombatEndEventArgs.cs`
|
||||
- [x] `Event/Combat/CombatEnemyHpRateChangedEventArgs.cs`
|
||||
- [x] `Event/Combat/CombatFinishReturnEventArgs.cs`
|
||||
- [x] `Event/Combat/CombatPauseEventArgs.cs`
|
||||
- [x] `Event/Combat/CombatProcessEventArgs.cs`
|
||||
- [x] `Event/Combat/CombatSelectItemClickEventArgs.cs` ⚠️ **已修改** - 依赖迁移的枚举
|
||||
- [x] `Event/EventForm/EventOptionItemSelectedEventArgs.cs`
|
||||
- [x] `Event/Game/NodeCompleteEventArgs.cs` ⚠️ **已修改** - 依赖迁移的枚举和数据结构
|
||||
- [x] `Event/Game/NodeEnterEventArgs.cs` ⚠️ **已修改** - 依赖迁移的枚举
|
||||
- [x] `Event/Game/NodeMapNodeClickEventArgs.cs`
|
||||
- [x] `Event/Game/NodeMapNodeEnterRequestedEventArgs.cs` ⚠️ **已修改** - 依赖迁移的枚举
|
||||
- [x] `Event/Game/TestMenuNodeClickEventArgs.cs`
|
||||
- [x] `Event/General/RewardSelectGiveUpEventArgs.cs`
|
||||
- [x] `Event/General/RewardSelectItemSelectedEventArgs.cs`
|
||||
- [x] `Event/General/RewardSelectRefreshEventArgs.cs`
|
||||
- [x] `Event/MainForm/RepoButtonClickedEventArgs.cs`
|
||||
- [x] `Event/MainForm/ReturnButtonClickedEventArgs.cs`
|
||||
- [x] `Event/Menu/MenuExitRequestedEventArgs.cs`
|
||||
- [x] `Event/Menu/MenuSettingsRequestedEventArgs.cs`
|
||||
- [x] `Event/Menu/MenuStartRequestedEventArgs.cs`
|
||||
- [x] `Event/RepoForm/CombineSlotClickedEventArgs.cs`
|
||||
- [x] `Event/RepoForm/RepoCombineRequestedEventArgs.cs`
|
||||
- [x] `Event/RepoForm/RepoFormReturnEventArgs.cs`
|
||||
- [x] `Event/RepoForm/RepoItemClickedEventArgs.cs` ⚠️ **已修改** - `UnityEngine.Vector2` → `System.Numerics.Vector2`
|
||||
- [x] `Event/RepoForm/RepoItemDragEndedEventArgs.cs`
|
||||
- [x] `Event/RepoForm/RepoParticipantAssignRequestedEventArgs.cs`
|
||||
- [x] `Event/RepoForm/RepoSellCancelRequestedEventArgs.cs`
|
||||
- [x] `Event/RepoForm/RepoSellConfirmRequestedEventArgs.cs`
|
||||
- [x] `Event/RepoForm/RepoSellModeToggleRequestedEventArgs.cs`
|
||||
- [x] `Event/Shop/ShopExitRequestedEventArgs.cs`
|
||||
- [x] `Event/Shop/ShopInventoryRequestedEventArgs.cs`
|
||||
- [x] `Event/Shop/ShopPurchaseRequestedEventArgs.cs`
|
||||
|
||||
> ⚠️ 注:以下 5 个 Event 文件在初始迁移后因跨层依赖被移除,后经修复(枚举迁移、Vector2替换)后重新加入:CombatSelectItemClickEventArgs、NodeCompleteEventArgs、NodeEnterEventArgs、NodeMapNodeEnterRequestedEventArgs、RepoItemClickedEventArgs
|
||||
|
||||
### UI/Base(3 个文件)
|
||||
|
||||
- [ ] `UI/Base/IUIFormController.cs`
|
||||
- [ ] `UI/Base/IUIUseCase.cs`
|
||||
- [ ] `UI/Base/UIContext.cs`
|
||||
|
||||
### Definition/Tag(37 个文件)
|
||||
|
||||
- [ ] `Definition/Tag/Aggregation/TagRuntimeData.cs`
|
||||
- [ ] `Definition/Tag/Aggregation/TowerTagAggregationService.cs`
|
||||
- [ ] `Definition/Tag/Combat/EnemyStatusTagRegistry.cs`
|
||||
- [ ] `Definition/Tag/Combat/Handlers/AttackShapeTagEffectHandler.cs`
|
||||
- [ ] `Definition/Tag/Combat/Handlers/NumericTagEffectHandler.cs`
|
||||
- [ ] `Definition/Tag/Combat/States/EnemyStatusTagStateBase.cs`
|
||||
- [ ] `Definition/Tag/Combat/States/FireTagState.cs`
|
||||
- [ ] `Definition/Tag/Combat/States/IceTagState.cs`
|
||||
- [ ] `Definition/Tag/Combat/StatusEffects/EnemyStatusTagEffectBase.cs`
|
||||
- [ ] `Definition/Tag/Combat/StatusEffects/FireTagEffect.cs`
|
||||
- [ ] `Definition/Tag/Combat/StatusEffects/IEnemyStatusTagEffect.cs`
|
||||
- [ ] `Definition/Tag/Combat/StatusEffects/IceTagEffect.cs`
|
||||
- [ ] `Definition/Tag/Combat/TagEffectResolver.cs`
|
||||
- [ ] `Definition/Tag/Generation/ComponentTagGenerationService.cs`
|
||||
- [ ] `Definition/Tag/Generation/InventoryTagRandomContext.cs`
|
||||
- [ ] `Definition/Tag/Generation/RarityTagBudgetRule.cs`
|
||||
- [ ] `Definition/Tag/Generation/RarityTagBudgetRuleRegistry.cs`
|
||||
- [ ] `Definition/Tag/Generation/TagGenerationRule.cs`
|
||||
- [ ] `Definition/Tag/Generation/TagGenerationRuleRegistry.cs`
|
||||
- [ ] `Definition/Tag/Metadata/Config/AbsoluteZeroTagConfig.cs`
|
||||
- [ ] `Definition/Tag/Metadata/Config/BurnSpreadTagConfig.cs`
|
||||
- [ ] `Definition/Tag/Metadata/Config/CritTagConfig.cs`
|
||||
- [ ] `Definition/Tag/Metadata/Config/ExecutionTagConfig.cs`
|
||||
- [ ] `Definition/Tag/Metadata/Config/FireTagConfig.cs`
|
||||
- [ ] `Definition/Tag/Metadata/Config/FreezeMaskTagConfig.cs`
|
||||
- [ ] `Definition/Tag/Metadata/Config/IceTagConfig.cs`
|
||||
- [ ] `Definition/Tag/Metadata/Config/IgniteBurstTagConfig.cs`
|
||||
- [ ] `Definition/Tag/Metadata/Config/InfernoTagConfig.cs`
|
||||
- [ ] `Definition/Tag/Metadata/Config/OverpenetrateTagConfig.cs`
|
||||
- [ ] `Definition/Tag/Metadata/Config/PierceTagConfig.cs`
|
||||
- [ ] `Definition/Tag/Metadata/Config/ShatterTagConfig.cs`
|
||||
- [ ] `Definition/Tag/Metadata/Config/TagConfigBase.cs`
|
||||
- [ ] `Definition/Tag/Metadata/TagDefinition.cs`
|
||||
|
||||
### Definition/Event(6 个文件)
|
||||
|
||||
- [ ] `Definition/Event/EventEffect/AddGoldEffect.cs`
|
||||
- [ ] `Definition/Event/EventEffect/AddRandomCompsEffect.cs`
|
||||
- [ ] `Definition/Event/EventEffect/DamageRandomTowerEnduranceEffect.cs`
|
||||
- [ ] `Definition/Event/EventEffect/EventEffectBase.cs`
|
||||
- [ ] `Definition/Event/EventEffect/RemoveRandomCompEffect.cs`
|
||||
- [ ] `Definition/Event/EventOptionExecutor.cs`
|
||||
- [ ] `Definition/Event/EventRequirement/CompCountAtLeastRequirement.cs`
|
||||
- [ ] `Definition/Event/EventRequirement/EventRequirementBase.cs`
|
||||
- [ ] `Definition/Event/EventRequirement/GoldAtLeastRequirement.cs`
|
||||
- [ ] `Definition/Event/EventRequirement/HasRelicRequirement.cs`
|
||||
- [ ] `Definition/Event/EventRequirement/TowerCountAtLeastRequirement.cs`
|
||||
|
||||
### Factory(6 个文件)
|
||||
|
||||
- [ ] `Factory/EventEffectFactory.cs`
|
||||
- [ ] `Factory/EventRequirementFactory.cs`
|
||||
- [ ] `Factory/OutGameDropItemBuilder.cs`
|
||||
- [ ] `Factory/PhaseEndConditionFactory.cs`
|
||||
- [ ] `Factory/RunStateFactory.cs`
|
||||
|
||||
### Network(12 个文件)
|
||||
|
||||
- [ ] `Network/CSPacketBase.cs`
|
||||
- [ ] `Network/CSPacketHeader.cs`
|
||||
- [ ] `Network/Packet/CSHeartBeat.cs`
|
||||
- [ ] `Network/Packet/SCHeartBeat.cs`
|
||||
- [ ] `Network/PacketBase.cs`
|
||||
- [ ] `Network/PacketHandler/SCHeartBeatHandler.cs`
|
||||
- [ ] `Network/PacketHandlerBase.cs`
|
||||
- [ ] `Network/PacketHeaderBase.cs`
|
||||
- [ ] `Network/PacketType.cs`
|
||||
- [ ] `Network/SCPacketBase.cs`
|
||||
- [ ] `Network/SCPacketHeader.cs`
|
||||
|
||||
### UI/Context / RawData(大量文件)
|
||||
|
||||
- [ ] `UI/DialogParams.cs`
|
||||
- [ ] `UI/Game/Context/CombineAreaContext.cs`
|
||||
- [ ] `UI/Game/Context/CompAreaContext.cs`
|
||||
- [ ] `UI/Game/Context/EventFormContext.cs`
|
||||
- [ ] `UI/Game/Context/EventOptionItemContext.cs`
|
||||
- [ ] `UI/Game/Context/MainFormContext.cs`
|
||||
- [ ] `UI/Game/Context/NodeItemContext.cs`
|
||||
- [ ] `UI/Game/Context/NodeMapFormContext.cs`
|
||||
- [ ] `UI/Game/Context/ParticipantAreaContext.cs`
|
||||
- [ ] `UI/Game/Context/RepoFormContext.cs`
|
||||
- [ ] `UI/Game/Context/RepoFormState.cs`
|
||||
- [ ] `UI/Game/Context/RepoItemClickActionType.cs`
|
||||
- [ ] `UI/Game/Context/RepoItemContext.cs`
|
||||
- [ ] `UI/Game/Context/SellAreaContext.cs`
|
||||
- [ ] `UI/Game/Context/TowerRepoItemContext.cs`
|
||||
- [ ] `UI/Game/RawData/EventFormRawData.cs`
|
||||
- [ ] `UI/Game/RawData/NodeMapFormRawData.cs`
|
||||
- [ ] `UI/Game/RawData/RepoFormRawData.cs`
|
||||
- [ ] `UI/Game/RepoParticipantAssignDialogUtility.cs`
|
||||
- [ ] `UI/Game/View/IRepoDragItemView.cs`
|
||||
- [ ] `UI/General/Context/DialogFormContext.cs`
|
||||
- [ ] `UI/General/Context/RewardItemContext.cs`
|
||||
- [ ] `UI/General/Context/RewardSelectFormContext.cs`
|
||||
- [ ] `UI/General/Context/TagItemContext.cs`
|
||||
- [ ] `UI/General/RawData/DialogFormRawData.cs`
|
||||
- [ ] `UI/General/RawData/ItemDescFormRawData.cs`
|
||||
- [ ] `UI/General/RawData/RewardSelectFormRawData.cs`
|
||||
- [ ] `UI/General/RawData/RewardSelectItemRawData.cs`
|
||||
- [ ] `UI/General/RawData/RewardSelectItemRawDataBuilder.cs`
|
||||
- [ ] `UI/Menu/Context/MenuFormContext.cs`
|
||||
- [ ] `UI/Menu/Context/TestMenuFormContext.cs`
|
||||
- [ ] `UI/Menu/RawData/MenuFormRawData.cs`
|
||||
- [ ] `UI/Shop/Context/GoodsItemContext.cs`
|
||||
- [ ] `UI/Shop/Context/ShopFormContext.cs`
|
||||
- [ ] `UI/Shop/RawData/GoodsItemRawData.cs`
|
||||
- [ ] `UI/Shop/RawData/ShopFormRawData.cs`
|
||||
- [ ] `UI/Templates/GameScene/Context/DisplayItemContext.cs`
|
||||
- [ ] `UI/Templates/GameScene/Context/DisplayListAreaContext.cs`
|
||||
- [ ] `UI/Templates/GameScene/Context/GoodsItemContext.cs`
|
||||
- [ ] `UI/Templates/GameScene/Context/HudFormContext.cs`
|
||||
- [ ] `UI/Templates/GameScene/Context/LevelUpFormContext.cs`
|
||||
- [ ] `UI/Templates/GameScene/Context/LevelUpRewardItemContext.cs`
|
||||
- [ ] `UI/Templates/GameScene/Context/ShopFormContext.cs`
|
||||
- [ ] `UI/Templates/GameScene/RawData/LevelUpFormRawData.cs`
|
||||
- [ ] `UI/Templates/GameScene/RawData/ShopFormRawData.cs`
|
||||
- [ ] `UI/Templates/MenuScene/Context/RoleItemContext.cs`
|
||||
- [ ] `UI/Templates/MenuScene/Context/RolePropertyAreaContext.cs`
|
||||
- [ ] `UI/Templates/MenuScene/Context/SelectRoleFormContext.cs`
|
||||
- [ ] `UI/Templates/MenuScene/Context/StartMenuFormContext.cs`
|
||||
- [ ] `UI/Templates/MenuScene/RawData/SelectRoleFormRawData.cs`
|
||||
- [ ] `UI/Templates/MenuScene/RawData/StartMenuFormRawData.cs`
|
||||
- [ ] `UI/Combat/Context/CombatFinishFormContext.cs`
|
||||
- [ ] `UI/Combat/Context/CombatInfoFormContext.cs`
|
||||
- [ ] `UI/Combat/Context/CombatSelectActionType.cs`
|
||||
- [ ] `UI/Combat/RawData/CombatFinishFormRawData.cs`
|
||||
- [ ] `UI/Combat/RawData/CombatInfoFormRawData.cs`
|
||||
- [ ] `UI/Combat/RawData/CombatSelectClickObjectType.cs`
|
||||
- [ ] `UI/Combat/RawData/CombatSelectDisplayMode.cs`
|
||||
|
||||
### CustomComponent 纯 C# 文件
|
||||
|
||||
- [ ] `Components/IDamageReceiver.cs`
|
||||
- [ ] `CustomComponent/CombatNode/CombatScheduler/CombatSettlementContext.cs`
|
||||
- [ ] `CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropContext.cs`
|
||||
- [ ] `CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResult.cs`
|
||||
- [ ] `CustomComponent/CombatNode/CombatScheduler/ICombatSchedulerPort.cs`
|
||||
- [ ] `CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/IPhaseEndCondition.cs`
|
||||
- [ ] `CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/NonePhaseEndCondition.cs`
|
||||
- [ ] `CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/PhaseEndConditionContext.cs`
|
||||
- [ ] `CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/TimeElapsedPhaseEndCondition.cs`
|
||||
- [ ] `CustomComponent/CombatNode/CombatSettlementCalculator.cs`
|
||||
- [ ] `CustomComponent/CombatNode/CombatSettlementCommitter.cs`
|
||||
- [ ] `CustomComponent/CombatNode/EnemyManager/EnemyLifecycleTracker.cs`
|
||||
- [ ] `CustomComponent/InventoryGeneration/DropPoolRoller.cs`
|
||||
- [ ] `CustomComponent/InventoryGeneration/InventoryGenerationRandomContext.cs`
|
||||
- [ ] `CustomComponent/InventoryGeneration/RewardCandidateBuilder.cs`
|
||||
- [ ] `CustomComponent/PlayerInventory/PlayerInventoryStateStore.cs`
|
||||
- [ ] `CustomComponent/PlayerInventory/PlayerInventoryTowerRosterService.cs`
|
||||
- [ ] `CustomComponent/PlayerInventory/PlayerInventoryTradeService.cs`
|
||||
|
||||
### Scene/Pathfinding
|
||||
|
||||
- [ ] `Scene/Pathfinding/IMapPathfinder.cs`
|
||||
|
||||
### Procedure 纯 C#
|
||||
|
||||
- [ ] `Procedure/ProcedureMain/FixedRunNodeSequenceBuilder.cs`
|
||||
- [ ] `Procedure/ProcedureMain/ProcedureMainCombatEntryValidationResult.cs`
|
||||
- [ ] `Procedure/ProcedureMain/ProcedureMainCombatEntryValidationService.cs`
|
||||
- [ ] `Procedure/ProcedureMain/ProcedureMainNodeEventGuardService.cs`
|
||||
- [ ] `Procedure/ProcedureMain/ProcedureMainParticipantTowerCleanupService.cs`
|
||||
- [ ] `Procedure/ProcedureMain/ProcedureMainRunCompletionService.cs`
|
||||
- [ ] `Procedure/ProcedureMain/ProcedureMainRunFlowService.cs`
|
||||
- [ ] `Procedure/ProcedureMain/RunStateAdvanceService.cs`
|
||||
|
||||
### Entity
|
||||
|
||||
- [ ] `Entity/EntityLogic/EnemyTagStatusRuntime.cs`
|
||||
|
||||
### Utility
|
||||
|
||||
- [ ] `Utility/EnumUtility.cs`
|
||||
- [ ] `Utility/InventoryCloneUtility.cs`
|
||||
- [ ] `Utility/InventoryParticipantUtility.cs`
|
||||
- [ ] `Utility/InventorySeedUtility.cs`
|
||||
- [ ] `Utility/ItemDescUtility.cs`
|
||||
- [ ] `Utility/ShopPriceRuleService.cs`
|
||||
- [ ] `Utility/WebUtility.cs`
|
||||
|
||||
---
|
||||
|
||||
## 阶段二:L1 重构迁移(需修改后迁移)
|
||||
|
||||
### 策略 1:Vector3 替换
|
||||
|
||||
将 `UnityEngine.Vector3` 替换为 `System.Numerics.Vector3`
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
|------|----------|
|
||||
| `Definition/DataStruct/AttackPayload.cs` | `Vector3 OriginPosition` → `System.Numerics.Vector3` |
|
||||
| `Definition/DataStruct/HitContext.cs` | `Vector3` → `System.Numerics.Vector3` |
|
||||
| `Entity/EntityLogic/CombatSelectInputService.cs` | `Vector2` → `System.Numerics.Vector2` |
|
||||
|
||||
### 策略 2:GameEntry 静态调用替换为注入服务
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
|------|----------|
|
||||
| `Definition/Tag/Metadata/TagDefinitionRegistry.cs` | `GameEntry.DataTable` → 注入 `IDataTableService` |
|
||||
| `Definition/Tag/Presentation/TagDisplayUtility.cs` | `GameEntry.DataTable` → 注入 `IDataTableService` |
|
||||
| `Utility/AssetUtility.cs` | `GameEntry.*` → 注入服务 |
|
||||
| `Utility/JsonNetUtility.cs` | `GameFramework.Utility.Json` → 注入 |
|
||||
|
||||
### 策略 3:DataTable 保持 L1
|
||||
|
||||
所有 DR* 类继承 `UnityGameFramework.Runtime.DataRowBase`,留在 L1:
|
||||
|
||||
- [ ] `DataTable/DREnemy.cs`
|
||||
- [ ] `DataTable/DRLevel.cs`
|
||||
- [ ] `DataTable/DRLevelPhase.cs`
|
||||
- [ ] `DataTable/DRLevelSpawnEntry.cs`
|
||||
- [ ] `DataTable/DREvent.cs`
|
||||
- [ ] `DataTable/DREntity.cs`
|
||||
- [ ] `DataTable/DRBaseComp.cs`
|
||||
- [ ] `DataTable/DRBearingComp.cs`
|
||||
- [ ] `DataTable/DRMusic.cs`
|
||||
- [ ] `DataTable/DRMuzzleComp.cs`
|
||||
- [ ] `DataTable/DROutGameDropPool.cs`
|
||||
- [ ] `DataTable/DRRarityTagBudget.cs`
|
||||
- [ ] `DataTable/DRScene.cs`
|
||||
- [ ] `DataTable/DRShopPrice.cs`
|
||||
- [ ] `DataTable/DRSound.cs`
|
||||
- [ ] `DataTable/DRTag.cs`
|
||||
- [ ] `DataTable/DRTagConfig.cs`
|
||||
- [ ] `DataTable/DRUIForm.cs`
|
||||
- [ ] `DataTable/DRUISound.cs`
|
||||
- [ ] `DataTable/DataTableExtension.cs`(含 Vector3/Color 解析)
|
||||
- [ ] `DataTable/BinaryReaderExtension.cs`
|
||||
|
||||
### 策略 4:Tilemap 接口抽象
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
|------|----------|
|
||||
| `Scene/Map/MapTopologyService.cs` | `Tilemap` → `ITilemap` 接口 |
|
||||
| `Scene/Map/TowerPlacementService.cs` | `Tilemap` → `ITilemap` 接口 |
|
||||
| `Scene/Map/TowerSelectionPresenter.cs` | `Vector3` → `System.Numerics.Vector3` |
|
||||
| `Scene/Map/MapCombatRuntimeBridge.cs` | Unity 类型 → 接口 |
|
||||
| `Scene/Pathfinding/GridMapPathfinder.cs` | `Vector3Int` → 自定义 struct |
|
||||
|
||||
### 策略 5:Constant.Layer 修复
|
||||
|
||||
- [ ] `Definition/Constant/Constant.Layer.cs` — `LayerMask.NameToLayer` → int 常量
|
||||
|
||||
### 策略 6:Color 替换
|
||||
|
||||
- [ ] `Entity/EntityLogic/CombatSelectUseCaseConfigurator.cs` — `UnityEngine.Color` → 纯 C# struct
|
||||
- [ ] `Procedure/ProcedureMain/RunState.cs` — `UnityEngine.Color` → 纯 C# struct
|
||||
- [ ] `Utility/IconColorGenerator.cs` — `UnityEngine.Color` → 纯 C# struct
|
||||
- [ ] `Utility/TowerIconComposeUtility.cs` — `UnityEngine.Color/Sprite` → 接口
|
||||
|
||||
### 策略 7:TagEffectResolver
|
||||
|
||||
- [ ] `Definition/Tag/Combat/TagEffectResolver.cs` — `Mathf` → `System.Math`
|
||||
|
||||
---
|
||||
|
||||
## 阶段三:L2 保持 Unity
|
||||
|
||||
以下文件属于 L2,直接保留在 Unity 项目中,无需修改:
|
||||
|
||||
### Base(3 个文件)
|
||||
|
||||
- [ ] `Base/GameEntry.cs`
|
||||
- [ ] `Base/GameEntry.Builtin.cs`
|
||||
- [ ] `Base/GameEntry.Custom.cs`
|
||||
|
||||
### Components(8 个文件)
|
||||
|
||||
- [ ] `Components/BasicBaseComp.cs`
|
||||
- [ ] `Components/BasicBearingComp.cs`
|
||||
- [ ] `Components/InputComponent.cs`
|
||||
- [ ] `Components/MovementComponent.cs`
|
||||
- [ ] `Components/ShooterBullet.cs`
|
||||
- [ ] `Components/ShooterMuzzleComp.cs`
|
||||
- [ ] `Components/TowerController.cs`
|
||||
|
||||
### CustomComponent/*Component(10 个文件)
|
||||
|
||||
- [ ] `CustomComponent/BuiltinDataComponent.cs`
|
||||
- [ ] `CustomComponent/CombatNode/CombatNodeComponent.cs`
|
||||
- [ ] `CustomComponent/EventNodeComponent.cs`
|
||||
- [ ] `CustomComponent/HPBar/HPBarComponent.cs`
|
||||
- [ ] `CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs`
|
||||
- [ ] `CustomComponent/PlayerInventory/PlayerInventoryComponent.cs`
|
||||
- [ ] `CustomComponent/ResolutionAdapterComponent.cs`
|
||||
- [ ] `CustomComponent/ShopNodeComponent.cs`
|
||||
- [ ] `CustomComponent/SpriteCacheComponent.cs`
|
||||
- [ ] `CustomComponent/TagRegistry/TagRegistryComponent.cs`
|
||||
- [ ] `CustomComponent/UIRouterComponent.cs`
|
||||
|
||||
### Entity/EntityLogic(7 个文件)
|
||||
|
||||
- [ ] `Entity/EntityLogic/EntityBase.cs`
|
||||
- [ ] `Entity/EntityLogic/EnemyEntity.cs`
|
||||
- [ ] `Entity/EntityLogic/TowerEntity.cs`
|
||||
- [ ] `Entity/EntityLogic/BulletEntity.cs`
|
||||
- [ ] `Entity/EntityLogic/Player.cs`
|
||||
- [ ] `Entity/EntityLogic/MapEntity.cs`
|
||||
- [ ] `Entity/EntityLogic/CombatSelectUseCaseConfigurator.cs`
|
||||
|
||||
### UI/View(大量文件)
|
||||
|
||||
所有继承 `UGuiForm` 的 View 类:
|
||||
|
||||
- [ ] `UI/Combat/View/CombatFinishForm.cs`
|
||||
- [ ] `UI/Combat/View/CombatInfoForm.cs`
|
||||
- [ ] `UI/Combat/View/CombatSelectBuildArea.cs`
|
||||
- [ ] `UI/Combat/View/CombatSelectForm.cs`
|
||||
- [ ] `UI/Combat/View/CombatSelectFuncArea.cs`
|
||||
- [ ] `UI/Combat/View/TowerSelectItem.cs`
|
||||
- [ ] `UI/Game/View/CombineArea.cs`
|
||||
- [ ] `UI/Game/View/CombineSlotItem.cs`
|
||||
- [ ] `UI/Game/View/CompArea.cs`
|
||||
- [ ] `UI/Game/View/EventForm.cs`
|
||||
- [ ] `UI/Game/View/EventOptionItem.cs`
|
||||
- [ ] `UI/Game/View/MainForm.cs`
|
||||
- [ ] `UI/Game/View/NodeItem.cs`
|
||||
- [ ] `UI/Game/View/NodeMapForm.cs`
|
||||
- [ ] `UI/Game/View/ParticipantArea.cs`
|
||||
- [ ] `UI/Game/View/RepoForm.cs`
|
||||
- [ ] `UI/Game/View/RepoItem.cs`
|
||||
- [ ] `UI/Game/View/SellArea.cs`
|
||||
- [ ] `UI/Game/View/TowerRepoItem.cs`
|
||||
- [ ] `UI/General/View/DialogForm.cs`
|
||||
- [ ] `UI/General/View/IconArea.cs`
|
||||
- [ ] `UI/General/View/ItemDescForm.cs`
|
||||
- [ ] `UI/General/View/RewardItem.cs`
|
||||
- [ ] `UI/General/View/RewardSelectForm.cs`
|
||||
- [ ] `UI/General/View/TagItem.cs`
|
||||
- [ ] `UI/General/View/TowerIconArea.cs`
|
||||
- [ ] `UI/Menu/View/MenuForm.cs`
|
||||
- [ ] `UI/Menu/View/TestMenuForm.cs`
|
||||
- [ ] `UI/Shop/View/GoodsItem.cs`
|
||||
- [ ] `UI/Shop/View/ShopForm.cs`
|
||||
- [ ] `UI/Templates/GameScene/View/DisplayItem.cs`
|
||||
- [ ] `UI/Templates/GameScene/View/DisplayItemInfoForm.cs`
|
||||
- [ ] `UI/Templates/GameScene/View/DisplayItemObject.cs`
|
||||
- [ ] `UI/Templates/GameScene/View/DisplayListArea.cs`
|
||||
- [ ] `UI/Templates/GameScene/View/HudForm.cs`
|
||||
- [ ] `UI/Templates/GameScene/View/LevelUpForm.cs`
|
||||
- [ ] `UI/Templates/GameScene/View/LevelUpRewardItem.cs`
|
||||
- [ ] `UI/Templates/GameScene/View/ShopForm.cs`
|
||||
- [ ] `UI/Templates/MenuScene/View/RoleItem.cs`
|
||||
- [ ] `UI/Templates/MenuScene/View/RolePropertyArea.cs`
|
||||
- [ ] `UI/Templates/MenuScene/View/SelectRoleForm.cs`
|
||||
- [ ] `UI/Templates/MenuScene/View/SettingForm.cs`
|
||||
- [ ] `UI/Templates/MenuScene/View/StartMenuForm.cs`
|
||||
- [ ] `UI/HPBarItem.cs`
|
||||
- [ ] `UI/CommonButton.cs`
|
||||
- [ ] `UI/UpdateResourceForm.cs`
|
||||
|
||||
### UI/Controller(多个文件)
|
||||
|
||||
- [ ] `UI/Combat/Controller/CombatFinishFormController.cs`
|
||||
- [ ] `UI/Combat/Controller/CombatInfoFormController.cs`
|
||||
- [ ] `UI/Combat/Controller/CombatSelectFormController.cs`
|
||||
- [ ] `UI/Game/Controller/EventFormController.cs`
|
||||
- [ ] `UI/Game/Controller/MainFormController.cs`
|
||||
- [ ] `UI/Game/Controller/NodeMapFormController.cs`
|
||||
- [ ] `UI/Game/Controller/RepoFormController.cs`
|
||||
- [ ] `UI/General/Controller/DialogFormController.cs`
|
||||
- [ ] `UI/General/Controller/ItemDescFormController.cs`
|
||||
- [ ] `UI/General/Controller/RewardSelectFormController.cs`
|
||||
- [ ] `UI/Menu/Controller/MenuFormController.cs`
|
||||
- [ ] `UI/Menu/Controller/TestMenuFormController.cs`
|
||||
- [ ] `UI/Shop/Controller/ShopFormController.cs`
|
||||
- [ ] `UI/Templates/GameScene/Controller/DisplayItemInfoFormController.cs`
|
||||
- [ ] `UI/Templates/GameScene/Controller/HudFormController.cs`
|
||||
- [ ] `UI/Templates/GameScene/Controller/LevelUpFormController.cs`
|
||||
- [ ] `UI/Templates/GameScene/Controller/ShopFormController.cs`
|
||||
- [ ] `UI/Templates/MenuScene/Controller/SelectRoleFormController.cs`
|
||||
- [ ] `UI/Templates/MenuScene/Controller/StartMenuFormController.cs`
|
||||
|
||||
### Procedure(13 个文件)
|
||||
|
||||
- [ ] `Procedure/Base/ProcedureBase.cs`
|
||||
- [ ] `Procedure/Base/ProcedureChangeScene.cs`
|
||||
- [ ] `Procedure/Base/ProcedureCheckResources.cs`
|
||||
- [ ] `Procedure/Base/ProcedureCheckVersion.cs`
|
||||
- [ ] `Procedure/Base/ProcedureInitResources.cs`
|
||||
- [ ] `Procedure/Base/ProcedureLaunch.cs`
|
||||
- [ ] `Procedure/Base/ProcedurePreload.cs`
|
||||
- [ ] `Procedure/Base/ProcedureSplash.cs`
|
||||
- [ ] `Procedure/Base/ProcedureUpdateResources.cs`
|
||||
- [ ] `Procedure/Base/ProcedureUpdateVersion.cs`
|
||||
- [ ] `Procedure/Base/ProcedureVerifyResources.cs`
|
||||
- [ ] `Procedure/ProcedureMain/ProcedureMain.cs`
|
||||
- [ ] `Procedure/ProcedureMenu.cs`
|
||||
- [ ] `Procedure/ProcedureTest.cs`
|
||||
|
||||
### Scene(5 个文件)
|
||||
|
||||
- [ ] `Scene/HideByBoundary.cs`
|
||||
- [ ] `Scene/Map/House.cs`
|
||||
- [ ] `Scene/Map/MapDataRefs.cs`
|
||||
- [ ] `Scene/Map/Spawner.cs`
|
||||
|
||||
### PoolObjectBase(3 个文件)
|
||||
|
||||
- [ ] `PoolObjectBase/HPBarItemObject.cs`
|
||||
- [ ] `PoolObjectBase/RepoItemObject.cs`
|
||||
- [ ] `PoolObjectBase/TowerRepoItemObject.cs`
|
||||
|
||||
### Network
|
||||
|
||||
- [ ] `Network/NetworkChannelHelper.cs`
|
||||
|
||||
### Editor(3 个文件)
|
||||
|
||||
- [ ] `Editor/GameFrameworkConfigs.cs`
|
||||
- [ ] `Editor/GeometryTDBuildEventHandler.cs`
|
||||
- [ ] `Editor/SceneSwitchLeft.cs`
|
||||
|
||||
---
|
||||
|
||||
## 关键依赖链
|
||||
|
||||
```
|
||||
L0 迁移阻塞链:
|
||||
├── TagDefinitionRegistry (L1 重构) ──→ DR* (L1) ──→ DataTableExtension (L1)
|
||||
├── AttackPayload (L1 重构) ──→ TagEffectResolver (L1 重构)
|
||||
└── MapTopologyService (L1 重构) ──→ EntityData (L2)
|
||||
```
|
||||
|
||||
### 重构优先级
|
||||
|
||||
1. **第一波**:Definition/Enum + Event ✅ 已完成
|
||||
2. **第二波**:DataTable 整体移入 L1
|
||||
3. **第三波**:Vector3 → System.Numerics.Vector3 替换
|
||||
4. **第四波**:GameEntry 静态调用 → 接口注入
|
||||
5. **第五波**:Tilemap 接口抽象
|
||||
6. **第六波**:剩余 L1 文件迁移
|
||||
|
||||
---
|
||||
|
||||
## 第一波修改记录(2026-04-30)
|
||||
|
||||
### 进度统计
|
||||
|
||||
| 类别 | 源文件数 | 已迁移 | 完成率 |
|
||||
|------|----------|--------|--------|
|
||||
| Definition/Enum | 23 | 27* | 100%+ |
|
||||
| Event | 35 | 35 | 100% |
|
||||
| **小计** | **58** | **62** | **~34% of L0** |
|
||||
|
||||
> *注:Enum 源文件23个,第一波新增4个枚举从其他层迁移,故总计27个。L0总约180个文件。
|
||||
|
||||
### 新增文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `Definition/Enum/CombatSelectActionType.cs` | 从 `UI/Combat/Context/` 迁移 |
|
||||
| `Definition/Enum/RunNodeType.cs` | 从 `Procedure/ProcedureMain/RunModel.cs` 迁移枚举 |
|
||||
| `Definition/Enum/RunNodeStatus.cs` | 从 `Procedure/ProcedureMain/RunModel.cs` 迁移枚举 |
|
||||
| `Definition/Enum/RunNodeCompletionStatus.cs` | 从 `Procedure/ProcedureMain/RunModel.cs` 迁移枚举 |
|
||||
| `Definition/DataStruct/RunItemState.cs` | 新建,简化版 |
|
||||
| `Definition/DataStruct/RunNodeCompletionSnapshot.cs` | 新建,简化版(移除了对 Unity 类型的依赖)|
|
||||
|
||||
### 修改文件
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
|------|----------|
|
||||
| `Event/RepoForm/RepoItemClickedEventArgs.cs` | `UnityEngine.Vector2` → `System.Numerics.Vector2` |
|
||||
| `Event/Combat/CombatSelectItemClickEventArgs.cs` | `using GeometryTD.UI` → 迁移的枚举 |
|
||||
| `Event/Game/NodeCompleteEventArgs.cs` | 依赖迁移的枚举和数据结构 |
|
||||
| `Event/Game/NodeEnterEventArgs.cs` | `using GeometryTD.Procedure` → 迁移的枚举 |
|
||||
| `Event/Game/NodeMapNodeEnterRequestedEventArgs.cs` | `using GeometryTD.Procedure` → 迁移的枚举 |
|
||||
|
||||
### 命名空间统一
|
||||
|
||||
所有新增/修改的 Enum 文件使用 `GeometryTD.Definition` 命名空间(与原有文件保持一致)
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] L0 独立构建成功(`dotnet build GeometryTD.Domain`)
|
||||
- [ ] L1 引用 L0 无循环依赖
|
||||
- [ ] L2 引用 L1 无循环依赖
|
||||
- [ ] 三层整体构建成功(`dotnet build Geometry-Tower-Defense-Base.sln`)
|
||||
- [ ] 原有游戏流程(战斗/商店/组装)在 Unity 中正常运行
|
||||
|
|
@ -0,0 +1,736 @@
|
|||
# CombatNode 设计规范(开发约束)
|
||||
|
||||
最后更新:2026-03-12
|
||||
|
||||
## 1. 适用范围与目标
|
||||
|
||||
本文描述 `CombatNode` 域的后续开发标准。
|
||||
|
||||
说明:
|
||||
- 本文是“目标架构约束”,不要求当前代码已经完全达成。
|
||||
- 后续新增功能、重构、拆分类、review 职责边界时,以本文为准。
|
||||
- 如果当前实现与本文不一致,新增代码优先向本文收敛,而不是继续扩大旧结构。
|
||||
|
||||
核心目标:
|
||||
- `CombatScheduler` 收敛为“状态机管理器”,不再继续堆积加载、结算、奖励选择等业务细节。
|
||||
- 战斗内资源收口到独立资源服务,由内部管理,不再由 `CombatNodeComponent` 直接持有真值。
|
||||
- `MapEntity` 通过 `MapData + Event` 获取战斗上下文,不反查 `CombatNode` 域内部状态。
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构总览
|
||||
|
||||
### 2.1 CombatNodeComponent(入口 Facade)
|
||||
|
||||
文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs`
|
||||
|
||||
长期职责:
|
||||
- 读取并缓存 `DRLevel / DRLevelPhase / DRLevelSpawnEntry`。
|
||||
- 按主题筛选关卡。
|
||||
- 启动/停止 `CombatScheduler`。
|
||||
- 对外暴露只读运行时属性。
|
||||
- 提供少量用户入口,例如 `StartCombat`、`TryEndCombatByPlayer`。
|
||||
|
||||
长期不负责:
|
||||
- 不直接持有 `Coin / Gold / BaseHp / Loot Backpack` 的真值。
|
||||
- 不直接缓存本局建塔属性快照。
|
||||
- 不直接发布战斗流程事件。
|
||||
- 不直接处理敌人掉落、结算、奖励选择、地图加载。
|
||||
|
||||
### 2.2 CombatScheduler(状态机管理器)
|
||||
|
||||
文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs`
|
||||
|
||||
长期职责:
|
||||
- 持有共享运行时数据与共享服务实例。
|
||||
- 管理状态实例。
|
||||
- 提供统一的 `ChangeState(...)` 状态迁移入口。
|
||||
- 提供敌人事件的公共处理入口。
|
||||
- 作为状态机生命周期边界,统一做运行时重置。
|
||||
|
||||
长期不负责:
|
||||
- 不直接硬编码加载流程。
|
||||
- 不直接硬编码结算流程。
|
||||
- 不直接硬编码奖励选择 UI 逻辑。
|
||||
- 不直接硬编码 `PhaseEndType` 结束条件。
|
||||
|
||||
推荐状态类命名:
|
||||
- `CombatLoadingState`
|
||||
- `CombatRunningPhaseState`
|
||||
- `CombatWaitingForPhaseEndState`
|
||||
- `CombatSettlementState`
|
||||
- `CombatRewardSelectionState`
|
||||
- `CombatFinishFormState`
|
||||
- `CombatWaitingForReturnState`
|
||||
- `CombatFailedState`
|
||||
|
||||
实现约束:
|
||||
- 上述状态类可以作为 `CombatScheduler` 的嵌套类实现,也可以拆成独立文件;但必须只服务于 `CombatScheduler` 状态机,不形成独立业务边界。
|
||||
- 共享数据与共享服务统一收口到 `CombatScheduler` 内部持有的运行时承载体,不允许散落在各状态类中。
|
||||
- 若 `CombatScheduler` 体量过大,允许在其内部实现中继续拆出:
|
||||
- `CombatSchedulerRuntime`:承载共享运行时字段与共享服务引用
|
||||
- `CombatSchedulerCoordinator`:承载多个状态共用的流程辅助方法
|
||||
- 上述拆分只属于 `CombatScheduler` 的内部实现细化,不改变 `CombatScheduler` 作为唯一状态机边界的职责。
|
||||
- 所有状态切换只能通过 `CombatScheduler.ChangeState(...)` 完成。
|
||||
- 状态类不能彼此直接操控。
|
||||
|
||||
### 2.3 EnemyManager(敌人域 Facade)
|
||||
|
||||
文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs`
|
||||
|
||||
长期职责:
|
||||
- 对状态机提供统一敌人域接口。
|
||||
- 编排敌人子服务。
|
||||
- 暴露只读事实:
|
||||
- `AliveEnemyCount`
|
||||
- `IsPhaseSpawnCompleted`
|
||||
- `HasAliveBoss`
|
||||
- 在敌人死亡或到家时,通过公共入口向 `CombatScheduler` 上报:
|
||||
- `OnEnemyDefeated(DREnemy enemy)`
|
||||
- `OnEnemyReachedBase(DREnemy enemy)`
|
||||
|
||||
长期不负责:
|
||||
- 不直接给资源入账。
|
||||
- 不直接扣基地血量。
|
||||
- 不直接决定状态切换。
|
||||
|
||||
---
|
||||
|
||||
## 3. 状态机模型
|
||||
|
||||
### 3.1 状态列表(目标)
|
||||
|
||||
- `Loading`
|
||||
- `RunningPhase`
|
||||
- `WaitingForPhaseEnd`
|
||||
- `Settlement`
|
||||
- `RewardSelection`
|
||||
- `FinishForm`
|
||||
- `WaitingForReturn`
|
||||
- `Failed`
|
||||
|
||||
说明:
|
||||
- 正常结束流只有一条状态链:
|
||||
- `Settlement -> RewardSelection(可选) -> FinishForm -> WaitingForReturn`
|
||||
- 正常通关、玩家主动结束、基地血量归零都走同一条结束链。
|
||||
- `Failed` 仅用于异常失败,不用于“基地被击破”这类正常战斗失败。
|
||||
|
||||
### 3.2 CombatLoadingState
|
||||
|
||||
职责:
|
||||
- 通过 `CombatLoadSession` 执行地图与基础战斗 UI 加载。
|
||||
- 从局内资源管理器读取本局快照。
|
||||
- 组装 `MapData` 并发起 `ShowEntity(MapEntity)`。
|
||||
|
||||
约束:
|
||||
- 只负责加载,不负责初始化局内资源。
|
||||
- 局内资源必须在进入状态机前初始化完成。
|
||||
|
||||
### 3.3 CombatRunningPhaseState
|
||||
|
||||
职责:
|
||||
- 执行当前 `DRLevelPhase` 的行为。
|
||||
- 推进 `SpawnEntry` 时序与出怪。
|
||||
- 管理 `EnemySpawnDirector` 的阶段级初始化与重置。
|
||||
- 在新 phase 开始时发布:
|
||||
- `CombatProcessEventArgs`
|
||||
- `CombatEnemyHpRateChangedEventArgs`
|
||||
|
||||
退出条件:
|
||||
- 当前 phase 的所有 `SpawnEntry` 已执行完毕时,进入 `WaitingForPhaseEnd`。
|
||||
- 若共享“结束战斗请求标记”已置位,也可结束当前运行态并转入正常结束链。
|
||||
|
||||
不负责:
|
||||
- 不根据 `PhaseEndType` 判断 phase 是否真正结束。
|
||||
- 不直接根据 `BaseHp` 或敌人死亡事件切状态。
|
||||
|
||||
### 3.4 CombatWaitingForPhaseEndState
|
||||
|
||||
职责:
|
||||
- 不再生成新敌人。
|
||||
- 根据 `PhaseEndType` 判断当前 phase 是否结束。
|
||||
|
||||
约束:
|
||||
- `PhaseEndType` 的判断由独立判定服务负责,不在状态内硬编码。
|
||||
- 每种 `PhaseEndType` 对应一个实现类。
|
||||
- 该判定服务为本状态专用,不作为全局共享服务常驻在 `CombatScheduler` 上。
|
||||
|
||||
### 3.5 CombatSettlementState
|
||||
|
||||
职责:
|
||||
- 进入时统一构造结算上下文。
|
||||
- 根据共享资源状态完成结算修正。
|
||||
- 决定后续进入 `RewardSelection` 还是 `FinishForm`。
|
||||
|
||||
负责的逻辑包括:
|
||||
- 基地血量奖励/惩罚。
|
||||
- 满血奖励选择的准入判断。
|
||||
- 生成最终展示摘要。
|
||||
- 准备待合并的结算背包快照。
|
||||
|
||||
约束:
|
||||
- 不依赖单独的 `CombatEndReason` 字段。
|
||||
- `BaseHp <= 0` 表示基地被击破。
|
||||
- 正常通关与玩家主动结束在结算产出上不区分原因。
|
||||
|
||||
### 3.6 CombatRewardSelectionState
|
||||
|
||||
职责:
|
||||
- 绑定、配置、打开、关闭 `RewardSelectForm`。
|
||||
- 处理奖励选择过程。
|
||||
- 将奖励选择结果写入“结束状态链持有的结算上下文”。
|
||||
|
||||
约束:
|
||||
- 不重新判断“是否应该出现奖励选择”。
|
||||
- 只处理选择过程本身。
|
||||
|
||||
### 3.7 CombatFinishFormState
|
||||
|
||||
职责:
|
||||
- 绑定、配置、打开、关闭 `CombatFinishForm`。
|
||||
- 读取结算上下文并展示最终结算结果。
|
||||
|
||||
### 3.8 CombatWaitingForReturnState
|
||||
|
||||
职责:
|
||||
- 等待玩家从结算返回。
|
||||
- 完成地图与战斗基础 UI 清理。
|
||||
- 完成正常退出收尾。
|
||||
- 在整场战斗真正退出时发布 `NodeCompleteEventArgs`。
|
||||
|
||||
### 3.9 CombatFailedState
|
||||
|
||||
职责:
|
||||
- 表示异常失败。
|
||||
- 保存并展示错误信息。
|
||||
- 执行异常收尾与剩余资源回收。
|
||||
|
||||
约束:
|
||||
- `Failed` 只处理异常路径。
|
||||
- “基地血量为 0”不进入 `Failed`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 共享服务与推荐命名
|
||||
|
||||
### 4.1 命名后缀词典
|
||||
|
||||
- `Scheduler`:只用于状态机边界或阶段推进总控,例如 `CombatScheduler`。
|
||||
- `Manager`:只用于子域 Facade/聚合入口,例如 `EnemyManager`。
|
||||
- `Coordinator`:只用于跨状态、跨服务的流程编排,不持有独立业务真值。
|
||||
- `Service`:只用于聚焦业务行为,不承担框架事件桥接或异步句柄跟踪。
|
||||
- `Calculator`:只用于纯计算与结果组装,不直接提交状态或驱动 UI。
|
||||
- `Session`:只用于一次加载/交互过程的生命周期对象。
|
||||
- `Bridge`:只用于框架边界适配器。
|
||||
- `Runtime`:只用于运行时可变状态承载。
|
||||
- `Context`:只用于被动数据包或共享上下文。
|
||||
- `Result`:只用于动作输出或结算产出;若外围服务名已表达动作语义,不再重复动作前缀。
|
||||
- `Flags`:只用于聚合布尔控制项,不承载流程编排逻辑。
|
||||
- `Resolver`:只用于映射、查找、判定、解析职责。
|
||||
- `Tracker`:只用于跟踪运行中的实体或事实真值。
|
||||
- `Port`:只用于向内部状态或 use case 暴露的受限宿主接口。
|
||||
|
||||
### 4.2 CombatLoadSession
|
||||
|
||||
文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs`
|
||||
|
||||
长期定位:
|
||||
- 长期保留的独立加载服务。
|
||||
- 专门负责地图与战斗内基础 UI 的加载/清理。
|
||||
|
||||
职责:
|
||||
- 加载地图实体。
|
||||
- 打开/关闭 `CombatInfoForm`。
|
||||
- 跟踪加载成功/失败状态。
|
||||
- 对外提供 `CurrentMap` 与 `IsReady`。
|
||||
|
||||
### 4.2.x CombatSchedulerRuntime / CombatSchedulerCoordinator(实现细化)
|
||||
|
||||
当前实现允许:
|
||||
- 用 `CombatSchedulerRuntime` 承载所有状态共享的运行时字段与共享服务引用。
|
||||
- 用 `CombatSchedulerCoordinator` 承载多个状态共用的流程辅助逻辑。
|
||||
|
||||
约束:
|
||||
- 两者都必须由 `CombatScheduler` 持有并统一管理生命周期。
|
||||
- 两者都不替代 `CombatScheduler` 对外暴露状态机边界。
|
||||
- `Runtime` 不负责状态切换。
|
||||
- `Coordinator` 不持有独立业务真值,只能围绕共享运行时做编排辅助。
|
||||
- 状态类只允许通过 `Runtime + Coordinator` 访问共享状态与共享流程,不应再直接耦合其他状态实现细节。
|
||||
|
||||
### 4.3 PhaseLoopRuntime
|
||||
|
||||
文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/PhaseLoopRuntime.cs`
|
||||
|
||||
长期定位:
|
||||
- 长期保留的独立 phase runtime 服务。
|
||||
|
||||
职责:
|
||||
- 维护当前 `DRLevelPhase`。
|
||||
- 维护 `DisplayPhaseIndex`、`PhaseCount`。
|
||||
- 维护统一的 phase 时间基准,例如 `phaseElapsedTime` 或 `phaseStartTime`。
|
||||
- 负责进入下一 phase。
|
||||
- 持有统一“请求结束战斗”标记。
|
||||
|
||||
约束:
|
||||
- 只做 phase 运行时数据管理,不直接切状态。
|
||||
|
||||
### 4.4 CombatRunResourceStore
|
||||
|
||||
文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatRunResourceStore.cs`
|
||||
|
||||
目标职责:
|
||||
- 持有本局 `Coin` 真值。
|
||||
- 持有本局累计 `Gold` 真值。
|
||||
- 持有本局 `BaseHp` 真值。
|
||||
- 持有本局战利品背包。
|
||||
- 持有本局建塔属性快照。
|
||||
- 提供只读快照给结束状态链与加载状态使用。
|
||||
- 发布资源变化事件:
|
||||
- `CombatCoinChangedEventArgs`
|
||||
- `CombatBaseHpChangedEventArgs`
|
||||
|
||||
初始化约束:
|
||||
- 在进入状态机前完成初始化。
|
||||
- 由内部从 `PlayerInventory` 获取并缓存本局建塔快照。
|
||||
|
||||
事件约束:
|
||||
- `Coin / BaseHp` 变化事件同时携带“当前值”和“变化量”。
|
||||
- `Gold` 只是结算累计值,不要求战斗内实时事件驱动。
|
||||
|
||||
### 4.5 InventoryGenerationComponent
|
||||
|
||||
文件:`Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs`
|
||||
|
||||
目标职责:
|
||||
- 作为局外组件产出的统一运行时入口。
|
||||
- 对外提供:
|
||||
- `BuildShopGoods(...)`
|
||||
- `ResolveEnemyDrop(...)`
|
||||
- `BuildRewardCandidates(...)`
|
||||
- 在内部编排:
|
||||
- `DropPoolRoller`
|
||||
- `RewardCandidateBuilder`
|
||||
- `OutGameDropRuleService`
|
||||
- `OutGameDropItemBuilder`
|
||||
- `InventoryGenerationRandomContext`
|
||||
|
||||
约束:
|
||||
- `CombatNode` 域不直接持有或复制组件产出规则。
|
||||
- `CombatScheduler` 与结算状态链只调用统一入口,不直接访问掉落池滚动或组件实例构造细节。
|
||||
- `InventoryGenerationComponent` 负责运行时入口、稳定临时实例 Id、Tag 随机上下文以及 `runSeed/sequenceIndex` 相关上下文。
|
||||
- 掉落是否产出组件由 `OutGameDropRuleService` 决定;掉落池行到组件实例的构造由 `OutGameDropItemBuilder` 决定。
|
||||
|
||||
### 4.5.x InventoryGenerationRandomContext
|
||||
|
||||
文件:`Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationRandomContext.cs`
|
||||
|
||||
目标职责:
|
||||
- 统一承载组件产出链路的随机合同。
|
||||
- 统一派生:
|
||||
- 商店 / 掉落 / 奖励候选的稳定随机流
|
||||
- 稳定临时组件 `InstanceId`
|
||||
- `InventoryTagRandomContext`
|
||||
|
||||
约束:
|
||||
- `ShopGoodsBuilder`、`DropPoolRoller`、`RewardCandidateBuilder` 不再直接使用全局 `UnityEngine.Random`。
|
||||
- 同一 `runSeed + sequenceIndex + sourceType + localOrdinal` 下,应得到一致的物品本体与 Tag 结果。
|
||||
|
||||
### 4.6 CombatSettlementCalculator
|
||||
|
||||
文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatSettlementCalculator.cs`
|
||||
|
||||
目标职责:
|
||||
- 只负责结算计算与 `CombatSettlementContext` 组装。
|
||||
- 负责基地血量奖励、奖励选择准入、奖励背包快照与耐久扣减目标计算。
|
||||
|
||||
约束:
|
||||
- 不直接并包到玩家库存。
|
||||
- 不直接打开 UI。
|
||||
- 不承担奖励候选生成。
|
||||
|
||||
### 4.7 CombatSettlementCommitter
|
||||
|
||||
文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatSettlementCommitter.cs`
|
||||
|
||||
目标职责:
|
||||
- 只负责把结算结果提交到玩家库存。
|
||||
- 负责结算背包并包与延迟耐久扣减落地。
|
||||
|
||||
约束:
|
||||
- 不重新计算结算上下文。
|
||||
- 不直接生成奖励候选或打开 UI。
|
||||
|
||||
### 4.8 IPhaseEndCondition
|
||||
|
||||
目标职责:
|
||||
- 作为 `PhaseEndType` 判定接口。
|
||||
- 每种 `PhaseEndType` 对应一个实现类。
|
||||
|
||||
只读输入:
|
||||
- 当前 `DRLevelPhase`
|
||||
- phase 时间信息
|
||||
- `AliveEnemyCount`
|
||||
- `IsPhaseSpawnCompleted`
|
||||
- `HasAliveBoss`
|
||||
|
||||
输出:
|
||||
- `bool ShouldExit`
|
||||
|
||||
约束:
|
||||
- 不直接切状态。
|
||||
- 不直接发事件。
|
||||
- 不直接改资源。
|
||||
|
||||
---
|
||||
|
||||
## 5. EnemyManager 子服务边界
|
||||
|
||||
### 5.1 EnemySpawnDirector
|
||||
|
||||
文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemySpawnDirector.cs`
|
||||
|
||||
职责:
|
||||
- 长期保留为独立服务。
|
||||
- 基于 `spawnEntries + phase time` 计算当前应执行的刷怪行为。
|
||||
- 提供“当前 phase 的 `SpawnEntry` 是否已全部执行完”的事实。
|
||||
|
||||
生命周期:
|
||||
- 由 `CombatRunningPhaseState` 在状态进入/退出时初始化与重置。
|
||||
|
||||
### 5.2 EnemySpawnPathResolver
|
||||
|
||||
文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemySpawnPathResolver.cs`
|
||||
|
||||
职责:
|
||||
- 缓存当前地图可用 `Spawner`。
|
||||
- 提供出生点与路径解析。
|
||||
|
||||
### 5.3 EnemyLifecycleTracker
|
||||
|
||||
文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyLifecycleTracker.cs`
|
||||
|
||||
职责:
|
||||
- 维护 `AliveEnemyCount` 真值。
|
||||
- 维护 `HasAliveBoss` 真值。
|
||||
- 追踪本局 tracked 敌人。
|
||||
- 导出 tracked ids 供清场使用。
|
||||
|
||||
Boss 识别规则:
|
||||
- Boss 身份由 `DRLevelSpawnEntry.EntryType == Boss` 决定。
|
||||
- 不由 `DREnemy` 自身类型决定。
|
||||
|
||||
### 5.4 EnemyConfigProvider
|
||||
|
||||
文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyConfigProvider.cs`
|
||||
|
||||
职责:
|
||||
- 读取 `DREnemy`。
|
||||
- 处理默认配置兜底。
|
||||
- 计算循环周目下的基础血量倍率。
|
||||
|
||||
---
|
||||
|
||||
## 6. 事件与数据流规范
|
||||
|
||||
### 6.1 MapEntity 与 Combat 域解耦
|
||||
|
||||
必须保持:
|
||||
- `MapEntity` 不直接查询 `CombatNodeComponent` 的运行时资源字段。
|
||||
- 战斗初始上下文通过 `MapData` 注入。
|
||||
- `Coin` 初值通过 `MapData` 传入。
|
||||
- 后续 `Coin` 变化通过 `CombatCoinChangedEventArgs` 同步。
|
||||
- `TowerStatsData` 等本局不变量直接放进 `MapData`。
|
||||
- `MapEntity` 不反查 Combat 域内部服务。
|
||||
|
||||
`MapData` 组装规则:
|
||||
- 由 `CombatLoadingState` 从局内资源管理器读取快照。
|
||||
- 由 `CombatLoadingState` 打包成 `MapData` 后再 `ShowEntity(MapEntity)`。
|
||||
|
||||
### 6.2 敌人事件处理
|
||||
|
||||
统一边界:
|
||||
- `EnemyManager` 只上报:
|
||||
- `OnEnemyDefeated(DREnemy enemy)`
|
||||
- `OnEnemyReachedBase(DREnemy enemy)`
|
||||
- `CombatScheduler` 公共层负责处理敌人事件的通用副作用:
|
||||
- 击杀:调用 `GameEntry.InventoryGeneration.ResolveEnemyDrop(...)`,再调用局内资源管理器入账。
|
||||
- 到家:调用局内资源管理器扣减 `BaseHp`。
|
||||
|
||||
约束:
|
||||
- 敌人事件入口不直接调用 `ChangeState(...)`。
|
||||
- `BaseHp <= 0` 的判断由当前状态在 `OnUpdate` 中处理。
|
||||
|
||||
### 6.3 战斗流程事件
|
||||
|
||||
发布边界:
|
||||
- 资源变化事件由局内资源管理器发布。
|
||||
- 流程/阶段事件由状态机或具体状态发布。
|
||||
|
||||
发布时间:
|
||||
- `NodeEnterEventArgs`:`Loading` 完成并正式进入首个 `RunningPhase` 时。
|
||||
- `CombatProcessEventArgs`:新 phase 的 `RunningPhase.OnEnter`。
|
||||
- `CombatEnemyHpRateChangedEventArgs`:与 `CombatProcessEventArgs` 同时发布。
|
||||
- `NodeCompleteEventArgs`:`WaitingForReturn` 完成清理、整场战斗真正退出时。
|
||||
|
||||
---
|
||||
|
||||
## 7. 结束链与结算上下文
|
||||
|
||||
### 7.1 统一结束链
|
||||
|
||||
正常结束统一走:
|
||||
- `Settlement`
|
||||
- `RewardSelection`(可选)
|
||||
- `FinishForm`
|
||||
- `WaitingForReturn`
|
||||
|
||||
### 7.2 结算上下文
|
||||
|
||||
归属:
|
||||
- 作为 `CombatScheduler` 上的共享字段存在。
|
||||
- `Settlement` 在 `OnEnter` 时统一构造。
|
||||
- `RewardSelection` 只追加奖励结果。
|
||||
- `FinishForm` 与 `WaitingForReturn` 只读取。
|
||||
|
||||
最小字段集合:
|
||||
- 最终结算的 `Gold/Coin` 结果
|
||||
- 待合并的背包快照
|
||||
- `BaseHp` 结算结果
|
||||
- 是否进入过奖励选择
|
||||
- `FinishForm` 所需摘要数据
|
||||
|
||||
命名约束:
|
||||
- 结算上下文中的布尔控制项统一收口到 `Flags`,不再使用 `Flow` 命名。
|
||||
|
||||
奖励选择约束:
|
||||
- 满血奖励选择结果只写入结算上下文。
|
||||
- 不直接写入局内资源管理器。
|
||||
- 最终由结束状态链统一合并到玩家背包。
|
||||
|
||||
---
|
||||
|
||||
## 8. 核心不变量(必须保持)
|
||||
|
||||
1. `CombatScheduler` 只做状态机管理与共享运行时收口,不继续吸收具体业务细节。
|
||||
2. `CombatNodeComponent` 不再持有战斗内资源真值。
|
||||
3. 局内 `Coin / Gold / BaseHp / Loot Backpack / BuildTowerSnapshots` 以 `CombatRunResourceStore` 为唯一真值来源。
|
||||
4. 组件产出规则以 `InventoryGenerationComponent` 为统一运行时入口;战斗掉落与奖励候选都通过它生成。
|
||||
5. 存活敌人数与 `HasAliveBoss` 以 `EnemyLifecycleTracker` 为唯一真值来源。
|
||||
6. Phase 运行时信息与统一结束标记以 `PhaseLoopRuntime` 为唯一真值来源。
|
||||
7. `PhaseEndType` 的退出条件以 `IPhaseEndCondition` 实现类为唯一判定入口。
|
||||
8. 状态切换只能通过 `CombatScheduler.ChangeState(...)` 完成。
|
||||
9. 敌人事件处理入口不直接切状态,状态只能在自己的 `OnUpdate` 中决定迁移。
|
||||
10. `MapEntity` 通过 `MapData + Event` 获取战斗上下文,不反查 Combat 域内部运行时。
|
||||
|
||||
---
|
||||
|
||||
## 9. 清理职责
|
||||
|
||||
- 敌人清理:`EnemyManager`,且只清理本局 tracked 敌人。
|
||||
- 地图与战斗基础 UI 清理:`CombatLoadSession`。
|
||||
- 结算/奖励 UI 清理:结束状态链或 `Failed` 状态。
|
||||
- 运行时数据重置:`CombatScheduler` 在状态机生命周期边界统一执行。
|
||||
|
||||
---
|
||||
|
||||
## 10. 扩展开发规范
|
||||
|
||||
### 10.1 新增刷怪类型或 SpawnEntry 行为
|
||||
|
||||
优先改 `EnemySpawnDirector`,不要把时序细节塞进 `CombatRunningPhaseState`。
|
||||
|
||||
### 10.2 新增 Phase 结束条件
|
||||
|
||||
新增 `IPhaseEndCondition` 实现类,不要在 `CombatWaitingForPhaseEndState` 里写大分支。
|
||||
|
||||
### 10.3 新增敌人掉落规则
|
||||
|
||||
优先改 `InventoryGenerationComponent` 及其下层规则模块,不要在 `EnemyManager`、`CombatScheduler` 或状态类里直接计算掉落。
|
||||
|
||||
### 10.3.x 新增奖励候选规则
|
||||
|
||||
优先改 `InventoryGenerationComponent`、`RewardCandidateBuilder` 或 `DropPoolRoller`,不要在结算状态链里复制一套候选生成规则。
|
||||
|
||||
### 10.4 新增战斗内资源或建塔快照规则
|
||||
|
||||
优先改 `CombatRunResourceStore`,不要回流到 `CombatNodeComponent`。
|
||||
|
||||
### 10.5 新增地图/战斗基础 UI 加载规则
|
||||
|
||||
优先改 `CombatLoadSession` 或 `CombatLoadingState`,不要把加载细节塞回 `CombatScheduler` 本体。
|
||||
|
||||
### 10.6 新增强化结算、奖励选择、结算 UI 逻辑
|
||||
|
||||
优先改结束状态链:
|
||||
- `CombatSettlementState`
|
||||
- `CombatRewardSelectionState`
|
||||
- `CombatFinishFormState`
|
||||
- `CombatWaitingForReturnState`
|
||||
|
||||
### 10.7 新增战斗流程事件
|
||||
|
||||
优先由具体状态或局内资源管理器发布,不要回流到 `CombatNodeComponent`。
|
||||
|
||||
---
|
||||
|
||||
## 11. 代码变更检查清单(PR 自检)
|
||||
|
||||
1. 新逻辑是否落在正确的状态或服务,而不是继续堆进 `CombatScheduler` 本体?
|
||||
2. `CombatNodeComponent` 是否仍然保持为轻量入口 Facade?
|
||||
3. 是否破坏了局内资源、掉落判定、phase runtime、phase end 判定的唯一真值来源?
|
||||
4. 敌人事件处理是否仍然只做公共副作用,而不直接切状态?
|
||||
5. 状态迁移是否仍然统一走 `ChangeState(...)`?
|
||||
6. `MapEntity` 是否仍然只通过 `MapData + Event` 获取战斗上下文?
|
||||
7. 清理是否仍按”敌人 / 地图基础 UI / 结算 UI / 运行时数据”分工执行?
|
||||
|
||||
---
|
||||
|
||||
## 12. Combat Economy(战斗经济)
|
||||
|
||||
> **Status**: 新增段落 — GDD 一致性审查中发现 Coin 经济在实现中存在但未写入文档
|
||||
> **最后更新**: 2026-04-30
|
||||
|
||||
本段说明战斗域内双货币体系的完整设计依据。所有数值均来自数据表驱动,无需代码硬编码。
|
||||
|
||||
---
|
||||
|
||||
### 12.1 双货币架构概述
|
||||
|
||||
游戏使用两层货币,分属不同生命周期:
|
||||
|
||||
| 货币 | 生命周期 | 存储位置 | 描述 |
|
||||
|------|----------|----------|------|
|
||||
| **Coin** | 单次战斗内 | `CombatRunResourceStore.CurrentCoin` | 战斗内部经济,用于建塔/升级/拆除 |
|
||||
| **Gold** | 整局运行 | `PlayerInventoryComponent.Gold` | 跨节点持久,用于商店购买、出售 |
|
||||
|
||||
**设计意图**: Coin 创造战斗内的即时决策张力(”我现在花 Coin 建塔还是留着防万一?”);Gold 创造跨节点的战略张力(”我是现在买还是等下一个商店?”)。两层经济分离,防止任一层的决策深度被稀释。
|
||||
|
||||
**关键约束**: Coin 不跨战斗持久,战斗结束时清零。战斗胜利后 `GainedGold` 入账,`GainedCoin` 不入账。
|
||||
|
||||
---
|
||||
|
||||
### 12.2 Coin(战斗内部货币)
|
||||
|
||||
#### 12.2.1 来源
|
||||
|
||||
| 来源 | 字段 | 说明 |
|
||||
|------|------|------|
|
||||
| 关卡初始 Coin | `DRLevel.StartCoin` | 战斗开始时发放,来源于 `CombatRunResourceStore.InitializeForCombat()` |
|
||||
| 敌人击杀奖励 | `DREnemy.DropCoin` | 每次击杀敌人时发放,来源于 `CombatRunResourceStore.AddEnemyDefeatedReward()` |
|
||||
|
||||
`StartCoin` 按关卡难度配置,确保玩家在战斗开始时有足够的决策空间。建议低难度关卡 `StartCoin` 较低,高难度/Boss 关卡较高。
|
||||
|
||||
`DropCoin` 按敌人类型配置。建议普通敌人 `DropCoin` 较低(5–15),精英敌人较高(20–50),与 `DropGold` 分开计算。
|
||||
|
||||
#### 12.2.2 消耗(Sinks)
|
||||
|
||||
| 操作 | 消耗方式 | 触发时机 |
|
||||
|------|----------|----------|
|
||||
| **BuildTower** | `TryConsumeCoin(buildTowerCost[i])` | 玩家在战斗内点击建塔位,每槽位独立计费 |
|
||||
| **UpgradeTower** | `TryConsumeCoin(upgradeCost)` | 玩家升级已有塔 |
|
||||
| **DestroyTower** | 无消耗;返回 `destroyGain` Coin | 玩家拆除已有塔,Coin 返还 |
|
||||
|
||||
建塔槽位共 4 个(对应 `BuildOptionCount = 4`),每个槽位可独立消耗 Coin。每槽位的 `buildTowerCost[i]` 由关卡或战斗阶段配置。
|
||||
|
||||
#### 12.2.3 追踪
|
||||
|
||||
`CombatRunResourceStore.GainedCoin` 记录本场战斗累计获得的 Coin。战斗结束时 `GainedCoin` 计入 `RunStats.coinsEarned`(跨战斗累计),但 Coin 本身不清零重置于下一个战斗,而是**每个新战斗重新从 `DRLevel.StartCoin` 开始**。
|
||||
|
||||
```
|
||||
// 每场新战斗开始时
|
||||
CurrentCoin = DRLevel.StartCoin // 从关卡配置重置,非累加
|
||||
GainedCoin = 0 // 重置,用于本场统计
|
||||
```
|
||||
|
||||
#### 12.2.4 数据驱动约束
|
||||
|
||||
| 约束 | 值 | 说明 |
|
||||
|------|---|------|
|
||||
| `DRLevel.StartCoin` | ≥ 0 | 建议普通关卡 50–200,Boss 关卡 100–300 |
|
||||
| `DREnemy.DropCoin` | ≥ 0 | 建议普通敌人 5–15,精英 20–50,Boss 0(避免重复计费) |
|
||||
| `buildTowerCost[i]` | ≥ 0 | 由 `CombatSelectFormUseCase` 从关卡配置读取,建议 20–80 每槽位 |
|
||||
| `upgradeCost` | ≥ 0 | 由 `CombatSelectFormUseCase` 从关卡配置读取,建议 50–150 |
|
||||
| `destroyGain` | ≥ 0 | 建议 = `buildTowerCost * 0.5`(返还 50%),与商店售价机制一致 |
|
||||
|
||||
---
|
||||
|
||||
### 12.3 Gold(战斗内获取部分)
|
||||
|
||||
战斗过程中 Gold 获取有两个来源:
|
||||
|
||||
#### 12.3.1 敌人击杀掉落
|
||||
|
||||
```
|
||||
// DREnemy 字段
|
||||
DropGold // 掉落金币数额
|
||||
DropPercent // 掉落概率 [0.0, 1.0]
|
||||
```
|
||||
|
||||
战斗胜利时(敌人被击杀或波次结束),若随机值 `≤ DropPercent`,则 `AddEnemyDefeatedReward(gainedCoin, gainedGold)` 被调用,`gainedGold = DropGold`。
|
||||
|
||||
#### 12.3.2 关卡胜利奖励
|
||||
|
||||
战斗胜利结算时(`CombatSettlementState`),`CombatSettlementCalculator` 计算本场战斗总 Gold 奖励:`DRLevel.RewardGold`,通过 `CombatRunResourceStore.AddSettlementGold()` 入账。
|
||||
|
||||
战斗失败时:无 `RewardGold`,`GainedGold = 0`。
|
||||
|
||||
#### 12.3.3 追踪
|
||||
|
||||
`CombatRunResourceStore.GainedGold` 记录本场战斗累计获得的 Gold(击杀掉落 + 胜利奖励)。战斗结束时通过 `GetRewardInventorySnapshot()` 合并至玩家主背包(`PlayerInventoryComponent.MergeInventory()`),触发 `MaxPlayerGold = 9999` 上限检查(溢出部分丢弃)。
|
||||
|
||||
---
|
||||
|
||||
### 12.4 Coin 与 Gold 的运行时边界
|
||||
|
||||
```
|
||||
战斗开始 → InitializeForCombat(level)
|
||||
└→ CurrentCoin = level.StartCoin // Coin 重置
|
||||
└→ GainedCoin = 0, GainedGold = 0 // 本场统计重置
|
||||
|
||||
战斗过程中(敌人死亡)→ AddEnemyDefeatedReward(dropCoin, dropGold)
|
||||
└→ CurrentCoin += dropCoin
|
||||
└→ CurrentGold (奖励库存) += dropGold // 不影响主背包
|
||||
└→ GainedCoin += dropCoin
|
||||
└→ GainedGold += dropGold
|
||||
|
||||
战斗胜利 → AddSettlementGold(RewardGold)
|
||||
└→ GainedGold += RewardGold
|
||||
|
||||
战斗结束 → GetRewardInventorySnapshot() → MergeInventory()
|
||||
└→ 主背包 Gold += min(rewardGold, MaxPlayerGold - currentGold)
|
||||
└→ 主背包组件 += 奖励组件
|
||||
└→ RunStats.coinsEarned += GainedCoin // 仅统计,不持久化 Coin
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 12.5 Progression 的 coinEarned 字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `RunStats.coinsEarned` | int | **跨战斗累计**:本 run 所有战斗累计获得的 Coin 总和(战斗失败也计入) |
|
||||
|
||||
`coinsEarned` 用于统计目的,不产生任何游戏内效果。它记录玩家在一局 run 中总共获得了多少 Coin — 可用于未来功能(如”累计获得 10000 Coin”成就),但当前无对应解锁或奖励。
|
||||
|
||||
---
|
||||
|
||||
### 12.6 与商店经济的隔离
|
||||
|
||||
Coin 仅在战斗域内流通。商店系统(`design/gdd/shop.md`)处理的 buy/sell 交易仅涉及 Gold,不涉及 Coin。
|
||||
|
||||
- 战斗内**不会**触发商店交易
|
||||
- 商店内**不会**消耗或获得 Coin
|
||||
- 战斗结束时,Coin 不转换为 Gold(`GainedCoin` 仅计入 `RunStats`,不进入玩家背包)
|
||||
|
||||
---
|
||||
|
||||
### 12.7 调试与一致性检查
|
||||
|
||||
| 检查项 | 预期结果 |
|
||||
|--------|----------|
|
||||
| 新战斗开始时 `CurrentCoin == DRLevel.StartCoin` | 相等 |
|
||||
| 战斗结束时 `GainedCoin ≥ 0` | 大于等于零 |
|
||||
| 战斗失败时 `GainedGold == 0` | 战斗失败不发放 Gold 奖励 |
|
||||
| `MaxPlayerGold` 上限检查 | `MergeInventory` 后背包 Gold ≤ 9999 |
|
||||
| Coin 消耗后 `CurrentCoin ≥ 0` | `TryConsumeCoin` 失败时 `CurrentCoin` 不变 |
|
||||
|
||||
Binary file not shown.
|
|
@ -0,0 +1,63 @@
|
|||
# 《几何塔防》
|
||||
|
||||
## 游戏定位
|
||||
塔防肉鸽
|
||||
|
||||
## 游戏主循环
|
||||
1. 进入游戏,使用当前样例库存中的组件组装初始防御塔并开始游戏
|
||||
2. 在关卡中玩家选择不同的节点推进关卡:
|
||||
- 战斗节点:布置防御塔抵御敌人进攻并收集防御塔组件
|
||||
- 事件节点:随机事件(不含战斗)
|
||||
- 商店节点:购买防御塔组件
|
||||
3. 节点后调整:玩家可使用三组件自由组装防御塔以抵御更强的敌人进攻
|
||||
4. 开始新一轮关卡
|
||||
|
||||
## 具体说明
|
||||
### 一、战前准备:
|
||||
1. 当前 M1 以样例库存和仓库内三组件组装链为准,不再要求独立的“开局二选三组件并组出两座塔”前置流程
|
||||
|
||||
### 二、节点:
|
||||
1. 当前 M1 只实现单一固定 Run:每个大关固定 10 个节点,最后一个节点固定为 Boss 战斗节点;多主题地图与大关间选择保留到后续阶段
|
||||
2. 主题地图示例(后续阶段):
|
||||
- 火山:高温(战斗节点中会随机触发火山喷发在地图上生成岩浆格)
|
||||
- 对于防御塔:部分组件在该高温下能发挥更强/更弱性能,若岩浆生成在防御塔上将会进一步强化高温对组件性能的影响
|
||||
- 对于敌人:敌人多具有火焰抗性,部分敌人能在岩浆格上更快的行走
|
||||
- 山地:地势起伏(地图格子有额外的高度条件,悬崖格子)
|
||||
- 对于防御塔:不同高度的攻击会有攻击范围变化,高打低范围加强,低打高范围缩小
|
||||
- 对于敌人:不同高度会有移动速度的差异,高往低走移速加快,低往高走移速降低,陆地敌人被击退到悬崖格子立即死亡
|
||||
3. 当前每个大关有 10 个固定节点,完成 Boss 节点后进入正式结束态并返回主菜单,不在 M1 内继续展开下一大关选择
|
||||
|
||||
#### 1. 战斗节点
|
||||
1. 玩家携带一定数量的防御塔进入关卡,在关卡内规定的位置布置防御塔,具体的游戏逻辑与一般的塔防游戏类似:
|
||||
- 关卡开始有一些资源用于布置防御塔,击杀敌人获取资源来布置或升级防御塔。
|
||||
- 敌人会选择最短路径由出怪口向玩家基地前进;道路阻挡与更复杂的路径改写机制保留到后续阶段
|
||||
2. 击杀敌人除了获取关卡内使用的资源外,还有概率掉落防御塔组件;每个小关卡结束后也会奖励组件与金币用于后续节点
|
||||
3. 关卡内设定一个胜利波次,当玩家存活的波次达到后会根据基地生命产生不同的结算:
|
||||
- = 100% :获得额外 30% 的金币,以及额外 1 次组件 3 选 1
|
||||
- >= 80% :获得额外 10% 的金币
|
||||
- >= 50% :无加成
|
||||
- < 50%:当前 M1 不再追加额外耐久惩罚;耐久已按“每场战斗结算后对本场参战塔固定扣 1”收口
|
||||
4. “胜利后继续挑战”保留到后续阶段,当前 M1 以正常结算回流节点地图为准
|
||||
|
||||
#### 2. 事件节点
|
||||
玩家经历一些有选项的随机事件,获取额外的奖励/惩罚。
|
||||
|
||||
示例:
|
||||
1. 玩家花费 100 金币赌马,有两个选择:(1) 30% 赢,赢了获得 250 金币。(2) 70% 赢,赢了获得 150 金币
|
||||
2. 玩家提供 2 个防御塔组件,获得 1 个不低于原来品质的防御塔组件
|
||||
3. 耐久换金币事件保留到后续阶段;当前 M1 只保留最小耐久闭环
|
||||
|
||||
#### 3. 商店节点
|
||||
1. 当前 M1 只实现组件商店的基础购买;商店内出售、刷新、复杂定价、卖塔加成与耐久折价保留到后续阶段
|
||||
2. 道具系统保留到后续阶段
|
||||
|
||||
### 三、节点后的调整
|
||||
1. 组装防御塔:每个防御塔都必须有枪口(攻击组件)、轴承(旋转组件)、底座(功能组件)三个组件,防御塔的品质由组件决定。当前 M1 已实现三组件完整合法参战、品质统一计算、组件实例 Tag 生成、塔级 Tag 汇总,以及首发 7 个 Tag 的战斗效果
|
||||
- 组件功能(某些组件还有全局性的属性倍率,比如高伤害穿透攻击的枪口会绑一个 0.5x攻击速度 的属性倍率来约束防御塔性能):
|
||||
- 枪口(攻击组件):决定攻击伤害,攻击方式(普通子弹、范围伤害、穿透激光……)
|
||||
- 轴承(旋转组件):决定枪口转速(某些攻击方式只有对准敌人后才能进行攻击),攻击范围
|
||||
- 底座(功能组件):决定攻击频率,攻击属性(火焰、毒素、冰……)
|
||||
- 品质计算:每个组件提供一定的品质权重(白:1,绿:2,蓝:3,紫:4,红:5),比如三个组件是 2 绿 1 白,那么防御塔的品质是 (2+2+1)/3=1.67,四舍五入后为 2 也就是绿色品质。
|
||||
- 各品质的配件槽与更深的配件系统保留到后续阶段
|
||||
- 组件 Tag 当前正式采用 `Tag.txt + RarityTagBudget.txt + TagConfig.txt` 三表方案:`Tag.txt` 负责基础字典、生成门槛、权重与启用态,`RarityTagBudget.txt` 负责按品质的 Tag 数量预算,`TagConfig.txt` 负责触发阶段、描述与效果参数
|
||||
2. 拆解与耐久:当前 M1 只保留最小耐久闭环,即每场战斗结算后按本场参战塔真实扣减 `1` 点耐久、`0` 耐久失效并拦截参战 / 战斗入口;连续属性衰减、自动销毁与维修系统保留到后续阶段
|
||||
|
|
@ -0,0 +1,667 @@
|
|||
# 程序集三层拆分方案
|
||||
|
||||
最后更新:2026-04-30
|
||||
|
||||
## 1. 设计目标
|
||||
|
||||
将项目拆分为三层,逐步解耦 Unity 依赖,实现核心业务逻辑的可测试性和可移植性:
|
||||
|
||||
- **L0(Domain)**:纯 C# 业务层,引用 GameFramework.dll 作为基础设施,可在独立解决方案中构建
|
||||
- **L1(Infrastructure)**:胶水层,连接 L0 与 Unity Runtime 类型
|
||||
- **L2(Presentation)**:表现层,Unity MonoBehaviour、UGuiForm、Entity 实现
|
||||
|
||||
---
|
||||
|
||||
## 2. GameFramework.dll 复用
|
||||
|
||||
项目自带的 `Assets/GameFramework/Libraries/GameFramework.dll` 是 **纯 C# 实现**的 GameFramework 核心库,包含 19 个模块。L0 直接引用此 DLL,无需重新实现。
|
||||
|
||||
### 2.1 可直接使用的模块
|
||||
|
||||
| 模块 | 提供内容 | L0 使用方式 |
|
||||
|------|---------|-----------|
|
||||
| **Event** | `EventManager`, `GameEventArgs` | `GameEntry.Event` 替换为 `EventManager` |
|
||||
| **ObjectPool** | `ObjectPoolManager`, `ObjectBase` | 继承 `ObjectBase`,无需自己实现池 |
|
||||
| **Fsm** | `FsmManager`, `FsmState`, `FsmState<T>` | 继承 `FsmState<T>` 构建状态机 |
|
||||
| **ReferencePool** | `ReferencePool` | `ReferencePool.Acquire<T>()` / `Release()` |
|
||||
| **DataNode** | 树状数据结构 | 直接使用 |
|
||||
| **Utility** | 通用工具类 | 直接使用 |
|
||||
| **Config** | 配置管理 | 直接使用 |
|
||||
| **DataTable** | 表格加载解析 | 数据行类依赖 GameFramework |
|
||||
|
||||
### 2.2 需要 Unity 适配的模块
|
||||
|
||||
以下模块需要 L1 提供 Unity 特定实现:
|
||||
|
||||
| 模块 | 原因 | L1 适配职责 |
|
||||
|------|------|------------|
|
||||
| **Resource** | 依赖 Unity Resources/AssetDatabase | 实现 `IResourceManager` |
|
||||
| **Scene** | 依赖 Unity SceneManager | 实现 `ISceneManager` |
|
||||
| **Entity** | 依赖 Unity GameObject | 实现 `IEntityManager` |
|
||||
| **UI** | 依赖 Unity UGUI | 实现 `IUIFormManager` |
|
||||
| **Sound** | 依赖 Unity Audio | 实现 `ISoundManager` |
|
||||
|
||||
### 2.3 复用带来的简化
|
||||
|
||||
```
|
||||
传统方案(自研基础设施)
|
||||
├── 需要自己实现事件系统
|
||||
├── 需要自己实现对象池基类
|
||||
├── 需要自己实现状态机框架
|
||||
└── 大量基础设施代码
|
||||
|
||||
GameFramework.dll 方案
|
||||
├── 事件 → GameFramework.Event.EventManager
|
||||
├── 对象池 → GameFramework.ObjectPool
|
||||
├── 状态机 → GameFramework.Fsm
|
||||
└── 专注业务逻辑实现
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 三层职责定义
|
||||
|
||||
### L0 - Domain(纯 C# 业务层)
|
||||
|
||||
- 引用 `GameFramework.dll`,使用其提供的 Event/ObjectPool/Fsm 等基础设施
|
||||
- 包含所有业务规则、状态机、领域逻辑
|
||||
- 无 `using UnityEngine`、`using UnityGameFramework.Runtime`
|
||||
- 通过接口与 L1 通信
|
||||
- 在独立 .sln 中构建,输出 DLL 导入 Unity
|
||||
|
||||
### L1 - Infrastructure(胶水层)
|
||||
|
||||
- 实现 GameFramework 的 Unity 特定接口(Resource/Scene/Entity/UI/Sound)
|
||||
- 实现 L0 定义的扩展接口(如 `ICombatEventHandler`)
|
||||
- 持有 L0 服务实例,管理 Unity 生命周期
|
||||
- 直接依赖 UnityEngine 和 GameFramework.Runtime
|
||||
|
||||
### L2 - Presentation(表现层)
|
||||
|
||||
- 所有 `MonoBehaviour` 类
|
||||
- View 层(UGuiForm 具体实现)
|
||||
- Entity Logic(Player、Enemy、Tower 具体实体)
|
||||
- ECS 组件(MovementComponent、ShooterBullet 等)
|
||||
|
||||
---
|
||||
|
||||
## 4. 程序集映射
|
||||
|
||||
### 4.1 L0 程序集
|
||||
|
||||
```
|
||||
GeometryTD.Domain/ ← 引用 GameFramework.dll
|
||||
├── Definition/
|
||||
│ ├── Enum/ # 所有枚举类型
|
||||
│ ├── Constant/ # 常量定义
|
||||
│ ├── DataStruct/ # 纯数据结构(AttackPayload, TowerStatsData 等)
|
||||
│ └── Tag/
|
||||
│ ├── Aggregation/ # Tag 汇总服务
|
||||
│ ├── Generation/ # Tag 生成规则
|
||||
│ ├── Metadata/ # Tag 定义元数据
|
||||
│ └── Combat/ # Tag 效果解析
|
||||
│
|
||||
├── GameFramework/ # GameFramework.dll 直接使用
|
||||
│ └── Event/ # 自定义事件 args,继承 GameEventArgs
|
||||
│
|
||||
├── CustomComponent/
|
||||
│ ├── CombatNode/ # 战斗域
|
||||
│ │ ├── CombatScheduler # 基于 GameFramework.Fsm
|
||||
│ │ ├── EnemyManager # 敌人生成、追踪
|
||||
│ │ ├── CombatRunResourceStore # 战斗资源存储
|
||||
│ │ └── CombatSettlementCalculator
|
||||
│ ├── PlayerInventory/ # 背包、交易、组装服务
|
||||
│ └── InventoryGeneration/ # 掉落、商店、奖励生成
|
||||
│
|
||||
├── UI/
|
||||
│ ├── Base/
|
||||
│ │ ├── IUIUseCase.cs
|
||||
│ │ └── UIContext.cs
|
||||
│ ├── Combat/
|
||||
│ │ ├── UseCase/ # CombatSelectFormUseCase, CombatInfoFormUseCase
|
||||
│ │ ├── RawData/
|
||||
│ │ └── Context/
|
||||
│ ├── Game/
|
||||
│ │ ├── UseCase/ # EventFormUseCase, NodeMapFormUseCase, RepoFormUseCase
|
||||
│ │ ├── RawData/
|
||||
│ │ └── Context/
|
||||
│ └── General/
|
||||
│ ├── UseCase/
|
||||
│ ├── RawData/
|
||||
│ └── Context/
|
||||
│
|
||||
└── Utility/
|
||||
├── InventoryCloneUtility.cs
|
||||
├── EnumUtility.cs
|
||||
└── InventoryRarityRuleService.cs
|
||||
```
|
||||
|
||||
### 4.2 L1 程序集
|
||||
|
||||
```
|
||||
GeometryTD.Infrastructure/
|
||||
├── GameFramework/ # GameFramework Unity 适配
|
||||
│ ├── Resource/ # 实现 IResourceManager
|
||||
│ ├── Scene/ # 实现 ISceneManager
|
||||
│ ├── Entity/ # 实现 IEntityManager
|
||||
│ ├── UI/ # 实现 IUIFormManager
|
||||
│ └── Sound/ # 实现 ISoundManager
|
||||
│
|
||||
├── CustomComponent/
|
||||
│ ├── CombatNode/
|
||||
│ │ └── CombatNodeComponent.cs # Unity Component
|
||||
│ ├── PlayerInventory/
|
||||
│ │ └── PlayerInventoryComponent.cs
|
||||
│ ├── InventoryGeneration/
|
||||
│ │ └── InventoryGenerationComponent.cs
|
||||
│ ├── EventNodeComponent.cs
|
||||
│ ├── ShopNodeComponent.cs
|
||||
│ ├── TagRegistry/
|
||||
│ │ └── TagRegistryComponent.cs
|
||||
│ └── BuiltinDataComponent.cs
|
||||
│
|
||||
├── Scene/
|
||||
│ └── Map/
|
||||
│ ├── MapTopologyService.cs # Tilemap 扫描
|
||||
│ ├── TowerPlacementService.cs # 依赖 GameEntry.Entity
|
||||
│ ├── TowerSelectionPresenter.cs
|
||||
│ └── MapCombatRuntimeBridge.cs
|
||||
│
|
||||
├── Entity/
|
||||
│ ├── EntityLogic/
|
||||
│ │ ├── EntityBase.cs
|
||||
│ │ ├── CombatSelectInputService.cs
|
||||
│ │ ├── CombatSelectUseCaseConfigurator.cs
|
||||
│ │ └── MapEntity.cs
|
||||
│ ├── EntityData/ # Unity 可序列化
|
||||
│ │ ├── MapData.cs
|
||||
│ │ ├── TowerData.cs
|
||||
│ │ ├── EnemyData.cs
|
||||
│ │ └── BulletData.cs
|
||||
│ └── EntityExtension.cs
|
||||
│
|
||||
├── UI/
|
||||
│ ├── Base/ # Unity 相关基类
|
||||
│ │ ├── UGuiForm.cs
|
||||
│ │ ├── UGuiGroupHelper.cs
|
||||
│ │ ├── UIFormControllerCommonBase.cs
|
||||
│ │ ├── UIExtension.cs
|
||||
│ │ └── UIFormControllerBase.cs
|
||||
│ └── Combat/
|
||||
│ └── Controller/
|
||||
│
|
||||
├── DataTable/ # GameFramework.DataTable 依赖
|
||||
│ └── DR*.cs
|
||||
│
|
||||
└── Procedure/ # GameFramework.Procedure
|
||||
└── ProcedureMain/
|
||||
```
|
||||
|
||||
### 4.3 L2 程序集
|
||||
|
||||
```
|
||||
GeometryTD.Presentation/
|
||||
├── UI/
|
||||
│ ├── Combat/
|
||||
│ │ └── View/
|
||||
│ ├── Game/
|
||||
│ │ └── View/
|
||||
│ ├── General/
|
||||
│ │ └── View/
|
||||
│ ├── Templates/
|
||||
│ └── Common/
|
||||
│
|
||||
├── Entity/
|
||||
│ └── EntityLogic/
|
||||
│ ├── Player.cs
|
||||
│ ├── Enemy.cs
|
||||
│ ├── TowerEntity.cs
|
||||
│ ├── BulletEntity.cs
|
||||
│ └── EnemyTagStatusRuntime.cs
|
||||
│
|
||||
└── Components/
|
||||
├── MovementComponent.cs
|
||||
├── ShooterBullet.cs
|
||||
├── ShooterMuzzleComp.cs
|
||||
├── TowerController.cs
|
||||
├── InputComponent.cs
|
||||
├── BasicBaseComp.cs
|
||||
└── BasicBearingComp.cs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. GameFramework 集成方式
|
||||
|
||||
### 5.1 事件系统
|
||||
|
||||
L0 使用 GameFramework 内置的 `EventManager`,无需自己实现事件系统:
|
||||
|
||||
```csharp
|
||||
// L0: 定义游戏事件,继承 GameFramework.Event.GameEventArgs
|
||||
public class CombatCoinChangedEventArgs : GameEventArgs
|
||||
{
|
||||
public const int EventId = typeof(CombatCoinChangedEventArgs).GetHashCode();
|
||||
|
||||
public int CurrentCoin { get; private set; }
|
||||
public int Delta { get; private set; }
|
||||
|
||||
public CombatCoinChangedEventArgs() { }
|
||||
|
||||
public CombatCoinChangedEventArgs(int currentCoin, int delta)
|
||||
{
|
||||
CurrentCoin = currentCoin;
|
||||
Delta = delta;
|
||||
}
|
||||
}
|
||||
|
||||
// L0: 服务中使用
|
||||
public class CombatRunResourceStore
|
||||
{
|
||||
private IEventManager _event;
|
||||
|
||||
public CombatRunResourceStore(IEventManager eventManager)
|
||||
{
|
||||
_event = eventManager;
|
||||
}
|
||||
|
||||
public void AddCoin(int amount)
|
||||
{
|
||||
CurrentCoin += amount;
|
||||
_event.Fire(this, CombatCoinChangedEventArgs.Create(CurrentCoin, amount));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 状态机
|
||||
|
||||
L0 使用 GameFramework 的 Fsm 模块:
|
||||
|
||||
```csharp
|
||||
// L0: 战斗状态机基于 GameFramework.Fsm
|
||||
public interface ICombatFsm { } // 空接口,用于泛型约束
|
||||
|
||||
public class CombatScheduler
|
||||
{
|
||||
private IFsmManager _fsmManager;
|
||||
private CombatSchedulerRuntime _runtime;
|
||||
|
||||
public CombatScheduler(IFsmManager fsmManager)
|
||||
{
|
||||
_fsmManager = fsmManager;
|
||||
_runtime = new CombatSchedulerRuntime();
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_fsmManager.CreateFsm<ICombatFsm>(this,
|
||||
new CombatLoadingState(),
|
||||
new CombatRunningPhaseState(),
|
||||
new CombatWaitingForPhaseEndState(),
|
||||
new CombatSettlementState());
|
||||
}
|
||||
}
|
||||
|
||||
// L0: 具体状态继承 FsmState
|
||||
public class CombatRunningPhaseState : FsmState<ICombatFsm>
|
||||
{
|
||||
protected internal override void OnEnter(ICombatFsm fsmOwner)
|
||||
{
|
||||
// 初始化
|
||||
}
|
||||
|
||||
protected internal override void OnUpdate(ICombatFsm fsmOwner, float elapseSeconds)
|
||||
{
|
||||
// 每帧更新
|
||||
}
|
||||
|
||||
protected internal override void OnLeave(ICombatFsm fsmOwner, bool isShutdown)
|
||||
{
|
||||
// 清理
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 对象池
|
||||
|
||||
L0 使用 GameFramework 的 ObjectPool:
|
||||
|
||||
```csharp
|
||||
// L0: 敌人继承 ObjectBase
|
||||
public class EnemyObject : ObjectBase
|
||||
{
|
||||
public int EnemyId { get; private set; }
|
||||
public int Health { get; private set; }
|
||||
|
||||
protected internal override void OnSpawn(bool isRecycle)
|
||||
{
|
||||
// 激活时调用
|
||||
}
|
||||
|
||||
protected internal override void OnDespawn(bool isRecycle)
|
||||
{
|
||||
// 回收时调用
|
||||
}
|
||||
|
||||
public void Initialize(int enemyId, int health)
|
||||
{
|
||||
EnemyId = enemyId;
|
||||
Health = health;
|
||||
}
|
||||
}
|
||||
|
||||
// L0: 使用对象池
|
||||
public class EnemyManager
|
||||
{
|
||||
private IObjectPoolManager _poolManager;
|
||||
|
||||
public EnemyManager(IObjectPoolManager poolManager)
|
||||
{
|
||||
_poolManager = poolManager;
|
||||
}
|
||||
|
||||
public void SpawnEnemy(int enemyId, int health)
|
||||
{
|
||||
var pool = _poolManager.GetOrCreateObjectPool<EnemyObject>("EnemyPool");
|
||||
var enemy = EnemyObject.Create(enemyId, health);
|
||||
pool.Register(enemy, true);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. L1 桥接设计
|
||||
|
||||
### 6.1 GameFramework Unity 适配接口
|
||||
|
||||
GameFramework.dll 定义了以下接口,L1 需要提供 Unity 实现:
|
||||
|
||||
```csharp
|
||||
// GameFramework 定义的接口(L0 引用)
|
||||
namespace GameFramework.Resource
|
||||
{
|
||||
public interface IResourceManager
|
||||
{
|
||||
void LoadAsset(string assetName, LoadAssetCallbacks callbacks, object userData);
|
||||
void UnloadAsset(string assetName);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// L1: Unity 实现
|
||||
public class UnityResourceManager : IResourceManager
|
||||
{
|
||||
public void LoadAsset(string assetName, LoadAssetCallbacks callbacks, object userData)
|
||||
{
|
||||
// 使用 Unity Resources.Load 或 Addressables
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 L0 服务生命周期管理
|
||||
|
||||
```csharp
|
||||
// L1: Unity Component 持有 L0 服务实例
|
||||
public class CombatNodeComponent : GameFrameworkComponent
|
||||
{
|
||||
private IEventManager _eventManager;
|
||||
private IObjectPoolManager _poolManager;
|
||||
private IFsmManager _fsmManager;
|
||||
|
||||
// L0 服务
|
||||
private CombatScheduler _scheduler;
|
||||
private EnemyManager _enemyManager;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// 初始化 GameFramework Unity 适配器
|
||||
_eventManager = GameEntry.GetComponent<EventComponent>();
|
||||
_poolManager = GameEntry.GetComponent<ObjectPoolComponent>();
|
||||
_fsmManager = GameEntry.GetComponent<FsmComponent>();
|
||||
|
||||
// 初始化 L0 服务(注入依赖)
|
||||
_enemyManager = new EnemyManager(_poolManager);
|
||||
_scheduler = new CombatScheduler(_fsmManager, _enemyManager);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Unity 类型处理策略
|
||||
|
||||
### 7.1 Vector3 / Quaternion / Color
|
||||
|
||||
| 类型 | 方案 |
|
||||
|------|------|
|
||||
| `Vector3` | 使用 `System.Numerics.Vector3`,GameFramework 内部已使用 |
|
||||
| `Quaternion` | 使用 `System.Numerics.Quaternion` |
|
||||
| `Color` | 定义纯 C# Color 结构 `{ float r, g, b, a; }` |
|
||||
|
||||
### 7.2 [Serializable]
|
||||
|
||||
```csharp
|
||||
// L0: POCO 数据结构
|
||||
public class TowerStatsData
|
||||
{
|
||||
public int[] AttackDamage { get; set; }
|
||||
public float[] AttackSpeed { get; set; }
|
||||
}
|
||||
|
||||
// L1: Unity 可序列化 DTO
|
||||
[Serializable]
|
||||
public class TowerStatsDataDto
|
||||
{
|
||||
[SerializeField] private int[] _attackDamage;
|
||||
[SerializeField] private float[] _attackSpeed;
|
||||
|
||||
public TowerStatsData ToDomain() => new()
|
||||
{
|
||||
AttackDamage = _attackDamage,
|
||||
AttackSpeed = _attackSpeed
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 Sprite 引用
|
||||
|
||||
```csharp
|
||||
// L0: 使用资源路径而非 Sprite 引用
|
||||
public class TowerSelectItemRawData
|
||||
{
|
||||
public string IconPath { get; set; } // "UI/Icons/TowerIcon"
|
||||
}
|
||||
|
||||
// L1: Controller 负责路径到 Sprite 的解析
|
||||
public class CombatSelectFormController
|
||||
{
|
||||
private SpriteCacheComponent _spriteCache;
|
||||
|
||||
private TowerSelectItemContext BuildContext(TowerSelectItemRawData raw)
|
||||
{
|
||||
return new TowerSelectItemContext
|
||||
{
|
||||
Icon = _spriteCache.GetSprite(raw.IconPath)
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 迁移顺序
|
||||
|
||||
### Phase 1: 基础设施搭建
|
||||
|
||||
| 任务 | 说明 |
|
||||
|------|------|
|
||||
| 创建 L0 项目,引用 GameFramework.dll | 验证基础依赖 |
|
||||
| 验证 GameFramework Event/Fsm/ObjectPool 可用 | 核心模块连通性测试 |
|
||||
| 创建 L1 基础结构 | Unity Component 基类、GameFramework 适配器 |
|
||||
|
||||
### Phase 2: 核心业务迁移(低风险优先)
|
||||
|
||||
| 迁移项 | 说明 | 依赖 |
|
||||
|--------|------|------|
|
||||
| `Definition/Enum/*` | 枚举类型 | 无 |
|
||||
| `Definition/Constant/*` | 常量定义 | 无 |
|
||||
| `UI/*/RawData/*` | 原始数据 | 无 |
|
||||
| `UI/*/Context/*` | 上下文 | 无 |
|
||||
| `Utility/*` | 工具类 | 无 |
|
||||
|
||||
### Phase 3: 业务域迁移
|
||||
|
||||
| 迁移项 | 说明 | 依赖 |
|
||||
|--------|------|------|
|
||||
| `InventoryGeneration/*` | 掉落、商店、奖励 | Phase 1-2 |
|
||||
| `PlayerInventory/*` | 背包、交易、组装 | Phase 1-2 |
|
||||
| `UI/*/UseCase/*` | 用例 | Phase 2 + Sprite 路径化 |
|
||||
| `Definition/Tag/*` | Tag 系统 | Phase 1-2 |
|
||||
|
||||
### Phase 4: 战斗域(核心难点)
|
||||
|
||||
| 迁移项 | 说明 | 依赖 |
|
||||
|--------|------|------|
|
||||
| `CombatNode/CombatScheduler` | 状态机 | GameFramework.Fsm |
|
||||
| `CombatNode/EnemyManager/*` | 敌人域 | GameFramework.ObjectPool |
|
||||
| `CombatNode/CombatRunResourceStore` | 资源存储 | GameFramework.Event |
|
||||
| `CombatNode/CombatSettlementCalculator` | 结算 | Phase 3 |
|
||||
|
||||
### Phase 5: 胶水和表现层迁移
|
||||
|
||||
| 迁移项 | 说明 |
|
||||
|--------|------|
|
||||
| `CustomComponent/*Component` | Unity Component |
|
||||
| `UI/Base/*` | Controller 基类 |
|
||||
| `UI/*/Controller/*` | Controller 实现 |
|
||||
| `UI/*/View/*` | View 实现 |
|
||||
| `Entity/EntityLogic/*` | Entity 实现 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 项目文件组织
|
||||
|
||||
### 9.1 L0 项目文件
|
||||
|
||||
```xml
|
||||
<!-- GeometryTD.Domain.csproj -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>GeometryTD.Domain</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="GameFramework">
|
||||
<HintPath>..\..\Assets\GameFramework\Libraries\GameFramework.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
### 9.2 L1 项目文件
|
||||
|
||||
```xml
|
||||
<!-- GeometryTD.Infrastructure.csproj -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GeometryTD.Domain\GeometryTD.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="UnityEngine">
|
||||
<HintPath>Unity\Editor\Data\Managed\UnityEngine\UnityEngine.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityGameFramework.Runtime">
|
||||
<!-- Unity GameFramework Runtime 引用 -->
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
### 9.3 Unity 资产结构
|
||||
|
||||
```
|
||||
Assets/
|
||||
├── GameFramework/
|
||||
│ └── Libraries/
|
||||
│ ├── GameFramework.dll ← L0 直接引用
|
||||
│ └── GameFramework.xml ← 文档
|
||||
│
|
||||
├── GameMain/
|
||||
│ ├── L0/ # L0 DLL 输出
|
||||
│ ├── L1/ # L1 源码或 DLL
|
||||
│ └── Scripts/
|
||||
│ └── L2/ # L2 表现层
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 验收标准
|
||||
|
||||
1. **L0 可独立编译**:不包含 UnityEngine.dll、UnityGameFramework.Runtime 引用
|
||||
2. **GameFramework.dll 正确引用**:L0 可使用 Event/ObjectPool/Fsm/ReferencePool
|
||||
3. **无循环依赖**:L2 → L1 → L0,单向依赖
|
||||
4. **接口隔离**:L0 与 Unity 的交互通过 L1 适配器
|
||||
5. **游戏流程完整**:拆分后战斗、商店、组装流程正常运行
|
||||
|
||||
---
|
||||
|
||||
## 11. 附录:GameFramework.dll 模块清单
|
||||
|
||||
```
|
||||
GameFramework.dll 包含以下 19 个模块(纯 C# 实现):
|
||||
|
||||
配置与数据
|
||||
├── Config - 全局配置管理
|
||||
├── DataNode - 树状数据结点
|
||||
├── DataTable - 表格数据管理
|
||||
└── Setting - 键值对存储
|
||||
|
||||
核心设施
|
||||
├── Event - 事件管理(EventPool)
|
||||
├── ObjectPool - 对象池(ObjectPoolManager)
|
||||
├── ReferencePool - 引用计数池
|
||||
├── Fsm - 有限状态机
|
||||
└── Procedure - 流程(ProcedureBase)
|
||||
|
||||
资源管理
|
||||
├── Resource - 资源加载(需要 Unity 适配器)
|
||||
├── Scene - 场景管理(需要 Unity 适配器)
|
||||
├── Entity - 实体管理(需要 Unity 适配器)
|
||||
├── UI - 界面管理(需要 Unity 适配器)
|
||||
└── Sound - 声音管理(需要 Unity 适配器)
|
||||
|
||||
网络与下载
|
||||
├── Network - Socket 长连接
|
||||
├── WebRequest - HTTP 短连接
|
||||
└── Download - 文件下载
|
||||
|
||||
辅助模块
|
||||
├── Localization - 多语言(需要资源适配器)
|
||||
├── FileSystem - 虚拟文件系统
|
||||
├── Debugger - 调试窗口
|
||||
└── Utility - 通用工具类
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 附录:当前 Unity 依赖渗透点
|
||||
|
||||
以下文件包含 `using UnityEngine` 或 `using UnityGameFramework.Runtime`,需要迁移:
|
||||
|
||||
| 文件 | Unity 依赖 | 目标层 |
|
||||
|------|-----------|-------|
|
||||
| `TowerPlacementService.cs` | `GameEntry.Entity` | L1 |
|
||||
| `MapTopologyService.cs` | `Tilemap`, `Vector3Int` | L1 |
|
||||
| `CombatSelectFormUseCase.cs` | `UnityEngine.Sprite` | L0 → 路径化 |
|
||||
| `TowerStatsData.cs` | `[Serializable]`, `Color` | L0 → POCO + DTO |
|
||||
| `AttackPayload.cs` | `Vector3` | L0 → System.Numerics |
|
||||
| `MapEntity.cs` | `MonoBehaviour` | L1 |
|
||||
| `EnemyEntity.cs` | `Transform` | L2 |
|
||||
| `GameEntry.Builtin.cs` | `GameFrameworkComponent` | L1 |
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
# 《几何塔防》MVP 范围说明(流程验证版)
|
||||
|
||||
---
|
||||
|
||||
## 一、MVP目标
|
||||
|
||||
本阶段目标:
|
||||
|
||||
> 验证游戏完整流程是否可运行,包括
|
||||
> 战斗节点 → 事件节点 → 商店节点 → 节点后组装 → 下一节点
|
||||
> 直至完成一大关。
|
||||
|
||||
本阶段不关注:
|
||||
|
||||
* 数值平衡
|
||||
* 构筑深度
|
||||
* 美术质量
|
||||
* 商业化系统
|
||||
* 长期留存
|
||||
|
||||
---
|
||||
|
||||
## 二、本阶段包含内容
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣ 基础游戏结构
|
||||
|
||||
* 单一主题地图(无环境机制)
|
||||
* 1个大关(固定10节点)
|
||||
* 最后节点为Boss战
|
||||
* 固定节点顺序(例如:战斗 → 事件 → 战斗 → 商店 → 战斗 → Boss)
|
||||
|
||||
不做节点地图选择 UI。
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ 战斗节点(核心可运行)
|
||||
|
||||
包含:
|
||||
|
||||
* 基地生命系统
|
||||
* 固定路径(单路径)
|
||||
* 敌人按最短路径移动
|
||||
* 5~8波敌人
|
||||
* 1种普通敌人 + 1种精英敌人 + 1种Boss
|
||||
* 胜利条件:达到波次
|
||||
* 失败条件:基地血量为0
|
||||
|
||||
战斗奖励:
|
||||
|
||||
* 敌人可概率掉落组件与金币
|
||||
* 战斗结算提供金币奖励
|
||||
* 基地满血通关时额外提供 1 次组件 3选1 奖励
|
||||
|
||||
不做:
|
||||
|
||||
* 多路径
|
||||
* 地图机制
|
||||
* 精细敌人AI
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ 事件节点(流程占位)
|
||||
|
||||
实现:
|
||||
|
||||
* 3个固定事件模板
|
||||
* 简单二选一结构
|
||||
* 直接修改金币或组件数量
|
||||
|
||||
例如:
|
||||
|
||||
* 获得100金币
|
||||
* 损失1个随机组件换取高品质组件
|
||||
|
||||
不做:
|
||||
|
||||
* 复杂概率算法
|
||||
* 连锁事件
|
||||
* 条件触发事件
|
||||
|
||||
---
|
||||
|
||||
### 4️⃣ 商店节点
|
||||
|
||||
实现:
|
||||
|
||||
* 显示4个随机组件
|
||||
* 可购买组件
|
||||
|
||||
不做:
|
||||
|
||||
* 商店内出售组件
|
||||
* 商店刷新
|
||||
* 动态定价
|
||||
* 经济平衡
|
||||
* 道具系统
|
||||
* 广告
|
||||
* 内购
|
||||
|
||||
---
|
||||
|
||||
### 5️⃣ 节点后组装系统
|
||||
|
||||
实现:
|
||||
|
||||
* 组件栏
|
||||
* 塔槽位
|
||||
* 拖拽组装
|
||||
* 组件替换
|
||||
* 基础属性与 Tag 展示
|
||||
|
||||
组件结构(精简):
|
||||
|
||||
* 枪口
|
||||
* 轴承
|
||||
* 底座
|
||||
* 基础 Tag 系统(首发 7 个:`Fire`、`Ice`、`Crit`、`Execution`、`Shatter`、`Inferno`、`AbsoluteZero`)
|
||||
|
||||
不做:
|
||||
|
||||
* 完整维修 / 折价 / 自动销毁系统
|
||||
* 高级 Tag 联动
|
||||
* 复杂触发矩阵
|
||||
|
||||
---
|
||||
|
||||
## 三、明确本阶段不做内容
|
||||
|
||||
以下内容全部排除在本阶段之外:
|
||||
|
||||
* ❌ 多主题地图
|
||||
* ❌ 地图机制(火山/山地)
|
||||
* ❌ 局外成长系统
|
||||
* ❌ 广告系统
|
||||
* ❌ 内购系统
|
||||
* ❌ 成就系统
|
||||
* ❌ 排行榜
|
||||
* ❌ 多流派深度平衡
|
||||
* ❌ 大规模敌人种类
|
||||
* ❌ 复杂Boss阶段机制
|
||||
* ❌ 数值精细调优
|
||||
* ❌ 高级特效优化
|
||||
|
||||
---
|
||||
|
||||
## 四、组件规模限制
|
||||
|
||||
为避免膨胀,本阶段限制:
|
||||
|
||||
* 总组件数量 ≤ 20
|
||||
* Tag数量 ≤ 8
|
||||
* 品质等级:白 / 绿 / 蓝 / 紫 / 红
|
||||
* 仅要求三组件主结构,不扩展更深的配件槽系统
|
||||
|
||||
---
|
||||
|
||||
## 五、UI范围
|
||||
|
||||
包含:
|
||||
|
||||
* 主界面(开始游戏)
|
||||
* 节点流程界面
|
||||
* 战斗界面
|
||||
* 商店界面
|
||||
* 组装界面
|
||||
* 结算界面
|
||||
|
||||
允许:
|
||||
|
||||
* 使用临时UI
|
||||
* 使用占位图
|
||||
* 不做动画优化
|
||||
|
||||
---
|
||||
|
||||
## 六、完成标准(验收条件)
|
||||
|
||||
本阶段完成定义为:
|
||||
|
||||
1. 可从开始游戏一路完成一个大关
|
||||
2. 节点之间流程无阻塞
|
||||
3. 组件可组装并生效
|
||||
4. 战斗可胜可负
|
||||
5. 商店可购买
|
||||
6. 不出现流程死锁
|
||||
7. 无严重崩溃或逻辑错误
|
||||
|
||||
只要流程跑通,即视为完成。
|
||||
|
||||
---
|
||||
|
||||
## 七、本阶段不评估指标
|
||||
|
||||
* 不评估留存
|
||||
* 不评估爽感强度
|
||||
* 不评估平衡性
|
||||
* 不评估变现能力
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
# MapEntity 设计规范(开发约束)
|
||||
|
||||
最后更新:2026-03-06
|
||||
|
||||
## 1. 目标与边界
|
||||
|
||||
`MapEntity` 现在是战斗地图域的编排层(orchestrator),目标是:
|
||||
- 对外暴露地图能力(格子查询、路径查询、建造操作入口)。
|
||||
- 在 Unity 生命周期中初始化/清理各子服务。
|
||||
- 承担输入与 UI 用例的连接,不承载具体业务算法细节。
|
||||
|
||||
`MapEntity` 不应再直接实现以下细节:
|
||||
- Tile 扫描与路径缓存算法。
|
||||
- 防御塔映射字典的增删改查细节。
|
||||
- 选中状态与攻击范围显示细节。
|
||||
- 鼠标拾取与 `CombatSelectFormUserData` 组装细节。
|
||||
- 战斗选择 UI 的库存快照解析、颜色映射与 Build 选项配置细节。
|
||||
|
||||
---
|
||||
|
||||
## 2. 模块划分(当前标准)
|
||||
|
||||
### 2.1 MapEntity(编排层)
|
||||
|
||||
文件:`Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs`
|
||||
|
||||
职责:
|
||||
- 生命周期编排:`OnInit / OnShow / OnUpdate / OnHide`
|
||||
- 子服务初始化与清理
|
||||
- UI 用例绑定(`CombatSelectFormUseCase`)
|
||||
- 将输入结果分发到选择器/建造器
|
||||
- 收集运行时上下文并委托给专用服务配置战斗选择 UI
|
||||
|
||||
### 2.2 MapTopologyService(地图拓扑层)
|
||||
|
||||
文件:`Assets/GameMain/Scripts/Scene/Map/MapTopologyService.cs`
|
||||
|
||||
职责:
|
||||
- 扫描 Tilemap,构建 `PathCells` / `FoundationCells`
|
||||
- 缓存 `Spawner -> 默认路径`
|
||||
- 提供路径查询:
|
||||
- `TryGetNearestPathCell`
|
||||
- `TryGetDefaultPathCells`
|
||||
- `TryFindPathCells`
|
||||
- `TryFindPathWorldPoints`
|
||||
|
||||
约束:
|
||||
- 只处理“地图拓扑与路径”,不处理经济、建塔、UI。
|
||||
|
||||
### 2.3 CombatSelectInputService(输入解析层)
|
||||
|
||||
文件:`Assets/GameMain/Scripts/Entity/EntityLogic/CombatSelectInputService.cs`
|
||||
|
||||
职责:
|
||||
- 鼠标屏幕坐标 -> 世界坐标
|
||||
- 判定点击对象类型(`None/Foundation/Tower`)
|
||||
- 组装 `CombatSelectFormUserData`
|
||||
|
||||
约束:
|
||||
- 只读上下文,不改变游戏状态。
|
||||
- Foundation/Tower 点击时,UI 定位由 Cell 中心决定(稳定定位)。
|
||||
|
||||
### 2.4 CombatSelectUseCaseConfigurator(战斗选择 UI 配置层)
|
||||
|
||||
文件:`Assets/GameMain/Scripts/Entity/EntityLogic/CombatSelectUseCaseConfigurator.cs`
|
||||
|
||||
职责:
|
||||
- 读取库存快照与参战塔快照
|
||||
- 构建组件映射与塔外观颜色
|
||||
- 配置 `CombatSelectFormUseCase` 的 Build/Upgrade/Destroy action 与显示参数
|
||||
- 缓存当前 Build 槽位对应的视觉信息,供建塔流程复用
|
||||
|
||||
约束:
|
||||
- 只负责 UI 选项配置与只读数据组装,不直接改变战斗状态。
|
||||
- 不处理鼠标输入、地图拓扑、塔实体生命周期。
|
||||
|
||||
### 2.5 TowerPlacementService(塔部署层)
|
||||
|
||||
当前文件:`Assets/GameMain/Scripts/Scene/Map/TowerPlacementService.cs`
|
||||
|
||||
职责:
|
||||
- 建造 / 升级 / 销毁(含升级失败回滚)
|
||||
- 维护塔映射状态:
|
||||
- `foundationCell -> towerEntityId`
|
||||
- `towerEntityId -> foundationCell`
|
||||
- `towerEntityId -> towerStats`
|
||||
- 提供整局清理接口
|
||||
|
||||
约束:
|
||||
- 仅处理塔生命周期与映射,不处理选中态和 UI。
|
||||
|
||||
### 2.6 TowerSelectionPresenter(选择展示层)
|
||||
|
||||
文件:`Assets/GameMain/Scripts/Scene/Map/TowerSelectionPresenter.cs`
|
||||
|
||||
职责:
|
||||
- 维护当前选中对象
|
||||
- 根据选中状态切换攻击范围显示(通过 `TowerEntity.SetAttackRangeVisible`)
|
||||
|
||||
约束:
|
||||
- 不做建造/升级/销毁。
|
||||
|
||||
---
|
||||
|
||||
## 3. 运行时主流程(简版)
|
||||
|
||||
1. `MapEntity.OnInit` 初始化并绑定 6 个对象:
|
||||
- `MapTopologyService`
|
||||
- `CombatSelectInputService`
|
||||
- `CombatSelectUseCaseConfigurator`
|
||||
- `TowerPlacementService`
|
||||
- `TowerSelectionPresenter`
|
||||
- `CombatSelectFormUseCase`
|
||||
2. `MapEntity.OnShow` 刷新地图拓扑,配置 UI action 与 Build 视觉数据。
|
||||
3. 每帧 `OnUpdate`:
|
||||
- 采集输入(InputService)
|
||||
- 更新选中对象(SelectionPresenter)
|
||||
- 打开/刷新 UI
|
||||
4. UI Build/Upgrade/Destroy action 回调:
|
||||
- 通过 PlacementService 改变塔状态
|
||||
- 通过 SelectionPresenter 同步选中和范围显示
|
||||
5. `OnHide`:
|
||||
- 关闭 UI
|
||||
- 隐藏并清理塔实体
|
||||
- 清理选择态与塔映射运行时状态
|
||||
- 清理拓扑缓存
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心不变量(必须保持)
|
||||
|
||||
1. `MapEntity` 只编排,不承载业务细节实现。
|
||||
2. `TowerPlacementService` 是塔映射状态的唯一写入口。
|
||||
3. `MapTopologyService` 是 Path/Foundation 数据的唯一来源。
|
||||
4. “当前可见攻击范围”同一时刻最多一个塔。
|
||||
5. 清场顺序固定:先关闭 UI,再隐藏塔,再清空选择与映射,再清理拓扑缓存。
|
||||
|
||||
---
|
||||
|
||||
## 5. 扩展开发规范
|
||||
|
||||
### 5.1 新增地图规则(地形、禁建、动态阻挡)
|
||||
|
||||
优先改 `MapTopologyService`,不要直接改 `MapEntity`。
|
||||
|
||||
### 5.2 新增建造规则(费用、限制、回滚策略)
|
||||
|
||||
优先改 `TowerPlacementService`,不要在 `MapEntity` 写分支。
|
||||
|
||||
### 5.3 新增点击交互或 UI 定位策略
|
||||
|
||||
优先改 `CombatSelectInputService`。
|
||||
|
||||
### 5.4 新增 Build 选项展示、塔外观颜色或库存驱动 UI 配置
|
||||
|
||||
优先改 `CombatSelectUseCaseConfigurator`。
|
||||
|
||||
### 5.5 新增选中表现(特效、描边、信息面板联动)
|
||||
|
||||
优先改 `TowerSelectionPresenter`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 代码变更检查清单(PR 自检)
|
||||
|
||||
1. 是否把新业务放进了对应服务,而不是 `MapEntity`?
|
||||
2. 是否破坏了“服务唯一写入口”不变量?
|
||||
3. Build/Upgrade 失败是否有完整回滚和金币返还?
|
||||
4. 清理路径是否覆盖了 `OnHide` 与重进地图场景?
|
||||
5. `dotnet build GeometryTD.sln` 是否通过?
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
# Tag System Design
|
||||
|
||||
最后更新:2026-03-13
|
||||
|
||||
> 目标:这是 GeometryTD 当前 Tag 系统的唯一正式口径。
|
||||
> 本文档只记录当前真实实现、当前边界与正式规则。
|
||||
> 长期扩展预案见 `docs/TagSystemRoadmap.md`;若两者冲突,以本文件为准。
|
||||
|
||||
## 1. 当前范围
|
||||
|
||||
M1 已完成 Tag 最小闭环:
|
||||
|
||||
- 组件实例 Tag 已统一生成,不再直接复制 `PossibleTag`
|
||||
- 塔级 Tag 已统一汇总为 `TagRuntimeData[]`
|
||||
- UI 展示已优先消费 `TagRuntimes`
|
||||
- 首发 7 个 Tag 已进入战斗实际生效
|
||||
|
||||
当前正式首发 7 个 Tag:
|
||||
|
||||
- `Fire`
|
||||
- `Ice`
|
||||
- `Crit`
|
||||
- `Execution`
|
||||
- `Shatter`
|
||||
- `Inferno`
|
||||
- `AbsoluteZero`
|
||||
|
||||
当前明确后移的 5 个 Tag:
|
||||
|
||||
- `BurnSpread`
|
||||
- `IgniteBurst`
|
||||
- `FreezeMask`
|
||||
- `Pierce`
|
||||
- `Overpenetrate`
|
||||
|
||||
当前不展开:
|
||||
|
||||
- 高级传播 / 多命中 / 击杀链式效果
|
||||
- 复杂流派激活矩阵
|
||||
- Tag 等级成长
|
||||
- `TagGroup` 运行时规则
|
||||
|
||||
## 2. 正式规则
|
||||
|
||||
1. Tag 在组件实例创建时随机;组塔阶段只汇总,不重新随机。
|
||||
2. 组件表的 `PossibleTag` 只表示候选池,不代表实例最终持有结果。
|
||||
3. 组塔后重复 Tag 不丢弃,而是转为塔级 `Stack`。
|
||||
4. 配置层正式采用 `Tag.txt + RarityTagBudget.txt + TagConfig.txt` 三表结构。
|
||||
5. `Tag.txt` 是 `IsImplemented` 的正式真相源;`TagConfig.txt` 只负责触发阶段、描述与运行时参数。
|
||||
6. 战斗中的 Tag 统一挂在 `AttackPayload -> HitContext -> TagEffectResolver` 链路上。
|
||||
7. 新逻辑应优先使用 `TagRuntimes`;`Tags` 只保留兼容投影职责。
|
||||
8. `TagGroup` 当前只作为展示元数据,不进入生成、汇总或战斗规则。
|
||||
|
||||
## 3. 配置与生成
|
||||
|
||||
### 3.1 三表职责
|
||||
|
||||
`Tag.txt`
|
||||
|
||||
- 负责基础字典与生成规则
|
||||
- 当前字段:`TagType`、`Name`、`TagGroup`、`MinRarity`、`Weight`、`IsImplemented`
|
||||
|
||||
`RarityTagBudget.txt`
|
||||
|
||||
- 负责按品质定义组件实例本次可抽取的 Tag 数量预算
|
||||
- 当前字段:`Rarity`、`MinCount`、`MaxCount`
|
||||
|
||||
`TagConfig.txt`
|
||||
|
||||
- 负责战斗与展示相关配置
|
||||
- 当前字段:`TriggerPhase`、`Description`、`ParamJson`
|
||||
|
||||
### 3.2 当前消费链
|
||||
|
||||
- `Tag.txt -> DRTag -> TagGenerationRuleRegistry`
|
||||
- 当前负责 `MinRarity`、`Weight`
|
||||
- `Tag.txt + TagConfig.txt -> TagDefinitionRegistry.ReloadFromRows(...)`
|
||||
- 当前负责 `IsImplemented`、`TriggerPhase`、`Description` 与正式运行时参数
|
||||
- `ProcedureMain -> GameEntry.TagRegistry.OnInit() -> TagRegistryComponent`
|
||||
- 当前在主流程进入时统一基于已加载数据表重建 Tag 定义层与生成规则层
|
||||
- `TagRegistryComponent` 对 `TagConfig`、`Tag`、`RarityTagBudget` 三张表采用 fail-fast 依赖约束;缺表时直接暴露初始化错误,不再静默跳过
|
||||
- `RarityTagBudget.txt -> DRRarityTagBudget -> RarityTagBudgetRuleRegistry`
|
||||
- 当前负责按品质读取 `MinCount / MaxCount`
|
||||
|
||||
### 3.3 运行时结构
|
||||
|
||||
- 组件实例保存 `TagType[]`
|
||||
- 塔实例保存 `TagRuntimeData[]`
|
||||
- 塔实例同时保留 `Tags` 作为兼容投影
|
||||
- 战斗载荷保存塔级 `TagRuntimeData[]`
|
||||
|
||||
```csharp
|
||||
public sealed class TagRuntimeData
|
||||
{
|
||||
public TagType TagType { get; set; }
|
||||
public int TotalStack { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 组件 Tag 生成
|
||||
|
||||
当前统一入口是 `ComponentTagGenerationService`,以下链路共用同一套规则:
|
||||
|
||||
- `InventorySeedUtility`
|
||||
- `InventoryGenerationComponent.BuildShopGoods(...)`
|
||||
- `InventoryGenerationComponent.ResolveEnemyDrop(...)`
|
||||
- `InventoryGenerationComponent.BuildRewardCandidates(...)`
|
||||
|
||||
当前流程:
|
||||
|
||||
1. 读取组件配置的 `PossibleTag`
|
||||
2. 读取 `Tag.txt` 中对应的生成规则
|
||||
3. 过滤掉 `None`、非法枚举、当前未首发支持的 Tag
|
||||
4. 过滤掉 `MinRarity > 当前组件品质` 的 Tag
|
||||
5. 根据 `RarityTagBudget.txt` 决定本次抽取数量
|
||||
6. 在候选池内按 `Weight` 抽取
|
||||
7. 单组件内不重复抽取同一 Tag
|
||||
8. 候选池不足时允许少于预算,不强行补满
|
||||
|
||||
### 3.5 可复现合同
|
||||
|
||||
Tag 随机结果的正式上下文为:
|
||||
|
||||
- `RunSeed`
|
||||
- `SourceType`
|
||||
- `ItemInstanceId`
|
||||
- `ConfigId`
|
||||
|
||||
当前运行时通过 `InventoryGenerationRandomContext + InventoryTagRandomContext` 统一承载上述字段:
|
||||
|
||||
- `InventoryGenerationRandomContext`
|
||||
- 统一承载 `runSeed + nodeSequenceIndex + sourceType + localOrdinal`
|
||||
- 统一派生产出链路自己的稳定随机流
|
||||
- 统一派生稳定的临时组件 `InstanceId`
|
||||
- `InventoryTagRandomContext`
|
||||
- 承载 Tag 生成所需的 `RunSeed / SourceType / ItemInstanceId / ConfigId`
|
||||
- 由 `InventoryGenerationRandomContext` 进一步派生
|
||||
|
||||
各来源构造口径:
|
||||
|
||||
- `Seed`:使用真实 `RunSeed` 与初始组件实例 Id
|
||||
- `Shop`:使用 `RunSeed + nodeSequenceIndex + goodsIndex + configId`
|
||||
- `Drop`:使用 `RunSeed + nodeSequenceIndex + dropOrdinal + configId`
|
||||
- `Reward`:使用 `RunSeed + nodeSequenceIndex + rewardOrdinal + configId`
|
||||
|
||||
## 4. 汇总、展示与战斗
|
||||
|
||||
### 4.1 汇总与展示
|
||||
|
||||
- 同一组件内不允许重复同一个 Tag
|
||||
- 不同组件之间允许重复
|
||||
- 组塔时不做重新随机
|
||||
- 汇总时按 `TagType` 分组并累加 `Stack`
|
||||
- 组件展示仍显示组件实例自己的 `Tags`
|
||||
- 塔展示优先显示 `TagRuntimes`
|
||||
- 若缺少 `TagRuntimes`,允许通过 `Tags` 回退构建兼容结果
|
||||
- 重复 Tag 以 `xN` 文本显示,例如 `Fire x2`
|
||||
- `TowerStatsData.Tags` 不是新的真相源,只用于兼容旧展示链路与旧数据
|
||||
|
||||
### 4.2 战斗上下文
|
||||
|
||||
`AttackPayload` 当前字段:
|
||||
|
||||
- `BaseDamage`
|
||||
- `AttackPropertyType`
|
||||
- `SourceEntityId`
|
||||
- `ProjectileEntityId`
|
||||
- `OriginPosition`
|
||||
- `TagRuntimes`
|
||||
|
||||
`HitContext` 当前字段:
|
||||
|
||||
- `AttackPayload`
|
||||
- `FinalDamage`
|
||||
- `IsCriticalHit`
|
||||
- `IsKilled`
|
||||
- `TargetEntityId`
|
||||
- `TargetPosition`
|
||||
- `TargetCurrentHealthBeforeHit`
|
||||
- `TargetCurrentHealthAfterHit`
|
||||
- `TargetMaxHealth`
|
||||
- `TargetMoveSpeedMultiplierBeforeHit`
|
||||
- `TargetStatusTagsBeforeHit`
|
||||
- `TargetStatusRuntime`
|
||||
- `CritRoll`
|
||||
- `StatusModifierContext`
|
||||
|
||||
`HitContext` 当前还提供:
|
||||
|
||||
- `HasTargetStatus(TagType)`
|
||||
- `HasSlowStatusBeforeHit`
|
||||
|
||||
### 4.3 分类与触发阶段
|
||||
|
||||
| 分类 | 说明 | 当前主触发阶段 |
|
||||
|------|------|----------------|
|
||||
| `Status` | 命中后在敌人身上形成可持续状态 | `OnAfterHit` |
|
||||
| `NumericModifier` | 命中前修正最终伤害 | `OnBeforeHit` |
|
||||
| `AttackShape` | 穿透、传播、爆炸等攻击形态变化 | `OnHit` / `OnKill` |
|
||||
| `StatusModifier` | 强化同次命中的状态类 Tag,但不独立生成敌人持有状态 | `OnAfterHit` |
|
||||
|
||||
### 4.4 当前已实现的首发 7 Tag
|
||||
|
||||
| Tag | 分类 | 配置阶段 | 当前真实行为 |
|
||||
|-----|------|----------|--------------|
|
||||
| `Fire` | `Status` | `OnAfterHit` | 命中后施加燃烧 DOT,且 `MaxEffectiveStack` 已进入基础层数结算 |
|
||||
| `Ice` | `Status` | `OnAfterHit` | 命中后施加减速 |
|
||||
| `Crit` | `NumericModifier` | `OnBeforeHit` | 按概率暴击并放大伤害 |
|
||||
| `Execution` | `NumericModifier` | `OnBeforeHit` | 对低血量目标增伤 |
|
||||
| `Shatter` | `NumericModifier` | `OnBeforeHit` | 对已减速目标增伤 |
|
||||
| `Inferno` | `StatusModifier` | `OnAfterHit` | 先由 resolver 解析为命中期修饰,再强化同次命中的 `Fire` 时长与伤害 |
|
||||
| `AbsoluteZero` | `StatusModifier` | `OnAfterHit` | 先由 resolver 解析为命中期修饰,再强化同次命中的 `Ice` 时长与减速强度 |
|
||||
|
||||
### 4.5 当前已后移的 5 Tag
|
||||
|
||||
| Tag | 分类 | 当前状态 |
|
||||
|-----|------|----------|
|
||||
| `BurnSpread` | `AttackShape` | 仅保留元数据、占位配置与占位路由,未实际生效 |
|
||||
| `IgniteBurst` | `AttackShape` | 仅保留元数据、占位配置与占位路由,未实际生效 |
|
||||
| `FreezeMask` | `AttackShape` | 仅保留元数据、占位配置与占位路由,未实际生效 |
|
||||
| `Pierce` | `AttackShape` | 仅保留元数据、占位配置与占位路由,未实际生效 |
|
||||
| `Overpenetrate` | `AttackShape` | 仅保留元数据、占位配置与占位路由,未实际生效 |
|
||||
|
||||
## 5. 当前运行时边界
|
||||
|
||||
- 数值类 Tag 在 `ResolveBeforeHit` 阶段生效
|
||||
- 状态类 Tag 通过 `EnemyTagStatusRuntime` 管理敌人持有状态
|
||||
- `StatusModifier` 已独立按 `OnAfterHit` 路由,并写入 `StatusModifierContext`
|
||||
- `Inferno` 与 `AbsoluteZero` 不生成敌人持有状态;没有 `Fire` / `Ice` 时不会单独产生效果
|
||||
- 后移 5 个攻击形态 Tag 的 `TriggerPhase` 当前正式口径为 `None`
|
||||
- 后移 5 个攻击形态 Tag 的 `ParamJson` 当前只视为占位说明,不是正式运行时字段
|
||||
|
||||
## 6. 非当前范围
|
||||
|
||||
- `BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 的真实战斗实现
|
||||
- 更复杂的多 Tag 联动与流派激活矩阵
|
||||
- 冻结积累条、击杀爆炸传播链、多命中弹道系统
|
||||
- Tag 等级成长、TagGroup 运行时规则、额外 Tag 元数据扩展表
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
# Tag System Roadmap
|
||||
|
||||
最后更新:2026-03-12
|
||||
|
||||
> 目标:记录 Tag 系统的长期扩展预案。
|
||||
> 本文档不是当前实现真相源;若与 `docs/TagSystemDesign.md` 冲突,以 `docs/TagSystemDesign.md` 为准。
|
||||
|
||||
## 1. 文档定位
|
||||
|
||||
- `TagSystemDesign.md` 负责当前正式口径
|
||||
- 本文档只负责后续扩展方向
|
||||
- 这里出现的能力都不代表当前已经实现,也不代表已经排进最近一期开发
|
||||
|
||||
## 2. 可继续推进的方向
|
||||
|
||||
### 2.1 第二批攻击形态 Tag
|
||||
|
||||
后续可基于现有 `AttackPayload -> HitContext -> TagEffectResolver` 合同继续推进:
|
||||
|
||||
- `BurnSpread`
|
||||
- `IgniteBurst`
|
||||
- `FreezeMask`
|
||||
- `Pierce`
|
||||
- `Overpenetrate`
|
||||
|
||||
进入实现前提:
|
||||
|
||||
- 不破坏当前 `HitContext` 主干
|
||||
- 优先补上下文字段,不回退到散装参数扩展
|
||||
- 每个 Tag 进入真实战斗效果前,都要先明确它需要的命中信息与回归测试
|
||||
|
||||
### 2.2 更复杂的多 Tag 联动
|
||||
|
||||
可考虑的后续方向:
|
||||
|
||||
- 更深的状态连锁
|
||||
- 击杀传播或爆炸传播链
|
||||
- 多 Tag 组合触发的特殊行为
|
||||
|
||||
进入实现前提:
|
||||
|
||||
- 先定义联动的真相源与优先级
|
||||
- 不把展示元数据直接升级成运行时规则
|
||||
|
||||
### 2.3 成长与元数据扩展
|
||||
|
||||
可考虑的后续方向:
|
||||
|
||||
- Tag 等级成长
|
||||
- 更多 Tag 元数据扩展表
|
||||
- 运行时需要时再讨论 `TagGroup` 是否升级为规则输入
|
||||
|
||||
进入实现前提:
|
||||
|
||||
- 当前三表职责仍保持清晰
|
||||
- 新字段必须同时明确配置真相源、运行时消费者与展示口径
|
||||
|
||||
## 3. 当前默认约束
|
||||
|
||||
- 第二批 Tag 未进入真实战斗效果前,继续保持占位配置与占位路由
|
||||
- `TagGroup` 继续只作为展示元数据,不提前接入运行时
|
||||
- 新预案优先进入本文件,不回写到 `TagSystemDesign.md` 主干
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
# UI 五层架构设计规范(RawData / Controller / View / Context / UseCase)
|
||||
|
||||
## 1. 适用范围
|
||||
|
||||
- 适用目录:`Assets/GameMain/Scripts/UI/*`
|
||||
- 重点对象:采用五层拆分的 UI 模块(`MenuScene`、`GameScene`、`General` 下的分层 UI)
|
||||
- 本文不展开 Unity GameFramework 底层实现细节,仅约束项目内 UI 代码组织与协作方式
|
||||
|
||||
## 2. 架构总览
|
||||
|
||||
UI 模块采用“输入数据 -> 业务编排 -> 展示数据 -> 渲染表现”的分层方式,核心链路如下:
|
||||
|
||||
1. 外部流程(Procedure/GameState)创建并绑定 UseCase
|
||||
2. 通过 `GameEntry.UIRouter` 打开指定 UI
|
||||
3. Controller 从 UseCase 取 RawData,并转换为 Context
|
||||
4. View 使用 Context 渲染
|
||||
5. View 通过事件回传交互,Controller 处理后驱动 UseCase 更新,再刷新 View
|
||||
|
||||
简化关系图:
|
||||
|
||||
```text
|
||||
Procedure/GameState
|
||||
-> UIRouter
|
||||
-> Controller <-> UseCase
|
||||
-> Context -> View
|
||||
View --(CustomEvent)--> Controller
|
||||
```
|
||||
|
||||
## 3. 五层职责定义
|
||||
|
||||
### 3.1 RawData 层
|
||||
|
||||
职责:承载“业务原始数据”,作为 UseCase 到 Controller 的传输模型。
|
||||
|
||||
约束:
|
||||
|
||||
- 命名:`XXXFormRawData`
|
||||
- 只描述数据,不包含 UI 渲染行为
|
||||
- 可保留领域对象或数据表对象(例如 `DRLevelUpReward`、`WeaponBase`)
|
||||
- 不依赖具体 View 组件
|
||||
|
||||
参考:
|
||||
|
||||
- `Assets/GameMain/Scripts/UI/Template/GameScene/RawData/ShopFormRawData.cs`
|
||||
- `Assets/GameMain/Scripts/UI/Template/GameScene/RawData/LevelUpFormRawData.cs`
|
||||
- `Assets/GameMain/Scripts/UI/Template/MenuScene/RawData/SelectRoleFormRawData.cs`
|
||||
|
||||
### 3.2 UseCase 层
|
||||
|
||||
职责:封装 UI 对应业务用例,负责业务规则、状态推进、数据生成。
|
||||
|
||||
约束:
|
||||
|
||||
- 实现 `IUIUseCase`
|
||||
- 命名:`XXXFormUseCase`
|
||||
- 对外提供 `CreateInitialModel / TryRefresh / Select / Confirm` 等语义化方法
|
||||
- 返回 RawData(或结果对象),不直接操作具体 View
|
||||
|
||||
参考:
|
||||
|
||||
- `Assets/GameMain/Scripts/UI/Template/GameScene/UseCase/ShopFormUseCase.cs`
|
||||
- `Assets/GameMain/Scripts/UI/Template/GameScene/UseCase/LevelUpFormUseCase.cs`
|
||||
- `Assets/GameMain/Scripts/UI/Template/MenuScene/UseCase/SelectRoleFormUseCase.cs`
|
||||
|
||||
### 3.3 Controller 层
|
||||
|
||||
职责:UI 编排层,连接 UseCase 与 View,管理 UI 生命周期、事件订阅、数据转换。
|
||||
|
||||
约束:
|
||||
|
||||
- 继承 `UIFormControllerCommonBase<TContext, TForm>`
|
||||
- 命名:`XXXFormController`
|
||||
- 通过 `BindUseCase(IUIUseCase)` 注入用例并做类型校验
|
||||
- `OpenUI(object userData = null)` 支持:`Context`、`RawData`、`null`
|
||||
- 负责 RawData -> Context 的转换(常见 `BuildContext`)
|
||||
- 在 `SubscribeCustomEvents / UnsubscribeCustomEvents` 成对管理事件
|
||||
- 可做局部刷新(避免整窗重建)
|
||||
|
||||
参考:
|
||||
|
||||
- `Assets/GameMain/Scripts/UI/Template/GameScene/Controller/ShopFormController.cs`
|
||||
- `Assets/GameMain/Scripts/UI/Template/GameScene/Controller/LevelUpFormController.cs`
|
||||
- `Assets/GameMain/Scripts/UI/Template/MenuScene/Controller/SelectRoleFormController.cs`
|
||||
|
||||
### 3.4 Context 层
|
||||
|
||||
职责:承载“可直接驱动 UI 展示”的上下文数据。
|
||||
|
||||
约束:
|
||||
|
||||
- 继承 `UIContext`
|
||||
- 命名:`XXXFormContext` 或 `XXXItemContext`
|
||||
- 字段以展示友好为目标(标题、描述、图标、稀有度、列表等)
|
||||
- 允许组合子 Context(例如列表区 + 条目)
|
||||
|
||||
参考:
|
||||
|
||||
- `Assets/GameMain/Scripts/UI/Template/GameScene/Context/ShopFormContext.cs`
|
||||
- `Assets/GameMain/Scripts/UI/Template/GameScene/Context/DisplayListAreaContext.cs`
|
||||
- `Assets/GameMain/Scripts/UI/Template/MenuScene/Context/SelectRoleFormContext.cs`
|
||||
|
||||
### 3.5 View 层
|
||||
|
||||
职责:纯表现层,负责控件绑定、显示刷新、交互事件抛出。
|
||||
|
||||
约束:
|
||||
|
||||
- Form 类继承 `UGuiForm`,子组件通常继承 `MonoBehaviour`
|
||||
- 命名:`XXXForm` / `XXXItem` / `XXXArea`
|
||||
- 提供 `RefreshUI(Context)`、`OnInit(Context)`、`OnReset()` 等渲染入口
|
||||
- 用户交互通过 `GameEntry.Event.Fire(...)` 通知 Controller
|
||||
- 不承载业务规则(计算、流程推进、数据筛选应在 UseCase)
|
||||
|
||||
参考:
|
||||
|
||||
- `Assets/GameMain/Scripts/UI/Template/GameScene/View/ShopForm.cs`
|
||||
- `Assets/GameMain/Scripts/UI/Template/GameScene/View/DisplayListArea.cs`
|
||||
- `Assets/GameMain/Scripts/UI/Template/MenuScene/View/SelectRoleForm.cs`
|
||||
|
||||
## 4. 标准交互流程
|
||||
|
||||
### 4.1 初始化与绑定
|
||||
|
||||
1. Procedure/GameState 创建 UseCase
|
||||
2. 调用 `GameEntry.UIRouter.BindUIUseCase(UIFormType.X, useCase)`
|
||||
|
||||
### 4.2 打开 UI
|
||||
|
||||
1. 调用 `GameEntry.UIRouter.OpenUI(UIFormType.X)`
|
||||
2. Controller 从 UseCase 取 RawData(或接收外部 RawData/Context)
|
||||
3. Controller 构建 Context 后打开/刷新 Form
|
||||
4. View 在 `OnOpen` 中校验 Context 类型并执行 `RefreshUI`
|
||||
|
||||
### 4.3 用户交互到刷新
|
||||
|
||||
1. View 触发事件(如购买、刷新、选择)
|
||||
2. Controller 监听事件并调用 UseCase
|
||||
3. UseCase 返回新数据或操作结果
|
||||
4. Controller 更新 Context 并刷新全部或局部 UI
|
||||
|
||||
### 4.4 关闭 UI
|
||||
|
||||
1. 调用 `GameEntry.UIRouter.CloseUI(UIFormType.X)`
|
||||
2. Controller 解除事件订阅并关闭窗体
|
||||
3. View `OnClose` 清理本地状态
|
||||
|
||||
## 5. 目录与命名规范
|
||||
|
||||
- 目录:`UI/<SceneDomain>/RawData|UseCase|Controller|Context|View`
|
||||
- 五层同名前缀保持一致:`ShopForm*`、`LevelUpForm*`、`SelectRoleForm*`
|
||||
- 子组件上下文命名:`RoleItemContext`、`DisplayItemContext`、`LevelUpRewardItemContext`
|
||||
- 新增 UI Form 时优先建立完整五层;仅纯静态展示可降级为 View-only
|
||||
|
||||
## 6. 依赖方向约束
|
||||
|
||||
允许依赖:
|
||||
|
||||
- `UseCase -> RawData / 领域对象`
|
||||
- `Controller -> UseCase + RawData + Context + View + Event`
|
||||
- `View -> Context + Event`
|
||||
|
||||
禁止依赖:
|
||||
|
||||
- `View -> UseCase`
|
||||
- `View -> 领域状态修改`
|
||||
- `Context/RawData -> View`
|
||||
|
||||
## 7. 新增一个五层 UI 的落地步骤
|
||||
|
||||
1. 在目标场景目录创建 `RawData / UseCase / Context / Controller / View` 对应类型
|
||||
2. 在 UseCase 中实现模型创建与交互方法
|
||||
3. 在 Controller 中实现 `BindUseCase`、`OpenUI`、`BuildContext`、事件订阅
|
||||
4. 在 View 中实现 `RefreshUI` 和交互事件抛出
|
||||
5. 在对应 Procedure/GameState 里完成 UseCase 绑定与 Open/Close 调用
|
||||
6. 自测三条主链路:首次打开、交互刷新、关闭重开
|
||||
|
||||
## 8. 项目当前实践说明
|
||||
|
||||
- `ShopForm`、`LevelUpForm`、`SelectRoleForm` 是当前五层模式的主要样板
|
||||
- `DialogForm` 也有 Controller/Context/RawData,但 UseCase 为可选
|
||||
- `HudForm`、`StartMenuForm` 当前为轻用例场景,可不强制 UseCase
|
||||
- `SettingForm`、`AboutForm` 属于历史直连型 UI,不属于五层完整样板
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
//------------------------------------------------------------
|
||||
// Game Framework
|
||||
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
|
||||
// Homepage: https://gameframework.cn/
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏入口。
|
||||
/// </summary>
|
||||
public partial class GameEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取游戏基础组件。
|
||||
/// </summary>
|
||||
public static BaseComponent Base { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置组件。
|
||||
/// </summary>
|
||||
public static ConfigComponent Config { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取数据结点组件。
|
||||
/// </summary>
|
||||
public static DataNodeComponent DataNode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取数据表组件。
|
||||
/// </summary>
|
||||
public static DataTableComponent DataTable { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取调试组件。
|
||||
/// </summary>
|
||||
public static DebuggerComponent Debugger { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取下载组件。
|
||||
/// </summary>
|
||||
public static DownloadComponent Download { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取实体组件。
|
||||
/// </summary>
|
||||
public static EntityComponent Entity { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取事件组件。
|
||||
/// </summary>
|
||||
public static EventComponent Event { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件系统组件。
|
||||
/// </summary>
|
||||
public static FileSystemComponent FileSystem { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取有限状态机组件。
|
||||
/// </summary>
|
||||
public static FsmComponent Fsm { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化组件。
|
||||
/// </summary>
|
||||
public static LocalizationComponent Localization { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取网络组件。
|
||||
/// </summary>
|
||||
public static NetworkComponent Network { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象池组件。
|
||||
/// </summary>
|
||||
public static ObjectPoolComponent ObjectPool { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取流程组件。
|
||||
/// </summary>
|
||||
public static ProcedureComponent Procedure { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取资源组件。
|
||||
/// </summary>
|
||||
public static ResourceComponent Resource { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取场景组件。
|
||||
/// </summary>
|
||||
public static SceneComponent Scene { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置组件。
|
||||
/// </summary>
|
||||
public static SettingComponent Setting { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取声音组件。
|
||||
/// </summary>
|
||||
public static SoundComponent Sound { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取界面组件。
|
||||
/// </summary>
|
||||
public static UIComponent UI { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取网络组件。
|
||||
/// </summary>
|
||||
public static WebRequestComponent WebRequest { get; private set; }
|
||||
|
||||
private static void InitBuiltinComponents()
|
||||
{
|
||||
Base = UnityGameFramework.Runtime.GameEntry.GetComponent<BaseComponent>();
|
||||
Config = UnityGameFramework.Runtime.GameEntry.GetComponent<ConfigComponent>();
|
||||
DataNode = UnityGameFramework.Runtime.GameEntry.GetComponent<DataNodeComponent>();
|
||||
DataTable = UnityGameFramework.Runtime.GameEntry.GetComponent<DataTableComponent>();
|
||||
Debugger = UnityGameFramework.Runtime.GameEntry.GetComponent<DebuggerComponent>();
|
||||
Download = UnityGameFramework.Runtime.GameEntry.GetComponent<DownloadComponent>();
|
||||
Entity = UnityGameFramework.Runtime.GameEntry.GetComponent<EntityComponent>();
|
||||
Event = UnityGameFramework.Runtime.GameEntry.GetComponent<EventComponent>();
|
||||
FileSystem = UnityGameFramework.Runtime.GameEntry.GetComponent<FileSystemComponent>();
|
||||
Fsm = UnityGameFramework.Runtime.GameEntry.GetComponent<FsmComponent>();
|
||||
Localization = UnityGameFramework.Runtime.GameEntry.GetComponent<LocalizationComponent>();
|
||||
Network = UnityGameFramework.Runtime.GameEntry.GetComponent<NetworkComponent>();
|
||||
ObjectPool = UnityGameFramework.Runtime.GameEntry.GetComponent<ObjectPoolComponent>();
|
||||
Procedure = UnityGameFramework.Runtime.GameEntry.GetComponent<ProcedureComponent>();
|
||||
Resource = UnityGameFramework.Runtime.GameEntry.GetComponent<ResourceComponent>();
|
||||
Scene = UnityGameFramework.Runtime.GameEntry.GetComponent<SceneComponent>();
|
||||
Setting = UnityGameFramework.Runtime.GameEntry.GetComponent<SettingComponent>();
|
||||
Sound = UnityGameFramework.Runtime.GameEntry.GetComponent<SoundComponent>();
|
||||
UI = UnityGameFramework.Runtime.GameEntry.GetComponent<UIComponent>();
|
||||
WebRequest = UnityGameFramework.Runtime.GameEntry.GetComponent<WebRequestComponent>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
using CustomComponent;
|
||||
using GeometryTD.CustomComponent;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏入口。
|
||||
/// </summary>
|
||||
public partial class GameEntry
|
||||
{
|
||||
public static BuiltinDataComponent BuiltinData { get; private set; }
|
||||
|
||||
public static PlayerInventoryComponent PlayerInventory { get; private set; }
|
||||
|
||||
public static HPBarComponent HPBar { get; private set; }
|
||||
|
||||
public static UIRouterComponent UIRouter { get; private set; }
|
||||
|
||||
public static EventNodeComponent EventNode { get; private set; }
|
||||
|
||||
public static CombatNodeComponent CombatNode { get; private set; }
|
||||
|
||||
public static ShopNodeComponent ShopNode { get; private set; }
|
||||
|
||||
public static ResolutionAdapterComponent ResolutionAdapter { get; private set; }
|
||||
|
||||
public static SpriteCacheComponent SpriteCache { get; private set; }
|
||||
|
||||
public static InventoryGenerationComponent InventoryGeneration { get; private set; }
|
||||
|
||||
public static TagRegistryComponent TagRegistry { get; private set; }
|
||||
|
||||
private static void InitCustomComponents()
|
||||
{
|
||||
BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent<BuiltinDataComponent>();
|
||||
PlayerInventory = UnityGameFramework.Runtime.GameEntry.GetComponent<PlayerInventoryComponent>();
|
||||
HPBar = UnityGameFramework.Runtime.GameEntry.GetComponent<HPBarComponent>();
|
||||
UIRouter = UnityGameFramework.Runtime.GameEntry.GetComponent<UIRouterComponent>();
|
||||
EventNode = UnityGameFramework.Runtime.GameEntry.GetComponent<EventNodeComponent>();
|
||||
CombatNode = UnityGameFramework.Runtime.GameEntry.GetComponent<CombatNodeComponent>();
|
||||
ShopNode = UnityGameFramework.Runtime.GameEntry.GetComponent<ShopNodeComponent>();
|
||||
ResolutionAdapter = UnityGameFramework.Runtime.GameEntry.GetComponent<ResolutionAdapterComponent>();
|
||||
SpriteCache = UnityGameFramework.Runtime.GameEntry.GetComponent<SpriteCacheComponent>();
|
||||
InventoryGeneration = UnityGameFramework.Runtime.GameEntry.GetComponent<InventoryGenerationComponent>();
|
||||
TagRegistry = UnityGameFramework.Runtime.GameEntry.GetComponent<TagRegistryComponent>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
//------------------------------------------------------------
|
||||
// Game Framework
|
||||
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
|
||||
// Homepage: https://gameframework.cn/
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏入口。
|
||||
/// </summary>
|
||||
public partial class GameEntry : MonoBehaviour
|
||||
{
|
||||
private void Start()
|
||||
{
|
||||
InitBuiltinComponents();
|
||||
InitCustomComponents();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Components
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class BasicBaseComp : MonoBehaviour
|
||||
{
|
||||
[SerializeField] [Min(0.01f)] private float _attackSpeed = 1f;
|
||||
|
||||
[SerializeField] private AttackPropertyType _attackPropertyType = AttackPropertyType.Physics;
|
||||
|
||||
[SerializeField] private SpriteRenderer _renderer;
|
||||
|
||||
private float _cooldownRemaining;
|
||||
|
||||
public float AttackSpeed => _attackSpeed;
|
||||
public AttackPropertyType AttackPropertyType => _attackPropertyType;
|
||||
public bool CanAttack => _cooldownRemaining <= 0f;
|
||||
|
||||
public void OnInit(float attackSpeed, AttackPropertyType attackPropertyType)
|
||||
{
|
||||
_attackSpeed = Mathf.Max(0.01f, attackSpeed);
|
||||
_attackPropertyType = attackPropertyType;
|
||||
_cooldownRemaining = 0f;
|
||||
}
|
||||
|
||||
public void OnReset()
|
||||
{
|
||||
_attackSpeed = 1f;
|
||||
_attackPropertyType = AttackPropertyType.None;
|
||||
_cooldownRemaining = 0f;
|
||||
}
|
||||
|
||||
public void SetColor(Color color)
|
||||
{
|
||||
if (_renderer != null)
|
||||
{
|
||||
_renderer.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
public void Tick(float deltaTime)
|
||||
{
|
||||
if (_cooldownRemaining <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cooldownRemaining = Mathf.Max(0f, _cooldownRemaining - Mathf.Max(0f, deltaTime));
|
||||
}
|
||||
|
||||
public bool TryAttack(BasicBearingComp bearingComp, ShooterMuzzleComp shooterMuzzleComp, Transform target, float deltaTime)
|
||||
{
|
||||
if (bearingComp == null || shooterMuzzleComp == null || target == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Tick(deltaTime);
|
||||
bool isAligned = bearingComp.TrackTarget(target, deltaTime);
|
||||
if (!isAligned || !CanAttack)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool fired = shooterMuzzleComp.Attack(target, _attackPropertyType);
|
||||
if (!fired)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_cooldownRemaining = 1f / _attackSpeed;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace Components
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class BasicBearingComp : MonoBehaviour
|
||||
{
|
||||
[SerializeField] [Min(1f)] private float _rotateSpeed = 180f;
|
||||
|
||||
[SerializeField] [Min(0.1f)] private float _attackRange = 5f;
|
||||
|
||||
[SerializeField] [Min(0.1f)] private float _aimToleranceAngle = 2f;
|
||||
|
||||
[SerializeField] private Transform _rotateRoot;
|
||||
|
||||
[SerializeField] private SpriteRenderer _renderer;
|
||||
|
||||
public float RotateSpeed => _rotateSpeed;
|
||||
public float AttackRange => _attackRange;
|
||||
public float AimToleranceAngle => _aimToleranceAngle;
|
||||
|
||||
public void OnInit(float rotateSpeed, float attackRange = 5f)
|
||||
{
|
||||
_rotateSpeed = Mathf.Max(1f, rotateSpeed);
|
||||
_attackRange = Mathf.Max(0.1f, attackRange);
|
||||
}
|
||||
|
||||
public void OnReset()
|
||||
{
|
||||
_rotateSpeed = 1f;
|
||||
_attackRange = 5f;
|
||||
}
|
||||
|
||||
public void SetColor(Color color)
|
||||
{
|
||||
if (_renderer != null)
|
||||
{
|
||||
_renderer.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsTargetInRange(Transform target, Transform origin = null)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Transform originTransform = origin != null ? origin : transform;
|
||||
Vector3 delta = target.position - originTransform.position;
|
||||
return delta.sqrMagnitude <= _attackRange * _attackRange;
|
||||
}
|
||||
|
||||
public bool TrackTarget(Transform target, float deltaTime)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Transform rotateRoot = _rotateRoot != null ? _rotateRoot : transform;
|
||||
Vector2 toTarget = (Vector2)(target.position - rotateRoot.position);
|
||||
|
||||
if (toTarget.sqrMagnitude <= Mathf.Epsilon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
float targetZAngle = Vector2.SignedAngle(Vector2.up, toTarget);
|
||||
float currentZAngle = rotateRoot.eulerAngles.z;
|
||||
float maxStep = _rotateSpeed * Mathf.Max(0f, deltaTime);
|
||||
float angleDelta = Mathf.DeltaAngle(currentZAngle, targetZAngle);
|
||||
float nextZAngle = currentZAngle + Mathf.Clamp(angleDelta, -maxStep, maxStep);
|
||||
rotateRoot.rotation = Quaternion.Euler(0f, 0f, nextZAngle);
|
||||
return IsTargetAligned(target);
|
||||
}
|
||||
|
||||
public bool IsTargetAligned(Transform target)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Transform rotateRoot = _rotateRoot != null ? _rotateRoot : transform;
|
||||
Vector2 toTarget = (Vector2)(target.position - rotateRoot.position);
|
||||
|
||||
if (toTarget.sqrMagnitude <= Mathf.Epsilon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
float targetZAngle = Vector2.SignedAngle(Vector2.up, toTarget);
|
||||
float currentZAngle = rotateRoot.eulerAngles.z;
|
||||
float angle = Mathf.Abs(Mathf.DeltaAngle(currentZAngle, targetZAngle));
|
||||
return angle <= _aimToleranceAngle;
|
||||
}
|
||||
|
||||
public bool TryAttack(ShooterMuzzleComp shooterMuzzleComp, Transform target, float deltaTime)
|
||||
{
|
||||
if (shooterMuzzleComp == null || target == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsTargetInRange(target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isAligned = TrackTarget(target, deltaTime);
|
||||
if (!isAligned)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return shooterMuzzleComp.Attack(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using GeometryTD.Definition;
|
||||
|
||||
namespace Components
|
||||
{
|
||||
public interface IDamageReceiver
|
||||
{
|
||||
void TakeDamage(AttackPayload attackPayload);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace Components
|
||||
{
|
||||
public class InputComponent : MonoBehaviour
|
||||
{
|
||||
private PlayerInputActions _inputActions;
|
||||
private bool _isListening = false;
|
||||
|
||||
private Vector3 _direction = Vector3.zero;
|
||||
public Vector3 Direction => _direction;
|
||||
|
||||
public void OnInit()
|
||||
{
|
||||
_inputActions = new();
|
||||
}
|
||||
|
||||
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
if (_isListening && _inputActions != null)
|
||||
{
|
||||
ReadInput();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnReset()
|
||||
{
|
||||
_inputActions = null;
|
||||
_isListening = false;
|
||||
}
|
||||
|
||||
private void ReadInput()
|
||||
{
|
||||
Vector2 rawDir = _inputActions.Game.Move.ReadValue<Vector2>();
|
||||
_direction = new Vector3(rawDir.x, 0, rawDir.y);
|
||||
}
|
||||
|
||||
public void SetListening(bool isListening)
|
||||
{
|
||||
_isListening = isListening;
|
||||
if (_isListening)
|
||||
{
|
||||
_inputActions.Enable();
|
||||
}
|
||||
else
|
||||
{
|
||||
_inputActions.Disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace Components
|
||||
{
|
||||
public class MovementComponent : MonoBehaviour
|
||||
{
|
||||
private bool _isMoving = false;
|
||||
private Vector3 _direction = Vector3.forward;
|
||||
private float _speed = 0;
|
||||
private float _speedMultiplier = 1f;
|
||||
private Transform _cachedTransform;
|
||||
|
||||
public void OnInit(float speed, Transform transform)
|
||||
{
|
||||
_speed = speed;
|
||||
_speedMultiplier = 1f;
|
||||
_cachedTransform = transform;
|
||||
}
|
||||
|
||||
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
if (_isMoving && _cachedTransform != null)
|
||||
{
|
||||
Move(elapseSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnReset()
|
||||
{
|
||||
_speed = 0;
|
||||
_speedMultiplier = 1f;
|
||||
_cachedTransform = null;
|
||||
_isMoving = false;
|
||||
}
|
||||
|
||||
private void Move(float deltaTime = 0)
|
||||
{
|
||||
_cachedTransform.Translate(_direction * (_speed * _speedMultiplier) * deltaTime);
|
||||
}
|
||||
|
||||
public void SetMove(bool isMoving) => _isMoving = isMoving;
|
||||
public void SetDirection(Vector3 direction) => _direction = direction;
|
||||
public void SetSpeedMultiplier(float speedMultiplier) => _speedMultiplier = Mathf.Max(0f, speedMultiplier);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
using UnityEngine;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
|
||||
namespace Components
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class ShooterBullet : MonoBehaviour
|
||||
{
|
||||
[SerializeField] [Min(0.1f)] private float _defaultSpeed = 12f;
|
||||
[SerializeField] [Min(0.02f)] private float _hitDistance = 0.15f;
|
||||
[SerializeField] [Min(0.1f)] private float _maxLifetime = 3f;
|
||||
|
||||
private Transform _target;
|
||||
private float _speed;
|
||||
private AttackPayload _attackPayload;
|
||||
private float _lifetime;
|
||||
private float _runtimeMaxLifetime;
|
||||
private bool _isRunning;
|
||||
private bool _despawnRequested;
|
||||
|
||||
public void OnShow(BulletData bulletData)
|
||||
{
|
||||
_target = bulletData != null ? bulletData.Target : null;
|
||||
_attackPayload = bulletData?.AttackPayload?.Clone() ?? new AttackPayload();
|
||||
_speed = bulletData != null && bulletData.Speed > 0f ? bulletData.Speed : _defaultSpeed;
|
||||
_runtimeMaxLifetime = bulletData != null && bulletData.MaxLifetime > 0f ? bulletData.MaxLifetime : _maxLifetime;
|
||||
_lifetime = 0f;
|
||||
_despawnRequested = false;
|
||||
_isRunning = _target != null;
|
||||
if (_isRunning)
|
||||
{
|
||||
Vector2 initialDirection = (Vector2)(_target.position - transform.position);
|
||||
RotateToDirection(initialDirection);
|
||||
}
|
||||
|
||||
if (!_isRunning)
|
||||
{
|
||||
_despawnRequested = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnReset()
|
||||
{
|
||||
_target = null;
|
||||
_speed = 0f;
|
||||
_attackPayload = new AttackPayload();
|
||||
_lifetime = 0f;
|
||||
_runtimeMaxLifetime = _maxLifetime;
|
||||
_isRunning = false;
|
||||
_despawnRequested = false;
|
||||
}
|
||||
|
||||
public void Tick(float deltaTime)
|
||||
{
|
||||
if (!_isRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lifetime += Mathf.Max(0f, deltaTime);
|
||||
if (_lifetime >= _runtimeMaxLifetime || _target == null)
|
||||
{
|
||||
_isRunning = false;
|
||||
_despawnRequested = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 currentPosition = transform.position;
|
||||
Vector2 toTarget = (Vector2)(_target.position - currentPosition);
|
||||
float hitDistanceSquared = _hitDistance * _hitDistance;
|
||||
if (toTarget.sqrMagnitude <= hitDistanceSquared)
|
||||
{
|
||||
HitTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 direction = toTarget.normalized;
|
||||
RotateToDirection(direction);
|
||||
float moveDistance = _speed * Mathf.Max(0f, deltaTime);
|
||||
if (moveDistance * moveDistance >= toTarget.sqrMagnitude)
|
||||
{
|
||||
Vector3 targetPosition = _target.position;
|
||||
transform.position = new Vector3(targetPosition.x, targetPosition.y, currentPosition.z);
|
||||
HitTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
transform.position += (Vector3)(direction * moveDistance);
|
||||
}
|
||||
|
||||
public bool TryConsumeDespawnRequest()
|
||||
{
|
||||
if (!_despawnRequested)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_despawnRequested = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HitTarget()
|
||||
{
|
||||
if (_target == null)
|
||||
{
|
||||
_isRunning = false;
|
||||
_despawnRequested = true;
|
||||
return;
|
||||
}
|
||||
|
||||
MonoBehaviour[] components = _target.GetComponentsInParent<MonoBehaviour>();
|
||||
foreach (var mono in components)
|
||||
{
|
||||
if (mono is IDamageReceiver damageReceiver)
|
||||
{
|
||||
damageReceiver.TakeDamage(_attackPayload);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_isRunning = false;
|
||||
_despawnRequested = true;
|
||||
}
|
||||
|
||||
private void RotateToDirection(Vector2 direction)
|
||||
{
|
||||
if (direction.sqrMagnitude <= Mathf.Epsilon)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float targetZAngle = Vector2.SignedAngle(Vector2.up, direction);
|
||||
transform.rotation = Quaternion.Euler(0f, 0f, targetZAngle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using GeometryTD;
|
||||
using GeometryTD.Entity;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Components
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class ShooterMuzzleComp : MonoBehaviour
|
||||
{
|
||||
[SerializeField] [Min(1f)] private int _attackDamage = 100;
|
||||
|
||||
[SerializeField] private AttackMethodType _attackMethodType = AttackMethodType.NormalBullet;
|
||||
|
||||
[SerializeField] [Min(1)] private int _bulletTypeId = 501;
|
||||
|
||||
[SerializeField] private Transform _muzzlePoint;
|
||||
|
||||
[SerializeField] [Min(0.1f)] private float _bulletSpeed = 12f;
|
||||
|
||||
[SerializeField] private SpriteRenderer _renderer;
|
||||
|
||||
private TagRuntimeData[] _tagRuntimes = System.Array.Empty<TagRuntimeData>();
|
||||
|
||||
public int AttackDamage => _attackDamage;
|
||||
public AttackMethodType AttackMethodType => _attackMethodType;
|
||||
|
||||
public void OnInit(
|
||||
int attackDamage,
|
||||
AttackMethodType attackMethodType = AttackMethodType.NormalBullet,
|
||||
int bulletTypeId = 501,
|
||||
TagRuntimeData[] tagRuntimes = null)
|
||||
{
|
||||
_attackDamage = Mathf.Max(1, attackDamage);
|
||||
_attackMethodType = attackMethodType;
|
||||
_bulletTypeId = Mathf.Max(1, bulletTypeId);
|
||||
_tagRuntimes = CloneTagRuntimes(tagRuntimes);
|
||||
}
|
||||
|
||||
public void OnReset()
|
||||
{
|
||||
_attackDamage = 1;
|
||||
_attackMethodType = AttackMethodType.None;
|
||||
_bulletTypeId = 501;
|
||||
_tagRuntimes = System.Array.Empty<TagRuntimeData>();
|
||||
}
|
||||
|
||||
public void SetColor(Color color)
|
||||
{
|
||||
if (_renderer != null)
|
||||
{
|
||||
_renderer.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Attack(Transform target)
|
||||
{
|
||||
return Attack(target, AttackPropertyType.None);
|
||||
}
|
||||
|
||||
public bool Attack(Transform target, AttackPropertyType attackPropertyType)
|
||||
{
|
||||
if (_attackMethodType != AttackMethodType.NormalBullet)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Transform spawnPoint = _muzzlePoint != null ? _muzzlePoint : transform;
|
||||
int bulletEntityId = GameEntry.Entity.GenerateSerialId();
|
||||
AttackPayload attackPayload = new AttackPayload
|
||||
{
|
||||
BaseDamage = _attackDamage,
|
||||
AttackPropertyType = attackPropertyType,
|
||||
SourceEntityId = ResolveSourceEntityId(),
|
||||
ProjectileEntityId = bulletEntityId,
|
||||
OriginPosition = spawnPoint.position,
|
||||
TagRuntimes = CloneTagRuntimes(_tagRuntimes)
|
||||
};
|
||||
BulletData bulletData = new BulletData(
|
||||
bulletEntityId,
|
||||
_bulletTypeId,
|
||||
spawnPoint.position,
|
||||
target,
|
||||
attackPayload,
|
||||
_bulletSpeed);
|
||||
GameEntry.Entity.ShowBullet(bulletData);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static TagRuntimeData[] CloneTagRuntimes(TagRuntimeData[] tagRuntimes)
|
||||
{
|
||||
if (tagRuntimes == null || tagRuntimes.Length <= 0)
|
||||
{
|
||||
return System.Array.Empty<TagRuntimeData>();
|
||||
}
|
||||
|
||||
TagRuntimeData[] cloned = new TagRuntimeData[tagRuntimes.Length];
|
||||
for (int i = 0; i < tagRuntimes.Length; i++)
|
||||
{
|
||||
TagRuntimeData runtime = tagRuntimes[i];
|
||||
if (runtime == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
cloned[i] = new TagRuntimeData
|
||||
{
|
||||
TagType = runtime.TagType,
|
||||
TotalStack = runtime.TotalStack
|
||||
};
|
||||
}
|
||||
|
||||
return cloned;
|
||||
}
|
||||
|
||||
private int ResolveSourceEntityId()
|
||||
{
|
||||
TowerEntity towerEntity = GetComponentInParent<TowerEntity>();
|
||||
return towerEntity != null ? towerEntity.Id : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,359 @@
|
|||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Components
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class TowerController : MonoBehaviour
|
||||
{
|
||||
private const string AttackRangeIndicatorObjectName = "AttackRangeIndicator";
|
||||
private const int MinTowerLevel = 0;
|
||||
private const int MaxTowerLevel = 4;
|
||||
|
||||
private static Material _attackRangeSharedMaterial;
|
||||
|
||||
[SerializeField] private ShooterMuzzleComp _muzzleComp;
|
||||
[SerializeField] private BasicBearingComp _bearingComp;
|
||||
[SerializeField] private BasicBaseComp _baseComp;
|
||||
[SerializeField] private Transform _scanOrigin;
|
||||
[SerializeField] [Min(0.02f)] private float _retargetInterval = 0.1f;
|
||||
[SerializeField] private LineRenderer _attackRangeRenderer;
|
||||
[SerializeField] [Min(12)] private int _attackRangeSegments = 64;
|
||||
[SerializeField] [Min(0.005f)] private float _attackRangeLineWidth = 0.08f;
|
||||
[SerializeField] private Color _attackRangeColor = new Color(0.1f, 1f, 0.4f, 0.8f);
|
||||
[SerializeField] private float _attackRangeZOffset = -0.01f;
|
||||
|
||||
private Transform _currentTarget;
|
||||
private float _retargetTimer;
|
||||
private float _attackRange;
|
||||
private int _towerLevel;
|
||||
private Color _muzzleColor = Color.white;
|
||||
private Color _bearingColor = Color.white;
|
||||
private Color _baseColor = Color.white;
|
||||
private TagRuntimeData[] _tagRuntimes = System.Array.Empty<TagRuntimeData>();
|
||||
|
||||
public Transform CurrentTarget => _currentTarget;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ResolveComponents();
|
||||
}
|
||||
|
||||
public void OnInit(TowerStatsData stats)
|
||||
{
|
||||
OnInit(stats, MinTowerLevel);
|
||||
}
|
||||
|
||||
public void OnInit(TowerStatsData stats, int towerLevel, Color muzzleColor, Color bearingColor, Color baseColor)
|
||||
{
|
||||
SetComponentColors(muzzleColor, bearingColor, baseColor);
|
||||
OnInit(stats, towerLevel);
|
||||
}
|
||||
|
||||
public void OnInit(TowerStatsData stats, int towerLevel)
|
||||
{
|
||||
ResolveComponents();
|
||||
if (stats == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_towerLevel = Mathf.Clamp(towerLevel, MinTowerLevel, MaxTowerLevel);
|
||||
int attackDamage = ResolveIntValue(stats.AttackDamage, _towerLevel, 1, 1);
|
||||
float rotateSpeed = ResolveFloatValue(stats.RotateSpeed, _towerLevel, 180f, 1f);
|
||||
float attackRange = ResolveFloatValue(stats.AttackRange, _towerLevel, 5f, 0.1f);
|
||||
float attackSpeed = ResolveFloatValue(stats.AttackSpeed, _towerLevel, 1f, 0.01f);
|
||||
_tagRuntimes = ResolveRuntimeTags(stats);
|
||||
|
||||
_muzzleComp?.OnInit(attackDamage, stats.AttackMethodType, tagRuntimes: _tagRuntimes);
|
||||
_bearingComp?.OnInit(rotateSpeed, attackRange);
|
||||
_baseComp?.OnInit(attackSpeed, stats.AttackPropertyType);
|
||||
ApplyComponentColors();
|
||||
SetAttackRange(attackRange);
|
||||
SetAttackRangeVisible(false);
|
||||
_currentTarget = null;
|
||||
_retargetTimer = 0f;
|
||||
}
|
||||
|
||||
public void OnReset()
|
||||
{
|
||||
SetAttackRangeVisible(false);
|
||||
_currentTarget = null;
|
||||
_retargetTimer = 0f;
|
||||
_towerLevel = MinTowerLevel;
|
||||
_tagRuntimes = System.Array.Empty<TagRuntimeData>();
|
||||
_muzzleComp?.OnReset();
|
||||
_bearingComp?.OnReset();
|
||||
_baseComp?.OnReset();
|
||||
}
|
||||
|
||||
public void SetAttackRangeVisible(bool visible)
|
||||
{
|
||||
EnsureAttackRangeRenderer();
|
||||
if (_attackRangeRenderer != null)
|
||||
{
|
||||
_attackRangeRenderer.enabled = visible;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAttackRange(float range)
|
||||
{
|
||||
_attackRange = Mathf.Max(0.05f, range);
|
||||
EnsureAttackRangeRenderer();
|
||||
RebuildAttackRangeGeometry();
|
||||
}
|
||||
|
||||
public void SetComponentColors(Color muzzleColor, Color bearingColor, Color baseColor)
|
||||
{
|
||||
_muzzleColor = muzzleColor;
|
||||
_bearingColor = bearingColor;
|
||||
_baseColor = baseColor;
|
||||
ApplyComponentColors();
|
||||
}
|
||||
|
||||
public void SetTarget(Transform target)
|
||||
{
|
||||
_currentTarget = target;
|
||||
}
|
||||
|
||||
public void ClearTarget()
|
||||
{
|
||||
_currentTarget = null;
|
||||
}
|
||||
|
||||
public void OnUpdate(float deltaTime)
|
||||
{
|
||||
if (!HasCoreComponents())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsTargetValid(_currentTarget) || !_bearingComp.IsTargetInRange(_currentTarget, _scanOrigin))
|
||||
{
|
||||
TryRetarget(deltaTime);
|
||||
}
|
||||
|
||||
if (_currentTarget == null)
|
||||
{
|
||||
_baseComp.Tick(deltaTime);
|
||||
return;
|
||||
}
|
||||
|
||||
_baseComp.TryAttack(_bearingComp, _muzzleComp, _currentTarget, deltaTime);
|
||||
}
|
||||
|
||||
private void TryRetarget(float deltaTime)
|
||||
{
|
||||
_retargetTimer -= Mathf.Max(0f, deltaTime);
|
||||
if (_retargetTimer > 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_retargetTimer = _retargetInterval;
|
||||
_currentTarget = FindNearestEnemyTarget();
|
||||
}
|
||||
|
||||
private Transform FindNearestEnemyTarget()
|
||||
{
|
||||
Vector3 origin = _scanOrigin != null ? _scanOrigin.position : transform.position;
|
||||
float maxRange = _bearingComp.AttackRange;
|
||||
float maxRangeSquared = maxRange * maxRange;
|
||||
|
||||
EnemyEntity bestEnemy = null;
|
||||
float bestDistanceSquared = float.MaxValue;
|
||||
|
||||
foreach (var enemy in EnemyEntity.ActiveEnemies)
|
||||
{
|
||||
if (enemy == null || !enemy.isActiveAndEnabled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Transform enemyTransform = enemy.transform;
|
||||
Vector3 delta = enemyTransform.position - origin;
|
||||
float distanceSquared = delta.sqrMagnitude;
|
||||
if (distanceSquared > maxRangeSquared || distanceSquared >= bestDistanceSquared)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistanceSquared = distanceSquared;
|
||||
bestEnemy = enemy;
|
||||
}
|
||||
|
||||
return bestEnemy != null ? bestEnemy.transform : null;
|
||||
}
|
||||
|
||||
private void ApplyComponentColors()
|
||||
{
|
||||
_muzzleComp?.SetColor(_muzzleColor);
|
||||
_bearingComp?.SetColor(_bearingColor);
|
||||
_baseComp?.SetColor(_baseColor);
|
||||
}
|
||||
|
||||
private void ResolveComponents()
|
||||
{
|
||||
if (_muzzleComp == null)
|
||||
{
|
||||
_muzzleComp = GetComponent<ShooterMuzzleComp>();
|
||||
}
|
||||
|
||||
if (_bearingComp == null)
|
||||
{
|
||||
_bearingComp = GetComponent<BasicBearingComp>();
|
||||
}
|
||||
|
||||
if (_baseComp == null)
|
||||
{
|
||||
_baseComp = GetComponent<BasicBaseComp>();
|
||||
}
|
||||
|
||||
ApplyComponentColors();
|
||||
EnsureAttackRangeRenderer();
|
||||
}
|
||||
|
||||
private bool HasCoreComponents()
|
||||
{
|
||||
return _muzzleComp != null && _bearingComp != null && _baseComp != null;
|
||||
}
|
||||
|
||||
private static bool IsTargetValid(Transform target)
|
||||
{
|
||||
return target != null && target.gameObject.activeInHierarchy;
|
||||
}
|
||||
|
||||
private static TagRuntimeData[] CloneTagRuntimes(TagRuntimeData[] source)
|
||||
{
|
||||
if (source == null || source.Length <= 0)
|
||||
{
|
||||
return System.Array.Empty<TagRuntimeData>();
|
||||
}
|
||||
|
||||
TagRuntimeData[] cloned = new TagRuntimeData[source.Length];
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
TagRuntimeData runtime = source[i];
|
||||
if (runtime == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
cloned[i] = new TagRuntimeData
|
||||
{
|
||||
TagType = runtime.TagType,
|
||||
TotalStack = runtime.TotalStack
|
||||
};
|
||||
}
|
||||
|
||||
return cloned;
|
||||
}
|
||||
|
||||
private static TagRuntimeData[] ResolveRuntimeTags(TowerStatsData stats)
|
||||
{
|
||||
if (stats == null)
|
||||
{
|
||||
return System.Array.Empty<TagRuntimeData>();
|
||||
}
|
||||
|
||||
if (stats.TagRuntimes != null && stats.TagRuntimes.Length > 0)
|
||||
{
|
||||
return CloneTagRuntimes(stats.TagRuntimes);
|
||||
}
|
||||
|
||||
return TowerTagAggregationService.BuildRuntimeTagsFromUniqueTags(stats.Tags);
|
||||
}
|
||||
|
||||
private static int ResolveIntValue(int[] values, int level, int fallback, int minValue)
|
||||
{
|
||||
int resolved = Mathf.Max(minValue, fallback);
|
||||
if (values == null || values.Length <= 0)
|
||||
{
|
||||
return resolved;
|
||||
}
|
||||
|
||||
int index = Mathf.Clamp(level, 0, values.Length - 1);
|
||||
return Mathf.Max(minValue, values[index]);
|
||||
}
|
||||
|
||||
private static float ResolveFloatValue(float[] values, int level, float fallback, float minValue)
|
||||
{
|
||||
float resolved = Mathf.Max(minValue, fallback);
|
||||
if (values == null || values.Length <= 0)
|
||||
{
|
||||
return resolved;
|
||||
}
|
||||
|
||||
int index = Mathf.Clamp(level, 0, values.Length - 1);
|
||||
return Mathf.Max(minValue, values[index]);
|
||||
}
|
||||
|
||||
private void EnsureAttackRangeRenderer()
|
||||
{
|
||||
if (_attackRangeRenderer == null)
|
||||
{
|
||||
Transform indicatorTransform = transform.Find(AttackRangeIndicatorObjectName);
|
||||
if (indicatorTransform == null)
|
||||
{
|
||||
GameObject indicatorObject = new GameObject(AttackRangeIndicatorObjectName);
|
||||
indicatorTransform = indicatorObject.transform;
|
||||
indicatorTransform.SetParent(transform, false);
|
||||
}
|
||||
|
||||
_attackRangeRenderer = indicatorTransform.GetComponent<LineRenderer>();
|
||||
if (_attackRangeRenderer == null)
|
||||
{
|
||||
_attackRangeRenderer = indicatorTransform.gameObject.AddComponent<LineRenderer>();
|
||||
}
|
||||
}
|
||||
|
||||
_attackRangeRenderer.useWorldSpace = false;
|
||||
_attackRangeRenderer.loop = true;
|
||||
_attackRangeRenderer.positionCount = Mathf.Max(12, _attackRangeSegments);
|
||||
_attackRangeRenderer.widthMultiplier = _attackRangeLineWidth;
|
||||
_attackRangeRenderer.startColor = _attackRangeColor;
|
||||
_attackRangeRenderer.endColor = _attackRangeColor;
|
||||
_attackRangeRenderer.numCapVertices = 4;
|
||||
_attackRangeRenderer.numCornerVertices = 4;
|
||||
_attackRangeRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
_attackRangeRenderer.receiveShadows = false;
|
||||
_attackRangeRenderer.allowOcclusionWhenDynamic = false;
|
||||
_attackRangeRenderer.enabled = false;
|
||||
|
||||
if (_attackRangeSharedMaterial == null)
|
||||
{
|
||||
Shader lineShader = Shader.Find("Sprites/Default");
|
||||
if (lineShader != null)
|
||||
{
|
||||
_attackRangeSharedMaterial = new Material(lineShader);
|
||||
}
|
||||
}
|
||||
|
||||
if (_attackRangeSharedMaterial != null)
|
||||
{
|
||||
_attackRangeRenderer.sharedMaterial = _attackRangeSharedMaterial;
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildAttackRangeGeometry()
|
||||
{
|
||||
if (_attackRangeRenderer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int segmentCount = Mathf.Max(12, _attackRangeSegments);
|
||||
_attackRangeRenderer.positionCount = segmentCount;
|
||||
|
||||
float stepAngle = Mathf.PI * 2f / segmentCount;
|
||||
for (int i = 0; i < segmentCount; i++)
|
||||
{
|
||||
float angle = stepAngle * i;
|
||||
float x = Mathf.Cos(angle) * _attackRange;
|
||||
float y = Mathf.Sin(angle) * _attackRange;
|
||||
_attackRangeRenderer.SetPosition(i, new Vector3(x, y, _attackRangeZOffset));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
//------------------------------------------------------------
|
||||
// Game Framework
|
||||
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
|
||||
// Homepage: https://gameframework.cn/
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
|
||||
using GameFramework;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class BuiltinDataComponent : GameFrameworkComponent
|
||||
{
|
||||
[SerializeField] private TextAsset _buildInfoTextAsset = null;
|
||||
|
||||
[SerializeField] private UpdateResourceForm _updateResourceFormTemplate = null;
|
||||
|
||||
private BuildInfo _buildInfo = null;
|
||||
|
||||
public BuildInfo BuildInfo => _buildInfo;
|
||||
|
||||
public UpdateResourceForm UpdateResourceFormTemplate => _updateResourceFormTemplate;
|
||||
|
||||
public void InitBuildInfo()
|
||||
{
|
||||
if (_buildInfoTextAsset == null || string.IsNullOrEmpty(_buildInfoTextAsset.text))
|
||||
{
|
||||
Log.Info("Build info can not be found or empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
_buildInfo = Utility.Json.ToObject<BuildInfo>(_buildInfoTextAsset.text);
|
||||
if (_buildInfo == null)
|
||||
{
|
||||
Log.Warning("Parse build info failure.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,396 @@
|
|||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Procedure;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// 鎴樻枟鑺傜偣缁勪欢
|
||||
/// </summary>
|
||||
public class CombatNodeComponent : GameFrameworkComponent
|
||||
{
|
||||
// Level.Id => Level
|
||||
private readonly Dictionary<int, DRLevel> _levelsById = new();
|
||||
|
||||
// LevelId => LevelPhases
|
||||
private readonly Dictionary<int, List<DRLevelPhase>> _phasesByLevelId = new();
|
||||
|
||||
// LevelPhase.Id => LevelSpawnEntries
|
||||
private readonly Dictionary<int, List<DRLevelSpawnEntry>> _spawnEntriesByPhaseId = new();
|
||||
private readonly Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>> _selectedSpawnEntriesByPhaseId = new();
|
||||
private readonly List<int> _levelIdBuffer = new();
|
||||
private readonly CombatScheduler _combatScheduler = new CombatScheduler();
|
||||
|
||||
private bool _runtimeInitialized;
|
||||
private bool _isCombatActive;
|
||||
private bool _lastCombatSucceeded = true;
|
||||
|
||||
public LevelThemeType CurrentThemeType { get; private set; }
|
||||
public DRLevel CurrentLevel { get; private set; }
|
||||
public int CurrentCoin => _isCombatActive ? _combatScheduler.CurrentCoin : 0;
|
||||
public int CurrentGold => _isCombatActive ? _combatScheduler.CurrentGold : 0;
|
||||
public int CurrentBaseHp => _isCombatActive ? _combatScheduler.CurrentBaseHp : 0;
|
||||
public bool LastCombatSucceeded => _lastCombatSucceeded;
|
||||
public bool CanEndCombat => _combatScheduler.CanEndCombat;
|
||||
public int CurrentBuildTowerCount => _isCombatActive ? _combatScheduler.CurrentBuildTowerCount : 0;
|
||||
public int LastDefeatedEnemyCount { get; private set; }
|
||||
public int LastGainedCoin { get; private set; }
|
||||
public int LastGainedGold { get; private set; }
|
||||
|
||||
public int CurrentLevelPhaseCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (CurrentLevel == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!_phasesByLevelId.TryGetValue(CurrentLevel.Id, out List<DRLevelPhase> phases) || phases == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return phases.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public int CurrentPhaseIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
return _combatScheduler.DisplayPhaseIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnInit(LevelThemeType themeType)
|
||||
{
|
||||
ShutdownCombatRuntime();
|
||||
|
||||
CurrentThemeType = themeType;
|
||||
CurrentLevel = null;
|
||||
_isCombatActive = false;
|
||||
_lastCombatSucceeded = true;
|
||||
LastDefeatedEnemyCount = 0;
|
||||
LastGainedCoin = 0;
|
||||
LastGainedGold = 0;
|
||||
_levelsById.Clear();
|
||||
_phasesByLevelId.Clear();
|
||||
_spawnEntriesByPhaseId.Clear();
|
||||
_selectedSpawnEntriesByPhaseId.Clear();
|
||||
_levelIdBuffer.Clear();
|
||||
|
||||
IDataTable<DRLevel> dtLevel = GameEntry.DataTable.GetDataTable<DRLevel>();
|
||||
IDataTable<DRLevelPhase> dtLevelPhase = GameEntry.DataTable.GetDataTable<DRLevelPhase>();
|
||||
IDataTable<DRLevelSpawnEntry> dtSpawnEntry = GameEntry.DataTable.GetDataTable<DRLevelSpawnEntry>();
|
||||
if (dtLevel == null || dtLevelPhase == null || dtSpawnEntry == null)
|
||||
{
|
||||
Log.Warning("CombatNodeComponent init failed. Missing data table(s).");
|
||||
return;
|
||||
}
|
||||
|
||||
DRLevel[] levels = dtLevel.GetAllDataRows();
|
||||
for (int i = 0; i < levels.Length; i++)
|
||||
{
|
||||
DRLevel level = levels[i];
|
||||
if (level.LevelThemeType != themeType)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_levelsById[level.Id] = level;
|
||||
_phasesByLevelId[level.Id] = new List<DRLevelPhase>();
|
||||
_levelIdBuffer.Add(level.Id);
|
||||
}
|
||||
|
||||
DRLevelPhase[] levelPhases = dtLevelPhase.GetAllDataRows();
|
||||
foreach (var phase in levelPhases)
|
||||
{
|
||||
int levelId = phase.Id / 1000;
|
||||
if (!_levelsById.ContainsKey(levelId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_phasesByLevelId.TryGetValue(levelId, out List<DRLevelPhase> phases))
|
||||
{
|
||||
phases = new List<DRLevelPhase>();
|
||||
_phasesByLevelId[levelId] = phases;
|
||||
}
|
||||
|
||||
phases.Add(phase);
|
||||
_spawnEntriesByPhaseId[phase.Id] = new List<DRLevelSpawnEntry>();
|
||||
}
|
||||
|
||||
DRLevelSpawnEntry[] spawnEntries = dtSpawnEntry.GetAllDataRows();
|
||||
foreach (var spawnEntry in spawnEntries)
|
||||
{
|
||||
int phaseId = spawnEntry.Id / 1000;
|
||||
int levelId = phaseId / 1000;
|
||||
if (!_levelsById.ContainsKey(levelId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_spawnEntriesByPhaseId.TryGetValue(phaseId, out List<DRLevelSpawnEntry> entries))
|
||||
{
|
||||
entries = new List<DRLevelSpawnEntry>();
|
||||
_spawnEntriesByPhaseId[phaseId] = entries;
|
||||
}
|
||||
|
||||
entries.Add(spawnEntry);
|
||||
}
|
||||
|
||||
foreach (List<DRLevelPhase> phaseList in _phasesByLevelId.Values)
|
||||
{
|
||||
phaseList.Sort((left, right) => left.Id.CompareTo(right.Id));
|
||||
}
|
||||
|
||||
foreach (List<DRLevelSpawnEntry> phaseSpawnEntries in _spawnEntriesByPhaseId.Values)
|
||||
{
|
||||
phaseSpawnEntries.Sort((left, right) =>
|
||||
{
|
||||
int timeCompare = left.StartTime.CompareTo(right.StartTime);
|
||||
if (timeCompare != 0)
|
||||
{
|
||||
return timeCompare;
|
||||
}
|
||||
|
||||
return left.Id.CompareTo(right.Id);
|
||||
});
|
||||
}
|
||||
|
||||
Log.Info(
|
||||
"CombatNodeComponent initialized. Theme={0}, Levels={1}, Phases={2}, Entries={3}.",
|
||||
themeType,
|
||||
_levelsById.Count,
|
||||
CountPhases(),
|
||||
CountEntries());
|
||||
|
||||
EnsureCombatRuntimeInitialized();
|
||||
}
|
||||
|
||||
public void StartCombat(
|
||||
int levelId = 0,
|
||||
RunNodeExecutionContext context = null)
|
||||
{
|
||||
if (!EnsureCombatRuntimeInitialized())
|
||||
{
|
||||
Log.Warning("CombatNodeComponent start failed. Missing scheduler runtime.");
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasLevel = levelId > 0
|
||||
? TryGetLevelById(levelId, out DRLevel selectedLevel)
|
||||
: TrySelectRandomLevel(out selectedLevel);
|
||||
if (!hasLevel)
|
||||
{
|
||||
Log.Warning(
|
||||
"CombatNodeComponent start failed. LevelId={0}. Missing cached level data or invalid level id.",
|
||||
levelId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_phasesByLevelId.TryGetValue(selectedLevel.Id, out List<DRLevelPhase> phaseList) ||
|
||||
phaseList == null ||
|
||||
phaseList.Count <= 0)
|
||||
{
|
||||
Log.Warning("CombatNodeComponent start failed. Level '{0}' has no phase data.", selectedLevel.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedSpawnEntriesByPhaseId.Clear();
|
||||
foreach (var phase in phaseList)
|
||||
{
|
||||
if (_spawnEntriesByPhaseId.TryGetValue(phase.Id, out List<DRLevelSpawnEntry> entries) &&
|
||||
entries != null)
|
||||
{
|
||||
_selectedSpawnEntriesByPhaseId[phase.Id] = entries;
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedSpawnEntriesByPhaseId[phase.Id] = new List<DRLevelSpawnEntry>();
|
||||
}
|
||||
}
|
||||
|
||||
CurrentLevel = selectedLevel;
|
||||
_isCombatActive = false;
|
||||
_lastCombatSucceeded = true;
|
||||
LastDefeatedEnemyCount = 0;
|
||||
LastGainedCoin = 0;
|
||||
LastGainedGold = 0;
|
||||
if (!_combatScheduler.Start(
|
||||
selectedLevel,
|
||||
phaseList,
|
||||
_selectedSpawnEntriesByPhaseId,
|
||||
context))
|
||||
{
|
||||
CurrentLevel = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_isCombatActive = true;
|
||||
}
|
||||
|
||||
public void EndCombat()
|
||||
{
|
||||
if (!_isCombatActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isCombatActive = false;
|
||||
LastDefeatedEnemyCount = _combatScheduler.DefeatedEnemyCount;
|
||||
LastGainedCoin = _combatScheduler.GainedCoin;
|
||||
LastGainedGold = _combatScheduler.GainedGold;
|
||||
CurrentLevel = null;
|
||||
}
|
||||
|
||||
public bool TryEndCombatByPlayer()
|
||||
{
|
||||
return _combatScheduler.TryEndCombatByPlayer();
|
||||
}
|
||||
|
||||
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
if (!_runtimeInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_combatScheduler.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
}
|
||||
|
||||
public void OnShutdown()
|
||||
{
|
||||
_isCombatActive = false;
|
||||
CurrentLevel = null;
|
||||
_lastCombatSucceeded = true;
|
||||
LastDefeatedEnemyCount = 0;
|
||||
LastGainedCoin = 0;
|
||||
LastGainedGold = 0;
|
||||
ShutdownCombatRuntime();
|
||||
}
|
||||
|
||||
public void OnCombatEndedByScheduler(bool succeeded)
|
||||
{
|
||||
_lastCombatSucceeded = succeeded;
|
||||
EndCombat();
|
||||
}
|
||||
|
||||
public bool TryConsumeCoin(int coin)
|
||||
{
|
||||
if (Mathf.Max(0, coin) <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_isCombatActive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _combatScheduler.TryConsumeCoin(coin);
|
||||
}
|
||||
|
||||
public bool TryGetBuildTowerStats(int buildIndex, out TowerStatsData stats)
|
||||
{
|
||||
if (!_isCombatActive)
|
||||
{
|
||||
stats = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return _combatScheduler.TryGetBuildTowerStats(buildIndex, out stats);
|
||||
}
|
||||
|
||||
public void AddCoin(int coin)
|
||||
{
|
||||
if (!_isCombatActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_combatScheduler.AddCoin(coin);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
OnShutdown();
|
||||
}
|
||||
|
||||
private bool EnsureCombatRuntimeInitialized()
|
||||
{
|
||||
if (_runtimeInitialized)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_combatScheduler.OnInit(OnCombatEndedByScheduler);
|
||||
_runtimeInitialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ShutdownCombatRuntime()
|
||||
{
|
||||
if (!_runtimeInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_combatScheduler.OnDestroy();
|
||||
_runtimeInitialized = false;
|
||||
}
|
||||
|
||||
private bool TrySelectRandomLevel(out DRLevel level)
|
||||
{
|
||||
level = null;
|
||||
if (_levelIdBuffer.Count <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int selectedIndex = Random.Range(0, _levelIdBuffer.Count);
|
||||
int selectedLevelId = _levelIdBuffer[selectedIndex];
|
||||
return _levelsById.TryGetValue(selectedLevelId, out level);
|
||||
}
|
||||
|
||||
private bool TryGetLevelById(int levelId, out DRLevel level)
|
||||
{
|
||||
level = null;
|
||||
if (levelId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _levelsById.TryGetValue(levelId, out level);
|
||||
}
|
||||
|
||||
private int CountPhases()
|
||||
{
|
||||
int count = 0;
|
||||
foreach (List<DRLevelPhase> phases in _phasesByLevelId.Values)
|
||||
{
|
||||
count += phases.Count;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private int CountEntries()
|
||||
{
|
||||
int count = 0;
|
||||
foreach (List<DRLevelSpawnEntry> list in _spawnEntriesByPhaseId.Values)
|
||||
{
|
||||
count += list.Count;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
using System;
|
||||
using GameFramework.Event;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatEventBridge
|
||||
{
|
||||
private Action<ShowEntitySuccessEventArgs> _onShowEntitySuccess;
|
||||
private Action<ShowEntityFailureEventArgs> _onShowEntityFailure;
|
||||
private Action<HideEntityCompleteEventArgs> _onHideEntityComplete;
|
||||
private Action<OpenUIFormSuccessEventArgs> _onOpenUIFormSuccess;
|
||||
private Action<OpenUIFormFailureEventArgs> _onOpenUIFormFailure;
|
||||
private Action<CloseUIFormCompleteEventArgs> _onCloseUIFormComplete;
|
||||
|
||||
private bool _isEntityEventSubscribed;
|
||||
private bool _isUIEventSubscribed;
|
||||
|
||||
public void Bind(
|
||||
Action<ShowEntitySuccessEventArgs> onShowEntitySuccess,
|
||||
Action<ShowEntityFailureEventArgs> onShowEntityFailure,
|
||||
Action<HideEntityCompleteEventArgs> onHideEntityComplete,
|
||||
Action<OpenUIFormSuccessEventArgs> onOpenUIFormSuccess,
|
||||
Action<OpenUIFormFailureEventArgs> onOpenUIFormFailure,
|
||||
Action<CloseUIFormCompleteEventArgs> onCloseUIFormComplete)
|
||||
{
|
||||
_onShowEntitySuccess = onShowEntitySuccess;
|
||||
_onShowEntityFailure = onShowEntityFailure;
|
||||
_onHideEntityComplete = onHideEntityComplete;
|
||||
_onOpenUIFormSuccess = onOpenUIFormSuccess;
|
||||
_onOpenUIFormFailure = onOpenUIFormFailure;
|
||||
_onCloseUIFormComplete = onCloseUIFormComplete;
|
||||
|
||||
EnsureEntityEventSubscribed();
|
||||
EnsureUIEventSubscribed();
|
||||
}
|
||||
|
||||
public void Unbind()
|
||||
{
|
||||
UnsubscribeEntityEvents();
|
||||
UnsubscribeUIEvents();
|
||||
|
||||
_onShowEntitySuccess = null;
|
||||
_onShowEntityFailure = null;
|
||||
_onHideEntityComplete = null;
|
||||
_onOpenUIFormSuccess = null;
|
||||
_onOpenUIFormFailure = null;
|
||||
_onCloseUIFormComplete = null;
|
||||
}
|
||||
|
||||
private void EnsureEntityEventSubscribed()
|
||||
{
|
||||
if (_isEntityEventSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||
GameEntry.Event.Subscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
|
||||
GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||
_isEntityEventSubscribed = true;
|
||||
}
|
||||
|
||||
private void EnsureUIEventSubscribed()
|
||||
{
|
||||
if (_isUIEventSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OnOpenUIFormSuccess);
|
||||
GameEntry.Event.Subscribe(OpenUIFormFailureEventArgs.EventId, OnOpenUIFormFailure);
|
||||
GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, OnCloseUIFormComplete);
|
||||
_isUIEventSubscribed = true;
|
||||
}
|
||||
|
||||
private void UnsubscribeEntityEvents()
|
||||
{
|
||||
if (!_isEntityEventSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||
GameEntry.Event.Unsubscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
|
||||
GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||
_isEntityEventSubscribed = false;
|
||||
}
|
||||
|
||||
private void UnsubscribeUIEvents()
|
||||
{
|
||||
if (!_isUIEventSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OnOpenUIFormSuccess);
|
||||
GameEntry.Event.Unsubscribe(OpenUIFormFailureEventArgs.EventId, OnOpenUIFormFailure);
|
||||
GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, OnCloseUIFormComplete);
|
||||
_isUIEventSubscribed = false;
|
||||
}
|
||||
|
||||
private void OnShowEntitySuccess(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is ShowEntitySuccessEventArgs args)
|
||||
{
|
||||
_onShowEntitySuccess?.Invoke(args);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShowEntityFailure(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is ShowEntityFailureEventArgs args)
|
||||
{
|
||||
_onShowEntityFailure?.Invoke(args);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHideEntityComplete(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is HideEntityCompleteEventArgs args)
|
||||
{
|
||||
_onHideEntityComplete?.Invoke(args);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOpenUIFormSuccess(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is OpenUIFormSuccessEventArgs args)
|
||||
{
|
||||
_onOpenUIFormSuccess?.Invoke(args);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOpenUIFormFailure(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is OpenUIFormFailureEventArgs args)
|
||||
{
|
||||
_onOpenUIFormFailure?.Invoke(args);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCloseUIFormComplete(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is CloseUIFormCompleteEventArgs args)
|
||||
{
|
||||
_onCloseUIFormComplete?.Invoke(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
using GameFramework.Resource;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatLoadSession
|
||||
{
|
||||
internal enum EventHandleStatus : byte
|
||||
{
|
||||
Ignored = 0,
|
||||
Succeeded = 1,
|
||||
Failed = 2
|
||||
}
|
||||
|
||||
private EntityComponent _entity;
|
||||
private CombatInfoFormUseCase _combatInfoFormUseCase;
|
||||
private int _loadingMapEntityId;
|
||||
private int _loadedMapEntityId;
|
||||
private int? _loadingCombatInfoFormSerialId;
|
||||
private bool _isCombatInfoFormReady;
|
||||
private MapEntity _currentMap;
|
||||
|
||||
public MapEntity CurrentMap => _currentMap;
|
||||
public bool IsReady => _currentMap != null && _isCombatInfoFormReady;
|
||||
|
||||
public void OnInit(EntityComponent entityComponent)
|
||||
{
|
||||
_entity = entityComponent;
|
||||
Reset();
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_loadingMapEntityId = 0;
|
||||
_loadedMapEntityId = 0;
|
||||
_loadingCombatInfoFormSerialId = null;
|
||||
_isCombatInfoFormReady = false;
|
||||
_currentMap = null;
|
||||
}
|
||||
|
||||
public bool StartLoading(DRLevel level, MapEntityLoadContext mapLoadContext, ICombatSchedulerPort schedulerPort, out string errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
if (_entity == null)
|
||||
{
|
||||
errorMessage = "CombatLoadSession start failed. Entity component is null.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryShowMap(level, mapLoadContext, out errorMessage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryOpenCombatInfoForm(schedulerPort, out errorMessage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RefreshCombatInfoForm(DRLevel currentLevel)
|
||||
{
|
||||
if (currentLevel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.CombatInfoForm);
|
||||
}
|
||||
|
||||
public void Cleanup()
|
||||
{
|
||||
if (_entity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_loadingMapEntityId != 0 && _entity.IsLoadingEntity(_loadingMapEntityId))
|
||||
{
|
||||
_entity.HideEntity(_loadingMapEntityId);
|
||||
}
|
||||
|
||||
if (_currentMap != null)
|
||||
{
|
||||
_entity.HideEntity(_currentMap);
|
||||
}
|
||||
else if (_loadedMapEntityId != 0)
|
||||
{
|
||||
UnityGameFramework.Runtime.Entity loadedMapEntity = _entity.GetEntity(_loadedMapEntityId);
|
||||
if (loadedMapEntity != null)
|
||||
{
|
||||
_entity.HideEntity(loadedMapEntity);
|
||||
}
|
||||
}
|
||||
|
||||
CloseCombatInfoForm();
|
||||
}
|
||||
|
||||
public EventHandleStatus HandleShowEntitySuccess(ShowEntitySuccessEventArgs args, out string errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
if (args == null)
|
||||
{
|
||||
return EventHandleStatus.Ignored;
|
||||
}
|
||||
|
||||
if (args.EntityLogicType != typeof(MapEntity) || args.Entity == null ||
|
||||
args.Entity.Id != _loadingMapEntityId)
|
||||
{
|
||||
return EventHandleStatus.Ignored;
|
||||
}
|
||||
|
||||
_loadedMapEntityId = _loadingMapEntityId;
|
||||
_loadingMapEntityId = 0;
|
||||
_currentMap = args.Entity.Logic as MapEntity;
|
||||
if (_currentMap == null)
|
||||
{
|
||||
errorMessage = $"Loaded map entity logic is invalid. EntityId={args.Entity.Id}.";
|
||||
return EventHandleStatus.Failed;
|
||||
}
|
||||
|
||||
return EventHandleStatus.Succeeded;
|
||||
}
|
||||
|
||||
public EventHandleStatus HandleShowEntityFailure(ShowEntityFailureEventArgs args, out string errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
if (args == null)
|
||||
{
|
||||
return EventHandleStatus.Ignored;
|
||||
}
|
||||
|
||||
if (args.EntityLogicType != typeof(MapEntity) || args.EntityId != _loadingMapEntityId)
|
||||
{
|
||||
return EventHandleStatus.Ignored;
|
||||
}
|
||||
|
||||
_loadingMapEntityId = 0;
|
||||
_currentMap = null;
|
||||
errorMessage = $"Map load failed. Asset='{args.EntityAssetName}', Error='{args.ErrorMessage}'.";
|
||||
return EventHandleStatus.Failed;
|
||||
}
|
||||
|
||||
public void HandleHideEntityComplete(HideEntityCompleteEventArgs args)
|
||||
{
|
||||
if (args == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.EntityId == _loadedMapEntityId)
|
||||
{
|
||||
_loadedMapEntityId = 0;
|
||||
_currentMap = null;
|
||||
}
|
||||
|
||||
if (args.EntityId == _loadingMapEntityId)
|
||||
{
|
||||
_loadingMapEntityId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public EventHandleStatus HandleOpenUIFormSuccess(OpenUIFormSuccessEventArgs args, out string errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
if (args == null || !_loadingCombatInfoFormSerialId.HasValue || args.UIForm == null)
|
||||
{
|
||||
return EventHandleStatus.Ignored;
|
||||
}
|
||||
|
||||
if (args.UIForm.SerialId != _loadingCombatInfoFormSerialId.Value)
|
||||
{
|
||||
return EventHandleStatus.Ignored;
|
||||
}
|
||||
|
||||
_isCombatInfoFormReady = args.UIForm.Logic is CombatInfoForm;
|
||||
if (!_isCombatInfoFormReady)
|
||||
{
|
||||
errorMessage = "CombatInfoForm open success but form logic type is invalid.";
|
||||
return EventHandleStatus.Failed;
|
||||
}
|
||||
|
||||
return EventHandleStatus.Succeeded;
|
||||
}
|
||||
|
||||
public EventHandleStatus HandleOpenUIFormFailure(OpenUIFormFailureEventArgs args, out string errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
if (args == null || !_loadingCombatInfoFormSerialId.HasValue)
|
||||
{
|
||||
return EventHandleStatus.Ignored;
|
||||
}
|
||||
|
||||
if (args.SerialId != _loadingCombatInfoFormSerialId.Value)
|
||||
{
|
||||
return EventHandleStatus.Ignored;
|
||||
}
|
||||
|
||||
_isCombatInfoFormReady = false;
|
||||
errorMessage = $"CombatInfoForm load failed. Asset='{args.UIFormAssetName}', Error='{args.ErrorMessage}'.";
|
||||
return EventHandleStatus.Failed;
|
||||
}
|
||||
|
||||
public void HandleCloseUIFormComplete(CloseUIFormCompleteEventArgs args)
|
||||
{
|
||||
if (args == null || !_loadingCombatInfoFormSerialId.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.SerialId == _loadingCombatInfoFormSerialId.Value)
|
||||
{
|
||||
_loadingCombatInfoFormSerialId = null;
|
||||
_isCombatInfoFormReady = false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryShowMap(DRLevel level, MapEntityLoadContext mapLoadContext, out string errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
if (level == null)
|
||||
{
|
||||
errorMessage = "CombatLoadSession map load failed. Level is null.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mapLoadContext?.InitialMapData == null)
|
||||
{
|
||||
errorMessage = "CombatLoadSession map load failed. MapEntityLoadContext is missing initial map data.";
|
||||
return false;
|
||||
}
|
||||
|
||||
string mapAssetName = level.Id.ToString();
|
||||
string mapAssetPath = AssetUtility.GetLevelMapAsset(mapAssetName);
|
||||
if (GameEntry.Resource.HasAsset(mapAssetPath) == HasAssetResult.NotExist)
|
||||
{
|
||||
errorMessage = $"CombatLoadSession map asset not found. Level='{level.Id}', Asset='{mapAssetPath}'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
_loadingMapEntityId = _entity.GenerateSerialId();
|
||||
MapData resolvedMapData = mapLoadContext.InitialMapData.CloneForEntity(_loadingMapEntityId, Vector3.zero);
|
||||
MapEntityLoadContext resolvedLoadContext = new MapEntityLoadContext(
|
||||
resolvedMapData,
|
||||
mapLoadContext.TryConsumeCoin,
|
||||
mapLoadContext.AddCoin);
|
||||
_entity.ShowMap(resolvedLoadContext, mapAssetName);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryOpenCombatInfoForm(ICombatSchedulerPort schedulerPort, out string errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
if (_combatInfoFormUseCase == null)
|
||||
{
|
||||
_combatInfoFormUseCase = new CombatInfoFormUseCase();
|
||||
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatInfoForm, _combatInfoFormUseCase);
|
||||
}
|
||||
|
||||
_combatInfoFormUseCase.Configure(
|
||||
() => BuildCombatInfoFormRawData(schedulerPort),
|
||||
() => schedulerPort != null && schedulerPort.CanEndCombat && schedulerPort.TryEndCombatByPlayer(),
|
||||
() => schedulerPort != null && schedulerPort.TryDebugFail("Manual debug fail from CombatInfoForm."));
|
||||
|
||||
int? serialId = GameEntry.UIRouter.OpenUI(UIFormType.CombatInfoForm);
|
||||
if (!serialId.HasValue)
|
||||
{
|
||||
errorMessage = "CombatLoadSession failed to open CombatInfoForm.";
|
||||
return false;
|
||||
}
|
||||
|
||||
_loadingCombatInfoFormSerialId = serialId.Value;
|
||||
_isCombatInfoFormReady = GameEntry.UI.HasUIForm(serialId.Value);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static CombatInfoFormRawData BuildCombatInfoFormRawData(ICombatSchedulerPort schedulerPort)
|
||||
{
|
||||
if (schedulerPort == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
DRLevel level = schedulerPort.CurrentLevel;
|
||||
LevelThemeType themeType = level != null ? level.LevelThemeType : LevelThemeType.None;
|
||||
int levelId = level != null ? level.Id : 0;
|
||||
int baseHpMax = level != null ? Mathf.Max(0, level.BaseHp) : 0;
|
||||
return CombatInfoFormUseCase.BuildRawData(
|
||||
themeType,
|
||||
levelId,
|
||||
schedulerPort.DisplayPhaseIndex,
|
||||
schedulerPort.PhaseCount,
|
||||
schedulerPort.CurrentCoin,
|
||||
schedulerPort.CurrentBaseHp,
|
||||
baseHpMax,
|
||||
schedulerPort.CanEndCombat);
|
||||
}
|
||||
|
||||
private void CloseCombatInfoForm()
|
||||
{
|
||||
GameEntry.UIRouter.CloseUI(UIFormType.CombatInfoForm);
|
||||
_loadingCombatInfoFormSerialId = null;
|
||||
_isCombatInfoFormReady = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class CombatRunResourceStore
|
||||
{
|
||||
private readonly List<TowerStatsData> _buildTowerStatsSnapshot = new();
|
||||
private readonly List<TowerItemData> _participantTowerSnapshot = new();
|
||||
private readonly BackpackInventoryData _rewardInventory = new();
|
||||
|
||||
private BackpackInventoryData _combatInventorySnapshot = new();
|
||||
private bool _isCombatActive;
|
||||
|
||||
public int CurrentCoin { get; private set; }
|
||||
public int CurrentGold => _isCombatActive ? GainedGold : 0;
|
||||
public int CurrentBaseHp { get; private set; }
|
||||
public int MaxBaseHp { get; private set; }
|
||||
public int CurrentBuildTowerCount => _isCombatActive ? _buildTowerStatsSnapshot.Count : 0;
|
||||
public int GainedCoin { get; private set; }
|
||||
public int GainedGold { get; private set; }
|
||||
|
||||
public void InitializeForCombat(DRLevel level)
|
||||
{
|
||||
Reset();
|
||||
if (level == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MaxBaseHp = Mathf.Max(0, level.BaseHp);
|
||||
CurrentBaseHp = MaxBaseHp;
|
||||
CurrentCoin = Mathf.Max(0, level.StartCoin);
|
||||
CacheCombatSnapshots();
|
||||
_isCombatActive = true;
|
||||
}
|
||||
|
||||
public void MarkCombatEnded()
|
||||
{
|
||||
_isCombatActive = false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_isCombatActive = false;
|
||||
CurrentCoin = 0;
|
||||
CurrentBaseHp = 0;
|
||||
MaxBaseHp = 0;
|
||||
GainedCoin = 0;
|
||||
GainedGold = 0;
|
||||
_buildTowerStatsSnapshot.Clear();
|
||||
_participantTowerSnapshot.Clear();
|
||||
_combatInventorySnapshot = new BackpackInventoryData();
|
||||
_rewardInventory.Gold = 0;
|
||||
_rewardInventory.MuzzleComponents.Clear();
|
||||
_rewardInventory.BearingComponents.Clear();
|
||||
_rewardInventory.BaseComponents.Clear();
|
||||
_rewardInventory.Towers.Clear();
|
||||
_rewardInventory.ParticipantTowerInstanceIds.Clear();
|
||||
}
|
||||
|
||||
public BackpackInventoryData GetRewardInventorySnapshot()
|
||||
{
|
||||
return InventoryCloneUtility.CloneInventory(_rewardInventory);
|
||||
}
|
||||
|
||||
public BackpackInventoryData GetCombatInventorySnapshot()
|
||||
{
|
||||
return InventoryCloneUtility.CloneInventory(_combatInventorySnapshot);
|
||||
}
|
||||
|
||||
public IReadOnlyList<TowerItemData> GetParticipantTowerSnapshot()
|
||||
{
|
||||
List<TowerItemData> snapshot = new List<TowerItemData>(_participantTowerSnapshot.Count);
|
||||
for (int i = 0; i < _participantTowerSnapshot.Count; i++)
|
||||
{
|
||||
TowerItemData tower = _participantTowerSnapshot[i];
|
||||
if (tower != null)
|
||||
{
|
||||
snapshot.Add(InventoryCloneUtility.CloneTower(tower));
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
public IReadOnlyList<long> GetParticipantTowerInstanceIdSnapshot()
|
||||
{
|
||||
List<long> snapshot = new List<long>(_participantTowerSnapshot.Count);
|
||||
for (int i = 0; i < _participantTowerSnapshot.Count; i++)
|
||||
{
|
||||
TowerItemData tower = _participantTowerSnapshot[i];
|
||||
if (tower != null && tower.InstanceId > 0)
|
||||
{
|
||||
snapshot.Add(tower.InstanceId);
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
public bool TryConsumeCoin(int coin)
|
||||
{
|
||||
int requiredCoin = Mathf.Max(0, coin);
|
||||
if (requiredCoin <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_isCombatActive || CurrentCoin < requiredCoin)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CurrentCoin -= requiredCoin;
|
||||
FireCoinChangedEvent(-requiredCoin);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AddCoin(int coin)
|
||||
{
|
||||
int gainedCoin = Mathf.Max(0, coin);
|
||||
if (!_isCombatActive || gainedCoin <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentCoin += gainedCoin;
|
||||
FireCoinChangedEvent(gainedCoin);
|
||||
}
|
||||
|
||||
public int ApplyBaseDamage(int damage)
|
||||
{
|
||||
int resolvedDamage = Mathf.Max(0, damage);
|
||||
if (!_isCombatActive || resolvedDamage <= 0)
|
||||
{
|
||||
return CurrentBaseHp;
|
||||
}
|
||||
|
||||
int previousBaseHp = CurrentBaseHp;
|
||||
CurrentBaseHp = Mathf.Max(0, CurrentBaseHp - resolvedDamage);
|
||||
int deltaBaseHp = CurrentBaseHp - previousBaseHp;
|
||||
if (deltaBaseHp != 0)
|
||||
{
|
||||
FireBaseHpChangedEvent(deltaBaseHp);
|
||||
}
|
||||
|
||||
return CurrentBaseHp;
|
||||
}
|
||||
|
||||
public bool TryGetBuildTowerStats(int buildIndex, out TowerStatsData stats)
|
||||
{
|
||||
stats = null;
|
||||
if (!_isCombatActive || buildIndex < 0 || buildIndex >= _buildTowerStatsSnapshot.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TowerStatsData sourceStats = _buildTowerStatsSnapshot[buildIndex];
|
||||
if (sourceStats == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
stats = InventoryCloneUtility.CloneTowerStats(sourceStats);
|
||||
return stats != null;
|
||||
}
|
||||
|
||||
public void AddEnemyDefeatedReward(int gainedCoin, int gainedGold)
|
||||
{
|
||||
if (!_isCombatActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int coin = Mathf.Max(0, gainedCoin);
|
||||
int gold = Mathf.Max(0, gainedGold);
|
||||
GainedCoin += coin;
|
||||
GainedGold += gold;
|
||||
if (coin > 0)
|
||||
{
|
||||
CurrentCoin += coin;
|
||||
FireCoinChangedEvent(coin);
|
||||
}
|
||||
|
||||
if (gold > 0)
|
||||
{
|
||||
_rewardInventory.Gold += gold;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddSettlementGold(int gainedGold)
|
||||
{
|
||||
int gold = Mathf.Max(0, gainedGold);
|
||||
if (gold <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GainedGold += gold;
|
||||
_rewardInventory.Gold += gold;
|
||||
}
|
||||
|
||||
public void AddEnemyDefeatedLoot(TowerCompItemData droppedItem)
|
||||
{
|
||||
if (!_isCombatActive || droppedItem == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AppendRewardItem(droppedItem);
|
||||
}
|
||||
|
||||
private void CacheCombatSnapshots()
|
||||
{
|
||||
_buildTowerStatsSnapshot.Clear();
|
||||
_participantTowerSnapshot.Clear();
|
||||
_combatInventorySnapshot = GameEntry.PlayerInventory != null
|
||||
? GameEntry.PlayerInventory.GetInventorySnapshot()
|
||||
: new BackpackInventoryData();
|
||||
if (GameEntry.PlayerInventory == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IReadOnlyList<TowerItemData> towers = GameEntry.PlayerInventory.GetParticipantTowerSnapshot();
|
||||
if (towers == null || towers.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < towers.Count; i++)
|
||||
{
|
||||
TowerItemData tower = towers[i];
|
||||
if (tower == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_participantTowerSnapshot.Add(InventoryCloneUtility.CloneTower(tower));
|
||||
if (tower.Stats != null)
|
||||
{
|
||||
_buildTowerStatsSnapshot.Add(InventoryCloneUtility.CloneTowerStats(tower.Stats));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FireCoinChangedEvent(int deltaCoin)
|
||||
{
|
||||
if (!_isCombatActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Fire(this, CombatCoinChangedEventArgs.Create(CurrentCoin, deltaCoin));
|
||||
}
|
||||
|
||||
private void FireBaseHpChangedEvent(int deltaBaseHp)
|
||||
{
|
||||
if (!_isCombatActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Fire(this, CombatBaseHpChangedEventArgs.Create(CurrentBaseHp, deltaBaseHp));
|
||||
}
|
||||
|
||||
private void AppendRewardItem(TowerCompItemData droppedItem)
|
||||
{
|
||||
switch (droppedItem)
|
||||
{
|
||||
case MuzzleCompItemData muzzleComp:
|
||||
_rewardInventory.MuzzleComponents.Add(InventoryCloneUtility.CloneMuzzleComp(muzzleComp));
|
||||
break;
|
||||
case BearingCompItemData bearingComp:
|
||||
_rewardInventory.BearingComponents.Add(InventoryCloneUtility.CloneBearingComp(bearingComp));
|
||||
break;
|
||||
case BaseCompItemData baseComp:
|
||||
_rewardInventory.BaseComponents.Add(InventoryCloneUtility.CloneBaseComp(baseComp));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD.Procedure;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public partial class CombatScheduler : ICombatSchedulerPort
|
||||
{
|
||||
private readonly CombatSchedulerRuntime _runtime = new();
|
||||
private readonly CombatSchedulerCoordinator _coordinator;
|
||||
|
||||
private bool _initialized;
|
||||
|
||||
public CombatScheduler()
|
||||
{
|
||||
_coordinator = new CombatSchedulerCoordinator(this, _runtime);
|
||||
}
|
||||
|
||||
public bool IsRunning =>
|
||||
_runtime.CurrentState is CombatLoadingState or CombatRunningPhaseState or CombatWaitingForPhaseEndState;
|
||||
|
||||
public bool IsCompleted => _runtime.IsCompleted;
|
||||
public DRLevel CurrentLevel => _runtime.CurrentLevel;
|
||||
public DRLevelPhase CurrentPhase => _runtime.PhaseLoopRuntime.CurrentPhase;
|
||||
public MapEntity CurrentMap => _runtime.LoadSession.CurrentMap;
|
||||
public int DisplayPhaseIndex => _runtime.PhaseLoopRuntime.DisplayPhaseIndex;
|
||||
public int PhaseCount => _runtime.PhaseLoopRuntime.PhaseCount;
|
||||
public bool CanEndCombat => _runtime.PhaseLoopRuntime.CanEndCombat;
|
||||
public int CurrentCoin => _runtime.CombatRunResourceStore.CurrentCoin;
|
||||
public int CurrentGold => _runtime.CombatRunResourceStore.CurrentGold;
|
||||
public int CurrentBaseHp => _runtime.CombatRunResourceStore.CurrentBaseHp;
|
||||
public int CurrentBuildTowerCount => _runtime.CombatRunResourceStore.CurrentBuildTowerCount;
|
||||
public int DefeatedEnemyCount => _runtime.EnemyManager.DefeatedEnemyCount;
|
||||
public int GainedCoin => _runtime.CombatRunResourceStore.GainedCoin;
|
||||
public int GainedGold => _runtime.CombatRunResourceStore.GainedGold;
|
||||
|
||||
public void OnInit(Action<bool> combatEndedCallback)
|
||||
{
|
||||
_runtime.CombatEndedCallback = combatEndedCallback;
|
||||
|
||||
if (!_initialized)
|
||||
{
|
||||
_runtime.Entity = GameEntry.Entity;
|
||||
_runtime.EventBridge.Bind(
|
||||
OnShowEntitySuccess,
|
||||
OnShowEntityFailure,
|
||||
OnHideEntityComplete,
|
||||
OnOpenUIFormSuccess,
|
||||
OnOpenUIFormFailure,
|
||||
OnCloseUIFormComplete);
|
||||
_runtime.EnemyManager.OnInit(this);
|
||||
_runtime.LoadSession.OnInit(_runtime.Entity);
|
||||
_coordinator.EnsureCombatFinishFormUseCaseBound();
|
||||
_coordinator.EnsureRewardSelectFormUseCaseBound();
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
_coordinator.ResetRuntime();
|
||||
}
|
||||
|
||||
public bool Start(
|
||||
DRLevel level,
|
||||
IReadOnlyList<DRLevelPhase> phases,
|
||||
IReadOnlyDictionary<int, IReadOnlyList<DRLevelSpawnEntry>> spawnEntriesByPhaseId,
|
||||
RunNodeExecutionContext context = null)
|
||||
{
|
||||
if (!_initialized || _runtime.Entity == null)
|
||||
{
|
||||
return _coordinator.HandleStartFailure("CombatScheduler start failed. Runtime is not initialized.");
|
||||
}
|
||||
|
||||
if (level == null || phases == null || phases.Count <= 0)
|
||||
{
|
||||
return _coordinator.HandleStartFailure("CombatScheduler start failed. Invalid level or phase data.");
|
||||
}
|
||||
|
||||
_coordinator.CleanupAllCombatEntities();
|
||||
_coordinator.CloseCombatFinishForm();
|
||||
_coordinator.CloseRewardSelectForm();
|
||||
_coordinator.CloseDialogForm();
|
||||
_runtime.EnemyManager.EndPhase();
|
||||
_runtime.EnemyManager.ResetCombatStats();
|
||||
_coordinator.ResetRuntime();
|
||||
_runtime.DidCombatWin = true;
|
||||
|
||||
_runtime.CurrentLevel = level;
|
||||
_runtime.NodeContext = context != null ? context.Clone() : null;
|
||||
_runtime.NextDropOrdinal = 0;
|
||||
_runtime.NextRewardOrdinal = 0;
|
||||
_runtime.CombatRunResourceStore.InitializeForCombat(level);
|
||||
foreach (var phase in phases)
|
||||
{
|
||||
if (phase != null)
|
||||
{
|
||||
_runtime.PhaseBuffer.Add(phase);
|
||||
}
|
||||
}
|
||||
|
||||
if (spawnEntriesByPhaseId != null)
|
||||
{
|
||||
foreach (var pair in spawnEntriesByPhaseId)
|
||||
{
|
||||
_runtime.SpawnEntriesByPhaseId[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
_runtime.PhaseLoopRuntime.SetPhases(_runtime.PhaseBuffer);
|
||||
if (_runtime.PhaseLoopRuntime.PhaseCount <= 0)
|
||||
{
|
||||
return _coordinator.HandleStartFailure(
|
||||
$"CombatScheduler start failed. Level '{level.Id}' has no phase data.");
|
||||
}
|
||||
|
||||
ChangeState(new CombatLoadingState(_runtime, _coordinator));
|
||||
Log.Info(
|
||||
"CombatScheduler started. Level={0}, PhaseCount={1}.",
|
||||
_runtime.CurrentLevel.Id,
|
||||
_runtime.PhaseLoopRuntime.PhaseCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
_runtime.CurrentState?.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_runtime.CurrentState?.OnExit();
|
||||
_runtime.CurrentState?.OnDestroy();
|
||||
_runtime.CurrentState = null;
|
||||
_coordinator.CleanupAllCombatEntities();
|
||||
_coordinator.CloseCombatFinishForm();
|
||||
_coordinator.CloseRewardSelectForm();
|
||||
_coordinator.CloseDialogForm();
|
||||
_runtime.EnemyManager.OnDestroy();
|
||||
_coordinator.ResetRuntime();
|
||||
_runtime.EventBridge.Unbind();
|
||||
_runtime.CombatFinishFormUseCase = null;
|
||||
_runtime.RewardSelectFormUseCase = null;
|
||||
_runtime.CombatEndedCallback = null;
|
||||
|
||||
_runtime.Entity = null;
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
public bool TryEndCombatByPlayer()
|
||||
{
|
||||
if (_runtime.CurrentState is not CombatRunningPhaseState &&
|
||||
_runtime.CurrentState is not CombatWaitingForPhaseEndState)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _runtime.PhaseLoopRuntime.TryRequestEndCombat();
|
||||
}
|
||||
|
||||
public void OnEnemyReachedBase(DREnemy enemy)
|
||||
{
|
||||
if (!IsRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int resolvedBaseDamage = enemy != null ? Mathf.Max(0, enemy.BaseDamage) : 0;
|
||||
if (resolvedBaseDamage <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_coordinator.ApplyBaseDamage(resolvedBaseDamage);
|
||||
}
|
||||
|
||||
public void OnEnemyDefeated(DREnemy enemy)
|
||||
{
|
||||
if (!IsRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnemyDropContext context = new(
|
||||
enemy,
|
||||
_runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||
_coordinator.ResolveCurrentThemeType());
|
||||
int nextDropOrdinal = _runtime.NextDropOrdinal;
|
||||
EnemyDropResult result = GameEntry.InventoryGeneration.ResolveEnemyDrop(
|
||||
context,
|
||||
_runtime.NodeContext != null ? _runtime.NodeContext.RunSeed : 0,
|
||||
_runtime.NodeContext != null ? _runtime.NodeContext.SequenceIndex : -1,
|
||||
ref nextDropOrdinal);
|
||||
_runtime.NextDropOrdinal = nextDropOrdinal;
|
||||
_runtime.CombatRunResourceStore.AddEnemyDefeatedReward(result.Coin, result.Gold);
|
||||
_runtime.CombatRunResourceStore.AddEnemyDefeatedLoot(result.LootItem);
|
||||
}
|
||||
|
||||
public bool OnCombatFinishReturnRequested()
|
||||
{
|
||||
if (_runtime.CurrentState is not CombatWaitingForReturnState waitingForReturnState)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
waitingForReturnState.RequestReturn();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryConsumeCoin(int coin)
|
||||
{
|
||||
return _runtime.CombatRunResourceStore.TryConsumeCoin(coin);
|
||||
}
|
||||
|
||||
public void AddCoin(int coin)
|
||||
{
|
||||
_runtime.CombatRunResourceStore.AddCoin(coin);
|
||||
}
|
||||
|
||||
public bool TryGetBuildTowerStats(int buildIndex, out TowerStatsData stats)
|
||||
{
|
||||
return _runtime.CombatRunResourceStore.TryGetBuildTowerStats(buildIndex, out stats);
|
||||
}
|
||||
|
||||
public bool TryDebugFail(string errorMessage)
|
||||
{
|
||||
if (_runtime.IsCompleted || _runtime.CurrentState == null || _runtime.CurrentState is CombatFailedState)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_coordinator.EnterFailureFallback(string.IsNullOrWhiteSpace(errorMessage)
|
||||
? "Manual debug fail."
|
||||
: errorMessage);
|
||||
return _runtime.CurrentState is CombatFailedState;
|
||||
}
|
||||
|
||||
void ICombatSchedulerPort.ChangeState(CombatStateBase nextState)
|
||||
{
|
||||
ChangeState(nextState);
|
||||
}
|
||||
|
||||
internal void ChangeState(CombatStateBase nextState)
|
||||
{
|
||||
if (ReferenceEquals(_runtime.CurrentState, nextState))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_runtime.CurrentState?.OnExit();
|
||||
_runtime.CurrentState?.OnDestroy();
|
||||
_runtime.CurrentState = nextState;
|
||||
_runtime.CurrentState?.OnInit();
|
||||
_runtime.CurrentState?.OnEnter();
|
||||
}
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
private void OnShowEntitySuccess(ShowEntitySuccessEventArgs args)
|
||||
{
|
||||
var status = _runtime.LoadSession.HandleShowEntitySuccess(args, out string errorMessage);
|
||||
if (status == CombatLoadSession.EventHandleStatus.Failed)
|
||||
{
|
||||
_coordinator.EnterFailureFallback(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status == CombatLoadSession.EventHandleStatus.Succeeded)
|
||||
{
|
||||
MapEntity map = _runtime.LoadSession.CurrentMap;
|
||||
Log.Info(
|
||||
"Map ready. LevelId={0}, PathCells={1}, FoundationCells={2}, Spawners={3}, House={4}.",
|
||||
_runtime.CurrentLevel != null ? _runtime.CurrentLevel.Id : 0,
|
||||
map.PathCells.Count,
|
||||
map.FoundationCells.Count,
|
||||
map.Spawners.Length,
|
||||
map.House != null ? map.House.name : "None");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShowEntityFailure(ShowEntityFailureEventArgs args)
|
||||
{
|
||||
var status = _runtime.LoadSession.HandleShowEntityFailure(args, out string errorMessage);
|
||||
if (status == CombatLoadSession.EventHandleStatus.Failed)
|
||||
{
|
||||
_coordinator.EnterFailureFallback(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHideEntityComplete(HideEntityCompleteEventArgs args)
|
||||
{
|
||||
_runtime.LoadSession.HandleHideEntityComplete(args);
|
||||
}
|
||||
|
||||
private void OnOpenUIFormSuccess(OpenUIFormSuccessEventArgs args)
|
||||
{
|
||||
var status = _runtime.LoadSession.HandleOpenUIFormSuccess(args, out string errorMessage);
|
||||
if (status == CombatLoadSession.EventHandleStatus.Failed)
|
||||
{
|
||||
_coordinator.EnterFailureFallback(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOpenUIFormFailure(OpenUIFormFailureEventArgs args)
|
||||
{
|
||||
var status = _runtime.LoadSession.HandleOpenUIFormFailure(args, out string errorMessage);
|
||||
if (status == CombatLoadSession.EventHandleStatus.Failed)
|
||||
{
|
||||
_coordinator.EnterFailureFallback(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCloseUIFormComplete(CloseUIFormCompleteEventArgs args)
|
||||
{
|
||||
_runtime.LoadSession.HandleCloseUIFormComplete(args);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Procedure;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatSchedulerCoordinator
|
||||
{
|
||||
private readonly ICombatSchedulerPort _schedulerPort;
|
||||
private readonly CombatSchedulerRuntime _runtime;
|
||||
public ICombatSchedulerPort Port => _schedulerPort;
|
||||
|
||||
public CombatSchedulerCoordinator(ICombatSchedulerPort schedulerPort, CombatSchedulerRuntime runtime)
|
||||
{
|
||||
_schedulerPort = schedulerPort;
|
||||
_runtime = runtime;
|
||||
}
|
||||
|
||||
public void ChangeState(CombatStateBase nextState)
|
||||
{
|
||||
_schedulerPort.ChangeState(nextState);
|
||||
}
|
||||
|
||||
public int ResolveEnemyHpRateMultiplier(int displayPhaseIndex, int phaseCount)
|
||||
{
|
||||
if (displayPhaseIndex <= 0 || phaseCount <= 0)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
int completedLoopCount = Mathf.Max(0, (displayPhaseIndex - 1) / phaseCount);
|
||||
if (completedLoopCount >= 30)
|
||||
{
|
||||
return int.MaxValue;
|
||||
}
|
||||
|
||||
return 1 << completedLoopCount;
|
||||
}
|
||||
|
||||
public void ResetRuntime()
|
||||
{
|
||||
_runtime.CurrentState = null;
|
||||
_runtime.PhaseBuffer.Clear();
|
||||
_runtime.SpawnEntriesByPhaseId.Clear();
|
||||
_runtime.PhaseLoopRuntime.Reset();
|
||||
_runtime.LoadSession.Reset();
|
||||
_runtime.CombatRunResourceStore.Reset();
|
||||
_runtime.SettlementContext = null;
|
||||
_runtime.CurrentLevel = null;
|
||||
_runtime.DidCombatWin = true;
|
||||
_runtime.IsCompleted = false;
|
||||
_runtime.NodeEnterFired = false;
|
||||
_runtime.NodeContext = null;
|
||||
_runtime.NextDropOrdinal = 0;
|
||||
_runtime.NextRewardOrdinal = 0;
|
||||
}
|
||||
|
||||
public void CleanupAllCombatEntities()
|
||||
{
|
||||
_runtime.LoadSession.Cleanup();
|
||||
_runtime.EnemyManager.CleanupTrackedEnemies();
|
||||
}
|
||||
|
||||
public void EnsureCombatFinishFormUseCaseBound()
|
||||
{
|
||||
_runtime.CombatFinishFormUseCase ??= new CombatFinishFormUseCase(_schedulerPort);
|
||||
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _runtime.CombatFinishFormUseCase);
|
||||
}
|
||||
|
||||
public void EnsureRewardSelectFormUseCaseBound()
|
||||
{
|
||||
_runtime.RewardSelectFormUseCase ??= new RewardSelectFormUseCase();
|
||||
GameEntry.UIRouter.BindUIUseCase(UIFormType.RewardSelectForm, _runtime.RewardSelectFormUseCase);
|
||||
}
|
||||
|
||||
public void OpenCombatFailureDialog(string errorMessage)
|
||||
{
|
||||
CloseDialogForm();
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.DialogForm, new DialogFormRawData
|
||||
{
|
||||
Mode = 1,
|
||||
Title = "Combat Error",
|
||||
Message = string.IsNullOrWhiteSpace(errorMessage) ? "Combat failed unexpectedly." : errorMessage,
|
||||
PauseGame = false,
|
||||
ConfirmText = "Return Menu",
|
||||
OnClickConfirm = OnCombatFailureDialogConfirmed
|
||||
});
|
||||
}
|
||||
|
||||
public bool TryBeginNextPhase()
|
||||
{
|
||||
if (!_runtime.PhaseLoopRuntime.TryEnterNextPhase(out DRLevelPhase nextPhase))
|
||||
{
|
||||
_schedulerPort.ChangeState(new CombatSettlementState(_runtime, this, true));
|
||||
return false;
|
||||
}
|
||||
|
||||
_runtime.SpawnEntriesByPhaseId.TryGetValue(nextPhase.Id, out IReadOnlyList<DRLevelSpawnEntry> spawnEntries);
|
||||
_schedulerPort.ChangeState(new CombatRunningPhaseState(_runtime, this, nextPhase, spawnEntries));
|
||||
return true;
|
||||
}
|
||||
|
||||
public void EnterWaitingForPhaseEnd()
|
||||
{
|
||||
_schedulerPort.ChangeState(new CombatWaitingForPhaseEndState(_runtime, this));
|
||||
}
|
||||
|
||||
public void CompleteCurrentPhase()
|
||||
{
|
||||
_runtime.EnemyManager.EndPhase();
|
||||
Log.Info(
|
||||
"CombatScheduler phase completed. Level={0}, Phase={1}, Elapsed={2:F2}s.",
|
||||
_runtime.CurrentLevel != null ? _runtime.CurrentLevel.Id : 0,
|
||||
_runtime.PhaseLoopRuntime.CurrentPhase != null ? _runtime.PhaseLoopRuntime.CurrentPhase.Id : 0,
|
||||
_runtime.PhaseLoopRuntime.CurrentPhaseElapsed);
|
||||
|
||||
TryBeginNextPhase();
|
||||
}
|
||||
|
||||
public bool ShouldEnterSettlementFromActiveState(out bool didCombatWin)
|
||||
{
|
||||
if (GetCurrentBaseHp() <= 0)
|
||||
{
|
||||
didCombatWin = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_runtime.PhaseLoopRuntime.IsEndCombatRequested)
|
||||
{
|
||||
didCombatWin = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
didCombatWin = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnFullBaseHpRewardSelected(RewardSelectItemRawData selectedReward)
|
||||
{
|
||||
if (_runtime.CurrentState is not CombatRewardSelectionState || _runtime.SettlementContext == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_runtime.CombatSettlementService.ApplySelectedReward(_runtime.SettlementContext, selectedReward);
|
||||
_schedulerPort.ChangeState(new CombatFinishFormState(_runtime, this));
|
||||
}
|
||||
|
||||
public void OnFullBaseHpRewardGiveUp()
|
||||
{
|
||||
if (_runtime.CurrentState is not CombatRewardSelectionState || _runtime.SettlementContext == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_schedulerPort.ChangeState(new CombatFinishFormState(_runtime, this));
|
||||
}
|
||||
|
||||
public LevelThemeType ResolveCurrentThemeType()
|
||||
{
|
||||
if (_runtime.CurrentLevel != null)
|
||||
{
|
||||
return _runtime.CurrentLevel.LevelThemeType;
|
||||
}
|
||||
|
||||
return LevelThemeType.None;
|
||||
}
|
||||
|
||||
public int ApplyBaseDamage(int damage)
|
||||
{
|
||||
return _runtime.CombatRunResourceStore.ApplyBaseDamage(damage);
|
||||
}
|
||||
|
||||
public int GetCurrentBaseHp()
|
||||
{
|
||||
return Mathf.Max(0, _runtime.CombatRunResourceStore.CurrentBaseHp);
|
||||
}
|
||||
|
||||
public void CloseCombatFinishForm()
|
||||
{
|
||||
GameEntry.UIRouter.CloseUI(UIFormType.CombatFinishForm);
|
||||
}
|
||||
|
||||
public void CloseRewardSelectForm()
|
||||
{
|
||||
GameEntry.UIRouter.CloseUI(UIFormType.RewardSelectForm);
|
||||
}
|
||||
|
||||
public void CloseDialogForm()
|
||||
{
|
||||
GameEntry.UIRouter.CloseUI(UIFormType.DialogForm);
|
||||
}
|
||||
|
||||
public void CompleteNormalCombatAndNotify(bool didCombatWin)
|
||||
{
|
||||
CompleteCombat(didCombatWin);
|
||||
RunNodeExecutionContext nodeContext = _runtime.NodeContext;
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeCompleteEventArgs.Create(
|
||||
nodeContext?.RunId,
|
||||
nodeContext?.NodeId ?? 0,
|
||||
nodeContext?.NodeType ?? RunNodeType.None,
|
||||
nodeContext?.SequenceIndex ?? -1,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
didCombatWin,
|
||||
nodeContext != null
|
||||
? nodeContext.CreateCompletionSnapshot(
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
|
||||
: null));
|
||||
}
|
||||
|
||||
public void CompleteFailureCombatAndNotify()
|
||||
{
|
||||
CleanupAllCombatEntities();
|
||||
CloseCombatFinishForm();
|
||||
CloseRewardSelectForm();
|
||||
CloseDialogForm();
|
||||
CompleteCombat(false);
|
||||
RunNodeExecutionContext nodeContext = _runtime.NodeContext;
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeCompleteEventArgs.Create(
|
||||
nodeContext?.RunId,
|
||||
nodeContext?.NodeId ?? 0,
|
||||
nodeContext?.NodeType ?? RunNodeType.None,
|
||||
nodeContext?.SequenceIndex ?? -1,
|
||||
RunNodeCompletionStatus.Exception,
|
||||
false,
|
||||
nodeContext != null
|
||||
? nodeContext.CreateCompletionSnapshot(
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
|
||||
: null));
|
||||
}
|
||||
|
||||
public bool HandleStartFailure(string errorMessage)
|
||||
{
|
||||
Log.Warning("{0}", errorMessage);
|
||||
_runtime.EnemyManager.EndPhase();
|
||||
CleanupAllCombatEntities();
|
||||
CloseCombatFinishForm();
|
||||
CloseRewardSelectForm();
|
||||
CloseDialogForm();
|
||||
ResetRuntime();
|
||||
return false;
|
||||
}
|
||||
|
||||
public void EnterFailureFallback(string errorMessage)
|
||||
{
|
||||
if (_runtime.CurrentState is CombatFailedState || _runtime.IsCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_schedulerPort.ChangeState(new CombatFailedState(_runtime, this, errorMessage));
|
||||
}
|
||||
|
||||
public void OnCombatFailureDialogConfirmed(object userData)
|
||||
{
|
||||
_ = userData;
|
||||
if (_runtime.CurrentState is not CombatFailedState || _runtime.IsCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CompleteFailureCombatAndNotify();
|
||||
}
|
||||
|
||||
private void CompleteCombat(bool succeeded)
|
||||
{
|
||||
_runtime.IsCompleted = true;
|
||||
_runtime.CurrentState = null;
|
||||
_runtime.CombatRunResourceStore.MarkCombatEnded();
|
||||
_runtime.CombatEndedCallback?.Invoke(succeeded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD.Procedure;
|
||||
using GeometryTD.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatSchedulerRuntime
|
||||
{
|
||||
public List<DRLevelPhase> PhaseBuffer { get; } = new();
|
||||
public Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>> SpawnEntriesByPhaseId { get; } = new();
|
||||
public EnemyManager EnemyManager { get; } = new();
|
||||
public PhaseLoopRuntime PhaseLoopRuntime { get; } = new();
|
||||
public CombatLoadSession LoadSession { get; } = new();
|
||||
public CombatEventBridge EventBridge { get; } = new();
|
||||
public CombatRunResourceStore CombatRunResourceStore { get; } = new();
|
||||
public CombatSettlementService CombatSettlementService { get; } = new();
|
||||
|
||||
public EntityComponent Entity { get; set; }
|
||||
public DRLevel CurrentLevel { get; set; }
|
||||
public CombatFinishFormUseCase CombatFinishFormUseCase { get; set; }
|
||||
public RewardSelectFormUseCase RewardSelectFormUseCase { get; set; }
|
||||
public CombatStateBase CurrentState { get; set; }
|
||||
public Action<bool> CombatEndedCallback { get; set; }
|
||||
public bool DidCombatWin { get; set; } = true;
|
||||
public bool IsCompleted { get; set; }
|
||||
public bool NodeEnterFired { get; set; }
|
||||
public CombatSettlementContext SettlementContext { get; set; }
|
||||
public RunNodeExecutionContext NodeContext { get; set; }
|
||||
public int NextDropOrdinal { get; set; }
|
||||
public int NextRewardOrdinal { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
using System.Collections.Generic;
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class CombatSettlementContext
|
||||
{
|
||||
public CombatSettlementFlags Flags { get; } = new();
|
||||
public CombatSettlementResult Result { get; } = new();
|
||||
public CombatSettlementSummary Summary { get; } = new();
|
||||
}
|
||||
|
||||
public sealed class CombatSettlementFlags
|
||||
{
|
||||
public bool ShouldOpenRewardSelection;
|
||||
public bool DidEnterRewardSelection;
|
||||
public bool IsCommitted;
|
||||
}
|
||||
|
||||
public sealed class CombatSettlementResult
|
||||
{
|
||||
public bool DidCombatWin;
|
||||
public int FinalCoin;
|
||||
public int FinalBaseHp;
|
||||
public int MaxBaseHp;
|
||||
public int DefeatedEnemyCount;
|
||||
public int GainedGold;
|
||||
public BackpackInventoryData RewardInventory;
|
||||
public CombatSettlementEnduranceResult Endurance { get; } = new();
|
||||
}
|
||||
|
||||
public sealed class CombatSettlementEnduranceResult
|
||||
{
|
||||
public List<long> TargetTowerInstanceIds { get; } = new();
|
||||
public float EnduranceLossPerComponent;
|
||||
public int AffectedTowerCount;
|
||||
}
|
||||
|
||||
public sealed class CombatSettlementSummary
|
||||
{
|
||||
public int DefeatedEnemyCount;
|
||||
public int GainedGold;
|
||||
public BackpackInventoryData RewardInventory;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class CombatSettlementService
|
||||
{
|
||||
private const int RewardSelectDisplayCount = 3;
|
||||
private readonly CombatSettlementCalculator _calculator = new();
|
||||
private readonly CombatSettlementCommitter _committer = new();
|
||||
|
||||
public CombatSettlementContext BuildSettlementContext(
|
||||
bool didCombatWin,
|
||||
DRLevel currentLevel,
|
||||
int defeatedEnemyCount,
|
||||
CombatRunResourceStore resourceStore)
|
||||
{
|
||||
return _calculator.BuildSettlementContext(
|
||||
didCombatWin,
|
||||
currentLevel,
|
||||
defeatedEnemyCount,
|
||||
resourceStore);
|
||||
}
|
||||
|
||||
public void CommitSettlementInventory(CombatSettlementContext settlementContext)
|
||||
{
|
||||
_committer.CommitSettlementInventory(settlementContext);
|
||||
}
|
||||
|
||||
public bool TryPrepareRewardSelection(
|
||||
CombatSettlementContext settlementContext,
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
ref int nextRewardOrdinal,
|
||||
RewardSelectFormUseCase rewardSelectFormUseCase,
|
||||
Action<RewardSelectItemRawData> onRewardSelected,
|
||||
Action onGiveUp)
|
||||
{
|
||||
if (settlementContext == null || rewardSelectFormUseCase == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IReadOnlyList<TowerCompItemData> candidateItems = GameEntry.InventoryGeneration.BuildRewardCandidates(
|
||||
displayPhaseIndex,
|
||||
themeType,
|
||||
RewardSelectDisplayCount,
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
ref nextRewardOrdinal);
|
||||
if (candidateItems == null || candidateItems.Count <= 0)
|
||||
{
|
||||
settlementContext.Flags.ShouldOpenRewardSelection = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
rewardSelectFormUseCase.SetCallbacks(onRewardSelected, onGiveUp);
|
||||
rewardSelectFormUseCase.ConfigureRewardCandidates(
|
||||
candidateItems,
|
||||
displayCount: RewardSelectDisplayCount,
|
||||
refreshCost: 0,
|
||||
allowRotateOnce: false,
|
||||
allowGiveUp: false,
|
||||
tipText: "基地满血奖励:请选择 1 个组件");
|
||||
|
||||
RewardSelectFormRawData rawData = rewardSelectFormUseCase.CreateInitialModel();
|
||||
if (rawData == null || rawData.RewardItems == null || rawData.RewardItems.Length <= 0)
|
||||
{
|
||||
settlementContext.Flags.ShouldOpenRewardSelection = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
settlementContext.Flags.DidEnterRewardSelection = true;
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.RewardSelectForm, rawData);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ApplySelectedReward(CombatSettlementContext settlementContext, RewardSelectItemRawData selectedReward)
|
||||
{
|
||||
_committer.ApplySelectedReward(settlementContext, selectedReward);
|
||||
}
|
||||
|
||||
public void OpenCombatFinishForm(
|
||||
CombatSettlementContext settlementContext,
|
||||
CombatFinishFormUseCase combatFinishFormUseCase)
|
||||
{
|
||||
if (settlementContext == null || combatFinishFormUseCase == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
combatFinishFormUseCase.SetSummary(settlementContext);
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.CombatFinishForm);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatFailedState : CombatStateBase
|
||||
{
|
||||
private readonly string _errorMessage;
|
||||
|
||||
public CombatFailedState(
|
||||
CombatSchedulerRuntime runtime,
|
||||
CombatSchedulerCoordinator coordinator,
|
||||
string errorMessage) : base(runtime, coordinator)
|
||||
{
|
||||
_errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public override void OnEnter()
|
||||
{
|
||||
Log.Error(
|
||||
"CombatScheduler failed. LevelId={0}, {1}",
|
||||
Runtime.CurrentLevel != null ? Runtime.CurrentLevel.Id : 0,
|
||||
_errorMessage);
|
||||
Runtime.EnemyManager.EndPhase();
|
||||
Coordinator.CloseCombatFinishForm();
|
||||
Coordinator.CloseRewardSelectForm();
|
||||
Coordinator.OpenCombatFailureDialog(_errorMessage);
|
||||
}
|
||||
|
||||
public override void OnExit()
|
||||
{
|
||||
Coordinator.CloseDialogForm();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatFinishFormState : CombatStateBase
|
||||
{
|
||||
public CombatFinishFormState(CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator)
|
||||
: base(runtime, coordinator)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnEnter()
|
||||
{
|
||||
if (Runtime.SettlementContext == null)
|
||||
{
|
||||
Coordinator.EnterFailureFallback("Combat finish form failed. Settlement context is missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
Coordinator.EnsureCombatFinishFormUseCaseBound();
|
||||
Runtime.CombatSettlementService.OpenCombatFinishForm(
|
||||
Runtime.SettlementContext,
|
||||
Runtime.CombatFinishFormUseCase);
|
||||
Coordinator.ChangeState(new CombatWaitingForReturnState(Runtime, Coordinator));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
using System.Collections.Generic;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatLoadingState : CombatStateBase
|
||||
{
|
||||
public CombatLoadingState(CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator)
|
||||
: base(runtime, coordinator)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnEnter()
|
||||
{
|
||||
if (Runtime.CurrentLevel == null)
|
||||
{
|
||||
Coordinator.EnterFailureFallback("Combat loading failed. Current level is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
MapEntityLoadContext mapLoadContext = BuildMapLoadContext();
|
||||
if (!Runtime.LoadSession.StartLoading(Runtime.CurrentLevel, mapLoadContext, Coordinator.Port, out string errorMessage))
|
||||
{
|
||||
Coordinator.EnterFailureFallback($"Combat loading failed. {errorMessage}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
_ = elapseSeconds;
|
||||
_ = realElapseSeconds;
|
||||
|
||||
if (!Runtime.LoadSession.IsReady)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Coordinator.TryBeginNextPhase();
|
||||
}
|
||||
|
||||
private MapEntityLoadContext BuildMapLoadContext()
|
||||
{
|
||||
List<TowerStatsData> buildTowerStatsSnapshot = new();
|
||||
for (int i = 0; i < Runtime.CombatRunResourceStore.CurrentBuildTowerCount; i++)
|
||||
{
|
||||
if (Runtime.CombatRunResourceStore.TryGetBuildTowerStats(i, out TowerStatsData stats) &&
|
||||
stats != null)
|
||||
{
|
||||
buildTowerStatsSnapshot.Add(stats);
|
||||
}
|
||||
}
|
||||
|
||||
MapData mapData = new MapData(
|
||||
entityId: 0,
|
||||
typeId: 0,
|
||||
levelId: Runtime.CurrentLevel.Id,
|
||||
position: Vector3.zero,
|
||||
initialCoin: Runtime.CombatRunResourceStore.CurrentCoin,
|
||||
buildTowerStatsSnapshot: buildTowerStatsSnapshot,
|
||||
inventorySnapshot: Runtime.CombatRunResourceStore.GetCombatInventorySnapshot(),
|
||||
participantTowerSnapshot: Runtime.CombatRunResourceStore.GetParticipantTowerSnapshot());
|
||||
return new MapEntityLoadContext(
|
||||
mapData,
|
||||
Runtime.CombatRunResourceStore.TryConsumeCoin,
|
||||
Runtime.CombatRunResourceStore.AddCoin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatRewardSelectionState : CombatStateBase
|
||||
{
|
||||
public CombatRewardSelectionState(CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator)
|
||||
: base(runtime, coordinator)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnEnter()
|
||||
{
|
||||
if (Runtime.SettlementContext == null)
|
||||
{
|
||||
Coordinator.EnterFailureFallback("Combat reward selection failed. Settlement context is missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
Coordinator.EnsureRewardSelectFormUseCaseBound();
|
||||
int nextRewardOrdinal = Runtime.NextRewardOrdinal;
|
||||
if (!Runtime.CombatSettlementService.TryPrepareRewardSelection(
|
||||
Runtime.SettlementContext,
|
||||
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||
Coordinator.ResolveCurrentThemeType(),
|
||||
Runtime.NodeContext != null ? Runtime.NodeContext.RunSeed : 0,
|
||||
Runtime.NodeContext != null ? Runtime.NodeContext.SequenceIndex : -1,
|
||||
ref nextRewardOrdinal,
|
||||
Runtime.RewardSelectFormUseCase,
|
||||
Coordinator.OnFullBaseHpRewardSelected,
|
||||
Coordinator.OnFullBaseHpRewardGiveUp))
|
||||
{
|
||||
Coordinator.ChangeState(new CombatFinishFormState(Runtime, Coordinator));
|
||||
return;
|
||||
}
|
||||
|
||||
Runtime.NextRewardOrdinal = nextRewardOrdinal;
|
||||
}
|
||||
|
||||
public override void OnExit()
|
||||
{
|
||||
Coordinator.CloseRewardSelectForm();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Procedure;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatRunningPhaseState : CombatStateBase
|
||||
{
|
||||
private readonly DRLevelPhase _phase;
|
||||
private readonly IReadOnlyList<DRLevelSpawnEntry> _spawnEntries;
|
||||
|
||||
public CombatRunningPhaseState(
|
||||
CombatSchedulerRuntime runtime,
|
||||
CombatSchedulerCoordinator coordinator,
|
||||
DRLevelPhase phase,
|
||||
IReadOnlyList<DRLevelSpawnEntry> spawnEntries) : base(runtime, coordinator)
|
||||
{
|
||||
_phase = phase;
|
||||
_spawnEntries = spawnEntries;
|
||||
}
|
||||
|
||||
public override void OnEnter()
|
||||
{
|
||||
Runtime.EnemyManager.BeginPhase(_phase, _spawnEntries);
|
||||
GameEntry.Event.Fire(
|
||||
Coordinator,
|
||||
CombatProcessEventArgs.Create(
|
||||
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||
Runtime.PhaseLoopRuntime.PhaseCount));
|
||||
GameEntry.Event.Fire(
|
||||
Coordinator,
|
||||
CombatEnemyHpRateChangedEventArgs.Create(
|
||||
Coordinator.ResolveEnemyHpRateMultiplier(
|
||||
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||
Runtime.PhaseLoopRuntime.PhaseCount)));
|
||||
|
||||
if (!Runtime.NodeEnterFired)
|
||||
{
|
||||
Runtime.NodeEnterFired = true;
|
||||
GameEntry.Event.Fire(
|
||||
Coordinator,
|
||||
NodeEnterEventArgs.Create(
|
||||
Runtime.NodeContext?.RunId,
|
||||
Runtime.NodeContext?.NodeId ?? 0,
|
||||
Runtime.NodeContext?.NodeType ?? RunNodeType.None,
|
||||
Runtime.NodeContext?.SequenceIndex ?? -1));
|
||||
}
|
||||
|
||||
Log.Info(
|
||||
"CombatScheduler phase started. Level={0}, Phase={1}, EndType={2}, Entries={3}.",
|
||||
Runtime.CurrentLevel != null ? Runtime.CurrentLevel.Id : 0,
|
||||
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||
_phase.EndType,
|
||||
_spawnEntries != null ? _spawnEntries.Count : 0);
|
||||
}
|
||||
|
||||
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
if (Runtime.PhaseLoopRuntime.CurrentPhase == null)
|
||||
{
|
||||
Coordinator.EnterFailureFallback("CombatScheduler update failed. Current phase is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
Runtime.PhaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds);
|
||||
Runtime.EnemyManager.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
|
||||
if (Coordinator.ShouldEnterSettlementFromActiveState(out bool didCombatWin))
|
||||
{
|
||||
Coordinator.ChangeState(new CombatSettlementState(Runtime, Coordinator, didCombatWin));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Runtime.EnemyManager.IsPhaseSpawnCompleted)
|
||||
{
|
||||
Coordinator.EnterWaitingForPhaseEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatSettlementState : CombatStateBase
|
||||
{
|
||||
private readonly bool _didCombatWin;
|
||||
|
||||
public CombatSettlementState(
|
||||
CombatSchedulerRuntime runtime,
|
||||
CombatSchedulerCoordinator coordinator,
|
||||
bool didCombatWin) : base(runtime, coordinator)
|
||||
{
|
||||
_didCombatWin = didCombatWin;
|
||||
}
|
||||
|
||||
public override void OnEnter()
|
||||
{
|
||||
Runtime.EnemyManager.EndPhase();
|
||||
Runtime.EnemyManager.CleanupTrackedEnemies();
|
||||
Runtime.DidCombatWin = _didCombatWin;
|
||||
Runtime.SettlementContext = Runtime.CombatSettlementService.BuildSettlementContext(
|
||||
_didCombatWin,
|
||||
Runtime.CurrentLevel,
|
||||
Runtime.EnemyManager.DefeatedEnemyCount,
|
||||
Runtime.CombatRunResourceStore);
|
||||
if (Runtime.SettlementContext.Flags.ShouldOpenRewardSelection)
|
||||
{
|
||||
Coordinator.ChangeState(new CombatRewardSelectionState(Runtime, Coordinator));
|
||||
return;
|
||||
}
|
||||
|
||||
Coordinator.ChangeState(new CombatFinishFormState(Runtime, Coordinator));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal abstract class CombatStateBase
|
||||
{
|
||||
protected CombatSchedulerRuntime Runtime { get; }
|
||||
protected CombatSchedulerCoordinator Coordinator { get; }
|
||||
|
||||
protected CombatStateBase(CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator)
|
||||
{
|
||||
Runtime = runtime;
|
||||
Coordinator = coordinator;
|
||||
}
|
||||
|
||||
public virtual void OnInit()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void OnEnter()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void OnExit()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void OnDestroy()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Factory;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatWaitingForPhaseEndState : CombatStateBase
|
||||
{
|
||||
public CombatWaitingForPhaseEndState(CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator)
|
||||
: base(runtime, coordinator)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
_ = realElapseSeconds;
|
||||
|
||||
DRLevelPhase currentPhase = Runtime.PhaseLoopRuntime.CurrentPhase;
|
||||
if (currentPhase == null)
|
||||
{
|
||||
Coordinator.EnterFailureFallback("CombatScheduler waiting phase failed. Current phase is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
Runtime.PhaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds);
|
||||
|
||||
if (Coordinator.ShouldEnterSettlementFromActiveState(out bool didCombatWin))
|
||||
{
|
||||
Coordinator.ChangeState(new CombatSettlementState(Runtime, Coordinator, didCombatWin));
|
||||
return;
|
||||
}
|
||||
|
||||
PhaseEndConditionContext conditionContext = new(
|
||||
currentPhase,
|
||||
Runtime.PhaseLoopRuntime.CurrentPhaseElapsed,
|
||||
Runtime.EnemyManager.IsPhaseSpawnCompleted,
|
||||
Runtime.EnemyManager.AliveEnemyCount,
|
||||
Runtime.EnemyManager.HasAliveBoss);
|
||||
IPhaseEndCondition endCondition = PhaseEndConditionFactory.Create(currentPhase.EndType);
|
||||
if (!endCondition.ShouldExit(conditionContext))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Coordinator.CompleteCurrentPhase();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class CombatWaitingForReturnState : CombatStateBase
|
||||
{
|
||||
private bool _returnRequested;
|
||||
|
||||
public CombatWaitingForReturnState(CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator)
|
||||
: base(runtime, coordinator)
|
||||
{
|
||||
}
|
||||
|
||||
public void RequestReturn()
|
||||
{
|
||||
_returnRequested = true;
|
||||
}
|
||||
|
||||
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
_ = elapseSeconds;
|
||||
_ = realElapseSeconds;
|
||||
|
||||
if (!_returnRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Runtime.SettlementContext == null)
|
||||
{
|
||||
Coordinator.EnterFailureFallback("Combat return failed. Settlement context is missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
Runtime.CombatSettlementService.CommitSettlementInventory(Runtime.SettlementContext);
|
||||
Runtime.LoadSession.Cleanup();
|
||||
Coordinator.CloseCombatFinishForm();
|
||||
Coordinator.CloseRewardSelectForm();
|
||||
Coordinator.CompleteNormalCombatAndNotify(Runtime.DidCombatWin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public readonly struct EnemyDropContext
|
||||
{
|
||||
public EnemyDropContext(DREnemy enemy, int displayPhaseIndex, LevelThemeType themeType)
|
||||
{
|
||||
Enemy = enemy;
|
||||
DisplayPhaseIndex = displayPhaseIndex;
|
||||
ThemeType = themeType;
|
||||
}
|
||||
|
||||
public DREnemy Enemy { get; }
|
||||
|
||||
public int DisplayPhaseIndex { get; }
|
||||
|
||||
public LevelThemeType ThemeType { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public readonly struct EnemyDropResult
|
||||
{
|
||||
public static EnemyDropResult Empty => new(0, 0, null);
|
||||
|
||||
public EnemyDropResult(int coin, int gold, TowerCompItemData lootItem)
|
||||
{
|
||||
Coin = coin;
|
||||
Gold = gold;
|
||||
LootItem = lootItem;
|
||||
}
|
||||
|
||||
public int Coin { get; }
|
||||
|
||||
public int Gold { get; }
|
||||
|
||||
public TowerCompItemData LootItem { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using GeometryTD.DataTable;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal interface ICombatSchedulerPort
|
||||
{
|
||||
DRLevel CurrentLevel { get; }
|
||||
int DisplayPhaseIndex { get; }
|
||||
int PhaseCount { get; }
|
||||
int CurrentCoin { get; }
|
||||
int CurrentBaseHp { get; }
|
||||
bool CanEndCombat { get; }
|
||||
|
||||
void ChangeState(CombatStateBase nextState);
|
||||
bool TryEndCombatByPlayer();
|
||||
bool TryDebugFail(string errorMessage);
|
||||
bool OnCombatFinishReturnRequested();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class BossDeadPhaseEndCondition : IPhaseEndCondition
|
||||
{
|
||||
public PhaseEndType EndType => PhaseEndType.BossDead;
|
||||
|
||||
public bool ShouldExit(in PhaseEndConditionContext context)
|
||||
{
|
||||
return context.IsPhaseSpawnCompleted && !context.HasAliveBoss;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class EnemiesClearedPhaseEndCondition : IPhaseEndCondition
|
||||
{
|
||||
public PhaseEndType EndType => PhaseEndType.EnemiesCleared;
|
||||
|
||||
public bool ShouldExit(in PhaseEndConditionContext context)
|
||||
{
|
||||
return context.IsPhaseSpawnCompleted && context.AliveEnemyCount <= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal interface IPhaseEndCondition
|
||||
{
|
||||
PhaseEndType EndType { get; }
|
||||
|
||||
bool ShouldExit(in PhaseEndConditionContext context);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class NonePhaseEndCondition : IPhaseEndCondition
|
||||
{
|
||||
public PhaseEndType EndType => PhaseEndType.None;
|
||||
|
||||
public bool ShouldExit(in PhaseEndConditionContext context)
|
||||
{
|
||||
if (context.Phase == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.Phase.DurationSeconds > 0 &&
|
||||
context.PhaseElapsedSeconds >= context.Phase.DurationSeconds)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return context.IsPhaseSpawnCompleted && context.AliveEnemyCount <= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
using GeometryTD.DataTable;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal readonly struct PhaseEndConditionContext
|
||||
{
|
||||
public PhaseEndConditionContext(
|
||||
DRLevelPhase phase,
|
||||
float phaseElapsedSeconds,
|
||||
bool isPhaseSpawnCompleted,
|
||||
int aliveEnemyCount,
|
||||
bool hasAliveBoss)
|
||||
{
|
||||
Phase = phase;
|
||||
PhaseElapsedSeconds = phaseElapsedSeconds;
|
||||
IsPhaseSpawnCompleted = isPhaseSpawnCompleted;
|
||||
AliveEnemyCount = aliveEnemyCount;
|
||||
HasAliveBoss = hasAliveBoss;
|
||||
}
|
||||
|
||||
public DRLevelPhase Phase { get; }
|
||||
|
||||
public float PhaseElapsedSeconds { get; }
|
||||
|
||||
public bool IsPhaseSpawnCompleted { get; }
|
||||
|
||||
public int AliveEnemyCount { get; }
|
||||
|
||||
public bool HasAliveBoss { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
using System.Globalization;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class TimeElapsedPhaseEndCondition : IPhaseEndCondition
|
||||
{
|
||||
public PhaseEndType EndType => PhaseEndType.TimeElapsed;
|
||||
|
||||
public bool ShouldExit(in PhaseEndConditionContext context)
|
||||
{
|
||||
if (context.Phase == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return context.PhaseElapsedSeconds >= ResolveTimeElapsedThreshold(context.Phase);
|
||||
}
|
||||
|
||||
private static float ResolveTimeElapsedThreshold(DRLevelPhase phase)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(phase.EndParam) &&
|
||||
float.TryParse(phase.EndParam, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
if (phase.DurationSeconds > 0)
|
||||
{
|
||||
return phase.DurationSeconds;
|
||||
}
|
||||
|
||||
return 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
using System.Collections.Generic;
|
||||
using GeometryTD.DataTable;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class PhaseLoopRuntime
|
||||
{
|
||||
private readonly List<DRLevelPhase> _phases = new();
|
||||
|
||||
private DRLevelPhase _currentPhase;
|
||||
private int _currentPhaseIndex = -1;
|
||||
private int _displayPhaseIndex;
|
||||
private int _completedLoopCount;
|
||||
private bool _canEndCombat;
|
||||
private bool _endCombatRequested;
|
||||
private float _currentPhaseElapsed;
|
||||
|
||||
public DRLevelPhase CurrentPhase => _currentPhase;
|
||||
public int DisplayPhaseIndex => _displayPhaseIndex;
|
||||
public bool CanEndCombat => _canEndCombat;
|
||||
public bool IsEndCombatRequested => _endCombatRequested;
|
||||
public float CurrentPhaseElapsed => _currentPhaseElapsed;
|
||||
public int PhaseCount => _phases.Count;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_phases.Clear();
|
||||
_currentPhase = null;
|
||||
_currentPhaseIndex = -1;
|
||||
_displayPhaseIndex = 0;
|
||||
_completedLoopCount = 0;
|
||||
_canEndCombat = false;
|
||||
_endCombatRequested = false;
|
||||
_currentPhaseElapsed = 0f;
|
||||
}
|
||||
|
||||
public void SetPhases(IReadOnlyList<DRLevelPhase> phases)
|
||||
{
|
||||
_phases.Clear();
|
||||
if (phases == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < phases.Count; i++)
|
||||
{
|
||||
DRLevelPhase phase = phases[i];
|
||||
if (phase != null)
|
||||
{
|
||||
_phases.Add(phase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryRequestEndCombat()
|
||||
{
|
||||
if (!_canEndCombat)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_endCombatRequested = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AdvancePhaseElapsed(float elapseSeconds)
|
||||
{
|
||||
_currentPhaseElapsed += elapseSeconds;
|
||||
}
|
||||
|
||||
public bool TryEnterNextPhase(out DRLevelPhase nextPhase)
|
||||
{
|
||||
nextPhase = null;
|
||||
if (_phases.Count <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_currentPhaseIndex++;
|
||||
if (_currentPhaseIndex >= _phases.Count)
|
||||
{
|
||||
_completedLoopCount++;
|
||||
_canEndCombat = true;
|
||||
|
||||
if (_endCombatRequested)
|
||||
{
|
||||
_currentPhase = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
_currentPhaseIndex = 0;
|
||||
}
|
||||
|
||||
_currentPhase = _phases[_currentPhaseIndex];
|
||||
_displayPhaseIndex = _completedLoopCount * _phases.Count + _currentPhaseIndex + 1;
|
||||
_currentPhaseElapsed = 0f;
|
||||
nextPhase = _currentPhase;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
using System.Collections.Generic;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class CombatSettlementCalculator
|
||||
{
|
||||
private const float FullBaseHpGoldBonusRate = 0.3f;
|
||||
private const float HighBaseHpGoldBonusRate = 0.1f;
|
||||
private const float HighBaseHpThreshold = 0.8f;
|
||||
private const float SettlementTowerEnduranceLoss = 1f;
|
||||
|
||||
public CombatSettlementContext BuildSettlementContext(
|
||||
bool didCombatWin,
|
||||
DRLevel currentLevel,
|
||||
int defeatedEnemyCount,
|
||||
CombatRunResourceStore resourceStore)
|
||||
{
|
||||
bool shouldOpenFullBaseHpRewardSelect = false;
|
||||
ResolveSettlementByBaseHp(
|
||||
didCombatWin,
|
||||
currentLevel,
|
||||
resourceStore,
|
||||
out int currentBaseHp,
|
||||
out int maxBaseHp,
|
||||
out int levelRewardGold,
|
||||
out float bonusRate,
|
||||
out int bonusGold,
|
||||
out shouldOpenFullBaseHpRewardSelect);
|
||||
|
||||
CombatSettlementContext settlementContext = new CombatSettlementContext();
|
||||
settlementContext.Result.DidCombatWin = didCombatWin;
|
||||
settlementContext.Result.FinalCoin = Mathf.Max(0, resourceStore != null ? resourceStore.CurrentCoin : 0);
|
||||
settlementContext.Result.FinalBaseHp = currentBaseHp;
|
||||
settlementContext.Result.MaxBaseHp = maxBaseHp;
|
||||
settlementContext.Result.DefeatedEnemyCount = Mathf.Max(0, defeatedEnemyCount);
|
||||
settlementContext.Result.GainedGold = Mathf.Max(0, resourceStore != null ? resourceStore.GainedGold : 0);
|
||||
settlementContext.Result.RewardInventory = resourceStore != null
|
||||
? resourceStore.GetRewardInventorySnapshot()
|
||||
: new BackpackInventoryData();
|
||||
PopulateEnduranceSettlement(settlementContext, resourceStore);
|
||||
settlementContext.Flags.ShouldOpenRewardSelection = shouldOpenFullBaseHpRewardSelect;
|
||||
settlementContext.Flags.DidEnterRewardSelection = false;
|
||||
settlementContext.Summary.DefeatedEnemyCount = settlementContext.Result.DefeatedEnemyCount;
|
||||
settlementContext.Summary.GainedGold = settlementContext.Result.GainedGold;
|
||||
settlementContext.Summary.RewardInventory = settlementContext.Result.RewardInventory;
|
||||
|
||||
Log.Info(
|
||||
"Combat settlement resolved. Level={0}, BaseHp={1}/{2}, LevelReward={3}, BonusRate={4:P0}, BonusGold={5}, FullHpRewardSelect={6}, EnduranceTargets={7}.",
|
||||
currentLevel != null ? currentLevel.Id : 0,
|
||||
currentBaseHp,
|
||||
maxBaseHp,
|
||||
levelRewardGold,
|
||||
bonusRate,
|
||||
bonusGold,
|
||||
shouldOpenFullBaseHpRewardSelect,
|
||||
settlementContext.Result.Endurance.TargetTowerInstanceIds.Count);
|
||||
return settlementContext;
|
||||
}
|
||||
|
||||
private static void ResolveSettlementByBaseHp(
|
||||
bool didCombatWin,
|
||||
DRLevel currentLevel,
|
||||
CombatRunResourceStore resourceStore,
|
||||
out int currentBaseHp,
|
||||
out int maxBaseHp,
|
||||
out int levelRewardGold,
|
||||
out float bonusRate,
|
||||
out int bonusGold,
|
||||
out bool shouldOpenFullBaseHpRewardSelect)
|
||||
{
|
||||
currentBaseHp = Mathf.Max(0, resourceStore != null ? resourceStore.CurrentBaseHp : 0);
|
||||
maxBaseHp = resourceStore != null ? Mathf.Max(0, resourceStore.MaxBaseHp) : 0;
|
||||
if (maxBaseHp > 0)
|
||||
{
|
||||
currentBaseHp = Mathf.Clamp(currentBaseHp, 0, maxBaseHp);
|
||||
}
|
||||
|
||||
levelRewardGold = currentLevel != null ? Mathf.Max(0, currentLevel.RewardGold) : 0;
|
||||
bonusRate = 0f;
|
||||
bonusGold = 0;
|
||||
shouldOpenFullBaseHpRewardSelect = false;
|
||||
|
||||
if (!didCombatWin || resourceStore == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (maxBaseHp > 0 && currentBaseHp >= maxBaseHp)
|
||||
{
|
||||
bonusRate = FullBaseHpGoldBonusRate;
|
||||
shouldOpenFullBaseHpRewardSelect = true;
|
||||
}
|
||||
else if (maxBaseHp > 0)
|
||||
{
|
||||
float hpRate = (float)currentBaseHp / maxBaseHp;
|
||||
if (hpRate >= HighBaseHpThreshold)
|
||||
{
|
||||
bonusRate = HighBaseHpGoldBonusRate;
|
||||
}
|
||||
}
|
||||
|
||||
int goldForBonusCalculation = Mathf.Max(0, resourceStore.GainedGold) + levelRewardGold;
|
||||
bonusGold = bonusRate > 0f ? Mathf.FloorToInt(goldForBonusCalculation * bonusRate) : 0;
|
||||
int settlementGold = levelRewardGold + bonusGold;
|
||||
resourceStore.AddSettlementGold(settlementGold);
|
||||
}
|
||||
|
||||
private static void PopulateEnduranceSettlement(
|
||||
CombatSettlementContext settlementContext,
|
||||
CombatRunResourceStore resourceStore)
|
||||
{
|
||||
if (settlementContext == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CombatSettlementEnduranceResult enduranceResult = settlementContext.Result.Endurance;
|
||||
enduranceResult.TargetTowerInstanceIds.Clear();
|
||||
enduranceResult.EnduranceLossPerComponent = SettlementTowerEnduranceLoss;
|
||||
|
||||
IReadOnlyList<long> participantTowerIds = resourceStore?.GetParticipantTowerInstanceIdSnapshot();
|
||||
if (participantTowerIds == null || participantTowerIds.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < participantTowerIds.Count; i++)
|
||||
{
|
||||
long towerId = participantTowerIds[i];
|
||||
if (towerId > 0)
|
||||
{
|
||||
enduranceResult.TargetTowerInstanceIds.Add(towerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
using GeometryTD.Definition;
|
||||
using GeometryTD.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class CombatSettlementCommitter
|
||||
{
|
||||
public void CommitSettlementInventory(CombatSettlementContext settlementContext)
|
||||
{
|
||||
if (settlementContext == null || settlementContext.Flags.IsCommitted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BackpackInventoryData rewardInventory = settlementContext.Result.RewardInventory ?? new BackpackInventoryData();
|
||||
GameEntry.PlayerInventory?.MergeInventory(rewardInventory);
|
||||
settlementContext.Result.RewardInventory = rewardInventory;
|
||||
settlementContext.Summary.RewardInventory = rewardInventory;
|
||||
settlementContext.Result.Endurance.AffectedTowerCount = ApplyDeferredSettlementEndurance(settlementContext);
|
||||
settlementContext.Flags.IsCommitted = true;
|
||||
}
|
||||
|
||||
public void ApplySelectedReward(CombatSettlementContext settlementContext, RewardSelectItemRawData selectedReward)
|
||||
{
|
||||
if (settlementContext?.Result.RewardInventory == null || selectedReward?.SourceItem == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TryAppendRewardComponent(settlementContext.Result.RewardInventory, selectedReward.SourceItem);
|
||||
settlementContext.Summary.RewardInventory = settlementContext.Result.RewardInventory;
|
||||
}
|
||||
|
||||
private static int ApplyDeferredSettlementEndurance(CombatSettlementContext settlementContext)
|
||||
{
|
||||
if (settlementContext == null ||
|
||||
settlementContext.Result.Endurance.EnduranceLossPerComponent <= 0f ||
|
||||
settlementContext.Result.Endurance.TargetTowerInstanceIds.Count <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
PlayerInventoryComponent inventory = GameEntry.PlayerInventory;
|
||||
if (inventory == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return inventory.ReduceTowerEndurance(
|
||||
settlementContext.Result.Endurance.TargetTowerInstanceIds,
|
||||
settlementContext.Result.Endurance.EnduranceLossPerComponent);
|
||||
}
|
||||
|
||||
private static bool TryAppendRewardComponent(BackpackInventoryData rewardInventory, TowerCompItemData selectedItem)
|
||||
{
|
||||
if (rewardInventory == null || selectedItem == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedItem is MuzzleCompItemData muzzleComp)
|
||||
{
|
||||
rewardInventory.MuzzleComponents.Add(muzzleComp);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (selectedItem is BearingCompItemData bearingComp)
|
||||
{
|
||||
rewardInventory.BearingComponents.Add(bearingComp);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (selectedItem is BaseCompItemData baseComp)
|
||||
{
|
||||
rewardInventory.BaseComponents.Add(baseComp);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
using System;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.DataTable;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class EnemyConfigProvider
|
||||
{
|
||||
private const int DefaultEnemyConfigId = 1;
|
||||
|
||||
private IDataTable<DREnemy> _drEnemy;
|
||||
private bool _enemyConfigMissingLogged;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_drEnemy = null;
|
||||
_enemyConfigMissingLogged = false;
|
||||
}
|
||||
|
||||
public DREnemy GetEnemyConfig(int enemyId)
|
||||
{
|
||||
if (_drEnemy == null)
|
||||
{
|
||||
_drEnemy = GameEntry.DataTable.GetDataTable<DREnemy>();
|
||||
if (_drEnemy == null)
|
||||
{
|
||||
if (!_enemyConfigMissingLogged)
|
||||
{
|
||||
Log.Warning("EnemyConfigProvider can not find DREnemy data table.");
|
||||
_enemyConfigMissingLogged = true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (enemyId > 0)
|
||||
{
|
||||
DREnemy targetConfig = _drEnemy.GetDataRow(enemyId);
|
||||
if (targetConfig != null)
|
||||
{
|
||||
return targetConfig;
|
||||
}
|
||||
}
|
||||
|
||||
DREnemy defaultConfig = _drEnemy.GetDataRow(DefaultEnemyConfigId);
|
||||
if (defaultConfig != null)
|
||||
{
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
DREnemy[] allConfigs = _drEnemy.GetAllDataRows();
|
||||
if (allConfigs.Length > 0)
|
||||
{
|
||||
return allConfigs[0];
|
||||
}
|
||||
|
||||
if (!_enemyConfigMissingLogged)
|
||||
{
|
||||
Log.Warning("EnemyConfigProvider found no enemy configs.");
|
||||
_enemyConfigMissingLogged = true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public int ResolveScaledEnemyBaseHp(int baseHp, CombatScheduler combatScheduler)
|
||||
{
|
||||
int resolvedBaseHp = Mathf.Max(1, baseHp);
|
||||
int completedLoopCount = ResolveCompletedLoopCount(combatScheduler);
|
||||
if (completedLoopCount <= 0)
|
||||
{
|
||||
return resolvedBaseHp;
|
||||
}
|
||||
|
||||
double scaled = resolvedBaseHp * Math.Pow(2d, completedLoopCount);
|
||||
if (scaled >= int.MaxValue)
|
||||
{
|
||||
return int.MaxValue;
|
||||
}
|
||||
|
||||
return Math.Max(1, (int)Math.Round(scaled));
|
||||
}
|
||||
|
||||
private static int ResolveCompletedLoopCount(CombatScheduler combatScheduler)
|
||||
{
|
||||
if (combatScheduler == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int phaseCount = combatScheduler.PhaseCount;
|
||||
int displayPhaseIndex = combatScheduler.DisplayPhaseIndex;
|
||||
if (phaseCount <= 0 || displayPhaseIndex <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Mathf.Max(0, (displayPhaseIndex - 1) / phaseCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
using System.Collections.Generic;
|
||||
using GeometryTD.DataTable;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class EnemyLifecycleTracker
|
||||
{
|
||||
private readonly HashSet<int> _aliveBossEntityIds = new();
|
||||
private readonly HashSet<int> _trackedEnemyEntityIds = new();
|
||||
private readonly Dictionary<int, DREnemy> _trackedEnemyConfigByEntityId = new();
|
||||
private readonly Dictionary<int, bool> _bossFlagsByEntityId = new();
|
||||
|
||||
public int AliveEnemyCount { get; private set; }
|
||||
public bool HasAliveBoss => _aliveBossEntityIds.Count > 0;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_aliveBossEntityIds.Clear();
|
||||
_bossFlagsByEntityId.Clear();
|
||||
_trackedEnemyEntityIds.Clear();
|
||||
_trackedEnemyConfigByEntityId.Clear();
|
||||
AliveEnemyCount = 0;
|
||||
}
|
||||
|
||||
public void TrackEnemy(int entityId, DREnemy enemyConfig, bool isBoss)
|
||||
{
|
||||
_trackedEnemyEntityIds.Add(entityId);
|
||||
_trackedEnemyConfigByEntityId[entityId] = enemyConfig;
|
||||
_bossFlagsByEntityId[entityId] = isBoss;
|
||||
}
|
||||
|
||||
public bool Contains(int entityId)
|
||||
{
|
||||
return _trackedEnemyEntityIds.Contains(entityId);
|
||||
}
|
||||
|
||||
public void HandleShowSuccess(int entityId)
|
||||
{
|
||||
if (_trackedEnemyEntityIds.Contains(entityId))
|
||||
{
|
||||
AliveEnemyCount++;
|
||||
if (_bossFlagsByEntityId.TryGetValue(entityId, out bool isBoss) && isBoss)
|
||||
{
|
||||
_aliveBossEntityIds.Add(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleShowFailure(int entityId)
|
||||
{
|
||||
_aliveBossEntityIds.Remove(entityId);
|
||||
_bossFlagsByEntityId.Remove(entityId);
|
||||
_trackedEnemyEntityIds.Remove(entityId);
|
||||
_trackedEnemyConfigByEntityId.Remove(entityId);
|
||||
}
|
||||
|
||||
public bool TryHandleHideComplete(int entityId, out DREnemy enemyConfig)
|
||||
{
|
||||
enemyConfig = null;
|
||||
if (!_trackedEnemyEntityIds.Remove(entityId))
|
||||
{
|
||||
_aliveBossEntityIds.Remove(entityId);
|
||||
_bossFlagsByEntityId.Remove(entityId);
|
||||
_trackedEnemyConfigByEntityId.Remove(entityId);
|
||||
return false;
|
||||
}
|
||||
|
||||
_trackedEnemyConfigByEntityId.TryGetValue(entityId, out enemyConfig);
|
||||
_aliveBossEntityIds.Remove(entityId);
|
||||
_bossFlagsByEntityId.Remove(entityId);
|
||||
_trackedEnemyConfigByEntityId.Remove(entityId);
|
||||
AliveEnemyCount = Mathf.Max(0, AliveEnemyCount - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void CopyTrackedEntityIdsTo(List<int> buffer)
|
||||
{
|
||||
if (buffer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
buffer.Clear();
|
||||
foreach (int trackedEnemyEntityId in _trackedEnemyEntityIds)
|
||||
{
|
||||
buffer.Add(trackedEnemyEntityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
using System.Collections.Generic;
|
||||
using GameFramework.Event;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class EnemyManager
|
||||
{
|
||||
private readonly List<int> _trackedEnemyIdBuffer = new();
|
||||
private readonly EnemySpawnDirector _enemySpawnDirector = new();
|
||||
private readonly EnemyConfigProvider _enemyConfigProvider = new();
|
||||
private readonly EnemySpawnPathResolver _enemySpawnPathResolver = new();
|
||||
private readonly EnemyLifecycleTracker _enemyLifecycleTracker = new();
|
||||
|
||||
private CombatScheduler _combatScheduler;
|
||||
private EntityComponent _entity;
|
||||
|
||||
private int _defeatedEnemyCount;
|
||||
private bool _initialized;
|
||||
|
||||
public int AliveEnemyCount => _enemyLifecycleTracker.AliveEnemyCount;
|
||||
public int DefeatedEnemyCount => _defeatedEnemyCount;
|
||||
public bool HasAliveBoss => _enemyLifecycleTracker.HasAliveBoss;
|
||||
public bool IsPhaseSpawnCompleted => _enemySpawnDirector.IsPhaseSpawnCompleted;
|
||||
public bool IsPhaseRunning => _enemySpawnDirector.IsPhaseRunning;
|
||||
|
||||
public void OnInit(CombatScheduler combatScheduler)
|
||||
{
|
||||
_combatScheduler = combatScheduler;
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_entity = GameEntry.Entity;
|
||||
_defeatedEnemyCount = 0;
|
||||
_enemySpawnDirector.Reset();
|
||||
_enemyConfigProvider.Reset();
|
||||
_enemySpawnPathResolver.Reset();
|
||||
_trackedEnemyIdBuffer.Clear();
|
||||
_enemyLifecycleTracker.Reset();
|
||||
|
||||
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||
GameEntry.Event.Subscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
|
||||
GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
public void BeginPhase(DRLevelPhase phase, IReadOnlyList<DRLevelSpawnEntry> spawnEntries)
|
||||
{
|
||||
if (!_initialized || _combatScheduler == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = phase;
|
||||
EndPhase();
|
||||
_enemySpawnPathResolver.RefreshCache(_combatScheduler, true);
|
||||
_enemySpawnDirector.BeginPhase(spawnEntries);
|
||||
}
|
||||
|
||||
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
if (!_initialized || _combatScheduler == null || !_enemySpawnDirector.IsPhaseRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_enemySpawnPathResolver.RefreshCache(_combatScheduler, false);
|
||||
_enemySpawnDirector.OnUpdate(elapseSeconds, SpawnEnemies);
|
||||
}
|
||||
|
||||
public void EndPhase()
|
||||
{
|
||||
_enemySpawnDirector.EndPhase();
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
_combatScheduler = null;
|
||||
return;
|
||||
}
|
||||
|
||||
CleanupTrackedEnemies();
|
||||
EndPhase();
|
||||
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||
GameEntry.Event.Unsubscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
|
||||
GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||
|
||||
_enemySpawnPathResolver.Reset();
|
||||
_trackedEnemyIdBuffer.Clear();
|
||||
_enemyLifecycleTracker.Reset();
|
||||
_defeatedEnemyCount = 0;
|
||||
_enemyConfigProvider.Reset();
|
||||
_combatScheduler = null;
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
public void ResetCombatStats()
|
||||
{
|
||||
_defeatedEnemyCount = 0;
|
||||
}
|
||||
|
||||
public void CleanupTrackedEnemies()
|
||||
{
|
||||
_enemyLifecycleTracker.CopyTrackedEntityIdsTo(_trackedEnemyIdBuffer);
|
||||
if (_trackedEnemyIdBuffer.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_enemyLifecycleTracker.Reset();
|
||||
|
||||
if (_entity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _trackedEnemyIdBuffer.Count; i++)
|
||||
{
|
||||
int trackedEnemyEntityId = _trackedEnemyIdBuffer[i];
|
||||
if (_entity.HasEntity(trackedEnemyEntityId) || _entity.IsLoadingEntity(trackedEnemyEntityId))
|
||||
{
|
||||
_entity.HideEntity(trackedEnemyEntityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnEnemies(DRLevelSpawnEntry entry, int spawnCount)
|
||||
{
|
||||
if (spawnCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_enemySpawnPathResolver.TryResolveSpawnPath(_combatScheduler, entry.SpawnPointId, out IReadOnlyList<Vector3> pathPoints))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DREnemy enemyConfig = _enemyConfigProvider.GetEnemyConfig(entry.EnemyId);
|
||||
if (enemyConfig == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int scaledBaseHp = _enemyConfigProvider.ResolveScaledEnemyBaseHp(enemyConfig.BaseHp, _combatScheduler);
|
||||
bool isBoss = entry.EntryType == Definition.EntryType.Boss;
|
||||
|
||||
for (int i = 0; i < spawnCount; i++)
|
||||
{
|
||||
int enemyEntityId = _entity.GenerateSerialId();
|
||||
_enemyLifecycleTracker.TrackEnemy(enemyEntityId, enemyConfig, isBoss);
|
||||
EnemyData enemyData = new EnemyData(
|
||||
enemyEntityId,
|
||||
enemyConfig.EntityId,
|
||||
pathPoints[0],
|
||||
scaledBaseHp,
|
||||
enemyConfig.Speed,
|
||||
pathPoints);
|
||||
_entity.ShowEnemy(enemyData);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShowEntitySuccess(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is ShowEntitySuccessEventArgs ne)) return;
|
||||
|
||||
if (ne.EntityLogicType == typeof(EnemyEntity) &&
|
||||
_enemyLifecycleTracker.Contains(ne.Entity.Id))
|
||||
{
|
||||
_enemyLifecycleTracker.HandleShowSuccess(ne.Entity.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShowEntityFailure(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is ShowEntityFailureEventArgs ne))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ne.EntityLogicType != typeof(EnemyEntity))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_enemyLifecycleTracker.HandleShowFailure(ne.EntityId);
|
||||
}
|
||||
|
||||
private void OnHideEntityComplete(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is HideEntityCompleteEventArgs ne))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_enemyLifecycleTracker.TryHandleHideComplete(ne.EntityId, out DREnemy enemyConfig))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool wasKilled = EnemyEntity.TryConsumeKilledFlag(ne.EntityId);
|
||||
bool isCombatRunning = _combatScheduler != null && _combatScheduler.IsRunning;
|
||||
|
||||
if (isCombatRunning && wasKilled && enemyConfig != null)
|
||||
{
|
||||
_defeatedEnemyCount++;
|
||||
_combatScheduler.OnEnemyDefeated(enemyConfig);
|
||||
}
|
||||
else if (isCombatRunning && !wasKilled && enemyConfig != null)
|
||||
{
|
||||
_combatScheduler.OnEnemyReachedBase(enemyConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class EnemySpawnDirector
|
||||
{
|
||||
private sealed class SpawnEntryRuntime
|
||||
{
|
||||
public DRLevelSpawnEntry Entry;
|
||||
public bool Completed;
|
||||
public float NextTriggerTime;
|
||||
public float EndTime;
|
||||
public int RemainingCount;
|
||||
}
|
||||
|
||||
private const float MinStreamInterval = 0.05f;
|
||||
private const float MinBurstGap = 0.01f;
|
||||
|
||||
private readonly List<SpawnEntryRuntime> _spawnRuntimes = new();
|
||||
private float _phaseElapsed;
|
||||
|
||||
public bool IsPhaseSpawnCompleted { get; private set; } = true;
|
||||
public bool IsPhaseRunning { get; private set; }
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
EndPhase();
|
||||
}
|
||||
|
||||
public void BeginPhase(IReadOnlyList<DRLevelSpawnEntry> spawnEntries)
|
||||
{
|
||||
EndPhase();
|
||||
|
||||
_phaseElapsed = 0f;
|
||||
IsPhaseRunning = true;
|
||||
IsPhaseSpawnCompleted = false;
|
||||
if (spawnEntries != null)
|
||||
{
|
||||
for (int i = 0; i < spawnEntries.Count; i++)
|
||||
{
|
||||
SpawnEntryRuntime runtime = BuildSpawnRuntime(spawnEntries[i]);
|
||||
if (runtime != null)
|
||||
{
|
||||
_spawnRuntimes.Add(runtime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IsPhaseSpawnCompleted = _spawnRuntimes.Count <= 0;
|
||||
}
|
||||
|
||||
public void OnUpdate(float elapseSeconds, Action<DRLevelSpawnEntry, int> spawnAction)
|
||||
{
|
||||
if (!IsPhaseRunning || spawnAction == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_phaseElapsed += elapseSeconds;
|
||||
UpdateSpawnRuntimes(spawnAction);
|
||||
}
|
||||
|
||||
public void EndPhase()
|
||||
{
|
||||
IsPhaseRunning = false;
|
||||
_phaseElapsed = 0f;
|
||||
_spawnRuntimes.Clear();
|
||||
IsPhaseSpawnCompleted = true;
|
||||
}
|
||||
|
||||
private SpawnEntryRuntime BuildSpawnRuntime(DRLevelSpawnEntry entry)
|
||||
{
|
||||
if (entry == null || entry.EntryType == EntryType.None)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
SpawnEntryRuntime runtime = new SpawnEntryRuntime
|
||||
{
|
||||
Entry = entry,
|
||||
Completed = false,
|
||||
NextTriggerTime = Mathf.Max(0f, entry.StartTime),
|
||||
EndTime = Mathf.Max(0f, entry.StartTime),
|
||||
RemainingCount = Mathf.Max(0, entry.Count)
|
||||
};
|
||||
|
||||
switch (entry.EntryType)
|
||||
{
|
||||
case EntryType.Stream:
|
||||
{
|
||||
float duration = Mathf.Max(0f, entry.Duration);
|
||||
runtime.EndTime = duration > 0f ? runtime.NextTriggerTime + duration : runtime.NextTriggerTime;
|
||||
runtime.Completed = entry.Count <= 0;
|
||||
return runtime;
|
||||
}
|
||||
case EntryType.Burst:
|
||||
case EntryType.Boss:
|
||||
runtime.Completed = runtime.RemainingCount <= 0;
|
||||
return runtime;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSpawnRuntimes(Action<DRLevelSpawnEntry, int> spawnAction)
|
||||
{
|
||||
bool allCompleted = true;
|
||||
for (int i = 0; i < _spawnRuntimes.Count; i++)
|
||||
{
|
||||
SpawnEntryRuntime runtime = _spawnRuntimes[i];
|
||||
if (runtime.Completed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (runtime.Entry.EntryType)
|
||||
{
|
||||
case EntryType.Stream:
|
||||
ProcessStreamRuntime(runtime, spawnAction);
|
||||
break;
|
||||
case EntryType.Burst:
|
||||
case EntryType.Boss:
|
||||
ProcessBurstRuntime(runtime, spawnAction);
|
||||
break;
|
||||
default:
|
||||
runtime.Completed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!runtime.Completed)
|
||||
{
|
||||
allCompleted = false;
|
||||
}
|
||||
}
|
||||
|
||||
IsPhaseSpawnCompleted = allCompleted;
|
||||
}
|
||||
|
||||
private void ProcessStreamRuntime(SpawnEntryRuntime runtime, Action<DRLevelSpawnEntry, int> spawnAction)
|
||||
{
|
||||
if (_phaseElapsed < runtime.NextTriggerTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int countPerWave = Mathf.Max(0, runtime.Entry.Count);
|
||||
if (countPerWave <= 0)
|
||||
{
|
||||
runtime.Completed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
float interval = runtime.Entry.Interval > 0f ? runtime.Entry.Interval : MinStreamInterval;
|
||||
while (_phaseElapsed >= runtime.NextTriggerTime && runtime.NextTriggerTime <= runtime.EndTime)
|
||||
{
|
||||
spawnAction(runtime.Entry, countPerWave);
|
||||
runtime.NextTriggerTime += interval;
|
||||
}
|
||||
|
||||
if (runtime.NextTriggerTime > runtime.EndTime)
|
||||
{
|
||||
runtime.Completed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessBurstRuntime(SpawnEntryRuntime runtime, Action<DRLevelSpawnEntry, int> spawnAction)
|
||||
{
|
||||
if (_phaseElapsed < runtime.NextTriggerTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (runtime.RemainingCount <= 0)
|
||||
{
|
||||
runtime.Completed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
float gap = runtime.Entry.Gap;
|
||||
if (gap <= 0f)
|
||||
{
|
||||
spawnAction(runtime.Entry, runtime.RemainingCount);
|
||||
runtime.RemainingCount = 0;
|
||||
runtime.Completed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
gap = Mathf.Max(gap, MinBurstGap);
|
||||
while (_phaseElapsed >= runtime.NextTriggerTime && runtime.RemainingCount > 0)
|
||||
{
|
||||
spawnAction(runtime.Entry, 1);
|
||||
runtime.RemainingCount--;
|
||||
runtime.NextTriggerTime += gap;
|
||||
}
|
||||
|
||||
runtime.Completed = runtime.RemainingCount <= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
using System.Collections.Generic;
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD.Map;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class EnemySpawnPathResolver
|
||||
{
|
||||
private readonly List<Spawner> _spawners = new();
|
||||
private readonly Dictionary<int, Spawner> _spawnerByOrder = new();
|
||||
private readonly List<Vector3> _pathBuffer = new();
|
||||
|
||||
private int _nextSpawnerIndex;
|
||||
private int _currentMapEntityId;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_spawners.Clear();
|
||||
_spawnerByOrder.Clear();
|
||||
_pathBuffer.Clear();
|
||||
_nextSpawnerIndex = 0;
|
||||
_currentMapEntityId = 0;
|
||||
}
|
||||
|
||||
public void RefreshCache(CombatScheduler combatScheduler, bool force)
|
||||
{
|
||||
MapEntity currentMap = combatScheduler != null ? combatScheduler.CurrentMap : null;
|
||||
if (currentMap == null)
|
||||
{
|
||||
Reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!force && _currentMapEntityId == currentMap.Id && _spawners.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_spawners.Clear();
|
||||
_spawnerByOrder.Clear();
|
||||
_nextSpawnerIndex = 0;
|
||||
_currentMapEntityId = currentMap.Id;
|
||||
|
||||
Spawner[] mapSpawners = currentMap.Spawners;
|
||||
for (int i = 0; i < mapSpawners.Length; i++)
|
||||
{
|
||||
Spawner spawner = mapSpawners[i];
|
||||
if (spawner == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!currentMap.TryGetDefaultPathCells(spawner, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_spawners.Add(spawner);
|
||||
if (spawner.SpawnOrder > 0 && !_spawnerByOrder.ContainsKey(spawner.SpawnOrder))
|
||||
{
|
||||
_spawnerByOrder[spawner.SpawnOrder] = spawner;
|
||||
}
|
||||
}
|
||||
|
||||
_spawners.Sort((left, right) => left.SpawnOrder.CompareTo(right.SpawnOrder));
|
||||
}
|
||||
|
||||
public bool TryResolveSpawnPath(CombatScheduler combatScheduler, int spawnPointId, out IReadOnlyList<Vector3> pathPoints)
|
||||
{
|
||||
pathPoints = null;
|
||||
MapEntity currentMap = combatScheduler != null ? combatScheduler.CurrentMap : null;
|
||||
if (currentMap == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Spawner spawner = ResolveSpawner(spawnPointId);
|
||||
if (spawner == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!currentMap.TryFindPathWorldPoints(spawner, null, _pathBuffer) || _pathBuffer.Count <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pathPoints = _pathBuffer;
|
||||
return true;
|
||||
}
|
||||
|
||||
private Spawner ResolveSpawner(int spawnPointId)
|
||||
{
|
||||
if (spawnPointId > 0 && _spawnerByOrder.TryGetValue(spawnPointId, out Spawner mappedSpawner))
|
||||
{
|
||||
return mappedSpawner;
|
||||
}
|
||||
|
||||
if (_spawners.Count <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Spawner fallbackSpawner = _spawners[_nextSpawnerIndex % _spawners.Count];
|
||||
_nextSpawnerIndex++;
|
||||
return fallbackSpawner;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Factory;
|
||||
using GeometryTD.Procedure;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class EventNodeComponent : GameFrameworkComponent
|
||||
{
|
||||
private RunNodeExecutionContext _activeContext;
|
||||
private EventItem _activeEvent;
|
||||
|
||||
private readonly List<EventItem> _eventItems = new List<EventItem>();
|
||||
|
||||
private EventFormUseCase _eventFormUseCase;
|
||||
private bool _initialized;
|
||||
|
||||
public void OnInit()
|
||||
{
|
||||
_eventItems.Clear();
|
||||
|
||||
IDataTable<DREvent> dtEvent = GameEntry.DataTable.GetDataTable<DREvent>();
|
||||
if (dtEvent == null)
|
||||
{
|
||||
Log.Warning("Event data table is not loaded.");
|
||||
_initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
DREvent[] rows = dtEvent.GetAllDataRows();
|
||||
foreach (var drEvent in rows)
|
||||
{
|
||||
EventOption[] options = ParseOptions(drEvent.OptionsRaw);
|
||||
_eventItems.Add(new EventItem(drEvent.Id, drEvent.Title, drEvent.Description, options));
|
||||
}
|
||||
|
||||
if (_eventFormUseCase == null)
|
||||
{
|
||||
_eventFormUseCase = new EventFormUseCase();
|
||||
}
|
||||
|
||||
GameEntry.UIRouter.BindUIUseCase(UIFormType.EventForm, _eventFormUseCase);
|
||||
|
||||
_initialized = true;
|
||||
Log.Info("EventNodeComponent initialized with {0} events.", _eventItems.Count);
|
||||
}
|
||||
|
||||
public void StartEvent(RunNodeExecutionContext context = null)
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
OnInit();
|
||||
}
|
||||
|
||||
if (_eventItems.Count <= 0)
|
||||
{
|
||||
Log.Warning("EventNodeComponent has no event data.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_eventFormUseCase == null)
|
||||
{
|
||||
Log.Warning("EventNodeComponent StartEvent failed. Event form is not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
_activeContext = context != null ? context.Clone() : null;
|
||||
_activeEvent = SelectActiveEvent(_activeContext);
|
||||
if (_activeEvent == null)
|
||||
{
|
||||
Log.Warning("EventNodeComponent StartEvent failed. No active event could be resolved.");
|
||||
return;
|
||||
}
|
||||
|
||||
_eventFormUseCase.BindEvent(_activeEvent, _activeContext);
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.EventForm);
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeEnterEventArgs.Create(
|
||||
_activeContext?.RunId,
|
||||
_activeContext?.NodeId ?? 0,
|
||||
_activeContext?.NodeType ?? RunNodeType.None,
|
||||
_activeContext?.SequenceIndex ?? -1));
|
||||
}
|
||||
|
||||
public void EndEvent()
|
||||
{
|
||||
GameEntry.UIRouter.CloseUI(UIFormType.EventForm);
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeCompleteEventArgs.Create(
|
||||
_activeContext?.RunId,
|
||||
_activeContext?.NodeId ?? 0,
|
||||
_activeContext?.NodeType ?? RunNodeType.None,
|
||||
_activeContext?.SequenceIndex ?? -1,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
true,
|
||||
_activeContext != null
|
||||
? _activeContext.CreateCompletionSnapshot(
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
|
||||
: null));
|
||||
ClearActiveNodeContext();
|
||||
}
|
||||
|
||||
private void ClearActiveNodeContext()
|
||||
{
|
||||
_activeContext = null;
|
||||
_activeEvent = null;
|
||||
_eventFormUseCase?.Clear();
|
||||
}
|
||||
|
||||
private static EventOption[] ParseOptions(string optionsRaw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(optionsRaw))
|
||||
{
|
||||
return System.Array.Empty<EventOption>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
JArray array = JArray.Parse(optionsRaw);
|
||||
List<EventOption> options = new List<EventOption>(array.Count);
|
||||
|
||||
for (int i = 0; i < array.Count; i++)
|
||||
{
|
||||
if (!(array[i] is JObject optionObj))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string optionText = optionObj.Value<string>("optionText") ?? string.Empty;
|
||||
float probability = optionObj.Value<float?>("probability") ?? 1f;
|
||||
|
||||
EventRequirementBase[] requirements = ParseRequirements(optionObj["requirements"] as JArray);
|
||||
EventEffectBase[] costEffects = ParseEffects(optionObj["costEffects"] as JArray, probability);
|
||||
EventEffectBase[] rewardEffects = ParseEffects(optionObj["rewardEffects"] as JArray, probability);
|
||||
|
||||
options.Add(new EventOption(optionText, requirements, costEffects, rewardEffects, probability));
|
||||
}
|
||||
|
||||
return options.ToArray();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Warning("Failed to parse event options json. {0}", e.Message);
|
||||
return System.Array.Empty<EventOption>();
|
||||
}
|
||||
}
|
||||
|
||||
private static EventRequirementBase[] ParseRequirements(JArray requirementsArray)
|
||||
{
|
||||
if (requirementsArray == null || requirementsArray.Count == 0)
|
||||
{
|
||||
return System.Array.Empty<EventRequirementBase>();
|
||||
}
|
||||
|
||||
List<EventRequirementBase> requirements = new List<EventRequirementBase>(requirementsArray.Count);
|
||||
for (int i = 0; i < requirementsArray.Count; i++)
|
||||
{
|
||||
if (!(requirementsArray[i] is JObject reqObj))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string type = reqObj.Value<string>("type");
|
||||
JObject param = reqObj["param"] as JObject;
|
||||
EventRequirementBase requirement = EventRequirementFactory.Create(type, param);
|
||||
if (requirement != null)
|
||||
{
|
||||
requirements.Add(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
return requirements.ToArray();
|
||||
}
|
||||
|
||||
private static EventEffectBase[] ParseEffects(JArray effectsArray, float probability)
|
||||
{
|
||||
if (effectsArray == null || effectsArray.Count == 0)
|
||||
{
|
||||
return System.Array.Empty<EventEffectBase>();
|
||||
}
|
||||
|
||||
List<EventEffectBase> effects = new List<EventEffectBase>(effectsArray.Count);
|
||||
for (int i = 0; i < effectsArray.Count; i++)
|
||||
{
|
||||
if (!(effectsArray[i] is JObject effectObj))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string type = effectObj.Value<string>("type");
|
||||
JObject param = effectObj["param"] as JObject;
|
||||
EventEffectBase effect = EventEffectFactory.Create(type, param, probability);
|
||||
if (effect != null)
|
||||
{
|
||||
effects.Add(effect);
|
||||
}
|
||||
}
|
||||
|
||||
return effects.ToArray();
|
||||
}
|
||||
|
||||
private EventItem SelectActiveEvent(RunNodeExecutionContext context)
|
||||
{
|
||||
if (_eventItems.Count <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
int randomIndex = Random.Range(0, _eventItems.Count);
|
||||
return _eventItems[randomIndex];
|
||||
}
|
||||
|
||||
System.Random random = new System.Random(BuildSelectionSeed(context));
|
||||
return _eventItems[random.Next(0, _eventItems.Count)];
|
||||
}
|
||||
|
||||
private static int BuildSelectionSeed(RunNodeExecutionContext context)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int seed = 17;
|
||||
seed = seed * 31 + context.RunSeed;
|
||||
seed = seed * 31 + context.SequenceIndex;
|
||||
seed = seed * 31 + context.NodeId;
|
||||
return seed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
using GameFramework.ObjectPool;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD;
|
||||
using GeometryTD.PoolObjectBase;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class HPBarComponent : GameFrameworkComponent
|
||||
{
|
||||
[FormerlySerializedAs("m_HPBarItemTemplate")] [SerializeField]
|
||||
private HPBarItem _hpBarItemTemplate = null;
|
||||
|
||||
[FormerlySerializedAs("m_HPBarInstanceRoot")] [SerializeField]
|
||||
private Transform _hpBarInstanceRoot = null;
|
||||
|
||||
[FormerlySerializedAs("m_InstancePoolCapacity")] [SerializeField]
|
||||
private int _instancePoolCapacity = 16;
|
||||
|
||||
private IObjectPool<HPBarItemObject> _hpBarItemObjectPool = null;
|
||||
private List<HPBarItem> _activeHPBarItems = null;
|
||||
private Canvas _cachedCanvas = null;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_hpBarInstanceRoot == null)
|
||||
{
|
||||
Log.Error("You must set HP bar instance root first.");
|
||||
return;
|
||||
}
|
||||
|
||||
_cachedCanvas = _hpBarInstanceRoot.GetComponent<Canvas>();
|
||||
_hpBarItemObjectPool =
|
||||
GameEntry.ObjectPool.CreateSingleSpawnObjectPool<HPBarItemObject>("HPBarItem", _instancePoolCapacity);
|
||||
_activeHPBarItems = new List<HPBarItem>();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
for (int i = _activeHPBarItems.Count - 1; i >= 0; i--)
|
||||
{
|
||||
HPBarItem hpBarItem = _activeHPBarItems[i];
|
||||
if (hpBarItem.Refresh())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
HideHPBar(hpBarItem);
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowHPBar(EntityBase entity, float fromHPRatio, float toHPRatio)
|
||||
{
|
||||
if (entity == null)
|
||||
{
|
||||
Log.Warning("Entity is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
HPBarItem hpBarItem = GetActiveHPBarItem(entity);
|
||||
if (hpBarItem == null)
|
||||
{
|
||||
hpBarItem = CreateHPBarItem(entity);
|
||||
_activeHPBarItems.Add(hpBarItem);
|
||||
}
|
||||
|
||||
hpBarItem.Init(entity, _cachedCanvas, fromHPRatio, toHPRatio);
|
||||
}
|
||||
|
||||
private void HideHPBar(HPBarItem hpBarItem)
|
||||
{
|
||||
hpBarItem.Reset();
|
||||
_activeHPBarItems.Remove(hpBarItem);
|
||||
_hpBarItemObjectPool.Unspawn(hpBarItem);
|
||||
}
|
||||
|
||||
private HPBarItem GetActiveHPBarItem(EntityBase entity)
|
||||
{
|
||||
if (entity == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _activeHPBarItems.Count; i++)
|
||||
{
|
||||
if (_activeHPBarItems[i].Owner == entity)
|
||||
{
|
||||
return _activeHPBarItems[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private HPBarItem CreateHPBarItem(EntityBase entity)
|
||||
{
|
||||
HPBarItem hpBarItem = null;
|
||||
HPBarItemObject hpBarItemObject = _hpBarItemObjectPool.Spawn();
|
||||
if (hpBarItemObject != null)
|
||||
{
|
||||
hpBarItem = (HPBarItem)hpBarItemObject.Target;
|
||||
}
|
||||
else
|
||||
{
|
||||
hpBarItem = Instantiate(_hpBarItemTemplate);
|
||||
Transform transform = hpBarItem.GetComponent<Transform>();
|
||||
transform.SetParent(_hpBarInstanceRoot);
|
||||
transform.localScale = Vector3.one;
|
||||
_hpBarItemObjectPool.Register(HPBarItemObject.Create(hpBarItem), true);
|
||||
}
|
||||
|
||||
return hpBarItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class DropPoolRoller
|
||||
{
|
||||
private const float RarityCurveScalePhase = 30f;
|
||||
private static readonly RarityType[] OrderedRarities =
|
||||
{
|
||||
RarityType.White,
|
||||
RarityType.Green,
|
||||
RarityType.Blue,
|
||||
RarityType.Purple,
|
||||
RarityType.Red
|
||||
};
|
||||
|
||||
private readonly List<DROutGameDropPool> _eligibleRowBuffer = new();
|
||||
private readonly float[] _rarityWeightBuffer = new float[OrderedRarities.Length];
|
||||
private readonly IDataTable<DROutGameDropPool> _dropPoolTable;
|
||||
|
||||
public DropPoolRoller(IDataTable<DROutGameDropPool> dropPoolTable)
|
||||
{
|
||||
_dropPoolTable = dropPoolTable;
|
||||
}
|
||||
|
||||
public bool TryRollRow(
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
Random random,
|
||||
out DROutGameDropPool selectedRow,
|
||||
out RarityType selectedRarity)
|
||||
{
|
||||
selectedRow = null;
|
||||
selectedRarity = RarityType.None;
|
||||
|
||||
DROutGameDropPool[] allRows = _dropPoolTable.GetAllDataRows();
|
||||
if (allRows == null || allRows.Length <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CollectEligibleRows(allRows, displayPhaseIndex, themeType);
|
||||
if (_eligibleRowBuffer.Count <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
selectedRarity = RollRarity(displayPhaseIndex, random);
|
||||
if (selectedRarity == RarityType.None)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int totalWeight = 0;
|
||||
DROutGameDropPool fallbackRow = null;
|
||||
foreach (var row in _eligibleRowBuffer)
|
||||
{
|
||||
if (!IsEligibleAtPhase(row, selectedRarity, displayPhaseIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int rowWeight = row.GetWeight(selectedRarity);
|
||||
if (rowWeight <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
fallbackRow = row;
|
||||
totalWeight += rowWeight;
|
||||
}
|
||||
|
||||
if (totalWeight <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int randomWeight = random.Next(1, totalWeight + 1);
|
||||
int cumulativeWeight = 0;
|
||||
foreach (var row in _eligibleRowBuffer)
|
||||
{
|
||||
if (!IsEligibleAtPhase(row, selectedRarity, displayPhaseIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int rowWeight = row.GetWeight(selectedRarity);
|
||||
if (rowWeight <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
cumulativeWeight += rowWeight;
|
||||
if (randomWeight <= cumulativeWeight)
|
||||
{
|
||||
selectedRow = row;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
selectedRow = fallbackRow;
|
||||
return selectedRow != null;
|
||||
}
|
||||
|
||||
private void CollectEligibleRows(
|
||||
DROutGameDropPool[] allRows,
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType)
|
||||
{
|
||||
_eligibleRowBuffer.Clear();
|
||||
|
||||
foreach (var row in allRows)
|
||||
{
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (row.LevelThemeType != themeType)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IsEligibleAtPhase(row, displayPhaseIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_eligibleRowBuffer.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
private RarityType RollRarity(int displayPhaseIndex, Random random)
|
||||
{
|
||||
for (int i = 0; i < _rarityWeightBuffer.Length; i++)
|
||||
{
|
||||
_rarityWeightBuffer[i] = 0f;
|
||||
}
|
||||
|
||||
float phaseT = Mathf.Clamp01((displayPhaseIndex - 1) / RarityCurveScalePhase);
|
||||
|
||||
foreach (var row in _eligibleRowBuffer)
|
||||
{
|
||||
for (int rarityIndex = 0; rarityIndex < OrderedRarities.Length; rarityIndex++)
|
||||
{
|
||||
RarityType rarity = OrderedRarities[rarityIndex];
|
||||
if (!IsEligibleAtPhase(row, rarity, displayPhaseIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int rowWeight = row.GetWeight(rarity);
|
||||
if (rowWeight <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float curveWeight = GetRarityCurveWeight(rarity, phaseT);
|
||||
if (curveWeight <= 0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_rarityWeightBuffer[rarityIndex] += rowWeight * curveWeight;
|
||||
}
|
||||
}
|
||||
|
||||
float totalWeight = 0f;
|
||||
foreach (var weight in _rarityWeightBuffer)
|
||||
{
|
||||
totalWeight += Mathf.Max(0f, weight);
|
||||
}
|
||||
|
||||
if (totalWeight <= 0f)
|
||||
{
|
||||
return RarityType.None;
|
||||
}
|
||||
|
||||
float randomWeight = (float)(random.NextDouble() * totalWeight);
|
||||
float cumulativeWeight = 0f;
|
||||
for (int rarityIndex = 0; rarityIndex < _rarityWeightBuffer.Length; rarityIndex++)
|
||||
{
|
||||
cumulativeWeight += Mathf.Max(0f, _rarityWeightBuffer[rarityIndex]);
|
||||
if (randomWeight <= cumulativeWeight)
|
||||
{
|
||||
return OrderedRarities[rarityIndex];
|
||||
}
|
||||
}
|
||||
|
||||
for (int rarityIndex = 0; rarityIndex < _rarityWeightBuffer.Length; rarityIndex++)
|
||||
{
|
||||
if (_rarityWeightBuffer[rarityIndex] > 0f)
|
||||
{
|
||||
return OrderedRarities[rarityIndex];
|
||||
}
|
||||
}
|
||||
|
||||
return RarityType.None;
|
||||
}
|
||||
|
||||
private static bool IsEligibleAtPhase(DROutGameDropPool row, int displayPhaseIndex)
|
||||
{
|
||||
for (int rarityIndex = 0; rarityIndex < OrderedRarities.Length; rarityIndex++)
|
||||
{
|
||||
RarityType rarity = OrderedRarities[rarityIndex];
|
||||
if (IsEligibleAtPhase(row, rarity, displayPhaseIndex))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsEligibleAtPhase(DROutGameDropPool row, RarityType rarity, int displayPhaseIndex)
|
||||
{
|
||||
if (row.GetWeight(rarity) <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int minPhase = row.GetMinPhase(rarity);
|
||||
int maxPhase = row.GetMaxPhase(rarity);
|
||||
return displayPhaseIndex >= minPhase && displayPhaseIndex <= maxPhase;
|
||||
}
|
||||
|
||||
private static float GetRarityCurveWeight(RarityType rarityType, float phaseT)
|
||||
{
|
||||
float hump = Mathf.Exp(-Mathf.Pow((phaseT - 0.35f) / 0.28f, 2f));
|
||||
switch (rarityType)
|
||||
{
|
||||
case RarityType.White:
|
||||
return Mathf.Max(0.05f, 0.18f + 1.25f * hump);
|
||||
case RarityType.Green:
|
||||
return Mathf.Max(0.05f, 0.35f + 0.55f * hump);
|
||||
case RarityType.Blue:
|
||||
return 0.18f + 0.55f * phaseT;
|
||||
case RarityType.Purple:
|
||||
return 0.05f + 0.22f * phaseT;
|
||||
case RarityType.Red:
|
||||
return 0.01f + 0.08f * phaseT * phaseT;
|
||||
default:
|
||||
return 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,399 @@
|
|||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Factory;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class InventoryGenerationComponent : GameFrameworkComponent
|
||||
{
|
||||
private readonly List<DRShopPrice> _shopPriceRows = new();
|
||||
private IDataTable<DRShopPrice> _shopPriceTable;
|
||||
private IDataTable<DROutGameDropPool> _dropPoolTable;
|
||||
private IDataTable<DRMuzzleComp> _muzzleCompTable;
|
||||
private IDataTable<DRBearingComp> _bearingCompTable;
|
||||
private IDataTable<DRBaseComp> _baseCompTable;
|
||||
private ShopGoodsBuilder _shopGoodsBuilder;
|
||||
private DropPoolRoller _dropPoolRoller;
|
||||
private RewardCandidateBuilder _rewardCandidateBuilder;
|
||||
private OutGameDropItemBuilder _outGameDropItemBuilder;
|
||||
|
||||
public List<GoodsItemRawData> BuildShopGoods(int goodsCount, int runSeed = 0, int sequenceIndex = -1)
|
||||
{
|
||||
EnsureShopBuilder();
|
||||
return _shopGoodsBuilder.BuildGoods(goodsCount, runSeed, sequenceIndex);
|
||||
}
|
||||
|
||||
public EnemyDropResult ResolveEnemyDrop(
|
||||
in EnemyDropContext context,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
ref int nextDropOrdinal)
|
||||
{
|
||||
DREnemy enemy = context.Enemy;
|
||||
if (enemy == null)
|
||||
{
|
||||
return EnemyDropResult.Empty;
|
||||
}
|
||||
|
||||
int coin = Mathf.Max(0, enemy.DropCoin);
|
||||
int gold = 0;
|
||||
float dropRate = enemy.DropPercent > 1f
|
||||
? Mathf.Clamp01(enemy.DropPercent * 0.01f)
|
||||
: Mathf.Clamp01(enemy.DropPercent);
|
||||
|
||||
int dropOrdinal = nextDropOrdinal;
|
||||
nextDropOrdinal++;
|
||||
InventoryGenerationRandomContext randomContext =
|
||||
new(runSeed, sequenceIndex, InventoryTagSourceType.Drop, dropOrdinal);
|
||||
Random random = randomContext.CreateRandom();
|
||||
|
||||
if (enemy.DropGold > 0 && dropRate > 0f && random.NextDouble() <= dropRate)
|
||||
{
|
||||
gold = Mathf.Max(0, enemy.DropGold);
|
||||
}
|
||||
|
||||
TowerCompItemData lootItem = null;
|
||||
if (OutGameDropRuleService.ShouldRollOutGameItem(context.DisplayPhaseIndex, random) &&
|
||||
TryRollOutGameItem(
|
||||
context.DisplayPhaseIndex,
|
||||
context.ThemeType,
|
||||
randomContext,
|
||||
random,
|
||||
out TowerCompItemData droppedItem))
|
||||
{
|
||||
lootItem = droppedItem;
|
||||
}
|
||||
|
||||
return new EnemyDropResult(coin, gold, lootItem);
|
||||
}
|
||||
|
||||
public IReadOnlyList<TowerCompItemData> BuildRewardCandidates(
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
int candidateCount,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
ref int nextRewardOrdinal)
|
||||
{
|
||||
RewardCandidateBuilder rewardCandidateBuilder = EnsureRewardCandidateBuilder();
|
||||
int rewardOrdinal = nextRewardOrdinal;
|
||||
IReadOnlyList<TowerCompItemData> candidates = rewardCandidateBuilder.BuildCandidates(
|
||||
displayPhaseIndex,
|
||||
themeType,
|
||||
candidateCount,
|
||||
CreateNextRewardRandomContext,
|
||||
BuildRewardCandidateItem);
|
||||
nextRewardOrdinal = rewardOrdinal;
|
||||
return candidates;
|
||||
|
||||
InventoryGenerationRandomContext CreateNextRewardRandomContext()
|
||||
{
|
||||
return new InventoryGenerationRandomContext(
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
InventoryTagSourceType.Reward,
|
||||
rewardOrdinal++);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<TowerCompItemData> BuildEventRewardComponents(
|
||||
int count,
|
||||
RarityType rarity,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
int effectIndex)
|
||||
{
|
||||
return BuildEventRewardComponents(
|
||||
count,
|
||||
rarity,
|
||||
rarity,
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
eventId,
|
||||
optionIndex,
|
||||
effectIndex);
|
||||
}
|
||||
|
||||
public IReadOnlyList<TowerCompItemData> BuildEventRewardComponents(
|
||||
int count,
|
||||
RarityType minRarity,
|
||||
RarityType maxRarity,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
int effectIndex)
|
||||
{
|
||||
EnsureComponentTables();
|
||||
|
||||
int resolvedCount = Mathf.Max(0, count);
|
||||
if (resolvedCount <= 0)
|
||||
{
|
||||
return System.Array.Empty<TowerCompItemData>();
|
||||
}
|
||||
|
||||
RarityType normalizedMinRarity = InventoryRarityRuleService.NormalizeComponentRarity(minRarity);
|
||||
RarityType normalizedMaxRarity = InventoryRarityRuleService.NormalizeComponentRarity(maxRarity);
|
||||
if (normalizedMinRarity > normalizedMaxRarity)
|
||||
{
|
||||
throw new System.InvalidOperationException(
|
||||
$"Event reward rarity range is invalid: {normalizedMinRarity} > {normalizedMaxRarity}.");
|
||||
}
|
||||
|
||||
List<TowerCompItemData> result = new List<TowerCompItemData>(resolvedCount);
|
||||
for (int i = 0; i < resolvedCount; i++)
|
||||
{
|
||||
InventoryGenerationRandomContext randomContext = CreateEventRandomContext(
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
eventId,
|
||||
optionIndex,
|
||||
effectIndex,
|
||||
i);
|
||||
Random random = randomContext.CreateRandom();
|
||||
result.Add(BuildRandomEventComponentItem(normalizedMinRarity, normalizedMaxRarity, randomContext, random));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void EnsureShopTables()
|
||||
{
|
||||
_shopPriceTable ??= GameEntry.DataTable.GetDataTable<DRShopPrice>();
|
||||
EnsureComponentTables();
|
||||
|
||||
if (_shopPriceTable == null)
|
||||
{
|
||||
throw new System.InvalidOperationException(
|
||||
"InventoryGenerationComponent requires ShopPrice data table.");
|
||||
}
|
||||
|
||||
if (_shopPriceRows.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DRShopPrice[] rows = _shopPriceTable.GetAllDataRows();
|
||||
if (rows == null || rows.Length <= 0)
|
||||
{
|
||||
throw new System.InvalidOperationException(
|
||||
"InventoryGenerationComponent requires at least one shop price row.");
|
||||
}
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
if (row != null)
|
||||
{
|
||||
_shopPriceRows.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
if (_shopPriceRows.Count <= 0)
|
||||
{
|
||||
throw new System.InvalidOperationException("InventoryGenerationComponent requires non-null shop price rows.");
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureShopBuilder()
|
||||
{
|
||||
EnsureShopTables();
|
||||
_shopGoodsBuilder ??= new ShopGoodsBuilder(
|
||||
_shopPriceRows,
|
||||
_muzzleCompTable,
|
||||
_bearingCompTable,
|
||||
_baseCompTable);
|
||||
}
|
||||
|
||||
private void EnsureDropTables()
|
||||
{
|
||||
_dropPoolTable ??= GameEntry.DataTable.GetDataTable<DROutGameDropPool>();
|
||||
EnsureComponentTables();
|
||||
|
||||
if (_dropPoolTable == null)
|
||||
{
|
||||
throw new System.InvalidOperationException(
|
||||
"InventoryGenerationComponent requires OutGameDropPool data table.");
|
||||
}
|
||||
}
|
||||
|
||||
private DropPoolRoller EnsureDropPoolRoller()
|
||||
{
|
||||
EnsureDropTables();
|
||||
_dropPoolRoller ??= new DropPoolRoller(_dropPoolTable);
|
||||
return _dropPoolRoller;
|
||||
}
|
||||
|
||||
private RewardCandidateBuilder EnsureRewardCandidateBuilder()
|
||||
{
|
||||
_rewardCandidateBuilder ??= new RewardCandidateBuilder(EnsureDropPoolRoller());
|
||||
return _rewardCandidateBuilder;
|
||||
}
|
||||
|
||||
private OutGameDropItemBuilder EnsureOutGameDropItemBuilder()
|
||||
{
|
||||
EnsureDropTables();
|
||||
_outGameDropItemBuilder ??= new OutGameDropItemBuilder(
|
||||
_muzzleCompTable,
|
||||
_bearingCompTable,
|
||||
_baseCompTable);
|
||||
return _outGameDropItemBuilder;
|
||||
}
|
||||
|
||||
private bool TryRollOutGameItem(
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random,
|
||||
out TowerCompItemData droppedItem)
|
||||
{
|
||||
droppedItem = null;
|
||||
DropPoolRoller dropPoolRoller = EnsureDropPoolRoller();
|
||||
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
|
||||
if (!dropPoolRoller.TryRollRow(
|
||||
phaseIndex,
|
||||
themeType,
|
||||
random,
|
||||
out DROutGameDropPool selectedRow,
|
||||
out RarityType selectedRarity) || selectedRow == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return EnsureOutGameDropItemBuilder().TryBuildItem(selectedRow, selectedRarity, randomContext, out droppedItem);
|
||||
}
|
||||
|
||||
private TowerCompItemData BuildRewardCandidateItem(
|
||||
DROutGameDropPool row,
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext)
|
||||
{
|
||||
if (!EnsureOutGameDropItemBuilder().TryBuildItem(row, rarity, randomContext, out TowerCompItemData droppedItem))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return droppedItem;
|
||||
}
|
||||
|
||||
private void EnsureComponentTables()
|
||||
{
|
||||
_muzzleCompTable ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
|
||||
_bearingCompTable ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
|
||||
_baseCompTable ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
|
||||
|
||||
if (_muzzleCompTable == null || _bearingCompTable == null || _baseCompTable == null)
|
||||
{
|
||||
throw new System.InvalidOperationException(
|
||||
"InventoryGenerationComponent requires MuzzleComp, BearingComp, and BaseComp data tables.");
|
||||
}
|
||||
}
|
||||
|
||||
private TowerCompItemData BuildRandomEventComponentItem(
|
||||
RarityType minRarity,
|
||||
RarityType maxRarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
RarityType rarity = ResolveEventRewardRarity(minRarity, maxRarity, random);
|
||||
int slotRoll = random.Next(0, 3);
|
||||
return slotRoll switch
|
||||
{
|
||||
0 => BuildRandomEventMuzzleItem(rarity, randomContext, random),
|
||||
1 => BuildRandomEventBearingItem(rarity, randomContext, random),
|
||||
_ => BuildRandomEventBaseItem(rarity, randomContext, random)
|
||||
};
|
||||
}
|
||||
|
||||
private static RarityType ResolveEventRewardRarity(
|
||||
RarityType minRarity,
|
||||
RarityType maxRarity,
|
||||
Random random)
|
||||
{
|
||||
if (minRarity >= maxRarity)
|
||||
{
|
||||
return minRarity;
|
||||
}
|
||||
|
||||
int rarityValue = random.Next((int)minRarity, (int)maxRarity + 1);
|
||||
return (RarityType)rarityValue;
|
||||
}
|
||||
|
||||
private MuzzleCompItemData BuildRandomEventMuzzleItem(
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRMuzzleComp[] rows = _muzzleCompTable.GetAllDataRows();
|
||||
DRMuzzleComp config = rows[random.Next(0, rows.Length)];
|
||||
return ComponentItemFactory.CreateMuzzle(
|
||||
config,
|
||||
randomContext.CreateStableItemInstanceId(),
|
||||
rarity,
|
||||
randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private BearingCompItemData BuildRandomEventBearingItem(
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRBearingComp[] rows = _bearingCompTable.GetAllDataRows();
|
||||
DRBearingComp config = rows[random.Next(0, rows.Length)];
|
||||
return ComponentItemFactory.CreateBearing(
|
||||
config,
|
||||
randomContext.CreateStableItemInstanceId(),
|
||||
rarity,
|
||||
randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private BaseCompItemData BuildRandomEventBaseItem(
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRBaseComp[] rows = _baseCompTable.GetAllDataRows();
|
||||
DRBaseComp config = rows[random.Next(0, rows.Length)];
|
||||
return ComponentItemFactory.CreateBase(
|
||||
config,
|
||||
randomContext.CreateStableItemInstanceId(),
|
||||
rarity,
|
||||
randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private static InventoryGenerationRandomContext CreateEventRandomContext(
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
int effectIndex,
|
||||
int itemIndex)
|
||||
{
|
||||
return new InventoryGenerationRandomContext(
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
InventoryTagSourceType.Event,
|
||||
BuildEventLocalOrdinal(eventId, optionIndex, effectIndex, itemIndex));
|
||||
}
|
||||
|
||||
private static int BuildEventLocalOrdinal(int eventId, int optionIndex, int effectIndex, int itemIndex)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int value = 17;
|
||||
value = value * 31 + eventId;
|
||||
value = value * 31 + optionIndex;
|
||||
value = value * 31 + effectIndex;
|
||||
value = value * 31 + itemIndex;
|
||||
return value & int.MaxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
using System;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.Definition
|
||||
{
|
||||
public readonly struct InventoryGenerationRandomContext
|
||||
{
|
||||
public InventoryGenerationRandomContext(
|
||||
int runSeed,
|
||||
int nodeSequenceIndex,
|
||||
InventoryTagSourceType sourceType,
|
||||
int localOrdinal)
|
||||
{
|
||||
RunSeed = runSeed;
|
||||
NodeSequenceIndex = nodeSequenceIndex;
|
||||
SourceType = sourceType;
|
||||
LocalOrdinal = localOrdinal;
|
||||
}
|
||||
|
||||
public int RunSeed { get; }
|
||||
|
||||
public int NodeSequenceIndex { get; }
|
||||
|
||||
public InventoryTagSourceType SourceType { get; }
|
||||
|
||||
public int LocalOrdinal { get; }
|
||||
|
||||
public Random CreateRandom()
|
||||
{
|
||||
return new Random(BuildSeed());
|
||||
}
|
||||
|
||||
public long CreateStableItemInstanceId()
|
||||
{
|
||||
long normalizedSource = ((long)Math.Max(0, (int)SourceType) + 1L) << 48;
|
||||
long normalizedSequence = ((long)Math.Max(0, NodeSequenceIndex) + 1L) << 24;
|
||||
long normalizedOrdinal = (uint)(Math.Max(0, LocalOrdinal) + 1);
|
||||
return normalizedSource | normalizedSequence | normalizedOrdinal;
|
||||
}
|
||||
|
||||
public InventoryTagRandomContext CreateTagRandomContext(int configId)
|
||||
{
|
||||
return SourceType switch
|
||||
{
|
||||
InventoryTagSourceType.Shop => InventoryTagRandomContext.CreateShop(RunSeed, NodeSequenceIndex, LocalOrdinal, configId),
|
||||
InventoryTagSourceType.Reward => InventoryTagRandomContext.CreateReward(RunSeed, NodeSequenceIndex, LocalOrdinal, configId),
|
||||
InventoryTagSourceType.Event => InventoryTagRandomContext.CreateEvent(RunSeed, NodeSequenceIndex, LocalOrdinal, configId),
|
||||
_ => InventoryTagRandomContext.CreateDrop(RunSeed, NodeSequenceIndex, LocalOrdinal, configId)
|
||||
};
|
||||
}
|
||||
|
||||
private int BuildSeed()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int seed = 17;
|
||||
seed = seed * 31 + RunSeed;
|
||||
seed = seed * 31 + NodeSequenceIndex;
|
||||
seed = seed * 31 + (int)SourceType;
|
||||
seed = seed * 31 + LocalOrdinal;
|
||||
return seed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using UnityEngine;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public static class OutGameDropRuleService
|
||||
{
|
||||
private const float DropChanceBase = 0.05f;
|
||||
private const float DropChancePerPhase = 0.2f;
|
||||
private const float DropChanceCap = 0.2f;
|
||||
|
||||
public static bool ShouldRollOutGameItem(int displayPhaseIndex, Random random)
|
||||
{
|
||||
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
|
||||
float dropChance = Mathf.Clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0f, DropChanceCap);
|
||||
return random.NextDouble() <= dropChance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class RewardCandidateBuilder
|
||||
{
|
||||
private readonly DropPoolRoller _dropPoolRoller;
|
||||
|
||||
public RewardCandidateBuilder(DropPoolRoller dropPoolRoller)
|
||||
{
|
||||
_dropPoolRoller = dropPoolRoller;
|
||||
}
|
||||
|
||||
public IReadOnlyList<TowerCompItemData> BuildCandidates(
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
int candidateCount,
|
||||
Func<InventoryGenerationRandomContext> createRandomContext,
|
||||
Func<DROutGameDropPool, RarityType, InventoryGenerationRandomContext, TowerCompItemData> buildRewardItem)
|
||||
{
|
||||
int resolvedCount = Mathf.Max(0, candidateCount);
|
||||
if (resolvedCount <= 0)
|
||||
{
|
||||
return Array.Empty<TowerCompItemData>();
|
||||
}
|
||||
|
||||
List<TowerCompItemData> candidates = new List<TowerCompItemData>(resolvedCount);
|
||||
HashSet<long> selectedPoolEntryKeys = new HashSet<long>();
|
||||
int maxAttempts = Mathf.Max(resolvedCount * 6, resolvedCount);
|
||||
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
|
||||
|
||||
int attempts = 0;
|
||||
while (candidates.Count < resolvedCount && attempts < maxAttempts)
|
||||
{
|
||||
attempts++;
|
||||
InventoryGenerationRandomContext randomContext = createRandomContext();
|
||||
Random random = randomContext.CreateRandom();
|
||||
if (!_dropPoolRoller.TryRollRow(
|
||||
phaseIndex,
|
||||
themeType,
|
||||
random,
|
||||
out DROutGameDropPool selectedRow,
|
||||
out RarityType selectedRarity) || selectedRow == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!selectedPoolEntryKeys.Add(BuildSelectionKey(selectedRow.Id, selectedRarity)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TowerCompItemData candidate = buildRewardItem(selectedRow, selectedRarity, randomContext);
|
||||
if (candidate == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.Add(candidate);
|
||||
}
|
||||
|
||||
attempts = 0;
|
||||
while (candidates.Count < resolvedCount && attempts < maxAttempts)
|
||||
{
|
||||
attempts++;
|
||||
InventoryGenerationRandomContext randomContext = createRandomContext();
|
||||
Random random = randomContext.CreateRandom();
|
||||
if (!_dropPoolRoller.TryRollRow(
|
||||
phaseIndex,
|
||||
themeType,
|
||||
random,
|
||||
out DROutGameDropPool selectedRow,
|
||||
out RarityType selectedRarity) || selectedRow == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
TowerCompItemData candidate = buildRewardItem(selectedRow, selectedRarity, randomContext);
|
||||
if (candidate == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.Add(candidate);
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static long BuildSelectionKey(int rowId, RarityType rarity)
|
||||
{
|
||||
return ((long)rowId << 32) | (uint)rarity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Factory;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class ShopGoodsBuilder
|
||||
{
|
||||
private readonly IReadOnlyList<DRShopPrice> _shopPriceRows;
|
||||
private readonly IDataTable<DRMuzzleComp> _muzzleCompTable;
|
||||
private readonly IDataTable<DRBearingComp> _bearingCompTable;
|
||||
private readonly IDataTable<DRBaseComp> _baseCompTable;
|
||||
|
||||
public ShopGoodsBuilder(
|
||||
IReadOnlyList<DRShopPrice> shopPriceRows,
|
||||
IDataTable<DRMuzzleComp> muzzleCompTable,
|
||||
IDataTable<DRBearingComp> bearingCompTable,
|
||||
IDataTable<DRBaseComp> baseCompTable)
|
||||
{
|
||||
_shopPriceRows = shopPriceRows;
|
||||
_muzzleCompTable = muzzleCompTable;
|
||||
_bearingCompTable = bearingCompTable;
|
||||
_baseCompTable = baseCompTable;
|
||||
}
|
||||
|
||||
public List<GoodsItemRawData> BuildGoods(
|
||||
int goodsCount,
|
||||
int runSeed,
|
||||
int sequenceIndex)
|
||||
{
|
||||
if (goodsCount <= 0)
|
||||
{
|
||||
return new List<GoodsItemRawData>();
|
||||
}
|
||||
|
||||
List<GoodsItemRawData> goodsItems = new(goodsCount);
|
||||
for (int i = 0; i < goodsCount; i++)
|
||||
{
|
||||
InventoryGenerationRandomContext randomContext =
|
||||
new(runSeed, sequenceIndex, InventoryTagSourceType.Shop, i);
|
||||
goodsItems.Add(BuildGoodsItem(i, randomContext));
|
||||
}
|
||||
|
||||
return goodsItems;
|
||||
}
|
||||
|
||||
private GoodsItemRawData BuildGoodsItem(
|
||||
int goodsIndex,
|
||||
InventoryGenerationRandomContext randomContext)
|
||||
{
|
||||
Random random = randomContext.CreateRandom();
|
||||
TowerCompItemData sourceItem = BuildRandomComponentItem(randomContext, random);
|
||||
return new GoodsItemRawData
|
||||
{
|
||||
GoodsIndex = goodsIndex,
|
||||
Title = sourceItem.Name,
|
||||
TypeText = BuildTypeText(sourceItem.SlotType),
|
||||
Description = BuildDescription(sourceItem),
|
||||
Price = ResolveRandomPrice(sourceItem.Rarity, random),
|
||||
Tags = sourceItem.Tags != null ? (TagType[])sourceItem.Tags.Clone() : Array.Empty<TagType>(),
|
||||
IconAreaContext = BuildIconAreaContext(sourceItem),
|
||||
SourceItem = sourceItem,
|
||||
IsPurchased = false
|
||||
};
|
||||
}
|
||||
|
||||
private TowerCompItemData BuildRandomComponentItem(
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
int slotRoll = random.Next(0, 3);
|
||||
DRShopPrice priceRow = _shopPriceRows[random.Next(0, _shopPriceRows.Count)];
|
||||
RarityType rarity = InventoryRarityRuleService.NormalizeComponentRarity(
|
||||
priceRow != null ? priceRow.Rarity : RarityType.White);
|
||||
|
||||
return slotRoll switch
|
||||
{
|
||||
0 => BuildRandomMuzzleItem(rarity, randomContext, random),
|
||||
1 => BuildRandomBearingItem(rarity, randomContext, random),
|
||||
_ => BuildRandomBaseItem(rarity, randomContext, random)
|
||||
};
|
||||
}
|
||||
|
||||
private MuzzleCompItemData BuildRandomMuzzleItem(
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRMuzzleComp[] rows = _muzzleCompTable.GetAllDataRows();
|
||||
DRMuzzleComp config = rows[random.Next(0, rows.Length)];
|
||||
long instanceId = randomContext.CreateStableItemInstanceId();
|
||||
return ComponentItemFactory.CreateMuzzle(config, instanceId, rarity, randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private BearingCompItemData BuildRandomBearingItem(
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRBearingComp[] rows = _bearingCompTable.GetAllDataRows();
|
||||
DRBearingComp config = rows[random.Next(0, rows.Length)];
|
||||
long instanceId = randomContext.CreateStableItemInstanceId();
|
||||
return ComponentItemFactory.CreateBearing(config, instanceId, rarity, randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private BaseCompItemData BuildRandomBaseItem(
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRBaseComp[] rows = _baseCompTable.GetAllDataRows();
|
||||
DRBaseComp config = rows[random.Next(0, rows.Length)];
|
||||
long instanceId = randomContext.CreateStableItemInstanceId();
|
||||
return ComponentItemFactory.CreateBase(config, instanceId, rarity, randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private int ResolveRandomPrice(RarityType rarity, Random random)
|
||||
{
|
||||
return ShopPriceRuleService.ResolveRandomBuyPrice(_shopPriceRows, rarity, random);
|
||||
}
|
||||
|
||||
private static IconAreaContext BuildIconAreaContext(TowerCompItemData item)
|
||||
{
|
||||
return new IconAreaContext
|
||||
{
|
||||
Rarity = item.Rarity,
|
||||
ComponentSlotType = item.SlotType,
|
||||
Color = IconColorGenerator.GenerateForComponent(item)
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildTypeText(TowerCompSlotType slotType)
|
||||
{
|
||||
return slotType switch
|
||||
{
|
||||
TowerCompSlotType.Muzzle => "枪口组件",
|
||||
TowerCompSlotType.Bearing => "轴承组件",
|
||||
TowerCompSlotType.Base => "底座组件",
|
||||
_ => "组件"
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildDescription(TowerCompItemData item)
|
||||
{
|
||||
if (item is MuzzleCompItemData muzzleComp)
|
||||
{
|
||||
return ItemDescUtility.BuildMuzzleDesc(muzzleComp);
|
||||
}
|
||||
|
||||
if (item is BearingCompItemData bearingComp)
|
||||
{
|
||||
return ItemDescUtility.BuildBearingDesc(bearingComp);
|
||||
}
|
||||
|
||||
if (item is BaseCompItemData baseComp)
|
||||
{
|
||||
return ItemDescUtility.BuildBaseDesc(baseComp);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class PlayerInventoryComponent : GameFrameworkComponent
|
||||
{
|
||||
private const int MaxParticipantTowerCount = 4;
|
||||
|
||||
private PlayerInventoryState _state;
|
||||
private PlayerInventoryQueryModel _queryModel;
|
||||
private PlayerInventoryCommandModel _commandModel;
|
||||
private PlayerInventoryTowerRosterService _towerRosterService;
|
||||
private PlayerInventoryTowerAssemblyService _towerAssemblyService;
|
||||
private PlayerInventoryTradeService _tradeService;
|
||||
|
||||
public int Gold
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _queryModel.Gold;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnInit(BackpackInventoryData initialInventory = null)
|
||||
{
|
||||
EnsureServices();
|
||||
_commandModel.Initialize(initialInventory ?? InventorySeedUtility.CreateSampleInventory(), MaxParticipantTowerCount);
|
||||
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
Log.Info(
|
||||
"PlayerInventory initialized. Gold={0}, Tower={1}, Muzzle={2}, Bearing={3}, Base={4}.",
|
||||
inventory.Gold,
|
||||
inventory.Towers.Count,
|
||||
inventory.MuzzleComponents.Count,
|
||||
inventory.BearingComponents.Count,
|
||||
inventory.BaseComponents.Count);
|
||||
}
|
||||
|
||||
public BackpackInventoryData GetInventorySnapshot()
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _queryModel.GetSnapshot();
|
||||
}
|
||||
|
||||
public void ReplaceInventorySnapshot(BackpackInventoryData inventorySnapshot)
|
||||
{
|
||||
EnsureServices();
|
||||
_commandModel.ReplaceInventorySnapshot(inventorySnapshot, MaxParticipantTowerCount);
|
||||
}
|
||||
|
||||
public IReadOnlyList<TowerItemData> GetParticipantTowerSnapshot()
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
if (inventory?.ParticipantTowerInstanceIds == null || inventory.ParticipantTowerInstanceIds.Count <= 0)
|
||||
{
|
||||
return new List<TowerItemData>();
|
||||
}
|
||||
|
||||
List<TowerItemData> result = new List<TowerItemData>(inventory.ParticipantTowerInstanceIds.Count);
|
||||
for (int i = 0; i < inventory.ParticipantTowerInstanceIds.Count && result.Count < MaxParticipantTowerCount; i++)
|
||||
{
|
||||
long towerId = inventory.ParticipantTowerInstanceIds[i];
|
||||
if (!_queryModel.TryGetTowerById(towerId, out TowerItemData tower) || tower == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(InventoryCloneUtility.CloneTower(tower));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void MergeInventory(BackpackInventoryData gainedInventory)
|
||||
{
|
||||
EnsureInitialized();
|
||||
PlayerInventoryMergeSummary summary = _commandModel.MergeInventory(gainedInventory);
|
||||
if (!summary.HasAnyGain)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Info(
|
||||
"PlayerInventory merged reward. Gold+{0}, Tower+{1}, Muzzle+{2}, Bearing+{3}, Base+{4}.",
|
||||
summary.GainedGold,
|
||||
summary.GainedTowerCount,
|
||||
summary.GainedMuzzleCount,
|
||||
summary.GainedBearingCount,
|
||||
summary.GainedBaseCount);
|
||||
}
|
||||
|
||||
public bool TryConsumeGold(int costGold)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _commandModel.TryConsumeGold(costGold);
|
||||
}
|
||||
|
||||
public bool TryPurchaseComponent(TowerCompItemData item, int price)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _tradeService.TryPurchaseComponent(item, price);
|
||||
}
|
||||
|
||||
public void AddGold(int gainGold)
|
||||
{
|
||||
EnsureInitialized();
|
||||
_commandModel.AddGold(gainGold);
|
||||
}
|
||||
|
||||
public ParticipantTowerAssignResult TryAddParticipantTower(long towerInstanceId, int maxCount = 4)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _towerRosterService.TryAddParticipantTower(towerInstanceId, maxCount);
|
||||
}
|
||||
|
||||
public bool TryRemoveParticipantTower(long towerInstanceId)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _towerRosterService.TryRemoveParticipantTower(towerInstanceId);
|
||||
}
|
||||
|
||||
public bool TryAssembleTower(
|
||||
long muzzleInstanceId,
|
||||
long bearingInstanceId,
|
||||
long baseInstanceId,
|
||||
out TowerItemData assembledTower)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _towerAssemblyService.TryAssembleTower(
|
||||
muzzleInstanceId,
|
||||
bearingInstanceId,
|
||||
baseInstanceId,
|
||||
out assembledTower);
|
||||
}
|
||||
|
||||
public int ReduceTowerEndurance(IReadOnlyList<long> towerInstanceIds, float enduranceLoss)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _towerRosterService.ReduceTowerEndurance(towerInstanceIds, enduranceLoss);
|
||||
}
|
||||
|
||||
public bool TryDisassembleTower(long towerInstanceId)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _towerAssemblyService.TryDisassembleTower(towerInstanceId);
|
||||
}
|
||||
|
||||
public bool TryGetSaleCandidate(long itemId, out PlayerInventorySaleCandidate candidate)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _tradeService.TryGetSaleCandidate(itemId, out candidate);
|
||||
}
|
||||
|
||||
public bool TrySellItems(IReadOnlyCollection<long> itemIds, out PlayerInventorySaleResult result)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _tradeService.TrySellItems(itemIds, out result);
|
||||
}
|
||||
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (_queryModel.IsInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnInit();
|
||||
}
|
||||
|
||||
private void EnsureServices()
|
||||
{
|
||||
_state ??= new PlayerInventoryState();
|
||||
_queryModel ??= new PlayerInventoryQueryModel(_state);
|
||||
_commandModel ??= new PlayerInventoryCommandModel(_state);
|
||||
_towerRosterService ??= new PlayerInventoryTowerRosterService(_queryModel, MaxParticipantTowerCount);
|
||||
_towerAssemblyService ??= new PlayerInventoryTowerAssemblyService(_queryModel, _commandModel);
|
||||
_tradeService ??= new PlayerInventoryTradeService(_queryModel, _commandModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,275 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public struct PlayerInventoryMergeSummary
|
||||
{
|
||||
public int GainedGold;
|
||||
public int GainedMuzzleCount;
|
||||
public int GainedBearingCount;
|
||||
public int GainedBaseCount;
|
||||
public int GainedTowerCount;
|
||||
|
||||
public bool HasAnyGain =>
|
||||
GainedGold > 0 || GainedMuzzleCount > 0 || GainedBearingCount > 0 || GainedBaseCount > 0 ||
|
||||
GainedTowerCount > 0;
|
||||
}
|
||||
|
||||
public sealed class PlayerInventoryState
|
||||
{
|
||||
public BackpackInventoryData Inventory = new BackpackInventoryData();
|
||||
public long NextInstanceId = 1;
|
||||
public bool IsInitialized;
|
||||
}
|
||||
|
||||
public sealed class PlayerInventoryQueryModel
|
||||
{
|
||||
private readonly PlayerInventoryState _state;
|
||||
|
||||
public PlayerInventoryQueryModel(PlayerInventoryState state)
|
||||
{
|
||||
_state = state;
|
||||
}
|
||||
|
||||
public BackpackInventoryData Inventory => _state.Inventory;
|
||||
public bool IsInitialized => _state.IsInitialized;
|
||||
public int Gold => _state.Inventory.Gold;
|
||||
|
||||
public BackpackInventoryData GetSnapshot()
|
||||
{
|
||||
return InventoryCloneUtility.CloneInventory(_state.Inventory);
|
||||
}
|
||||
|
||||
public bool TryGetTowerById(long towerInstanceId, out TowerItemData tower)
|
||||
{
|
||||
return InventoryParticipantUtility.TryGetTowerById(_state.Inventory, towerInstanceId, out tower);
|
||||
}
|
||||
|
||||
public bool TryGetComponentById<TComp>(List<TComp> components, long instanceId, out TComp result)
|
||||
where TComp : TowerCompItemData
|
||||
{
|
||||
result = null;
|
||||
if (components == null || instanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < components.Count; i++)
|
||||
{
|
||||
TComp component = components[i];
|
||||
if (component != null && component.InstanceId == instanceId)
|
||||
{
|
||||
result = component;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PlayerInventoryCommandModel
|
||||
{
|
||||
private readonly PlayerInventoryState _state;
|
||||
|
||||
public PlayerInventoryCommandModel(PlayerInventoryState state)
|
||||
{
|
||||
_state = state;
|
||||
}
|
||||
|
||||
public void Initialize(BackpackInventoryData sourceInventory, int maxParticipantTowerCount)
|
||||
{
|
||||
_state.Inventory = InventoryCloneUtility.CloneInventory(sourceInventory);
|
||||
InventoryParticipantUtility.NormalizeParticipantState(_state.Inventory, maxParticipantTowerCount);
|
||||
RebuildNextInstanceId();
|
||||
_state.IsInitialized = true;
|
||||
}
|
||||
|
||||
public void ReplaceInventorySnapshot(BackpackInventoryData sourceInventory, int maxParticipantTowerCount)
|
||||
{
|
||||
Initialize(sourceInventory, maxParticipantTowerCount);
|
||||
}
|
||||
|
||||
public PlayerInventoryMergeSummary MergeInventory(BackpackInventoryData gainedInventory)
|
||||
{
|
||||
PlayerInventoryMergeSummary summary = default;
|
||||
if (gainedInventory == null)
|
||||
{
|
||||
return summary;
|
||||
}
|
||||
|
||||
summary.GainedGold = Mathf.Max(0, gainedInventory.Gold);
|
||||
if (summary.GainedGold > 0)
|
||||
{
|
||||
_state.Inventory.Gold += summary.GainedGold;
|
||||
}
|
||||
|
||||
if (gainedInventory.MuzzleComponents != null)
|
||||
{
|
||||
for (int i = 0; i < gainedInventory.MuzzleComponents.Count; i++)
|
||||
{
|
||||
MuzzleCompItemData source = gainedInventory.MuzzleComponents[i];
|
||||
if (source == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
MuzzleCompItemData cloned = InventoryCloneUtility.CloneMuzzleComp(source);
|
||||
cloned.InstanceId = AllocateInstanceId();
|
||||
_state.Inventory.MuzzleComponents.Add(cloned);
|
||||
summary.GainedMuzzleCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (gainedInventory.BearingComponents != null)
|
||||
{
|
||||
for (int i = 0; i < gainedInventory.BearingComponents.Count; i++)
|
||||
{
|
||||
BearingCompItemData source = gainedInventory.BearingComponents[i];
|
||||
if (source == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
BearingCompItemData cloned = InventoryCloneUtility.CloneBearingComp(source);
|
||||
cloned.InstanceId = AllocateInstanceId();
|
||||
_state.Inventory.BearingComponents.Add(cloned);
|
||||
summary.GainedBearingCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (gainedInventory.BaseComponents != null)
|
||||
{
|
||||
for (int i = 0; i < gainedInventory.BaseComponents.Count; i++)
|
||||
{
|
||||
BaseCompItemData source = gainedInventory.BaseComponents[i];
|
||||
if (source == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
BaseCompItemData cloned = InventoryCloneUtility.CloneBaseComp(source);
|
||||
cloned.InstanceId = AllocateInstanceId();
|
||||
_state.Inventory.BaseComponents.Add(cloned);
|
||||
summary.GainedBaseCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (gainedInventory.Towers != null)
|
||||
{
|
||||
for (int i = 0; i < gainedInventory.Towers.Count; i++)
|
||||
{
|
||||
TowerItemData source = gainedInventory.Towers[i];
|
||||
if (source == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TowerItemData cloned = InventoryCloneUtility.CloneTower(source);
|
||||
cloned.InstanceId = AllocateInstanceId();
|
||||
_state.Inventory.Towers.Add(cloned);
|
||||
summary.GainedTowerCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
public bool TryConsumeGold(int costGold)
|
||||
{
|
||||
int resolvedCost = Mathf.Max(0, costGold);
|
||||
if (resolvedCost <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_state.Inventory.Gold < resolvedCost)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_state.Inventory.Gold -= resolvedCost;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AddGold(int gainGold)
|
||||
{
|
||||
int resolvedGain = Mathf.Max(0, gainGold);
|
||||
if (resolvedGain <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_state.Inventory.Gold += resolvedGain;
|
||||
}
|
||||
|
||||
public long AllocateInstanceId()
|
||||
{
|
||||
if (_state.NextInstanceId < 1)
|
||||
{
|
||||
_state.NextInstanceId = 1;
|
||||
}
|
||||
|
||||
return _state.NextInstanceId++;
|
||||
}
|
||||
|
||||
private void RebuildNextInstanceId()
|
||||
{
|
||||
long maxInstanceId = 0;
|
||||
BackpackInventoryData inventory = _state.Inventory;
|
||||
if (inventory.Towers != null)
|
||||
{
|
||||
for (int i = 0; i < inventory.Towers.Count; i++)
|
||||
{
|
||||
TowerItemData item = inventory.Towers[i];
|
||||
if (item != null)
|
||||
{
|
||||
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inventory.MuzzleComponents != null)
|
||||
{
|
||||
for (int i = 0; i < inventory.MuzzleComponents.Count; i++)
|
||||
{
|
||||
MuzzleCompItemData item = inventory.MuzzleComponents[i];
|
||||
if (item != null)
|
||||
{
|
||||
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inventory.BearingComponents != null)
|
||||
{
|
||||
for (int i = 0; i < inventory.BearingComponents.Count; i++)
|
||||
{
|
||||
BearingCompItemData item = inventory.BearingComponents[i];
|
||||
if (item != null)
|
||||
{
|
||||
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inventory.BaseComponents != null)
|
||||
{
|
||||
for (int i = 0; i < inventory.BaseComponents.Count; i++)
|
||||
{
|
||||
BaseCompItemData item = inventory.BaseComponents[i];
|
||||
if (item != null)
|
||||
{
|
||||
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_state.NextInstanceId = Math.Max(1, maxInstanceId + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
internal sealed class PlayerInventoryTowerAssemblyService
|
||||
{
|
||||
private const int TowerLevelCount = 5;
|
||||
|
||||
private readonly PlayerInventoryQueryModel _queryModel;
|
||||
private readonly PlayerInventoryCommandModel _commandModel;
|
||||
private IDataTable<DRMuzzleComp> _drMuzzleComp;
|
||||
private IDataTable<DRBearingComp> _drBearingComp;
|
||||
private IDataTable<DRBaseComp> _drBaseComp;
|
||||
|
||||
public PlayerInventoryTowerAssemblyService(
|
||||
PlayerInventoryQueryModel queryModel,
|
||||
PlayerInventoryCommandModel commandModel)
|
||||
{
|
||||
_queryModel = queryModel;
|
||||
_commandModel = commandModel;
|
||||
}
|
||||
|
||||
public bool TryAssembleTower(
|
||||
long muzzleInstanceId,
|
||||
long bearingInstanceId,
|
||||
long baseInstanceId,
|
||||
out TowerItemData assembledTower)
|
||||
{
|
||||
assembledTower = null;
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
if (muzzleInstanceId <= 0 || bearingInstanceId <= 0 || baseInstanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_queryModel.TryGetComponentById(inventory.MuzzleComponents, muzzleInstanceId, out MuzzleCompItemData muzzleComp) ||
|
||||
!_queryModel.TryGetComponentById(inventory.BearingComponents, bearingInstanceId, out BearingCompItemData bearingComp) ||
|
||||
!_queryModel.TryGetComponentById(inventory.BaseComponents, baseInstanceId, out BaseCompItemData baseComp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (muzzleComp.IsAssembledIntoTower || bearingComp.IsAssembledIntoTower || baseComp.IsAssembledIntoTower)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryBuildTowerStats(muzzleComp, bearingComp, baseComp, out TowerStatsData stats))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
long towerInstanceId = _commandModel.AllocateInstanceId();
|
||||
TowerItemData tower = new TowerItemData
|
||||
{
|
||||
InstanceId = towerInstanceId,
|
||||
Name = $"组装防御塔-{towerInstanceId}",
|
||||
Rarity = InventoryRarityRuleService.ResolveTowerRarity(
|
||||
muzzleComp.Rarity,
|
||||
bearingComp.Rarity,
|
||||
baseComp.Rarity),
|
||||
MuzzleComponentInstanceId = muzzleComp.InstanceId,
|
||||
BearingComponentInstanceId = bearingComp.InstanceId,
|
||||
BaseComponentInstanceId = baseComp.InstanceId,
|
||||
Stats = stats
|
||||
};
|
||||
|
||||
muzzleComp.IsAssembledIntoTower = true;
|
||||
bearingComp.IsAssembledIntoTower = true;
|
||||
baseComp.IsAssembledIntoTower = true;
|
||||
inventory.Towers.Add(tower);
|
||||
assembledTower = InventoryCloneUtility.CloneTower(tower);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryBuildTowerStats(
|
||||
MuzzleCompItemData muzzleComp,
|
||||
BearingCompItemData bearingComp,
|
||||
BaseCompItemData baseComp,
|
||||
out TowerStatsData stats)
|
||||
{
|
||||
stats = null;
|
||||
if (muzzleComp == null || bearingComp == null || baseComp == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DRMuzzleComp muzzleConfig = EnsureMuzzleTable()?.GetDataRow(muzzleComp.ConfigId);
|
||||
DRBearingComp bearingConfig = EnsureBearingTable()?.GetDataRow(bearingComp.ConfigId);
|
||||
DRBaseComp baseConfig = EnsureBaseTable()?.GetDataRow(baseComp.ConfigId);
|
||||
if (muzzleConfig == null || bearingConfig == null || baseConfig == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
stats = new TowerStatsData
|
||||
{
|
||||
AttackDamage = BuildLevelIntArray(muzzleComp.AttackDamage, muzzleComp.Rarity, muzzleConfig.AttackDamagePerLevel),
|
||||
DamageRandomRate = Mathf.Max(0f, muzzleComp.DamageRandomRate),
|
||||
RotateSpeed = BuildLevelFloatArray(bearingComp.RotateSpeed, bearingComp.Rarity, bearingConfig.RotateSpeedPerLevel),
|
||||
AttackRange = BuildLevelFloatArray(bearingComp.AttackRange, bearingComp.Rarity, bearingConfig.AttackRangePerLevel),
|
||||
AttackSpeed = BuildLevelFloatArray(baseComp.AttackSpeed, baseComp.Rarity, baseConfig.AttackSpeedPerLevel),
|
||||
AttackMethodType = muzzleComp.AttackMethodType,
|
||||
AttackPropertyType = baseComp.AttackPropertyType,
|
||||
TagRuntimes = TowerTagAggregationService.AggregateTowerTags(
|
||||
muzzleComp.Tags,
|
||||
bearingComp.Tags,
|
||||
baseComp.Tags)
|
||||
};
|
||||
stats.Tags = TowerTagAggregationService.FlattenUniqueTags(stats.TagRuntimes);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int[] BuildLevelIntArray(int[] rarityBaseArray, RarityType rarity, int perLevel)
|
||||
{
|
||||
int baseValue = ResolveRarityBaseValue(rarityBaseArray, rarity);
|
||||
int[] values = new int[TowerLevelCount];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i] = Mathf.Max(0, baseValue + perLevel * i);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private static float[] BuildLevelFloatArray(float[] rarityBaseArray, RarityType rarity, float perLevel)
|
||||
{
|
||||
float baseValue = ResolveRarityBaseValue(rarityBaseArray, rarity);
|
||||
float[] values = new float[TowerLevelCount];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i] = Mathf.Max(0f, baseValue + perLevel * i);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private static int ResolveRarityBaseValue(int[] rarityBaseArray, RarityType rarity)
|
||||
{
|
||||
if (rarityBaseArray == null || rarityBaseArray.Length <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int rarityIndex = Mathf.Clamp((int)rarity - 1, 0, rarityBaseArray.Length - 1);
|
||||
return rarityBaseArray[rarityIndex];
|
||||
}
|
||||
|
||||
private static float ResolveRarityBaseValue(float[] rarityBaseArray, RarityType rarity)
|
||||
{
|
||||
if (rarityBaseArray == null || rarityBaseArray.Length <= 0)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
int rarityIndex = Mathf.Clamp((int)rarity - 1, 0, rarityBaseArray.Length - 1);
|
||||
return rarityBaseArray[rarityIndex];
|
||||
}
|
||||
|
||||
private IDataTable<DRMuzzleComp> EnsureMuzzleTable()
|
||||
{
|
||||
_drMuzzleComp ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
|
||||
return _drMuzzleComp;
|
||||
}
|
||||
|
||||
private IDataTable<DRBearingComp> EnsureBearingTable()
|
||||
{
|
||||
_drBearingComp ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
|
||||
return _drBearingComp;
|
||||
}
|
||||
|
||||
private IDataTable<DRBaseComp> EnsureBaseTable()
|
||||
{
|
||||
_drBaseComp ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
|
||||
return _drBaseComp;
|
||||
}
|
||||
|
||||
public bool TryDisassembleTower(long towerInstanceId)
|
||||
{
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
if (towerInstanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_queryModel.TryGetTowerById(towerInstanceId, out TowerItemData tower) || tower == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_queryModel.TryGetComponentById(inventory.MuzzleComponents, tower.MuzzleComponentInstanceId, out MuzzleCompItemData muzzleComp) ||
|
||||
!_queryModel.TryGetComponentById(inventory.BearingComponents, tower.BearingComponentInstanceId, out BearingCompItemData bearingComp) ||
|
||||
!_queryModel.TryGetComponentById(inventory.BaseComponents, tower.BaseComponentInstanceId, out BaseCompItemData baseComp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
InventoryParticipantUtility.TryRemoveParticipantTower(inventory, towerInstanceId, MaxParticipantTowerCount);
|
||||
|
||||
muzzleComp.IsAssembledIntoTower = false;
|
||||
bearingComp.IsAssembledIntoTower = false;
|
||||
baseComp.IsAssembledIntoTower = false;
|
||||
|
||||
inventory.Towers.Remove(tower);
|
||||
return true;
|
||||
}
|
||||
|
||||
private const int MaxParticipantTowerCount = 4;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class PlayerInventoryTowerRosterService
|
||||
{
|
||||
private readonly PlayerInventoryQueryModel _queryModel;
|
||||
private readonly int _maxParticipantTowerCount;
|
||||
|
||||
public PlayerInventoryTowerRosterService(PlayerInventoryQueryModel queryModel, int maxParticipantTowerCount)
|
||||
{
|
||||
_queryModel = queryModel;
|
||||
_maxParticipantTowerCount = Mathf.Max(1, maxParticipantTowerCount);
|
||||
}
|
||||
|
||||
public ParticipantTowerAssignResult TryAddParticipantTower(long towerInstanceId, int maxCount)
|
||||
{
|
||||
int resolvedMaxCount = Mathf.Max(1, maxCount);
|
||||
resolvedMaxCount = Mathf.Min(resolvedMaxCount, _maxParticipantTowerCount);
|
||||
return InventoryParticipantUtility.TryAddParticipantTower(
|
||||
_queryModel.Inventory,
|
||||
towerInstanceId,
|
||||
resolvedMaxCount);
|
||||
}
|
||||
|
||||
public bool TryRemoveParticipantTower(long towerInstanceId)
|
||||
{
|
||||
return InventoryParticipantUtility.TryRemoveParticipantTower(
|
||||
_queryModel.Inventory,
|
||||
towerInstanceId,
|
||||
_maxParticipantTowerCount);
|
||||
}
|
||||
|
||||
public int ReduceTowerEndurance(IReadOnlyList<long> towerInstanceIds, float enduranceLoss)
|
||||
{
|
||||
return InventoryTowerEnduranceUtility.ReduceTowerEndurance(
|
||||
_queryModel.Inventory,
|
||||
towerInstanceIds,
|
||||
enduranceLoss);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InventoryTowerEnduranceUtility
|
||||
{
|
||||
public static int ReduceTowerEndurance(
|
||||
BackpackInventoryData inventory,
|
||||
IReadOnlyList<long> towerInstanceIds,
|
||||
float enduranceLoss)
|
||||
{
|
||||
float resolvedLoss = Mathf.Max(0f, enduranceLoss);
|
||||
if (inventory?.Towers == null ||
|
||||
inventory.Towers.Count <= 0 ||
|
||||
resolvedLoss <= 0f ||
|
||||
towerInstanceIds == null ||
|
||||
towerInstanceIds.Count <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
Dictionary<long, MuzzleCompItemData> muzzleMap = BuildComponentMap(inventory.MuzzleComponents);
|
||||
Dictionary<long, BearingCompItemData> bearingMap = BuildComponentMap(inventory.BearingComponents);
|
||||
Dictionary<long, BaseCompItemData> baseMap = BuildComponentMap(inventory.BaseComponents);
|
||||
HashSet<long> processedTowerIds = new HashSet<long>();
|
||||
|
||||
int affectedCount = 0;
|
||||
for (int i = 0; i < towerInstanceIds.Count; i++)
|
||||
{
|
||||
long towerInstanceId = towerInstanceIds[i];
|
||||
if (towerInstanceId <= 0 || !processedTowerIds.Add(towerInstanceId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!InventoryParticipantUtility.TryGetTowerById(inventory, towerInstanceId, out TowerItemData tower) ||
|
||||
tower == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool towerAffected = false;
|
||||
if (muzzleMap.TryGetValue(tower.MuzzleComponentInstanceId, out MuzzleCompItemData muzzleComp))
|
||||
{
|
||||
towerAffected |= TryReduceComponentEndurance(muzzleComp, resolvedLoss);
|
||||
}
|
||||
|
||||
if (bearingMap.TryGetValue(tower.BearingComponentInstanceId, out BearingCompItemData bearingComp))
|
||||
{
|
||||
towerAffected |= TryReduceComponentEndurance(bearingComp, resolvedLoss);
|
||||
}
|
||||
|
||||
if (baseMap.TryGetValue(tower.BaseComponentInstanceId, out BaseCompItemData baseComp))
|
||||
{
|
||||
towerAffected |= TryReduceComponentEndurance(baseComp, resolvedLoss);
|
||||
}
|
||||
|
||||
if (towerAffected)
|
||||
{
|
||||
affectedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return affectedCount;
|
||||
}
|
||||
|
||||
private static bool TryReduceComponentEndurance(TowerCompItemData component, float enduranceLoss)
|
||||
{
|
||||
if (component == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float originalEndurance = component.Endurance;
|
||||
float nextEndurance = Mathf.Clamp(originalEndurance - Mathf.Max(0f, enduranceLoss), 0f, 100f);
|
||||
if (nextEndurance >= originalEndurance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
component.Endurance = nextEndurance;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Dictionary<long, TComp> BuildComponentMap<TComp>(List<TComp> components)
|
||||
where TComp : TowerCompItemData
|
||||
{
|
||||
Dictionary<long, TComp> map = new Dictionary<long, TComp>();
|
||||
if (components == null || components.Count <= 0)
|
||||
{
|
||||
return map;
|
||||
}
|
||||
|
||||
for (int i = 0; i < components.Count; i++)
|
||||
{
|
||||
TComp component = components[i];
|
||||
if (component == null || component.InstanceId <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
map[component.InstanceId] = component;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,400 @@
|
|||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public enum PlayerInventorySaleFailureReason : byte
|
||||
{
|
||||
None = 0,
|
||||
InvalidSelection = 1,
|
||||
ItemNotFound = 2,
|
||||
AssembledComponent = 3,
|
||||
ParticipantTower = 4,
|
||||
MissingTowerComponent = 5
|
||||
}
|
||||
|
||||
public sealed class PlayerInventorySaleCandidate
|
||||
{
|
||||
public long ItemId;
|
||||
public bool IsSellable;
|
||||
public bool IsTower;
|
||||
public int Price;
|
||||
public PlayerInventorySaleFailureReason FailureReason;
|
||||
}
|
||||
|
||||
public sealed class PlayerInventorySaleResult
|
||||
{
|
||||
public int GainedGold;
|
||||
public int SoldComponentCount;
|
||||
public int SoldTowerCount;
|
||||
public PlayerInventorySaleFailureReason FailureReason;
|
||||
|
||||
public bool IsSuccess => FailureReason == PlayerInventorySaleFailureReason.None;
|
||||
public int SoldItemCount => SoldComponentCount + SoldTowerCount;
|
||||
}
|
||||
|
||||
public sealed class PlayerInventoryTradeService
|
||||
{
|
||||
private readonly PlayerInventoryQueryModel _queryModel;
|
||||
private readonly PlayerInventoryCommandModel _commandModel;
|
||||
private IDataTable<DRShopPrice> _shopPriceTable;
|
||||
|
||||
public PlayerInventoryTradeService(
|
||||
PlayerInventoryQueryModel queryModel,
|
||||
PlayerInventoryCommandModel commandModel,
|
||||
IDataTable<DRShopPrice> shopPriceTable = null)
|
||||
{
|
||||
_queryModel = queryModel;
|
||||
_commandModel = commandModel;
|
||||
_shopPriceTable = shopPriceTable;
|
||||
}
|
||||
|
||||
public bool TryPurchaseComponent(TowerCompItemData item, int price)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_commandModel.TryConsumeGold(price))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BackpackInventoryData inventoryDelta = WrapSingleItem(item);
|
||||
PlayerInventoryMergeSummary summary = _commandModel.MergeInventory(inventoryDelta);
|
||||
return summary.HasAnyGain;
|
||||
}
|
||||
|
||||
public bool TryGetSaleCandidate(long itemId, out PlayerInventorySaleCandidate candidate)
|
||||
{
|
||||
candidate = BuildSaleCandidate(itemId);
|
||||
return candidate != null;
|
||||
}
|
||||
|
||||
public bool TrySellItems(IReadOnlyCollection<long> itemIds, out PlayerInventorySaleResult result)
|
||||
{
|
||||
result = new PlayerInventorySaleResult();
|
||||
if (itemIds == null || itemIds.Count <= 0)
|
||||
{
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.InvalidSelection;
|
||||
return false;
|
||||
}
|
||||
|
||||
HashSet<long> uniqueIds = new HashSet<long>();
|
||||
List<PlayerInventorySaleCandidate> candidates = new List<PlayerInventorySaleCandidate>(itemIds.Count);
|
||||
foreach (long itemId in itemIds)
|
||||
{
|
||||
if (itemId <= 0 || !uniqueIds.Add(itemId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PlayerInventorySaleCandidate candidate = BuildSaleCandidate(itemId);
|
||||
if (candidate == null || !candidate.IsSellable)
|
||||
{
|
||||
result.FailureReason = candidate?.FailureReason ?? PlayerInventorySaleFailureReason.ItemNotFound;
|
||||
return false;
|
||||
}
|
||||
|
||||
candidates.Add(candidate);
|
||||
}
|
||||
|
||||
if (candidates.Count <= 0)
|
||||
{
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.InvalidSelection;
|
||||
return false;
|
||||
}
|
||||
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
PlayerInventorySaleCandidate candidate = candidates[i];
|
||||
if (candidate.IsTower)
|
||||
{
|
||||
if (!TryRemoveTower(inventory, candidate.ItemId))
|
||||
{
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.MissingTowerComponent;
|
||||
return false;
|
||||
}
|
||||
|
||||
result.SoldTowerCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TryRemoveComponent(inventory, candidate.ItemId))
|
||||
{
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.ItemNotFound;
|
||||
return false;
|
||||
}
|
||||
|
||||
result.SoldComponentCount++;
|
||||
}
|
||||
|
||||
result.GainedGold += Mathf.Max(0, candidate.Price);
|
||||
}
|
||||
|
||||
_commandModel.AddGold(result.GainedGold);
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.None;
|
||||
return true;
|
||||
}
|
||||
|
||||
private PlayerInventorySaleCandidate BuildSaleCandidate(long itemId)
|
||||
{
|
||||
if (itemId <= 0)
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.InvalidSelection
|
||||
};
|
||||
}
|
||||
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
if (inventory == null)
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.ItemNotFound
|
||||
};
|
||||
}
|
||||
|
||||
if (_queryModel.TryGetTowerById(itemId, out TowerItemData tower) && tower != null)
|
||||
{
|
||||
if (inventory.ParticipantTowerInstanceIds != null && inventory.ParticipantTowerInstanceIds.Contains(itemId))
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
IsTower = true,
|
||||
FailureReason = PlayerInventorySaleFailureReason.ParticipantTower
|
||||
};
|
||||
}
|
||||
|
||||
if (!ShopPriceRuleService.TryResolveTowerSalePrice(tower, inventory, out int towerPrice, EnsureShopPriceTable()))
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
IsTower = true,
|
||||
FailureReason = PlayerInventorySaleFailureReason.MissingTowerComponent
|
||||
};
|
||||
}
|
||||
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = true,
|
||||
IsTower = true,
|
||||
Price = towerPrice,
|
||||
FailureReason = PlayerInventorySaleFailureReason.None
|
||||
};
|
||||
}
|
||||
|
||||
if (TryGetComponentById(inventory.MuzzleComponents, itemId, out MuzzleCompItemData muzzleComp))
|
||||
{
|
||||
return BuildComponentCandidate(muzzleComp);
|
||||
}
|
||||
|
||||
if (TryGetComponentById(inventory.BearingComponents, itemId, out BearingCompItemData bearingComp))
|
||||
{
|
||||
return BuildComponentCandidate(bearingComp);
|
||||
}
|
||||
|
||||
if (TryGetComponentById(inventory.BaseComponents, itemId, out BaseCompItemData baseComp))
|
||||
{
|
||||
return BuildComponentCandidate(baseComp);
|
||||
}
|
||||
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.ItemNotFound
|
||||
};
|
||||
}
|
||||
|
||||
private PlayerInventorySaleCandidate BuildComponentCandidate(TowerCompItemData component)
|
||||
{
|
||||
if (component == null)
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.ItemNotFound
|
||||
};
|
||||
}
|
||||
|
||||
if (component.IsAssembledIntoTower)
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = component.InstanceId,
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.AssembledComponent
|
||||
};
|
||||
}
|
||||
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = component.InstanceId,
|
||||
IsSellable = true,
|
||||
IsTower = false,
|
||||
Price = ShopPriceRuleService.ResolveComponentSalePrice(component, EnsureShopPriceTable()),
|
||||
FailureReason = PlayerInventorySaleFailureReason.None
|
||||
};
|
||||
}
|
||||
|
||||
private bool TryRemoveTower(BackpackInventoryData inventory, long towerId)
|
||||
{
|
||||
if (inventory?.Towers == null || towerId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TowerItemData targetTower = null;
|
||||
for (int i = 0; i < inventory.Towers.Count; i++)
|
||||
{
|
||||
TowerItemData tower = inventory.Towers[i];
|
||||
if (tower != null && tower.InstanceId == towerId)
|
||||
{
|
||||
targetTower = tower;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetTower == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ContainsInstanceId(inventory.MuzzleComponents, targetTower.MuzzleComponentInstanceId) ||
|
||||
!ContainsInstanceId(inventory.BearingComponents, targetTower.BearingComponentInstanceId) ||
|
||||
!ContainsInstanceId(inventory.BaseComponents, targetTower.BaseComponentInstanceId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool removedMuzzle = RemoveByInstanceId(inventory.MuzzleComponents, targetTower.MuzzleComponentInstanceId);
|
||||
bool removedBearing = RemoveByInstanceId(inventory.BearingComponents, targetTower.BearingComponentInstanceId);
|
||||
bool removedBase = RemoveByInstanceId(inventory.BaseComponents, targetTower.BaseComponentInstanceId);
|
||||
if (!removedMuzzle || !removedBearing || !removedBase)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
inventory.Towers.Remove(targetTower);
|
||||
inventory.ParticipantTowerInstanceIds?.Remove(towerId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryRemoveComponent(BackpackInventoryData inventory, long itemId)
|
||||
{
|
||||
return RemoveByInstanceId(inventory?.MuzzleComponents, itemId) ||
|
||||
RemoveByInstanceId(inventory?.BearingComponents, itemId) ||
|
||||
RemoveByInstanceId(inventory?.BaseComponents, itemId);
|
||||
}
|
||||
|
||||
private static bool RemoveByInstanceId<TItem>(List<TItem> items, long instanceId)
|
||||
where TItem : class
|
||||
{
|
||||
if (items == null || instanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
switch (items[i])
|
||||
{
|
||||
case TowerCompItemData component when component.InstanceId == instanceId:
|
||||
items.RemoveAt(i);
|
||||
return true;
|
||||
case TowerItemData tower when tower.InstanceId == instanceId:
|
||||
items.RemoveAt(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ContainsInstanceId<TItem>(IReadOnlyList<TItem> items, long instanceId)
|
||||
where TItem : class
|
||||
{
|
||||
if (items == null || instanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
switch (items[i])
|
||||
{
|
||||
case TowerCompItemData component when component.InstanceId == instanceId:
|
||||
return true;
|
||||
case TowerItemData tower when tower.InstanceId == instanceId:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetComponentById<TComp>(IReadOnlyList<TComp> items, long instanceId, out TComp result)
|
||||
where TComp : TowerCompItemData
|
||||
{
|
||||
result = null;
|
||||
if (items == null || instanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
TComp item = items[i];
|
||||
if (item != null && item.InstanceId == instanceId)
|
||||
{
|
||||
result = item;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static BackpackInventoryData WrapSingleItem(TowerCompItemData item)
|
||||
{
|
||||
BackpackInventoryData inventory = new BackpackInventoryData();
|
||||
switch (item)
|
||||
{
|
||||
case MuzzleCompItemData muzzleComp:
|
||||
inventory.MuzzleComponents.Add(InventoryCloneUtility.CloneMuzzleComp(muzzleComp));
|
||||
break;
|
||||
case BearingCompItemData bearingComp:
|
||||
inventory.BearingComponents.Add(InventoryCloneUtility.CloneBearingComp(bearingComp));
|
||||
break;
|
||||
case BaseCompItemData baseComp:
|
||||
inventory.BaseComponents.Add(InventoryCloneUtility.CloneBaseComp(baseComp));
|
||||
break;
|
||||
}
|
||||
|
||||
return inventory;
|
||||
}
|
||||
|
||||
private IDataTable<DRShopPrice> EnsureShopPriceTable()
|
||||
{
|
||||
_shopPriceTable ??= GameEntry.DataTable.GetDataTable<DRShopPrice>();
|
||||
return _shopPriceTable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,445 @@
|
|||
using System.Collections.Generic;
|
||||
using GameFramework.Event;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Keeps gameplay camera and UI in a fixed design aspect, then fills extra area with black masks.
|
||||
/// </summary>
|
||||
public class ResolutionAdapterComponent : GameFrameworkComponent
|
||||
{
|
||||
private const float DefaultCanvasPlaneDistance = 100f;
|
||||
|
||||
[SerializeField] private Vector2 _referenceResolution = new Vector2(2560f, 1600f);
|
||||
[SerializeField] private bool _adaptUiCanvasToViewport = true;
|
||||
[SerializeField] private bool _enableBlackMask = true;
|
||||
[SerializeField] private List<Transform> _uiRoots = new List<Transform>();
|
||||
|
||||
private readonly List<Canvas> _canvasBuffer = new List<Canvas>(32);
|
||||
private readonly List<Canvas> _trackedCanvases = new List<Canvas>(32);
|
||||
private readonly HashSet<Canvas> _trackedCanvasSet = new HashSet<Canvas>();
|
||||
|
||||
private Camera _mainCamera;
|
||||
private Rect _targetViewport = new Rect(0f, 0f, 1f, 1f);
|
||||
private int _cachedScreenWidth = -1;
|
||||
private int _cachedScreenHeight = -1;
|
||||
|
||||
private bool _canvasCacheDirty = true;
|
||||
private bool _uiEventSubscribed;
|
||||
private bool _missingRootWarned;
|
||||
|
||||
private Canvas _maskCanvas;
|
||||
private RectTransform _leftMask;
|
||||
private RectTransform _rightMask;
|
||||
private RectTransform _topMask;
|
||||
private RectTransform _bottomMask;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
TryEnsureUiEventSubscribed();
|
||||
ApplyAdaptation(true);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
TryEnsureUiEventSubscribed();
|
||||
ApplyAdaptation(false);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
UnsubscribeUiEvents();
|
||||
|
||||
if (_mainCamera != null)
|
||||
{
|
||||
_mainCamera.rect = new Rect(0f, 0f, 1f, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyAdaptation(bool force)
|
||||
{
|
||||
if (_referenceResolution.x <= 0f || _referenceResolution.y <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Camera resolvedCamera = ResolveMainCamera();
|
||||
bool cameraChanged = resolvedCamera != _mainCamera;
|
||||
bool screenChanged = Screen.width != _cachedScreenWidth || Screen.height != _cachedScreenHeight;
|
||||
bool needRecalculate = force || cameraChanged || screenChanged;
|
||||
|
||||
if (needRecalculate)
|
||||
{
|
||||
Camera lastCamera = _mainCamera;
|
||||
if (lastCamera != null && lastCamera != resolvedCamera)
|
||||
{
|
||||
lastCamera.rect = new Rect(0f, 0f, 1f, 1f);
|
||||
}
|
||||
|
||||
_mainCamera = resolvedCamera;
|
||||
_cachedScreenWidth = Screen.width;
|
||||
_cachedScreenHeight = Screen.height;
|
||||
_targetViewport = CalculateViewport(_cachedScreenWidth, _cachedScreenHeight);
|
||||
|
||||
ApplyCameraViewport();
|
||||
UpdateMaskLayout();
|
||||
}
|
||||
|
||||
if (!_adaptUiCanvasToViewport)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!needRecalculate && !_canvasCacheDirty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyCanvasAdaptation();
|
||||
}
|
||||
|
||||
private Camera ResolveMainCamera()
|
||||
{
|
||||
if (GameEntry.Scene != null && GameEntry.Scene.MainCamera != null)
|
||||
{
|
||||
return GameEntry.Scene.MainCamera;
|
||||
}
|
||||
|
||||
if (Camera.main != null)
|
||||
{
|
||||
return Camera.main;
|
||||
}
|
||||
|
||||
if (_mainCamera != null && _mainCamera.isActiveAndEnabled)
|
||||
{
|
||||
return _mainCamera;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Rect CalculateViewport(int width, int height)
|
||||
{
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
return new Rect(0f, 0f, 1f, 1f);
|
||||
}
|
||||
|
||||
float referenceAspect = _referenceResolution.x / _referenceResolution.y;
|
||||
float screenAspect = (float)width / height;
|
||||
|
||||
if (Mathf.Approximately(referenceAspect, screenAspect))
|
||||
{
|
||||
return new Rect(0f, 0f, 1f, 1f);
|
||||
}
|
||||
|
||||
if (screenAspect > referenceAspect)
|
||||
{
|
||||
float viewportWidth = referenceAspect / screenAspect;
|
||||
return new Rect((1f - viewportWidth) * 0.5f, 0f, viewportWidth, 1f);
|
||||
}
|
||||
|
||||
float viewportHeight = screenAspect / referenceAspect;
|
||||
return new Rect(0f, (1f - viewportHeight) * 0.5f, 1f, viewportHeight);
|
||||
}
|
||||
|
||||
private void ApplyCameraViewport()
|
||||
{
|
||||
if (_mainCamera == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_mainCamera.rect = _targetViewport;
|
||||
}
|
||||
|
||||
private void ApplyCanvasAdaptation()
|
||||
{
|
||||
if (_mainCamera == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_uiRoots == null || _uiRoots.Count == 0)
|
||||
{
|
||||
if (!_missingRootWarned)
|
||||
{
|
||||
_missingRootWarned = true;
|
||||
Log.Warning(
|
||||
"ResolutionAdapterComponent missing injected roots. Assign UI/HPBar root transforms in scene.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_canvasCacheDirty)
|
||||
{
|
||||
RebuildCanvasCache();
|
||||
}
|
||||
|
||||
for (int i = _trackedCanvases.Count - 1; i >= 0; i--)
|
||||
{
|
||||
Canvas canvas = _trackedCanvases[i];
|
||||
if (canvas == null)
|
||||
{
|
||||
_trackedCanvases.RemoveAt(i);
|
||||
_canvasCacheDirty = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
ApplyCanvasSettings(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildCanvasCache()
|
||||
{
|
||||
_canvasCacheDirty = false;
|
||||
_trackedCanvases.Clear();
|
||||
_trackedCanvasSet.Clear();
|
||||
|
||||
foreach (var root in _uiRoots)
|
||||
{
|
||||
CollectCanvases(root);
|
||||
}
|
||||
}
|
||||
|
||||
private void CollectCanvases(Transform root)
|
||||
{
|
||||
if (root == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
root.GetComponentsInChildren(true, _canvasBuffer);
|
||||
foreach (Canvas canvas in _canvasBuffer)
|
||||
{
|
||||
if (canvas == null || canvas == _maskCanvas || canvas.renderMode == RenderMode.WorldSpace)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_trackedCanvasSet.Add(canvas))
|
||||
{
|
||||
_trackedCanvases.Add(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
_canvasBuffer.Clear();
|
||||
}
|
||||
|
||||
private void ApplyCanvasSettings(Canvas canvas)
|
||||
{
|
||||
if (canvas == _maskCanvas || canvas.renderMode == RenderMode.WorldSpace)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.renderMode = RenderMode.ScreenSpaceCamera;
|
||||
canvas.worldCamera = _mainCamera;
|
||||
canvas.planeDistance = ResolveCanvasPlaneDistance(_mainCamera);
|
||||
|
||||
CanvasScaler scaler = canvas.GetComponent<CanvasScaler>();
|
||||
if (scaler != null)
|
||||
{
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = _referenceResolution;
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
private static float ResolveCanvasPlaneDistance(Camera camera)
|
||||
{
|
||||
if (camera == null)
|
||||
{
|
||||
return DefaultCanvasPlaneDistance;
|
||||
}
|
||||
|
||||
float minDistance = camera.nearClipPlane + 0.05f;
|
||||
float maxDistance = camera.farClipPlane - 0.05f;
|
||||
if (maxDistance <= minDistance)
|
||||
{
|
||||
return minDistance;
|
||||
}
|
||||
|
||||
return Mathf.Clamp(DefaultCanvasPlaneDistance, minDistance, maxDistance);
|
||||
}
|
||||
|
||||
private void TryEnsureUiEventSubscribed()
|
||||
{
|
||||
if (_uiEventSubscribed || GameEntry.Event == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OnUIFormChanged);
|
||||
GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, OnUIFormChanged);
|
||||
_uiEventSubscribed = true;
|
||||
_canvasCacheDirty = true;
|
||||
}
|
||||
|
||||
private void UnsubscribeUiEvents()
|
||||
{
|
||||
if (!_uiEventSubscribed || GameEntry.Event == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OnUIFormChanged);
|
||||
GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, OnUIFormChanged);
|
||||
_uiEventSubscribed = false;
|
||||
}
|
||||
|
||||
private void OnUIFormChanged(object sender, GameEventArgs e)
|
||||
{
|
||||
_canvasCacheDirty = true;
|
||||
}
|
||||
|
||||
private void UpdateMaskLayout()
|
||||
{
|
||||
if (!_enableBlackMask)
|
||||
{
|
||||
SetMaskVisible(false, false);
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureMaskObjects();
|
||||
if (_maskCanvas == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float horizontalPadding = Mathf.Max(0f, (1f - _targetViewport.width) * 0.5f * _cachedScreenWidth);
|
||||
float verticalPadding = Mathf.Max(0f, (1f - _targetViewport.height) * 0.5f * _cachedScreenHeight);
|
||||
|
||||
bool showVerticalMask = horizontalPadding > 0.5f;
|
||||
bool showHorizontalMask = verticalPadding > 0.5f;
|
||||
|
||||
SetMaskVisible(showVerticalMask, showHorizontalMask);
|
||||
if (showVerticalMask)
|
||||
{
|
||||
SetVerticalMaskRect(_leftMask, true, horizontalPadding);
|
||||
SetVerticalMaskRect(_rightMask, false, horizontalPadding);
|
||||
}
|
||||
|
||||
if (showHorizontalMask)
|
||||
{
|
||||
SetHorizontalMaskRect(_topMask, true, verticalPadding);
|
||||
SetHorizontalMaskRect(_bottomMask, false, verticalPadding);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureMaskObjects()
|
||||
{
|
||||
if (_maskCanvas != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject maskRoot = new GameObject(
|
||||
"ResolutionMask",
|
||||
typeof(RectTransform),
|
||||
typeof(Canvas),
|
||||
typeof(CanvasScaler),
|
||||
typeof(GraphicRaycaster));
|
||||
maskRoot.transform.SetParent(transform, false);
|
||||
|
||||
_maskCanvas = maskRoot.GetComponent<Canvas>();
|
||||
_maskCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
_maskCanvas.overrideSorting = true;
|
||||
_maskCanvas.sortingOrder = short.MaxValue;
|
||||
|
||||
CanvasScaler scaler = maskRoot.GetComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ConstantPixelSize;
|
||||
scaler.scaleFactor = 1f;
|
||||
|
||||
GraphicRaycaster raycaster = maskRoot.GetComponent<GraphicRaycaster>();
|
||||
raycaster.enabled = false;
|
||||
|
||||
RectTransform rootRect = maskRoot.GetComponent<RectTransform>();
|
||||
rootRect.anchorMin = Vector2.zero;
|
||||
rootRect.anchorMax = Vector2.one;
|
||||
rootRect.anchoredPosition = Vector2.zero;
|
||||
rootRect.sizeDelta = Vector2.zero;
|
||||
|
||||
_leftMask = CreateMaskRect("LeftMask", rootRect);
|
||||
_rightMask = CreateMaskRect("RightMask", rootRect);
|
||||
_topMask = CreateMaskRect("TopMask", rootRect);
|
||||
_bottomMask = CreateMaskRect("BottomMask", rootRect);
|
||||
}
|
||||
|
||||
private static RectTransform CreateMaskRect(string maskName, Transform parent)
|
||||
{
|
||||
GameObject maskObject = new GameObject(maskName, typeof(RectTransform), typeof(Image));
|
||||
maskObject.transform.SetParent(parent, false);
|
||||
|
||||
Image image = maskObject.GetComponent<Image>();
|
||||
image.color = Color.black;
|
||||
image.raycastTarget = false;
|
||||
|
||||
return maskObject.GetComponent<RectTransform>();
|
||||
}
|
||||
|
||||
private void SetMaskVisible(bool showVerticalMask, bool showHorizontalMask)
|
||||
{
|
||||
if (_maskCanvas == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool visible = showVerticalMask || showHorizontalMask;
|
||||
_maskCanvas.enabled = visible;
|
||||
|
||||
if (_leftMask != null)
|
||||
{
|
||||
_leftMask.gameObject.SetActive(showVerticalMask);
|
||||
}
|
||||
|
||||
if (_rightMask != null)
|
||||
{
|
||||
_rightMask.gameObject.SetActive(showVerticalMask);
|
||||
}
|
||||
|
||||
if (_topMask != null)
|
||||
{
|
||||
_topMask.gameObject.SetActive(showHorizontalMask);
|
||||
}
|
||||
|
||||
if (_bottomMask != null)
|
||||
{
|
||||
_bottomMask.gameObject.SetActive(showHorizontalMask);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetVerticalMaskRect(RectTransform maskRect, bool isLeft, float width)
|
||||
{
|
||||
if (maskRect == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
maskRect.anchorMin = new Vector2(isLeft ? 0f : 1f, 0f);
|
||||
maskRect.anchorMax = new Vector2(isLeft ? 0f : 1f, 1f);
|
||||
maskRect.pivot = new Vector2(isLeft ? 0f : 1f, 0.5f);
|
||||
maskRect.anchoredPosition = Vector2.zero;
|
||||
maskRect.sizeDelta = new Vector2(width, 0f);
|
||||
}
|
||||
|
||||
private static void SetHorizontalMaskRect(RectTransform maskRect, bool isTop, float height)
|
||||
{
|
||||
if (maskRect == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
maskRect.anchorMin = new Vector2(0f, isTop ? 1f : 0f);
|
||||
maskRect.anchorMax = new Vector2(1f, isTop ? 1f : 0f);
|
||||
maskRect.pivot = new Vector2(0.5f, isTop ? 1f : 0f);
|
||||
maskRect.anchoredPosition = Vector2.zero;
|
||||
maskRect.sizeDelta = new Vector2(0f, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
using GeometryTD.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Procedure;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class ShopNodeComponent : GameFrameworkComponent
|
||||
{
|
||||
private RunNodeExecutionContext _activeContext;
|
||||
|
||||
private ShopFormUseCase _shopFormUseCase;
|
||||
private bool _initialized;
|
||||
|
||||
public void OnInit()
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_shopFormUseCase ??= new ShopFormUseCase();
|
||||
GameEntry.UIRouter.BindUIUseCase(UIFormType.ShopForm, _shopFormUseCase);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
public void StartShop(RunNodeExecutionContext context = null)
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
OnInit();
|
||||
}
|
||||
|
||||
int runSeed = context?.RunSeed ?? 0;
|
||||
int sequenceIndex = context?.SequenceIndex ?? -1;
|
||||
if (_shopFormUseCase == null || !_shopFormUseCase.PrepareForOpen(runSeed, sequenceIndex))
|
||||
{
|
||||
Log.Warning("ShopNodeComponent.StartShop() failed. Shop use case is unavailable or goods generation failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
_activeContext = context != null ? context.Clone() : null;
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.ShopForm);
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeEnterEventArgs.Create(
|
||||
_activeContext?.RunId,
|
||||
_activeContext?.NodeId ?? 0,
|
||||
_activeContext?.NodeType ?? RunNodeType.None,
|
||||
_activeContext?.SequenceIndex ?? -1));
|
||||
}
|
||||
|
||||
public void EndShop()
|
||||
{
|
||||
GameEntry.UIRouter.CloseUI(UIFormType.ShopForm);
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeCompleteEventArgs.Create(
|
||||
_activeContext?.RunId,
|
||||
_activeContext?.NodeId ?? 0,
|
||||
_activeContext?.NodeType ?? RunNodeType.None,
|
||||
_activeContext?.SequenceIndex ?? -1,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
true,
|
||||
_activeContext != null
|
||||
? _activeContext.CreateCompletionSnapshot(
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
|
||||
: null));
|
||||
ClearActiveNodeContext();
|
||||
}
|
||||
|
||||
private void ClearActiveNodeContext()
|
||||
{
|
||||
_activeContext = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.Resource;
|
||||
using GeometryTD.CustomUtility;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class SpriteCacheComponent : GameFrameworkComponent
|
||||
{
|
||||
[SerializeField] private float _pixelsPerUnit = 100f;
|
||||
[SerializeField] private Vector2 _defaultPivot = new(0.5f, 0.5f);
|
||||
|
||||
private Dictionary<string, Sprite> _spriteCache;
|
||||
private Dictionary<string, List<Action<Sprite>>> _pendingCallbacks;
|
||||
private ResourceComponent _resource;
|
||||
|
||||
void Start()
|
||||
{
|
||||
_spriteCache = new Dictionary<string, Sprite>();
|
||||
_pendingCallbacks = new Dictionary<string, List<Action<Sprite>>>();
|
||||
_resource = GameEntry.Resource;
|
||||
}
|
||||
|
||||
public void GetSprite(string assetName, Action<Sprite> callback)
|
||||
{
|
||||
if (_spriteCache.TryGetValue(assetName, out var sprite))
|
||||
{
|
||||
callback?.Invoke(sprite);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pendingCallbacks.TryGetValue(assetName, out var pendingList))
|
||||
{
|
||||
pendingList.Add(callback);
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingCallbacks[assetName] = new List<Action<Sprite>> { callback };
|
||||
_resource.LoadAsset
|
||||
(
|
||||
AssetUtility.GetTextureAsset(assetName),
|
||||
Constant.AssetPriority.UIFormAsset,
|
||||
new LoadAssetCallbacks(
|
||||
(resourcePath, asset, duration, userData) =>
|
||||
{
|
||||
Log.Debug(resourcePath);
|
||||
Texture2D texture = asset as Texture2D;
|
||||
Sprite loadedSprite = null;
|
||||
|
||||
if (texture != null)
|
||||
{
|
||||
loadedSprite = Sprite.Create(
|
||||
texture,
|
||||
new Rect(0, 0, texture.width, texture.height),
|
||||
_defaultPivot,
|
||||
_pixelsPerUnit);
|
||||
|
||||
_spriteCache[assetName] = loadedSprite;
|
||||
}
|
||||
|
||||
if (_pendingCallbacks.TryGetValue(assetName, out var callbacks))
|
||||
{
|
||||
_pendingCallbacks.Remove(assetName);
|
||||
for (int i = 0; i < callbacks.Count; i++)
|
||||
{
|
||||
callbacks[i]?.Invoke(loadedSprite);
|
||||
}
|
||||
}
|
||||
},
|
||||
(resourcePath, status, errorMessage, userData) =>
|
||||
{
|
||||
Log.Error("Can not load icon '{0}' from '{1}' with error message '{2}'.",
|
||||
assetName,
|
||||
resourcePath,
|
||||
errorMessage);
|
||||
|
||||
if (_pendingCallbacks.TryGetValue(assetName, out var callbacks))
|
||||
{
|
||||
_pendingCallbacks.Remove(assetName);
|
||||
for (int i = 0; i < callbacks.Count; i++)
|
||||
{
|
||||
callbacks[i]?.Invoke(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_spriteCache.Clear();
|
||||
_pendingCallbacks.Clear();
|
||||
_resource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
using System;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class TagRegistryComponent : GameFrameworkComponent
|
||||
{
|
||||
private IDataTable<DRTagConfig> _tagConfigTable;
|
||||
private IDataTable<DRTag> _tagTable;
|
||||
private IDataTable<DRRarityTagBudget> _rarityTagBudgetTable;
|
||||
|
||||
public void OnInit()
|
||||
{
|
||||
ReloadAllFromLoadedTables();
|
||||
}
|
||||
|
||||
public void ReloadAllFromLoadedTables()
|
||||
{
|
||||
ReloadTagDefinitionsAndGenerationFromLoadedTables();
|
||||
ReloadRarityTagBudgetFromLoadedTable();
|
||||
}
|
||||
|
||||
public void ReloadTagDefinitionsAndGenerationFromLoadedTables()
|
||||
{
|
||||
EnsureTagDefinitionTables();
|
||||
DRTagConfig[] tagConfigRows = _tagConfigTable.GetAllDataRows();
|
||||
DRTag[] tagRows = _tagTable.GetAllDataRows();
|
||||
|
||||
TagGenerationRuleRegistry.LoadFromRows(tagRows);
|
||||
TagDefinitionRegistry.ReloadFromRows(tagConfigRows, tagRows);
|
||||
}
|
||||
|
||||
public void ReloadRarityTagBudgetFromLoadedTable()
|
||||
{
|
||||
EnsureRarityTagBudgetTable();
|
||||
RarityTagBudgetRuleRegistry.LoadFromRows(_rarityTagBudgetTable.GetAllDataRows());
|
||||
}
|
||||
|
||||
private void EnsureTagDefinitionTables()
|
||||
{
|
||||
_tagConfigTable ??= GameEntry.DataTable.GetDataTable<DRTagConfig>();
|
||||
_tagTable ??= GameEntry.DataTable.GetDataTable<DRTag>();
|
||||
|
||||
if (_tagConfigTable == null || _tagTable == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"TagRegistryComponent requires TagConfig and Tag data tables to be loaded before initialization.");
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureRarityTagBudgetTable()
|
||||
{
|
||||
_rarityTagBudgetTable ??= GameEntry.DataTable.GetDataTable<DRRarityTagBudget>();
|
||||
if (_rarityTagBudgetTable == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"TagRegistryComponent requires RarityTagBudget data table to be loaded before initialization.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.Definition;
|
||||
using UnityGameFramework.Runtime;
|
||||
using GeometryTD.UI;
|
||||
|
||||
namespace CustomComponent
|
||||
{
|
||||
public class UIRouterComponent : GameFrameworkComponent
|
||||
{
|
||||
private readonly Dictionary<UIFormType, IUIFormController> _routeControllers = new();
|
||||
|
||||
private const string UINameSpace = "GeometryTD.UI";
|
||||
|
||||
public void BindUIUseCase(UIFormType uiFormType, IUIUseCase useCase)
|
||||
{
|
||||
IUIFormController controller = GetOrCreateController(uiFormType);
|
||||
controller.BindUseCase(useCase);
|
||||
}
|
||||
|
||||
public int? OpenUI(UIFormType uiFormType, object userData = null)
|
||||
{
|
||||
IUIFormController controller = GetOrCreateController(uiFormType);
|
||||
return controller.OpenUI(userData);
|
||||
}
|
||||
|
||||
public void CloseUI(UIFormType uiFormType)
|
||||
{
|
||||
IUIFormController controller = GetOrCreateController(uiFormType);
|
||||
controller.CloseUI();
|
||||
}
|
||||
|
||||
private IUIFormController GetOrCreateController(UIFormType uiFormType)
|
||||
{
|
||||
if (_routeControllers.TryGetValue(uiFormType, out IUIFormController controller))
|
||||
{
|
||||
return controller;
|
||||
}
|
||||
|
||||
string typename = $"{UINameSpace}.{uiFormType}Controller";
|
||||
Type controllerType = Type.GetType(typename);
|
||||
if (controllerType == null)
|
||||
{
|
||||
controller = new DefaultUIFormController(uiFormType);
|
||||
Log.Warning("Can not find UI Controller for type '{0}'.", typename);
|
||||
}
|
||||
else
|
||||
{
|
||||
controller = (IUIFormController)Activator.CreateInstance(controllerType);
|
||||
}
|
||||
|
||||
_routeControllers.Add(uiFormType, controller);
|
||||
return controller;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
foreach (KeyValuePair<UIFormType, IUIFormController> pair in _routeControllers)
|
||||
{
|
||||
pair.Value.CloseUI();
|
||||
}
|
||||
|
||||
_routeControllers.Clear();
|
||||
}
|
||||
|
||||
private class DefaultUIFormController : IUIFormController
|
||||
{
|
||||
private readonly UIFormType _uiFormType;
|
||||
private int? _lastSerialId;
|
||||
|
||||
public DefaultUIFormController(UIFormType uiFormType)
|
||||
{
|
||||
_uiFormType = uiFormType;
|
||||
}
|
||||
|
||||
public int? OpenUI(object userData = null)
|
||||
{
|
||||
_lastSerialId = GameEntry.UI.OpenUIForm(_uiFormType, userData);
|
||||
return _lastSerialId;
|
||||
}
|
||||
|
||||
public void CloseUI()
|
||||
{
|
||||
if (_lastSerialId.HasValue)
|
||||
{
|
||||
GameEntry.UI.CloseUIForm(_lastSerialId.Value);
|
||||
_lastSerialId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
UGuiForm uiForm = GameEntry.UI.GetUIForm(_uiFormType);
|
||||
if (uiForm != null)
|
||||
{
|
||||
GameEntry.UI.CloseUIForm(uiForm);
|
||||
}
|
||||
}
|
||||
|
||||
public void BindUseCase(IUIUseCase useCase)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
//------------------------------------------------------------
|
||||
// Game Framework
|
||||
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
|
||||
// Homepage: https://gameframework.cn/
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
public static class BinaryReaderExtension
|
||||
{
|
||||
public static Color32 ReadColor32(this BinaryReader binaryReader)
|
||||
{
|
||||
return new Color32(binaryReader.ReadByte(), binaryReader.ReadByte(), binaryReader.ReadByte(), binaryReader.ReadByte());
|
||||
}
|
||||
|
||||
public static Color ReadColor(this BinaryReader binaryReader)
|
||||
{
|
||||
return new Color(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle());
|
||||
}
|
||||
|
||||
public static DateTime ReadDateTime(this BinaryReader binaryReader)
|
||||
{
|
||||
return new DateTime(binaryReader.ReadInt64());
|
||||
}
|
||||
|
||||
public static Quaternion ReadQuaternion(this BinaryReader binaryReader)
|
||||
{
|
||||
return new Quaternion(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle());
|
||||
}
|
||||
|
||||
public static Rect ReadRect(this BinaryReader binaryReader)
|
||||
{
|
||||
return new Rect(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle());
|
||||
}
|
||||
|
||||
public static Vector2 ReadVector2(this BinaryReader binaryReader)
|
||||
{
|
||||
return new Vector2(binaryReader.ReadSingle(), binaryReader.ReadSingle());
|
||||
}
|
||||
|
||||
public static Vector3 ReadVector3(this BinaryReader binaryReader)
|
||||
{
|
||||
return new Vector3(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle());
|
||||
}
|
||||
|
||||
public static Vector4 ReadVector4(this BinaryReader binaryReader)
|
||||
{
|
||||
return new Vector4(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
using System;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.CustomUtility;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 底座组件表
|
||||
/// </summary>
|
||||
public class DRBaseComp : DataRowBase
|
||||
{
|
||||
private int m_Id = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 获取底座组件编号
|
||||
/// </summary>
|
||||
public override int Id => m_Id;
|
||||
|
||||
/// <summary>
|
||||
/// 获取底座组件名
|
||||
/// </summary>
|
||||
public string Name { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取底座组件攻击速度数组(秒/次)
|
||||
/// </summary>
|
||||
public float[] AttackSpeed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取底座组件每级提升攻击速度(值为负数)
|
||||
/// </summary>
|
||||
public float AttackSpeedPerLevel { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取攻击属性
|
||||
/// </summary>
|
||||
public AttackPropertyType AttackPropertyType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取属性约束
|
||||
/// </summary>
|
||||
public string Constraint { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取可能出现的 Tag
|
||||
/// </summary>
|
||||
public TagType[] PossibleTag { get; private set; }
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
Name = columnStrings[index++];
|
||||
AttackSpeed = GenerateAttackSpeed(columnStrings[index++]);
|
||||
AttackSpeedPerLevel = float.Parse(columnStrings[index++]);
|
||||
AttackPropertyType = EnumUtility<AttackPropertyType>.Get(columnStrings[index++]);
|
||||
Constraint = columnStrings[index++];
|
||||
PossibleTag = GeneratePossibleTag(columnStrings[index++]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private float[] GenerateAttackSpeed(string raw)
|
||||
{
|
||||
if (!raw.StartsWith('[') || !raw.EndsWith(']'))
|
||||
{
|
||||
throw new ArgumentException("Input must be enclosed in square brackets.");
|
||||
}
|
||||
|
||||
if (raw.Length == 2) return new float[] { };
|
||||
string[] attackSpeedRaws = raw.Substring(1, raw.Length - 2).Split(",");
|
||||
int length = attackSpeedRaws.Length;
|
||||
var attackSpeed = new float[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
attackSpeed[i] = float.Parse(attackSpeedRaws[i]);
|
||||
}
|
||||
|
||||
return attackSpeed;
|
||||
}
|
||||
|
||||
private TagType[] GeneratePossibleTag(string raw)
|
||||
{
|
||||
if (!raw.StartsWith('[') || !raw.EndsWith(']'))
|
||||
{
|
||||
throw new ArgumentException("Input must be enclosed in square brackets.");
|
||||
}
|
||||
|
||||
if (raw.Length == 2) return new TagType[] { };
|
||||
string[] tagTypes = raw.Substring(1, raw.Length - 2).Split(",");
|
||||
int length = tagTypes.Length;
|
||||
var tags = new TagType[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
tags[i] = EnumUtility<TagType>.Get(tagTypes[i]);
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
using System;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.CustomUtility;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 轴承组件表
|
||||
/// </summary>
|
||||
public class DRBearingComp : DataRowBase
|
||||
{
|
||||
private int m_Id = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 获取轴承组件编号
|
||||
/// </summary>
|
||||
public override int Id => m_Id;
|
||||
|
||||
/// <summary>
|
||||
/// 获取轴承组件名
|
||||
/// </summary>
|
||||
public string Name { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取轴承组件旋转速度数组
|
||||
/// </summary>
|
||||
public float[] RotateSpeed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取轴承组件每级提升旋转速度
|
||||
/// </summary>
|
||||
public float RotateSpeedPerLevel { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取攻击范围
|
||||
/// </summary>
|
||||
public float[] AttackRange { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取每级提升攻击范围
|
||||
/// </summary>
|
||||
public float AttackRangePerLevel { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取属性约束
|
||||
/// </summary>
|
||||
public string Constraint { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取可能出现的 Tag
|
||||
/// </summary>
|
||||
public TagType[] PossibleTag { get; private set; }
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
Name = columnStrings[index++];
|
||||
RotateSpeed = GenerateFloatArray(columnStrings[index++]);
|
||||
RotateSpeedPerLevel = float.Parse(columnStrings[index++]);
|
||||
AttackRange = GenerateFloatArray(columnStrings[index++]);
|
||||
AttackRangePerLevel = float.Parse(columnStrings[index++]);
|
||||
Constraint = columnStrings[index++];
|
||||
PossibleTag = GeneratePossibleTag(columnStrings[index++]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private float[] GenerateFloatArray(string raw)
|
||||
{
|
||||
if (!raw.StartsWith('[') || !raw.EndsWith(']'))
|
||||
{
|
||||
throw new ArgumentException("Input must be enclosed in square brackets.");
|
||||
}
|
||||
|
||||
if (raw.Length == 2) return new float[] { };
|
||||
string[] raws = raw.Substring(1, raw.Length - 2).Split(",");
|
||||
int length = raws.Length;
|
||||
var array = new float[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
array[i] = float.Parse(raws[i]);
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
private TagType[] GeneratePossibleTag(string raw)
|
||||
{
|
||||
if (!raw.StartsWith('[') || !raw.EndsWith(']'))
|
||||
{
|
||||
throw new ArgumentException("Input must be enclosed in square brackets.");
|
||||
}
|
||||
|
||||
if (raw.Length == 2) return new TagType[] { };
|
||||
string[] tagTypes = raw.Substring(1, raw.Length - 2).Split(",");
|
||||
int length = tagTypes.Length;
|
||||
var tags = new TagType[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
tags[i] = EnumUtility<TagType>.Get(tagTypes[i]);
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人配置表
|
||||
/// </summary>
|
||||
public class DREnemy : DataRowBase
|
||||
{
|
||||
private int m_Id = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 获取敌人编号
|
||||
/// </summary>
|
||||
public override int Id => m_Id;
|
||||
|
||||
/// <summary>
|
||||
/// 获取敌人实体编号
|
||||
/// </summary>
|
||||
public int EntityId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取敌人基础血量
|
||||
/// </summary>
|
||||
public int BaseHp { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取敌人基础基地伤害
|
||||
/// </summary>
|
||||
public int BaseDamage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取敌人移动速度
|
||||
/// </summary>
|
||||
public float Speed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取敌人掉落硬币
|
||||
/// </summary>
|
||||
public int DropCoin { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取敌人掉落金币
|
||||
/// </summary>
|
||||
public int DropGold { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取敌人掉落金币概率
|
||||
/// </summary>
|
||||
public float DropPercent { get; private set; }
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
EntityId = int.Parse(columnStrings[index++]);
|
||||
BaseHp = int.Parse(columnStrings[index++]);
|
||||
BaseDamage = int.Parse(columnStrings[index++]);
|
||||
Speed = float.Parse(columnStrings[index++]);
|
||||
DropCoin = int.Parse(columnStrings[index++]);
|
||||
DropGold = int.Parse(columnStrings[index++]);
|
||||
DropPercent = float.Parse(columnStrings[index++]);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
//------------------------------------------------------------
|
||||
// Game Framework
|
||||
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
|
||||
// Homepage: https://gameframework.cn/
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
// 此文件由工具自动生成,请勿直接修改。
|
||||
// 生成时间:2021-06-16 21:54:35.576
|
||||
//------------------------------------------------------------
|
||||
|
||||
using GameFramework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 实体表。
|
||||
/// </summary>
|
||||
public class DREntity : DataRowBase
|
||||
{
|
||||
private int m_Id = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 获取实体编号。
|
||||
/// </summary>
|
||||
public override int Id => m_Id;
|
||||
|
||||
/// <summary>
|
||||
/// 获取资源名称。
|
||||
/// </summary>
|
||||
public string AssetName
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
AssetName = columnStrings[index++];
|
||||
|
||||
GeneratePropertyArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool ParseDataRow(byte[] dataRowBytes, int startIndex, int length, object userData)
|
||||
{
|
||||
using (MemoryStream memoryStream = new MemoryStream(dataRowBytes, startIndex, length, false))
|
||||
{
|
||||
using (BinaryReader binaryReader = new BinaryReader(memoryStream, Encoding.UTF8))
|
||||
{
|
||||
m_Id = binaryReader.Read7BitEncodedInt32();
|
||||
AssetName = binaryReader.ReadString();
|
||||
}
|
||||
}
|
||||
|
||||
GeneratePropertyArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void GeneratePropertyArray()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
using UnityGameFramework.Runtime;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 事件配置表
|
||||
/// </summary>
|
||||
public class DREvent : DataRowBase
|
||||
{
|
||||
private int m_Id = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 获取事件编号
|
||||
/// </summary>
|
||||
public override int Id => m_Id;
|
||||
|
||||
/// <summary>
|
||||
/// 获取事件题目
|
||||
/// </summary>
|
||||
public string Title { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取事件描述
|
||||
/// </summary>
|
||||
public string Description { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取事件选项
|
||||
/// </summary>
|
||||
/// <remarks>原始字符串(如 JSON 文本),不在此处做解析。</remarks>
|
||||
public string OptionsRaw { get; private set; }
|
||||
|
||||
public string Option1Raw { get; private set; }
|
||||
|
||||
public string Option2Raw { get; private set; }
|
||||
|
||||
public string Option3Raw { get; private set; }
|
||||
|
||||
public string Option4Raw { get; private set; }
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
Title = columnStrings[index++];
|
||||
Description = columnStrings[index++];
|
||||
Option1Raw = columnStrings[index++];
|
||||
Option2Raw = columnStrings[index++];
|
||||
Option3Raw = columnStrings[index++];
|
||||
Option4Raw = columnStrings[index++];
|
||||
OptionsRaw = BuildOptionsRaw(Option1Raw, Option2Raw, Option3Raw, Option4Raw);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string BuildOptionsRaw(params string[] optionColumns)
|
||||
{
|
||||
JArray array = new JArray();
|
||||
for (int i = 0; i < optionColumns.Length; i++)
|
||||
{
|
||||
string optionRaw = optionColumns[i];
|
||||
if (string.IsNullOrWhiteSpace(optionRaw))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
array.Add(JObject.Parse(optionRaw));
|
||||
}
|
||||
|
||||
return array.ToString(Newtonsoft.Json.Formatting.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
using System;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.CustomUtility;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 关卡表
|
||||
/// </summary>
|
||||
public class DRLevel : DataRowBase
|
||||
{
|
||||
private int m_Id = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 获取关卡编号
|
||||
/// </summary>
|
||||
public override int Id => m_Id;
|
||||
|
||||
/// <summary>
|
||||
/// 获取关卡所属主题类型
|
||||
/// </summary>
|
||||
public LevelThemeType LevelThemeType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取关卡初始基地生命
|
||||
/// </summary>
|
||||
public int BaseHp { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取关卡初始硬币数量
|
||||
/// </summary>
|
||||
public int StartCoin { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取关卡胜利条件
|
||||
/// </summary>
|
||||
public LevelVictoryType LevelVictoryType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取关卡胜利奖励金币
|
||||
/// </summary>
|
||||
public int RewardGold { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取关卡胜利条件参数
|
||||
/// </summary>
|
||||
public string VictoryParams { get; private set; }
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
LevelThemeType = EnumUtility<LevelThemeType>.Get(columnStrings[index++]);
|
||||
BaseHp = int.Parse(columnStrings[index++]);
|
||||
StartCoin = int.Parse(columnStrings[index++]);
|
||||
LevelVictoryType = EnumUtility<LevelVictoryType>.Get(columnStrings[index++]);
|
||||
VictoryParams = columnStrings[index++];
|
||||
RewardGold = int.Parse(columnStrings[index++]);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
using System;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 关卡阶段表
|
||||
/// </summary>
|
||||
public class DRLevelPhase : DataRowBase
|
||||
{
|
||||
private int m_Id = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 获取关卡阶段编号
|
||||
/// </summary>
|
||||
public override int Id => m_Id;
|
||||
|
||||
/// <summary>
|
||||
/// 获取阶段持续时间(秒)
|
||||
/// </summary>
|
||||
public int DurationSeconds { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取阶段结束条件类型
|
||||
/// </summary>
|
||||
public PhaseEndType EndType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取阶段结束参数
|
||||
/// </summary>
|
||||
public string EndParam { get; private set; }
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
DurationSeconds = int.Parse(columnStrings[index++]);
|
||||
EndType = ParsePhaseEndType(columnStrings[index++]);
|
||||
EndParam = columnStrings[index++];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private PhaseEndType ParsePhaseEndType(string raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
return PhaseEndType.None;
|
||||
}
|
||||
|
||||
return EnumUtility<PhaseEndType>.Get(raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
using System;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 关卡阶段出怪条目表
|
||||
/// </summary>
|
||||
public class DRLevelSpawnEntry : DataRowBase
|
||||
{
|
||||
private int m_Id = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 获取阶段条目编号
|
||||
/// </summary>
|
||||
public override int Id => m_Id;
|
||||
|
||||
/// <summary>
|
||||
/// 获取敌人出生口编号
|
||||
/// </summary>
|
||||
public int SpawnPointId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取相对开始时间(秒)
|
||||
/// </summary>
|
||||
public int StartTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取出怪条目类型
|
||||
/// </summary>
|
||||
public EntryType EntryType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取敌人编号
|
||||
/// </summary>
|
||||
public int EnemyId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取单次出怪数量
|
||||
/// </summary>
|
||||
public int Count { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取出怪间隔(秒)
|
||||
/// </summary>
|
||||
public float Interval { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取持续时间(秒)
|
||||
/// </summary>
|
||||
public int Duration { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取单怪出生间隔(秒)
|
||||
/// </summary>
|
||||
public float Gap { get; private set; }
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
SpawnPointId = int.Parse(columnStrings[index++]);
|
||||
StartTime = int.Parse(columnStrings[index++]);
|
||||
EntryType = ParseEntryType(columnStrings[index++]);
|
||||
EnemyId = int.Parse(columnStrings[index++]);
|
||||
Count = int.Parse(columnStrings[index++]);
|
||||
Interval = float.Parse(columnStrings[index++]);
|
||||
Duration = int.Parse(columnStrings[index++]);
|
||||
Gap = float.Parse(columnStrings[index++]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private EntryType ParseEntryType(string raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
return EntryType.None;
|
||||
}
|
||||
|
||||
return EnumUtility<EntryType>.Get(raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
//------------------------------------------------------------
|
||||
// Game Framework
|
||||
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
|
||||
// Homepage: https://gameframework.cn/
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
// 此文件由工具自动生成,请勿直接修改。
|
||||
// 生成时间:2021-06-16 21:54:35.591
|
||||
//------------------------------------------------------------
|
||||
|
||||
using GameFramework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 音乐配置表。
|
||||
/// </summary>
|
||||
public class DRMusic : DataRowBase
|
||||
{
|
||||
private int m_Id = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 获取音乐编号。
|
||||
/// </summary>
|
||||
public override int Id
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_Id;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取资源名称。
|
||||
/// </summary>
|
||||
public string AssetName
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
AssetName = columnStrings[index++];
|
||||
|
||||
GeneratePropertyArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool ParseDataRow(byte[] dataRowBytes, int startIndex, int length, object userData)
|
||||
{
|
||||
using (MemoryStream memoryStream = new MemoryStream(dataRowBytes, startIndex, length, false))
|
||||
{
|
||||
using (BinaryReader binaryReader = new BinaryReader(memoryStream, Encoding.UTF8))
|
||||
{
|
||||
m_Id = binaryReader.Read7BitEncodedInt32();
|
||||
AssetName = binaryReader.ReadString();
|
||||
}
|
||||
}
|
||||
|
||||
GeneratePropertyArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void GeneratePropertyArray()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
using System;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.CustomUtility;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 枪口组件表
|
||||
/// </summary>
|
||||
public class DRMuzzleComp : DataRowBase
|
||||
{
|
||||
private int m_Id = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 获取枪口组件编号
|
||||
/// </summary>
|
||||
public override int Id => m_Id;
|
||||
|
||||
/// <summary>
|
||||
/// 获取枪口组件名
|
||||
/// </summary>
|
||||
public string Name { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取枪口组件攻击伤害数组
|
||||
/// </summary>
|
||||
public int[] AttackDamage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取枪口组件每级提升攻击伤害值
|
||||
/// </summary>
|
||||
public int AttackDamagePerLevel { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取攻击伤害浮动
|
||||
/// </summary>
|
||||
public float DamageRandomRate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取枪口攻击方式
|
||||
/// </summary>
|
||||
public AttackMethodType AttackMethodType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取属性约束
|
||||
/// </summary>
|
||||
public string Constraint { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取可能出现的 Tag
|
||||
/// </summary>
|
||||
public TagType[] PossibleTag { get; private set; }
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
Name = columnStrings[index++];
|
||||
AttackDamage = GenerateAttackDamage(columnStrings[index++]);
|
||||
AttackDamagePerLevel = int.Parse(columnStrings[index++]);
|
||||
DamageRandomRate = float.Parse(columnStrings[index++]);
|
||||
AttackMethodType = EnumUtility<AttackMethodType>.Get(columnStrings[index++]);
|
||||
Constraint = columnStrings[index++];
|
||||
PossibleTag = GeneratePossibleTag(columnStrings[index++]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private int[] GenerateAttackDamage(string raw)
|
||||
{
|
||||
if (!raw.StartsWith('[') || !raw.EndsWith(']'))
|
||||
{
|
||||
throw new ArgumentException("Input must be enclosed in square brackets.");
|
||||
}
|
||||
|
||||
if (raw.Length == 2) return new int[] { };
|
||||
string[] raws = raw.Substring(1, raw.Length - 2).Split(",");
|
||||
int length = raws.Length;
|
||||
var damages = new int[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
damages[i] = int.Parse(raws[i]);
|
||||
}
|
||||
|
||||
return damages;
|
||||
}
|
||||
|
||||
private TagType[] GeneratePossibleTag(string raw)
|
||||
{
|
||||
if (!raw.StartsWith('[') || !raw.EndsWith(']'))
|
||||
{
|
||||
throw new ArgumentException("Input must be enclosed in square brackets.");
|
||||
}
|
||||
|
||||
if (raw.Length == 2) return new TagType[] { };
|
||||
string[] tagTypes = raw.Substring(1, raw.Length - 2).Split(",");
|
||||
int length = tagTypes.Length;
|
||||
var tags = new TagType[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
tags[i] = EnumUtility<TagType>.Get(tagTypes[i]);
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
using System;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 局外道具掉落池配置表。
|
||||
/// </summary>
|
||||
public class DROutGameDropPool : DataRowBase
|
||||
{
|
||||
private int m_Id = 0;
|
||||
|
||||
public override int Id => m_Id;
|
||||
|
||||
public LevelThemeType LevelThemeType { get; private set; }
|
||||
public string ItemType { get; private set; }
|
||||
public int ItemId { get; private set; }
|
||||
public int[] Weights { get; private set; }
|
||||
public int[] MinPhase { get; private set; }
|
||||
public int[] MaxPhase { get; private set; }
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
LevelThemeType = EnumUtility<LevelThemeType>.Get(columnStrings[index++]);
|
||||
ItemType = columnStrings[index++];
|
||||
ItemId = ParseIntOrDefault(columnStrings[index++], 0);
|
||||
Weights = ParseWeights(columnStrings[index++]);
|
||||
MinPhase = ParsePhases(columnStrings[index++], 1);
|
||||
MaxPhase = ParsePhases(columnStrings[index++], int.MaxValue);
|
||||
NormalizePhaseRanges();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public int GetWeight(RarityType rarity)
|
||||
{
|
||||
int index = Mathf.Clamp((int)rarity - 1, 0, RarityCount - 1);
|
||||
return Weights[index];
|
||||
}
|
||||
|
||||
public int GetMinPhase(RarityType rarity)
|
||||
{
|
||||
int index = Mathf.Clamp((int)rarity - 1, 0, RarityCount - 1);
|
||||
return MinPhase[index];
|
||||
}
|
||||
|
||||
public int GetMaxPhase(RarityType rarity)
|
||||
{
|
||||
int index = Mathf.Clamp((int)rarity - 1, 0, RarityCount - 1);
|
||||
return MaxPhase[index];
|
||||
}
|
||||
|
||||
private static int[] ParseWeights(string raw)
|
||||
{
|
||||
if (!raw.StartsWith('[') || !raw.EndsWith(']'))
|
||||
{
|
||||
throw new ArgumentException("Weights must be enclosed in square brackets.");
|
||||
}
|
||||
|
||||
if (raw.Length == 2)
|
||||
{
|
||||
return new int[RarityCount];
|
||||
}
|
||||
|
||||
string[] raws = raw.Substring(1, raw.Length - 2).Split(",");
|
||||
if (raws.Length != RarityCount)
|
||||
{
|
||||
throw new ArgumentException($"Weights must contain exactly {RarityCount} values.");
|
||||
}
|
||||
|
||||
int[] weights = new int[RarityCount];
|
||||
for (int i = 0; i < raws.Length; i++)
|
||||
{
|
||||
weights[i] = Mathf.Max(0, int.Parse(raws[i]));
|
||||
}
|
||||
|
||||
return weights;
|
||||
}
|
||||
|
||||
private static int[] ParsePhases(string raw, int fallbackValue)
|
||||
{
|
||||
if (!raw.StartsWith('[') || !raw.EndsWith(']'))
|
||||
{
|
||||
throw new ArgumentException("Phase ranges must be enclosed in square brackets.");
|
||||
}
|
||||
|
||||
if (raw.Length == 2)
|
||||
{
|
||||
int[] fallbackValues = new int[RarityCount];
|
||||
for (int i = 0; i < fallbackValues.Length; i++)
|
||||
{
|
||||
fallbackValues[i] = fallbackValue;
|
||||
}
|
||||
|
||||
return fallbackValues;
|
||||
}
|
||||
|
||||
string[] raws = raw.Substring(1, raw.Length - 2).Split(",");
|
||||
if (raws.Length != RarityCount)
|
||||
{
|
||||
throw new ArgumentException($"Phase ranges must contain exactly {RarityCount} values.");
|
||||
}
|
||||
|
||||
int[] phases = new int[RarityCount];
|
||||
for (int i = 0; i < raws.Length; i++)
|
||||
{
|
||||
phases[i] = int.Parse(raws[i]);
|
||||
}
|
||||
|
||||
return phases;
|
||||
}
|
||||
|
||||
private void NormalizePhaseRanges()
|
||||
{
|
||||
for (int i = 0; i < RarityCount; i++)
|
||||
{
|
||||
if (MinPhase[i] <= 0)
|
||||
{
|
||||
MinPhase[i] = 1;
|
||||
}
|
||||
|
||||
if (MaxPhase[i] < MinPhase[i])
|
||||
{
|
||||
MaxPhase[i] = MinPhase[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int ParseIntOrDefault(string raw, int fallbackValue)
|
||||
{
|
||||
if (int.TryParse(raw, out int parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
private const int RarityCount = (int)RarityType.Red;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
public sealed class DRRarityTagBudget : DataRowBase
|
||||
{
|
||||
private int m_Id;
|
||||
|
||||
public override int Id => m_Id;
|
||||
|
||||
public RarityType Rarity { get; private set; }
|
||||
|
||||
public int MinCount { get; private set; }
|
||||
|
||||
public int MaxCount { get; private set; }
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
Rarity = EnumUtility<RarityType>.Get(columnStrings[index++]);
|
||||
MinCount = int.Parse(columnStrings[index++]);
|
||||
MaxCount = int.Parse(columnStrings[index++]);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
using GameFramework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 场景配置表。
|
||||
/// </summary>
|
||||
public class DRScene : DataRowBase
|
||||
{
|
||||
private int m_Id = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 获取场景编号。
|
||||
/// </summary>
|
||||
public override int Id
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_Id;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取资源名称。
|
||||
/// </summary>
|
||||
public string AssetName
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取背景音乐编号。
|
||||
/// </summary>
|
||||
public int BackgroundMusicId
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
AssetName = columnStrings[index++];
|
||||
BackgroundMusicId = int.Parse(columnStrings[index++]);
|
||||
|
||||
GeneratePropertyArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool ParseDataRow(byte[] dataRowBytes, int startIndex, int length, object userData)
|
||||
{
|
||||
using (MemoryStream memoryStream = new MemoryStream(dataRowBytes, startIndex, length, false))
|
||||
{
|
||||
using (BinaryReader binaryReader = new BinaryReader(memoryStream, Encoding.UTF8))
|
||||
{
|
||||
m_Id = binaryReader.Read7BitEncodedInt32();
|
||||
AssetName = binaryReader.ReadString();
|
||||
BackgroundMusicId = binaryReader.Read7BitEncodedInt32();
|
||||
}
|
||||
}
|
||||
|
||||
GeneratePropertyArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void GeneratePropertyArray()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
public class DRShopPrice : DataRowBase
|
||||
{
|
||||
private int m_Id = 0;
|
||||
|
||||
public override int Id => m_Id;
|
||||
|
||||
public RarityType Rarity { get; set; }
|
||||
|
||||
public int MinPrice { get; set; }
|
||||
|
||||
public int MaxPrice { get; set; }
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
Rarity = EnumUtility<RarityType>.Get(columnStrings[index++]);
|
||||
MinPrice = int.Parse(columnStrings[index++]);
|
||||
MaxPrice = int.Parse(columnStrings[index++]);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 声音配置表。
|
||||
/// </summary>
|
||||
public class DRSound : DataRowBase
|
||||
{
|
||||
private int m_Id = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 获取声音编号。
|
||||
/// </summary>
|
||||
public override int Id
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_Id;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取资源名称。
|
||||
/// </summary>
|
||||
public string AssetName
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取优先级(默认0,128最高,-128最低)。
|
||||
/// </summary>
|
||||
public int Priority
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否循环。
|
||||
/// </summary>
|
||||
public bool Loop
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取音量(0~1)。
|
||||
/// </summary>
|
||||
public float Volume
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取声音空间混合量(0为2D,1为3D,中间值混合效果)。
|
||||
/// </summary>
|
||||
public float SpatialBlend
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取声音最大距离。
|
||||
/// </summary>
|
||||
public float MaxDistance
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
AssetName = columnStrings[index++];
|
||||
Priority = int.Parse(columnStrings[index++]);
|
||||
Loop = bool.Parse(columnStrings[index++]);
|
||||
Volume = float.Parse(columnStrings[index++]);
|
||||
SpatialBlend = float.Parse(columnStrings[index++]);
|
||||
MaxDistance = float.Parse(columnStrings[index++]);
|
||||
|
||||
GeneratePropertyArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool ParseDataRow(byte[] dataRowBytes, int startIndex, int length, object userData)
|
||||
{
|
||||
using (MemoryStream memoryStream = new MemoryStream(dataRowBytes, startIndex, length, false))
|
||||
{
|
||||
using (BinaryReader binaryReader = new BinaryReader(memoryStream, Encoding.UTF8))
|
||||
{
|
||||
m_Id = binaryReader.Read7BitEncodedInt32();
|
||||
AssetName = binaryReader.ReadString();
|
||||
Priority = binaryReader.Read7BitEncodedInt32();
|
||||
Loop = binaryReader.ReadBoolean();
|
||||
Volume = binaryReader.ReadSingle();
|
||||
SpatialBlend = binaryReader.ReadSingle();
|
||||
MaxDistance = binaryReader.ReadSingle();
|
||||
}
|
||||
}
|
||||
|
||||
GeneratePropertyArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void GeneratePropertyArray()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
using System;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.CustomUtility;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
/// <summary>
|
||||
/// Tag 表
|
||||
/// </summary>
|
||||
public class DRTag : DataRowBase
|
||||
{
|
||||
private int m_Id = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Tag 编号
|
||||
/// </summary>
|
||||
public override int Id => m_Id;
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Tag 名
|
||||
/// </summary>
|
||||
public string Name { get; private set; }
|
||||
|
||||
public TagType TagType => (TagType)m_Id;
|
||||
|
||||
public string TagGroupText { get; private set; }
|
||||
|
||||
public RarityType MinRarity { get; private set; }
|
||||
|
||||
public int Weight { get; private set; }
|
||||
|
||||
public bool IsImplemented { get; private set; }
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
Name = columnStrings[index++];
|
||||
|
||||
bool hasExtendedColumns = columnStrings.Length - index >= 4;
|
||||
if (hasExtendedColumns)
|
||||
{
|
||||
TagGroupText = columnStrings[index++];
|
||||
MinRarity = EnumUtility<RarityType>.Get(columnStrings[index++]);
|
||||
Weight = int.Parse(columnStrings[index++]);
|
||||
IsImplemented = bool.Parse(columnStrings[index++]);
|
||||
}
|
||||
else
|
||||
{
|
||||
TagGroupText = string.Empty;
|
||||
MinRarity = EnumUtility<RarityType>.Get(columnStrings[index++]);
|
||||
Weight = int.Parse(columnStrings[index++]);
|
||||
IsImplemented = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
public sealed class DRTagConfig : DataRowBase
|
||||
{
|
||||
private int m_Id;
|
||||
|
||||
public override int Id => m_Id;
|
||||
|
||||
public TagType TagType { get; private set; }
|
||||
|
||||
public TagTriggerPhase TriggerPhase { get; private set; }
|
||||
|
||||
public string Description { get; private set; }
|
||||
|
||||
public string ParamJson { get; private set; }
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
TagType = EnumUtility<TagType>.Get(columnStrings[index++]);
|
||||
TriggerPhase = EnumUtility<TagTriggerPhase>.Get(columnStrings[index++]);
|
||||
Description = columnStrings[index++];
|
||||
ParamJson = columnStrings[index++];
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 界面配置表。
|
||||
/// </summary>
|
||||
public class DRUIForm : DataRowBase
|
||||
{
|
||||
private int m_Id = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 获取界面编号。
|
||||
/// </summary>
|
||||
public override int Id => m_Id;
|
||||
|
||||
/// <summary>
|
||||
/// 获取资源名称。
|
||||
/// </summary>
|
||||
public string AssetName { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取界面组名称。
|
||||
/// </summary>
|
||||
public string UIGroupName { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否允许多个界面实例。
|
||||
/// </summary>
|
||||
public bool AllowMultiInstance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否暂停被其覆盖的界面。
|
||||
/// </summary>
|
||||
public bool PauseCoveredUIForm { get; private set; }
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
AssetName = columnStrings[index++];
|
||||
UIGroupName = columnStrings[index++];
|
||||
AllowMultiInstance = bool.Parse(columnStrings[index++]);
|
||||
PauseCoveredUIForm = bool.Parse(columnStrings[index++]);
|
||||
|
||||
GeneratePropertyArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool ParseDataRow(byte[] dataRowBytes, int startIndex, int length, object userData)
|
||||
{
|
||||
using (MemoryStream memoryStream = new MemoryStream(dataRowBytes, startIndex, length, false))
|
||||
{
|
||||
using (BinaryReader binaryReader = new BinaryReader(memoryStream, Encoding.UTF8))
|
||||
{
|
||||
m_Id = binaryReader.Read7BitEncodedInt32();
|
||||
AssetName = binaryReader.ReadString();
|
||||
UIGroupName = binaryReader.ReadString();
|
||||
AllowMultiInstance = binaryReader.ReadBoolean();
|
||||
PauseCoveredUIForm = binaryReader.ReadBoolean();
|
||||
}
|
||||
}
|
||||
|
||||
GeneratePropertyArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void GeneratePropertyArray()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
//------------------------------------------------------------
|
||||
// Game Framework
|
||||
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
|
||||
// Homepage: https://gameframework.cn/
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
// 此文件由工具自动生成,请勿直接修改。
|
||||
// 生成时间:2021-06-16 21:54:35.666
|
||||
//------------------------------------------------------------
|
||||
|
||||
using GameFramework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 声音配置表。
|
||||
/// </summary>
|
||||
public class DRUISound : DataRowBase
|
||||
{
|
||||
private int m_Id = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 获取声音编号。
|
||||
/// </summary>
|
||||
public override int Id
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_Id;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取资源名称。
|
||||
/// </summary>
|
||||
public string AssetName
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取优先级(默认0,128最高,-128最低)。
|
||||
/// </summary>
|
||||
public int Priority
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取音量(0~1)。
|
||||
/// </summary>
|
||||
public float Volume
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
for (int i = 0; i < columnStrings.Length; i++)
|
||||
{
|
||||
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
index++;
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
AssetName = columnStrings[index++];
|
||||
Priority = int.Parse(columnStrings[index++]);
|
||||
Volume = float.Parse(columnStrings[index++]);
|
||||
|
||||
GeneratePropertyArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool ParseDataRow(byte[] dataRowBytes, int startIndex, int length, object userData)
|
||||
{
|
||||
using (MemoryStream memoryStream = new MemoryStream(dataRowBytes, startIndex, length, false))
|
||||
{
|
||||
using (BinaryReader binaryReader = new BinaryReader(memoryStream, Encoding.UTF8))
|
||||
{
|
||||
m_Id = binaryReader.Read7BitEncodedInt32();
|
||||
AssetName = binaryReader.ReadString();
|
||||
Priority = binaryReader.Read7BitEncodedInt32();
|
||||
Volume = binaryReader.ReadSingle();
|
||||
}
|
||||
}
|
||||
|
||||
GeneratePropertyArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void GeneratePropertyArray()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
//------------------------------------------------------------
|
||||
// Game Framework
|
||||
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
|
||||
// Homepage: https://gameframework.cn/
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
|
||||
using GameFramework.DataTable;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
public static class DataTableExtension
|
||||
{
|
||||
private const string DataRowClassPrefixName = "GeometryTD.DataTable.DR";
|
||||
internal static readonly char[] DataSplitSeparators = new char[] { '\t' };
|
||||
internal static readonly char[] DataTrimSeparators = new char[] { '\"' };
|
||||
|
||||
public static void LoadDataTable(this DataTableComponent dataTableComponent, string dataTableName, string dataTableAssetName, object userData)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dataTableName))
|
||||
{
|
||||
Log.Warning("Data table name is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
string[] splitedNames = dataTableName.Split('_');
|
||||
if (splitedNames.Length > 2)
|
||||
{
|
||||
Log.Warning("Data table name is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
string dataRowClassName = DataRowClassPrefixName + splitedNames[0];
|
||||
Type dataRowType = Type.GetType(dataRowClassName);
|
||||
if (dataRowType == null)
|
||||
{
|
||||
Log.Warning("Can not get data row type with class name '{0}'.", dataRowClassName);
|
||||
return;
|
||||
}
|
||||
|
||||
string name = splitedNames.Length > 1 ? splitedNames[1] : null;
|
||||
DataTableBase dataTable = dataTableComponent.CreateDataTable(dataRowType, name);
|
||||
dataTable.ReadData(dataTableAssetName, Constant.AssetPriority.DataTableAsset, userData);
|
||||
}
|
||||
|
||||
public static Color32 ParseColor32(string value)
|
||||
{
|
||||
string[] splitedValue = value.Split(',');
|
||||
return new Color32(byte.Parse(splitedValue[0]), byte.Parse(splitedValue[1]), byte.Parse(splitedValue[2]), byte.Parse(splitedValue[3]));
|
||||
}
|
||||
|
||||
public static Color ParseColor(string value)
|
||||
{
|
||||
string[] splitedValue = value.Split(',');
|
||||
return new Color(float.Parse(splitedValue[0]), float.Parse(splitedValue[1]), float.Parse(splitedValue[2]), float.Parse(splitedValue[3]));
|
||||
}
|
||||
|
||||
public static Quaternion ParseQuaternion(string value)
|
||||
{
|
||||
string[] splitedValue = value.Split(',');
|
||||
return new Quaternion(float.Parse(splitedValue[0]), float.Parse(splitedValue[1]), float.Parse(splitedValue[2]), float.Parse(splitedValue[3]));
|
||||
}
|
||||
|
||||
public static Rect ParseRect(string value)
|
||||
{
|
||||
string[] splitedValue = value.Split(',');
|
||||
return new Rect(float.Parse(splitedValue[0]), float.Parse(splitedValue[1]), float.Parse(splitedValue[2]), float.Parse(splitedValue[3]));
|
||||
}
|
||||
|
||||
public static Vector2 ParseVector2(string value)
|
||||
{
|
||||
string[] splitedValue = value.Split(',');
|
||||
return new Vector2(float.Parse(splitedValue[0]), float.Parse(splitedValue[1]));
|
||||
}
|
||||
|
||||
public static Vector3 ParseVector3(string value)
|
||||
{
|
||||
string[] splitedValue = value.Split(',');
|
||||
return new Vector3(float.Parse(splitedValue[0]), float.Parse(splitedValue[1]), float.Parse(splitedValue[2]));
|
||||
}
|
||||
|
||||
public static Vector4 ParseVector4(string value)
|
||||
{
|
||||
string[] splitedValue = value.Split(',');
|
||||
return new Vector4(float.Parse(splitedValue[0]), float.Parse(splitedValue[1]), float.Parse(splitedValue[2]), float.Parse(splitedValue[3]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace GeometryTD.Definition
|
||||
{
|
||||
public enum CombatParticipantTowerValidationFailureReason
|
||||
{
|
||||
None = 0,
|
||||
TowerMissing = 1,
|
||||
MissingMuzzleComponent = 2,
|
||||
MissingBearingComponent = 3,
|
||||
MissingBaseComponent = 4,
|
||||
BrokenMuzzleComponent = 5,
|
||||
BrokenBearingComponent = 6,
|
||||
BrokenBaseComponent = 7
|
||||
}
|
||||
|
||||
public sealed class CombatParticipantTowerValidationResult
|
||||
{
|
||||
public long TowerInstanceId { get; set; }
|
||||
|
||||
public bool IsValid => FailureReason == CombatParticipantTowerValidationFailureReason.None;
|
||||
|
||||
public CombatParticipantTowerValidationFailureReason FailureReason { get; set; }
|
||||
|
||||
public TowerItemData Tower { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CombatParticipantTowerValidationSummary
|
||||
{
|
||||
public IReadOnlyList<TowerItemData> ValidTowers { get; set; } = System.Array.Empty<TowerItemData>();
|
||||
|
||||
public IReadOnlyList<CombatParticipantTowerValidationResult> InvalidResults { get; set; } =
|
||||
System.Array.Empty<CombatParticipantTowerValidationResult>();
|
||||
|
||||
public bool HasAnyValidParticipantTower => ValidTowers != null && ValidTowers.Count > 0;
|
||||
}
|
||||
|
||||
public static class CombatParticipantTowerValidationService
|
||||
{
|
||||
public static CombatParticipantTowerValidationResult ValidateTower(
|
||||
BackpackInventoryData inventory,
|
||||
long towerInstanceId)
|
||||
{
|
||||
if (!TryGetTowerById(inventory, towerInstanceId, out TowerItemData tower))
|
||||
{
|
||||
return new CombatParticipantTowerValidationResult
|
||||
{
|
||||
TowerInstanceId = towerInstanceId,
|
||||
FailureReason = CombatParticipantTowerValidationFailureReason.TowerMissing
|
||||
};
|
||||
}
|
||||
|
||||
return ValidateTower(inventory, tower);
|
||||
}
|
||||
|
||||
public static CombatParticipantTowerValidationResult ValidateTower(
|
||||
BackpackInventoryData inventory,
|
||||
TowerItemData tower)
|
||||
{
|
||||
if (tower == null || tower.InstanceId <= 0)
|
||||
{
|
||||
return new CombatParticipantTowerValidationResult
|
||||
{
|
||||
TowerInstanceId = tower != null ? tower.InstanceId : 0,
|
||||
Tower = tower,
|
||||
FailureReason = CombatParticipantTowerValidationFailureReason.TowerMissing
|
||||
};
|
||||
}
|
||||
|
||||
if (!TryGetComponentById(inventory?.MuzzleComponents, tower.MuzzleComponentInstanceId, out MuzzleCompItemData muzzleComponent))
|
||||
{
|
||||
return new CombatParticipantTowerValidationResult
|
||||
{
|
||||
TowerInstanceId = tower.InstanceId,
|
||||
Tower = tower,
|
||||
FailureReason = CombatParticipantTowerValidationFailureReason.MissingMuzzleComponent
|
||||
};
|
||||
}
|
||||
|
||||
if (muzzleComponent.Endurance <= 0f)
|
||||
{
|
||||
return new CombatParticipantTowerValidationResult
|
||||
{
|
||||
TowerInstanceId = tower.InstanceId,
|
||||
Tower = tower,
|
||||
FailureReason = CombatParticipantTowerValidationFailureReason.BrokenMuzzleComponent
|
||||
};
|
||||
}
|
||||
|
||||
if (!TryGetComponentById(inventory?.BearingComponents, tower.BearingComponentInstanceId, out BearingCompItemData bearingComponent))
|
||||
{
|
||||
return new CombatParticipantTowerValidationResult
|
||||
{
|
||||
TowerInstanceId = tower.InstanceId,
|
||||
Tower = tower,
|
||||
FailureReason = CombatParticipantTowerValidationFailureReason.MissingBearingComponent
|
||||
};
|
||||
}
|
||||
|
||||
if (bearingComponent.Endurance <= 0f)
|
||||
{
|
||||
return new CombatParticipantTowerValidationResult
|
||||
{
|
||||
TowerInstanceId = tower.InstanceId,
|
||||
Tower = tower,
|
||||
FailureReason = CombatParticipantTowerValidationFailureReason.BrokenBearingComponent
|
||||
};
|
||||
}
|
||||
|
||||
if (!TryGetComponentById(inventory?.BaseComponents, tower.BaseComponentInstanceId, out BaseCompItemData baseComponent))
|
||||
{
|
||||
return new CombatParticipantTowerValidationResult
|
||||
{
|
||||
TowerInstanceId = tower.InstanceId,
|
||||
Tower = tower,
|
||||
FailureReason = CombatParticipantTowerValidationFailureReason.MissingBaseComponent
|
||||
};
|
||||
}
|
||||
|
||||
if (baseComponent.Endurance <= 0f)
|
||||
{
|
||||
return new CombatParticipantTowerValidationResult
|
||||
{
|
||||
TowerInstanceId = tower.InstanceId,
|
||||
Tower = tower,
|
||||
FailureReason = CombatParticipantTowerValidationFailureReason.BrokenBaseComponent
|
||||
};
|
||||
}
|
||||
|
||||
return new CombatParticipantTowerValidationResult
|
||||
{
|
||||
TowerInstanceId = tower.InstanceId,
|
||||
Tower = tower,
|
||||
FailureReason = CombatParticipantTowerValidationFailureReason.None
|
||||
};
|
||||
}
|
||||
|
||||
public static CombatParticipantTowerValidationSummary ValidateParticipantTowers(BackpackInventoryData inventory)
|
||||
{
|
||||
List<TowerItemData> validTowers = new List<TowerItemData>();
|
||||
List<CombatParticipantTowerValidationResult> invalidResults =
|
||||
new List<CombatParticipantTowerValidationResult>();
|
||||
HashSet<long> processedTowerIds = new HashSet<long>();
|
||||
|
||||
if (inventory?.ParticipantTowerInstanceIds == null || inventory.ParticipantTowerInstanceIds.Count <= 0)
|
||||
{
|
||||
return new CombatParticipantTowerValidationSummary
|
||||
{
|
||||
ValidTowers = validTowers,
|
||||
InvalidResults = invalidResults
|
||||
};
|
||||
}
|
||||
|
||||
for (int i = 0; i < inventory.ParticipantTowerInstanceIds.Count; i++)
|
||||
{
|
||||
long towerInstanceId = inventory.ParticipantTowerInstanceIds[i];
|
||||
if (towerInstanceId <= 0 || !processedTowerIds.Add(towerInstanceId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
CombatParticipantTowerValidationResult result = ValidateTower(inventory, towerInstanceId);
|
||||
if (result.IsValid)
|
||||
{
|
||||
validTowers.Add(result.Tower);
|
||||
}
|
||||
else
|
||||
{
|
||||
invalidResults.Add(result);
|
||||
}
|
||||
}
|
||||
|
||||
return new CombatParticipantTowerValidationSummary
|
||||
{
|
||||
ValidTowers = validTowers,
|
||||
InvalidResults = invalidResults
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryGetTowerById(BackpackInventoryData inventory, long towerInstanceId, out TowerItemData tower)
|
||||
{
|
||||
tower = null;
|
||||
if (inventory?.Towers == null || towerInstanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < inventory.Towers.Count; i++)
|
||||
{
|
||||
TowerItemData candidate = inventory.Towers[i];
|
||||
if (candidate != null && candidate.InstanceId == towerInstanceId)
|
||||
{
|
||||
tower = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetComponentById<TComponent>(
|
||||
IReadOnlyList<TComponent> components,
|
||||
long componentInstanceId,
|
||||
out TComponent resolvedComponent)
|
||||
where TComponent : TowerCompItemData
|
||||
{
|
||||
resolvedComponent = null;
|
||||
if (components == null || componentInstanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < components.Count; i++)
|
||||
{
|
||||
TComponent component = components[i];
|
||||
if (component != null && component.InstanceId == componentInstanceId)
|
||||
{
|
||||
resolvedComponent = component;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
namespace GeometryTD.Definition
|
||||
{
|
||||
public static class CombatParticipantTowerValidationText
|
||||
{
|
||||
public static string GetFailureReasonMessage(CombatParticipantTowerValidationFailureReason failureReason)
|
||||
{
|
||||
switch (failureReason)
|
||||
{
|
||||
case CombatParticipantTowerValidationFailureReason.TowerMissing:
|
||||
return "已不存在,无法参战。";
|
||||
case CombatParticipantTowerValidationFailureReason.MissingMuzzleComponent:
|
||||
return "缺少枪口组件。";
|
||||
case CombatParticipantTowerValidationFailureReason.MissingBearingComponent:
|
||||
return "缺少轴承组件。";
|
||||
case CombatParticipantTowerValidationFailureReason.MissingBaseComponent:
|
||||
return "缺少底座组件。";
|
||||
case CombatParticipantTowerValidationFailureReason.BrokenMuzzleComponent:
|
||||
return "枪口组件耐久为 0,无法参战。";
|
||||
case CombatParticipantTowerValidationFailureReason.BrokenBearingComponent:
|
||||
return "轴承组件耐久为 0,无法参战。";
|
||||
case CombatParticipantTowerValidationFailureReason.BrokenBaseComponent:
|
||||
return "底座组件耐久为 0,无法参战。";
|
||||
default:
|
||||
return "不满足当前参战条件。";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue