From fb2252f6887764e3916233bd55fe45b90c90ba18 Mon Sep 17 00:00:00 2001 From: SepComet <2428390463@qq.com> Date: Thu, 30 Apr 2026 19:33:16 +0800 Subject: [PATCH] init --- .gitignore | 38 + CLAUDE.md | 67 + TODO.md | 611 + docs/CombatNodeArchitecture.md | 736 + docs/CombatReward爆率表.xlsx | Bin 0 -> 26333 bytes docs/GameDesign.md | 63 + docs/LayeredArchitectureDesign.md | 667 + docs/MVP-Scope.md | 199 + docs/MapEntityArchitecture.md | 170 + docs/TagSystemDesign.md | 238 + docs/TagSystemRoadmap.md | 62 + docs/UI-5层架构设计规范.md | 182 + src-ref/Base/GameEntry.Builtin.cs | 138 + src-ref/Base/GameEntry.Custom.cs | 45 + src-ref/Base/GameEntry.cs | 20 + src-ref/Components/BasicBaseComp.cs | 77 + src-ref/Components/BasicBearingComp.cs | 120 + src-ref/Components/IDamageReceiver.cs | 9 + src-ref/Components/InputComponent.cs | 51 + src-ref/Components/MovementComponent.cs | 45 + src-ref/Components/ShooterBullet.cs | 137 + src-ref/Components/ShooterMuzzleComp.cs | 128 + src-ref/Components/TowerController.cs | 359 + .../CustomComponent/BuiltinDataComponent.cs | 45 + .../CombatNode/CombatNodeComponent.cs | 396 + .../CombatScheduler/CombatEventBridge.cs | 151 + .../CombatScheduler/CombatLoadSession.cs | 315 + .../CombatScheduler/CombatRunResourceStore.cs | 288 + .../CombatScheduler/CombatScheduler.cs | 327 + .../CombatSchedulerCoordinator.cs | 282 + .../CombatScheduler/CombatSchedulerRuntime.cs | 36 + .../CombatSettlementContext.cs | 45 + .../CombatSettlementService.cs | 104 + .../CombatStates/CombatFailedState.cs | 34 + .../CombatStates/CombatFinishFormState.cs | 25 + .../CombatStates/CombatLoadingState.cs | 71 + .../CombatRewardSelectionState.cs | 43 + .../CombatStates/CombatRunningPhaseState.cs | 82 + .../CombatStates/CombatSettlementState.cs | 34 + .../CombatStates/CombatStateBase.cs | 34 + .../CombatWaitingForPhaseEndState.cs | 47 + .../CombatWaitingForReturnState.cs | 40 + .../EnemyDrop/EnemyDropContext.cs | 21 + .../EnemyDrop/EnemyDropResult.cs | 22 + .../CombatScheduler/ICombatSchedulerPort.cs | 19 + .../BossDeadPhaseEndCondition.cs | 14 + .../EnemiesClearedPhaseEndCondition.cs | 14 + .../PhaseEndConditions/IPhaseEndCondition.cs | 11 + .../NonePhaseEndCondition.cs | 25 + .../PhaseEndConditionContext.cs | 31 + .../TimeElapsedPhaseEndCondition.cs | 37 + .../CombatScheduler/PhaseLoopRuntime.cs | 101 + .../CombatNode/CombatSettlementCalculator.cs | 141 + .../CombatNode/CombatSettlementCommitter.cs | 83 + .../EnemyManager/EnemyConfigProvider.cs | 104 + .../EnemyManager/EnemyLifecycleTracker.cs | 91 + .../CombatNode/EnemyManager/EnemyManager.cs | 223 + .../EnemyManager/EnemySpawnDirector.cs | 203 + .../EnemyManager/EnemySpawnPathResolver.cs | 110 + src-ref/CustomComponent/EventNodeComponent.cs | 240 + .../CustomComponent/HPBar/HPBarComponent.cs | 123 + .../InventoryGeneration/DropPoolRoller.cs | 252 + .../InventoryGenerationComponent.cs | 399 + .../InventoryGenerationRandomContext.cs | 65 + .../OutGameDropRuleService.cs | 19 + .../RewardCandidateBuilder.cs | 100 + .../InventoryGeneration/ShopGoodsBuilder.cs | 170 + .../PlayerInventoryComponent.cs | 186 + .../PlayerInventoryStateStore.cs | 275 + .../PlayerInventoryTowerAssemblyService.cs | 216 + .../PlayerInventoryTowerRosterService.cs | 149 + .../PlayerInventoryTradeService.cs | 400 + .../ResolutionAdapterComponent.cs | 445 + src-ref/CustomComponent/ShopNodeComponent.cs | 78 + .../CustomComponent/SpriteCacheComponent.cs | 99 + .../TagRegistry/TagRegistryComponent.cs | 64 + src-ref/CustomComponent/UIRouterComponent.cs | 104 + src-ref/DataTable/BinaryReaderExtension.cs | 56 + src-ref/DataTable/DRBaseComp.cs | 110 + src-ref/DataTable/DRBearingComp.cs | 116 + src-ref/DataTable/DREnemy.cs | 75 + src-ref/DataTable/DREntity.cs | 80 + src-ref/DataTable/DREvent.cs | 82 + src-ref/DataTable/DRLevel.cs | 72 + src-ref/DataTable/DRLevelPhase.cs | 64 + src-ref/DataTable/DRLevelSpawnEntry.cs | 94 + src-ref/DataTable/DRMusic.cs | 86 + src-ref/DataTable/DRMuzzleComp.cs | 116 + src-ref/DataTable/DROutGameDropPool.cs | 154 + src-ref/DataTable/DRRarityTagBudget.cs | 38 + src-ref/DataTable/DRScene.cs | 87 + src-ref/DataTable/DRShopPrice.cs | 38 + src-ref/DataTable/DRSound.cs | 127 + src-ref/DataTable/DRTag.cs | 68 + src-ref/DataTable/DRTagConfig.cs | 41 + src-ref/DataTable/DRUIForm.cs | 83 + src-ref/DataTable/DRUISound.cs | 108 + src-ref/DataTable/DataTableExtension.cs | 91 + .../CombatParticipantTowerValidation.cs | 226 + .../CombatParticipantTowerValidationText.cs | 28 + .../Constant/Constant.AssetPriority.cs | 40 + src-ref/Definition/Constant/Constant.Layer.cs | 29 + .../Definition/Constant/Constant.Setting.cs | 25 + .../Definition/DataStruct/AttackPayload.cs | 30 + .../DataStruct/BackpackInventoryData.cs | 42 + src-ref/Definition/DataStruct/BuildInfo.cs | 54 + src-ref/Definition/DataStruct/EventItem.cs | 18 + src-ref/Definition/DataStruct/EventOption.cs | 25 + src-ref/Definition/DataStruct/HitContext.cs | 62 + src-ref/Definition/DataStruct/ImpactData.cs | 24 + .../DataStruct/TowerCompItemData.cs | 99 + .../Definition/DataStruct/TowerItemData.cs | 54 + .../Definition/DataStruct/TowerStatsData.cs | 22 + src-ref/Definition/DataStruct/VersionInfo.cs | 68 + src-ref/Definition/Enum/AttackMethodType.cs | 9 + src-ref/Definition/Enum/AttackPropertyType.cs | 13 + src-ref/Definition/Enum/CampType.cs | 40 + src-ref/Definition/Enum/EntryType.cs | 13 + src-ref/Definition/Enum/EventEffectType.cs | 21 + .../Definition/Enum/EventRequirementType.cs | 11 + .../Definition/Enum/InventoryTagSourceType.cs | 11 + src-ref/Definition/Enum/LevelThemeType.cs | 10 + src-ref/Definition/Enum/LevelVictoryType.cs | 10 + src-ref/Definition/Enum/PhaseEndType.cs | 13 + .../ProcedureMainCombatEntryBlockReason.cs | 9 + .../Definition/Enum/ProcedureMainFlowPhase.cs | 9 + .../Enum/ProcedureMainRunAdvanceResult.cs | 10 + .../Enum/ProcedureMainRunCompletionResult.cs | 9 + src-ref/Definition/Enum/RarityType.cs | 12 + src-ref/Definition/Enum/RelationType.cs | 28 + .../Enum/RepoItemClickActionType.cs | 9 + src-ref/Definition/Enum/SceneType.cs | 9 + src-ref/Definition/Enum/TagCategory.cs | 12 + src-ref/Definition/Enum/TagTriggerPhase.cs | 11 + src-ref/Definition/Enum/TagType.cs | 31 + src-ref/Definition/Enum/TowerCompSlotType.cs | 11 + src-ref/Definition/Enum/UIFormType.cs | 85 + .../Event/EventEffect/AddGoldEffect.cs | 25 + .../Event/EventEffect/AddRandomCompsEffect.cs | 48 + .../DamageRandomTowerEnduranceEffect.cs | 34 + .../Event/EventEffect/EventEffectBase.cs | 14 + .../EventEffect/RemoveRandomCompEffect.cs | 27 + .../Definition/Event/EventOptionExecutor.cs | 490 + .../CompCountAtLeastRequirement.cs | 26 + .../EventRequirement/EventRequirementBase.cs | 12 + .../GoldAtLeastRequirement.cs | 24 + .../EventRequirement/HasRelicRequirement.cs | 24 + .../TowerCountAtLeastRequirement.cs | 24 + .../Definition/InventoryRarityRuleService.cs | 32 + .../ParticipantTowerAssignResult.cs | 22 + .../Tag/Aggregation/TagRuntimeData.cs | 11 + .../Aggregation/TowerTagAggregationService.cs | 130 + .../Tag/Combat/EnemyStatusTagRegistry.cs | 25 + .../Handlers/AttackShapeTagEffectHandler.cs | 41 + .../Handlers/NumericTagEffectHandler.cs | 109 + .../Combat/States/EnemyStatusTagStateBase.cs | 7 + .../Tag/Combat/States/FireTagState.cs | 9 + .../Tag/Combat/States/IceTagState.cs | 8 + .../StatusEffects/EnemyStatusTagEffectBase.cs | 30 + .../Tag/Combat/StatusEffects/FireTagEffect.cs | 75 + .../StatusEffects/IEnemyStatusTagEffect.cs | 13 + .../Tag/Combat/StatusEffects/IceTagEffect.cs | 54 + .../Tag/Combat/TagEffectResolver.cs | 177 + .../ComponentTagGenerationService.cs | 203 + .../Generation/InventoryTagRandomContext.cs | 72 + .../Tag/Generation/RarityTagBudgetRule.cs | 9 + .../Generation/RarityTagBudgetRuleRegistry.cs | 73 + .../Tag/Generation/TagGenerationRule.cs | 9 + .../Generation/TagGenerationRuleRegistry.cs | 74 + .../Metadata/Config/AbsoluteZeroTagConfig.cs | 15 + .../Metadata/Config/BurnSpreadTagConfig.cs | 12 + .../Tag/Metadata/Config/CritTagConfig.cs | 15 + .../Tag/Metadata/Config/ExecutionTagConfig.cs | 15 + .../Tag/Metadata/Config/FireTagConfig.cs | 16 + .../Metadata/Config/FreezeMaskTagConfig.cs | 12 + .../Tag/Metadata/Config/IceTagConfig.cs | 16 + .../Metadata/Config/IgniteBurstTagConfig.cs | 12 + .../Tag/Metadata/Config/InfernoTagConfig.cs | 15 + .../Metadata/Config/OverpenetrateTagConfig.cs | 12 + .../Tag/Metadata/Config/PierceTagConfig.cs | 12 + .../Tag/Metadata/Config/ShatterTagConfig.cs | 15 + .../Tag/Metadata/Config/TagConfigBase.cs | 15 + .../Definition/Tag/Metadata/TagDefinition.cs | 14 + .../Tag/Metadata/TagDefinitionRegistry.cs | 214 + .../Tag/Presentation/TagDisplayUtility.cs | 175 + src-ref/Editor/GameFrameworkConfigs.cs | 30 + src-ref/Editor/GeometryTD.Editor.asmdef | 19 + src-ref/Editor/GeometryTDBuildEventHandler.cs | 83 + src-ref/Editor/SceneSwitchLeft.cs | 52 + src-ref/Entity/EntityData/BulletData.cs | 56 + src-ref/Entity/EntityData/EnemyData.cs | 83 + src-ref/Entity/EntityData/EntityDataBase.cs | 58 + src-ref/Entity/EntityData/MapData.cs | 127 + .../Entity/EntityData/MapEntityLoadContext.cs | 18 + src-ref/Entity/EntityData/PlayerData.cs | 40 + src-ref/Entity/EntityData/TowerData.cs | 60 + src-ref/Entity/EntityExtension.cs | 97 + src-ref/Entity/EntityLogic/BulletEntity.cs | 76 + .../EntityLogic/CombatSelectInputService.cs | 128 + .../CombatSelectUseCaseConfigurator.cs | 158 + src-ref/Entity/EntityLogic/EnemyEntity.cs | 237 + .../EntityLogic/EnemyTagStatusRuntime.cs | 104 + src-ref/Entity/EntityLogic/EntityBase.cs | 114 + src-ref/Entity/EntityLogic/MapEntity.cs | 439 + src-ref/Entity/EntityLogic/Player.cs | 55 + src-ref/Entity/EntityLogic/TowerEntity.cs | 77 + .../Combat/CombatBaseHpChangedEventArgs.cs | 37 + .../Combat/CombatCoinChangedEventArgs.cs | 37 + .../Event/Combat/CombatDebugFailEventArgs.cs | 21 + src-ref/Event/Combat/CombatEndEventArgs.cs | 25 + .../CombatEnemyHpRateChangedEventArgs.cs | 31 + .../Combat/CombatFinishReturnEventArgs.cs | 21 + src-ref/Event/Combat/CombatPauseEventArgs.cs | 21 + .../Event/Combat/CombatProcessEventArgs.cs | 37 + .../Combat/CombatSelectItemClickEventArgs.cs | 31 + .../EventOptionItemSelectedEventArgs.cs | 27 + src-ref/Event/Game/NodeCompleteEventArgs.cs | 68 + src-ref/Event/Game/NodeEnterEventArgs.cs | 49 + .../Event/Game/NodeMapNodeClickEventArgs.cs | 26 + .../NodeMapNodeEnterRequestedEventArgs.cs | 43 + .../Event/Game/TestMenuNodeClickEventArgs.cs | 33 + .../General/RewardSelectGiveUpEventArgs.cs | 21 + .../RewardSelectItemSelectedEventArgs.cs | 26 + .../General/RewardSelectRefreshEventArgs.cs | 21 + .../MainForm/RepoButtonClickedEventArgs.cs | 27 + .../MainForm/ReturnButtonClickedEventArgs.cs | 27 + .../Event/Menu/MenuExitRequestedEventArgs.cs | 21 + .../Menu/MenuSettingsRequestedEventArgs.cs | 21 + .../Event/Menu/MenuStartRequestedEventArgs.cs | 21 + .../RepoForm/CombineSlotClickedEventArgs.cs | 26 + .../RepoForm/RepoCombineRequestedEventArgs.cs | 32 + .../Event/RepoForm/RepoFormReturnEventArgs.cs | 21 + .../RepoForm/RepoItemClickedEventArgs.cs | 31 + .../RepoForm/RepoItemDragEndedEventArgs.cs | 30 + ...RepoParticipantAssignRequestedEventArgs.cs | 27 + .../RepoSellCancelRequestedEventArgs.cs | 21 + .../RepoSellConfirmRequestedEventArgs.cs | 21 + .../RepoSellModeToggleRequestedEventArgs.cs | 21 + .../Event/Shop/ShopExitRequestedEventArgs.cs | 21 + .../Shop/ShopInventoryRequestedEventArgs.cs | 21 + .../Shop/ShopPurchaseRequestedEventArgs.cs | 26 + src-ref/Factory/ComponentItemFactory.cs | 82 + src-ref/Factory/EventEffectFactory.cs | 104 + src-ref/Factory/EventRequirementFactory.cs | 75 + src-ref/Factory/OutGameDropItemBuilder.cs | 88 + src-ref/Factory/PhaseEndConditionFactory.cs | 25 + src-ref/Factory/RunStateFactory.cs | 59 + src-ref/Network/CSPacketBase.cs | 20 + src-ref/Network/CSPacketHeader.cs | 20 + src-ref/Network/NetworkChannelHelper.cs | 280 + src-ref/Network/Packet/CSHeartBeat.cs | 32 + src-ref/Network/Packet/SCHeartBeat.cs | 32 + src-ref/Network/PacketBase.cs | 32 + .../PacketHandler/SCHeartBeatHandler.cs | 29 + src-ref/Network/PacketHandlerBase.cs | 21 + src-ref/Network/PacketHeaderBase.cs | 46 + src-ref/Network/PacketType.cs | 27 + src-ref/Network/SCPacketBase.cs | 20 + src-ref/Network/SCPacketHeader.cs | 20 + src-ref/PoolObjectBase/HPBarItemObject.cs | 28 + src-ref/PoolObjectBase/RepoItemObject.cs | 28 + src-ref/PoolObjectBase/TowerRepoItemObject.cs | 28 + src-ref/Procedure/Base/ProcedureBase.cs | 19 + .../Procedure/Base/ProcedureChangeScene.cs | 149 + .../Procedure/Base/ProcedureCheckResources.cs | 57 + .../Procedure/Base/ProcedureCheckVersion.cs | 162 + .../Procedure/Base/ProcedureInitResources.cs | 48 + src-ref/Procedure/Base/ProcedureLaunch.cs | 129 + src-ref/Procedure/Base/ProcedurePreload.cs | 237 + src-ref/Procedure/Base/ProcedureSplash.cs | 45 + .../Base/ProcedureUpdateResources.cs | 270 + .../Procedure/Base/ProcedureUpdateVersion.cs | 70 + .../Base/ProcedureVerifyResources.cs | 77 + .../FixedRunNodeSequenceBuilder.cs | 52 + .../Procedure/ProcedureMain/ProcedureMain.cs | 468 + ...rocedureMainCombatEntryValidationResult.cs | 13 + ...ocedureMainCombatEntryValidationService.cs | 123 + .../ProcedureMainNodeEventGuardService.cs | 22 + ...edureMainParticipantTowerCleanupService.cs | 106 + .../ProcedureMainRunCompletionService.cs | 26 + .../ProcedureMainRunFlowService.cs | 27 + src-ref/Procedure/ProcedureMain/RunModel.cs | 355 + .../ProcedureMain/RunStateAdvanceService.cs | 56 + src-ref/Procedure/ProcedureMenu.cs | 59 + src-ref/Procedure/ProcedureTest.cs | 30 + src-ref/Scene/HideByBoundary.cs | 30 + src-ref/Scene/Map/House.cs | 10 + src-ref/Scene/Map/MapCombatRuntimeBridge.cs | 85 + src-ref/Scene/Map/MapDataRefs.cs | 37 + src-ref/Scene/Map/MapTopologyService.cs | 254 + src-ref/Scene/Map/Spawner.cs | 14 + src-ref/Scene/Map/TowerPlacementService.cs | 361 + src-ref/Scene/Map/TowerSelectionPresenter.cs | 134 + .../Scene/Pathfinding/GridMapPathfinder.cs | 134 + src-ref/Scene/Pathfinding/IMapPathfinder.cs | 15 + src-ref/Sound/SoundExtension.cs | 170 + src-ref/UI/Base/IUIFormController.cs | 9 + src-ref/UI/Base/IUIUseCase.cs | 6 + src-ref/UI/Base/UGuiForm.cs | 242 + src-ref/UI/Base/UGuiGroupHelper.cs | 53 + src-ref/UI/Base/UIContext.cs | 6 + src-ref/UI/Base/UIExtension.cs | 182 + src-ref/UI/Base/UIFormControllerBase.cs | 10 + src-ref/UI/Base/UIFormControllerCommonBase.cs | 183 + .../Combat/Context/CombatFinishFormContext.cs | 10 + .../Combat/Context/CombatInfoFormContext.cs | 13 + .../Combat/Context/CombatSelectActionType.cs | 10 + .../Combat/Context/CombatSelectFormContext.cs | 15 + .../Combat/Context/TowerSelectItemContext.cs | 15 + ...mbatFinishFormController.ContextBuilder.cs | 269 + ...CombatInfoFormController.ContextBuilder.cs | 76 + ...mbatSelectFormController.ContextBuilder.cs | 77 + .../Controller/CombatFinishFormController.cs | 152 + .../Controller/CombatInfoFormController.cs | 181 + .../Controller/CombatSelectFormController.cs | 223 + .../Combat/RawData/CombatFinishFormRawData.cs | 12 + .../Combat/RawData/CombatInfoFormRawData.cs | 18 + .../RawData/CombatSelectClickObjectType.cs | 9 + .../Combat/RawData/CombatSelectDisplayMode.cs | 9 + .../Combat/RawData/CombatSelectFormRawData.cs | 14 + .../RawData/CombatSelectFormUserData.cs | 16 + .../Combat/RawData/TowerSelectItemRawData.cs | 19 + .../Combat/UseCase/CombatFinishFormUseCase.cs | 62 + .../Combat/UseCase/CombatInfoFormUseCase.cs | 96 + .../Combat/UseCase/CombatSelectFormUseCase.cs | 337 + src-ref/UI/Combat/View/CombatFinishForm.cs | 173 + src-ref/UI/Combat/View/CombatInfoForm.cs | 136 + .../UI/Combat/View/CombatSelectBuildArea.cs | 60 + src-ref/UI/Combat/View/CombatSelectForm.cs | 141 + .../UI/Combat/View/CombatSelectFuncArea.cs | 53 + src-ref/UI/Combat/View/TowerSelectItem.cs | 118 + src-ref/UI/CommonButton.cs | 108 + src-ref/UI/DialogParams.cs | 116 + src-ref/UI/Game/Context/CombineAreaContext.cs | 6 + src-ref/UI/Game/Context/CompAreaContext.cs | 8 + src-ref/UI/Game/Context/EventFormContext.cs | 12 + .../UI/Game/Context/EventOptionItemContext.cs | 10 + src-ref/UI/Game/Context/MainFormContext.cs | 6 + src-ref/UI/Game/Context/NodeItemContext.cs | 16 + src-ref/UI/Game/Context/NodeMapFormContext.cs | 11 + .../UI/Game/Context/ParticipantAreaContext.cs | 8 + src-ref/UI/Game/Context/RepoFormContext.cs | 16 + src-ref/UI/Game/Context/RepoFormState.cs | 8 + .../Game/Context/RepoItemClickActionType.cs | 8 + src-ref/UI/Game/Context/RepoItemContext.cs | 19 + src-ref/UI/Game/Context/SellAreaContext.cs | 10 + .../UI/Game/Context/TowerRepoItemContext.cs | 9 + .../NodeMapFormController.ContextBuilder.cs | 41 + .../RepoFormController.ContextBuilder.cs | 497 + ...wardSelectFormController.ContextBuilder.cs | 142 + .../UI/Game/Controller/EventFormController.cs | 129 + .../UI/Game/Controller/MainFormController.cs | 79 + .../Game/Controller/NodeMapFormController.cs | 130 + .../UI/Game/Controller/RepoFormController.cs | 425 + src-ref/UI/Game/RawData/EventFormRawData.cs | 18 + src-ref/UI/Game/RawData/NodeMapFormRawData.cs | 28 + src-ref/UI/Game/RawData/RepoFormRawData.cs | 13 + .../RepoParticipantAssignDialogUtility.cs | 45 + src-ref/UI/Game/UseCase/EventFormUseCase.cs | 106 + src-ref/UI/Game/UseCase/NodeMapFormUseCase.cs | 156 + src-ref/UI/Game/UseCase/RepoFormUseCase.cs | 209 + src-ref/UI/Game/View/CombineArea.cs | 169 + src-ref/UI/Game/View/CombineSlotItem.cs | 69 + src-ref/UI/Game/View/CompArea.cs | 203 + src-ref/UI/Game/View/EventForm.cs | 82 + src-ref/UI/Game/View/EventOptionItem.cs | 85 + src-ref/UI/Game/View/IRepoDragItemView.cs | 19 + src-ref/UI/Game/View/MainForm.cs | 49 + src-ref/UI/Game/View/NodeItem.cs | 121 + src-ref/UI/Game/View/NodeMapForm.cs | 89 + src-ref/UI/Game/View/ParticipantArea.cs | 194 + src-ref/UI/Game/View/RepoForm.cs | 136 + src-ref/UI/Game/View/RepoItem.cs | 381 + src-ref/UI/Game/View/SellArea.cs | 112 + src-ref/UI/Game/View/TowerRepoItem.cs | 109 + .../UI/General/Context/DialogFormContext.cs | 19 + src-ref/UI/General/Context/IconAreaContext.cs | 15 + .../UI/General/Context/ItemDescFormContext.cs | 14 + .../UI/General/Context/RewardItemContext.cs | 12 + .../Context/RewardSelectFormContext.cs | 11 + src-ref/UI/General/Context/TagItemContext.cs | 11 + .../General/Context/TowerIconAreaContext.cs | 21 + .../Controller/DialogFormController.cs | 78 + .../Controller/ItemDescFormController.cs | 121 + .../Controller/RewardSelectFormController.cs | 162 + .../UI/General/RawData/DialogFormRawData.cs | 109 + .../UI/General/RawData/ItemDescFormRawData.cs | 16 + .../RawData/RewardSelectFormRawData.cs | 11 + .../RawData/RewardSelectItemRawData.cs | 17 + .../RawData/RewardSelectItemRawDataBuilder.cs | 56 + .../UI/General/UseCase/ItemDescFormUseCase.cs | 9 + .../UseCase/RewardSelectFormUseCase.cs | 236 + src-ref/UI/General/View/DialogForm.cs | 208 + src-ref/UI/General/View/IconArea.cs | 105 + src-ref/UI/General/View/ItemDescForm.cs | 392 + src-ref/UI/General/View/RewardItem.cs | 230 + src-ref/UI/General/View/RewardSelectForm.cs | 167 + src-ref/UI/General/View/TagItem.cs | 27 + src-ref/UI/General/View/TowerIconArea.cs | 72 + src-ref/UI/HPBarItem.cs | 122 + src-ref/UI/Menu/Context/MenuFormContext.cs | 13 + .../UI/Menu/Context/TestMenuFormContext.cs | 9 + .../UI/Menu/Controller/MenuFormController.cs | 129 + .../Menu/Controller/TestMenuFormController.cs | 57 + src-ref/UI/Menu/RawData/MenuFormRawData.cs | 13 + src-ref/UI/Menu/UseCase/MenuFormUseCase.cs | 16 + src-ref/UI/Menu/View/MenuForm.cs | 76 + src-ref/UI/Menu/View/TestMenuForm.cs | 80 + src-ref/UI/Shop/Context/GoodsItemContext.cs | 14 + src-ref/UI/Shop/Context/ShopFormContext.cs | 8 + .../ShopFormController.ContextBuilder.cs | 59 + .../UI/Shop/Controller/ShopFormController.cs | 137 + src-ref/UI/Shop/RawData/GoodsItemRawData.cs | 17 + src-ref/UI/Shop/RawData/ShopFormRawData.cs | 10 + src-ref/UI/Shop/UseCase/ShopFormUseCase.cs | 76 + src-ref/UI/Shop/View/GoodsItem.cs | 195 + src-ref/UI/Shop/View/ShopForm.cs | 94 + .../GameScene/Context/DisplayItemContext.cs | 11 + .../Context/DisplayItemInfoFormContext.cs | 18 + .../Context/DisplayListAreaContext.cs | 14 + .../GameScene/Context/GoodsItemContext.cs | 15 + .../GameScene/Context/HudFormContext.cs | 10 + .../GameScene/Context/LevelUpFormContext.cs | 10 + .../Context/LevelUpRewardItemContext.cs | 14 + .../GameScene/Context/ShopFormContext.cs | 15 + .../DisplayItemInfoFormController.cs | 116 + .../GameScene/Controller/HudFormController.cs | 46 + .../Controller/LevelUpFormController.cs | 169 + .../Controller/ShopFormController.cs | 411 + .../RawData/DisplayItemInfoFormRawData.cs | 20 + .../GameScene/RawData/LevelUpFormRawData.cs | 11 + .../GameScene/RawData/ShopFormRawData.cs | 18 + .../UseCase/DisplayItemInfoFormUseCase.cs | 75 + .../GameScene/UseCase/LevelUpFormUseCase.cs | 195 + .../GameScene/UseCase/ShopFormUseCase.cs | 444 + .../Templates/GameScene/View/DisplayItem.cs | 71 + .../GameScene/View/DisplayItemInfoForm.cs | 205 + .../GameScene/View/DisplayItemObject.cs | 27 + .../GameScene/View/DisplayListArea.cs | 285 + .../UI/Templates/GameScene/View/HudForm.cs | 142 + .../Templates/GameScene/View/LevelUpForm.cs | 70 + .../GameScene/View/LevelUpRewardItem.cs | 47 + .../UI/Templates/GameScene/View/ShopForm.cs | 192 + .../MenuScene/Context/RoleItemContext.cs | 8 + .../Context/RolePropertyAreaContext.cs | 8 + .../Context/SelectRoleFormContext.cs | 9 + .../MenuScene/Context/StartMenuFormContext.cs | 6 + .../Controller/SelectRoleFormController.cs | 156 + .../Controller/StartMenuFormController.cs | 137 + .../RawData/SelectRoleFormRawData.cs | 11 + .../MenuScene/RawData/StartMenuFormRawData.cs | 7 + .../UseCase/SelectRoleFormUseCase.cs | 99 + .../UI/Templates/MenuScene/View/RoleItem.cs | 40 + .../MenuScene/View/RolePropertyArea.cs | 37 + .../MenuScene/View/SelectRoleForm.cs | 100 + .../Templates/MenuScene/View/SettingForm.cs | 189 + .../Templates/MenuScene/View/StartMenuForm.cs | 64 + src-ref/UI/UpdateResourceForm.cs | 30 + src-ref/Utility/AssetUtility.cs | 81 + src-ref/Utility/EnumUtility.cs | 27 + src-ref/Utility/IconColorGenerator.cs | 443 + src-ref/Utility/InventoryCloneUtility.cs | 236 + .../Utility/InventoryParticipantUtility.cs | 180 + src-ref/Utility/InventorySeedUtility.cs | 198 + src-ref/Utility/ItemDescUtility.cs | 282 + src-ref/Utility/JsonNetUtility.cs | 26 + src-ref/Utility/ShopPriceRuleService.cs | 122 + .../Utility/TowerComposedIconCacheUtility.cs | 130 + src-ref/Utility/TowerIconComposeUtility.cs | 148 + src-ref/Utility/WebUtility.cs | 24 + src/Class1.cs | 9 + src/Geometry-Tower-Defense-Base.csproj | 15 + src/Geometry-Tower-Defense-Base.sln | 65 + .../Definition/DataStruct/BuildInfo.cs | 54 + .../Definition/DataStruct/ImpactData.cs | 24 + .../Definition/DataStruct/RunItemState.cs | 20 + .../DataStruct/RunNodeCompletionSnapshot.cs | 42 + .../DataStruct/TowerCompItemData.cs | 99 + .../Definition/DataStruct/VersionInfo.cs | 68 + .../Definition/Enum/AttackMethodType.cs | 9 + .../Definition/Enum/AttackPropertyType.cs | 13 + .../Definition/Enum/CampType.cs | 40 + .../Definition/Enum/CombatSelectActionType.cs | 10 + .../Definition/Enum/EntryType.cs | 13 + .../Definition/Enum/EventEffectType.cs | 21 + .../Definition/Enum/EventRequirementType.cs | 11 + .../Definition/Enum/InventoryTagSourceType.cs | 11 + .../Definition/Enum/LevelThemeType.cs | 10 + .../Definition/Enum/LevelVictoryType.cs | 10 + .../Definition/Enum/PhaseEndType.cs | 13 + .../ProcedureMainCombatEntryBlockReason.cs | 9 + .../Definition/Enum/ProcedureMainFlowPhase.cs | 9 + .../Enum/ProcedureMainRunAdvanceResult.cs | 10 + .../Enum/ProcedureMainRunCompletionResult.cs | 9 + .../Definition/Enum/RarityType.cs | 12 + .../Definition/Enum/RelationType.cs | 28 + .../Enum/RepoItemClickActionType.cs | 9 + .../Enum/RunNodeCompletionStatus.cs | 9 + .../Definition/Enum/RunNodeStatus.cs | 11 + .../Definition/Enum/RunNodeType.cs | 11 + .../Definition/Enum/SceneType.cs | 9 + .../Definition/Enum/TagCategory.cs | 12 + .../Definition/Enum/TagTriggerPhase.cs | 11 + .../Definition/Enum/TagType.cs | 31 + .../Definition/Enum/TowerCompSlotType.cs | 11 + .../Definition/Enum/UIFormType.cs | 85 + .../Combat/CombatBaseHpChangedEventArgs.cs | 37 + .../Combat/CombatCoinChangedEventArgs.cs | 37 + .../Event/Combat/CombatDebugFailEventArgs.cs | 21 + .../Event/Combat/CombatEndEventArgs.cs | 25 + .../CombatEnemyHpRateChangedEventArgs.cs | 31 + .../Combat/CombatFinishReturnEventArgs.cs | 21 + .../Event/Combat/CombatPauseEventArgs.cs | 21 + .../Event/Combat/CombatProcessEventArgs.cs | 37 + .../Combat/CombatSelectItemClickEventArgs.cs | 31 + .../EventOptionItemSelectedEventArgs.cs | 27 + .../Event/Game/NodeCompleteEventArgs.cs | 68 + .../Event/Game/NodeEnterEventArgs.cs | 49 + .../Event/Game/NodeMapNodeClickEventArgs.cs | 26 + .../NodeMapNodeEnterRequestedEventArgs.cs | 43 + .../Event/Game/TestMenuNodeClickEventArgs.cs | 33 + .../General/RewardSelectGiveUpEventArgs.cs | 21 + .../RewardSelectItemSelectedEventArgs.cs | 26 + .../General/RewardSelectRefreshEventArgs.cs | 21 + .../MainForm/RepoButtonClickedEventArgs.cs | 27 + .../MainForm/ReturnButtonClickedEventArgs.cs | 27 + .../Event/Menu/MenuExitRequestedEventArgs.cs | 21 + .../Menu/MenuSettingsRequestedEventArgs.cs | 21 + .../Event/Menu/MenuStartRequestedEventArgs.cs | 21 + .../RepoForm/CombineSlotClickedEventArgs.cs | 26 + .../RepoForm/RepoCombineRequestedEventArgs.cs | 32 + .../Event/RepoForm/RepoFormReturnEventArgs.cs | 21 + .../RepoForm/RepoItemClickedEventArgs.cs | 31 + .../RepoForm/RepoItemDragEndedEventArgs.cs | 30 + ...RepoParticipantAssignRequestedEventArgs.cs | 27 + .../RepoSellCancelRequestedEventArgs.cs | 21 + .../RepoSellConfirmRequestedEventArgs.cs | 21 + .../RepoSellModeToggleRequestedEventArgs.cs | 21 + .../Event/Shop/ShopExitRequestedEventArgs.cs | 21 + .../Shop/ShopInventoryRequestedEventArgs.cs | 21 + .../Shop/ShopPurchaseRequestedEventArgs.cs | 26 + .../GeometryTD.Domain.csproj | 19 + .../UI/Base/IUIFormController.cs | 9 + src/GeometryTD.Domain/UI/Base/IUIUseCase.cs | 6 + src/GeometryTD.Domain/UI/Base/UIContext.cs | 6 + .../GeometryTD.Infrastructure.csproj | 23 + .../GeometryTD.Presentation.csproj | 16 + src/GeometryTD.slnx | 2 + src/Libraries/GameFramework.dll | Bin 0 -> 415744 bytes src/Libraries/GameFramework.dll.meta | 19 + src/Libraries/GameFramework.xml | 27039 ++++++++++++++++ src/Libraries/GameFramework.xml.meta | 7 + src/Libraries/ICSharpCode.SharpZipLib.dll | Bin 0 -> 200704 bytes .../ICSharpCode.SharpZipLib.dll.meta | 19 + src/Libraries/link.xml | 41 + src/Libraries/link.xml.meta | 7 + 556 files changed, 69721 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 TODO.md create mode 100644 docs/CombatNodeArchitecture.md create mode 100644 docs/CombatReward爆率表.xlsx create mode 100644 docs/GameDesign.md create mode 100644 docs/LayeredArchitectureDesign.md create mode 100644 docs/MVP-Scope.md create mode 100644 docs/MapEntityArchitecture.md create mode 100644 docs/TagSystemDesign.md create mode 100644 docs/TagSystemRoadmap.md create mode 100644 docs/UI-5层架构设计规范.md create mode 100644 src-ref/Base/GameEntry.Builtin.cs create mode 100644 src-ref/Base/GameEntry.Custom.cs create mode 100644 src-ref/Base/GameEntry.cs create mode 100644 src-ref/Components/BasicBaseComp.cs create mode 100644 src-ref/Components/BasicBearingComp.cs create mode 100644 src-ref/Components/IDamageReceiver.cs create mode 100644 src-ref/Components/InputComponent.cs create mode 100644 src-ref/Components/MovementComponent.cs create mode 100644 src-ref/Components/ShooterBullet.cs create mode 100644 src-ref/Components/ShooterMuzzleComp.cs create mode 100644 src-ref/Components/TowerController.cs create mode 100644 src-ref/CustomComponent/BuiltinDataComponent.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatNodeComponent.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/CombatEventBridge.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/CombatRunResourceStore.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerCoordinator.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntime.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/CombatSettlementContext.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/CombatSettlementService.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatFailedState.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatFinishFormState.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatLoadingState.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRewardSelectionState.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRunningPhaseState.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatSettlementState.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatStateBase.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForPhaseEndState.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForReturnState.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropContext.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResult.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/ICombatSchedulerPort.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/BossDeadPhaseEndCondition.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/EnemiesClearedPhaseEndCondition.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/IPhaseEndCondition.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/NonePhaseEndCondition.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/PhaseEndConditionContext.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/TimeElapsedPhaseEndCondition.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseLoopRuntime.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatSettlementCalculator.cs create mode 100644 src-ref/CustomComponent/CombatNode/CombatSettlementCommitter.cs create mode 100644 src-ref/CustomComponent/CombatNode/EnemyManager/EnemyConfigProvider.cs create mode 100644 src-ref/CustomComponent/CombatNode/EnemyManager/EnemyLifecycleTracker.cs create mode 100644 src-ref/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs create mode 100644 src-ref/CustomComponent/CombatNode/EnemyManager/EnemySpawnDirector.cs create mode 100644 src-ref/CustomComponent/CombatNode/EnemyManager/EnemySpawnPathResolver.cs create mode 100644 src-ref/CustomComponent/EventNodeComponent.cs create mode 100644 src-ref/CustomComponent/HPBar/HPBarComponent.cs create mode 100644 src-ref/CustomComponent/InventoryGeneration/DropPoolRoller.cs create mode 100644 src-ref/CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs create mode 100644 src-ref/CustomComponent/InventoryGeneration/InventoryGenerationRandomContext.cs create mode 100644 src-ref/CustomComponent/InventoryGeneration/OutGameDropRuleService.cs create mode 100644 src-ref/CustomComponent/InventoryGeneration/RewardCandidateBuilder.cs create mode 100644 src-ref/CustomComponent/InventoryGeneration/ShopGoodsBuilder.cs create mode 100644 src-ref/CustomComponent/PlayerInventory/PlayerInventoryComponent.cs create mode 100644 src-ref/CustomComponent/PlayerInventory/PlayerInventoryStateStore.cs create mode 100644 src-ref/CustomComponent/PlayerInventory/PlayerInventoryTowerAssemblyService.cs create mode 100644 src-ref/CustomComponent/PlayerInventory/PlayerInventoryTowerRosterService.cs create mode 100644 src-ref/CustomComponent/PlayerInventory/PlayerInventoryTradeService.cs create mode 100644 src-ref/CustomComponent/ResolutionAdapterComponent.cs create mode 100644 src-ref/CustomComponent/ShopNodeComponent.cs create mode 100644 src-ref/CustomComponent/SpriteCacheComponent.cs create mode 100644 src-ref/CustomComponent/TagRegistry/TagRegistryComponent.cs create mode 100644 src-ref/CustomComponent/UIRouterComponent.cs create mode 100644 src-ref/DataTable/BinaryReaderExtension.cs create mode 100644 src-ref/DataTable/DRBaseComp.cs create mode 100644 src-ref/DataTable/DRBearingComp.cs create mode 100644 src-ref/DataTable/DREnemy.cs create mode 100644 src-ref/DataTable/DREntity.cs create mode 100644 src-ref/DataTable/DREvent.cs create mode 100644 src-ref/DataTable/DRLevel.cs create mode 100644 src-ref/DataTable/DRLevelPhase.cs create mode 100644 src-ref/DataTable/DRLevelSpawnEntry.cs create mode 100644 src-ref/DataTable/DRMusic.cs create mode 100644 src-ref/DataTable/DRMuzzleComp.cs create mode 100644 src-ref/DataTable/DROutGameDropPool.cs create mode 100644 src-ref/DataTable/DRRarityTagBudget.cs create mode 100644 src-ref/DataTable/DRScene.cs create mode 100644 src-ref/DataTable/DRShopPrice.cs create mode 100644 src-ref/DataTable/DRSound.cs create mode 100644 src-ref/DataTable/DRTag.cs create mode 100644 src-ref/DataTable/DRTagConfig.cs create mode 100644 src-ref/DataTable/DRUIForm.cs create mode 100644 src-ref/DataTable/DRUISound.cs create mode 100644 src-ref/DataTable/DataTableExtension.cs create mode 100644 src-ref/Definition/CombatParticipantTowerValidation.cs create mode 100644 src-ref/Definition/CombatParticipantTowerValidationText.cs create mode 100644 src-ref/Definition/Constant/Constant.AssetPriority.cs create mode 100644 src-ref/Definition/Constant/Constant.Layer.cs create mode 100644 src-ref/Definition/Constant/Constant.Setting.cs create mode 100644 src-ref/Definition/DataStruct/AttackPayload.cs create mode 100644 src-ref/Definition/DataStruct/BackpackInventoryData.cs create mode 100644 src-ref/Definition/DataStruct/BuildInfo.cs create mode 100644 src-ref/Definition/DataStruct/EventItem.cs create mode 100644 src-ref/Definition/DataStruct/EventOption.cs create mode 100644 src-ref/Definition/DataStruct/HitContext.cs create mode 100644 src-ref/Definition/DataStruct/ImpactData.cs create mode 100644 src-ref/Definition/DataStruct/TowerCompItemData.cs create mode 100644 src-ref/Definition/DataStruct/TowerItemData.cs create mode 100644 src-ref/Definition/DataStruct/TowerStatsData.cs create mode 100644 src-ref/Definition/DataStruct/VersionInfo.cs create mode 100644 src-ref/Definition/Enum/AttackMethodType.cs create mode 100644 src-ref/Definition/Enum/AttackPropertyType.cs create mode 100644 src-ref/Definition/Enum/CampType.cs create mode 100644 src-ref/Definition/Enum/EntryType.cs create mode 100644 src-ref/Definition/Enum/EventEffectType.cs create mode 100644 src-ref/Definition/Enum/EventRequirementType.cs create mode 100644 src-ref/Definition/Enum/InventoryTagSourceType.cs create mode 100644 src-ref/Definition/Enum/LevelThemeType.cs create mode 100644 src-ref/Definition/Enum/LevelVictoryType.cs create mode 100644 src-ref/Definition/Enum/PhaseEndType.cs create mode 100644 src-ref/Definition/Enum/ProcedureMainCombatEntryBlockReason.cs create mode 100644 src-ref/Definition/Enum/ProcedureMainFlowPhase.cs create mode 100644 src-ref/Definition/Enum/ProcedureMainRunAdvanceResult.cs create mode 100644 src-ref/Definition/Enum/ProcedureMainRunCompletionResult.cs create mode 100644 src-ref/Definition/Enum/RarityType.cs create mode 100644 src-ref/Definition/Enum/RelationType.cs create mode 100644 src-ref/Definition/Enum/RepoItemClickActionType.cs create mode 100644 src-ref/Definition/Enum/SceneType.cs create mode 100644 src-ref/Definition/Enum/TagCategory.cs create mode 100644 src-ref/Definition/Enum/TagTriggerPhase.cs create mode 100644 src-ref/Definition/Enum/TagType.cs create mode 100644 src-ref/Definition/Enum/TowerCompSlotType.cs create mode 100644 src-ref/Definition/Enum/UIFormType.cs create mode 100644 src-ref/Definition/Event/EventEffect/AddGoldEffect.cs create mode 100644 src-ref/Definition/Event/EventEffect/AddRandomCompsEffect.cs create mode 100644 src-ref/Definition/Event/EventEffect/DamageRandomTowerEnduranceEffect.cs create mode 100644 src-ref/Definition/Event/EventEffect/EventEffectBase.cs create mode 100644 src-ref/Definition/Event/EventEffect/RemoveRandomCompEffect.cs create mode 100644 src-ref/Definition/Event/EventOptionExecutor.cs create mode 100644 src-ref/Definition/Event/EventRequirement/CompCountAtLeastRequirement.cs create mode 100644 src-ref/Definition/Event/EventRequirement/EventRequirementBase.cs create mode 100644 src-ref/Definition/Event/EventRequirement/GoldAtLeastRequirement.cs create mode 100644 src-ref/Definition/Event/EventRequirement/HasRelicRequirement.cs create mode 100644 src-ref/Definition/Event/EventRequirement/TowerCountAtLeastRequirement.cs create mode 100644 src-ref/Definition/InventoryRarityRuleService.cs create mode 100644 src-ref/Definition/ParticipantTowerAssignResult.cs create mode 100644 src-ref/Definition/Tag/Aggregation/TagRuntimeData.cs create mode 100644 src-ref/Definition/Tag/Aggregation/TowerTagAggregationService.cs create mode 100644 src-ref/Definition/Tag/Combat/EnemyStatusTagRegistry.cs create mode 100644 src-ref/Definition/Tag/Combat/Handlers/AttackShapeTagEffectHandler.cs create mode 100644 src-ref/Definition/Tag/Combat/Handlers/NumericTagEffectHandler.cs create mode 100644 src-ref/Definition/Tag/Combat/States/EnemyStatusTagStateBase.cs create mode 100644 src-ref/Definition/Tag/Combat/States/FireTagState.cs create mode 100644 src-ref/Definition/Tag/Combat/States/IceTagState.cs create mode 100644 src-ref/Definition/Tag/Combat/StatusEffects/EnemyStatusTagEffectBase.cs create mode 100644 src-ref/Definition/Tag/Combat/StatusEffects/FireTagEffect.cs create mode 100644 src-ref/Definition/Tag/Combat/StatusEffects/IEnemyStatusTagEffect.cs create mode 100644 src-ref/Definition/Tag/Combat/StatusEffects/IceTagEffect.cs create mode 100644 src-ref/Definition/Tag/Combat/TagEffectResolver.cs create mode 100644 src-ref/Definition/Tag/Generation/ComponentTagGenerationService.cs create mode 100644 src-ref/Definition/Tag/Generation/InventoryTagRandomContext.cs create mode 100644 src-ref/Definition/Tag/Generation/RarityTagBudgetRule.cs create mode 100644 src-ref/Definition/Tag/Generation/RarityTagBudgetRuleRegistry.cs create mode 100644 src-ref/Definition/Tag/Generation/TagGenerationRule.cs create mode 100644 src-ref/Definition/Tag/Generation/TagGenerationRuleRegistry.cs create mode 100644 src-ref/Definition/Tag/Metadata/Config/AbsoluteZeroTagConfig.cs create mode 100644 src-ref/Definition/Tag/Metadata/Config/BurnSpreadTagConfig.cs create mode 100644 src-ref/Definition/Tag/Metadata/Config/CritTagConfig.cs create mode 100644 src-ref/Definition/Tag/Metadata/Config/ExecutionTagConfig.cs create mode 100644 src-ref/Definition/Tag/Metadata/Config/FireTagConfig.cs create mode 100644 src-ref/Definition/Tag/Metadata/Config/FreezeMaskTagConfig.cs create mode 100644 src-ref/Definition/Tag/Metadata/Config/IceTagConfig.cs create mode 100644 src-ref/Definition/Tag/Metadata/Config/IgniteBurstTagConfig.cs create mode 100644 src-ref/Definition/Tag/Metadata/Config/InfernoTagConfig.cs create mode 100644 src-ref/Definition/Tag/Metadata/Config/OverpenetrateTagConfig.cs create mode 100644 src-ref/Definition/Tag/Metadata/Config/PierceTagConfig.cs create mode 100644 src-ref/Definition/Tag/Metadata/Config/ShatterTagConfig.cs create mode 100644 src-ref/Definition/Tag/Metadata/Config/TagConfigBase.cs create mode 100644 src-ref/Definition/Tag/Metadata/TagDefinition.cs create mode 100644 src-ref/Definition/Tag/Metadata/TagDefinitionRegistry.cs create mode 100644 src-ref/Definition/Tag/Presentation/TagDisplayUtility.cs create mode 100644 src-ref/Editor/GameFrameworkConfigs.cs create mode 100644 src-ref/Editor/GeometryTD.Editor.asmdef create mode 100644 src-ref/Editor/GeometryTDBuildEventHandler.cs create mode 100644 src-ref/Editor/SceneSwitchLeft.cs create mode 100644 src-ref/Entity/EntityData/BulletData.cs create mode 100644 src-ref/Entity/EntityData/EnemyData.cs create mode 100644 src-ref/Entity/EntityData/EntityDataBase.cs create mode 100644 src-ref/Entity/EntityData/MapData.cs create mode 100644 src-ref/Entity/EntityData/MapEntityLoadContext.cs create mode 100644 src-ref/Entity/EntityData/PlayerData.cs create mode 100644 src-ref/Entity/EntityData/TowerData.cs create mode 100644 src-ref/Entity/EntityExtension.cs create mode 100644 src-ref/Entity/EntityLogic/BulletEntity.cs create mode 100644 src-ref/Entity/EntityLogic/CombatSelectInputService.cs create mode 100644 src-ref/Entity/EntityLogic/CombatSelectUseCaseConfigurator.cs create mode 100644 src-ref/Entity/EntityLogic/EnemyEntity.cs create mode 100644 src-ref/Entity/EntityLogic/EnemyTagStatusRuntime.cs create mode 100644 src-ref/Entity/EntityLogic/EntityBase.cs create mode 100644 src-ref/Entity/EntityLogic/MapEntity.cs create mode 100644 src-ref/Entity/EntityLogic/Player.cs create mode 100644 src-ref/Entity/EntityLogic/TowerEntity.cs create mode 100644 src-ref/Event/Combat/CombatBaseHpChangedEventArgs.cs create mode 100644 src-ref/Event/Combat/CombatCoinChangedEventArgs.cs create mode 100644 src-ref/Event/Combat/CombatDebugFailEventArgs.cs create mode 100644 src-ref/Event/Combat/CombatEndEventArgs.cs create mode 100644 src-ref/Event/Combat/CombatEnemyHpRateChangedEventArgs.cs create mode 100644 src-ref/Event/Combat/CombatFinishReturnEventArgs.cs create mode 100644 src-ref/Event/Combat/CombatPauseEventArgs.cs create mode 100644 src-ref/Event/Combat/CombatProcessEventArgs.cs create mode 100644 src-ref/Event/Combat/CombatSelectItemClickEventArgs.cs create mode 100644 src-ref/Event/EventForm/EventOptionItemSelectedEventArgs.cs create mode 100644 src-ref/Event/Game/NodeCompleteEventArgs.cs create mode 100644 src-ref/Event/Game/NodeEnterEventArgs.cs create mode 100644 src-ref/Event/Game/NodeMapNodeClickEventArgs.cs create mode 100644 src-ref/Event/Game/NodeMapNodeEnterRequestedEventArgs.cs create mode 100644 src-ref/Event/Game/TestMenuNodeClickEventArgs.cs create mode 100644 src-ref/Event/General/RewardSelectGiveUpEventArgs.cs create mode 100644 src-ref/Event/General/RewardSelectItemSelectedEventArgs.cs create mode 100644 src-ref/Event/General/RewardSelectRefreshEventArgs.cs create mode 100644 src-ref/Event/MainForm/RepoButtonClickedEventArgs.cs create mode 100644 src-ref/Event/MainForm/ReturnButtonClickedEventArgs.cs create mode 100644 src-ref/Event/Menu/MenuExitRequestedEventArgs.cs create mode 100644 src-ref/Event/Menu/MenuSettingsRequestedEventArgs.cs create mode 100644 src-ref/Event/Menu/MenuStartRequestedEventArgs.cs create mode 100644 src-ref/Event/RepoForm/CombineSlotClickedEventArgs.cs create mode 100644 src-ref/Event/RepoForm/RepoCombineRequestedEventArgs.cs create mode 100644 src-ref/Event/RepoForm/RepoFormReturnEventArgs.cs create mode 100644 src-ref/Event/RepoForm/RepoItemClickedEventArgs.cs create mode 100644 src-ref/Event/RepoForm/RepoItemDragEndedEventArgs.cs create mode 100644 src-ref/Event/RepoForm/RepoParticipantAssignRequestedEventArgs.cs create mode 100644 src-ref/Event/RepoForm/RepoSellCancelRequestedEventArgs.cs create mode 100644 src-ref/Event/RepoForm/RepoSellConfirmRequestedEventArgs.cs create mode 100644 src-ref/Event/RepoForm/RepoSellModeToggleRequestedEventArgs.cs create mode 100644 src-ref/Event/Shop/ShopExitRequestedEventArgs.cs create mode 100644 src-ref/Event/Shop/ShopInventoryRequestedEventArgs.cs create mode 100644 src-ref/Event/Shop/ShopPurchaseRequestedEventArgs.cs create mode 100644 src-ref/Factory/ComponentItemFactory.cs create mode 100644 src-ref/Factory/EventEffectFactory.cs create mode 100644 src-ref/Factory/EventRequirementFactory.cs create mode 100644 src-ref/Factory/OutGameDropItemBuilder.cs create mode 100644 src-ref/Factory/PhaseEndConditionFactory.cs create mode 100644 src-ref/Factory/RunStateFactory.cs create mode 100644 src-ref/Network/CSPacketBase.cs create mode 100644 src-ref/Network/CSPacketHeader.cs create mode 100644 src-ref/Network/NetworkChannelHelper.cs create mode 100644 src-ref/Network/Packet/CSHeartBeat.cs create mode 100644 src-ref/Network/Packet/SCHeartBeat.cs create mode 100644 src-ref/Network/PacketBase.cs create mode 100644 src-ref/Network/PacketHandler/SCHeartBeatHandler.cs create mode 100644 src-ref/Network/PacketHandlerBase.cs create mode 100644 src-ref/Network/PacketHeaderBase.cs create mode 100644 src-ref/Network/PacketType.cs create mode 100644 src-ref/Network/SCPacketBase.cs create mode 100644 src-ref/Network/SCPacketHeader.cs create mode 100644 src-ref/PoolObjectBase/HPBarItemObject.cs create mode 100644 src-ref/PoolObjectBase/RepoItemObject.cs create mode 100644 src-ref/PoolObjectBase/TowerRepoItemObject.cs create mode 100644 src-ref/Procedure/Base/ProcedureBase.cs create mode 100644 src-ref/Procedure/Base/ProcedureChangeScene.cs create mode 100644 src-ref/Procedure/Base/ProcedureCheckResources.cs create mode 100644 src-ref/Procedure/Base/ProcedureCheckVersion.cs create mode 100644 src-ref/Procedure/Base/ProcedureInitResources.cs create mode 100644 src-ref/Procedure/Base/ProcedureLaunch.cs create mode 100644 src-ref/Procedure/Base/ProcedurePreload.cs create mode 100644 src-ref/Procedure/Base/ProcedureSplash.cs create mode 100644 src-ref/Procedure/Base/ProcedureUpdateResources.cs create mode 100644 src-ref/Procedure/Base/ProcedureUpdateVersion.cs create mode 100644 src-ref/Procedure/Base/ProcedureVerifyResources.cs create mode 100644 src-ref/Procedure/ProcedureMain/FixedRunNodeSequenceBuilder.cs create mode 100644 src-ref/Procedure/ProcedureMain/ProcedureMain.cs create mode 100644 src-ref/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationResult.cs create mode 100644 src-ref/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationService.cs create mode 100644 src-ref/Procedure/ProcedureMain/ProcedureMainNodeEventGuardService.cs create mode 100644 src-ref/Procedure/ProcedureMain/ProcedureMainParticipantTowerCleanupService.cs create mode 100644 src-ref/Procedure/ProcedureMain/ProcedureMainRunCompletionService.cs create mode 100644 src-ref/Procedure/ProcedureMain/ProcedureMainRunFlowService.cs create mode 100644 src-ref/Procedure/ProcedureMain/RunModel.cs create mode 100644 src-ref/Procedure/ProcedureMain/RunStateAdvanceService.cs create mode 100644 src-ref/Procedure/ProcedureMenu.cs create mode 100644 src-ref/Procedure/ProcedureTest.cs create mode 100644 src-ref/Scene/HideByBoundary.cs create mode 100644 src-ref/Scene/Map/House.cs create mode 100644 src-ref/Scene/Map/MapCombatRuntimeBridge.cs create mode 100644 src-ref/Scene/Map/MapDataRefs.cs create mode 100644 src-ref/Scene/Map/MapTopologyService.cs create mode 100644 src-ref/Scene/Map/Spawner.cs create mode 100644 src-ref/Scene/Map/TowerPlacementService.cs create mode 100644 src-ref/Scene/Map/TowerSelectionPresenter.cs create mode 100644 src-ref/Scene/Pathfinding/GridMapPathfinder.cs create mode 100644 src-ref/Scene/Pathfinding/IMapPathfinder.cs create mode 100644 src-ref/Sound/SoundExtension.cs create mode 100644 src-ref/UI/Base/IUIFormController.cs create mode 100644 src-ref/UI/Base/IUIUseCase.cs create mode 100644 src-ref/UI/Base/UGuiForm.cs create mode 100644 src-ref/UI/Base/UGuiGroupHelper.cs create mode 100644 src-ref/UI/Base/UIContext.cs create mode 100644 src-ref/UI/Base/UIExtension.cs create mode 100644 src-ref/UI/Base/UIFormControllerBase.cs create mode 100644 src-ref/UI/Base/UIFormControllerCommonBase.cs create mode 100644 src-ref/UI/Combat/Context/CombatFinishFormContext.cs create mode 100644 src-ref/UI/Combat/Context/CombatInfoFormContext.cs create mode 100644 src-ref/UI/Combat/Context/CombatSelectActionType.cs create mode 100644 src-ref/UI/Combat/Context/CombatSelectFormContext.cs create mode 100644 src-ref/UI/Combat/Context/TowerSelectItemContext.cs create mode 100644 src-ref/UI/Combat/ContextBuilder/CombatFinishFormController.ContextBuilder.cs create mode 100644 src-ref/UI/Combat/ContextBuilder/CombatInfoFormController.ContextBuilder.cs create mode 100644 src-ref/UI/Combat/ContextBuilder/CombatSelectFormController.ContextBuilder.cs create mode 100644 src-ref/UI/Combat/Controller/CombatFinishFormController.cs create mode 100644 src-ref/UI/Combat/Controller/CombatInfoFormController.cs create mode 100644 src-ref/UI/Combat/Controller/CombatSelectFormController.cs create mode 100644 src-ref/UI/Combat/RawData/CombatFinishFormRawData.cs create mode 100644 src-ref/UI/Combat/RawData/CombatInfoFormRawData.cs create mode 100644 src-ref/UI/Combat/RawData/CombatSelectClickObjectType.cs create mode 100644 src-ref/UI/Combat/RawData/CombatSelectDisplayMode.cs create mode 100644 src-ref/UI/Combat/RawData/CombatSelectFormRawData.cs create mode 100644 src-ref/UI/Combat/RawData/CombatSelectFormUserData.cs create mode 100644 src-ref/UI/Combat/RawData/TowerSelectItemRawData.cs create mode 100644 src-ref/UI/Combat/UseCase/CombatFinishFormUseCase.cs create mode 100644 src-ref/UI/Combat/UseCase/CombatInfoFormUseCase.cs create mode 100644 src-ref/UI/Combat/UseCase/CombatSelectFormUseCase.cs create mode 100644 src-ref/UI/Combat/View/CombatFinishForm.cs create mode 100644 src-ref/UI/Combat/View/CombatInfoForm.cs create mode 100644 src-ref/UI/Combat/View/CombatSelectBuildArea.cs create mode 100644 src-ref/UI/Combat/View/CombatSelectForm.cs create mode 100644 src-ref/UI/Combat/View/CombatSelectFuncArea.cs create mode 100644 src-ref/UI/Combat/View/TowerSelectItem.cs create mode 100644 src-ref/UI/CommonButton.cs create mode 100644 src-ref/UI/DialogParams.cs create mode 100644 src-ref/UI/Game/Context/CombineAreaContext.cs create mode 100644 src-ref/UI/Game/Context/CompAreaContext.cs create mode 100644 src-ref/UI/Game/Context/EventFormContext.cs create mode 100644 src-ref/UI/Game/Context/EventOptionItemContext.cs create mode 100644 src-ref/UI/Game/Context/MainFormContext.cs create mode 100644 src-ref/UI/Game/Context/NodeItemContext.cs create mode 100644 src-ref/UI/Game/Context/NodeMapFormContext.cs create mode 100644 src-ref/UI/Game/Context/ParticipantAreaContext.cs create mode 100644 src-ref/UI/Game/Context/RepoFormContext.cs create mode 100644 src-ref/UI/Game/Context/RepoFormState.cs create mode 100644 src-ref/UI/Game/Context/RepoItemClickActionType.cs create mode 100644 src-ref/UI/Game/Context/RepoItemContext.cs create mode 100644 src-ref/UI/Game/Context/SellAreaContext.cs create mode 100644 src-ref/UI/Game/Context/TowerRepoItemContext.cs create mode 100644 src-ref/UI/Game/ContextBuilder/NodeMapFormController.ContextBuilder.cs create mode 100644 src-ref/UI/Game/ContextBuilder/RepoFormController.ContextBuilder.cs create mode 100644 src-ref/UI/Game/ContextBuilder/RewardSelectFormController.ContextBuilder.cs create mode 100644 src-ref/UI/Game/Controller/EventFormController.cs create mode 100644 src-ref/UI/Game/Controller/MainFormController.cs create mode 100644 src-ref/UI/Game/Controller/NodeMapFormController.cs create mode 100644 src-ref/UI/Game/Controller/RepoFormController.cs create mode 100644 src-ref/UI/Game/RawData/EventFormRawData.cs create mode 100644 src-ref/UI/Game/RawData/NodeMapFormRawData.cs create mode 100644 src-ref/UI/Game/RawData/RepoFormRawData.cs create mode 100644 src-ref/UI/Game/RepoParticipantAssignDialogUtility.cs create mode 100644 src-ref/UI/Game/UseCase/EventFormUseCase.cs create mode 100644 src-ref/UI/Game/UseCase/NodeMapFormUseCase.cs create mode 100644 src-ref/UI/Game/UseCase/RepoFormUseCase.cs create mode 100644 src-ref/UI/Game/View/CombineArea.cs create mode 100644 src-ref/UI/Game/View/CombineSlotItem.cs create mode 100644 src-ref/UI/Game/View/CompArea.cs create mode 100644 src-ref/UI/Game/View/EventForm.cs create mode 100644 src-ref/UI/Game/View/EventOptionItem.cs create mode 100644 src-ref/UI/Game/View/IRepoDragItemView.cs create mode 100644 src-ref/UI/Game/View/MainForm.cs create mode 100644 src-ref/UI/Game/View/NodeItem.cs create mode 100644 src-ref/UI/Game/View/NodeMapForm.cs create mode 100644 src-ref/UI/Game/View/ParticipantArea.cs create mode 100644 src-ref/UI/Game/View/RepoForm.cs create mode 100644 src-ref/UI/Game/View/RepoItem.cs create mode 100644 src-ref/UI/Game/View/SellArea.cs create mode 100644 src-ref/UI/Game/View/TowerRepoItem.cs create mode 100644 src-ref/UI/General/Context/DialogFormContext.cs create mode 100644 src-ref/UI/General/Context/IconAreaContext.cs create mode 100644 src-ref/UI/General/Context/ItemDescFormContext.cs create mode 100644 src-ref/UI/General/Context/RewardItemContext.cs create mode 100644 src-ref/UI/General/Context/RewardSelectFormContext.cs create mode 100644 src-ref/UI/General/Context/TagItemContext.cs create mode 100644 src-ref/UI/General/Context/TowerIconAreaContext.cs create mode 100644 src-ref/UI/General/Controller/DialogFormController.cs create mode 100644 src-ref/UI/General/Controller/ItemDescFormController.cs create mode 100644 src-ref/UI/General/Controller/RewardSelectFormController.cs create mode 100644 src-ref/UI/General/RawData/DialogFormRawData.cs create mode 100644 src-ref/UI/General/RawData/ItemDescFormRawData.cs create mode 100644 src-ref/UI/General/RawData/RewardSelectFormRawData.cs create mode 100644 src-ref/UI/General/RawData/RewardSelectItemRawData.cs create mode 100644 src-ref/UI/General/RawData/RewardSelectItemRawDataBuilder.cs create mode 100644 src-ref/UI/General/UseCase/ItemDescFormUseCase.cs create mode 100644 src-ref/UI/General/UseCase/RewardSelectFormUseCase.cs create mode 100644 src-ref/UI/General/View/DialogForm.cs create mode 100644 src-ref/UI/General/View/IconArea.cs create mode 100644 src-ref/UI/General/View/ItemDescForm.cs create mode 100644 src-ref/UI/General/View/RewardItem.cs create mode 100644 src-ref/UI/General/View/RewardSelectForm.cs create mode 100644 src-ref/UI/General/View/TagItem.cs create mode 100644 src-ref/UI/General/View/TowerIconArea.cs create mode 100644 src-ref/UI/HPBarItem.cs create mode 100644 src-ref/UI/Menu/Context/MenuFormContext.cs create mode 100644 src-ref/UI/Menu/Context/TestMenuFormContext.cs create mode 100644 src-ref/UI/Menu/Controller/MenuFormController.cs create mode 100644 src-ref/UI/Menu/Controller/TestMenuFormController.cs create mode 100644 src-ref/UI/Menu/RawData/MenuFormRawData.cs create mode 100644 src-ref/UI/Menu/UseCase/MenuFormUseCase.cs create mode 100644 src-ref/UI/Menu/View/MenuForm.cs create mode 100644 src-ref/UI/Menu/View/TestMenuForm.cs create mode 100644 src-ref/UI/Shop/Context/GoodsItemContext.cs create mode 100644 src-ref/UI/Shop/Context/ShopFormContext.cs create mode 100644 src-ref/UI/Shop/ContextBuilder/ShopFormController.ContextBuilder.cs create mode 100644 src-ref/UI/Shop/Controller/ShopFormController.cs create mode 100644 src-ref/UI/Shop/RawData/GoodsItemRawData.cs create mode 100644 src-ref/UI/Shop/RawData/ShopFormRawData.cs create mode 100644 src-ref/UI/Shop/UseCase/ShopFormUseCase.cs create mode 100644 src-ref/UI/Shop/View/GoodsItem.cs create mode 100644 src-ref/UI/Shop/View/ShopForm.cs create mode 100644 src-ref/UI/Templates/GameScene/Context/DisplayItemContext.cs create mode 100644 src-ref/UI/Templates/GameScene/Context/DisplayItemInfoFormContext.cs create mode 100644 src-ref/UI/Templates/GameScene/Context/DisplayListAreaContext.cs create mode 100644 src-ref/UI/Templates/GameScene/Context/GoodsItemContext.cs create mode 100644 src-ref/UI/Templates/GameScene/Context/HudFormContext.cs create mode 100644 src-ref/UI/Templates/GameScene/Context/LevelUpFormContext.cs create mode 100644 src-ref/UI/Templates/GameScene/Context/LevelUpRewardItemContext.cs create mode 100644 src-ref/UI/Templates/GameScene/Context/ShopFormContext.cs create mode 100644 src-ref/UI/Templates/GameScene/Controller/DisplayItemInfoFormController.cs create mode 100644 src-ref/UI/Templates/GameScene/Controller/HudFormController.cs create mode 100644 src-ref/UI/Templates/GameScene/Controller/LevelUpFormController.cs create mode 100644 src-ref/UI/Templates/GameScene/Controller/ShopFormController.cs create mode 100644 src-ref/UI/Templates/GameScene/RawData/DisplayItemInfoFormRawData.cs create mode 100644 src-ref/UI/Templates/GameScene/RawData/LevelUpFormRawData.cs create mode 100644 src-ref/UI/Templates/GameScene/RawData/ShopFormRawData.cs create mode 100644 src-ref/UI/Templates/GameScene/UseCase/DisplayItemInfoFormUseCase.cs create mode 100644 src-ref/UI/Templates/GameScene/UseCase/LevelUpFormUseCase.cs create mode 100644 src-ref/UI/Templates/GameScene/UseCase/ShopFormUseCase.cs create mode 100644 src-ref/UI/Templates/GameScene/View/DisplayItem.cs create mode 100644 src-ref/UI/Templates/GameScene/View/DisplayItemInfoForm.cs create mode 100644 src-ref/UI/Templates/GameScene/View/DisplayItemObject.cs create mode 100644 src-ref/UI/Templates/GameScene/View/DisplayListArea.cs create mode 100644 src-ref/UI/Templates/GameScene/View/HudForm.cs create mode 100644 src-ref/UI/Templates/GameScene/View/LevelUpForm.cs create mode 100644 src-ref/UI/Templates/GameScene/View/LevelUpRewardItem.cs create mode 100644 src-ref/UI/Templates/GameScene/View/ShopForm.cs create mode 100644 src-ref/UI/Templates/MenuScene/Context/RoleItemContext.cs create mode 100644 src-ref/UI/Templates/MenuScene/Context/RolePropertyAreaContext.cs create mode 100644 src-ref/UI/Templates/MenuScene/Context/SelectRoleFormContext.cs create mode 100644 src-ref/UI/Templates/MenuScene/Context/StartMenuFormContext.cs create mode 100644 src-ref/UI/Templates/MenuScene/Controller/SelectRoleFormController.cs create mode 100644 src-ref/UI/Templates/MenuScene/Controller/StartMenuFormController.cs create mode 100644 src-ref/UI/Templates/MenuScene/RawData/SelectRoleFormRawData.cs create mode 100644 src-ref/UI/Templates/MenuScene/RawData/StartMenuFormRawData.cs create mode 100644 src-ref/UI/Templates/MenuScene/UseCase/SelectRoleFormUseCase.cs create mode 100644 src-ref/UI/Templates/MenuScene/View/RoleItem.cs create mode 100644 src-ref/UI/Templates/MenuScene/View/RolePropertyArea.cs create mode 100644 src-ref/UI/Templates/MenuScene/View/SelectRoleForm.cs create mode 100644 src-ref/UI/Templates/MenuScene/View/SettingForm.cs create mode 100644 src-ref/UI/Templates/MenuScene/View/StartMenuForm.cs create mode 100644 src-ref/UI/UpdateResourceForm.cs create mode 100644 src-ref/Utility/AssetUtility.cs create mode 100644 src-ref/Utility/EnumUtility.cs create mode 100644 src-ref/Utility/IconColorGenerator.cs create mode 100644 src-ref/Utility/InventoryCloneUtility.cs create mode 100644 src-ref/Utility/InventoryParticipantUtility.cs create mode 100644 src-ref/Utility/InventorySeedUtility.cs create mode 100644 src-ref/Utility/ItemDescUtility.cs create mode 100644 src-ref/Utility/JsonNetUtility.cs create mode 100644 src-ref/Utility/ShopPriceRuleService.cs create mode 100644 src-ref/Utility/TowerComposedIconCacheUtility.cs create mode 100644 src-ref/Utility/TowerIconComposeUtility.cs create mode 100644 src-ref/Utility/WebUtility.cs create mode 100644 src/Class1.cs create mode 100644 src/Geometry-Tower-Defense-Base.csproj create mode 100644 src/Geometry-Tower-Defense-Base.sln create mode 100644 src/GeometryTD.Domain/Definition/DataStruct/BuildInfo.cs create mode 100644 src/GeometryTD.Domain/Definition/DataStruct/ImpactData.cs create mode 100644 src/GeometryTD.Domain/Definition/DataStruct/RunItemState.cs create mode 100644 src/GeometryTD.Domain/Definition/DataStruct/RunNodeCompletionSnapshot.cs create mode 100644 src/GeometryTD.Domain/Definition/DataStruct/TowerCompItemData.cs create mode 100644 src/GeometryTD.Domain/Definition/DataStruct/VersionInfo.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/AttackMethodType.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/AttackPropertyType.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/CampType.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/CombatSelectActionType.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/EntryType.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/EventEffectType.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/EventRequirementType.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/InventoryTagSourceType.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/LevelThemeType.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/LevelVictoryType.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/PhaseEndType.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/ProcedureMainCombatEntryBlockReason.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/ProcedureMainFlowPhase.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/ProcedureMainRunAdvanceResult.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/ProcedureMainRunCompletionResult.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/RarityType.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/RelationType.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/RepoItemClickActionType.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/RunNodeCompletionStatus.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/RunNodeStatus.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/RunNodeType.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/SceneType.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/TagCategory.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/TagTriggerPhase.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/TagType.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/TowerCompSlotType.cs create mode 100644 src/GeometryTD.Domain/Definition/Enum/UIFormType.cs create mode 100644 src/GeometryTD.Domain/Event/Combat/CombatBaseHpChangedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Combat/CombatCoinChangedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Combat/CombatDebugFailEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Combat/CombatEndEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Combat/CombatEnemyHpRateChangedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Combat/CombatFinishReturnEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Combat/CombatPauseEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Combat/CombatProcessEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Combat/CombatSelectItemClickEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/EventForm/EventOptionItemSelectedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Game/NodeCompleteEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Game/NodeEnterEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Game/NodeMapNodeClickEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Game/NodeMapNodeEnterRequestedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Game/TestMenuNodeClickEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/General/RewardSelectGiveUpEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/General/RewardSelectItemSelectedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/General/RewardSelectRefreshEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/MainForm/RepoButtonClickedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/MainForm/ReturnButtonClickedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Menu/MenuExitRequestedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Menu/MenuSettingsRequestedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Menu/MenuStartRequestedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/RepoForm/CombineSlotClickedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/RepoForm/RepoCombineRequestedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/RepoForm/RepoFormReturnEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/RepoForm/RepoItemClickedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/RepoForm/RepoItemDragEndedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/RepoForm/RepoParticipantAssignRequestedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/RepoForm/RepoSellCancelRequestedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/RepoForm/RepoSellConfirmRequestedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/RepoForm/RepoSellModeToggleRequestedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Shop/ShopExitRequestedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Shop/ShopInventoryRequestedEventArgs.cs create mode 100644 src/GeometryTD.Domain/Event/Shop/ShopPurchaseRequestedEventArgs.cs create mode 100644 src/GeometryTD.Domain/GeometryTD.Domain.csproj create mode 100644 src/GeometryTD.Domain/UI/Base/IUIFormController.cs create mode 100644 src/GeometryTD.Domain/UI/Base/IUIUseCase.cs create mode 100644 src/GeometryTD.Domain/UI/Base/UIContext.cs create mode 100644 src/GeometryTD.Infrastructure/GeometryTD.Infrastructure.csproj create mode 100644 src/GeometryTD.Presentation/GeometryTD.Presentation.csproj create mode 100644 src/GeometryTD.slnx create mode 100644 src/Libraries/GameFramework.dll create mode 100644 src/Libraries/GameFramework.dll.meta create mode 100644 src/Libraries/GameFramework.xml create mode 100644 src/Libraries/GameFramework.xml.meta create mode 100644 src/Libraries/ICSharpCode.SharpZipLib.dll create mode 100644 src/Libraries/ICSharpCode.SharpZipLib.dll.meta create mode 100644 src/Libraries/link.xml create mode 100644 src/Libraries/link.xml.meta diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce57316 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..cc23663 --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..66d88f8 --- /dev/null +++ b/TODO.md @@ -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 中正常运行 diff --git a/docs/CombatNodeArchitecture.md b/docs/CombatNodeArchitecture.md new file mode 100644 index 0000000..b3b0136 --- /dev/null +++ b/docs/CombatNodeArchitecture.md @@ -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` 不变 | + diff --git a/docs/CombatReward爆率表.xlsx b/docs/CombatReward爆率表.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..c3ca833934cedb98f7b4e4331bc236a98fa254ae GIT binary patch literal 26333 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 literal 0 HcmV?d00001 diff --git a/docs/GameDesign.md b/docs/GameDesign.md new file mode 100644 index 0000000..305c43c --- /dev/null +++ b/docs/GameDesign.md @@ -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` 耐久失效并拦截参战 / 战斗入口;连续属性衰减、自动销毁与维修系统保留到后续阶段 diff --git a/docs/LayeredArchitectureDesign.md b/docs/LayeredArchitectureDesign.md new file mode 100644 index 0000000..b458db6 --- /dev/null +++ b/docs/LayeredArchitectureDesign.md @@ -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` | 继承 `FsmState` 构建状态机 | +| **ReferencePool** | `ReferencePool` | `ReferencePool.Acquire()` / `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(this, + new CombatLoadingState(), + new CombatRunningPhaseState(), + new CombatWaitingForPhaseEndState(), + new CombatSettlementState()); + } +} + +// L0: 具体状态继承 FsmState +public class CombatRunningPhaseState : FsmState +{ + 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("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(); + _poolManager = GameEntry.GetComponent(); + _fsmManager = GameEntry.GetComponent(); + + // 初始化 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 + + + + netstandard2.1 + 9.0 + enable + GeometryTD.Domain + + + + ..\..\Assets\GameFramework\Libraries\GameFramework.dll + + + +``` + +### 9.2 L1 项目文件 + +```xml + + + + netstandard2.1 + enable + + + + + + + Unity\Editor\Data\Managed\UnityEngine\UnityEngine.dll + + + + + + +``` + +### 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 | diff --git a/docs/MVP-Scope.md b/docs/MVP-Scope.md new file mode 100644 index 0000000..0c69d6e --- /dev/null +++ b/docs/MVP-Scope.md @@ -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. 无严重崩溃或逻辑错误 + +只要流程跑通,即视为完成。 + +--- + +## 七、本阶段不评估指标 + +* 不评估留存 +* 不评估爽感强度 +* 不评估平衡性 +* 不评估变现能力 diff --git a/docs/MapEntityArchitecture.md b/docs/MapEntityArchitecture.md new file mode 100644 index 0000000..adcc0fc --- /dev/null +++ b/docs/MapEntityArchitecture.md @@ -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` 是否通过? diff --git a/docs/TagSystemDesign.md b/docs/TagSystemDesign.md new file mode 100644 index 0000000..455e98f --- /dev/null +++ b/docs/TagSystemDesign.md @@ -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 元数据扩展表 diff --git a/docs/TagSystemRoadmap.md b/docs/TagSystemRoadmap.md new file mode 100644 index 0000000..4461222 --- /dev/null +++ b/docs/TagSystemRoadmap.md @@ -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` 主干 diff --git a/docs/UI-5层架构设计规范.md b/docs/UI-5层架构设计规范.md new file mode 100644 index 0000000..cc5ea6e --- /dev/null +++ b/docs/UI-5层架构设计规范.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` +- 命名:`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//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,不属于五层完整样板 diff --git a/src-ref/Base/GameEntry.Builtin.cs b/src-ref/Base/GameEntry.Builtin.cs new file mode 100644 index 0000000..b9ace14 --- /dev/null +++ b/src-ref/Base/GameEntry.Builtin.cs @@ -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; + +/// +/// 游戏入口。 +/// +public partial class GameEntry +{ + /// + /// 获取游戏基础组件。 + /// + public static BaseComponent Base { get; private set; } + + /// + /// 获取配置组件。 + /// + public static ConfigComponent Config { get; private set; } + + /// + /// 获取数据结点组件。 + /// + public static DataNodeComponent DataNode { get; private set; } + + /// + /// 获取数据表组件。 + /// + public static DataTableComponent DataTable { get; private set; } + + /// + /// 获取调试组件。 + /// + public static DebuggerComponent Debugger { get; private set; } + + /// + /// 获取下载组件。 + /// + public static DownloadComponent Download { get; private set; } + + /// + /// 获取实体组件。 + /// + public static EntityComponent Entity { get; private set; } + + /// + /// 获取事件组件。 + /// + public static EventComponent Event { get; private set; } + + /// + /// 获取文件系统组件。 + /// + public static FileSystemComponent FileSystem { get; private set; } + + /// + /// 获取有限状态机组件。 + /// + public static FsmComponent Fsm { get; private set; } + + /// + /// 获取本地化组件。 + /// + public static LocalizationComponent Localization { get; private set; } + + /// + /// 获取网络组件。 + /// + public static NetworkComponent Network { get; private set; } + + /// + /// 获取对象池组件。 + /// + public static ObjectPoolComponent ObjectPool { get; private set; } + + /// + /// 获取流程组件。 + /// + public static ProcedureComponent Procedure { get; private set; } + + /// + /// 获取资源组件。 + /// + public static ResourceComponent Resource { get; private set; } + + /// + /// 获取场景组件。 + /// + public static SceneComponent Scene { get; private set; } + + /// + /// 获取配置组件。 + /// + public static SettingComponent Setting { get; private set; } + + /// + /// 获取声音组件。 + /// + public static SoundComponent Sound { get; private set; } + + /// + /// 获取界面组件。 + /// + public static UIComponent UI { get; private set; } + + /// + /// 获取网络组件。 + /// + public static WebRequestComponent WebRequest { get; private set; } + + private static void InitBuiltinComponents() + { + Base = UnityGameFramework.Runtime.GameEntry.GetComponent(); + Config = UnityGameFramework.Runtime.GameEntry.GetComponent(); + DataNode = UnityGameFramework.Runtime.GameEntry.GetComponent(); + DataTable = UnityGameFramework.Runtime.GameEntry.GetComponent(); + Debugger = UnityGameFramework.Runtime.GameEntry.GetComponent(); + Download = UnityGameFramework.Runtime.GameEntry.GetComponent(); + Entity = UnityGameFramework.Runtime.GameEntry.GetComponent(); + Event = UnityGameFramework.Runtime.GameEntry.GetComponent(); + FileSystem = UnityGameFramework.Runtime.GameEntry.GetComponent(); + Fsm = UnityGameFramework.Runtime.GameEntry.GetComponent(); + Localization = UnityGameFramework.Runtime.GameEntry.GetComponent(); + Network = UnityGameFramework.Runtime.GameEntry.GetComponent(); + ObjectPool = UnityGameFramework.Runtime.GameEntry.GetComponent(); + Procedure = UnityGameFramework.Runtime.GameEntry.GetComponent(); + Resource = UnityGameFramework.Runtime.GameEntry.GetComponent(); + Scene = UnityGameFramework.Runtime.GameEntry.GetComponent(); + Setting = UnityGameFramework.Runtime.GameEntry.GetComponent(); + Sound = UnityGameFramework.Runtime.GameEntry.GetComponent(); + UI = UnityGameFramework.Runtime.GameEntry.GetComponent(); + WebRequest = UnityGameFramework.Runtime.GameEntry.GetComponent(); + } +} \ No newline at end of file diff --git a/src-ref/Base/GameEntry.Custom.cs b/src-ref/Base/GameEntry.Custom.cs new file mode 100644 index 0000000..f107607 --- /dev/null +++ b/src-ref/Base/GameEntry.Custom.cs @@ -0,0 +1,45 @@ +using CustomComponent; +using GeometryTD.CustomComponent; + +/// +/// 游戏入口。 +/// +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(); + PlayerInventory = UnityGameFramework.Runtime.GameEntry.GetComponent(); + HPBar = UnityGameFramework.Runtime.GameEntry.GetComponent(); + UIRouter = UnityGameFramework.Runtime.GameEntry.GetComponent(); + EventNode = UnityGameFramework.Runtime.GameEntry.GetComponent(); + CombatNode = UnityGameFramework.Runtime.GameEntry.GetComponent(); + ShopNode = UnityGameFramework.Runtime.GameEntry.GetComponent(); + ResolutionAdapter = UnityGameFramework.Runtime.GameEntry.GetComponent(); + SpriteCache = UnityGameFramework.Runtime.GameEntry.GetComponent(); + InventoryGeneration = UnityGameFramework.Runtime.GameEntry.GetComponent(); + TagRegistry = UnityGameFramework.Runtime.GameEntry.GetComponent(); + } +} diff --git a/src-ref/Base/GameEntry.cs b/src-ref/Base/GameEntry.cs new file mode 100644 index 0000000..0f9c33e --- /dev/null +++ b/src-ref/Base/GameEntry.cs @@ -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; + +/// +/// 游戏入口。 +/// +public partial class GameEntry : MonoBehaviour +{ + private void Start() + { + InitBuiltinComponents(); + InitCustomComponents(); + } +} \ No newline at end of file diff --git a/src-ref/Components/BasicBaseComp.cs b/src-ref/Components/BasicBaseComp.cs new file mode 100644 index 0000000..e99d3f3 --- /dev/null +++ b/src-ref/Components/BasicBaseComp.cs @@ -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; + } + } +} diff --git a/src-ref/Components/BasicBearingComp.cs b/src-ref/Components/BasicBearingComp.cs new file mode 100644 index 0000000..da94261 --- /dev/null +++ b/src-ref/Components/BasicBearingComp.cs @@ -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); + } + } +} diff --git a/src-ref/Components/IDamageReceiver.cs b/src-ref/Components/IDamageReceiver.cs new file mode 100644 index 0000000..e5f1e13 --- /dev/null +++ b/src-ref/Components/IDamageReceiver.cs @@ -0,0 +1,9 @@ +using GeometryTD.Definition; + +namespace Components +{ + public interface IDamageReceiver + { + void TakeDamage(AttackPayload attackPayload); + } +} diff --git a/src-ref/Components/InputComponent.cs b/src-ref/Components/InputComponent.cs new file mode 100644 index 0000000..5418928 --- /dev/null +++ b/src-ref/Components/InputComponent.cs @@ -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(); + _direction = new Vector3(rawDir.x, 0, rawDir.y); + } + + public void SetListening(bool isListening) + { + _isListening = isListening; + if (_isListening) + { + _inputActions.Enable(); + } + else + { + _inputActions.Disable(); + } + } + } +} \ No newline at end of file diff --git a/src-ref/Components/MovementComponent.cs b/src-ref/Components/MovementComponent.cs new file mode 100644 index 0000000..44a4297 --- /dev/null +++ b/src-ref/Components/MovementComponent.cs @@ -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); + } +} diff --git a/src-ref/Components/ShooterBullet.cs b/src-ref/Components/ShooterBullet.cs new file mode 100644 index 0000000..63cdecb --- /dev/null +++ b/src-ref/Components/ShooterBullet.cs @@ -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(); + 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); + } + } +} diff --git a/src-ref/Components/ShooterMuzzleComp.cs b/src-ref/Components/ShooterMuzzleComp.cs new file mode 100644 index 0000000..efd23c2 --- /dev/null +++ b/src-ref/Components/ShooterMuzzleComp.cs @@ -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(); + + 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(); + } + + 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[] 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(); + return towerEntity != null ? towerEntity.Id : 0; + } + } +} diff --git a/src-ref/Components/TowerController.cs b/src-ref/Components/TowerController.cs new file mode 100644 index 0000000..99a9803 --- /dev/null +++ b/src-ref/Components/TowerController.cs @@ -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(); + + 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(); + _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(); + } + + if (_bearingComp == null) + { + _bearingComp = GetComponent(); + } + + if (_baseComp == null) + { + _baseComp = GetComponent(); + } + + 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[] 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(); + } + + 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(); + if (_attackRangeRenderer == null) + { + _attackRangeRenderer = indicatorTransform.gameObject.AddComponent(); + } + } + + _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)); + } + } + } +} diff --git a/src-ref/CustomComponent/BuiltinDataComponent.cs b/src-ref/CustomComponent/BuiltinDataComponent.cs new file mode 100644 index 0000000..80f45bf --- /dev/null +++ b/src-ref/CustomComponent/BuiltinDataComponent.cs @@ -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(_buildInfoTextAsset.text); + if (_buildInfo == null) + { + Log.Warning("Parse build info failure."); + return; + } + } + } +} \ No newline at end of file diff --git a/src-ref/CustomComponent/CombatNode/CombatNodeComponent.cs b/src-ref/CustomComponent/CombatNode/CombatNodeComponent.cs new file mode 100644 index 0000000..b8b2b21 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatNodeComponent.cs @@ -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 +{ + /// + /// 鎴樻枟鑺傜偣缁勪欢 + /// + public class CombatNodeComponent : GameFrameworkComponent + { + // Level.Id => Level + private readonly Dictionary _levelsById = new(); + + // LevelId => LevelPhases + private readonly Dictionary> _phasesByLevelId = new(); + + // LevelPhase.Id => LevelSpawnEntries + private readonly Dictionary> _spawnEntriesByPhaseId = new(); + private readonly Dictionary> _selectedSpawnEntriesByPhaseId = new(); + private readonly List _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 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 dtLevel = GameEntry.DataTable.GetDataTable(); + IDataTable dtLevelPhase = GameEntry.DataTable.GetDataTable(); + IDataTable dtSpawnEntry = GameEntry.DataTable.GetDataTable(); + 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(); + _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 phases)) + { + phases = new List(); + _phasesByLevelId[levelId] = phases; + } + + phases.Add(phase); + _spawnEntriesByPhaseId[phase.Id] = new List(); + } + + 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 entries)) + { + entries = new List(); + _spawnEntriesByPhaseId[phaseId] = entries; + } + + entries.Add(spawnEntry); + } + + foreach (List phaseList in _phasesByLevelId.Values) + { + phaseList.Sort((left, right) => left.Id.CompareTo(right.Id)); + } + + foreach (List 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 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 entries) && + entries != null) + { + _selectedSpawnEntriesByPhaseId[phase.Id] = entries; + } + else + { + _selectedSpawnEntriesByPhaseId[phase.Id] = new List(); + } + } + + 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 phases in _phasesByLevelId.Values) + { + count += phases.Count; + } + + return count; + } + + private int CountEntries() + { + int count = 0; + foreach (List list in _spawnEntriesByPhaseId.Values) + { + count += list.Count; + } + + return count; + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatEventBridge.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatEventBridge.cs new file mode 100644 index 0000000..6ec91d0 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatEventBridge.cs @@ -0,0 +1,151 @@ +using System; +using GameFramework.Event; +using UnityGameFramework.Runtime; + +namespace GeometryTD.CustomComponent +{ + internal sealed class CombatEventBridge + { + private Action _onShowEntitySuccess; + private Action _onShowEntityFailure; + private Action _onHideEntityComplete; + private Action _onOpenUIFormSuccess; + private Action _onOpenUIFormFailure; + private Action _onCloseUIFormComplete; + + private bool _isEntityEventSubscribed; + private bool _isUIEventSubscribed; + + public void Bind( + Action onShowEntitySuccess, + Action onShowEntityFailure, + Action onHideEntityComplete, + Action onOpenUIFormSuccess, + Action onOpenUIFormFailure, + Action 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); + } + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs new file mode 100644 index 0000000..7d3dc7b --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs @@ -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; + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatRunResourceStore.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatRunResourceStore.cs new file mode 100644 index 0000000..2af5d12 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatRunResourceStore.cs @@ -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 _buildTowerStatsSnapshot = new(); + private readonly List _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 GetParticipantTowerSnapshot() + { + List snapshot = new List(_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 GetParticipantTowerInstanceIdSnapshot() + { + List snapshot = new List(_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 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; + } + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs new file mode 100644 index 0000000..1ae7f10 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs @@ -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 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 phases, + IReadOnlyDictionary> 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 + } +} \ No newline at end of file diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerCoordinator.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerCoordinator.cs new file mode 100644 index 0000000..b20740e --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerCoordinator.cs @@ -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 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); + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntime.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntime.cs new file mode 100644 index 0000000..159bc11 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntime.cs @@ -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 PhaseBuffer { get; } = new(); + public Dictionary> 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 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; } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatSettlementContext.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatSettlementContext.cs new file mode 100644 index 0000000..068362f --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatSettlementContext.cs @@ -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 TargetTowerInstanceIds { get; } = new(); + public float EnduranceLossPerComponent; + public int AffectedTowerCount; + } + + public sealed class CombatSettlementSummary + { + public int DefeatedEnemyCount; + public int GainedGold; + public BackpackInventoryData RewardInventory; + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatSettlementService.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatSettlementService.cs new file mode 100644 index 0000000..a3f64de --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatSettlementService.cs @@ -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 onRewardSelected, + Action onGiveUp) + { + if (settlementContext == null || rewardSelectFormUseCase == null) + { + return false; + } + + IReadOnlyList 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); + } + + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatFailedState.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatFailedState.cs new file mode 100644 index 0000000..a82a73a --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatFailedState.cs @@ -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(); + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatFinishFormState.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatFinishFormState.cs new file mode 100644 index 0000000..1d1c375 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatFinishFormState.cs @@ -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)); + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatLoadingState.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatLoadingState.cs new file mode 100644 index 0000000..330cc10 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatLoadingState.cs @@ -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 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); + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRewardSelectionState.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRewardSelectionState.cs new file mode 100644 index 0000000..92987b5 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRewardSelectionState.cs @@ -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(); + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRunningPhaseState.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRunningPhaseState.cs new file mode 100644 index 0000000..2a188f3 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRunningPhaseState.cs @@ -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 _spawnEntries; + + public CombatRunningPhaseState( + CombatSchedulerRuntime runtime, + CombatSchedulerCoordinator coordinator, + DRLevelPhase phase, + IReadOnlyList 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(); + } + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatSettlementState.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatSettlementState.cs new file mode 100644 index 0000000..09656f9 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatSettlementState.cs @@ -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)); + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatStateBase.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatStateBase.cs new file mode 100644 index 0000000..54248c7 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatStateBase.cs @@ -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() + { + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForPhaseEndState.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForPhaseEndState.cs new file mode 100644 index 0000000..440fcc0 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForPhaseEndState.cs @@ -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(); + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForReturnState.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForReturnState.cs new file mode 100644 index 0000000..fa84c0a --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForReturnState.cs @@ -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); + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropContext.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropContext.cs new file mode 100644 index 0000000..57e1289 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropContext.cs @@ -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; } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResult.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResult.cs new file mode 100644 index 0000000..50f5cdd --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResult.cs @@ -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; } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/ICombatSchedulerPort.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/ICombatSchedulerPort.cs new file mode 100644 index 0000000..52bd1ab --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/ICombatSchedulerPort.cs @@ -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(); + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/BossDeadPhaseEndCondition.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/BossDeadPhaseEndCondition.cs new file mode 100644 index 0000000..f4990d7 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/BossDeadPhaseEndCondition.cs @@ -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; + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/EnemiesClearedPhaseEndCondition.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/EnemiesClearedPhaseEndCondition.cs new file mode 100644 index 0000000..aeeb420 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/EnemiesClearedPhaseEndCondition.cs @@ -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; + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/IPhaseEndCondition.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/IPhaseEndCondition.cs new file mode 100644 index 0000000..eeba254 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/IPhaseEndCondition.cs @@ -0,0 +1,11 @@ +using GeometryTD.Definition; + +namespace GeometryTD.CustomComponent +{ + internal interface IPhaseEndCondition + { + PhaseEndType EndType { get; } + + bool ShouldExit(in PhaseEndConditionContext context); + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/NonePhaseEndCondition.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/NonePhaseEndCondition.cs new file mode 100644 index 0000000..3c05abf --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/NonePhaseEndCondition.cs @@ -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; + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/PhaseEndConditionContext.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/PhaseEndConditionContext.cs new file mode 100644 index 0000000..49dc8d1 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/PhaseEndConditionContext.cs @@ -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; } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/TimeElapsedPhaseEndCondition.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/TimeElapsedPhaseEndCondition.cs new file mode 100644 index 0000000..165c0e5 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/TimeElapsedPhaseEndCondition.cs @@ -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; + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseLoopRuntime.cs b/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseLoopRuntime.cs new file mode 100644 index 0000000..c61024b --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatScheduler/PhaseLoopRuntime.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using GeometryTD.DataTable; + +namespace GeometryTD.CustomComponent +{ + internal sealed class PhaseLoopRuntime + { + private readonly List _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 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; + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatSettlementCalculator.cs b/src-ref/CustomComponent/CombatNode/CombatSettlementCalculator.cs new file mode 100644 index 0000000..0fa0fc3 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatSettlementCalculator.cs @@ -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 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); + } + } + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/CombatSettlementCommitter.cs b/src-ref/CustomComponent/CombatNode/CombatSettlementCommitter.cs new file mode 100644 index 0000000..f4897c7 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/CombatSettlementCommitter.cs @@ -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; + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/EnemyManager/EnemyConfigProvider.cs b/src-ref/CustomComponent/CombatNode/EnemyManager/EnemyConfigProvider.cs new file mode 100644 index 0000000..d2c5223 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/EnemyManager/EnemyConfigProvider.cs @@ -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; + private bool _enemyConfigMissingLogged; + + public void Reset() + { + _drEnemy = null; + _enemyConfigMissingLogged = false; + } + + public DREnemy GetEnemyConfig(int enemyId) + { + if (_drEnemy == null) + { + _drEnemy = GameEntry.DataTable.GetDataTable(); + 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); + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/EnemyManager/EnemyLifecycleTracker.cs b/src-ref/CustomComponent/CombatNode/EnemyManager/EnemyLifecycleTracker.cs new file mode 100644 index 0000000..9408783 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/EnemyManager/EnemyLifecycleTracker.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using GeometryTD.DataTable; +using UnityEngine; + +namespace GeometryTD.CustomComponent +{ + internal sealed class EnemyLifecycleTracker + { + private readonly HashSet _aliveBossEntityIds = new(); + private readonly HashSet _trackedEnemyEntityIds = new(); + private readonly Dictionary _trackedEnemyConfigByEntityId = new(); + private readonly Dictionary _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 buffer) + { + if (buffer == null) + { + return; + } + + buffer.Clear(); + foreach (int trackedEnemyEntityId in _trackedEnemyEntityIds) + { + buffer.Add(trackedEnemyEntityId); + } + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs b/src-ref/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs new file mode 100644 index 0000000..b8bb859 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs @@ -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 _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 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 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); + } + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/EnemyManager/EnemySpawnDirector.cs b/src-ref/CustomComponent/CombatNode/EnemyManager/EnemySpawnDirector.cs new file mode 100644 index 0000000..6df3c02 --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/EnemyManager/EnemySpawnDirector.cs @@ -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 _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 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 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 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 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 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; + } + } +} diff --git a/src-ref/CustomComponent/CombatNode/EnemyManager/EnemySpawnPathResolver.cs b/src-ref/CustomComponent/CombatNode/EnemyManager/EnemySpawnPathResolver.cs new file mode 100644 index 0000000..e8696df --- /dev/null +++ b/src-ref/CustomComponent/CombatNode/EnemyManager/EnemySpawnPathResolver.cs @@ -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 _spawners = new(); + private readonly Dictionary _spawnerByOrder = new(); + private readonly List _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 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; + } + } +} diff --git a/src-ref/CustomComponent/EventNodeComponent.cs b/src-ref/CustomComponent/EventNodeComponent.cs new file mode 100644 index 0000000..b2b8f64 --- /dev/null +++ b/src-ref/CustomComponent/EventNodeComponent.cs @@ -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 _eventItems = new List(); + + private EventFormUseCase _eventFormUseCase; + private bool _initialized; + + public void OnInit() + { + _eventItems.Clear(); + + IDataTable dtEvent = GameEntry.DataTable.GetDataTable(); + 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(); + } + + try + { + JArray array = JArray.Parse(optionsRaw); + List options = new List(array.Count); + + for (int i = 0; i < array.Count; i++) + { + if (!(array[i] is JObject optionObj)) + { + continue; + } + + string optionText = optionObj.Value("optionText") ?? string.Empty; + float probability = optionObj.Value("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(); + } + } + + private static EventRequirementBase[] ParseRequirements(JArray requirementsArray) + { + if (requirementsArray == null || requirementsArray.Count == 0) + { + return System.Array.Empty(); + } + + List requirements = new List(requirementsArray.Count); + for (int i = 0; i < requirementsArray.Count; i++) + { + if (!(requirementsArray[i] is JObject reqObj)) + { + continue; + } + + string type = reqObj.Value("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(); + } + + List effects = new List(effectsArray.Count); + for (int i = 0; i < effectsArray.Count; i++) + { + if (!(effectsArray[i] is JObject effectObj)) + { + continue; + } + + string type = effectObj.Value("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; + } + } + } +} diff --git a/src-ref/CustomComponent/HPBar/HPBarComponent.cs b/src-ref/CustomComponent/HPBar/HPBarComponent.cs new file mode 100644 index 0000000..25c4208 --- /dev/null +++ b/src-ref/CustomComponent/HPBar/HPBarComponent.cs @@ -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 _hpBarItemObjectPool = null; + private List _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(); + _hpBarItemObjectPool = + GameEntry.ObjectPool.CreateSingleSpawnObjectPool("HPBarItem", _instancePoolCapacity); + _activeHPBarItems = new List(); + } + + 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.SetParent(_hpBarInstanceRoot); + transform.localScale = Vector3.one; + _hpBarItemObjectPool.Register(HPBarItemObject.Create(hpBarItem), true); + } + + return hpBarItem; + } + } +} \ No newline at end of file diff --git a/src-ref/CustomComponent/InventoryGeneration/DropPoolRoller.cs b/src-ref/CustomComponent/InventoryGeneration/DropPoolRoller.cs new file mode 100644 index 0000000..285ff87 --- /dev/null +++ b/src-ref/CustomComponent/InventoryGeneration/DropPoolRoller.cs @@ -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 _eligibleRowBuffer = new(); + private readonly float[] _rarityWeightBuffer = new float[OrderedRarities.Length]; + private readonly IDataTable _dropPoolTable; + + public DropPoolRoller(IDataTable 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; + } + } + } +} diff --git a/src-ref/CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs b/src-ref/CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs new file mode 100644 index 0000000..0c08fce --- /dev/null +++ b/src-ref/CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs @@ -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 _shopPriceRows = new(); + private IDataTable _shopPriceTable; + private IDataTable _dropPoolTable; + private IDataTable _muzzleCompTable; + private IDataTable _bearingCompTable; + private IDataTable _baseCompTable; + private ShopGoodsBuilder _shopGoodsBuilder; + private DropPoolRoller _dropPoolRoller; + private RewardCandidateBuilder _rewardCandidateBuilder; + private OutGameDropItemBuilder _outGameDropItemBuilder; + + public List 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 BuildRewardCandidates( + int displayPhaseIndex, + LevelThemeType themeType, + int candidateCount, + int runSeed, + int sequenceIndex, + ref int nextRewardOrdinal) + { + RewardCandidateBuilder rewardCandidateBuilder = EnsureRewardCandidateBuilder(); + int rewardOrdinal = nextRewardOrdinal; + IReadOnlyList candidates = rewardCandidateBuilder.BuildCandidates( + displayPhaseIndex, + themeType, + candidateCount, + CreateNextRewardRandomContext, + BuildRewardCandidateItem); + nextRewardOrdinal = rewardOrdinal; + return candidates; + + InventoryGenerationRandomContext CreateNextRewardRandomContext() + { + return new InventoryGenerationRandomContext( + runSeed, + sequenceIndex, + InventoryTagSourceType.Reward, + rewardOrdinal++); + } + } + + public IReadOnlyList 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 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(); + } + + 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 result = new List(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(); + 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(); + 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(); + _bearingCompTable ??= GameEntry.DataTable.GetDataTable(); + _baseCompTable ??= GameEntry.DataTable.GetDataTable(); + + 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; + } + } + } +} diff --git a/src-ref/CustomComponent/InventoryGeneration/InventoryGenerationRandomContext.cs b/src-ref/CustomComponent/InventoryGeneration/InventoryGenerationRandomContext.cs new file mode 100644 index 0000000..3a9e460 --- /dev/null +++ b/src-ref/CustomComponent/InventoryGeneration/InventoryGenerationRandomContext.cs @@ -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; + } + } + } +} diff --git a/src-ref/CustomComponent/InventoryGeneration/OutGameDropRuleService.cs b/src-ref/CustomComponent/InventoryGeneration/OutGameDropRuleService.cs new file mode 100644 index 0000000..609564e --- /dev/null +++ b/src-ref/CustomComponent/InventoryGeneration/OutGameDropRuleService.cs @@ -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; + } + } +} diff --git a/src-ref/CustomComponent/InventoryGeneration/RewardCandidateBuilder.cs b/src-ref/CustomComponent/InventoryGeneration/RewardCandidateBuilder.cs new file mode 100644 index 0000000..6cf542c --- /dev/null +++ b/src-ref/CustomComponent/InventoryGeneration/RewardCandidateBuilder.cs @@ -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 BuildCandidates( + int displayPhaseIndex, + LevelThemeType themeType, + int candidateCount, + Func createRandomContext, + Func buildRewardItem) + { + int resolvedCount = Mathf.Max(0, candidateCount); + if (resolvedCount <= 0) + { + return Array.Empty(); + } + + List candidates = new List(resolvedCount); + HashSet selectedPoolEntryKeys = new HashSet(); + 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; + } + } +} diff --git a/src-ref/CustomComponent/InventoryGeneration/ShopGoodsBuilder.cs b/src-ref/CustomComponent/InventoryGeneration/ShopGoodsBuilder.cs new file mode 100644 index 0000000..f2b15b8 --- /dev/null +++ b/src-ref/CustomComponent/InventoryGeneration/ShopGoodsBuilder.cs @@ -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 _shopPriceRows; + private readonly IDataTable _muzzleCompTable; + private readonly IDataTable _bearingCompTable; + private readonly IDataTable _baseCompTable; + + public ShopGoodsBuilder( + IReadOnlyList shopPriceRows, + IDataTable muzzleCompTable, + IDataTable bearingCompTable, + IDataTable baseCompTable) + { + _shopPriceRows = shopPriceRows; + _muzzleCompTable = muzzleCompTable; + _bearingCompTable = bearingCompTable; + _baseCompTable = baseCompTable; + } + + public List BuildGoods( + int goodsCount, + int runSeed, + int sequenceIndex) + { + if (goodsCount <= 0) + { + return new List(); + } + + List 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(), + 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; + } + } +} diff --git a/src-ref/CustomComponent/PlayerInventory/PlayerInventoryComponent.cs b/src-ref/CustomComponent/PlayerInventory/PlayerInventoryComponent.cs new file mode 100644 index 0000000..254838f --- /dev/null +++ b/src-ref/CustomComponent/PlayerInventory/PlayerInventoryComponent.cs @@ -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 GetParticipantTowerSnapshot() + { + EnsureInitialized(); + + BackpackInventoryData inventory = _queryModel.Inventory; + if (inventory?.ParticipantTowerInstanceIds == null || inventory.ParticipantTowerInstanceIds.Count <= 0) + { + return new List(); + } + + List result = new List(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 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 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); + } + } +} diff --git a/src-ref/CustomComponent/PlayerInventory/PlayerInventoryStateStore.cs b/src-ref/CustomComponent/PlayerInventory/PlayerInventoryStateStore.cs new file mode 100644 index 0000000..a3fd94f --- /dev/null +++ b/src-ref/CustomComponent/PlayerInventory/PlayerInventoryStateStore.cs @@ -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(List 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); + } + } +} diff --git a/src-ref/CustomComponent/PlayerInventory/PlayerInventoryTowerAssemblyService.cs b/src-ref/CustomComponent/PlayerInventory/PlayerInventoryTowerAssemblyService.cs new file mode 100644 index 0000000..a252396 --- /dev/null +++ b/src-ref/CustomComponent/PlayerInventory/PlayerInventoryTowerAssemblyService.cs @@ -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; + private IDataTable _drBearingComp; + private IDataTable _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 EnsureMuzzleTable() + { + _drMuzzleComp ??= GameEntry.DataTable.GetDataTable(); + return _drMuzzleComp; + } + + private IDataTable EnsureBearingTable() + { + _drBearingComp ??= GameEntry.DataTable.GetDataTable(); + return _drBearingComp; + } + + private IDataTable EnsureBaseTable() + { + _drBaseComp ??= GameEntry.DataTable.GetDataTable(); + 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; + } +} diff --git a/src-ref/CustomComponent/PlayerInventory/PlayerInventoryTowerRosterService.cs b/src-ref/CustomComponent/PlayerInventory/PlayerInventoryTowerRosterService.cs new file mode 100644 index 0000000..4488642 --- /dev/null +++ b/src-ref/CustomComponent/PlayerInventory/PlayerInventoryTowerRosterService.cs @@ -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 towerInstanceIds, float enduranceLoss) + { + return InventoryTowerEnduranceUtility.ReduceTowerEndurance( + _queryModel.Inventory, + towerInstanceIds, + enduranceLoss); + } + } + + public static class InventoryTowerEnduranceUtility + { + public static int ReduceTowerEndurance( + BackpackInventoryData inventory, + IReadOnlyList 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 muzzleMap = BuildComponentMap(inventory.MuzzleComponents); + Dictionary bearingMap = BuildComponentMap(inventory.BearingComponents); + Dictionary baseMap = BuildComponentMap(inventory.BaseComponents); + HashSet processedTowerIds = new HashSet(); + + 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 BuildComponentMap(List components) + where TComp : TowerCompItemData + { + Dictionary map = new Dictionary(); + 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; + } + } +} diff --git a/src-ref/CustomComponent/PlayerInventory/PlayerInventoryTradeService.cs b/src-ref/CustomComponent/PlayerInventory/PlayerInventoryTradeService.cs new file mode 100644 index 0000000..65c569d --- /dev/null +++ b/src-ref/CustomComponent/PlayerInventory/PlayerInventoryTradeService.cs @@ -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 _shopPriceTable; + + public PlayerInventoryTradeService( + PlayerInventoryQueryModel queryModel, + PlayerInventoryCommandModel commandModel, + IDataTable 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 itemIds, out PlayerInventorySaleResult result) + { + result = new PlayerInventorySaleResult(); + if (itemIds == null || itemIds.Count <= 0) + { + result.FailureReason = PlayerInventorySaleFailureReason.InvalidSelection; + return false; + } + + HashSet uniqueIds = new HashSet(); + List candidates = new List(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(List 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(IReadOnlyList 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(IReadOnlyList 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 EnsureShopPriceTable() + { + _shopPriceTable ??= GameEntry.DataTable.GetDataTable(); + return _shopPriceTable; + } + } +} diff --git a/src-ref/CustomComponent/ResolutionAdapterComponent.cs b/src-ref/CustomComponent/ResolutionAdapterComponent.cs new file mode 100644 index 0000000..3a39863 --- /dev/null +++ b/src-ref/CustomComponent/ResolutionAdapterComponent.cs @@ -0,0 +1,445 @@ +using System.Collections.Generic; +using GameFramework.Event; +using UnityEngine; +using UnityEngine.UI; +using UnityGameFramework.Runtime; + +namespace GeometryTD.CustomComponent +{ + /// + /// Keeps gameplay camera and UI in a fixed design aspect, then fills extra area with black masks. + /// + 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 _uiRoots = new List(); + + private readonly List _canvasBuffer = new List(32); + private readonly List _trackedCanvases = new List(32); + private readonly HashSet _trackedCanvasSet = new HashSet(); + + 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(); + 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(); + _maskCanvas.renderMode = RenderMode.ScreenSpaceOverlay; + _maskCanvas.overrideSorting = true; + _maskCanvas.sortingOrder = short.MaxValue; + + CanvasScaler scaler = maskRoot.GetComponent(); + scaler.uiScaleMode = CanvasScaler.ScaleMode.ConstantPixelSize; + scaler.scaleFactor = 1f; + + GraphicRaycaster raycaster = maskRoot.GetComponent(); + raycaster.enabled = false; + + RectTransform rootRect = maskRoot.GetComponent(); + 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.color = Color.black; + image.raycastTarget = false; + + return maskObject.GetComponent(); + } + + 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); + } + } +} \ No newline at end of file diff --git a/src-ref/CustomComponent/ShopNodeComponent.cs b/src-ref/CustomComponent/ShopNodeComponent.cs new file mode 100644 index 0000000..f3d873b --- /dev/null +++ b/src-ref/CustomComponent/ShopNodeComponent.cs @@ -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; + } + } +} diff --git a/src-ref/CustomComponent/SpriteCacheComponent.cs b/src-ref/CustomComponent/SpriteCacheComponent.cs new file mode 100644 index 0000000..ea6c40f --- /dev/null +++ b/src-ref/CustomComponent/SpriteCacheComponent.cs @@ -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 _spriteCache; + private Dictionary>> _pendingCallbacks; + private ResourceComponent _resource; + + void Start() + { + _spriteCache = new Dictionary(); + _pendingCallbacks = new Dictionary>>(); + _resource = GameEntry.Resource; + } + + public void GetSprite(string assetName, Action 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> { 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; + } + } +} diff --git a/src-ref/CustomComponent/TagRegistry/TagRegistryComponent.cs b/src-ref/CustomComponent/TagRegistry/TagRegistryComponent.cs new file mode 100644 index 0000000..c00780f --- /dev/null +++ b/src-ref/CustomComponent/TagRegistry/TagRegistryComponent.cs @@ -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 _tagConfigTable; + private IDataTable _tagTable; + private IDataTable _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(); + _tagTable ??= GameEntry.DataTable.GetDataTable(); + + 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(); + if (_rarityTagBudgetTable == null) + { + throw new InvalidOperationException( + "TagRegistryComponent requires RarityTagBudget data table to be loaded before initialization."); + } + } + } +} diff --git a/src-ref/CustomComponent/UIRouterComponent.cs b/src-ref/CustomComponent/UIRouterComponent.cs new file mode 100644 index 0000000..cae30c0 --- /dev/null +++ b/src-ref/CustomComponent/UIRouterComponent.cs @@ -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 _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 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) + { + } + } + + } +} diff --git a/src-ref/DataTable/BinaryReaderExtension.cs b/src-ref/DataTable/BinaryReaderExtension.cs new file mode 100644 index 0000000..58a6cb6 --- /dev/null +++ b/src-ref/DataTable/BinaryReaderExtension.cs @@ -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()); + } + } +} diff --git a/src-ref/DataTable/DRBaseComp.cs b/src-ref/DataTable/DRBaseComp.cs new file mode 100644 index 0000000..82653de --- /dev/null +++ b/src-ref/DataTable/DRBaseComp.cs @@ -0,0 +1,110 @@ +using System; +using GeometryTD.Definition; +using GeometryTD.CustomUtility; +using UnityGameFramework.Runtime; + +namespace GeometryTD.DataTable +{ + /// + /// 底座组件表 + /// + public class DRBaseComp : DataRowBase + { + private int m_Id = 0; + + /// + /// 获取底座组件编号 + /// + public override int Id => m_Id; + + /// + /// 获取底座组件名 + /// + public string Name { get; private set; } + + /// + /// 获取底座组件攻击速度数组(秒/次) + /// + public float[] AttackSpeed { get; private set; } + + /// + /// 获取底座组件每级提升攻击速度(值为负数) + /// + public float AttackSpeedPerLevel { get; private set; } + + /// + /// 获取攻击属性 + /// + public AttackPropertyType AttackPropertyType { get; private set; } + + /// + /// 获取属性约束 + /// + public string Constraint { get; private set; } + + /// + /// 获取可能出现的 Tag + /// + 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.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.Get(tagTypes[i]); + } + + return tags; + } + } +} \ No newline at end of file diff --git a/src-ref/DataTable/DRBearingComp.cs b/src-ref/DataTable/DRBearingComp.cs new file mode 100644 index 0000000..54cc521 --- /dev/null +++ b/src-ref/DataTable/DRBearingComp.cs @@ -0,0 +1,116 @@ +using System; +using GeometryTD.Definition; +using GeometryTD.CustomUtility; +using UnityGameFramework.Runtime; + +namespace GeometryTD.DataTable +{ + /// + /// 轴承组件表 + /// + public class DRBearingComp : DataRowBase + { + private int m_Id = 0; + + /// + /// 获取轴承组件编号 + /// + public override int Id => m_Id; + + /// + /// 获取轴承组件名 + /// + public string Name { get; private set; } + + /// + /// 获取轴承组件旋转速度数组 + /// + public float[] RotateSpeed { get; private set; } + + /// + /// 获取轴承组件每级提升旋转速度 + /// + public float RotateSpeedPerLevel { get; private set; } + + /// + /// 获取攻击范围 + /// + public float[] AttackRange { get; private set; } + + /// + /// 获取每级提升攻击范围 + /// + public float AttackRangePerLevel { get; private set; } + + /// + /// 获取属性约束 + /// + public string Constraint { get; private set; } + + /// + /// 获取可能出现的 Tag + /// + 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.Get(tagTypes[i]); + } + + return tags; + } + } +} \ No newline at end of file diff --git a/src-ref/DataTable/DREnemy.cs b/src-ref/DataTable/DREnemy.cs new file mode 100644 index 0000000..435bbc5 --- /dev/null +++ b/src-ref/DataTable/DREnemy.cs @@ -0,0 +1,75 @@ +using UnityGameFramework.Runtime; + +namespace GeometryTD.DataTable +{ + /// + /// 敌人配置表 + /// + public class DREnemy : DataRowBase + { + private int m_Id = 0; + + /// + /// 获取敌人编号 + /// + public override int Id => m_Id; + + /// + /// 获取敌人实体编号 + /// + public int EntityId { get; private set; } + + /// + /// 获取敌人基础血量 + /// + public int BaseHp { get; private set; } + + /// + /// 获取敌人基础基地伤害 + /// + public int BaseDamage { get; private set; } + + /// + /// 获取敌人移动速度 + /// + public float Speed { get; private set; } + + /// + /// 获取敌人掉落硬币 + /// + public int DropCoin { get; private set; } + + /// + /// 获取敌人掉落金币 + /// + public int DropGold { get; private set; } + + /// + /// 获取敌人掉落金币概率 + /// + 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; + } + } +} \ No newline at end of file diff --git a/src-ref/DataTable/DREntity.cs b/src-ref/DataTable/DREntity.cs new file mode 100644 index 0000000..0719139 --- /dev/null +++ b/src-ref/DataTable/DREntity.cs @@ -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 +{ + /// + /// 实体表。 + /// + public class DREntity : DataRowBase + { + private int m_Id = 0; + + /// + /// 获取实体编号。 + /// + public override int Id => m_Id; + + /// + /// 获取资源名称。 + /// + 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() + { + + } + } +} diff --git a/src-ref/DataTable/DREvent.cs b/src-ref/DataTable/DREvent.cs new file mode 100644 index 0000000..1a1167d --- /dev/null +++ b/src-ref/DataTable/DREvent.cs @@ -0,0 +1,82 @@ +using UnityGameFramework.Runtime; +using Newtonsoft.Json.Linq; + +namespace GeometryTD.DataTable +{ + /// + /// 事件配置表 + /// + public class DREvent : DataRowBase + { + private int m_Id = 0; + + /// + /// 获取事件编号 + /// + public override int Id => m_Id; + + /// + /// 获取事件题目 + /// + public string Title { get; private set; } + + /// + /// 获取事件描述 + /// + public string Description { get; private set; } + + /// + /// 获取事件选项 + /// + /// 原始字符串(如 JSON 文本),不在此处做解析。 + 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); + } + } +} diff --git a/src-ref/DataTable/DRLevel.cs b/src-ref/DataTable/DRLevel.cs new file mode 100644 index 0000000..fecd64e --- /dev/null +++ b/src-ref/DataTable/DRLevel.cs @@ -0,0 +1,72 @@ +using System; +using GeometryTD.Definition; +using GeometryTD.CustomUtility; +using UnityGameFramework.Runtime; + +namespace GeometryTD.DataTable +{ + /// + /// 关卡表 + /// + public class DRLevel : DataRowBase + { + private int m_Id = 0; + + /// + /// 获取关卡编号 + /// + public override int Id => m_Id; + + /// + /// 获取关卡所属主题类型 + /// + public LevelThemeType LevelThemeType { get; private set; } + + /// + /// 获取关卡初始基地生命 + /// + public int BaseHp { get; private set; } + + /// + /// 获取关卡初始硬币数量 + /// + public int StartCoin { get; private set; } + + /// + /// 获取关卡胜利条件 + /// + public LevelVictoryType LevelVictoryType { get; private set; } + + /// + /// 获取关卡胜利奖励金币 + /// + public int RewardGold { get; private set; } + + /// + /// 获取关卡胜利条件参数 + /// + 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.Get(columnStrings[index++]); + BaseHp = int.Parse(columnStrings[index++]); + StartCoin = int.Parse(columnStrings[index++]); + LevelVictoryType = EnumUtility.Get(columnStrings[index++]); + VictoryParams = columnStrings[index++]; + RewardGold = int.Parse(columnStrings[index++]); + + return true; + } + } +} \ No newline at end of file diff --git a/src-ref/DataTable/DRLevelPhase.cs b/src-ref/DataTable/DRLevelPhase.cs new file mode 100644 index 0000000..c98fe1e --- /dev/null +++ b/src-ref/DataTable/DRLevelPhase.cs @@ -0,0 +1,64 @@ +using System; +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using UnityGameFramework.Runtime; + +namespace GeometryTD.DataTable +{ + /// + /// 关卡阶段表 + /// + public class DRLevelPhase : DataRowBase + { + private int m_Id = 0; + + /// + /// 获取关卡阶段编号 + /// + public override int Id => m_Id; + + /// + /// 获取阶段持续时间(秒) + /// + public int DurationSeconds { get; private set; } + + /// + /// 获取阶段结束条件类型 + /// + public PhaseEndType EndType { get; private set; } + + /// + /// 获取阶段结束参数 + /// + 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.Get(raw); + } + } +} diff --git a/src-ref/DataTable/DRLevelSpawnEntry.cs b/src-ref/DataTable/DRLevelSpawnEntry.cs new file mode 100644 index 0000000..f8c41b3 --- /dev/null +++ b/src-ref/DataTable/DRLevelSpawnEntry.cs @@ -0,0 +1,94 @@ +using System; +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using UnityGameFramework.Runtime; + +namespace GeometryTD.DataTable +{ + /// + /// 关卡阶段出怪条目表 + /// + public class DRLevelSpawnEntry : DataRowBase + { + private int m_Id = 0; + + /// + /// 获取阶段条目编号 + /// + public override int Id => m_Id; + + /// + /// 获取敌人出生口编号 + /// + public int SpawnPointId { get; private set; } + + /// + /// 获取相对开始时间(秒) + /// + public int StartTime { get; private set; } + + /// + /// 获取出怪条目类型 + /// + public EntryType EntryType { get; private set; } + + /// + /// 获取敌人编号 + /// + public int EnemyId { get; private set; } + + /// + /// 获取单次出怪数量 + /// + public int Count { get; private set; } + + /// + /// 获取出怪间隔(秒) + /// + public float Interval { get; private set; } + + /// + /// 获取持续时间(秒) + /// + public int Duration { get; private set; } + + /// + /// 获取单怪出生间隔(秒) + /// + 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.Get(raw); + } + } +} diff --git a/src-ref/DataTable/DRMusic.cs b/src-ref/DataTable/DRMusic.cs new file mode 100644 index 0000000..b992428 --- /dev/null +++ b/src-ref/DataTable/DRMusic.cs @@ -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 +{ + /// + /// 音乐配置表。 + /// + public class DRMusic : DataRowBase + { + private int m_Id = 0; + + /// + /// 获取音乐编号。 + /// + public override int Id + { + get + { + return m_Id; + } + } + + /// + /// 获取资源名称。 + /// + 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() + { + + } + } +} diff --git a/src-ref/DataTable/DRMuzzleComp.cs b/src-ref/DataTable/DRMuzzleComp.cs new file mode 100644 index 0000000..15ff629 --- /dev/null +++ b/src-ref/DataTable/DRMuzzleComp.cs @@ -0,0 +1,116 @@ +using System; +using GeometryTD.Definition; +using GeometryTD.CustomUtility; +using UnityGameFramework.Runtime; + +namespace GeometryTD.DataTable +{ + /// + /// 枪口组件表 + /// + public class DRMuzzleComp : DataRowBase + { + private int m_Id = 0; + + /// + /// 获取枪口组件编号 + /// + public override int Id => m_Id; + + /// + /// 获取枪口组件名 + /// + public string Name { get; private set; } + + /// + /// 获取枪口组件攻击伤害数组 + /// + public int[] AttackDamage { get; private set; } + + /// + /// 获取枪口组件每级提升攻击伤害值 + /// + public int AttackDamagePerLevel { get; private set; } + + /// + /// 获取攻击伤害浮动 + /// + public float DamageRandomRate { get; private set; } + + /// + /// 获取枪口攻击方式 + /// + public AttackMethodType AttackMethodType { get; private set; } + + /// + /// 获取属性约束 + /// + public string Constraint { get; private set; } + + /// + /// 获取可能出现的 Tag + /// + 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.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.Get(tagTypes[i]); + } + + return tags; + } + } +} \ No newline at end of file diff --git a/src-ref/DataTable/DROutGameDropPool.cs b/src-ref/DataTable/DROutGameDropPool.cs new file mode 100644 index 0000000..6972bed --- /dev/null +++ b/src-ref/DataTable/DROutGameDropPool.cs @@ -0,0 +1,154 @@ +using System; +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.DataTable +{ + /// + /// 局外道具掉落池配置表。 + /// + 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.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; + } +} diff --git a/src-ref/DataTable/DRRarityTagBudget.cs b/src-ref/DataTable/DRRarityTagBudget.cs new file mode 100644 index 0000000..cb4ec0a --- /dev/null +++ b/src-ref/DataTable/DRRarityTagBudget.cs @@ -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.Get(columnStrings[index++]); + MinCount = int.Parse(columnStrings[index++]); + MaxCount = int.Parse(columnStrings[index++]); + + return true; + } + } +} diff --git a/src-ref/DataTable/DRScene.cs b/src-ref/DataTable/DRScene.cs new file mode 100644 index 0000000..1481609 --- /dev/null +++ b/src-ref/DataTable/DRScene.cs @@ -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 +{ + /// + /// 场景配置表。 + /// + public class DRScene : DataRowBase + { + private int m_Id = 0; + + /// + /// 获取场景编号。 + /// + public override int Id + { + get + { + return m_Id; + } + } + + /// + /// 获取资源名称。 + /// + public string AssetName + { + get; + private set; + } + + /// + /// 获取背景音乐编号。 + /// + 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() + { + + } + } +} diff --git a/src-ref/DataTable/DRShopPrice.cs b/src-ref/DataTable/DRShopPrice.cs new file mode 100644 index 0000000..14d2bdb --- /dev/null +++ b/src-ref/DataTable/DRShopPrice.cs @@ -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.Get(columnStrings[index++]); + MinPrice = int.Parse(columnStrings[index++]); + MaxPrice = int.Parse(columnStrings[index++]); + + return true; + } + } +} \ No newline at end of file diff --git a/src-ref/DataTable/DRSound.cs b/src-ref/DataTable/DRSound.cs new file mode 100644 index 0000000..8e02629 --- /dev/null +++ b/src-ref/DataTable/DRSound.cs @@ -0,0 +1,127 @@ +using System.IO; +using System.Text; +using UnityGameFramework.Runtime; + +namespace GeometryTD.DataTable +{ + /// + /// 声音配置表。 + /// + public class DRSound : DataRowBase + { + private int m_Id = 0; + + /// + /// 获取声音编号。 + /// + public override int Id + { + get + { + return m_Id; + } + } + + /// + /// 获取资源名称。 + /// + public string AssetName + { + get; + private set; + } + + /// + /// 获取优先级(默认0,128最高,-128最低)。 + /// + public int Priority + { + get; + private set; + } + + /// + /// 获取是否循环。 + /// + public bool Loop + { + get; + private set; + } + + /// + /// 获取音量(0~1)。 + /// + public float Volume + { + get; + private set; + } + + /// + /// 获取声音空间混合量(0为2D,1为3D,中间值混合效果)。 + /// + public float SpatialBlend + { + get; + private set; + } + + /// + /// 获取声音最大距离。 + /// + 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() + { + + } + } +} diff --git a/src-ref/DataTable/DRTag.cs b/src-ref/DataTable/DRTag.cs new file mode 100644 index 0000000..c063794 --- /dev/null +++ b/src-ref/DataTable/DRTag.cs @@ -0,0 +1,68 @@ +using System; +using GeometryTD.Definition; +using GeometryTD.CustomUtility; +using UnityGameFramework.Runtime; + +namespace GeometryTD.DataTable +{ + /// + /// Tag 表 + /// + public class DRTag : DataRowBase + { + private int m_Id = 0; + + /// + /// 获取 Tag 编号 + /// + public override int Id => m_Id; + + /// + /// 获取 Tag 名 + /// + 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.Get(columnStrings[index++]); + Weight = int.Parse(columnStrings[index++]); + IsImplemented = bool.Parse(columnStrings[index++]); + } + else + { + TagGroupText = string.Empty; + MinRarity = EnumUtility.Get(columnStrings[index++]); + Weight = int.Parse(columnStrings[index++]); + IsImplemented = true; + } + + return true; + } + } +} diff --git a/src-ref/DataTable/DRTagConfig.cs b/src-ref/DataTable/DRTagConfig.cs new file mode 100644 index 0000000..e9000f2 --- /dev/null +++ b/src-ref/DataTable/DRTagConfig.cs @@ -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.Get(columnStrings[index++]); + TriggerPhase = EnumUtility.Get(columnStrings[index++]); + Description = columnStrings[index++]; + ParamJson = columnStrings[index++]; + + return true; + } + } +} \ No newline at end of file diff --git a/src-ref/DataTable/DRUIForm.cs b/src-ref/DataTable/DRUIForm.cs new file mode 100644 index 0000000..a17e73c --- /dev/null +++ b/src-ref/DataTable/DRUIForm.cs @@ -0,0 +1,83 @@ +using System.IO; +using System.Text; +using UnityGameFramework.Runtime; + +namespace GeometryTD.DataTable +{ + /// + /// 界面配置表。 + /// + public class DRUIForm : DataRowBase + { + private int m_Id = 0; + + /// + /// 获取界面编号。 + /// + public override int Id => m_Id; + + /// + /// 获取资源名称。 + /// + public string AssetName { get; private set; } + + /// + /// 获取界面组名称。 + /// + public string UIGroupName { get; private set; } + + /// + /// 获取是否允许多个界面实例。 + /// + public bool AllowMultiInstance { get; private set; } + + /// + /// 获取是否暂停被其覆盖的界面。 + /// + 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() + { + + } + } +} diff --git a/src-ref/DataTable/DRUISound.cs b/src-ref/DataTable/DRUISound.cs new file mode 100644 index 0000000..f8e4493 --- /dev/null +++ b/src-ref/DataTable/DRUISound.cs @@ -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 +{ + /// + /// 声音配置表。 + /// + public class DRUISound : DataRowBase + { + private int m_Id = 0; + + /// + /// 获取声音编号。 + /// + public override int Id + { + get + { + return m_Id; + } + } + + /// + /// 获取资源名称。 + /// + public string AssetName + { + get; + private set; + } + + /// + /// 获取优先级(默认0,128最高,-128最低)。 + /// + public int Priority + { + get; + private set; + } + + /// + /// 获取音量(0~1)。 + /// + 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() + { + + } + } +} diff --git a/src-ref/DataTable/DataTableExtension.cs b/src-ref/DataTable/DataTableExtension.cs new file mode 100644 index 0000000..853b99e --- /dev/null +++ b/src-ref/DataTable/DataTableExtension.cs @@ -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])); + } + } +} diff --git a/src-ref/Definition/CombatParticipantTowerValidation.cs b/src-ref/Definition/CombatParticipantTowerValidation.cs new file mode 100644 index 0000000..3272d4e --- /dev/null +++ b/src-ref/Definition/CombatParticipantTowerValidation.cs @@ -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 ValidTowers { get; set; } = System.Array.Empty(); + + public IReadOnlyList InvalidResults { get; set; } = + System.Array.Empty(); + + 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 validTowers = new List(); + List invalidResults = + new List(); + HashSet processedTowerIds = new HashSet(); + + 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( + IReadOnlyList 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; + } + } +} diff --git a/src-ref/Definition/CombatParticipantTowerValidationText.cs b/src-ref/Definition/CombatParticipantTowerValidationText.cs new file mode 100644 index 0000000..afe5b8d --- /dev/null +++ b/src-ref/Definition/CombatParticipantTowerValidationText.cs @@ -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 "不满足当前参战条件。"; + } + } + } +} diff --git a/src-ref/Definition/Constant/Constant.AssetPriority.cs b/src-ref/Definition/Constant/Constant.AssetPriority.cs new file mode 100644 index 0000000..2e0a9d1 --- /dev/null +++ b/src-ref/Definition/Constant/Constant.AssetPriority.cs @@ -0,0 +1,40 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +namespace GeometryTD +{ + public static partial class Constant + { + /// + /// 资源优先级。 + /// + public static class AssetPriority + { + public const int ConfigAsset = 100; + public const int DataTableAsset = 100; + public const int DictionaryAsset = 100; + public const int FontAsset = 50; + public const int MusicAsset = 20; + public const int SceneAsset = 0; + public const int SoundAsset = 30; + public const int UIFormAsset = 50; + public const int UISoundAsset = 30; + + public const int MyAircraftAsset = 90; + public const int PlayerAsset = 90; + public const int AircraftAsset = 80; + public const int ThrusterAsset = 30; + public const int WeaponAsset = 30; + public const int ArmorAsset = 30; + public const int BulletAsset = 80; + public const int AsteroiAsset = 80; + public const int EnemyAsset = 80; + public const int MapAsset = 85; + public const int EffectAsset = 80; + } + } +} diff --git a/src-ref/Definition/Constant/Constant.Layer.cs b/src-ref/Definition/Constant/Constant.Layer.cs new file mode 100644 index 0000000..536c69f --- /dev/null +++ b/src-ref/Definition/Constant/Constant.Layer.cs @@ -0,0 +1,29 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using UnityEngine; + +namespace GeometryTD +{ + public static partial class Constant + { + /// + /// 层。 + /// + public static class Layer + { + public const string DefaultLayerName = "Default"; + public static readonly int DefaultLayerId = LayerMask.NameToLayer(DefaultLayerName); + + public const string UILayerName = "UI"; + public static readonly int UILayerId = LayerMask.NameToLayer(UILayerName); + + public const string TargetableObjectLayerName = "Targetable Object"; + public static readonly int TargetableObjectLayerId = LayerMask.NameToLayer(TargetableObjectLayerName); + } + } +} diff --git a/src-ref/Definition/Constant/Constant.Setting.cs b/src-ref/Definition/Constant/Constant.Setting.cs new file mode 100644 index 0000000..519ccd6 --- /dev/null +++ b/src-ref/Definition/Constant/Constant.Setting.cs @@ -0,0 +1,25 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +namespace GeometryTD +{ + public static partial class Constant + { + public static class Setting + { + public const string Language = "Setting.Language"; + public const string SoundGroupMuted = "Setting.{0}Muted"; + public const string SoundGroupVolume = "Setting.{0}Volume"; + public const string MusicMuted = "Setting.MusicMuted"; + public const string MusicVolume = "Setting.MusicVolume"; + public const string SoundMuted = "Setting.SoundMuted"; + public const string SoundVolume = "Setting.SoundVolume"; + public const string UISoundMuted = "Setting.UISoundMuted"; + public const string UISoundVolume = "Setting.UISoundVolume"; + } + } +} diff --git a/src-ref/Definition/DataStruct/AttackPayload.cs b/src-ref/Definition/DataStruct/AttackPayload.cs new file mode 100644 index 0000000..51cacd3 --- /dev/null +++ b/src-ref/Definition/DataStruct/AttackPayload.cs @@ -0,0 +1,30 @@ +using System; +using GeometryTD.CustomUtility; +using UnityEngine; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class AttackPayload + { + public int BaseDamage { get; set; } + public AttackPropertyType AttackPropertyType { get; set; } + public int SourceEntityId { get; set; } + public int ProjectileEntityId { get; set; } + public Vector3 OriginPosition { get; set; } + public TagRuntimeData[] TagRuntimes { get; set; } = Array.Empty(); + + public AttackPayload Clone() + { + return new AttackPayload + { + BaseDamage = BaseDamage, + AttackPropertyType = AttackPropertyType, + SourceEntityId = SourceEntityId, + ProjectileEntityId = ProjectileEntityId, + OriginPosition = OriginPosition, + TagRuntimes = InventoryCloneUtility.CloneTagRuntimes(TagRuntimes) + }; + } + } +} diff --git a/src-ref/Definition/DataStruct/BackpackInventoryData.cs b/src-ref/Definition/DataStruct/BackpackInventoryData.cs new file mode 100644 index 0000000..e7198dd --- /dev/null +++ b/src-ref/Definition/DataStruct/BackpackInventoryData.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; + +namespace GeometryTD.Definition +{ + /// + /// 玩家背包聚合数据。 + /// + [Serializable] + public sealed class BackpackInventoryData + { + /// + /// 背包金币。 + /// + public int Gold { get; set; } + + /// + /// 背包中的枪口组件实例。 + /// + public List MuzzleComponents { get; set; } = new List(); + + /// + /// 背包中的轴承组件实例。 + /// + public List BearingComponents { get; set; } = new List(); + + /// + /// 背包中的底座组件实例。 + /// + public List BaseComponents { get; set; } = new List(); + + /// + /// 背包中的防御塔实例。 + /// + public List Towers { get; set; } = new List(); + + /// + /// 参与战斗的防御塔 Id。 + /// + public List ParticipantTowerInstanceIds { get; set; } = new List(); + } +} diff --git a/src-ref/Definition/DataStruct/BuildInfo.cs b/src-ref/Definition/DataStruct/BuildInfo.cs new file mode 100644 index 0000000..658b102 --- /dev/null +++ b/src-ref/Definition/DataStruct/BuildInfo.cs @@ -0,0 +1,54 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +namespace GeometryTD.Definition +{ + public class BuildInfo + { + public string GameVersion + { + get; + set; + } + + public int InternalGameVersion + { + get; + set; + } + + public string CheckVersionUrl + { + get; + set; + } + + public string WindowsAppUrl + { + get; + set; + } + + public string MacOSAppUrl + { + get; + set; + } + + public string IOSAppUrl + { + get; + set; + } + + public string AndroidAppUrl + { + get; + set; + } + } +} diff --git a/src-ref/Definition/DataStruct/EventItem.cs b/src-ref/Definition/DataStruct/EventItem.cs new file mode 100644 index 0000000..62883e6 --- /dev/null +++ b/src-ref/Definition/DataStruct/EventItem.cs @@ -0,0 +1,18 @@ +namespace GeometryTD.Definition +{ + public class EventItem + { + public EventItem(int id, string title, string description, EventOption[] options) + { + Id = id; + Title = title; + Description = description; + Options = options ?? System.Array.Empty(); + } + + public int Id { get; private set; } + public string Title { get; private set; } + public string Description { get; private set; } + public EventOption[] Options { get; private set; } + } +} diff --git a/src-ref/Definition/DataStruct/EventOption.cs b/src-ref/Definition/DataStruct/EventOption.cs new file mode 100644 index 0000000..3afb457 --- /dev/null +++ b/src-ref/Definition/DataStruct/EventOption.cs @@ -0,0 +1,25 @@ +namespace GeometryTD.Definition +{ + public class EventOption + { + public string OptionText { get; private set; } + public EventRequirementBase[] Requirements { get; private set; } + public EventEffectBase[] CostEffects { get; private set; } + public EventEffectBase[] RewardEffects { get; private set; } + public float Probability { get; private set; } + + public EventOption( + string optionText, + EventRequirementBase[] requirements, + EventEffectBase[] costEffects, + EventEffectBase[] rewardEffects, + float probability = 1f) + { + OptionText = optionText; + Requirements = requirements ?? System.Array.Empty(); + CostEffects = costEffects ?? System.Array.Empty(); + RewardEffects = rewardEffects ?? System.Array.Empty(); + Probability = probability; + } + } +} diff --git a/src-ref/Definition/DataStruct/HitContext.cs b/src-ref/Definition/DataStruct/HitContext.cs new file mode 100644 index 0000000..debc4ea --- /dev/null +++ b/src-ref/Definition/DataStruct/HitContext.cs @@ -0,0 +1,62 @@ +using System; +using GeometryTD.Entity; +using UnityEngine; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class HitStatusModifierContext + { + public float BonusBurnDurationSeconds { get; set; } + public float BonusBurnDamagePerSecond { get; set; } + public float BonusSlowDurationSeconds { get; set; } + public float BonusSlowRatio { get; set; } + + public void Reset() + { + BonusBurnDurationSeconds = 0f; + BonusBurnDamagePerSecond = 0f; + BonusSlowDurationSeconds = 0f; + BonusSlowRatio = 0f; + } + } + + [Serializable] + public sealed class HitContext + { + public AttackPayload AttackPayload { get; set; } + public int FinalDamage { get; set; } + public bool IsCriticalHit { get; set; } + public bool IsKilled { get; set; } + public int TargetEntityId { get; set; } + public Vector3 TargetPosition { get; set; } + public int TargetCurrentHealthBeforeHit { get; set; } + public int TargetCurrentHealthAfterHit { get; set; } + public int TargetMaxHealth { get; set; } + public float TargetMoveSpeedMultiplierBeforeHit { get; set; } = 1f; + public TagType[] TargetStatusTagsBeforeHit { get; set; } = Array.Empty(); + public EnemyTagStatusRuntime TargetStatusRuntime { get; set; } + public float? CritRoll { get; set; } + public HitStatusModifierContext StatusModifierContext { get; set; } = new HitStatusModifierContext(); + + public bool HasTargetStatus(TagType tagType) + { + if (tagType == TagType.None || TargetStatusTagsBeforeHit == null || TargetStatusTagsBeforeHit.Length <= 0) + { + return false; + } + + for (int i = 0; i < TargetStatusTagsBeforeHit.Length; i++) + { + if (TargetStatusTagsBeforeHit[i] == tagType) + { + return true; + } + } + + return false; + } + + public bool HasSlowStatusBeforeHit => TargetMoveSpeedMultiplierBeforeHit < 0.999f || HasTargetStatus(TagType.Ice); + } +} diff --git a/src-ref/Definition/DataStruct/ImpactData.cs b/src-ref/Definition/DataStruct/ImpactData.cs new file mode 100644 index 0000000..1db0f0b --- /dev/null +++ b/src-ref/Definition/DataStruct/ImpactData.cs @@ -0,0 +1,24 @@ +using System.Runtime.InteropServices; + +namespace GeometryTD.Definition +{ + [StructLayout(LayoutKind.Auto)] + public struct ImpactData + { + public ImpactData(CampType camp, int hp, int attack, int defense) + { + Camp = camp; + HP = hp; + Attack = attack; + Defense = defense; + } + + public CampType Camp { get; } + + public int HP { get; } + + public int Attack { get; } + + public int Defense { get; } + } +} \ No newline at end of file diff --git a/src-ref/Definition/DataStruct/TowerCompItemData.cs b/src-ref/Definition/DataStruct/TowerCompItemData.cs new file mode 100644 index 0000000..6ba31d2 --- /dev/null +++ b/src-ref/Definition/DataStruct/TowerCompItemData.cs @@ -0,0 +1,99 @@ +using System; + +namespace GeometryTD.Definition +{ + /// + /// 背包内组件实例基类(非 DataTable,表示玩家持有物)。 + /// + [Serializable] + public abstract class TowerCompItemData + { + /// + /// 组件实例唯一 Id。 + /// + public long InstanceId { get; set; } + + /// + /// 组件配置 Id(对应 DataTable Id)。 + /// + public int ConfigId { get; set; } + + /// + /// 组件槽位类型。 + /// + public TowerCompSlotType SlotType { get; protected set; } + + /// + /// 组件名称。 + /// + public string Name { get; set; } + + /// + /// 组件品质。 + /// + public RarityType Rarity { get; set; } + + /// + /// 组件当前耐久(0~100)。 + /// + public float Endurance { get; set; } = 100f; + + public bool IsAssembledIntoTower { get; set; } + + /// + /// 组件约束(先沿用 DataTable 原定义)。 + /// + public string Constraint { get; set; } + + /// + /// 组件当前 Tag(实例态)。 + /// + public TagType[] Tags { get; set; } + } + + [Serializable] + public sealed class MuzzleCompItemData : TowerCompItemData + { + public MuzzleCompItemData() + { + SlotType = TowerCompSlotType.Muzzle; + } + + public int[] AttackDamage { get; set; } + public float DamageRandomRate { get; set; } + public AttackMethodType AttackMethodType { get; set; } + } + + [Serializable] + public sealed class BearingCompItemData : TowerCompItemData + { + public BearingCompItemData() + { + SlotType = TowerCompSlotType.Bearing; + } + + public float[] RotateSpeed { get; set; } + public float[] AttackRange { get; set; } + } + + [Serializable] + public sealed class BaseCompItemData : TowerCompItemData + { + public BaseCompItemData() + { + SlotType = TowerCompSlotType.Base; + } + + public float[] AttackSpeed { get; set; } + public AttackPropertyType AttackPropertyType { get; set; } + } + + [Serializable] + public sealed class AccessoryItemData : TowerCompItemData + { + public AccessoryItemData() + { + SlotType = TowerCompSlotType.Accessory; + } + } +} diff --git a/src-ref/Definition/DataStruct/TowerItemData.cs b/src-ref/Definition/DataStruct/TowerItemData.cs new file mode 100644 index 0000000..d2ba619 --- /dev/null +++ b/src-ref/Definition/DataStruct/TowerItemData.cs @@ -0,0 +1,54 @@ +using System; +using Newtonsoft.Json; +using UnityEngine; + +namespace GeometryTD.Definition +{ + /// + /// 背包内防御塔实例数据。 + /// + [Serializable] + public sealed class TowerItemData + { + /// + /// 防御塔实例唯一 Id。 + /// + public long InstanceId { get; set; } + + /// + /// 防御塔显示名称。 + /// + public string Name { get; set; } + + /// + /// 防御塔品质。 + /// + public RarityType Rarity { get; set; } + + public bool IsParticipatingInCombat { get; set; } + + /// + /// 构成该防御塔的枪口组件实例 Id。 + /// + public long MuzzleComponentInstanceId { get; set; } + + /// + /// 构成该防御塔的轴承组件实例 Id。 + /// + public long BearingComponentInstanceId { get; set; } + + /// + /// 构成该防御塔的底座组件实例 Id。 + /// + public long BaseComponentInstanceId { get; set; } + + /// + /// 防御塔独立属性,不依赖组件对象引用。 + /// + public TowerStatsData Stats { get; set; } = new TowerStatsData(); + + [JsonIgnore] public Sprite ComposedIconSprite { get; set; } + + [JsonIgnore] public string ComposedIconKey { get; set; } + } +} \ No newline at end of file diff --git a/src-ref/Definition/DataStruct/TowerStatsData.cs b/src-ref/Definition/DataStruct/TowerStatsData.cs new file mode 100644 index 0000000..e23379a --- /dev/null +++ b/src-ref/Definition/DataStruct/TowerStatsData.cs @@ -0,0 +1,22 @@ +using System; + +namespace GeometryTD.Definition +{ + /// + /// 防御塔独立属性快照。 + /// 注意:这里是塔实例的独立值,不通过组件引用实时计算。 + /// + [Serializable] + public sealed class TowerStatsData + { + public int[] AttackDamage { get; set; } + public float DamageRandomRate { get; set; } + public float[] RotateSpeed { get; set; } + public float[] AttackRange { get; set; } + public float[] AttackSpeed { get; set; } + public AttackMethodType AttackMethodType { get; set; } + public AttackPropertyType AttackPropertyType { get; set; } + public TagRuntimeData[] TagRuntimes { get; set; } + public TagType[] Tags { get; set; } + } +} \ No newline at end of file diff --git a/src-ref/Definition/DataStruct/VersionInfo.cs b/src-ref/Definition/DataStruct/VersionInfo.cs new file mode 100644 index 0000000..24cd43a --- /dev/null +++ b/src-ref/Definition/DataStruct/VersionInfo.cs @@ -0,0 +1,68 @@ +namespace GeometryTD.Definition +{ + public class VersionInfo + { + // 是否需要强制更新游戏应用 + public bool ForceUpdateGame + { + get; + set; + } + + // 最新的游戏版本号 + public string LatestGameVersion + { + get; + set; + } + + // 最新的游戏内部版本号 + public int InternalGameVersion + { + get; + set; + } + + // 最新的资源内部版本号 + public int InternalResourceVersion + { + get; + set; + } + + // 资源更新下载地址 + public string UpdatePrefixUri + { + get; + set; + } + + // 资源版本列表长度 + public int VersionListLength + { + get; + set; + } + + // 资源版本列表哈希值 + public int VersionListHashCode + { + get; + set; + } + + // 资源版本列表压缩后长度 + public int VersionListCompressedLength + { + get; + set; + } + + // 资源版本列表压缩后哈希值 + public int VersionListCompressedHashCode + { + get; + set; + } + } +} diff --git a/src-ref/Definition/Enum/AttackMethodType.cs b/src-ref/Definition/Enum/AttackMethodType.cs new file mode 100644 index 0000000..78d4b0f --- /dev/null +++ b/src-ref/Definition/Enum/AttackMethodType.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public enum AttackMethodType : byte + { + None = 0, + NormalBullet = 1, + Range + } +} \ No newline at end of file diff --git a/src-ref/Definition/Enum/AttackPropertyType.cs b/src-ref/Definition/Enum/AttackPropertyType.cs new file mode 100644 index 0000000..5a0f3d9 --- /dev/null +++ b/src-ref/Definition/Enum/AttackPropertyType.cs @@ -0,0 +1,13 @@ +namespace GeometryTD.Definition +{ + public enum AttackPropertyType : byte + { + None = 0, + Physics = 1, + Fire = 2, + Water = 3, + Earth = 4, + Poison = 5, + Ice = 6 + } +} \ No newline at end of file diff --git a/src-ref/Definition/Enum/CampType.cs b/src-ref/Definition/Enum/CampType.cs new file mode 100644 index 0000000..ddba1c9 --- /dev/null +++ b/src-ref/Definition/Enum/CampType.cs @@ -0,0 +1,40 @@ +namespace GeometryTD.Definition +{ + /// + /// 阵营类型。 + /// + public enum CampType : byte + { + Unknown = 0, + + /// + /// 第一玩家阵营。 + /// + Player, + + /// + /// 第一敌人阵营。 + /// + Enemy, + + /// + /// 第一中立阵营。 + /// + Neutral, + + /// + /// 第二玩家阵营。 + /// + Player2, + + /// + /// 第二敌人阵营。 + /// + Enemy2, + + /// + /// 第二中立阵营 + /// + Neutral2, + } +} diff --git a/src-ref/Definition/Enum/EntryType.cs b/src-ref/Definition/Enum/EntryType.cs new file mode 100644 index 0000000..e29a644 --- /dev/null +++ b/src-ref/Definition/Enum/EntryType.cs @@ -0,0 +1,13 @@ +namespace GeometryTD.Definition +{ + /// + /// 出怪条目类型 + /// + public enum EntryType : byte + { + None = 0, + Stream = 1, + Burst = 2, + Boss = 3 + } +} diff --git a/src-ref/Definition/Enum/EventEffectType.cs b/src-ref/Definition/Enum/EventEffectType.cs new file mode 100644 index 0000000..0231272 --- /dev/null +++ b/src-ref/Definition/Enum/EventEffectType.cs @@ -0,0 +1,21 @@ +namespace GeometryTD.Definition +{ + public enum EventEffectType + { + None, + + AddGold, + RemoveGold, + + AddRandomComps, + RemoveRandomComps, + + AddRandomCompsEndurance, + RemoveRandomCompsEndurance, + + AddRandomTowersEndurance, + DamageRandomTowersEndurance, + + TransformComponents, + } +} \ No newline at end of file diff --git a/src-ref/Definition/Enum/EventRequirementType.cs b/src-ref/Definition/Enum/EventRequirementType.cs new file mode 100644 index 0000000..eb02abd --- /dev/null +++ b/src-ref/Definition/Enum/EventRequirementType.cs @@ -0,0 +1,11 @@ +namespace GeometryTD.Definition +{ + public enum EventRequirementType + { + None, + GoldAtLeast, + CompCountAtLeast, + TowerCountAtLeast, + HasRelic, + } +} \ No newline at end of file diff --git a/src-ref/Definition/Enum/InventoryTagSourceType.cs b/src-ref/Definition/Enum/InventoryTagSourceType.cs new file mode 100644 index 0000000..2f21259 --- /dev/null +++ b/src-ref/Definition/Enum/InventoryTagSourceType.cs @@ -0,0 +1,11 @@ +namespace GeometryTD.Definition +{ + public enum InventoryTagSourceType : byte + { + Seed = 1, + Shop = 2, + Drop = 3, + Reward = 4, + Event = 5, + } +} diff --git a/src-ref/Definition/Enum/LevelThemeType.cs b/src-ref/Definition/Enum/LevelThemeType.cs new file mode 100644 index 0000000..8efa818 --- /dev/null +++ b/src-ref/Definition/Enum/LevelThemeType.cs @@ -0,0 +1,10 @@ +namespace GeometryTD.Definition +{ + public enum LevelThemeType + { + None, + Plain, + Volcano, + Mountain + } +} \ No newline at end of file diff --git a/src-ref/Definition/Enum/LevelVictoryType.cs b/src-ref/Definition/Enum/LevelVictoryType.cs new file mode 100644 index 0000000..b58db43 --- /dev/null +++ b/src-ref/Definition/Enum/LevelVictoryType.cs @@ -0,0 +1,10 @@ +namespace GeometryTD.Definition +{ + public enum LevelVictoryType + { + None, + PhasesCleared, + BossDead, + TimeElapsed + } +} \ No newline at end of file diff --git a/src-ref/Definition/Enum/PhaseEndType.cs b/src-ref/Definition/Enum/PhaseEndType.cs new file mode 100644 index 0000000..a8b1e2e --- /dev/null +++ b/src-ref/Definition/Enum/PhaseEndType.cs @@ -0,0 +1,13 @@ +namespace GeometryTD.Definition +{ + /// + /// 关卡阶段结束类型 + /// + public enum PhaseEndType : byte + { + None = 0, + TimeElapsed = 1, + EnemiesCleared = 2, + BossDead = 3 + } +} diff --git a/src-ref/Definition/Enum/ProcedureMainCombatEntryBlockReason.cs b/src-ref/Definition/Enum/ProcedureMainCombatEntryBlockReason.cs new file mode 100644 index 0000000..a3feef7 --- /dev/null +++ b/src-ref/Definition/Enum/ProcedureMainCombatEntryBlockReason.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public enum ProcedureMainCombatEntryBlockReason + { + None = 0, + InventoryUnavailable = 1, + NoValidParticipantTower = 2 + } +} \ No newline at end of file diff --git a/src-ref/Definition/Enum/ProcedureMainFlowPhase.cs b/src-ref/Definition/Enum/ProcedureMainFlowPhase.cs new file mode 100644 index 0000000..d9305ba --- /dev/null +++ b/src-ref/Definition/Enum/ProcedureMainFlowPhase.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public enum ProcedureMainFlowPhase + { + Hub = 0, + NodeActive = 1, + RunCompletedPendingFinish = 2 + } +} \ No newline at end of file diff --git a/src-ref/Definition/Enum/ProcedureMainRunAdvanceResult.cs b/src-ref/Definition/Enum/ProcedureMainRunAdvanceResult.cs new file mode 100644 index 0000000..6fe78cc --- /dev/null +++ b/src-ref/Definition/Enum/ProcedureMainRunAdvanceResult.cs @@ -0,0 +1,10 @@ +namespace GeometryTD.Definition +{ + public enum ProcedureMainRunAdvanceResult + { + NoChange = 0, + NodeException = 1, + AdvancedToNextNode = 2, + RunCompleted = 3 + } +} \ No newline at end of file diff --git a/src-ref/Definition/Enum/ProcedureMainRunCompletionResult.cs b/src-ref/Definition/Enum/ProcedureMainRunCompletionResult.cs new file mode 100644 index 0000000..f5fe2f6 --- /dev/null +++ b/src-ref/Definition/Enum/ProcedureMainRunCompletionResult.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public enum ProcedureMainRunCompletionResult + { + NoChange = 0, + ShowCompletionDialog = 1, + ReturnToMenu = 2 + } +} \ No newline at end of file diff --git a/src-ref/Definition/Enum/RarityType.cs b/src-ref/Definition/Enum/RarityType.cs new file mode 100644 index 0000000..3cd10df --- /dev/null +++ b/src-ref/Definition/Enum/RarityType.cs @@ -0,0 +1,12 @@ +namespace GeometryTD.Definition +{ + public enum RarityType + { + None = 0, + White = 1, + Green = 2, + Blue = 3, + Purple = 4, + Red = 5 + } +} \ No newline at end of file diff --git a/src-ref/Definition/Enum/RelationType.cs b/src-ref/Definition/Enum/RelationType.cs new file mode 100644 index 0000000..cd56fc9 --- /dev/null +++ b/src-ref/Definition/Enum/RelationType.cs @@ -0,0 +1,28 @@ +namespace GeometryTD.Definition +{ + /// + /// 关系类型。 + /// + public enum RelationType : byte + { + /// + /// 未知的。 + /// + Unknown, + + /// + /// 友好的。 + /// + Friendly, + + /// + /// 中立的。 + /// + Neutral, + + /// + /// 敌对的。 + /// + Hostile + } +} diff --git a/src-ref/Definition/Enum/RepoItemClickActionType.cs b/src-ref/Definition/Enum/RepoItemClickActionType.cs new file mode 100644 index 0000000..8e74193 --- /dev/null +++ b/src-ref/Definition/Enum/RepoItemClickActionType.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public enum RepoItemClickActionType + { + OpenDetail, + RemoveParticipant, + + } +} \ No newline at end of file diff --git a/src-ref/Definition/Enum/SceneType.cs b/src-ref/Definition/Enum/SceneType.cs new file mode 100644 index 0000000..9a96df7 --- /dev/null +++ b/src-ref/Definition/Enum/SceneType.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public enum SceneType : byte + { + Launch = 0, + Menu = 1, + Main = 2, + } +} \ No newline at end of file diff --git a/src-ref/Definition/Enum/TagCategory.cs b/src-ref/Definition/Enum/TagCategory.cs new file mode 100644 index 0000000..2cf6a22 --- /dev/null +++ b/src-ref/Definition/Enum/TagCategory.cs @@ -0,0 +1,12 @@ +namespace GeometryTD.Definition +{ + public enum TagCategory : byte + { + None = 0, + Status = 1, + NumericModifier = 2, + AttackShape = 3, + // Enhances another applied status on the same hit, but does not create its own enemy-held state. + StatusModifier = 4 + } +} diff --git a/src-ref/Definition/Enum/TagTriggerPhase.cs b/src-ref/Definition/Enum/TagTriggerPhase.cs new file mode 100644 index 0000000..cfa9d7f --- /dev/null +++ b/src-ref/Definition/Enum/TagTriggerPhase.cs @@ -0,0 +1,11 @@ +namespace GeometryTD.Definition +{ + public enum TagTriggerPhase : byte + { + None = 0, + OnBeforeHit = 1, + OnHit = 2, + OnAfterHit = 3, + OnKill = 4 + } +} diff --git a/src-ref/Definition/Enum/TagType.cs b/src-ref/Definition/Enum/TagType.cs new file mode 100644 index 0000000..28c0bec --- /dev/null +++ b/src-ref/Definition/Enum/TagType.cs @@ -0,0 +1,31 @@ +namespace GeometryTD.Definition +{ + public enum TagType : byte + { + None = 0, + + /// + /// 元素 + /// + Fire, + BurnSpread, + IgniteBurst, + Inferno, + + /// + /// 控制 + /// + Ice, + FreezeMask, + Shatter, + AbsoluteZero, + + /// + /// 穿透 + /// + Pierce, + Crit, + Overpenetrate, + Execution, + } +} \ No newline at end of file diff --git a/src-ref/Definition/Enum/TowerCompSlotType.cs b/src-ref/Definition/Enum/TowerCompSlotType.cs new file mode 100644 index 0000000..10737c8 --- /dev/null +++ b/src-ref/Definition/Enum/TowerCompSlotType.cs @@ -0,0 +1,11 @@ +namespace GeometryTD.Definition +{ + public enum TowerCompSlotType : byte + { + None = 0, + Muzzle = 1, + Bearing = 2, + Base = 3, + Accessory = 4, + } +} diff --git a/src-ref/Definition/Enum/UIFormType.cs b/src-ref/Definition/Enum/UIFormType.cs new file mode 100644 index 0000000..3d7553f --- /dev/null +++ b/src-ref/Definition/Enum/UIFormType.cs @@ -0,0 +1,85 @@ +namespace GeometryTD.Definition +{ + /// + /// 界面编号。 + /// + public enum UIFormType : byte + { + Undefined = 0, + + /// + /// 弹出框。 + /// + DialogForm = 1, + + /// + /// 主菜单。 + /// + MenuForm = 100, + + /// + /// 设置。 + /// + SettingForm = 101, + + /// + /// 关于。 + /// + AboutForm = 102, + + /// + /// 主界面。 + /// + MainForm = 110, + + /// + /// 仓库界面。 + /// + RepoForm = 111, + + /// + /// 节点地图界面。 + /// + NodeMapForm = 112, + + /// + /// 道具详细信息界面。 + /// + ItemDescForm = 113, + + /// + /// 奖励三选一界面。 + /// + RewardSelectForm = 114, + + /// + /// 事件节点界面。 + /// + EventForm = 130, + + /// + /// 战斗信息界面。 + /// + CombatInfoForm = 140, + + /// + /// 战斗结算界面。 + /// + CombatFinishForm = 141, + + /// + /// 战斗选择界面 + /// + CombatSelectForm = 142, + + /// + /// 商店界面。 + /// + ShopForm = 150, + + /// + /// 测试菜单。 + /// + TestMenuForm = 200, + } +} \ No newline at end of file diff --git a/src-ref/Definition/Event/EventEffect/AddGoldEffect.cs b/src-ref/Definition/Event/EventEffect/AddGoldEffect.cs new file mode 100644 index 0000000..a3ed1f7 --- /dev/null +++ b/src-ref/Definition/Event/EventEffect/AddGoldEffect.cs @@ -0,0 +1,25 @@ +namespace GeometryTD.Definition +{ + public class AddGoldEffect : EventEffectBase + { + public override EventEffectType EffectType => EventEffectType.AddGold; + public override EventEffectParam Param => _param; + private AddGoldParam _param; + + public AddGoldEffect(AddGoldParam param, float? probability = null) + { + _param = param; + Probability = probability; + } + } + + public class AddGoldParam : EventEffectParam + { + public int Count; + + public AddGoldParam(int count) + { + Count = count; + } + } +} diff --git a/src-ref/Definition/Event/EventEffect/AddRandomCompsEffect.cs b/src-ref/Definition/Event/EventEffect/AddRandomCompsEffect.cs new file mode 100644 index 0000000..d711a20 --- /dev/null +++ b/src-ref/Definition/Event/EventEffect/AddRandomCompsEffect.cs @@ -0,0 +1,48 @@ +namespace GeometryTD.Definition +{ + public class AddRandomCompsEffect : EventEffectBase + { + public override EventEffectType EffectType => EventEffectType.AddRandomComps; + public override EventEffectParam Param => _param; + private AddRandomCompsParam _param; + + public AddRandomCompsEffect(AddRandomCompsParam param, float? probability = null) + { + _param = param; + Probability = probability; + } + } + + + public class AddRandomCompsParam : EventEffectParam + { + public int Count; + public RarityType Rarity; + public RarityType MinRarity; + public RarityType MaxRarity; + + public AddRandomCompsParam(int count, RarityType rarity) + { + Count = count; + RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity); + Rarity = normalizedRarity; + MinRarity = normalizedRarity; + MaxRarity = normalizedRarity; + } + + public AddRandomCompsParam(int count, RarityType minRarity, RarityType maxRarity) + { + Count = count; + MinRarity = InventoryRarityRuleService.NormalizeComponentRarity(minRarity); + MaxRarity = InventoryRarityRuleService.NormalizeComponentRarity(maxRarity); + if (MinRarity > MaxRarity) + { + throw new System.ArgumentOutOfRangeException( + nameof(minRarity), + $"AddRandomComps rarity range is invalid: {MinRarity} > {MaxRarity}."); + } + + Rarity = MinRarity == MaxRarity ? MinRarity : RarityType.None; + } + } +} diff --git a/src-ref/Definition/Event/EventEffect/DamageRandomTowerEnduranceEffect.cs b/src-ref/Definition/Event/EventEffect/DamageRandomTowerEnduranceEffect.cs new file mode 100644 index 0000000..6dcddfa --- /dev/null +++ b/src-ref/Definition/Event/EventEffect/DamageRandomTowerEnduranceEffect.cs @@ -0,0 +1,34 @@ +namespace GeometryTD.Definition +{ + public class DamageRandomTowerEnduranceEffect : EventEffectBase + { + public override EventEffectType EffectType => EventEffectType.DamageRandomTowersEndurance; + public override EventEffectParam Param => _param; + private DamageRandomTowerEnduranceParam _param; + + public DamageRandomTowerEnduranceEffect(DamageRandomTowerEnduranceParam param, float? probability = null) + { + _param = param; + Probability = probability; + } + } + + public class DamageRandomTowerEnduranceParam : EventEffectParam + { + /// + /// 减少耐久的防御塔数量 + /// + public int Count; + + /// + /// 防御塔耐久减少的量 + /// + public int Amount; + + public DamageRandomTowerEnduranceParam(int count, int amount) + { + Count = count; + Amount = amount; + } + } +} diff --git a/src-ref/Definition/Event/EventEffect/EventEffectBase.cs b/src-ref/Definition/Event/EventEffect/EventEffectBase.cs new file mode 100644 index 0000000..e80516a --- /dev/null +++ b/src-ref/Definition/Event/EventEffect/EventEffectBase.cs @@ -0,0 +1,14 @@ +namespace GeometryTD.Definition +{ + public abstract class EventEffectBase + { + public abstract EventEffectType EffectType { get; } + public abstract EventEffectParam Param { get; } + public float? Probability; + } + + public class EventEffectParam + { + + } +} \ No newline at end of file diff --git a/src-ref/Definition/Event/EventEffect/RemoveRandomCompEffect.cs b/src-ref/Definition/Event/EventEffect/RemoveRandomCompEffect.cs new file mode 100644 index 0000000..7ac22d5 --- /dev/null +++ b/src-ref/Definition/Event/EventEffect/RemoveRandomCompEffect.cs @@ -0,0 +1,27 @@ +namespace GeometryTD.Definition +{ + public class RemoveRandomCompsEffect : EventEffectBase + { + public override EventEffectType EffectType => EventEffectType.RemoveRandomComps; + public override EventEffectParam Param => _param; + private RemoveRandomCompsParam _param; + + public RemoveRandomCompsEffect(RemoveRandomCompsParam param, float? probability = null) + { + _param = param; + Probability = probability; + } + } + + public class RemoveRandomCompsParam : EventEffectParam + { + public int Count; + public RarityType Rarity; + + public RemoveRandomCompsParam(int Count, RarityType Rarity) + { + this.Count = Count; + this.Rarity = Rarity; + } + } +} diff --git a/src-ref/Definition/Event/EventOptionExecutor.cs b/src-ref/Definition/Event/EventOptionExecutor.cs new file mode 100644 index 0000000..b94fe72 --- /dev/null +++ b/src-ref/Definition/Event/EventOptionExecutor.cs @@ -0,0 +1,490 @@ +using System; +using System.Collections.Generic; +using GeometryTD.CustomComponent; +using GeometryTD.CustomUtility; +using GeometryTD.Procedure; +using UnityEngine; + +namespace GeometryTD.Definition +{ + public sealed class EventOptionExecutor + { + public EventOptionAvailability EvaluateOption(EventOption option, BackpackInventoryData inventory) + { + if (option == null) + { + return EventOptionAvailability.Blocked("选项配置无效"); + } + + EventRequirementBase[] requirements = option.Requirements ?? Array.Empty(); + for (int i = 0; i < requirements.Length; i++) + { + EventRequirementBase requirement = requirements[i]; + if (requirement == null) + { + continue; + } + + if (!IsRequirementSatisfied(requirement, inventory)) + { + return EventOptionAvailability.Blocked(BuildBlockedReason(requirement)); + } + } + + return EventOptionAvailability.Selectable(); + } + + public EventOptionExecutionResult Execute( + EventItem eventItem, + int optionIndex, + RunNodeExecutionContext context, + BackpackInventoryData workingInventory) + { + if (eventItem == null || workingInventory == null) + { + return EventOptionExecutionResult.Rejected("事件数据无效"); + } + + if (optionIndex < 0 || optionIndex >= eventItem.Options.Length) + { + return EventOptionExecutionResult.Rejected("事件选项索引无效"); + } + + EventOption option = eventItem.Options[optionIndex]; + EventOptionAvailability availability = EvaluateOption(option, workingInventory); + if (!availability.IsSelectable) + { + return EventOptionExecutionResult.Rejected(availability.BlockedReason); + } + + ApplyEffects(option.CostEffects, eventItem, optionIndex, context, workingInventory, isRewardPhase: false); + + bool isProbabilitySuccess = RollProbability(eventItem.Id, optionIndex, context, option.Probability); + if (isProbabilitySuccess) + { + ApplyEffects(option.RewardEffects, eventItem, optionIndex, context, workingInventory, isRewardPhase: true); + } + + return EventOptionExecutionResult.Accepted(isProbabilitySuccess); + } + + private void ApplyEffects( + EventEffectBase[] effects, + EventItem eventItem, + int optionIndex, + RunNodeExecutionContext context, + BackpackInventoryData workingInventory, + bool isRewardPhase) + { + if (effects == null || effects.Length <= 0) + { + return; + } + + for (int i = 0; i < effects.Length; i++) + { + EventEffectBase effect = effects[i]; + if (effect == null) + { + continue; + } + + switch (effect.EffectType) + { + case EventEffectType.AddGold: + ApplyAddGoldEffect((AddGoldParam)effect.Param, workingInventory); + break; + case EventEffectType.RemoveRandomComps: + ApplyRemoveRandomComponentsEffect( + (RemoveRandomCompsParam)effect.Param, + eventItem.Id, + optionIndex, + context, + workingInventory, + i); + break; + case EventEffectType.AddRandomComps: + ApplyAddRandomComponentsEffect( + (AddRandomCompsParam)effect.Param, + eventItem.Id, + optionIndex, + context, + workingInventory, + i); + break; + case EventEffectType.DamageRandomTowersEndurance: + ApplyDamageRandomTowerEnduranceEffect( + (DamageRandomTowerEnduranceParam)effect.Param, + eventItem.Id, + optionIndex, + context, + workingInventory, + i); + break; + default: + throw new InvalidOperationException( + $"Unsupported event effect at runtime: {effect.EffectType} (rewardPhase={isRewardPhase})."); + } + } + } + + private static void ApplyAddGoldEffect(AddGoldParam param, BackpackInventoryData workingInventory) + { + int nextGold = workingInventory.Gold + (param?.Count ?? 0); + if (nextGold < 0) + { + throw new InvalidOperationException("Event gold effect would reduce gold below zero."); + } + + workingInventory.Gold = nextGold; + } + + private static void ApplyRemoveRandomComponentsEffect( + RemoveRandomCompsParam param, + int eventId, + int optionIndex, + RunNodeExecutionContext context, + BackpackInventoryData workingInventory, + int effectIndex) + { + int removeCount = Mathf.Max(0, param?.Count ?? 0); + if (removeCount <= 0) + { + return; + } + + List candidates = CollectLooseComponents(workingInventory, param.Rarity); + if (candidates.Count < removeCount) + { + throw new InvalidOperationException("Event component removal effect does not have enough candidates."); + } + + System.Random random = CreateRandom(context, eventId, optionIndex, effectIndex, 101); + Shuffle(candidates, random); + for (int i = 0; i < removeCount; i++) + { + RemoveComponentByInstanceId(workingInventory, candidates[i]); + } + } + + private static void ApplyAddRandomComponentsEffect( + AddRandomCompsParam param, + int eventId, + int optionIndex, + RunNodeExecutionContext context, + BackpackInventoryData workingInventory, + int effectIndex) + { + int addCount = Mathf.Max(0, param?.Count ?? 0); + if (addCount <= 0) + { + return; + } + + if (GameEntry.InventoryGeneration == null) + { + throw new InvalidOperationException("Event component generation requires InventoryGenerationComponent."); + } + + IReadOnlyList generatedComponents = GameEntry.InventoryGeneration.BuildEventRewardComponents( + addCount, + param.MinRarity, + param.MaxRarity, + context?.RunSeed ?? 0, + context?.SequenceIndex ?? -1, + eventId, + optionIndex, + effectIndex); + for (int i = 0; i < generatedComponents.Count; i++) + { + AddComponentToInventory(workingInventory, generatedComponents[i]); + } + } + + private static void ApplyDamageRandomTowerEnduranceEffect( + DamageRandomTowerEnduranceParam param, + int eventId, + int optionIndex, + RunNodeExecutionContext context, + BackpackInventoryData workingInventory, + int effectIndex) + { + int towerCount = Mathf.Max(0, param?.Count ?? 0); + float enduranceLoss = Mathf.Max(0, param?.Amount ?? 0); + if (towerCount <= 0 || enduranceLoss <= 0f || workingInventory.Towers == null || workingInventory.Towers.Count <= 0) + { + return; + } + + List candidateTowerIds = new List(workingInventory.Towers.Count); + for (int i = 0; i < workingInventory.Towers.Count; i++) + { + TowerItemData tower = workingInventory.Towers[i]; + if (tower != null && tower.InstanceId > 0) + { + candidateTowerIds.Add(tower.InstanceId); + } + } + + if (candidateTowerIds.Count <= 0) + { + return; + } + + System.Random random = CreateRandom(context, eventId, optionIndex, effectIndex, 211); + Shuffle(candidateTowerIds, random); + int resolvedCount = Mathf.Min(towerCount, candidateTowerIds.Count); + List selectedTowerIds = candidateTowerIds.GetRange(0, resolvedCount); + InventoryTowerEnduranceUtility.ReduceTowerEndurance(workingInventory, selectedTowerIds, enduranceLoss); + } + + private static bool IsRequirementSatisfied(EventRequirementBase requirement, BackpackInventoryData inventory) + { + switch (requirement.RequirementType) + { + case EventRequirementType.GoldAtLeast: + return (inventory?.Gold ?? 0) >= ((GoldAtLeastParam)requirement.Param).Gold; + case EventRequirementType.CompCountAtLeast: + CompCountAtLeastParam compParam = (CompCountAtLeastParam)requirement.Param; + return CollectLooseComponents(inventory, compParam.Rarity).Count >= compParam.Count; + case EventRequirementType.TowerCountAtLeast: + return (inventory?.Towers?.Count ?? 0) >= ((TowerCountAtLeastParam)requirement.Param).Count; + default: + throw new InvalidOperationException( + $"Unsupported event requirement at runtime: {requirement.RequirementType}."); + } + } + + private static string BuildBlockedReason(EventRequirementBase requirement) + { + switch (requirement.RequirementType) + { + case EventRequirementType.GoldAtLeast: + return $"需要至少 {((GoldAtLeastParam)requirement.Param).Gold} 金币"; + case EventRequirementType.CompCountAtLeast: + CompCountAtLeastParam compParam = (CompCountAtLeastParam)requirement.Param; + return $"需要至少 {compParam.Count} 个未装配的{GetRarityText(compParam.Rarity)}组件"; + case EventRequirementType.TowerCountAtLeast: + return $"需要至少 {((TowerCountAtLeastParam)requirement.Param).Count} 座防御塔"; + default: + throw new InvalidOperationException( + $"Unsupported event requirement at runtime: {requirement.RequirementType}."); + } + } + + private static bool RollProbability(int eventId, int optionIndex, RunNodeExecutionContext context, float probability) + { + float clampedProbability = Mathf.Clamp01(probability); + if (clampedProbability >= 1f) + { + return true; + } + + if (clampedProbability <= 0f) + { + return false; + } + + System.Random random = CreateRandom(context, eventId, optionIndex, 0, 17); + return random.NextDouble() <= clampedProbability; + } + + private static System.Random CreateRandom( + RunNodeExecutionContext context, + int eventId, + int optionIndex, + int effectIndex, + int salt) + { + unchecked + { + int seed = 17; + seed = seed * 31 + (context?.RunSeed ?? 0); + seed = seed * 31 + (context?.SequenceIndex ?? -1); + seed = seed * 31 + eventId; + seed = seed * 31 + optionIndex; + seed = seed * 31 + effectIndex; + seed = seed * 31 + salt; + return new System.Random(seed); + } + } + + private static List CollectLooseComponents( + BackpackInventoryData inventory, + RarityType rarity) + { + List result = new List(); + if (inventory == null) + { + return result; + } + + RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity); + CollectFromList(result, inventory.MuzzleComponents, normalizedRarity); + CollectFromList(result, inventory.BearingComponents, normalizedRarity); + CollectFromList(result, inventory.BaseComponents, normalizedRarity); + return result; + } + + private static void CollectFromList( + List destination, + List source, + RarityType rarity) + where TComp : TowerCompItemData + { + if (source == null) + { + return; + } + + for (int i = 0; i < source.Count; i++) + { + TComp component = source[i]; + if (component == null || component.IsAssembledIntoTower) + { + continue; + } + + if (InventoryRarityRuleService.NormalizeComponentRarity(component.Rarity) != rarity) + { + continue; + } + + destination.Add(component); + } + } + + private static void RemoveComponentByInstanceId(BackpackInventoryData inventory, TowerCompItemData component) + { + switch (component) + { + case MuzzleCompItemData muzzleComp: + RemoveByInstanceId(inventory.MuzzleComponents, muzzleComp.InstanceId); + break; + case BearingCompItemData bearingComp: + RemoveByInstanceId(inventory.BearingComponents, bearingComp.InstanceId); + break; + case BaseCompItemData baseComp: + RemoveByInstanceId(inventory.BaseComponents, baseComp.InstanceId); + break; + default: + throw new InvalidOperationException($"Unsupported component type for event removal: {component?.GetType().Name}"); + } + } + + private static void AddComponentToInventory(BackpackInventoryData inventory, TowerCompItemData component) + { + if (component == null) + { + return; + } + + switch (component) + { + 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; + default: + throw new InvalidOperationException($"Unsupported component type for event addition: {component.GetType().Name}"); + } + } + + private static void RemoveByInstanceId(List components, long instanceId) + where TComp : TowerCompItemData + { + if (components == null) + { + return; + } + + for (int i = 0; i < components.Count; i++) + { + TComp component = components[i]; + if (component != null && component.InstanceId == instanceId) + { + components.RemoveAt(i); + return; + } + } + + throw new InvalidOperationException($"Failed to remove component instance #{instanceId} from inventory."); + } + + private static void Shuffle(IList values, System.Random random) + { + for (int i = values.Count - 1; i > 0; i--) + { + int swapIndex = random.Next(0, i + 1); + (values[i], values[swapIndex]) = (values[swapIndex], values[i]); + } + } + + private static string GetRarityText(RarityType rarity) + { + return InventoryRarityRuleService.NormalizeComponentRarity(rarity) switch + { + RarityType.White => "白色", + RarityType.Green => "绿色", + RarityType.Blue => "蓝色", + RarityType.Purple => "紫色", + RarityType.Red => "红色", + _ => "未知" + }; + } + } + + public sealed class EventOptionAvailability + { + private EventOptionAvailability(bool isSelectable, string blockedReason) + { + IsSelectable = isSelectable; + BlockedReason = blockedReason ?? string.Empty; + } + + public bool IsSelectable { get; } + + public string BlockedReason { get; } + + public static EventOptionAvailability Selectable() + { + return new EventOptionAvailability(true, string.Empty); + } + + public static EventOptionAvailability Blocked(string blockedReason) + { + return new EventOptionAvailability(false, blockedReason); + } + } + + public sealed class EventOptionExecutionResult + { + private EventOptionExecutionResult(bool isAccepted, bool isProbabilitySuccess, string failureReason) + { + IsAccepted = isAccepted; + IsProbabilitySuccess = isProbabilitySuccess; + FailureReason = failureReason ?? string.Empty; + } + + public bool IsAccepted { get; } + + public bool IsProbabilitySuccess { get; } + + public string FailureReason { get; } + + public static EventOptionExecutionResult Accepted(bool isProbabilitySuccess) + { + return new EventOptionExecutionResult(true, isProbabilitySuccess, string.Empty); + } + + public static EventOptionExecutionResult Rejected(string failureReason) + { + return new EventOptionExecutionResult(false, false, failureReason); + } + } +} diff --git a/src-ref/Definition/Event/EventRequirement/CompCountAtLeastRequirement.cs b/src-ref/Definition/Event/EventRequirement/CompCountAtLeastRequirement.cs new file mode 100644 index 0000000..d54a09b --- /dev/null +++ b/src-ref/Definition/Event/EventRequirement/CompCountAtLeastRequirement.cs @@ -0,0 +1,26 @@ +namespace GeometryTD.Definition +{ + public class CompCountAtLeastRequirement : EventRequirementBase + { + public override EventRequirementType RequirementType => EventRequirementType.CompCountAtLeast; + public override EventRequirementParam Param => _param; + private CompCountAtLeastParam _param; + + public CompCountAtLeastRequirement(CompCountAtLeastParam param) + { + _param = param; + } + } + + public class CompCountAtLeastParam : EventRequirementParam + { + public int Count; + public RarityType Rarity; + + public CompCountAtLeastParam(int count, RarityType rarity) + { + Count = count; + Rarity = rarity; + } + } +} diff --git a/src-ref/Definition/Event/EventRequirement/EventRequirementBase.cs b/src-ref/Definition/Event/EventRequirement/EventRequirementBase.cs new file mode 100644 index 0000000..179e497 --- /dev/null +++ b/src-ref/Definition/Event/EventRequirement/EventRequirementBase.cs @@ -0,0 +1,12 @@ +namespace GeometryTD.Definition +{ + public abstract class EventRequirementBase + { + public abstract EventRequirementType RequirementType { get; } + public abstract EventRequirementParam Param { get; } + } + + public class EventRequirementParam + { + } +} \ No newline at end of file diff --git a/src-ref/Definition/Event/EventRequirement/GoldAtLeastRequirement.cs b/src-ref/Definition/Event/EventRequirement/GoldAtLeastRequirement.cs new file mode 100644 index 0000000..13325c8 --- /dev/null +++ b/src-ref/Definition/Event/EventRequirement/GoldAtLeastRequirement.cs @@ -0,0 +1,24 @@ +namespace GeometryTD.Definition +{ + public class GoldAtLeastRequirement : EventRequirementBase + { + public override EventRequirementType RequirementType => EventRequirementType.GoldAtLeast; + public override EventRequirementParam Param => _param; + private GoldAtLeastParam _param; + + public GoldAtLeastRequirement(GoldAtLeastParam param) + { + _param = param; + } + } + + public class GoldAtLeastParam : EventRequirementParam + { + public int Gold; + + public GoldAtLeastParam(int gold) + { + Gold = gold; + } + } +} diff --git a/src-ref/Definition/Event/EventRequirement/HasRelicRequirement.cs b/src-ref/Definition/Event/EventRequirement/HasRelicRequirement.cs new file mode 100644 index 0000000..7aff552 --- /dev/null +++ b/src-ref/Definition/Event/EventRequirement/HasRelicRequirement.cs @@ -0,0 +1,24 @@ +namespace GeometryTD.Definition +{ + public class HasRelicRequirement : EventRequirementBase + { + public override EventRequirementType RequirementType => EventRequirementType.HasRelic; + public override EventRequirementParam Param => _param; + private HasRelicParam _param; + + public HasRelicRequirement(HasRelicParam param) + { + _param = param; + } + } + + public class HasRelicParam : EventRequirementParam + { + public int RelicId; + + public HasRelicParam(int relicId) + { + RelicId = relicId; + } + } +} diff --git a/src-ref/Definition/Event/EventRequirement/TowerCountAtLeastRequirement.cs b/src-ref/Definition/Event/EventRequirement/TowerCountAtLeastRequirement.cs new file mode 100644 index 0000000..d0d3723 --- /dev/null +++ b/src-ref/Definition/Event/EventRequirement/TowerCountAtLeastRequirement.cs @@ -0,0 +1,24 @@ +namespace GeometryTD.Definition +{ + public class TowerCountAtLeastRequirement : EventRequirementBase + { + public override EventRequirementType RequirementType => EventRequirementType.TowerCountAtLeast; + public override EventRequirementParam Param => _param; + private TowerCountAtLeastParam _param; + + public TowerCountAtLeastRequirement(TowerCountAtLeastParam param) + { + _param = param; + } + } + + public class TowerCountAtLeastParam : EventRequirementParam + { + public int Count; + + public TowerCountAtLeastParam(int count) + { + Count = count; + } + } +} diff --git a/src-ref/Definition/InventoryRarityRuleService.cs b/src-ref/Definition/InventoryRarityRuleService.cs new file mode 100644 index 0000000..8b14402 --- /dev/null +++ b/src-ref/Definition/InventoryRarityRuleService.cs @@ -0,0 +1,32 @@ +using UnityEngine; + +namespace GeometryTD.Definition +{ + public static class InventoryRarityRuleService + { + public static RarityType NormalizeComponentRarity(RarityType rarity) + { + if (rarity < RarityType.White || rarity > RarityType.Red) + { + return RarityType.White; + } + + return rarity; + } + + public static RarityType ResolveTowerRarity( + RarityType muzzleRarity, + RarityType bearingRarity, + RarityType baseRarity) + { + int normalizedMuzzle = (int)NormalizeComponentRarity(muzzleRarity); + int normalizedBearing = (int)NormalizeComponentRarity(bearingRarity); + int normalizedBase = (int)NormalizeComponentRarity(baseRarity); + + float average = (normalizedMuzzle + normalizedBearing + normalizedBase) / 3f; + int floored = Mathf.FloorToInt(average); + int clamped = Mathf.Clamp(floored, (int)RarityType.White, (int)RarityType.Red); + return (RarityType)clamped; + } + } +} diff --git a/src-ref/Definition/ParticipantTowerAssignResult.cs b/src-ref/Definition/ParticipantTowerAssignResult.cs new file mode 100644 index 0000000..91de081 --- /dev/null +++ b/src-ref/Definition/ParticipantTowerAssignResult.cs @@ -0,0 +1,22 @@ +namespace GeometryTD.Definition +{ + public enum ParticipantTowerAssignFailureReason + { + None = 0, + TowerMissing = 1, + InvalidTower = 2, + AlreadyAssigned = 3, + ParticipantAreaFull = 4 + } + + public sealed class ParticipantTowerAssignResult + { + public long TowerInstanceId { get; set; } + + public bool IsSuccess => FailureReason == ParticipantTowerAssignFailureReason.None; + + public ParticipantTowerAssignFailureReason FailureReason { get; set; } + + public CombatParticipantTowerValidationFailureReason ValidationFailureReason { get; set; } + } +} diff --git a/src-ref/Definition/Tag/Aggregation/TagRuntimeData.cs b/src-ref/Definition/Tag/Aggregation/TagRuntimeData.cs new file mode 100644 index 0000000..da34de2 --- /dev/null +++ b/src-ref/Definition/Tag/Aggregation/TagRuntimeData.cs @@ -0,0 +1,11 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class TagRuntimeData + { + public TagType TagType { get; set; } + public int TotalStack { get; set; } + } +} diff --git a/src-ref/Definition/Tag/Aggregation/TowerTagAggregationService.cs b/src-ref/Definition/Tag/Aggregation/TowerTagAggregationService.cs new file mode 100644 index 0000000..38d2934 --- /dev/null +++ b/src-ref/Definition/Tag/Aggregation/TowerTagAggregationService.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; + +namespace GeometryTD.Definition +{ + public static class TowerTagAggregationService + { + public static TagRuntimeData[] AggregateTowerTags(params TagType[][] componentTags) + { + Dictionary stackByTag = new Dictionary(); + if (componentTags != null) + { + foreach (var tags in componentTags) + { + if (tags == null) + { + continue; + } + + foreach (var tagType in tags) + { + if (tagType == TagType.None || !Enum.IsDefined(typeof(TagType), tagType)) + { + continue; + } + + if (stackByTag.TryGetValue(tagType, out int stack)) + { + stackByTag[tagType] = stack + 1; + } + else + { + stackByTag[tagType] = 1; + } + } + } + } + + if (stackByTag.Count <= 0) + { + return Array.Empty(); + } + + List runtimes = new List(stackByTag.Count); + foreach (var pair in stackByTag) + { + runtimes.Add(new TagRuntimeData + { + TagType = pair.Key, + TotalStack = Math.Max(1, pair.Value) + }); + } + + runtimes.Sort((left, right) => left.TagType.CompareTo(right.TagType)); + return runtimes.ToArray(); + } + + public static TagRuntimeData[] BuildRuntimeTagsFromUniqueTags(IReadOnlyList tags) + { + if (tags == null || tags.Count <= 0) + { + return Array.Empty(); + } + + HashSet uniqueTags = new HashSet(); + foreach (var tagType in tags) + { + if (tagType == TagType.None || !Enum.IsDefined(typeof(TagType), tagType)) + { + continue; + } + + uniqueTags.Add(tagType); + } + + if (uniqueTags.Count <= 0) + { + return Array.Empty(); + } + + TagType[] orderedTags = new TagType[uniqueTags.Count]; + uniqueTags.CopyTo(orderedTags); + Array.Sort(orderedTags); + + TagRuntimeData[] runtimes = new TagRuntimeData[orderedTags.Length]; + for (int i = 0; i < orderedTags.Length; i++) + { + runtimes[i] = new TagRuntimeData + { + TagType = orderedTags[i], + TotalStack = 1 + }; + } + + return runtimes; + } + + public static TagType[] FlattenUniqueTags(IReadOnlyList tagRuntimes) + { + if (tagRuntimes == null || tagRuntimes.Count <= 0) + { + return Array.Empty(); + } + + List tags = new List(tagRuntimes.Count); + foreach (var runtime in tagRuntimes) + { + if (runtime == null || runtime.TagType == TagType.None || !Enum.IsDefined(typeof(TagType), runtime.TagType)) + { + continue; + } + + if (runtime.TotalStack <= 0) + { + continue; + } + + tags.Add(runtime.TagType); + } + + if (tags.Count <= 0) + { + return Array.Empty(); + } + + tags.Sort(); + return tags.ToArray(); + } + } +} diff --git a/src-ref/Definition/Tag/Combat/EnemyStatusTagRegistry.cs b/src-ref/Definition/Tag/Combat/EnemyStatusTagRegistry.cs new file mode 100644 index 0000000..2388d54 --- /dev/null +++ b/src-ref/Definition/Tag/Combat/EnemyStatusTagRegistry.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using GeometryTD.Entity; +using UnityEngine; + +namespace GeometryTD.Definition +{ + public static class EnemyStatusTagRegistry + { + // Only tags that create or tick enemy-held status state belong here. + private static readonly Dictionary EffectsByTag = + new Dictionary + { + [TagType.Fire] = new FireTagEffect(), + [TagType.Ice] = new IceTagEffect() + }; + + public static IReadOnlyDictionary Effects => EffectsByTag; + + public static bool TryGetEffect(TagType tagType, out IEnemyStatusTagEffect effect) + { + return EffectsByTag.TryGetValue(tagType, out effect); + } + } +} diff --git a/src-ref/Definition/Tag/Combat/Handlers/AttackShapeTagEffectHandler.cs b/src-ref/Definition/Tag/Combat/Handlers/AttackShapeTagEffectHandler.cs new file mode 100644 index 0000000..3876229 --- /dev/null +++ b/src-ref/Definition/Tag/Combat/Handlers/AttackShapeTagEffectHandler.cs @@ -0,0 +1,41 @@ +namespace GeometryTD.Definition +{ + public static class AttackShapeTagEffectHandler + { + public static void Apply(HitContext hitContext, TagTriggerPhase triggerPhase) + { + if (hitContext?.AttackPayload?.TagRuntimes == null || hitContext.AttackPayload.TagRuntimes.Length <= 0) + { + return; + } + + for (int i = 0; i < hitContext.AttackPayload.TagRuntimes.Length; i++) + { + TagRuntimeData runtime = hitContext.AttackPayload.TagRuntimes[i]; + if (runtime == null || runtime.TotalStack <= 0) + { + continue; + } + + if (!TagDefinitionRegistry.TryGetDefinition(runtime.TagType, out TagDefinition definition) || + definition == null || + definition.Category != TagCategory.AttackShape || + definition.TriggerPhase != triggerPhase || + definition.Config == null) + { + continue; + } + + switch (runtime.TagType) + { + case TagType.BurnSpread: + case TagType.IgniteBurst: + case TagType.FreezeMask: + case TagType.Pierce: + case TagType.Overpenetrate: + break; + } + } + } + } +} diff --git a/src-ref/Definition/Tag/Combat/Handlers/NumericTagEffectHandler.cs b/src-ref/Definition/Tag/Combat/Handlers/NumericTagEffectHandler.cs new file mode 100644 index 0000000..6ab3232 --- /dev/null +++ b/src-ref/Definition/Tag/Combat/Handlers/NumericTagEffectHandler.cs @@ -0,0 +1,109 @@ +using UnityEngine; + +namespace GeometryTD.Definition +{ + public static class NumericTagEffectHandler + { + public static void ApplyBeforeHit(HitContext hitContext) + { + if (hitContext == null || hitContext.AttackPayload == null) + { + return; + } + + TagRuntimeData[] tagRuntimes = hitContext.AttackPayload.TagRuntimes; + if (tagRuntimes == null || tagRuntimes.Length <= 0) + { + return; + } + + for (int i = 0; i < tagRuntimes.Length; i++) + { + TagRuntimeData runtime = tagRuntimes[i]; + if (runtime == null || runtime.TotalStack <= 0) + { + continue; + } + + if (!TagDefinitionRegistry.TryGetDefinition(runtime.TagType, out TagDefinition definition) || + definition == null || + definition.Category != TagCategory.NumericModifier || + definition.TriggerPhase != TagTriggerPhase.OnBeforeHit || + definition.Config == null) + { + continue; + } + + switch (runtime.TagType) + { + case TagType.Crit: + ApplyCrit(hitContext, runtime.TotalStack, definition.Config as CritTagConfig); + break; + case TagType.Execution: + ApplyExecution(hitContext, runtime.TotalStack, definition.Config as ExecutionTagConfig); + break; + case TagType.Shatter: + ApplyShatter(hitContext, runtime.TotalStack, definition.Config as ShatterTagConfig); + break; + } + } + + hitContext.IsKilled = hitContext.TargetCurrentHealthBeforeHit > 0 && + hitContext.FinalDamage >= hitContext.TargetCurrentHealthBeforeHit; + } + + private static void ApplyCrit(HitContext hitContext, int stack, CritTagConfig config) + { + if (config == null || !config.IsImplemented || stack <= 0) + { + return; + } + + float chance = Mathf.Clamp01(stack * config.CritChancePerStack); + float resolvedCritRoll = hitContext.CritRoll ?? Random.value; + if (resolvedCritRoll > chance) + { + return; + } + + hitContext.FinalDamage = Mathf.Max(0, Mathf.RoundToInt(hitContext.FinalDamage * config.CritDamageMultiplier)); + hitContext.IsCriticalHit = true; + } + + private static void ApplyExecution( + HitContext hitContext, + int stack, + ExecutionTagConfig config) + { + if (config == null || !config.IsImplemented || stack <= 0 || hitContext.TargetMaxHealth <= 0) + { + return; + } + + float healthRatio = Mathf.Clamp01((float)Mathf.Max(0, hitContext.TargetCurrentHealthBeforeHit) / hitContext.TargetMaxHealth); + if (healthRatio > config.TargetHealthThreshold) + { + return; + } + + float multiplier = 1f + stack * config.DamageBonusPerStack; + hitContext.FinalDamage = Mathf.Max(0, Mathf.RoundToInt(hitContext.FinalDamage * multiplier)); + } + + private static void ApplyShatter(HitContext hitContext, int stack, ShatterTagConfig config) + { + if (config == null || !config.IsImplemented || stack <= 0) + { + return; + } + + if (config.RequiresSlowedTarget && !hitContext.HasSlowStatusBeforeHit) + { + return; + } + + float multiplier = 1f + stack * config.DamageBonusPerStack; + hitContext.FinalDamage = Mathf.Max(0, Mathf.RoundToInt(hitContext.FinalDamage * multiplier)); + } + } +} diff --git a/src-ref/Definition/Tag/Combat/States/EnemyStatusTagStateBase.cs b/src-ref/Definition/Tag/Combat/States/EnemyStatusTagStateBase.cs new file mode 100644 index 0000000..1b80158 --- /dev/null +++ b/src-ref/Definition/Tag/Combat/States/EnemyStatusTagStateBase.cs @@ -0,0 +1,7 @@ +namespace GeometryTD.Definition +{ + public abstract class EnemyStatusTagStateBase + { + } + +} \ No newline at end of file diff --git a/src-ref/Definition/Tag/Combat/States/FireTagState.cs b/src-ref/Definition/Tag/Combat/States/FireTagState.cs new file mode 100644 index 0000000..12bb3ed --- /dev/null +++ b/src-ref/Definition/Tag/Combat/States/FireTagState.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public sealed class FireTagState : EnemyStatusTagStateBase + { + public float RemainingDuration { get; set; } + public float DamagePerSecond { get; set; } + public float PendingDamage { get; set; } + } +} diff --git a/src-ref/Definition/Tag/Combat/States/IceTagState.cs b/src-ref/Definition/Tag/Combat/States/IceTagState.cs new file mode 100644 index 0000000..f99287c --- /dev/null +++ b/src-ref/Definition/Tag/Combat/States/IceTagState.cs @@ -0,0 +1,8 @@ +namespace GeometryTD.Definition +{ + public sealed class IceTagState : EnemyStatusTagStateBase + { + public float RemainingDuration { get; set; } + public float SlowMultiplier { get; set; } = 1f; + } +} \ No newline at end of file diff --git a/src-ref/Definition/Tag/Combat/StatusEffects/EnemyStatusTagEffectBase.cs b/src-ref/Definition/Tag/Combat/StatusEffects/EnemyStatusTagEffectBase.cs new file mode 100644 index 0000000..6938d57 --- /dev/null +++ b/src-ref/Definition/Tag/Combat/StatusEffects/EnemyStatusTagEffectBase.cs @@ -0,0 +1,30 @@ +using System; +using GeometryTD.Entity; + +namespace GeometryTD.Definition +{ + public abstract class EnemyStatusTagEffectBase : IEnemyStatusTagEffect + { + public abstract TagType TagType { get; } + + public virtual void Apply(HitContext hitContext, TagRuntimeData runtimeData) + { + _ = hitContext; + _ = runtimeData; + } + + public virtual bool Tick(EnemyTagStatusRuntime runtime, float deltaTime, Action applyDamage) + { + _ = runtime; + _ = deltaTime; + _ = applyDamage; + return false; + } + + public virtual float GetMoveSpeedMultiplier(EnemyTagStatusRuntime runtime) + { + _ = runtime; + return 1f; + } + } +} diff --git a/src-ref/Definition/Tag/Combat/StatusEffects/FireTagEffect.cs b/src-ref/Definition/Tag/Combat/StatusEffects/FireTagEffect.cs new file mode 100644 index 0000000..91de299 --- /dev/null +++ b/src-ref/Definition/Tag/Combat/StatusEffects/FireTagEffect.cs @@ -0,0 +1,75 @@ +using System; +using GeometryTD.Entity; +using UnityEngine; + +namespace GeometryTD.Definition +{ + public sealed class FireTagEffect : EnemyStatusTagEffectBase + { + public override TagType TagType => TagType.Fire; + + public override void Apply(HitContext hitContext, TagRuntimeData runtimeData) + { + Debug.Assert(TagDefinitionRegistry.TryGetDefinition(TagType, out TagDefinition definition)); + Debug.Assert(hitContext != null); + Debug.Assert(hitContext.TargetStatusRuntime != null); + + FireTagConfig config = definition.Config as FireTagConfig; + Debug.Assert(config != null); + if (!config.IsImplemented) + { + return; + } + + EnemyTagStatusRuntime runtime = hitContext.TargetStatusRuntime; + FireTagState state = runtime.GetOrCreateState(TagType); + int effectiveStack = Mathf.Max(0, Mathf.Min(runtimeData.TotalStack, config.MaxEffectiveStack)); + HitStatusModifierContext modifierContext = hitContext.StatusModifierContext ?? new HitStatusModifierContext(); + float burnDamagePerSecond = Mathf.Max( + 0f, + config.BurnDamagePerSecondPerStack * effectiveStack + modifierContext.BonusBurnDamagePerSecond); + + if (burnDamagePerSecond <= 0f) + { + return; + } + + state.RemainingDuration = Mathf.Max( + state.RemainingDuration, + config.BurnDurationSeconds + modifierContext.BonusBurnDurationSeconds); + state.DamagePerSecond = Mathf.Max(state.DamagePerSecond, burnDamagePerSecond); + runtime.Activate(TagType); + } + + public override bool Tick(EnemyTagStatusRuntime runtime, float deltaTime, Action applyDamage) + { + FireTagState state = runtime.GetState(TagType); + Debug.Assert(state != null); + + float resolvedDeltaTime = Mathf.Max(0f, deltaTime); + if (resolvedDeltaTime <= 0f || state.RemainingDuration <= 0f) + { + return state.RemainingDuration > 0f; + } + + float appliedDuration = Mathf.Min(resolvedDeltaTime, state.RemainingDuration); + state.RemainingDuration = Mathf.Max(0f, state.RemainingDuration - appliedDuration); + state.PendingDamage += state.DamagePerSecond * appliedDuration; + + int resolvedDamage = Mathf.FloorToInt(state.PendingDamage); + if (resolvedDamage > 0) + { + applyDamage?.Invoke(resolvedDamage); + state.PendingDamage -= resolvedDamage; + } + + if (state.RemainingDuration > 0f) + { + return true; + } + + state.PendingDamage = 0f; + return false; + } + } +} diff --git a/src-ref/Definition/Tag/Combat/StatusEffects/IEnemyStatusTagEffect.cs b/src-ref/Definition/Tag/Combat/StatusEffects/IEnemyStatusTagEffect.cs new file mode 100644 index 0000000..2a3bc0b --- /dev/null +++ b/src-ref/Definition/Tag/Combat/StatusEffects/IEnemyStatusTagEffect.cs @@ -0,0 +1,13 @@ +using System; +using GeometryTD.Entity; + +namespace GeometryTD.Definition +{ + public interface IEnemyStatusTagEffect + { + TagType TagType { get; } + void Apply(HitContext hitContext, TagRuntimeData runtimeData); + bool Tick(EnemyTagStatusRuntime runtime, float deltaTime, Action applyDamage); + float GetMoveSpeedMultiplier(EnemyTagStatusRuntime runtime); + } +} diff --git a/src-ref/Definition/Tag/Combat/StatusEffects/IceTagEffect.cs b/src-ref/Definition/Tag/Combat/StatusEffects/IceTagEffect.cs new file mode 100644 index 0000000..f0b1f14 --- /dev/null +++ b/src-ref/Definition/Tag/Combat/StatusEffects/IceTagEffect.cs @@ -0,0 +1,54 @@ +using System; +using GeometryTD.Entity; +using UnityEngine; + +namespace GeometryTD.Definition +{ + public sealed class IceTagEffect : EnemyStatusTagEffectBase + { + public override TagType TagType => TagType.Ice; + + public override void Apply(HitContext hitContext, TagRuntimeData runtimeData) + { + Debug.Assert(hitContext != null); + Debug.Assert(hitContext.TargetStatusRuntime != null); + Debug.Assert(runtimeData != null); + Debug.Assert(runtimeData.TotalStack > 0); + Debug.Assert(TagDefinitionRegistry.TryGetDefinition(TagType, out TagDefinition definition)); + + IceTagConfig config = definition.Config as IceTagConfig; + Debug.Assert(config != null); + if (!config.IsImplemented) + { + return; + } + + EnemyTagStatusRuntime runtime = hitContext.TargetStatusRuntime; + IceTagState state = runtime.GetOrCreateState(TagType); + HitStatusModifierContext modifierContext = hitContext.StatusModifierContext ?? new HitStatusModifierContext(); + float slowRatio = runtimeData.TotalStack * config.SlowRatioPerStack + modifierContext.BonusSlowRatio; + float slowMultiplier = Mathf.Max(config.MinMoveSpeedMultiplier, 1f - slowRatio); + state.RemainingDuration = Mathf.Max( + state.RemainingDuration, + config.SlowDurationSeconds + modifierContext.BonusSlowDurationSeconds); + state.SlowMultiplier = Mathf.Min(state.SlowMultiplier, slowMultiplier); + runtime.Activate(TagType); + } + + public override bool Tick(EnemyTagStatusRuntime runtime, float deltaTime, Action applyDamage) + { + _ = applyDamage; + IceTagState state = runtime.GetState(TagType); + Debug.Assert(state != null); + + state.RemainingDuration = Mathf.Max(0f, state.RemainingDuration - Mathf.Max(0f, deltaTime)); + return state.RemainingDuration > 0f; + } + + public override float GetMoveSpeedMultiplier(EnemyTagStatusRuntime runtime) + { + IceTagState state = runtime.GetState(TagType); + return state == null ? 1f : state.SlowMultiplier; + } + } +} diff --git a/src-ref/Definition/Tag/Combat/TagEffectResolver.cs b/src-ref/Definition/Tag/Combat/TagEffectResolver.cs new file mode 100644 index 0000000..1c0d3f0 --- /dev/null +++ b/src-ref/Definition/Tag/Combat/TagEffectResolver.cs @@ -0,0 +1,177 @@ +using GeometryTD.Entity; +using UnityEngine; + +namespace GeometryTD.Definition +{ + public static class TagEffectResolver + { + private static readonly HitStatusModifierContext EmptyStatusModifierContext = new HitStatusModifierContext(); + + public static HitContext ResolveBeforeHit(HitContext hitContext) + { + if (hitContext == null) + { + return new HitContext + { + AttackPayload = new AttackPayload() + }; + } + + AttackPayload resolvedPayload = hitContext.AttackPayload?.Clone() ?? new AttackPayload(); + HitContext resolvedContext = new HitContext + { + AttackPayload = resolvedPayload, + FinalDamage = Mathf.Max(0, resolvedPayload.BaseDamage), + IsCriticalHit = false, + IsKilled = hitContext.TargetCurrentHealthBeforeHit > 0 && + resolvedPayload.BaseDamage >= hitContext.TargetCurrentHealthBeforeHit, + TargetEntityId = hitContext.TargetEntityId, + TargetPosition = hitContext.TargetPosition, + TargetCurrentHealthBeforeHit = hitContext.TargetCurrentHealthBeforeHit, + TargetCurrentHealthAfterHit = hitContext.TargetCurrentHealthAfterHit, + TargetMaxHealth = hitContext.TargetMaxHealth, + TargetMoveSpeedMultiplierBeforeHit = hitContext.TargetMoveSpeedMultiplierBeforeHit, + TargetStatusTagsBeforeHit = hitContext.TargetStatusTagsBeforeHit ?? System.Array.Empty(), + TargetStatusRuntime = hitContext.TargetStatusRuntime, + CritRoll = hitContext.CritRoll, + StatusModifierContext = new HitStatusModifierContext() + }; + + NumericTagEffectHandler.ApplyBeforeHit(resolvedContext); + return resolvedContext; + } + + public static void ApplyAfterHit(HitContext hitContext) + { + if (hitContext?.StatusModifierContext == null) + { + if (hitContext != null) + { + hitContext.StatusModifierContext = new HitStatusModifierContext(); + } + } + + hitContext?.StatusModifierContext?.Reset(); + ApplyStatusModifiers(hitContext); + + if (hitContext?.AttackPayload != null && hitContext.TargetStatusRuntime != null) + { + TagRuntimeData[] tagRuntimes = hitContext.AttackPayload.TagRuntimes; + if (tagRuntimes != null && tagRuntimes.Length > 0) + { + foreach (var tag in tagRuntimes) + { + if (tag == null || tag.TotalStack <= 0) + { + continue; + } + + if (!TagDefinitionRegistry.TryGetDefinition(tag.TagType, out TagDefinition definition) || + definition == null || + definition.Category != TagCategory.Status || + definition.TriggerPhase != TagTriggerPhase.OnAfterHit || + definition.Config == null || + !EnemyStatusTagRegistry.TryGetEffect(tag.TagType, out IEnemyStatusTagEffect effect)) + { + continue; + } + + effect.Apply(hitContext, tag); + } + } + } + + AttackShapeTagEffectHandler.Apply(hitContext, TagTriggerPhase.OnAfterHit); + } + + public static void ApplyOnHit(HitContext hitContext) + { + AttackShapeTagEffectHandler.Apply(hitContext, TagTriggerPhase.OnHit); + } + + public static void ApplyOnKill(HitContext hitContext) + { + AttackShapeTagEffectHandler.Apply(hitContext, TagTriggerPhase.OnKill); + } + + public static int GetTagStack(TagRuntimeData[] tagRuntimes, TagType tagType) + { + if (tagRuntimes == null || tagRuntimes.Length <= 0 || tagType == TagType.None) + { + return 0; + } + + foreach (var tag in tagRuntimes) + { + if (tag == null || tag.TagType != tagType || tag.TotalStack <= 0) + { + continue; + } + + return tag.TotalStack; + } + + return 0; + } + + private static void ApplyStatusModifiers(HitContext hitContext) + { + if (hitContext?.AttackPayload?.TagRuntimes == null || hitContext.AttackPayload.TagRuntimes.Length <= 0) + { + return; + } + + HitStatusModifierContext modifierContext = hitContext.StatusModifierContext ?? EmptyStatusModifierContext; + for (int i = 0; i < hitContext.AttackPayload.TagRuntimes.Length; i++) + { + TagRuntimeData runtime = hitContext.AttackPayload.TagRuntimes[i]; + if (runtime == null || runtime.TotalStack <= 0) + { + continue; + } + + if (!TagDefinitionRegistry.TryGetDefinition(runtime.TagType, out TagDefinition definition) || + definition == null || + definition.Category != TagCategory.StatusModifier || + definition.TriggerPhase != TagTriggerPhase.OnAfterHit || + definition.Config == null || + !definition.Config.IsImplemented) + { + continue; + } + + switch (runtime.TagType) + { + case TagType.Inferno: + ApplyInfernoModifier(runtime.TotalStack, definition.Config as InfernoTagConfig, modifierContext); + break; + case TagType.AbsoluteZero: + ApplyAbsoluteZeroModifier(runtime.TotalStack, definition.Config as AbsoluteZeroTagConfig, modifierContext); + break; + } + } + } + + private static void ApplyInfernoModifier(int stack, InfernoTagConfig config, HitStatusModifierContext modifierContext) + { + if (config == null || stack <= 0) + { + return; + } + + modifierContext.BonusBurnDurationSeconds += stack * config.BonusBurnDurationSeconds; + modifierContext.BonusBurnDamagePerSecond += stack * config.BonusBurnDamagePerSecondPerStack; + } + + private static void ApplyAbsoluteZeroModifier(int stack, AbsoluteZeroTagConfig config, HitStatusModifierContext modifierContext) + { + if (config == null || stack <= 0) + { + return; + } + + modifierContext.BonusSlowDurationSeconds += stack * config.BonusSlowDurationSeconds; + modifierContext.BonusSlowRatio += stack * config.BonusSlowRatioPerStack; + } + } +} diff --git a/src-ref/Definition/Tag/Generation/ComponentTagGenerationService.cs b/src-ref/Definition/Tag/Generation/ComponentTagGenerationService.cs new file mode 100644 index 0000000..4a8fa8f --- /dev/null +++ b/src-ref/Definition/Tag/Generation/ComponentTagGenerationService.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using Random = System.Random; + +namespace GeometryTD.Definition +{ + public static class ComponentTagGenerationService + { + public static TagType[] ResolveComponentTags( + IReadOnlyList possibleTags, + RarityType rarity, + InventoryTagRandomContext randomContext, + IReadOnlyDictionary rulesByTag = null, + IReadOnlyDictionary rarityTagBudgetRulesByRarity = null) + { + IReadOnlyDictionary ruleLookup = rulesByTag ?? TagGenerationRuleRegistry.Rules; + TagType[] eligibleTags = GetEligibleTags(possibleTags, rarity, ruleLookup); + if (eligibleTags.Length <= 0) + { + return Array.Empty(); + } + + Random random = new Random(BuildStableSeed(rarity, randomContext)); + int tagBudget = ResolveRarityTagBudget(rarity, random, rarityTagBudgetRulesByRarity); + if (tagBudget <= 0) + { + return Array.Empty(); + } + + int finalCount = Math.Min(tagBudget, eligibleTags.Length); + List pool = new List(eligibleTags); + TagType[] result = new TagType[finalCount]; + for (int i = 0; i < finalCount; i++) + { + int index = RollWeightedIndex(pool, ruleLookup, random); + result[i] = pool[index]; + pool.RemoveAt(index); + } + + return result; + } + + public static TagType[] ResolveComponentTags( + IReadOnlyList possibleTags, + RarityType rarity, + InventoryTagSourceType sourceType, + long itemInstanceId, + int configId, + IReadOnlyDictionary rulesByTag = null, + IReadOnlyDictionary rarityTagBudgetRulesByRarity = null) + { + return ResolveComponentTags( + possibleTags, + rarity, + new InventoryTagRandomContext(0, sourceType, itemInstanceId, configId), + rulesByTag, + rarityTagBudgetRulesByRarity); + } + + public static TagType[] GetEligibleTags( + IReadOnlyList possibleTags, + RarityType rarity, + IReadOnlyDictionary rulesByTag = null) + { + if (possibleTags == null || possibleTags.Count <= 0) + { + return Array.Empty(); + } + + RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity); + IReadOnlyDictionary ruleLookup = rulesByTag ?? TagGenerationRuleRegistry.Rules; + HashSet uniqueTags = new HashSet(); + + for (int i = 0; i < possibleTags.Count; i++) + { + TagType tagType = possibleTags[i]; + if (tagType == TagType.None || !Enum.IsDefined(typeof(TagType), tagType) || !IsSupportedLaunchTag(tagType)) + { + continue; + } + + if (!TryGetRule(tagType, ruleLookup, out TagGenerationRule rule)) + { + continue; + } + + if (normalizedRarity < InventoryRarityRuleService.NormalizeComponentRarity(rule.MinRarity)) + { + continue; + } + + uniqueTags.Add(tagType); + } + + if (uniqueTags.Count <= 0) + { + return Array.Empty(); + } + + TagType[] result = new TagType[uniqueTags.Count]; + uniqueTags.CopyTo(result); + Array.Sort(result); + return result; + } + + public static int ResolveRarityTagBudget( + RarityType rarity, + Random random, + IReadOnlyDictionary rarityTagBudgetRulesByRarity = null) + { + RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity); + IReadOnlyDictionary ruleLookup = rarityTagBudgetRulesByRarity ?? RarityTagBudgetRuleRegistry.Rules; + RarityTagBudgetRule rule = GetRarityTagBudgetRule(normalizedRarity, ruleLookup); + + Debug.Assert(rule.MinCount >= 0); + Debug.Assert(rule.MaxCount >= rule.MinCount); + + if (rule.MinCount == rule.MaxCount) + { + return rule.MinCount; + } + + random ??= new Random(0); + return random.Next(rule.MinCount, rule.MaxCount + 1); + } + + private static bool IsSupportedLaunchTag(TagType tagType) + { + return TagDefinitionRegistry.TryGetDefinition(tagType, out TagDefinition definition) && + definition.Config != null && + definition.Config.IsImplemented; + } + + private static bool TryGetRule( + TagType tagType, + IReadOnlyDictionary ruleLookup, + out TagGenerationRule rule) + { + if (ruleLookup != null && ruleLookup.TryGetValue(tagType, out rule)) + { + return true; + } + + rule = null; + return false; + } + + private static RarityTagBudgetRule GetRarityTagBudgetRule( + RarityType rarity, + IReadOnlyDictionary ruleLookup) + { + Debug.Assert(ruleLookup != null); + Debug.Assert(ruleLookup.TryGetValue(rarity, out _)); + return ruleLookup[rarity]; + } + + private static int RollWeightedIndex( + IReadOnlyList pool, + IReadOnlyDictionary ruleLookup, + Random random) + { + int totalWeight = 0; + for (int i = 0; i < pool.Count; i++) + { + TagGenerationRule rule = ruleLookup[pool[i]]; + Debug.Assert(rule.Weight > 0); + totalWeight += rule.Weight; + } + + Debug.Assert(totalWeight > 0); + + int roll = random.Next(1, totalWeight + 1); + int cumulative = 0; + for (int i = 0; i < pool.Count; i++) + { + TagGenerationRule rule = ruleLookup[pool[i]]; + cumulative += rule.Weight; + if (roll <= cumulative) + { + return i; + } + } + + return pool.Count - 1; + } + + private static int BuildStableSeed(RarityType rarity, InventoryTagRandomContext randomContext) + { + unchecked + { + int seed = 17; + seed = seed * 31 + randomContext.RunSeed; + seed = seed * 31 + (int)InventoryRarityRuleService.NormalizeComponentRarity(rarity); + seed = seed * 31 + (int)randomContext.SourceType; + seed = seed * 31 + randomContext.ConfigId; + seed = seed * 31 + (int)randomContext.ItemInstanceId; + seed = seed * 31 + (int)(randomContext.ItemInstanceId >> 32); + return seed; + } + } + } +} diff --git a/src-ref/Definition/Tag/Generation/InventoryTagRandomContext.cs b/src-ref/Definition/Tag/Generation/InventoryTagRandomContext.cs new file mode 100644 index 0000000..24adbb6 --- /dev/null +++ b/src-ref/Definition/Tag/Generation/InventoryTagRandomContext.cs @@ -0,0 +1,72 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public readonly struct InventoryTagRandomContext + { + public InventoryTagRandomContext(int runSeed, InventoryTagSourceType sourceType, long itemInstanceId, int configId) + { + RunSeed = runSeed; + SourceType = sourceType; + ItemInstanceId = itemInstanceId; + ConfigId = configId; + } + + public int RunSeed { get; } + + public InventoryTagSourceType SourceType { get; } + + public long ItemInstanceId { get; } + + public int ConfigId { get; } + + public static InventoryTagRandomContext CreateSeed(int runSeed, long itemInstanceId, int configId) + { + return new InventoryTagRandomContext(runSeed, InventoryTagSourceType.Seed, itemInstanceId, configId); + } + + public static InventoryTagRandomContext CreateShop(int runSeed, int nodeSequenceIndex, int goodsIndex, int configId) + { + return new InventoryTagRandomContext( + runSeed, + InventoryTagSourceType.Shop, + ComposeLocalItemInstanceId(nodeSequenceIndex, goodsIndex), + configId); + } + + public static InventoryTagRandomContext CreateDrop(int runSeed, int nodeSequenceIndex, int dropOrdinal, int configId) + { + return new InventoryTagRandomContext( + runSeed, + InventoryTagSourceType.Drop, + ComposeLocalItemInstanceId(nodeSequenceIndex, dropOrdinal), + configId); + } + + public static InventoryTagRandomContext CreateReward(int runSeed, int nodeSequenceIndex, int rewardOrdinal, int configId) + { + return new InventoryTagRandomContext( + runSeed, + InventoryTagSourceType.Reward, + ComposeLocalItemInstanceId(nodeSequenceIndex, rewardOrdinal), + configId); + } + + public static InventoryTagRandomContext CreateEvent(int runSeed, int nodeSequenceIndex, int eventOrdinal, int configId) + { + return new InventoryTagRandomContext( + runSeed, + InventoryTagSourceType.Event, + ComposeLocalItemInstanceId(nodeSequenceIndex, eventOrdinal), + configId); + } + + public static long ComposeLocalItemInstanceId(int nodeSequenceIndex, int localOrdinal) + { + long normalizedSequence = Math.Max(0, nodeSequenceIndex) + 1L; + long normalizedOrdinal = Math.Max(0, localOrdinal) + 1L; + return (normalizedSequence << 32) | (uint)normalizedOrdinal; + } + } +} diff --git a/src-ref/Definition/Tag/Generation/RarityTagBudgetRule.cs b/src-ref/Definition/Tag/Generation/RarityTagBudgetRule.cs new file mode 100644 index 0000000..60ac68c --- /dev/null +++ b/src-ref/Definition/Tag/Generation/RarityTagBudgetRule.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public sealed class RarityTagBudgetRule + { + public RarityType Rarity { get; set; } + public int MinCount { get; set; } + public int MaxCount { get; set; } + } +} diff --git a/src-ref/Definition/Tag/Generation/RarityTagBudgetRuleRegistry.cs b/src-ref/Definition/Tag/Generation/RarityTagBudgetRuleRegistry.cs new file mode 100644 index 0000000..395447e --- /dev/null +++ b/src-ref/Definition/Tag/Generation/RarityTagBudgetRuleRegistry.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using GeometryTD.DataTable; +using UnityEngine; + +namespace GeometryTD.Definition +{ + public static class RarityTagBudgetRuleRegistry + { + private static readonly Dictionary RulesByRarity = CreateDefaultRules(); + + public static IReadOnlyDictionary Rules => RulesByRarity; + + public static void ResetToDefaults() + { + RulesByRarity.Clear(); + foreach (KeyValuePair pair in CreateDefaultRules()) + { + RulesByRarity.Add(pair.Key, pair.Value); + } + } + + public static void LoadFromRows(IEnumerable rows) + { + ResetToDefaults(); + foreach (DRRarityTagBudget row in rows) + { + ApplyRow(row); + } + } + + public static bool TryGetRule(RarityType rarity, out RarityTagBudgetRule rule) + { + return RulesByRarity.TryGetValue(rarity, out rule); + } + + private static Dictionary CreateDefaultRules() + { + return new Dictionary + { + [RarityType.White] = CreateRule(RarityType.White, 0, 1), + [RarityType.Green] = CreateRule(RarityType.Green, 0, 2), + [RarityType.Blue] = CreateRule(RarityType.Blue, 1, 3), + [RarityType.Purple] = CreateRule(RarityType.Purple, 1, 3), + [RarityType.Red] = CreateRule(RarityType.Red, 2, 4) + }; + } + + private static RarityTagBudgetRule CreateRule(RarityType rarity, int minCount, int maxCount) + { + Debug.Assert(rarity >= RarityType.White && rarity <= RarityType.Red); + Debug.Assert(minCount >= 0); + Debug.Assert(maxCount >= minCount); + + return new RarityTagBudgetRule + { + Rarity = rarity, + MinCount = minCount, + MaxCount = maxCount + }; + } + + private static void ApplyRow(DRRarityTagBudget row) + { + Debug.Assert(row != null); + Debug.Assert(row.Id > 0); + Debug.Assert(row.Rarity >= RarityType.White && row.Rarity <= RarityType.Red); + Debug.Assert(row.MinCount >= 0); + Debug.Assert(row.MaxCount >= row.MinCount); + + RulesByRarity[row.Rarity] = CreateRule(row.Rarity, row.MinCount, row.MaxCount); + } + } +} diff --git a/src-ref/Definition/Tag/Generation/TagGenerationRule.cs b/src-ref/Definition/Tag/Generation/TagGenerationRule.cs new file mode 100644 index 0000000..fb5a2bd --- /dev/null +++ b/src-ref/Definition/Tag/Generation/TagGenerationRule.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public sealed class TagGenerationRule + { + public TagType TagType { get; set; } + public RarityType MinRarity { get; set; } + public int Weight { get; set; } + } +} diff --git a/src-ref/Definition/Tag/Generation/TagGenerationRuleRegistry.cs b/src-ref/Definition/Tag/Generation/TagGenerationRuleRegistry.cs new file mode 100644 index 0000000..f6a38e7 --- /dev/null +++ b/src-ref/Definition/Tag/Generation/TagGenerationRuleRegistry.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using GeometryTD.DataTable; +using UnityEngine; + +namespace GeometryTD.Definition +{ + public static class TagGenerationRuleRegistry + { + private static readonly Dictionary RulesByTag = CreateDefaultRules(); + + public static IReadOnlyDictionary Rules => RulesByTag; + + public static void ResetToDefaults() + { + RulesByTag.Clear(); + foreach (KeyValuePair pair in CreateDefaultRules()) + { + RulesByTag.Add(pair.Key, pair.Value); + } + } + + public static void LoadFromRows(IEnumerable rows) + { + ResetToDefaults(); + foreach (DRTag row in rows) + { + ApplyRow(row); + } + } + + public static bool TryGetRule(TagType tagType, out TagGenerationRule rule) + { + return RulesByTag.TryGetValue(tagType, out rule); + } + + private static Dictionary CreateDefaultRules() + { + return new Dictionary + { + [TagType.Fire] = CreateRule(TagType.Fire, RarityType.White, 20), + [TagType.BurnSpread] = CreateRule(TagType.BurnSpread, RarityType.White, 20), + [TagType.IgniteBurst] = CreateRule(TagType.IgniteBurst, RarityType.Green, 15), + [TagType.Inferno] = CreateRule(TagType.Inferno, RarityType.Purple, 5), + [TagType.Ice] = CreateRule(TagType.Ice, RarityType.White, 1), + [TagType.FreezeMask] = CreateRule(TagType.FreezeMask, RarityType.White, 20), + [TagType.Shatter] = CreateRule(TagType.Shatter, RarityType.Green, 15), + [TagType.AbsoluteZero] = CreateRule(TagType.AbsoluteZero, RarityType.Purple, 1), + [TagType.Pierce] = CreateRule(TagType.Pierce, RarityType.White, 20), + [TagType.Crit] = CreateRule(TagType.Crit, RarityType.White, 20), + [TagType.Overpenetrate] = CreateRule(TagType.Overpenetrate, RarityType.Green, 15), + [TagType.Execution] = CreateRule(TagType.Execution, RarityType.Purple, 5) + }; + } + + private static TagGenerationRule CreateRule(TagType tagType, RarityType minRarity, int weight) + { + return new TagGenerationRule + { + TagType = tagType, + MinRarity = minRarity, + Weight = weight + }; + } + + private static void ApplyRow(DRTag row) + { + Debug.Assert(row != null); + Debug.Assert(row.Id > 0); + Debug.Assert(row.Weight > 0); + + RulesByTag[row.TagType] = CreateRule(row.TagType, row.MinRarity, row.Weight); + } + } +} diff --git a/src-ref/Definition/Tag/Metadata/Config/AbsoluteZeroTagConfig.cs b/src-ref/Definition/Tag/Metadata/Config/AbsoluteZeroTagConfig.cs new file mode 100644 index 0000000..1cbe5b2 --- /dev/null +++ b/src-ref/Definition/Tag/Metadata/Config/AbsoluteZeroTagConfig.cs @@ -0,0 +1,15 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class AbsoluteZeroTagConfig : TagConfigBase + { + public AbsoluteZeroTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public float BonusSlowDurationSeconds { get; set; } = 1f; + public float BonusSlowRatioPerStack { get; set; } = 0.1f; + } +} diff --git a/src-ref/Definition/Tag/Metadata/Config/BurnSpreadTagConfig.cs b/src-ref/Definition/Tag/Metadata/Config/BurnSpreadTagConfig.cs new file mode 100644 index 0000000..03943ac --- /dev/null +++ b/src-ref/Definition/Tag/Metadata/Config/BurnSpreadTagConfig.cs @@ -0,0 +1,12 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class BurnSpreadTagConfig : TagConfigBase + { + public BurnSpreadTagConfig(bool isImplemented) : base(isImplemented) + { + } + } +} diff --git a/src-ref/Definition/Tag/Metadata/Config/CritTagConfig.cs b/src-ref/Definition/Tag/Metadata/Config/CritTagConfig.cs new file mode 100644 index 0000000..b97228f --- /dev/null +++ b/src-ref/Definition/Tag/Metadata/Config/CritTagConfig.cs @@ -0,0 +1,15 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class CritTagConfig : TagConfigBase + { + public CritTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public float CritChancePerStack { get; set; } = 0.1f; + public float CritDamageMultiplier { get; set; } = 1.5f; + } +} \ No newline at end of file diff --git a/src-ref/Definition/Tag/Metadata/Config/ExecutionTagConfig.cs b/src-ref/Definition/Tag/Metadata/Config/ExecutionTagConfig.cs new file mode 100644 index 0000000..068fa75 --- /dev/null +++ b/src-ref/Definition/Tag/Metadata/Config/ExecutionTagConfig.cs @@ -0,0 +1,15 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class ExecutionTagConfig : TagConfigBase + { + public ExecutionTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public float TargetHealthThreshold { get; set; } = 0.3f; + public float DamageBonusPerStack { get; set; } = 0.5f; + } +} \ No newline at end of file diff --git a/src-ref/Definition/Tag/Metadata/Config/FireTagConfig.cs b/src-ref/Definition/Tag/Metadata/Config/FireTagConfig.cs new file mode 100644 index 0000000..66fedd4 --- /dev/null +++ b/src-ref/Definition/Tag/Metadata/Config/FireTagConfig.cs @@ -0,0 +1,16 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class FireTagConfig : TagConfigBase + { + public FireTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public float BurnDurationSeconds { get; set; } = 3f; + public float BurnDamagePerSecondPerStack { get; set; } = 20f; + public int MaxEffectiveStack { get; set; } = 5; + } +} \ No newline at end of file diff --git a/src-ref/Definition/Tag/Metadata/Config/FreezeMaskTagConfig.cs b/src-ref/Definition/Tag/Metadata/Config/FreezeMaskTagConfig.cs new file mode 100644 index 0000000..3d0b381 --- /dev/null +++ b/src-ref/Definition/Tag/Metadata/Config/FreezeMaskTagConfig.cs @@ -0,0 +1,12 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class FreezeMaskTagConfig : TagConfigBase + { + public FreezeMaskTagConfig(bool isImplemented) : base(isImplemented) + { + } + } +} diff --git a/src-ref/Definition/Tag/Metadata/Config/IceTagConfig.cs b/src-ref/Definition/Tag/Metadata/Config/IceTagConfig.cs new file mode 100644 index 0000000..4a7a9aa --- /dev/null +++ b/src-ref/Definition/Tag/Metadata/Config/IceTagConfig.cs @@ -0,0 +1,16 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class IceTagConfig : TagConfigBase + { + public IceTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public float SlowDurationSeconds { get; set; } = 2f; + public float SlowRatioPerStack { get; set; } = 0.2f; + public float MinMoveSpeedMultiplier { get; set; } = 0.4f; + } +} \ No newline at end of file diff --git a/src-ref/Definition/Tag/Metadata/Config/IgniteBurstTagConfig.cs b/src-ref/Definition/Tag/Metadata/Config/IgniteBurstTagConfig.cs new file mode 100644 index 0000000..a042c3c --- /dev/null +++ b/src-ref/Definition/Tag/Metadata/Config/IgniteBurstTagConfig.cs @@ -0,0 +1,12 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class IgniteBurstTagConfig : TagConfigBase + { + public IgniteBurstTagConfig(bool isImplemented) : base(isImplemented) + { + } + } +} diff --git a/src-ref/Definition/Tag/Metadata/Config/InfernoTagConfig.cs b/src-ref/Definition/Tag/Metadata/Config/InfernoTagConfig.cs new file mode 100644 index 0000000..82e6806 --- /dev/null +++ b/src-ref/Definition/Tag/Metadata/Config/InfernoTagConfig.cs @@ -0,0 +1,15 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class InfernoTagConfig : TagConfigBase + { + public InfernoTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public float BonusBurnDurationSeconds { get; set; } = 2f; + public float BonusBurnDamagePerSecondPerStack { get; set; } = 20f; + } +} diff --git a/src-ref/Definition/Tag/Metadata/Config/OverpenetrateTagConfig.cs b/src-ref/Definition/Tag/Metadata/Config/OverpenetrateTagConfig.cs new file mode 100644 index 0000000..b3b20a2 --- /dev/null +++ b/src-ref/Definition/Tag/Metadata/Config/OverpenetrateTagConfig.cs @@ -0,0 +1,12 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class OverpenetrateTagConfig : TagConfigBase + { + public OverpenetrateTagConfig(bool isImplemented) : base(isImplemented) + { + } + } +} diff --git a/src-ref/Definition/Tag/Metadata/Config/PierceTagConfig.cs b/src-ref/Definition/Tag/Metadata/Config/PierceTagConfig.cs new file mode 100644 index 0000000..d68fd57 --- /dev/null +++ b/src-ref/Definition/Tag/Metadata/Config/PierceTagConfig.cs @@ -0,0 +1,12 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class PierceTagConfig : TagConfigBase + { + public PierceTagConfig(bool isImplemented) : base(isImplemented) + { + } + } +} diff --git a/src-ref/Definition/Tag/Metadata/Config/ShatterTagConfig.cs b/src-ref/Definition/Tag/Metadata/Config/ShatterTagConfig.cs new file mode 100644 index 0000000..c1ac605 --- /dev/null +++ b/src-ref/Definition/Tag/Metadata/Config/ShatterTagConfig.cs @@ -0,0 +1,15 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class ShatterTagConfig : TagConfigBase + { + public ShatterTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public bool RequiresSlowedTarget { get; set; } = true; + public float DamageBonusPerStack { get; set; } = 0.25f; + } +} diff --git a/src-ref/Definition/Tag/Metadata/Config/TagConfigBase.cs b/src-ref/Definition/Tag/Metadata/Config/TagConfigBase.cs new file mode 100644 index 0000000..11635a4 --- /dev/null +++ b/src-ref/Definition/Tag/Metadata/Config/TagConfigBase.cs @@ -0,0 +1,15 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public abstract class TagConfigBase + { + public bool IsImplemented { get; set; } + + protected TagConfigBase(bool isImplemented) + { + IsImplemented = isImplemented; + } + } +} \ No newline at end of file diff --git a/src-ref/Definition/Tag/Metadata/TagDefinition.cs b/src-ref/Definition/Tag/Metadata/TagDefinition.cs new file mode 100644 index 0000000..710ca92 --- /dev/null +++ b/src-ref/Definition/Tag/Metadata/TagDefinition.cs @@ -0,0 +1,14 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class TagDefinition + { + public TagType TagType { get; set; } + public TagCategory Category { get; set; } + public TagTriggerPhase TriggerPhase { get; set; } + public string Description { get; set; } + public TagConfigBase Config { get; set; } + } +} diff --git a/src-ref/Definition/Tag/Metadata/TagDefinitionRegistry.cs b/src-ref/Definition/Tag/Metadata/TagDefinitionRegistry.cs new file mode 100644 index 0000000..9b3de3e --- /dev/null +++ b/src-ref/Definition/Tag/Metadata/TagDefinitionRegistry.cs @@ -0,0 +1,214 @@ +using System.Collections.Generic; +using GeometryTD.DataTable; +using Newtonsoft.Json.Linq; +using UnityEngine; + +namespace GeometryTD.Definition +{ + public static class TagDefinitionRegistry + { + private static readonly Dictionary DefinitionsByTag = CreateDefaultDefinitions(); + + public static IReadOnlyDictionary Definitions => DefinitionsByTag; + + public static void ResetToDefaults() + { + DefinitionsByTag.Clear(); + foreach (KeyValuePair pair in CreateDefaultDefinitions()) + { + DefinitionsByTag.Add(pair.Key, pair.Value); + } + } + + public static void LoadFromRows(IEnumerable rows) + { + ReloadFromRows(rows, null); + } + + public static bool TryGetDefinition(TagType tagType, out TagDefinition definition) + { + return DefinitionsByTag.TryGetValue(tagType, out definition); + } + + public static void ApplyTagRows(IEnumerable rows) + { + if (rows == null) + { + return; + } + + foreach (DRTag row in rows) + { + ApplyTagRow(row); + } + } + + public static void ReloadFromRows(IEnumerable tagConfigRows, IEnumerable tagRows) + { + ResetToDefaults(); + + if (tagConfigRows != null) + { + foreach (DRTagConfig row in tagConfigRows) + { + ApplyRow(row); + } + } + + if (tagRows != null) + { + foreach (DRTag row in tagRows) + { + ApplyTagRow(row); + } + } + } + + private static Dictionary CreateDefaultDefinitions() + { + return new Dictionary + { + [TagType.Fire] = CreateDefinition(TagType.Fire, TagCategory.Status, TagTriggerPhase.OnAfterHit, new FireTagConfig(true)), + [TagType.BurnSpread] = CreateDefinition(TagType.BurnSpread, TagCategory.AttackShape, TagTriggerPhase.None, new BurnSpreadTagConfig(false)), + [TagType.IgniteBurst] = CreateDefinition(TagType.IgniteBurst, TagCategory.AttackShape, TagTriggerPhase.None, new IgniteBurstTagConfig(false)), + [TagType.Inferno] = CreateDefinition(TagType.Inferno, TagCategory.StatusModifier, TagTriggerPhase.OnAfterHit, new InfernoTagConfig(true)), + [TagType.Ice] = CreateDefinition(TagType.Ice, TagCategory.Status, TagTriggerPhase.OnAfterHit, new IceTagConfig(true)), + [TagType.FreezeMask] = CreateDefinition(TagType.FreezeMask, TagCategory.AttackShape, TagTriggerPhase.None, new FreezeMaskTagConfig(false)), + [TagType.Shatter] = CreateDefinition(TagType.Shatter, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit, new ShatterTagConfig(true)), + [TagType.AbsoluteZero] = CreateDefinition(TagType.AbsoluteZero, TagCategory.StatusModifier, TagTriggerPhase.OnAfterHit, new AbsoluteZeroTagConfig(true)), + [TagType.Pierce] = CreateDefinition(TagType.Pierce, TagCategory.AttackShape, TagTriggerPhase.None, new PierceTagConfig(false)), + [TagType.Crit] = CreateDefinition(TagType.Crit, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit, new CritTagConfig(true)), + [TagType.Overpenetrate] = CreateDefinition(TagType.Overpenetrate, TagCategory.AttackShape, TagTriggerPhase.None, new OverpenetrateTagConfig(false)), + [TagType.Execution] = CreateDefinition(TagType.Execution, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit, new ExecutionTagConfig(true)) + }; + } + + private static TagDefinition CreateDefinition( + TagType tagType, + TagCategory category, + TagTriggerPhase triggerPhase, + TagConfigBase config) + { + return new TagDefinition + { + TagType = tagType, + Category = category, + TriggerPhase = triggerPhase, + Description = string.Empty, + Config = config + }; + } + + private static void ApplyRow(DRTagConfig row) + { + Debug.Assert(row != null); + Debug.Assert(DefinitionsByTag.TryGetValue(row.TagType, out TagDefinition definition)); + + definition.TriggerPhase = row.TriggerPhase; + definition.Description = row.Description ?? string.Empty; + + if (string.IsNullOrWhiteSpace(row.ParamJson)) + { + return; + } + + JObject param = JObject.Parse(row.ParamJson); + switch (row.TagType) + { + case TagType.Fire: + ApplyFireConfig((FireTagConfig)definition.Config, param); + break; + case TagType.Inferno: + ApplyInfernoConfig((InfernoTagConfig)definition.Config, param); + break; + case TagType.Ice: + ApplyIceConfig((IceTagConfig)definition.Config, param); + break; + case TagType.Shatter: + ApplyShatterConfig((ShatterTagConfig)definition.Config, param); + break; + case TagType.AbsoluteZero: + ApplyAbsoluteZeroConfig((AbsoluteZeroTagConfig)definition.Config, param); + break; + case TagType.Crit: + ApplyCritConfig((CritTagConfig)definition.Config, param); + break; + case TagType.Execution: + ApplyExecutionConfig((ExecutionTagConfig)definition.Config, param); + break; + } + } + + private static void ApplyTagRow(DRTag row) + { + Debug.Assert(row != null); + Debug.Assert(row.Id > 0); + Debug.Assert(DefinitionsByTag.TryGetValue(row.TagType, out TagDefinition definition)); + Debug.Assert(definition.Config != null); + + definition.Config.IsImplemented = row.IsImplemented; + } + + private static void ApplyFireConfig(FireTagConfig config, JObject param) + { + config.BurnDurationSeconds = ReadFloat(param, nameof(FireTagConfig.BurnDurationSeconds), config.BurnDurationSeconds); + config.BurnDamagePerSecondPerStack = ReadFloat(param, nameof(FireTagConfig.BurnDamagePerSecondPerStack), config.BurnDamagePerSecondPerStack); + config.MaxEffectiveStack = ReadInt(param, nameof(FireTagConfig.MaxEffectiveStack), config.MaxEffectiveStack); + } + + private static void ApplyInfernoConfig(InfernoTagConfig config, JObject param) + { + config.BonusBurnDurationSeconds = ReadFloat(param, nameof(InfernoTagConfig.BonusBurnDurationSeconds), config.BonusBurnDurationSeconds); + config.BonusBurnDamagePerSecondPerStack = ReadFloat(param, nameof(InfernoTagConfig.BonusBurnDamagePerSecondPerStack), config.BonusBurnDamagePerSecondPerStack); + } + + private static void ApplyIceConfig(IceTagConfig config, JObject param) + { + config.SlowDurationSeconds = ReadFloat(param, nameof(IceTagConfig.SlowDurationSeconds), config.SlowDurationSeconds); + config.SlowRatioPerStack = ReadFloat(param, nameof(IceTagConfig.SlowRatioPerStack), config.SlowRatioPerStack); + config.MinMoveSpeedMultiplier = ReadFloat(param, nameof(IceTagConfig.MinMoveSpeedMultiplier), config.MinMoveSpeedMultiplier); + } + + private static void ApplyShatterConfig(ShatterTagConfig config, JObject param) + { + config.RequiresSlowedTarget = ReadBool(param, nameof(ShatterTagConfig.RequiresSlowedTarget), config.RequiresSlowedTarget); + config.DamageBonusPerStack = ReadFloat(param, nameof(ShatterTagConfig.DamageBonusPerStack), config.DamageBonusPerStack); + } + + private static void ApplyAbsoluteZeroConfig(AbsoluteZeroTagConfig config, JObject param) + { + config.BonusSlowDurationSeconds = ReadFloat(param, nameof(AbsoluteZeroTagConfig.BonusSlowDurationSeconds), config.BonusSlowDurationSeconds); + config.BonusSlowRatioPerStack = ReadFloat(param, nameof(AbsoluteZeroTagConfig.BonusSlowRatioPerStack), config.BonusSlowRatioPerStack); + } + + private static void ApplyCritConfig(CritTagConfig config, JObject param) + { + config.CritChancePerStack = ReadFloat(param, nameof(CritTagConfig.CritChancePerStack), config.CritChancePerStack); + config.CritDamageMultiplier = ReadFloat(param, nameof(CritTagConfig.CritDamageMultiplier), config.CritDamageMultiplier); + } + + private static void ApplyExecutionConfig(ExecutionTagConfig config, JObject param) + { + config.TargetHealthThreshold = ReadFloat(param, nameof(ExecutionTagConfig.TargetHealthThreshold), config.TargetHealthThreshold); + config.DamageBonusPerStack = ReadFloat(param, nameof(ExecutionTagConfig.DamageBonusPerStack), config.DamageBonusPerStack); + } + + private static float ReadFloat(JObject param, string key, float defaultValue) + { + JToken token = param[key]; + return token == null ? defaultValue : token.Value(); + } + + private static int ReadInt(JObject param, string key, int defaultValue) + { + JToken token = param[key]; + return token == null ? defaultValue : token.Value(); + } + + private static bool ReadBool(JObject param, string key, bool defaultValue) + { + JToken token = param[key]; + return token == null ? defaultValue : token.Value(); + } + } +} diff --git a/src-ref/Definition/Tag/Presentation/TagDisplayUtility.cs b/src-ref/Definition/Tag/Presentation/TagDisplayUtility.cs new file mode 100644 index 0000000..461ca74 --- /dev/null +++ b/src-ref/Definition/Tag/Presentation/TagDisplayUtility.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using GeometryTD.DataTable; +using GeometryTD.Definition; + +namespace GeometryTD.CustomUtility +{ + public static class TagDisplayUtility + { + public static string ResolveTagName(TagType tagType) + { + if (tagType == TagType.None) + { + return string.Empty; + } + + if (GameEntry.DataTable != null) + { + var tagTable = GameEntry.DataTable.GetDataTable(); + if (tagTable != null) + { + DRTag tagRow = tagTable.GetDataRow((int)tagType); + if (tagRow != null && !string.IsNullOrWhiteSpace(tagRow.Name)) + { + return tagRow.Name; + } + } + } + + return tagType.ToString(); + } + + public static string[] BuildTagTexts(IReadOnlyList tags) + { + if (tags == null || tags.Count <= 0) + { + return Array.Empty(); + } + + List results = new List(tags.Count); + for (int i = 0; i < tags.Count; i++) + { + string tagName = ResolveTagName(tags[i]); + if (!string.IsNullOrWhiteSpace(tagName)) + { + results.Add(tagName); + } + } + + return results.ToArray(); + } + + public static string[] BuildTagTexts(IReadOnlyList tagRuntimes) + { + if (tagRuntimes == null || tagRuntimes.Count <= 0) + { + return Array.Empty(); + } + + List results = new List(tagRuntimes.Count); + for (int i = 0; i < tagRuntimes.Count; i++) + { + TagRuntimeData tagRuntime = tagRuntimes[i]; + if (tagRuntime == null || tagRuntime.TagType == TagType.None || tagRuntime.TotalStack <= 0) + { + continue; + } + + string tagName = ResolveTagName(tagRuntime.TagType); + if (string.IsNullOrWhiteSpace(tagName)) + { + continue; + } + + results.Add(tagRuntime.TotalStack > 1 ? $"{tagName} x{tagRuntime.TotalStack}" : tagName); + } + + return results.ToArray(); + } + + public static string[] BuildTowerTagTexts(TowerStatsData towerStats) + { + if (towerStats == null) + { + return Array.Empty(); + } + + if (towerStats.TagRuntimes != null && towerStats.TagRuntimes.Length > 0) + { + return BuildTagTexts(towerStats.TagRuntimes); + } + + return BuildTagTexts(towerStats.Tags); + } + + public static string BuildTagDescriptionText(IReadOnlyList tags) + { + if (tags == null || tags.Count <= 0) + { + return string.Empty; + } + + List results = new List(tags.Count); + for (int i = 0; i < tags.Count; i++) + { + TagType tagType = tags[i]; + if (tagType == TagType.None) + { + continue; + } + + string tagName = ResolveTagName(tagType); + string tagDescription = ResolveTagDescription(tagType); + if (string.IsNullOrWhiteSpace(tagDescription)) + { + continue; + } + + results.Add(string.IsNullOrWhiteSpace(tagName) ? tagDescription : $"{tagName}: {tagDescription}"); + } + + return string.Join("\n", results); + } + + public static string BuildTagDescriptionText(IReadOnlyList tagRuntimes) + { + if (tagRuntimes == null || tagRuntimes.Count <= 0) + { + return string.Empty; + } + + List results = new List(tagRuntimes.Count); + for (int i = 0; i < tagRuntimes.Count; i++) + { + TagRuntimeData runtime = tagRuntimes[i]; + if (runtime == null || runtime.TagType == TagType.None || runtime.TotalStack <= 0) + { + continue; + } + + string tagName = ResolveTagName(runtime.TagType); + if (runtime.TotalStack > 1 && !string.IsNullOrWhiteSpace(tagName)) + { + tagName = $"{tagName} x{runtime.TotalStack}"; + } + + string tagDescription = ResolveTagDescription(runtime.TagType); + if (string.IsNullOrWhiteSpace(tagDescription)) + { + continue; + } + + results.Add(string.IsNullOrWhiteSpace(tagName) ? tagDescription : $"{tagName}: {tagDescription}"); + } + + return string.Join("\n", results); + } + + public static string ResolveTagDescription(TagType tagType) + { + if (tagType == TagType.None) + { + return string.Empty; + } + + if (TagDefinitionRegistry.TryGetDefinition(tagType, out TagDefinition definition) && + !string.IsNullOrWhiteSpace(definition.Description)) + { + return definition.Description; + } + + return string.Empty; + } + } +} diff --git a/src-ref/Editor/GameFrameworkConfigs.cs b/src-ref/Editor/GameFrameworkConfigs.cs new file mode 100644 index 0000000..e8cf2a6 --- /dev/null +++ b/src-ref/Editor/GameFrameworkConfigs.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using GameFramework; +using System.IO; +using UnityEngine; +using UnityGameFramework.Editor; +using UnityGameFramework.Editor.ResourceTools; + +namespace GeometryTD.Editor +{ + public static class GameFrameworkConfigs + { + [BuildSettingsConfigPath] + public static string BuildSettingsConfig = Utility.Path.GetRegularPath(Path.Combine(Application.dataPath, "GameMain/Configs/BuildSettings.xml")); + + [ResourceCollectionConfigPath] + public static string ResourceCollectionConfig = Utility.Path.GetRegularPath(Path.Combine(Application.dataPath, "GameMain/Configs/ResourceCollection.xml")); + + [ResourceEditorConfigPath] + public static string ResourceEditorConfig = Utility.Path.GetRegularPath(Path.Combine(Application.dataPath, "GameMain/Configs/ResourceEditor.xml")); + + [ResourceBuilderConfigPath] + public static string ResourceBuilderConfig = Utility.Path.GetRegularPath(Path.Combine(Application.dataPath, "GameMain/Configs/ResourceBuilder.xml")); + } +} diff --git a/src-ref/Editor/GeometryTD.Editor.asmdef b/src-ref/Editor/GeometryTD.Editor.asmdef new file mode 100644 index 0000000..2682f22 --- /dev/null +++ b/src-ref/Editor/GeometryTD.Editor.asmdef @@ -0,0 +1,19 @@ +{ + "name": "GeometryTD.Editor", + "rootNamespace": "GeometryTD.Editor", + "references": [ + "GeometryTD.Runtime", + "UnityGameFramework.Editor" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/src-ref/Editor/GeometryTDBuildEventHandler.cs b/src-ref/Editor/GeometryTDBuildEventHandler.cs new file mode 100644 index 0000000..ae9f723 --- /dev/null +++ b/src-ref/Editor/GeometryTDBuildEventHandler.cs @@ -0,0 +1,83 @@ +using GameFramework; +using System.IO; +using UnityEditor; +using UnityEngine; +using UnityGameFramework.Editor.ResourceTools; + +namespace GeometryTD.Editor +{ + public sealed class GeometryTDBuildEventHandler : IBuildEventHandler + { + public bool ContinueOnFailure + { + get + { + return false; + } + } + + public void OnPreprocessAllPlatforms(string productName, string companyName, string gameIdentifier, string gameFrameworkVersion, string unityVersion, string applicableGameVersion, int internalResourceVersion, + Platform platforms, AssetBundleCompressionType assetBundleCompression, string compressionHelperTypeName, bool additionalCompressionSelected, bool forceRebuildAssetBundleSelected, string buildEventHandlerTypeName, string outputDirectory, BuildAssetBundleOptions buildAssetBundleOptions, + string workingPath, bool outputPackageSelected, string outputPackagePath, bool outputFullSelected, string outputFullPath, bool outputPackedSelected, string outputPackedPath, string buildReportPath) + { + string streamingAssetsPath = Utility.Path.GetRegularPath(Path.Combine(Application.dataPath, "StreamingAssets")); + string[] fileNames = Directory.GetFiles(streamingAssetsPath, "*", SearchOption.AllDirectories); + foreach (string fileName in fileNames) + { + if (fileName.Contains(".gitkeep")) + { + continue; + } + + File.Delete(fileName); + } + + Utility.Path.RemoveEmptyDirectory(streamingAssetsPath); + } + + public void OnPostprocessAllPlatforms(string productName, string companyName, string gameIdentifier, string gameFrameworkVersion, string unityVersion, string applicableGameVersion, int internalResourceVersion, + Platform platforms, AssetBundleCompressionType assetBundleCompression, string compressionHelperTypeName, bool additionalCompressionSelected, bool forceRebuildAssetBundleSelected, string buildEventHandlerTypeName, string outputDirectory, BuildAssetBundleOptions buildAssetBundleOptions, + string workingPath, bool outputPackageSelected, string outputPackagePath, bool outputFullSelected, string outputFullPath, bool outputPackedSelected, string outputPackedPath, string buildReportPath) + { + } + + public void OnPreprocessPlatform(Platform platform, string workingPath, bool outputPackageSelected, string outputPackagePath, bool outputFullSelected, string outputFullPath, bool outputPackedSelected, string outputPackedPath) + { + } + + public void OnBuildAssetBundlesComplete(Platform platform, string workingPath, bool outputPackageSelected, string outputPackagePath, bool outputFullSelected, string outputFullPath, bool outputPackedSelected, string outputPackedPath, AssetBundleManifest assetBundleManifest) + { + } + + public void OnOutputUpdatableVersionListData(Platform platform, string versionListPath, int versionListLength, int versionListHashCode, int versionListCompressedLength, int versionListCompressedHashCode) + { + } + + public void OnPostprocessPlatform(Platform platform, string workingPath, bool outputPackageSelected, string outputPackagePath, bool outputFullSelected, string outputFullPath, bool outputPackedSelected, string outputPackedPath, bool isSuccess) + { + if (!outputPackageSelected) + { + return; + } + + if (platform != Platform.Windows) + { + return; + } + + string streamingAssetsPath = Utility.Path.GetRegularPath(Path.Combine(Application.dataPath, "StreamingAssets")); + string[] fileNames = Directory.GetFiles(outputPackagePath, "*", SearchOption.AllDirectories); + foreach (string fileName in fileNames) + { + string destFileName = Utility.Path.GetRegularPath(Path.Combine(streamingAssetsPath, fileName.Substring(outputPackagePath.Length))); + FileInfo destFileInfo = new FileInfo(destFileName); + if (!destFileInfo.Directory.Exists) + { + destFileInfo.Directory.Create(); + } + + File.Copy(fileName, destFileName); + } + } + } +} diff --git a/src-ref/Editor/SceneSwitchLeft.cs b/src-ref/Editor/SceneSwitchLeft.cs new file mode 100644 index 0000000..de5f845 --- /dev/null +++ b/src-ref/Editor/SceneSwitchLeft.cs @@ -0,0 +1,52 @@ +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using System.IO; + +[InitializeOnLoad] +public class SceneSwitchLeft +{ + static SceneSwitchLeft() + { + // 注册到全局工具栏绘制事件 + SceneView.duringSceneGui += OnSceneGUI; + } + + static void OnSceneGUI(SceneView sceneView) + { + // 在 Scene 视图的左上角绘制一个下拉菜单 + Handles.BeginGUI(); + + GUILayout.BeginArea(new Rect(10, 10, 200, 100)); + if (EditorGUILayout.DropdownButton(new GUIContent("快速切换场景"), FocusType.Passive, EditorStyles.toolbarDropDown)) + { + ShowSceneMenu(); + } + GUILayout.EndArea(); + + Handles.EndGUI(); + } + + static void ShowSceneMenu() + { + GenericMenu menu = new GenericMenu(); + + // 查找项目中所有启用(Enabled)的场景 + string[] sceneGuids = AssetDatabase.FindAssets("t:Scene"); + + foreach (string guid in sceneGuids) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + string name = Path.GetFileNameWithoutExtension(path); + + menu.AddItem(new GUIContent(name), false, () => { + if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) + { + EditorSceneManager.OpenScene(path); + } + }); + } + + menu.ShowAsContext(); + } +} \ No newline at end of file diff --git a/src-ref/Entity/EntityData/BulletData.cs b/src-ref/Entity/EntityData/BulletData.cs new file mode 100644 index 0000000..cc93b53 --- /dev/null +++ b/src-ref/Entity/EntityData/BulletData.cs @@ -0,0 +1,56 @@ +using System; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.Entity.EntityData +{ + [Serializable] + public class BulletData : EntityDataBase + { + [SerializeField] private Transform _target = null; + [SerializeField] private float _speed = 0f; + [SerializeField] private float _maxLifetime = 3f; + [SerializeField] private AttackPayload _attackPayload = new AttackPayload(); + + public BulletData( + int entityId, + int typeId, + Vector3 position, + Transform target, + AttackPayload attackPayload, + float speed, + float maxLifetime = 3f) : base(entityId, typeId) + { + Position = position; + + _target = target; + _attackPayload = attackPayload?.Clone() ?? new AttackPayload(); + _speed = speed; + _maxLifetime = maxLifetime; + } + + public Transform Target + { + get => _target; + set => _target = value; + } + + public float Speed + { + get => _speed; + set => _speed = value; + } + + public float MaxLifetime + { + get => _maxLifetime; + set => _maxLifetime = value; + } + + public AttackPayload AttackPayload + { + get => _attackPayload; + set => _attackPayload = value?.Clone() ?? new AttackPayload(); + } + } +} diff --git a/src-ref/Entity/EntityData/EnemyData.cs b/src-ref/Entity/EntityData/EnemyData.cs new file mode 100644 index 0000000..891912f --- /dev/null +++ b/src-ref/Entity/EntityData/EnemyData.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.Entity.EntityData +{ + [Serializable] + public class EnemyData : EntityDataBase + { + [SerializeField] private CampType _camp = CampType.Enemy; + + [SerializeField] private int _maxHealth = 0; + + [SerializeField] private float _speed = 0; + + [SerializeField] private Transform _player = null; + + [SerializeField] private List _pathPoints = new List(); + + public EnemyData(int entityId, int typeId, Transform player, Vector3 pos, int maxHp, float speed) : base( + entityId, typeId) + { + _maxHealth = maxHp; + _speed = speed; + Position = pos; + _player = player; + _pathPoints.Clear(); + } + + public EnemyData(int entityId, int typeId, Vector3 pos, int maxHp, float speed, + IReadOnlyList pathPoints) : base(entityId, typeId) + { + _maxHealth = maxHp; + _speed = speed; + Position = pos; + _player = null; + SetPathPoints(pathPoints); + } + + public CampType Camp + { + get => _camp; + set => _camp = value; + } + + public int MaxHealth + { + get => _maxHealth; + set => _maxHealth = value; + } + + public float Speed + { + get => _speed; + set => _speed = value; + } + + public Transform Player + { + get => _player; + set => _player = value; + } + + public IReadOnlyList PathPoints => _pathPoints; + + public bool HasPath => _pathPoints.Count > 0; + + public void SetPathPoints(IReadOnlyList pathPoints) + { + _pathPoints.Clear(); + if (pathPoints == null) + { + return; + } + + for (int i = 0; i < pathPoints.Count; i++) + { + _pathPoints.Add(pathPoints[i]); + } + } + } +} diff --git a/src-ref/Entity/EntityData/EntityDataBase.cs b/src-ref/Entity/EntityData/EntityDataBase.cs new file mode 100644 index 0000000..1e3817f --- /dev/null +++ b/src-ref/Entity/EntityData/EntityDataBase.cs @@ -0,0 +1,58 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using System; +using UnityEngine; + +namespace GeometryTD.Entity.EntityData +{ + [Serializable] + public abstract class EntityDataBase + { + [SerializeField] private int _id = 0; + + [SerializeField] private int _typeId = 0; + + [SerializeField] private Vector3 _position = Vector3.zero; + + [SerializeField] private Quaternion _rotation = Quaternion.identity; + + public EntityDataBase(int entityId, int typeId) + { + _id = entityId; + _typeId = typeId; + } + + /// + /// 生成的实体编号。 + /// + public int Id => _id; + + /// + /// 实体类型编号(外键)。 + /// + public int TypeId => _typeId; + + /// + /// 实体位置。 + /// + public Vector3 Position + { + get => _position; + set => _position = value; + } + + /// + /// 实体朝向。 + /// + public Quaternion Rotation + { + get => _rotation; + set => _rotation = value; + } + } +} \ No newline at end of file diff --git a/src-ref/Entity/EntityData/MapData.cs b/src-ref/Entity/EntityData/MapData.cs new file mode 100644 index 0000000..0a8b151 --- /dev/null +++ b/src-ref/Entity/EntityData/MapData.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using GeometryTD.Definition; +using GeometryTD.CustomUtility; +using UnityEngine; + +namespace GeometryTD.Entity.EntityData +{ + [Serializable] + public class MapData : EntityDataBase + { + [SerializeField] private int _levelId = 0; + [SerializeField] private int _initialCoin = 0; + [SerializeField] private TowerStatsData[] _buildTowerStatsSnapshot = Array.Empty(); + [SerializeField] private BackpackInventoryData _inventorySnapshot; + [SerializeField] private TowerItemData[] _participantTowerSnapshot = Array.Empty(); + + public MapData(int entityId, int levelId, Vector3 position) : this(entityId, 0, levelId, position) + { + } + + public MapData(int entityId, int typeId, int levelId, Vector3 position) : base(entityId, typeId) + { + _levelId = levelId; + Position = position; + } + + public MapData( + int entityId, + int typeId, + int levelId, + Vector3 position, + int initialCoin, + IReadOnlyList buildTowerStatsSnapshot, + BackpackInventoryData inventorySnapshot, + IReadOnlyList participantTowerSnapshot) : base(entityId, typeId) + { + _levelId = levelId; + Position = position; + _initialCoin = Mathf.Max(0, initialCoin); + SetBuildTowerStatsSnapshot(buildTowerStatsSnapshot); + _inventorySnapshot = inventorySnapshot != null + ? InventoryCloneUtility.CloneInventory(inventorySnapshot) + : null; + SetParticipantTowerSnapshot(participantTowerSnapshot); + } + + public int LevelId + { + get => _levelId; + set => _levelId = value; + } + + public int InitialCoin + { + get => _initialCoin; + set => _initialCoin = Mathf.Max(0, value); + } + + public int CurrentBuildTowerCount => _buildTowerStatsSnapshot != null ? _buildTowerStatsSnapshot.Length : 0; + public BackpackInventoryData InventorySnapshot => _inventorySnapshot != null + ? InventoryCloneUtility.CloneInventory(_inventorySnapshot) + : null; + public IReadOnlyList ParticipantTowerSnapshot => _participantTowerSnapshot; + + public bool TryGetBuildTowerStats(int buildIndex, out TowerStatsData stats) + { + stats = null; + if (_buildTowerStatsSnapshot == null || buildIndex < 0 || buildIndex >= _buildTowerStatsSnapshot.Length) + { + return false; + } + + TowerStatsData sourceStats = _buildTowerStatsSnapshot[buildIndex]; + if (sourceStats == null) + { + return false; + } + + stats = InventoryCloneUtility.CloneTowerStats(sourceStats); + return stats != null; + } + + public void SetBuildTowerStatsSnapshot(IReadOnlyList buildTowerStatsSnapshot) + { + if (buildTowerStatsSnapshot == null || buildTowerStatsSnapshot.Count <= 0) + { + _buildTowerStatsSnapshot = Array.Empty(); + return; + } + + _buildTowerStatsSnapshot = new TowerStatsData[buildTowerStatsSnapshot.Count]; + for (int i = 0; i < buildTowerStatsSnapshot.Count; i++) + { + _buildTowerStatsSnapshot[i] = InventoryCloneUtility.CloneTowerStats(buildTowerStatsSnapshot[i]); + } + } + + public MapData CloneForEntity(int entityId, Vector3 position) + { + return new MapData( + entityId, + TypeId, + _levelId, + position, + _initialCoin, + _buildTowerStatsSnapshot, + _inventorySnapshot, + _participantTowerSnapshot); + } + + public void SetParticipantTowerSnapshot(IReadOnlyList participantTowerSnapshot) + { + if (participantTowerSnapshot == null || participantTowerSnapshot.Count <= 0) + { + _participantTowerSnapshot = Array.Empty(); + return; + } + + _participantTowerSnapshot = new TowerItemData[participantTowerSnapshot.Count]; + for (int i = 0; i < participantTowerSnapshot.Count; i++) + { + _participantTowerSnapshot[i] = InventoryCloneUtility.CloneTower(participantTowerSnapshot[i]); + } + } + } +} diff --git a/src-ref/Entity/EntityData/MapEntityLoadContext.cs b/src-ref/Entity/EntityData/MapEntityLoadContext.cs new file mode 100644 index 0000000..b38659e --- /dev/null +++ b/src-ref/Entity/EntityData/MapEntityLoadContext.cs @@ -0,0 +1,18 @@ +using System; + +namespace GeometryTD.Entity.EntityData +{ + public sealed class MapEntityLoadContext + { + public MapEntityLoadContext(MapData initialMapData, Func tryConsumeCoin, Action addCoin) + { + InitialMapData = initialMapData; + TryConsumeCoin = tryConsumeCoin; + AddCoin = addCoin; + } + + public MapData InitialMapData { get; } + public Func TryConsumeCoin { get; } + public Action AddCoin { get; } + } +} diff --git a/src-ref/Entity/EntityData/PlayerData.cs b/src-ref/Entity/EntityData/PlayerData.cs new file mode 100644 index 0000000..fb7dc49 --- /dev/null +++ b/src-ref/Entity/EntityData/PlayerData.cs @@ -0,0 +1,40 @@ +using System; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.Entity.EntityData +{ + [Serializable] + public class PlayerData : EntityDataBase + { + [SerializeField] private CampType _camp = CampType.Player; + + [SerializeField] private int _maxHealth = 0; + + [SerializeField] private float _speed = 0; + + public PlayerData(int entityId, int typeId, int maxHp, float speed) : base(entityId, typeId) + { + _maxHealth = maxHp; + _speed = speed; + } + + public CampType Camp + { + get => _camp; + set => _camp = value; + } + + public int MaxHealth + { + get => _maxHealth; + set => _maxHealth = value; + } + + public float Speed + { + get => _speed; + set => _speed = value; + } + } +} \ No newline at end of file diff --git a/src-ref/Entity/EntityData/TowerData.cs b/src-ref/Entity/EntityData/TowerData.cs new file mode 100644 index 0000000..e230f2d --- /dev/null +++ b/src-ref/Entity/EntityData/TowerData.cs @@ -0,0 +1,60 @@ +using System; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.Entity.EntityData +{ + [Serializable] + public class TowerData : EntityDataBase + { + [SerializeField] private TowerStatsData _stats; + [SerializeField] private int _towerLevel = 0; + [SerializeField] private Color _muzzleColor; + [SerializeField] private Color _bearingColor; + [SerializeField] private Color _baseColor; + + public TowerData(int entityId, int typeId, Vector3 position, Quaternion rotation, + TowerStatsData stats, int towerLevel = 0, Color? muzzleColor = null, Color? bearingColor = null, + Color? baseColor = null) + : base(entityId, typeId) + { + Position = position; + Rotation = rotation; + _stats = stats ?? new TowerStatsData(); + _towerLevel = Mathf.Max(0, towerLevel); + _muzzleColor = muzzleColor ?? Color.white; + _bearingColor = bearingColor ?? Color.white; + _baseColor = baseColor ?? Color.white; + } + + public TowerStatsData Stats + { + get => _stats; + set => _stats = value ?? new TowerStatsData(); + } + + public int TowerLevel + { + get => _towerLevel; + set => _towerLevel = Mathf.Max(0, value); + } + + public Color MuzzleColor + { + get => _muzzleColor; + set => _muzzleColor = value; + } + + public Color BearingColor + { + get => _bearingColor; + set => _bearingColor = value; + } + + public Color BaseColor + { + get => _baseColor; + set => _baseColor = value; + } + } +} \ No newline at end of file diff --git a/src-ref/Entity/EntityExtension.cs b/src-ref/Entity/EntityExtension.cs new file mode 100644 index 0000000..c2afddd --- /dev/null +++ b/src-ref/Entity/EntityExtension.cs @@ -0,0 +1,97 @@ +using GameFramework.DataTable; +using System; +using GeometryTD.CustomUtility; +using GeometryTD.DataTable; +using GeometryTD.Entity; +using GeometryTD.Entity.EntityData; +using UnityGameFramework.Runtime; + +namespace GeometryTD +{ + public static class EntityExtension + { + private static int _serialId = 0; + + public static EntityBase GetGameEntity(this EntityComponent entityComponent, int entityId) + { + UnityGameFramework.Runtime.Entity entity = entityComponent.GetEntity(entityId); + if (entity == null) + { + return null; + } + + return (EntityBase)entity.Logic; + } + + public static void HideEntity(this EntityComponent entityComponent, EntityBase entity) + { + entityComponent.HideEntity(entity.Entity); + } + + public static void AttachEntity(this EntityComponent entityComponent, EntityBase entityBase, int ownerId, + string parentTransformPath = null, object userData = null) + { + entityComponent.AttachEntity(entityBase.Entity, ownerId, parentTransformPath, userData); + } + + public static void ShowEnemy(this EntityComponent entityComponent, EnemyData data) + { + entityComponent.ShowEntity(typeof(EnemyEntity), "Enemy", Constant.AssetPriority.EnemyAsset, data); + } + + public static void ShowDefenseTower(this EntityComponent entityComponent, TowerData data) + { + entityComponent.ShowEntity(typeof(TowerEntity), "Tower", Constant.AssetPriority.EnemyAsset, data); + } + + public static void ShowBullet(this EntityComponent entityComponent, BulletData data) + { + entityComponent.ShowEntity(typeof(BulletEntity), "Bullet", Constant.AssetPriority.BulletAsset, data); + } + + public static void ShowMap(this EntityComponent entityComponent, MapEntityLoadContext loadContext) + { + ShowMap(entityComponent, loadContext, null); + } + + public static void ShowMap(this EntityComponent entityComponent, MapEntityLoadContext loadContext, string mapAssetName) + { + MapData data = loadContext?.InitialMapData; + if (data == null) + { + Log.Warning("Map data is invalid."); + return; + } + + string resolvedMapAssetName = string.IsNullOrEmpty(mapAssetName) ? data.LevelId.ToString() : mapAssetName; + string mapAssetPath = AssetUtility.GetLevelMapAsset(resolvedMapAssetName); + entityComponent.ShowEntity(data.Id, typeof(MapEntity), mapAssetPath, "Map", Constant.AssetPriority.MapAsset, loadContext); + } + + private static void ShowEntity(this EntityComponent entityComponent, Type logicType, string entityGroup, + int priority, EntityDataBase data) + { + if (data == null) + { + Log.Warning("Data is invalid."); + return; + } + + IDataTable dtEntity = GameEntry.DataTable.GetDataTable(); + DREntity drEntity = dtEntity.GetDataRow(data.TypeId); + if (drEntity == null) + { + Log.Warning("Can not load entity id '{0}' from data table.", data.TypeId.ToString()); + return; + } + + entityComponent.ShowEntity(data.Id, logicType, AssetUtility.GetEntityAsset(drEntity.AssetName), entityGroup, + priority, data); + } + + public static int GenerateSerialId(this EntityComponent entityComponent) + { + return --_serialId; + } + } +} diff --git a/src-ref/Entity/EntityLogic/BulletEntity.cs b/src-ref/Entity/EntityLogic/BulletEntity.cs new file mode 100644 index 0000000..ace1df6 --- /dev/null +++ b/src-ref/Entity/EntityLogic/BulletEntity.cs @@ -0,0 +1,76 @@ +using Components; +using GeometryTD.Entity.EntityData; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.Entity +{ + public class BulletEntity : EntityBase + { + private ShooterBullet _shooterBullet; + + protected override void OnInit(object userData) + { + base.OnInit(userData); + + _shooterBullet = GetComponent(); + if (_shooterBullet == null) + { + Log.Error("ShooterBullet component is missing on bullet entity '{0}'.", name); + } + } + + protected override void OnShow(object userData) + { + base.OnShow(userData); + + if (_shooterBullet == null) + { + GameEntry.Entity.HideEntity(Entity); + return; + } + + if (userData is not BulletData bulletData) + { + Log.Warning("BulletData is invalid for bullet entity '{0}'.", Id); + GameEntry.Entity.HideEntity(Entity); + return; + } + + ConstrainToZRotation(); + _shooterBullet.OnShow(bulletData); + if (_shooterBullet.TryConsumeDespawnRequest()) + { + GameEntry.Entity.HideEntity(Entity); + } + } + + protected override void OnUpdate(float elapseSeconds, float realElapseSeconds) + { + base.OnUpdate(elapseSeconds, realElapseSeconds); + + if (_shooterBullet == null) + { + return; + } + + _shooterBullet.Tick(elapseSeconds); + if (_shooterBullet.TryConsumeDespawnRequest()) + { + GameEntry.Entity.HideEntity(Entity); + } + } + + protected override void OnHide(bool isShutdown, object userData) + { + _shooterBullet?.OnReset(); + base.OnHide(isShutdown, userData); + } + + private void ConstrainToZRotation() + { + Vector3 localEulerAngles = CachedTransform.localEulerAngles; + CachedTransform.localRotation = Quaternion.Euler(0f, 0f, localEulerAngles.z); + } + } +} diff --git a/src-ref/Entity/EntityLogic/CombatSelectInputService.cs b/src-ref/Entity/EntityLogic/CombatSelectInputService.cs new file mode 100644 index 0000000..913e607 --- /dev/null +++ b/src-ref/Entity/EntityLogic/CombatSelectInputService.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using GeometryTD.UI; +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.Tilemaps; + +namespace GeometryTD.Entity +{ + public sealed class CombatSelectInputService + { + public bool TryBuildUserData( + Tilemap tilemap, + Transform mapTransform, + IReadOnlyDictionary towerEntityIdByFoundationCell, + Func isFoundationCell, + Func isTowerAtMaxLevel, + int upgradeCost, + int destroyGain, + out CombatSelectFormUserData userData) + { + userData = null; + if (tilemap == null || !TryGetPointerWorldPosition(tilemap, mapTransform, out Vector3 worldPosition, + out Vector2 screenPosition)) + { + return false; + } + + Vector3Int clickedCell = tilemap.WorldToCell(worldPosition); + CombatSelectClickObjectType clickObjectType = CombatSelectClickObjectType.None; + int towerEntityId = 0; + Vector2 resolvedScreenPosition = screenPosition; + + if (towerEntityIdByFoundationCell != null && + towerEntityIdByFoundationCell.TryGetValue(clickedCell, out int occupiedTowerEntityId)) + { + clickObjectType = CombatSelectClickObjectType.Tower; + towerEntityId = occupiedTowerEntityId; + resolvedScreenPosition = BuildScreenPositionFromCell(tilemap, clickedCell, screenPosition); + } + else if (isFoundationCell != null && isFoundationCell(clickedCell)) + { + clickObjectType = CombatSelectClickObjectType.Foundation; + resolvedScreenPosition = BuildScreenPositionFromCell(tilemap, clickedCell, screenPosition); + } + + userData = new CombatSelectFormUserData + { + ClickObjectType = clickObjectType, + ScreenPosition = resolvedScreenPosition, + WorldPosition = worldPosition, + CellPosition = clickedCell, + TowerEntityId = towerEntityId, + IsTowerAtMaxLevel = towerEntityId != 0 && isTowerAtMaxLevel != null && isTowerAtMaxLevel(towerEntityId), + UpgradeCost = Mathf.Max(0, upgradeCost), + DestroyGain = Mathf.Max(0, destroyGain) + }; + return true; + } + + private static bool TryGetPointerWorldPosition(Tilemap tilemap, Transform mapTransform, + out Vector3 worldPosition, + out Vector2 screenPosition) + { + worldPosition = Vector3.zero; + screenPosition = Vector2.zero; + + Camera mainCamera = GameEntry.Scene != null ? GameEntry.Scene.MainCamera : Camera.main; + if (mainCamera == null) + { + return false; + } + + Mouse mouse = Mouse.current; + if (mouse == null) + { + return false; + } + + Vector2 pointerPosition = mouse.position.ReadValue(); + Ray ray = mainCamera.ScreenPointToRay(pointerPosition); + float mapPlaneZ = tilemap != null + ? tilemap.transform.position.z + : (mapTransform != null ? mapTransform.position.z : 0f); + Vector3 planeNormal = mainCamera.transform.forward.sqrMagnitude > Mathf.Epsilon + ? -mainCamera.transform.forward + : Vector3.forward; + Plane mapPlane = new Plane(planeNormal, new Vector3(0f, 0f, mapPlaneZ)); + if (!mapPlane.Raycast(ray, out float enterDistance)) + { + return false; + } + + worldPosition = ray.GetPoint(enterDistance); + screenPosition = BuildScreenPosition(pointerPosition); + return true; + } + + private static Vector2 BuildScreenPosition(Vector3 pointerScreenPosition) + { + return new Vector2(pointerScreenPosition.x, pointerScreenPosition.y); + } + + private static Vector2 BuildScreenPositionFromCell(Tilemap tilemap, Vector3Int cellPosition, + Vector2 fallbackScreenPosition) + { + if (tilemap == null) + { + return fallbackScreenPosition; + } + + Camera mainCamera = GameEntry.Scene != null ? GameEntry.Scene.MainCamera : Camera.main; + if (mainCamera == null) + { + return fallbackScreenPosition; + } + + Vector3 cellCenterWorld = tilemap.GetCellCenterWorld(cellPosition); + Vector3 cellScreenPosition = mainCamera.WorldToScreenPoint(cellCenterWorld); + if (cellScreenPosition.z < 0f) + { + return fallbackScreenPosition; + } + + return BuildScreenPosition(cellScreenPosition); + } + } +} diff --git a/src-ref/Entity/EntityLogic/CombatSelectUseCaseConfigurator.cs b/src-ref/Entity/EntityLogic/CombatSelectUseCaseConfigurator.cs new file mode 100644 index 0000000..fcfc781 --- /dev/null +++ b/src-ref/Entity/EntityLogic/CombatSelectUseCaseConfigurator.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using GeometryTD.UI; +using UnityEngine; + +namespace GeometryTD.Entity +{ + public sealed class CombatSelectUseCaseConfigurator + { + private const int BuildOptionCount = 4; + + private readonly BuildTowerVisualInfo[] _buildTowerVisualInfos = new BuildTowerVisualInfo[BuildOptionCount]; + + public void Configure( + CombatSelectFormUseCase useCase, + Func coinProvider, + Func> buildActionFactory, + Func upgradeAction, + int upgradeCost, + Func destroyAction, + int destroyGain, + int[] buildTowerCosts, + int currentBuildTowerCount, + BackpackInventoryData inventorySnapshot, + IReadOnlyList participantTowers) + { + if (useCase == null) + { + return; + } + + useCase.SetCoinProvider(coinProvider); + + Dictionary muzzleMap = BuildComponentMap(inventorySnapshot?.MuzzleComponents); + Dictionary bearingMap = BuildComponentMap(inventorySnapshot?.BearingComponents); + Dictionary baseMap = BuildComponentMap(inventorySnapshot?.BaseComponents); + + int availableBuildCount = Mathf.Clamp(currentBuildTowerCount, 0, BuildOptionCount); + for (int i = 0; i < BuildOptionCount; i++) + { + bool isBuildAvailable = i < availableBuildCount; + BuildTowerVisualInfo buildVisual = ResolveBuildTowerVisual(participantTowers, i, muzzleMap, bearingMap, baseMap); + _buildTowerVisualInfos[i] = buildVisual; + useCase.SetBuildAction( + i, + isBuildAvailable && buildActionFactory != null ? buildActionFactory.Invoke(i) : null, + GetBuildTowerCost(buildTowerCosts, i), + null, + isBuildAvailable, + buildVisual.BaseColor, + buildVisual.BearingColor, + buildVisual.MuzzleColor); + useCase.SetBuildVisible(i, isBuildAvailable); + } + + useCase.SetUpgradeAction(upgradeAction, Mathf.Max(0, upgradeCost)); + useCase.SetDestroyAction(destroyAction, Mathf.Max(0, destroyGain)); + } + + public BuildTowerVisualInfo GetBuildVisualInfo(int buildIndex) + { + if (buildIndex < 0 || buildIndex >= _buildTowerVisualInfos.Length) + { + return BuildTowerVisualInfo.Default; + } + + return _buildTowerVisualInfos[buildIndex]; + } + + private static int GetBuildTowerCost(int[] buildTowerCosts, int buildIndex) + { + if (buildTowerCosts == null || buildIndex < 0 || buildIndex >= buildTowerCosts.Length) + { + return 0; + } + + return Mathf.Max(0, buildTowerCosts[buildIndex]); + } + + private static BuildTowerVisualInfo ResolveBuildTowerVisual( + IReadOnlyList participantTowers, + int buildIndex, + IReadOnlyDictionary muzzleMap, + IReadOnlyDictionary bearingMap, + IReadOnlyDictionary baseMap) + { + if (participantTowers == null || buildIndex < 0 || buildIndex >= participantTowers.Count) + { + return BuildTowerVisualInfo.Default; + } + + TowerItemData tower = participantTowers[buildIndex]; + if (tower == null) + { + return BuildTowerVisualInfo.Default; + } + + Color muzzleColor = ResolveComponentColor(muzzleMap, tower.MuzzleComponentInstanceId); + Color bearingColor = ResolveComponentColor(bearingMap, tower.BearingComponentInstanceId); + Color baseColor = ResolveComponentColor(baseMap, tower.BaseComponentInstanceId); + return new BuildTowerVisualInfo(muzzleColor, bearingColor, baseColor); + } + + private static Color ResolveComponentColor(IReadOnlyDictionary componentMap, long componentId) + where TComp : TowerCompItemData + { + if (componentMap == null || componentId <= 0) + { + return Color.white; + } + + return componentMap.TryGetValue(componentId, out TComp component) && component != null + ? IconColorGenerator.GenerateForComponent(component) + : Color.white; + } + + private static Dictionary BuildComponentMap(IReadOnlyList items) + where TComp : TowerCompItemData + { + Dictionary map = new Dictionary(); + if (items == null) + { + return map; + } + + for (int i = 0; i < items.Count; i++) + { + TComp item = items[i]; + if (item == null || item.InstanceId <= 0) + { + continue; + } + + map[item.InstanceId] = item; + } + + return map; + } + } + + public readonly struct BuildTowerVisualInfo + { + public static BuildTowerVisualInfo Default => new BuildTowerVisualInfo(Color.white, Color.white, Color.white); + + public BuildTowerVisualInfo(Color muzzleColor, Color bearingColor, Color baseColor) + { + MuzzleColor = muzzleColor; + BearingColor = bearingColor; + BaseColor = baseColor; + } + + public Color MuzzleColor { get; } + public Color BearingColor { get; } + public Color BaseColor { get; } + } +} diff --git a/src-ref/Entity/EntityLogic/EnemyEntity.cs b/src-ref/Entity/EntityLogic/EnemyEntity.cs new file mode 100644 index 0000000..d554ba7 --- /dev/null +++ b/src-ref/Entity/EntityLogic/EnemyEntity.cs @@ -0,0 +1,237 @@ +using Components; +using System.Collections.Generic; +using GeometryTD.Definition; +using GeometryTD.Entity.EntityData; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.Entity +{ + public class EnemyEntity : EntityBase, IDamageReceiver + { + private const float WaypointReachDistance = 0.05f; + private static readonly List _activeEnemies = new(); + private static readonly HashSet _killedEnemyEntityIds = new(); + + private Transform _target; + private float _speed; + private int _maxHealth; + private int _currentHealth; + private MovementComponent _movementComponent; + private readonly List _pathPoints = new(); + private int _pathPointIndex; + private bool _isDespawnRequested; + private readonly EnemyTagStatusRuntime _tagStatusRuntime = new(); + + public static IReadOnlyList ActiveEnemies => _activeEnemies; + public int CurrentHealth => _currentHealth; + public int MaxHealth => _maxHealth; + public bool HasSlowStatus => _tagStatusRuntime.GetMoveSpeedMultiplier() < 0.999f; + public float MoveSpeedMultiplier => _tagStatusRuntime.GetMoveSpeedMultiplier(); + + public static bool TryConsumeKilledFlag(int entityId) + { + return _killedEnemyEntityIds.Remove(entityId); + } + + protected override void OnInit(object userData) + { + base.OnInit(userData); + + _movementComponent = GetComponent(); + } + + protected override void OnShow(object userData) + { + base.OnShow(userData); + + _target = null; + _pathPoints.Clear(); + _pathPointIndex = 0; + _isDespawnRequested = false; + _maxHealth = 1; + _currentHealth = 1; + _tagStatusRuntime.Reset(); + if (userData is EnemyData enemyData) + { + _speed = enemyData.Speed; + _target = enemyData.Player; + _maxHealth = Mathf.Max(1, enemyData.MaxHealth); + _currentHealth = _maxHealth; + if (enemyData.HasPath) + { + IReadOnlyList pathPoints = enemyData.PathPoints; + for (int i = 0; i < pathPoints.Count; i++) + { + _pathPoints.Add(pathPoints[i]); + } + } + } + + _movementComponent.OnInit(_speed, CachedTransform); + _movementComponent.SetMove(true); + _killedEnemyEntityIds.Remove(Id); + if (!_activeEnemies.Contains(this)) + { + _activeEnemies.Add(this); + } + } + + protected override void OnUpdate(float elapseSeconds, float realElapseSeconds) + { + base.OnUpdate(elapseSeconds, realElapseSeconds); + _tagStatusRuntime.Tick(elapseSeconds, ApplyDamageFromStatus); + + if (_pathPoints.Count > 0) + { + UpdatePathMovement(elapseSeconds, realElapseSeconds); + return; + } + } + + protected override void OnHide(bool isShutdown, object userData) + { + _movementComponent.SetMove(false); + _pathPoints.Clear(); + _pathPointIndex = 0; + _isDespawnRequested = false; + _maxHealth = 0; + _currentHealth = 0; + _tagStatusRuntime.Reset(); + _activeEnemies.Remove(this); + + base.OnHide(isShutdown, userData); + } + + private void OnDestroy() + { + _activeEnemies.Remove(this); + } + + public void TakeDamage(AttackPayload attackPayload) + { + if (_isDespawnRequested || _currentHealth <= 0) + { + return; + } + + HitContext hitContext = TagEffectResolver.ResolveBeforeHit(new HitContext + { + AttackPayload = attackPayload, + TargetEntityId = Id, + TargetPosition = CachedTransform.position, + TargetCurrentHealthBeforeHit = _currentHealth, + TargetCurrentHealthAfterHit = _currentHealth, + TargetMaxHealth = _maxHealth, + TargetMoveSpeedMultiplierBeforeHit = _tagStatusRuntime.GetMoveSpeedMultiplier(), + TargetStatusTagsBeforeHit = _tagStatusRuntime.GetActiveTagSnapshot(), + TargetStatusRuntime = _tagStatusRuntime + }); + ApplyDirectDamage(hitContext.FinalDamage); + hitContext.TargetCurrentHealthAfterHit = _currentHealth; + hitContext.IsKilled = _currentHealth <= 0; + TagEffectResolver.ApplyOnHit(hitContext); + TagEffectResolver.ApplyAfterHit(hitContext); + if (hitContext.IsKilled) + { + TagEffectResolver.ApplyOnKill(hitContext); + } + } + + private void UpdatePathMovement(float elapseSeconds, float realElapseSeconds) + { + if (_isDespawnRequested) + { + return; + } + + if (_pathPointIndex >= _pathPoints.Count) + { + DespawnOnReachHouse(); + return; + } + + Vector3 direction = GetDirectionToPathPoint(_pathPoints[_pathPointIndex], out float distanceSquared); + if (distanceSquared <= WaypointReachDistance * WaypointReachDistance) + { + _pathPointIndex++; + if (_pathPointIndex >= _pathPoints.Count) + { + DespawnOnReachHouse(); + return; + } + + direction = GetDirectionToPathPoint(_pathPoints[_pathPointIndex], out _); + } + + if (direction.sqrMagnitude <= Mathf.Epsilon) + { + _movementComponent.SetMove(false); + return; + } + + _movementComponent.SetSpeedMultiplier(_tagStatusRuntime.GetMoveSpeedMultiplier()); + _movementComponent.SetMove(true); + _movementComponent.SetDirection(direction); + _movementComponent.OnUpdate(elapseSeconds, realElapseSeconds); + } + + private Vector3 GetDirectionToPathPoint(Vector3 worldPoint, out float distanceSquared) + { + Vector3 delta = worldPoint - CachedTransform.position; + distanceSquared = delta.sqrMagnitude; + if (distanceSquared <= Mathf.Epsilon) + { + return Vector3.zero; + } + + return delta.normalized; + } + + private void DespawnOnReachHouse() + { + RequestDespawn(); + } + + private void RequestDespawn() + { + if (_isDespawnRequested) + { + return; + } + + _isDespawnRequested = true; + _movementComponent.SetMove(false); + GameEntry.Entity.HideEntity(Entity); + } + + private void ApplyDamageFromStatus(int damage) + { + ApplyDirectDamage(damage); + } + + private void ApplyDirectDamage(int damage) + { + if (_isDespawnRequested || damage <= 0 || _currentHealth <= 0) + { + return; + } + + int previousHealth = _currentHealth; + _currentHealth = Mathf.Max(0, _currentHealth - damage); + if (_maxHealth > 0) + { + GameEntry.HPBar?.ShowHPBar(this, (float)previousHealth / _maxHealth, + (float)_currentHealth / _maxHealth); + } + + if (_currentHealth > 0) + { + return; + } + + _killedEnemyEntityIds.Add(Id); + RequestDespawn(); + } + } +} diff --git a/src-ref/Entity/EntityLogic/EnemyTagStatusRuntime.cs b/src-ref/Entity/EntityLogic/EnemyTagStatusRuntime.cs new file mode 100644 index 0000000..9618fde --- /dev/null +++ b/src-ref/Entity/EntityLogic/EnemyTagStatusRuntime.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.Entity +{ + public sealed class EnemyTagStatusRuntime + { + private readonly Dictionary _statesByTag = new(); + private readonly List _activeTags = new(); + + public bool HasStatus(TagType tagType) + { + return _statesByTag.ContainsKey(tagType); + } + + public TagType[] GetActiveTagSnapshot() + { + if (_activeTags.Count <= 0) + { + return Array.Empty(); + } + + return _activeTags.ToArray(); + } + + public void Reset() + { + _statesByTag.Clear(); + _activeTags.Clear(); + } + + public void Activate(TagType tagType) + { + if (_activeTags.Contains(tagType)) + { + return; + } + + _activeTags.Add(tagType); + } + + public TState GetState(TagType tagType) where TState : EnemyStatusTagStateBase + { + if (!_statesByTag.TryGetValue(tagType, out EnemyStatusTagStateBase state)) + { + return null; + } + + Debug.Assert(state is TState); + return state as TState; + } + + public TState GetOrCreateState(TagType tagType) where TState : EnemyStatusTagStateBase, new() + { + if (_statesByTag.TryGetValue(tagType, out EnemyStatusTagStateBase existingState)) + { + Debug.Assert(existingState is TState); + return existingState as TState; + } + + TState state = new TState(); + _statesByTag[tagType] = state; + return state; + } + + public void Tick(float deltaTime, Action applyDamage) + { + float resolvedDeltaTime = Mathf.Max(0f, deltaTime); + if (resolvedDeltaTime <= 0f) + { + return; + } + + for (int i = _activeTags.Count - 1; i >= 0; i--) + { + TagType tagType = _activeTags[i]; + Debug.Assert(EnemyStatusTagRegistry.TryGetEffect(tagType, out IEnemyStatusTagEffect effect)); + + if (effect.Tick(this, resolvedDeltaTime, applyDamage)) + { + continue; + } + + _statesByTag.Remove(tagType); + _activeTags.RemoveAt(i); + } + } + + public float GetMoveSpeedMultiplier() + { + float multiplier = 1f; + for (int i = 0; i < _activeTags.Count; i++) + { + TagType tagType = _activeTags[i]; + Debug.Assert(EnemyStatusTagRegistry.TryGetEffect(tagType, out IEnemyStatusTagEffect effect)); + multiplier = Mathf.Min(multiplier, effect.GetMoveSpeedMultiplier(this)); + } + + return multiplier; + } + } +} diff --git a/src-ref/Entity/EntityLogic/EntityBase.cs b/src-ref/Entity/EntityLogic/EntityBase.cs new file mode 100644 index 0000000..8ae9ef6 --- /dev/null +++ b/src-ref/Entity/EntityLogic/EntityBase.cs @@ -0,0 +1,114 @@ +using GeometryTD.Entity.EntityData; +using GameFramework; +using GeometryTD; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.Entity +{ + public abstract class EntityBase : EntityLogic + { + [SerializeField] private EntityDataBase _entityData = null; + + public int Id => Entity.Id; + + public Animation CachedAnimation + { + get; + private set; + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnInit(object userData) +#else + protected internal override void OnInit(object userData) +#endif + { + base.OnInit(userData); + CachedAnimation = GetComponent(); + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnRecycle() +#else + protected internal override void OnRecycle() +#endif + { + base.OnRecycle(); + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnShow(object userData) +#else + protected internal override void OnShow(object userData) +#endif + { + base.OnShow(userData); + + _entityData = userData as EntityDataBase; + if (_entityData == null) + { + Log.Error("Entity data is invalid."); + return; + } + + Name = Utility.Text.Format("[Entity {0}]", Id); + CachedTransform.localPosition = _entityData.Position; + CachedTransform.localRotation = _entityData.Rotation; + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnHide(bool isShutdown, object userData) +#else + protected internal override void OnHide(bool isShutdown, object userData) +#endif + { + base.OnHide(isShutdown, userData); + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnAttached(EntityLogic childEntity, Transform parentTransform, object userData) +#else + protected internal override void OnAttached(EntityLogic childEntity, Transform parentTransform, object userData) +#endif + { + base.OnAttached(childEntity, parentTransform, userData); + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnDetached(EntityLogic childEntity, object userData) +#else + protected internal override void OnDetached(EntityLogic childEntity, object userData) +#endif + { + base.OnDetached(childEntity, userData); + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnAttachTo(EntityLogic parentEntity, Transform parentTransform, object userData) +#else + protected internal override void OnAttachTo(EntityLogic parentEntity, Transform parentTransform, object userData) +#endif + { + base.OnAttachTo(parentEntity, parentTransform, userData); + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnDetachFrom(EntityLogic parentEntity, object userData) +#else + protected internal override void OnDetachFrom(EntityLogic parentEntity, object userData) +#endif + { + base.OnDetachFrom(parentEntity, userData); + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnUpdate(float elapseSeconds, float realElapseSeconds) +#else + protected internal override void OnUpdate(float elapseSeconds, float realElapseSeconds) +#endif + { + base.OnUpdate(elapseSeconds, realElapseSeconds); + } + } +} diff --git a/src-ref/Entity/EntityLogic/MapEntity.cs b/src-ref/Entity/EntityLogic/MapEntity.cs new file mode 100644 index 0000000..8c056cf --- /dev/null +++ b/src-ref/Entity/EntityLogic/MapEntity.cs @@ -0,0 +1,439 @@ +using System; +using System.Collections.Generic; +using GeometryTD.CustomComponent; +using GeometryTD.Map; +using GeometryTD.Definition; +using GeometryTD.Entity.EntityData; +using GeometryTD.UI; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.InputSystem; +using UnityEngine.Tilemaps; +using UnityGameFramework.Runtime; + +namespace GeometryTD.Entity +{ + public class MapEntity : EntityBase + { + private const int DefaultTowerTypeId = 401; + + private static readonly Spawner[] EmptySpawners = Array.Empty(); + + [SerializeField] private bool _enableCombatSelectInput = true; + + [SerializeField] private int _towerTypeId = DefaultTowerTypeId; + + [SerializeField] private int[] _buildTowerCosts = { 40, 60, 60, 80 }; + + [SerializeField] private int _upgradeCost = 50; + + [SerializeField] private int _destroyGain = 40; + + private MapDataRefs _mapDataRefs; + private MapData _mapData; + private MapTopologyService _mapTopologyService; + private CombatSelectFormUseCase _combatSelectFormUseCase; + private CombatSelectInputService _combatSelectInputService; + private TowerPlacementService _towerPlacementService; + private TowerSelectionPresenter _towerSelectionPresenter; + private CombatSelectUseCaseConfigurator _combatSelectUseCaseConfigurator; + private MapCombatRuntimeBridge _combatRuntimeBridge; + + public IReadOnlyList PathCells => _mapTopologyService != null + ? _mapTopologyService.PathCells + : Array.Empty(); + public IReadOnlyList FoundationCells => _mapTopologyService != null + ? _mapTopologyService.FoundationCells + : Array.Empty(); + public Tilemap Tilemap => _mapDataRefs != null ? _mapDataRefs.Tilemap : null; + public Spawner[] Spawners => _mapDataRefs?.Spawners ?? EmptySpawners; + public House House => _mapDataRefs?.House; + + public bool IsPathCell(Vector3Int cellPosition) + { + return _mapTopologyService != null && _mapTopologyService.IsPathCell(cellPosition); + } + + public bool IsFoundationCell(Vector3Int cellPosition) + { + return _mapTopologyService != null && _mapTopologyService.IsFoundationCell(cellPosition); + } + + public bool TryGetNearestPathCell(Vector3 worldPosition, out Vector3Int pathCell) + { + pathCell = default; + return _mapTopologyService != null && _mapTopologyService.TryGetNearestPathCell(Tilemap, worldPosition, out pathCell); + } + + public Vector3 GetPathCellCenterWorld(Vector3Int pathCell) + { + return _mapTopologyService != null + ? _mapTopologyService.GetPathCellCenterWorld(Tilemap, pathCell) + : Vector3.zero; + } + + public bool TryGetDefaultPathCells(Spawner spawner, out IReadOnlyList pathCells) + { + pathCells = null; + return _mapTopologyService != null && _mapTopologyService.TryGetDefaultPathCells(spawner, out pathCells); + } + + public bool TryFindPathCells(Spawner spawner, IReadOnlyCollection blockedCells, + List pathResult) + { + return _mapTopologyService != null && _mapTopologyService.TryFindPathCells(spawner, blockedCells, pathResult); + } + + public bool TryFindPathWorldPoints(Spawner spawner, IReadOnlyCollection blockedCells, + List worldPathResult) + { + return _mapTopologyService != null && + _mapTopologyService.TryFindPathWorldPoints(Tilemap, spawner, blockedCells, worldPathResult); + } + + protected override void OnInit(object userData) + { + base.OnInit(userData); + + _mapDataRefs = GetComponent(); + if (_mapDataRefs == null) + { + Log.Error("MapDataRefs is missing on map entity '{0}'.", name); + } + + InitializeCombatSelectUseCase(); + InitializeCombatSelectUseCaseConfigurator(); + InitializeCombatSelectInputService(); + InitializeMapTopologyService(); + InitializeTowerPlacementService(); + InitializeTowerSelectionPresenter(); + InitializeCombatRuntimeBridge(); + } + + protected override void OnShow(object userData) + { + MapEntityLoadContext loadContext = ResolveLoadContext(userData); + _mapData = loadContext?.InitialMapData; + if (_mapData == null) + { + Log.Warning("MapData is invalid for map entity '{0}'.", Id); + } + + base.OnShow(_mapData); + + _combatRuntimeBridge?.Initialize(loadContext, name); + + RefreshTiles(); + ConfigureCombatSelectUseCase(); + HideCombatSelectForm(); + } + + protected override void OnHide(bool isShutdown, object userData) + { + _combatRuntimeBridge?.Reset(); + HideCombatSelectForm(); + _towerPlacementService?.HideAndClearAllPlacedTowers(); + ClearSelectionState(); + ClearTowerTracking(); + ClearMapTopology(); + base.OnHide(isShutdown, userData); + } + + protected override void OnUpdate(float elapseSeconds, float realElapseSeconds) + { + base.OnUpdate(elapseSeconds, realElapseSeconds); + HandleCombatSelectInput(); + } + + private void RefreshTiles() + { + ClearMapTopology(); + ClearTowerTracking(); + ClearSelectionState(); + + if (_mapDataRefs == null) + { + _mapDataRefs = GetComponent(); + if (_mapDataRefs == null) + { + Log.Error("MapDataRefs is missing on map entity '{0}'.", name); + return; + } + } + + Tilemap tilemap = _mapDataRefs.Tilemap; + if (tilemap == null) + { + Log.Error("Tilemap reference is missing in MapDataRefs on '{0}'.", name); + return; + } + + _mapTopologyService?.Refresh(tilemap, Spawners, House, name, _mapData != null ? _mapData.LevelId : 0); + } + + private void ClearMapTopology() + { + _mapTopologyService?.Clear(); + } + + private void ClearTowerTracking() + { + _towerPlacementService?.ClearTracking(); + } + + private void ClearSelectionState() + { + _towerSelectionPresenter?.ClearSelectedObject(); + } + + private void InitializeCombatSelectUseCase() + { + if (_combatSelectFormUseCase == null) + { + _combatSelectFormUseCase = new CombatSelectFormUseCase(); + } + + GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatSelectForm, _combatSelectFormUseCase); + } + + private void InitializeCombatSelectUseCaseConfigurator() + { + if (_combatSelectUseCaseConfigurator == null) + { + _combatSelectUseCaseConfigurator = new CombatSelectUseCaseConfigurator(); + } + } + + private void InitializeTowerSelectionPresenter() + { + if (_towerSelectionPresenter == null) + { + _towerSelectionPresenter = new TowerSelectionPresenter(); + } + } + + private void InitializeTowerPlacementService() + { + if (_towerPlacementService == null) + { + _towerPlacementService = new TowerPlacementService(); + } + } + + private void InitializeCombatSelectInputService() + { + if (_combatSelectInputService == null) + { + _combatSelectInputService = new CombatSelectInputService(); + } + } + + private void InitializeMapTopologyService() + { + if (_mapTopologyService == null) + { + _mapTopologyService = new MapTopologyService(); + } + } + + private void InitializeCombatRuntimeBridge() + { + if (_combatRuntimeBridge == null) + { + _combatRuntimeBridge = new MapCombatRuntimeBridge(); + } + } + + private void ConfigureCombatSelectUseCase() + { + _combatSelectFormUseCase.SetCoinProvider(GetCurrentCoin); + _combatSelectUseCaseConfigurator?.Configure( + _combatSelectFormUseCase, + GetCurrentCoin, + buildIndex => () => TryBuildTower(buildIndex), + TryUpgradeTower, + _upgradeCost, + TryDestroyTower, + _destroyGain, + _buildTowerCosts, + GetCurrentBuildTowerCount(), + _mapData != null ? _mapData.InventorySnapshot : null, + _mapData != null ? _mapData.ParticipantTowerSnapshot : null); + } + + private MapEntityLoadContext ResolveLoadContext(object userData) + { + if (userData is MapEntityLoadContext loadContext) + { + return loadContext; + } + + return null; + } + + private void HandleCombatSelectInput() + { + Mouse mouse = Mouse.current; + if (!_enableCombatSelectInput || mouse == null || !mouse.leftButton.wasPressedThisFrame) + { + return; + } + + if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject()) + { + return; + } + + if (_combatSelectInputService == null || + !_combatSelectInputService.TryBuildUserData(Tilemap, CachedTransform, + _towerPlacementService != null ? _towerPlacementService.TowerEntityIdByFoundationCell : null, + IsFoundationCell, IsTowerAtMaxLevel, _upgradeCost, _destroyGain, + out CombatSelectFormUserData userData)) + { + userData = new CombatSelectFormUserData + { + ClickObjectType = CombatSelectClickObjectType.None + }; + } + + ApplySelectedObject(userData); + GameEntry.UIRouter.OpenUI(UIFormType.CombatSelectForm, userData); + } + + private bool IsTowerAtMaxLevel(int towerEntityId) + { + return _towerPlacementService != null && _towerPlacementService.IsTowerAtMaxLevel(towerEntityId); + } + + private void ApplySelectedObject(CombatSelectFormUserData userData) + { + _towerSelectionPresenter?.ApplySelectedObject(userData); + } + + private bool TryBuildTower(int buildIndex) + { + if (_towerSelectionPresenter == null || + _towerPlacementService == null || + !_towerSelectionPresenter.TryGetSelectedFoundationCell(out Vector3Int selectedFoundationCell) || + !IsFoundationCell(selectedFoundationCell)) + { + return false; + } + + if (_mapData == null || !_mapData.TryGetBuildTowerStats(buildIndex, out TowerStatsData buildTowerStats)) + { + return false; + } + + BuildTowerVisualInfo buildVisual = _combatSelectUseCaseConfigurator != null + ? _combatSelectUseCaseConfigurator.GetBuildVisualInfo(buildIndex) + : BuildTowerVisualInfo.Default; + + if (!_towerPlacementService.TryBuildTower(selectedFoundationCell, IsFoundationCell, buildIndex, _buildTowerCosts, + buildTowerStats, buildVisual.MuzzleColor, buildVisual.BearingColor, buildVisual.BaseColor, _towerTypeId, Tilemap, TryConsumeCoin, AddCoin, out int towerEntityId)) + { + return false; + } + + _towerSelectionPresenter.SelectTower(selectedFoundationCell, towerEntityId); + return true; + } + + private bool TryUpgradeTower() + { + if (_towerSelectionPresenter == null || + _towerPlacementService == null || + !_towerSelectionPresenter.TryGetSelectedTower(_towerPlacementService.FoundationCellByTowerEntityId, + out int towerEntityId, + out Vector3Int foundationCell)) + { + return false; + } + + if (!_towerPlacementService.TryUpgradeTower(towerEntityId, _upgradeCost, _towerTypeId, Tilemap, + TryConsumeCoin, AddCoin, out int resultTowerEntityId, out foundationCell)) + { + if (resultTowerEntityId != 0) + { + _towerSelectionPresenter.SelectTower(foundationCell, resultTowerEntityId); + } + else + { + _towerSelectionPresenter.ClearSelectedObject(); + } + return false; + } + + _towerSelectionPresenter.SelectTower(foundationCell, resultTowerEntityId); + return true; + } + + private bool TryDestroyTower() + { + if (_towerSelectionPresenter == null || + _towerPlacementService == null || + !_towerSelectionPresenter.TryGetSelectedTower(_towerPlacementService.FoundationCellByTowerEntityId, + out int towerEntityId, + out Vector3Int foundationCell)) + { + return false; + } + + if (!_towerPlacementService.TryDestroyTower(towerEntityId, _destroyGain, AddCoin, out foundationCell)) + { + return false; + } + + _towerSelectionPresenter.ClearSelectedObject(); + return true; + } + + private void HideCombatSelectForm() + { + _combatSelectFormUseCase?.Hide(); + GameEntry.UIRouter.CloseUI(UIFormType.CombatSelectForm); + } + + private int GetCurrentBuildTowerCount() + { + if (_mapData == null) + { + return 0; + } + + return Mathf.Clamp(_mapData.CurrentBuildTowerCount, 0, 4); + } + + private bool TryConsumeCoin(int cost) + { + int requiredCoin = Mathf.Max(0, cost); + if (requiredCoin <= 0) + { + return true; + } + + if (_mapData == null) + { + return false; + } + + return _combatRuntimeBridge != null && _combatRuntimeBridge.TryConsumeCoin(requiredCoin); + } + + private int GetCurrentCoin() + { + return Mathf.Max(0, _combatRuntimeBridge != null ? _combatRuntimeBridge.CurrentCoin : 0); + } + + private void AddCoin(int coin) + { + int amount = Mathf.Max(0, coin); + if (amount <= 0) + { + return; + } + + _combatRuntimeBridge?.AddCoin(amount); + } + } +} diff --git a/src-ref/Entity/EntityLogic/Player.cs b/src-ref/Entity/EntityLogic/Player.cs new file mode 100644 index 0000000..444d7b0 --- /dev/null +++ b/src-ref/Entity/EntityLogic/Player.cs @@ -0,0 +1,55 @@ +using Components; +using GeometryTD.Entity; +using GeometryTD.Entity.EntityData; + +namespace GeometryTD.Entity +{ + public class Player : EntityBase + { + private float _speed; + private InputComponent _inputComponent; + private MovementComponent _movementComponent; + + protected override void OnInit(object userData) + { + base.OnInit(userData); + + _inputComponent = GetComponent(); + _movementComponent = GetComponent(); + } + + protected override void OnShow(object userData) + { + base.OnShow(userData); + + if (userData is PlayerData playerData) + { + _speed = playerData.Speed; + } + + _inputComponent.OnInit(); + _inputComponent.SetListening(true); + + _movementComponent.OnInit(_speed, this.CachedTransform); + _movementComponent.SetMove(true); + } + + protected override void OnUpdate(float elapseSeconds, float realElapseSeconds) + { + base.OnUpdate(elapseSeconds, realElapseSeconds); + + _inputComponent.OnUpdate(elapseSeconds, realElapseSeconds); + + _movementComponent.SetDirection(_inputComponent.Direction); + _movementComponent.OnUpdate(elapseSeconds, realElapseSeconds); + } + + protected override void OnHide(bool isShutdown, object userData) + { + _inputComponent.SetListening(false); + _movementComponent.SetMove(false); + + base.OnHide(isShutdown, userData); + } + } +} \ No newline at end of file diff --git a/src-ref/Entity/EntityLogic/TowerEntity.cs b/src-ref/Entity/EntityLogic/TowerEntity.cs new file mode 100644 index 0000000..dba64fc --- /dev/null +++ b/src-ref/Entity/EntityLogic/TowerEntity.cs @@ -0,0 +1,77 @@ +using Components; +using GeometryTD.Definition; +using GeometryTD.Entity.EntityData; +using UnityGameFramework.Runtime; + +namespace GeometryTD.Entity +{ + public class TowerEntity : EntityBase + { + private TowerController _towerController; + + public void SetAttackRangeVisible(bool visible) + { + _towerController?.SetAttackRangeVisible(visible); + } + + public bool TryApplyStats(TowerStatsData stats, int towerLevel) + { + if (_towerController == null || stats == null) + { + return false; + } + + _towerController.OnInit(stats, towerLevel); + return true; + } + + protected override void OnInit(object userData) + { + base.OnInit(userData); + + _towerController = GetComponent(); + if (_towerController == null) + { + Log.Error("DefenseTowerController is missing on tower entity '{0}'.", name); + } + } + + protected override void OnShow(object userData) + { + base.OnShow(userData); + + if (_towerController == null) + { + return; + } + + if (userData is not TowerData towerData) + { + Log.Warning("DefenseTowerData is invalid for tower entity '{0}'.", Id); + _towerController.OnReset(); + GameEntry.Entity.HideEntity(Entity); + return; + } + + _towerController.OnInit( + towerData.Stats, + towerData.TowerLevel, + towerData.MuzzleColor, + towerData.BearingColor, + towerData.BaseColor); + } + + protected override void OnUpdate(float elapseSeconds, float realElapseSeconds) + { + base.OnUpdate(elapseSeconds, realElapseSeconds); + _towerController?.OnUpdate(elapseSeconds); + } + + protected override void OnHide(bool isShutdown, object userData) + { + _towerController?.SetAttackRangeVisible(false); + _towerController?.OnReset(); + base.OnHide(isShutdown, userData); + } + } +} diff --git a/src-ref/Event/Combat/CombatBaseHpChangedEventArgs.cs b/src-ref/Event/Combat/CombatBaseHpChangedEventArgs.cs new file mode 100644 index 0000000..7486e02 --- /dev/null +++ b/src-ref/Event/Combat/CombatBaseHpChangedEventArgs.cs @@ -0,0 +1,37 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombatBaseHpChangedEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatBaseHpChangedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int CurrentBaseHp { get; private set; } + public int DeltaBaseHp { get; private set; } + + + public CombatBaseHpChangedEventArgs() + { + CurrentBaseHp = 100; + DeltaBaseHp = 0; + } + + public static CombatBaseHpChangedEventArgs Create(int currentBaseHp, int deltaBaseHp = 0) + { + var args = ReferencePool.Acquire(); + args.CurrentBaseHp = currentBaseHp; + args.DeltaBaseHp = deltaBaseHp; + + return args; + } + + public override void Clear() + { + CurrentBaseHp = 100; + DeltaBaseHp = 0; + } + } +} diff --git a/src-ref/Event/Combat/CombatCoinChangedEventArgs.cs b/src-ref/Event/Combat/CombatCoinChangedEventArgs.cs new file mode 100644 index 0000000..f0b173c --- /dev/null +++ b/src-ref/Event/Combat/CombatCoinChangedEventArgs.cs @@ -0,0 +1,37 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombatCoinChangedEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatCoinChangedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int CurrentCoin { get; private set; } + public int DeltaCoin { get; private set; } + + + public CombatCoinChangedEventArgs() + { + CurrentCoin = 0; + DeltaCoin = 0; + } + + public static CombatCoinChangedEventArgs Create(int currentCoin, int deltaCoin = 0) + { + var args = ReferencePool.Acquire(); + args.CurrentCoin = currentCoin; + args.DeltaCoin = deltaCoin; + + return args; + } + + public override void Clear() + { + CurrentCoin = 0; + DeltaCoin = 0; + } + } +} diff --git a/src-ref/Event/Combat/CombatDebugFailEventArgs.cs b/src-ref/Event/Combat/CombatDebugFailEventArgs.cs new file mode 100644 index 0000000..ae32419 --- /dev/null +++ b/src-ref/Event/Combat/CombatDebugFailEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombatDebugFailEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatDebugFailEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static CombatDebugFailEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src-ref/Event/Combat/CombatEndEventArgs.cs b/src-ref/Event/Combat/CombatEndEventArgs.cs new file mode 100644 index 0000000..b2a9589 --- /dev/null +++ b/src-ref/Event/Combat/CombatEndEventArgs.cs @@ -0,0 +1,25 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombatEndEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatEndEventArgs).GetHashCode(); + + public override int Id => EventId; + + public CombatEndEventArgs() + { + } + + public static CombatEndEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} \ No newline at end of file diff --git a/src-ref/Event/Combat/CombatEnemyHpRateChangedEventArgs.cs b/src-ref/Event/Combat/CombatEnemyHpRateChangedEventArgs.cs new file mode 100644 index 0000000..b21d2fc --- /dev/null +++ b/src-ref/Event/Combat/CombatEnemyHpRateChangedEventArgs.cs @@ -0,0 +1,31 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombatEnemyHpRateChangedEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatEnemyHpRateChangedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int EnemyHpRateMultiplier { get; private set; } + + public CombatEnemyHpRateChangedEventArgs() + { + EnemyHpRateMultiplier = 1; + } + + public static CombatEnemyHpRateChangedEventArgs Create(int enemyHpRateMultiplier) + { + var args = ReferencePool.Acquire(); + args.EnemyHpRateMultiplier = enemyHpRateMultiplier > 0 ? enemyHpRateMultiplier : 1; + return args; + } + + public override void Clear() + { + EnemyHpRateMultiplier = 1; + } + } +} diff --git a/src-ref/Event/Combat/CombatFinishReturnEventArgs.cs b/src-ref/Event/Combat/CombatFinishReturnEventArgs.cs new file mode 100644 index 0000000..452de0d --- /dev/null +++ b/src-ref/Event/Combat/CombatFinishReturnEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombatFinishReturnEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatFinishReturnEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static CombatFinishReturnEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src-ref/Event/Combat/CombatPauseEventArgs.cs b/src-ref/Event/Combat/CombatPauseEventArgs.cs new file mode 100644 index 0000000..97f0bdd --- /dev/null +++ b/src-ref/Event/Combat/CombatPauseEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombatPauseEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatPauseEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static CombatPauseEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src-ref/Event/Combat/CombatProcessEventArgs.cs b/src-ref/Event/Combat/CombatProcessEventArgs.cs new file mode 100644 index 0000000..fa5c36e --- /dev/null +++ b/src-ref/Event/Combat/CombatProcessEventArgs.cs @@ -0,0 +1,37 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombatProcessEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatProcessEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int CurrentPhase { get; private set; } + + public int TotalPhase { get; private set; } + + public CombatProcessEventArgs() + { + CurrentPhase = 0; + TotalPhase = 0; + } + + public static CombatProcessEventArgs Create(int currentPhase, int totalPhase) + { + var args = ReferencePool.Acquire(); + args.CurrentPhase = currentPhase; + args.TotalPhase = totalPhase; + + return args; + } + + public override void Clear() + { + CurrentPhase = 0; + TotalPhase = 0; + } + } +} \ No newline at end of file diff --git a/src-ref/Event/Combat/CombatSelectItemClickEventArgs.cs b/src-ref/Event/Combat/CombatSelectItemClickEventArgs.cs new file mode 100644 index 0000000..acc1709 --- /dev/null +++ b/src-ref/Event/Combat/CombatSelectItemClickEventArgs.cs @@ -0,0 +1,31 @@ +using GameFramework; +using GameFramework.Event; +using GeometryTD.UI; + +namespace GeometryTD.CustomEvent +{ + public class CombatSelectItemClickEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatSelectItemClickEventArgs).GetHashCode(); + + public override int Id => EventId; + + public CombatSelectActionType ActionType { get; private set; } = CombatSelectActionType.None; + + public int ActionIndex { get; private set; } = -1; + + public static CombatSelectItemClickEventArgs Create(CombatSelectActionType actionType, int actionIndex) + { + CombatSelectItemClickEventArgs args = ReferencePool.Acquire(); + args.ActionType = actionType; + args.ActionIndex = actionIndex; + return args; + } + + public override void Clear() + { + ActionType = CombatSelectActionType.None; + ActionIndex = -1; + } + } +} diff --git a/src-ref/Event/EventForm/EventOptionItemSelectedEventArgs.cs b/src-ref/Event/EventForm/EventOptionItemSelectedEventArgs.cs new file mode 100644 index 0000000..d7b10d8 --- /dev/null +++ b/src-ref/Event/EventForm/EventOptionItemSelectedEventArgs.cs @@ -0,0 +1,27 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class EventOptionItemSelectedEventArgs : GameEventArgs + { + public static int EventId => typeof(EventOptionItemSelectedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int SelectedItemId { get; private set; } = -1; + + public static EventOptionItemSelectedEventArgs Create(int selectedItemId) + { + var args = ReferencePool.Acquire(); + args.SelectedItemId = selectedItemId; + + return args; + } + + public override void Clear() + { + SelectedItemId = -1; + } + } +} \ No newline at end of file diff --git a/src-ref/Event/Game/NodeCompleteEventArgs.cs b/src-ref/Event/Game/NodeCompleteEventArgs.cs new file mode 100644 index 0000000..13b82d1 --- /dev/null +++ b/src-ref/Event/Game/NodeCompleteEventArgs.cs @@ -0,0 +1,68 @@ +using GameFramework; +using GameFramework.Event; +using GeometryTD.Procedure; + +namespace GeometryTD.CustomEvent +{ + public class NodeCompleteEventArgs : GameEventArgs + { + public static int EventId = typeof(NodeCompleteEventArgs).GetHashCode(); + + public override int Id => EventId; + + public string RunId { get; private set; } + + public int NodeId { get; private set; } + + public RunNodeType NodeType { get; private set; } + + public int SequenceIndex { get; private set; } + + public RunNodeCompletionStatus CompletionStatus { get; private set; } + + public bool CombatWon { get; private set; } + + public RunNodeCompletionSnapshot CompletionSnapshot { get; private set; } + + public NodeCompleteEventArgs() + { + } + + public static NodeCompleteEventArgs Create() + { + return Create(null, 0, RunNodeType.None, -1, RunNodeCompletionStatus.Completed, true, null); + } + + public static NodeCompleteEventArgs Create( + string runId, + int nodeId, + RunNodeType nodeType, + int sequenceIndex, + RunNodeCompletionStatus completionStatus, + bool combatWon, + RunNodeCompletionSnapshot completionSnapshot) + { + var args = ReferencePool.Acquire(); + args.RunId = runId; + args.NodeId = nodeId; + args.NodeType = nodeType; + args.SequenceIndex = sequenceIndex; + args.CompletionStatus = completionStatus; + args.CombatWon = combatWon; + args.CompletionSnapshot = completionSnapshot != null ? completionSnapshot.Clone() : null; + + return args; + } + + public override void Clear() + { + RunId = null; + NodeId = 0; + NodeType = RunNodeType.None; + SequenceIndex = -1; + CompletionStatus = RunNodeCompletionStatus.None; + CombatWon = false; + CompletionSnapshot = null; + } + } +} diff --git a/src-ref/Event/Game/NodeEnterEventArgs.cs b/src-ref/Event/Game/NodeEnterEventArgs.cs new file mode 100644 index 0000000..04b20e4 --- /dev/null +++ b/src-ref/Event/Game/NodeEnterEventArgs.cs @@ -0,0 +1,49 @@ +using GameFramework; +using GameFramework.Event; +using GeometryTD.Procedure; + +namespace GeometryTD.CustomEvent +{ + public class NodeEnterEventArgs : GameEventArgs + { + public static int EventId = typeof(NodeEnterEventArgs).GetHashCode(); + + public override int Id => EventId; + + public string RunId { get; private set; } + + public int NodeId { get; private set; } + + public RunNodeType NodeType { get; private set; } + + public int SequenceIndex { get; private set; } + + public NodeEnterEventArgs() + { + } + + public static NodeEnterEventArgs Create() + { + return Create(null, 0, RunNodeType.None, -1); + } + + public static NodeEnterEventArgs Create(string runId, int nodeId, RunNodeType nodeType, int sequenceIndex) + { + var args = ReferencePool.Acquire(); + args.RunId = runId; + args.NodeId = nodeId; + args.NodeType = nodeType; + args.SequenceIndex = sequenceIndex; + + return args; + } + + public override void Clear() + { + RunId = null; + NodeId = 0; + NodeType = RunNodeType.None; + SequenceIndex = -1; + } + } +} diff --git a/src-ref/Event/Game/NodeMapNodeClickEventArgs.cs b/src-ref/Event/Game/NodeMapNodeClickEventArgs.cs new file mode 100644 index 0000000..8b3fb8e --- /dev/null +++ b/src-ref/Event/Game/NodeMapNodeClickEventArgs.cs @@ -0,0 +1,26 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public sealed class NodeMapNodeClickEventArgs : GameEventArgs + { + public static int EventId => typeof(NodeMapNodeClickEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int SequenceIndex { get; private set; } + + public static NodeMapNodeClickEventArgs Create(int sequenceIndex) + { + NodeMapNodeClickEventArgs args = ReferencePool.Acquire(); + args.SequenceIndex = sequenceIndex; + return args; + } + + public override void Clear() + { + SequenceIndex = -1; + } + } +} diff --git a/src-ref/Event/Game/NodeMapNodeEnterRequestedEventArgs.cs b/src-ref/Event/Game/NodeMapNodeEnterRequestedEventArgs.cs new file mode 100644 index 0000000..064a930 --- /dev/null +++ b/src-ref/Event/Game/NodeMapNodeEnterRequestedEventArgs.cs @@ -0,0 +1,43 @@ +using GameFramework; +using GameFramework.Event; +using GeometryTD.Procedure; + +namespace GeometryTD.CustomEvent +{ + public sealed class NodeMapNodeEnterRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(NodeMapNodeEnterRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public string RunId { get; private set; } + + public int NodeId { get; private set; } + + public RunNodeType NodeType { get; private set; } + + public int SequenceIndex { get; private set; } + + public static NodeMapNodeEnterRequestedEventArgs Create( + string runId, + int nodeId, + RunNodeType nodeType, + int sequenceIndex) + { + NodeMapNodeEnterRequestedEventArgs args = ReferencePool.Acquire(); + args.RunId = runId; + args.NodeId = nodeId; + args.NodeType = nodeType; + args.SequenceIndex = sequenceIndex; + return args; + } + + public override void Clear() + { + RunId = null; + NodeId = 0; + NodeType = RunNodeType.None; + SequenceIndex = -1; + } + } +} diff --git a/src-ref/Event/Game/TestMenuNodeClickEventArgs.cs b/src-ref/Event/Game/TestMenuNodeClickEventArgs.cs new file mode 100644 index 0000000..df7f777 --- /dev/null +++ b/src-ref/Event/Game/TestMenuNodeClickEventArgs.cs @@ -0,0 +1,33 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public enum TestMenuNodeType : byte + { + Combat = 1, + Event = 2, + Shop = 3 + } + + public class TestMenuNodeClickEventArgs : GameEventArgs + { + public static int EventId => typeof(TestMenuNodeClickEventArgs).GetHashCode(); + + public override int Id => EventId; + + public TestMenuNodeType NodeType { get; private set; } + + public static TestMenuNodeClickEventArgs Create(TestMenuNodeType nodeType) + { + TestMenuNodeClickEventArgs args = ReferencePool.Acquire(); + args.NodeType = nodeType; + return args; + } + + public override void Clear() + { + NodeType = 0; + } + } +} diff --git a/src-ref/Event/General/RewardSelectGiveUpEventArgs.cs b/src-ref/Event/General/RewardSelectGiveUpEventArgs.cs new file mode 100644 index 0000000..9289192 --- /dev/null +++ b/src-ref/Event/General/RewardSelectGiveUpEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class RewardSelectGiveUpEventArgs : GameEventArgs + { + public static int EventId => typeof(RewardSelectGiveUpEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static RewardSelectGiveUpEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src-ref/Event/General/RewardSelectItemSelectedEventArgs.cs b/src-ref/Event/General/RewardSelectItemSelectedEventArgs.cs new file mode 100644 index 0000000..80d46ba --- /dev/null +++ b/src-ref/Event/General/RewardSelectItemSelectedEventArgs.cs @@ -0,0 +1,26 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class RewardSelectItemSelectedEventArgs : GameEventArgs + { + public static int EventId => typeof(RewardSelectItemSelectedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int SelectedIndex { get; private set; } = -1; + + public static RewardSelectItemSelectedEventArgs Create(int selectedIndex) + { + RewardSelectItemSelectedEventArgs args = ReferencePool.Acquire(); + args.SelectedIndex = selectedIndex; + return args; + } + + public override void Clear() + { + SelectedIndex = -1; + } + } +} diff --git a/src-ref/Event/General/RewardSelectRefreshEventArgs.cs b/src-ref/Event/General/RewardSelectRefreshEventArgs.cs new file mode 100644 index 0000000..aaa01df --- /dev/null +++ b/src-ref/Event/General/RewardSelectRefreshEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class RewardSelectRefreshEventArgs : GameEventArgs + { + public static int EventId => typeof(RewardSelectRefreshEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static RewardSelectRefreshEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src-ref/Event/MainForm/RepoButtonClickedEventArgs.cs b/src-ref/Event/MainForm/RepoButtonClickedEventArgs.cs new file mode 100644 index 0000000..9d57b2f --- /dev/null +++ b/src-ref/Event/MainForm/RepoButtonClickedEventArgs.cs @@ -0,0 +1,27 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class RepoButtonClickedEventArgs : GameEventArgs + { + public static int EventId = typeof(RepoButtonClickedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public RepoButtonClickedEventArgs() + { + } + + public static RepoButtonClickedEventArgs Create() + { + var args = ReferencePool.Acquire(); + + return args; + } + + public override void Clear() + { + } + } +} \ No newline at end of file diff --git a/src-ref/Event/MainForm/ReturnButtonClickedEventArgs.cs b/src-ref/Event/MainForm/ReturnButtonClickedEventArgs.cs new file mode 100644 index 0000000..02ee195 --- /dev/null +++ b/src-ref/Event/MainForm/ReturnButtonClickedEventArgs.cs @@ -0,0 +1,27 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class ReturnButtonClickedEventArgs : GameEventArgs + { + public static int EventId = typeof(ReturnButtonClickedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public ReturnButtonClickedEventArgs() + { + } + + public static ReturnButtonClickedEventArgs Create() + { + var args = ReferencePool.Acquire(); + + return args; + } + + public override void Clear() + { + } + } +} \ No newline at end of file diff --git a/src-ref/Event/Menu/MenuExitRequestedEventArgs.cs b/src-ref/Event/Menu/MenuExitRequestedEventArgs.cs new file mode 100644 index 0000000..ec44241 --- /dev/null +++ b/src-ref/Event/Menu/MenuExitRequestedEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class MenuExitRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(MenuExitRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static MenuExitRequestedEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src-ref/Event/Menu/MenuSettingsRequestedEventArgs.cs b/src-ref/Event/Menu/MenuSettingsRequestedEventArgs.cs new file mode 100644 index 0000000..6d9db5e --- /dev/null +++ b/src-ref/Event/Menu/MenuSettingsRequestedEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class MenuSettingsRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(MenuSettingsRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static MenuSettingsRequestedEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src-ref/Event/Menu/MenuStartRequestedEventArgs.cs b/src-ref/Event/Menu/MenuStartRequestedEventArgs.cs new file mode 100644 index 0000000..76ffb38 --- /dev/null +++ b/src-ref/Event/Menu/MenuStartRequestedEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class MenuStartRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(MenuStartRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static MenuStartRequestedEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src-ref/Event/RepoForm/CombineSlotClickedEventArgs.cs b/src-ref/Event/RepoForm/CombineSlotClickedEventArgs.cs new file mode 100644 index 0000000..6d9c780 --- /dev/null +++ b/src-ref/Event/RepoForm/CombineSlotClickedEventArgs.cs @@ -0,0 +1,26 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombineSlotClickedEventArgs : GameEventArgs + { + public static int EventId => typeof(CombineSlotClickedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int SlotIndex { get; private set; } = -1; + + public static CombineSlotClickedEventArgs Create(int slotIndex) + { + CombineSlotClickedEventArgs args = ReferencePool.Acquire(); + args.SlotIndex = slotIndex; + return args; + } + + public override void Clear() + { + SlotIndex = -1; + } + } +} diff --git a/src-ref/Event/RepoForm/RepoCombineRequestedEventArgs.cs b/src-ref/Event/RepoForm/RepoCombineRequestedEventArgs.cs new file mode 100644 index 0000000..2fef81e --- /dev/null +++ b/src-ref/Event/RepoForm/RepoCombineRequestedEventArgs.cs @@ -0,0 +1,32 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public sealed class RepoCombineRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(RepoCombineRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public long MuzzleItemId { get; private set; } + public long BearingItemId { get; private set; } + public long BaseItemId { get; private set; } + + public static RepoCombineRequestedEventArgs Create(long muzzleItemId, long bearingItemId, long baseItemId) + { + RepoCombineRequestedEventArgs args = ReferencePool.Acquire(); + args.MuzzleItemId = muzzleItemId; + args.BearingItemId = bearingItemId; + args.BaseItemId = baseItemId; + return args; + } + + public override void Clear() + { + MuzzleItemId = 0; + BearingItemId = 0; + BaseItemId = 0; + } + } +} diff --git a/src-ref/Event/RepoForm/RepoFormReturnEventArgs.cs b/src-ref/Event/RepoForm/RepoFormReturnEventArgs.cs new file mode 100644 index 0000000..d9fa41e --- /dev/null +++ b/src-ref/Event/RepoForm/RepoFormReturnEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class RepoFormReturnEventArgs : GameEventArgs + { + public static int EventId => typeof(RepoFormReturnEventArgs).GetHashCode(); + + public override int Id => EventId; + + public override void Clear() + { + } + + public static RepoFormReturnEventArgs Create() + { + return ReferencePool.Acquire(); + } + } +} \ No newline at end of file diff --git a/src-ref/Event/RepoForm/RepoItemClickedEventArgs.cs b/src-ref/Event/RepoForm/RepoItemClickedEventArgs.cs new file mode 100644 index 0000000..b7e97e2 --- /dev/null +++ b/src-ref/Event/RepoForm/RepoItemClickedEventArgs.cs @@ -0,0 +1,31 @@ +using GameFramework; +using GameFramework.Event; +using UnityEngine; + +namespace GeometryTD.CustomEvent +{ + public sealed class RepoItemClickedEventArgs : GameEventArgs + { + public static int EventId => typeof(RepoItemClickedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public long ItemId { get; private set; } + + public Vector2 ScreenPosition { get; private set; } + + public static RepoItemClickedEventArgs Create(long itemId, Vector2 screenPosition) + { + RepoItemClickedEventArgs args = ReferencePool.Acquire(); + args.ItemId = itemId; + args.ScreenPosition = screenPosition; + return args; + } + + public override void Clear() + { + ItemId = 0; + ScreenPosition = Vector2.zero; + } + } +} diff --git a/src-ref/Event/RepoForm/RepoItemDragEndedEventArgs.cs b/src-ref/Event/RepoForm/RepoItemDragEndedEventArgs.cs new file mode 100644 index 0000000..8d8a8e0 --- /dev/null +++ b/src-ref/Event/RepoForm/RepoItemDragEndedEventArgs.cs @@ -0,0 +1,30 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class RepoItemDragEndedEventArgs : GameEventArgs + { + public static int EventId => typeof(RepoItemDragEndedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public long ItemId { get; private set; } + + public bool Assigned { get; private set; } + + public static RepoItemDragEndedEventArgs Create(long itemId, bool assigned) + { + RepoItemDragEndedEventArgs args = ReferencePool.Acquire(); + args.ItemId = itemId; + args.Assigned = assigned; + return args; + } + + public override void Clear() + { + ItemId = 0; + Assigned = false; + } + } +} diff --git a/src-ref/Event/RepoForm/RepoParticipantAssignRequestedEventArgs.cs b/src-ref/Event/RepoForm/RepoParticipantAssignRequestedEventArgs.cs new file mode 100644 index 0000000..7f5e344 --- /dev/null +++ b/src-ref/Event/RepoForm/RepoParticipantAssignRequestedEventArgs.cs @@ -0,0 +1,27 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public sealed class RepoParticipantAssignRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(RepoParticipantAssignRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public long TowerItemId { get; private set; } + + public static RepoParticipantAssignRequestedEventArgs Create(long towerItemId) + { + RepoParticipantAssignRequestedEventArgs args = + ReferencePool.Acquire(); + args.TowerItemId = towerItemId; + return args; + } + + public override void Clear() + { + TowerItemId = 0; + } + } +} diff --git a/src-ref/Event/RepoForm/RepoSellCancelRequestedEventArgs.cs b/src-ref/Event/RepoForm/RepoSellCancelRequestedEventArgs.cs new file mode 100644 index 0000000..5ad03b0 --- /dev/null +++ b/src-ref/Event/RepoForm/RepoSellCancelRequestedEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public sealed class RepoSellCancelRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(RepoSellCancelRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static RepoSellCancelRequestedEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src-ref/Event/RepoForm/RepoSellConfirmRequestedEventArgs.cs b/src-ref/Event/RepoForm/RepoSellConfirmRequestedEventArgs.cs new file mode 100644 index 0000000..ce0557b --- /dev/null +++ b/src-ref/Event/RepoForm/RepoSellConfirmRequestedEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public sealed class RepoSellConfirmRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(RepoSellConfirmRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static RepoSellConfirmRequestedEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src-ref/Event/RepoForm/RepoSellModeToggleRequestedEventArgs.cs b/src-ref/Event/RepoForm/RepoSellModeToggleRequestedEventArgs.cs new file mode 100644 index 0000000..e96c3e0 --- /dev/null +++ b/src-ref/Event/RepoForm/RepoSellModeToggleRequestedEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public sealed class RepoSellModeToggleRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(RepoSellModeToggleRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static RepoSellModeToggleRequestedEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src-ref/Event/Shop/ShopExitRequestedEventArgs.cs b/src-ref/Event/Shop/ShopExitRequestedEventArgs.cs new file mode 100644 index 0000000..7af3669 --- /dev/null +++ b/src-ref/Event/Shop/ShopExitRequestedEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public sealed class ShopExitRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(ShopExitRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static ShopExitRequestedEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src-ref/Event/Shop/ShopInventoryRequestedEventArgs.cs b/src-ref/Event/Shop/ShopInventoryRequestedEventArgs.cs new file mode 100644 index 0000000..b299c12 --- /dev/null +++ b/src-ref/Event/Shop/ShopInventoryRequestedEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public sealed class ShopInventoryRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(ShopInventoryRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static ShopInventoryRequestedEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src-ref/Event/Shop/ShopPurchaseRequestedEventArgs.cs b/src-ref/Event/Shop/ShopPurchaseRequestedEventArgs.cs new file mode 100644 index 0000000..eece0c2 --- /dev/null +++ b/src-ref/Event/Shop/ShopPurchaseRequestedEventArgs.cs @@ -0,0 +1,26 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public sealed class ShopPurchaseRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(ShopPurchaseRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int GoodsIndex { get; private set; } + + public static ShopPurchaseRequestedEventArgs Create(int goodsIndex) + { + ShopPurchaseRequestedEventArgs args = ReferencePool.Acquire(); + args.GoodsIndex = goodsIndex; + return args; + } + + public override void Clear() + { + GoodsIndex = -1; + } + } +} diff --git a/src-ref/Factory/ComponentItemFactory.cs b/src-ref/Factory/ComponentItemFactory.cs new file mode 100644 index 0000000..2519701 --- /dev/null +++ b/src-ref/Factory/ComponentItemFactory.cs @@ -0,0 +1,82 @@ +using System; +using GeometryTD.DataTable; +using GeometryTD.Definition; + +namespace GeometryTD.Factory +{ + public static class ComponentItemFactory + { + public static MuzzleCompItemData CreateMuzzle( + DRMuzzleComp config, + long instanceId, + RarityType rarity, + InventoryTagRandomContext randomContext) + { + RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity); + return new MuzzleCompItemData + { + InstanceId = instanceId, + ConfigId = config.Id, + Name = config.Name, + Rarity = normalizedRarity, + Endurance = 100f, + Constraint = config.Constraint, + Tags = ComponentTagGenerationService.ResolveComponentTags( + config.PossibleTag, + normalizedRarity, + randomContext), + AttackDamage = config.AttackDamage != null ? (int[])config.AttackDamage.Clone() : Array.Empty(), + DamageRandomRate = config.DamageRandomRate, + AttackMethodType = config.AttackMethodType + }; + } + + public static BearingCompItemData CreateBearing( + DRBearingComp config, + long instanceId, + RarityType rarity, + InventoryTagRandomContext randomContext) + { + RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity); + return new BearingCompItemData + { + InstanceId = instanceId, + ConfigId = config.Id, + Name = config.Name, + Rarity = normalizedRarity, + Endurance = 100f, + Constraint = config.Constraint, + Tags = ComponentTagGenerationService.ResolveComponentTags( + config.PossibleTag, + normalizedRarity, + randomContext), + RotateSpeed = config.RotateSpeed != null ? (float[])config.RotateSpeed.Clone() : Array.Empty(), + AttackRange = config.AttackRange != null ? (float[])config.AttackRange.Clone() : Array.Empty() + }; + } + + public static BaseCompItemData CreateBase( + DRBaseComp config, + long instanceId, + RarityType rarity, + InventoryTagRandomContext randomContext) + { + RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity); + return new BaseCompItemData + { + InstanceId = instanceId, + ConfigId = config.Id, + Name = config.Name, + Rarity = normalizedRarity, + Endurance = 100f, + Constraint = config.Constraint, + Tags = ComponentTagGenerationService.ResolveComponentTags( + config.PossibleTag, + normalizedRarity, + randomContext), + AttackSpeed = config.AttackSpeed != null ? (float[])config.AttackSpeed.Clone() : Array.Empty(), + AttackPropertyType = config.AttackPropertyType + }; + } + } +} diff --git a/src-ref/Factory/EventEffectFactory.cs b/src-ref/Factory/EventEffectFactory.cs new file mode 100644 index 0000000..f77abd9 --- /dev/null +++ b/src-ref/Factory/EventEffectFactory.cs @@ -0,0 +1,104 @@ +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using Newtonsoft.Json.Linq; +using UnityGameFramework.Runtime; + +namespace GeometryTD.Factory +{ + public static class EventEffectFactory + { + public static EventEffectBase Create(string rawType, JObject param, float? probability = null) + { + if (string.IsNullOrWhiteSpace(rawType)) + { + return null; + } + + EventEffectType type = EnumUtility.Get(rawType); + switch (type) + { + case EventEffectType.AddGold: + { + int count = GetInt(param, "Count"); + return new AddGoldEffect(new AddGoldParam(count), probability); + } + case EventEffectType.RemoveRandomComps: + { + int count = GetInt(param, "Count"); + RarityType rarity = EnumUtility.Get(GetString(param, "Rarity")); + return new RemoveRandomCompsEffect(new RemoveRandomCompsParam(count, rarity), probability); + } + case EventEffectType.AddRandomComps: + { + int count = GetInt(param, "Count"); + bool hasMinRarity = TryGetString(param, "MinRarity", out string minRarityRaw); + bool hasMaxRarity = TryGetString(param, "MaxRarity", out string maxRarityRaw); + if (hasMinRarity || hasMaxRarity) + { + if (!hasMinRarity || !hasMaxRarity) + { + throw new System.InvalidOperationException( + "AddRandomComps requires both MinRarity and MaxRarity when using ranged rarity config."); + } + + RarityType minRarity = EnumUtility.Get(minRarityRaw); + RarityType maxRarity = EnumUtility.Get(maxRarityRaw); + return new AddRandomCompsEffect(new AddRandomCompsParam(count, minRarity, maxRarity), probability); + } + + RarityType rarity = EnumUtility.Get(GetString(param, "Rarity")); + return new AddRandomCompsEffect(new AddRandomCompsParam(count, rarity), probability); + } + case EventEffectType.DamageRandomTowersEndurance: + { + int count = GetInt(param, "Count"); + int amount = GetInt(param, "Amount"); + return new DamageRandomTowerEnduranceEffect(new DamageRandomTowerEnduranceParam(count, amount), probability); + } + default: + Log.Warning("Unsupported EventEffectType '{0}'.", rawType); + return null; + } + } + + private static int GetInt(JObject param, params string[] keys) + { + if (param == null) + { + return 0; + } + + foreach (string key in keys) + { + if (param.TryGetValue(key, out JToken token) && token.Type != JTokenType.Null) + { + return token.Value(); + } + } + + return 0; + } + + private static string GetString(JObject param, string key) + { + if (param == null || !param.TryGetValue(key, out JToken token) || token.Type == JTokenType.Null) + { + return string.Empty; + } + + return token.Value(); + } + + private static bool TryGetString(JObject param, string key, out string value) + { + value = string.Empty; + if (param == null || !param.TryGetValue(key, out JToken token) || token.Type == JTokenType.Null) + { + return false; + } + + value = token.Value(); + return !string.IsNullOrWhiteSpace(value); + } + } +} diff --git a/src-ref/Factory/EventRequirementFactory.cs b/src-ref/Factory/EventRequirementFactory.cs new file mode 100644 index 0000000..d5ec065 --- /dev/null +++ b/src-ref/Factory/EventRequirementFactory.cs @@ -0,0 +1,75 @@ +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using Newtonsoft.Json.Linq; +using UnityGameFramework.Runtime; + +namespace GeometryTD.Factory +{ + public static class EventRequirementFactory + { + public static EventRequirementBase Create(string rawType, JObject param) + { + if (string.IsNullOrWhiteSpace(rawType)) + { + return null; + } + + EventRequirementType type = EnumUtility.Get(rawType); + switch (type) + { + case EventRequirementType.GoldAtLeast: + { + int count = GetInt(param, "Count", "Gold"); + return new GoldAtLeastRequirement(new GoldAtLeastParam(count)); + } + case EventRequirementType.CompCountAtLeast: + { + int count = GetInt(param, "Count"); + RarityType rarity = EnumUtility.Get(GetString(param, "Rarity")); + return new CompCountAtLeastRequirement(new CompCountAtLeastParam(count, rarity)); + } + case EventRequirementType.TowerCountAtLeast: + { + int count = GetInt(param, "Count"); + return new TowerCountAtLeastRequirement(new TowerCountAtLeastParam(count)); + } + case EventRequirementType.HasRelic: + { + int relicId = GetInt(param, "RelicId", "Id"); + return new HasRelicRequirement(new HasRelicParam(relicId)); + } + default: + Log.Warning("Unsupported EventRequirementType '{0}'.", rawType); + return null; + } + } + + private static int GetInt(JObject param, params string[] keys) + { + if (param == null) + { + return 0; + } + + foreach (string key in keys) + { + if (param.TryGetValue(key, out JToken token) && token.Type != JTokenType.Null) + { + return token.Value(); + } + } + + return 0; + } + + private static string GetString(JObject param, string key) + { + if (param == null || !param.TryGetValue(key, out JToken token) || token.Type == JTokenType.Null) + { + return string.Empty; + } + + return token.Value(); + } + } +} diff --git a/src-ref/Factory/OutGameDropItemBuilder.cs b/src-ref/Factory/OutGameDropItemBuilder.cs new file mode 100644 index 0000000..0a5c30a --- /dev/null +++ b/src-ref/Factory/OutGameDropItemBuilder.cs @@ -0,0 +1,88 @@ +using System; +using GameFramework.DataTable; +using GeometryTD.DataTable; +using GeometryTD.Definition; + +namespace GeometryTD.Factory +{ + public sealed class OutGameDropItemBuilder + { + private readonly IDataTable _muzzleCompTable; + private readonly IDataTable _bearingCompTable; + private readonly IDataTable _baseCompTable; + + public OutGameDropItemBuilder( + IDataTable muzzleCompTable, + IDataTable bearingCompTable, + IDataTable baseCompTable) + { + _muzzleCompTable = muzzleCompTable; + _bearingCompTable = bearingCompTable; + _baseCompTable = baseCompTable; + } + + public bool TryBuildItem( + DROutGameDropPool row, + RarityType rarity, + InventoryGenerationRandomContext randomContext, + out TowerCompItemData droppedItem) + { + droppedItem = null; + if (row.ItemId <= 0 || string.IsNullOrWhiteSpace(row.ItemType)) + { + return false; + } + + string itemType = row.ItemType.Trim(); + if (itemType.Equals("MuzzleComp", StringComparison.OrdinalIgnoreCase)) + { + DRMuzzleComp config = _muzzleCompTable.GetDataRow(row.ItemId); + if (config == null) + { + return false; + } + + droppedItem = ComponentItemFactory.CreateMuzzle( + config, + randomContext.CreateStableItemInstanceId(), + rarity, + randomContext.CreateTagRandomContext(config.Id)); + return true; + } + + if (itemType.Equals("BearingComp", StringComparison.OrdinalIgnoreCase)) + { + DRBearingComp config = _bearingCompTable.GetDataRow(row.ItemId); + if (config == null) + { + return false; + } + + droppedItem = ComponentItemFactory.CreateBearing( + config, + randomContext.CreateStableItemInstanceId(), + rarity, + randomContext.CreateTagRandomContext(config.Id)); + return true; + } + + if (itemType.Equals("BaseComp", StringComparison.OrdinalIgnoreCase)) + { + DRBaseComp config = _baseCompTable.GetDataRow(row.ItemId); + if (config == null) + { + return false; + } + + droppedItem = ComponentItemFactory.CreateBase( + config, + randomContext.CreateStableItemInstanceId(), + rarity, + randomContext.CreateTagRandomContext(config.Id)); + return true; + } + + return false; + } + } +} diff --git a/src-ref/Factory/PhaseEndConditionFactory.cs b/src-ref/Factory/PhaseEndConditionFactory.cs new file mode 100644 index 0000000..f3aaab5 --- /dev/null +++ b/src-ref/Factory/PhaseEndConditionFactory.cs @@ -0,0 +1,25 @@ +using GeometryTD.CustomComponent; +using GeometryTD.Definition; + +namespace GeometryTD.Factory +{ + internal static class PhaseEndConditionFactory + { + private static readonly IPhaseEndCondition None = new NonePhaseEndCondition(); + private static readonly IPhaseEndCondition TimeElapsed = new TimeElapsedPhaseEndCondition(); + private static readonly IPhaseEndCondition EnemiesCleared = new EnemiesClearedPhaseEndCondition(); + private static readonly IPhaseEndCondition BossDead = new BossDeadPhaseEndCondition(); + + public static IPhaseEndCondition Create(PhaseEndType endType) + { + return endType switch + { + PhaseEndType.TimeElapsed => TimeElapsed, + PhaseEndType.EnemiesCleared => EnemiesCleared, + PhaseEndType.BossDead => BossDead, + PhaseEndType.None => None, + _ => None + }; + } + } +} diff --git a/src-ref/Factory/RunStateFactory.cs b/src-ref/Factory/RunStateFactory.cs new file mode 100644 index 0000000..d945aeb --- /dev/null +++ b/src-ref/Factory/RunStateFactory.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using GeometryTD.Definition; +using GeometryTD.Procedure; + +namespace GeometryTD.Factory +{ + public static class RunStateFactory + { + public static RunState CreateFixedRun( + LevelThemeType themeType, + BackpackInventoryData initialInventorySnapshot, + string runId = null, + int runSeed = 0) + { + IReadOnlyList fixedNodeSeeds = FixedRunNodeSequenceBuilder.Build(themeType); + return Create(themeType, initialInventorySnapshot, fixedNodeSeeds, runId, runSeed); + } + + public static RunState Create( + LevelThemeType themeType, + BackpackInventoryData initialInventorySnapshot, + IEnumerable nodeSeeds, + string runId = null, + int runSeed = 0) + { + List nodes = new List(); + if (nodeSeeds != null) + { + int sequenceIndex = 0; + foreach (RunNodeSeed seed in nodeSeeds) + { + if (seed == null) + { + continue; + } + + nodes.Add(new RunNodeState + { + NodeId = seed.NodeId > 0 ? seed.NodeId : sequenceIndex + 1, + NodeType = seed.NodeType, + ThemeType = seed.ThemeType == LevelThemeType.None ? themeType : seed.ThemeType, + LinkedLevelId = seed.LinkedLevelId, + SequenceIndex = seed.SequenceIndex >= 0 ? seed.SequenceIndex : sequenceIndex, + Status = sequenceIndex == 0 ? RunNodeStatus.Available : RunNodeStatus.Locked + }); + sequenceIndex++; + } + } + + return new RunState(runId, runSeed, themeType, nodes, initialInventorySnapshot); + } + + public static int CreateRunSeed() + { + int seed = System.Guid.NewGuid().GetHashCode(); + return seed == 0 ? 1 : seed; + } + } +} \ No newline at end of file diff --git a/src-ref/Network/CSPacketBase.cs b/src-ref/Network/CSPacketBase.cs new file mode 100644 index 0000000..4cd1a52 --- /dev/null +++ b/src-ref/Network/CSPacketBase.cs @@ -0,0 +1,20 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +namespace GeometryTD +{ + public abstract class CSPacketBase : PacketBase + { + public override PacketType PacketType + { + get + { + return PacketType.ClientToServer; + } + } + } +} diff --git a/src-ref/Network/CSPacketHeader.cs b/src-ref/Network/CSPacketHeader.cs new file mode 100644 index 0000000..57bb103 --- /dev/null +++ b/src-ref/Network/CSPacketHeader.cs @@ -0,0 +1,20 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +namespace GeometryTD +{ + public sealed class CSPacketHeader : PacketHeaderBase + { + public override PacketType PacketType + { + get + { + return PacketType.ClientToServer; + } + } + } +} diff --git a/src-ref/Network/NetworkChannelHelper.cs b/src-ref/Network/NetworkChannelHelper.cs new file mode 100644 index 0000000..6e24b39 --- /dev/null +++ b/src-ref/Network/NetworkChannelHelper.cs @@ -0,0 +1,280 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using GameFramework; +using GameFramework.Event; +using GameFramework.Network; +using ProtoBuf; +using ProtoBuf.Meta; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using UnityGameFramework.Runtime; + +namespace GeometryTD +{ + public class NetworkChannelHelper : INetworkChannelHelper + { + private readonly Dictionary m_ServerToClientPacketTypes = new Dictionary(); + private readonly MemoryStream m_CachedStream = new MemoryStream(1024 * 8); + private INetworkChannel m_NetworkChannel = null; + + /// + /// 获取消息包头长度。 + /// + public int PacketHeaderLength + { + get + { + return sizeof(int); + } + } + + /// + /// 初始化网络频道辅助器。 + /// + /// 网络频道。 + public void Initialize(INetworkChannel networkChannel) + { + m_NetworkChannel = networkChannel; + + // 反射注册包和包处理函数。 + Type packetBaseType = typeof(SCPacketBase); + Type packetHandlerBaseType = typeof(PacketHandlerBase); + Assembly assembly = Assembly.GetExecutingAssembly(); + Type[] types = assembly.GetTypes(); + for (int i = 0; i < types.Length; i++) + { + if (!types[i].IsClass || types[i].IsAbstract) + { + continue; + } + + if (types[i].BaseType == packetBaseType) + { + PacketBase packetBase = (PacketBase)Activator.CreateInstance(types[i]); + Type packetType = GetServerToClientPacketType(packetBase.Id); + if (packetType != null) + { + Log.Warning("Already exist packet type '{0}', check '{1}' or '{2}'?.", packetBase.Id.ToString(), packetType.Name, packetBase.GetType().Name); + continue; + } + + m_ServerToClientPacketTypes.Add(packetBase.Id, types[i]); + } + else if (types[i].BaseType == packetHandlerBaseType) + { + IPacketHandler packetHandler = (IPacketHandler)Activator.CreateInstance(types[i]); + m_NetworkChannel.RegisterHandler(packetHandler); + } + } + + GameEntry.Event.Subscribe(UnityGameFramework.Runtime.NetworkConnectedEventArgs.EventId, OnNetworkConnected); + GameEntry.Event.Subscribe(UnityGameFramework.Runtime.NetworkClosedEventArgs.EventId, OnNetworkClosed); + GameEntry.Event.Subscribe(UnityGameFramework.Runtime.NetworkMissHeartBeatEventArgs.EventId, OnNetworkMissHeartBeat); + GameEntry.Event.Subscribe(UnityGameFramework.Runtime.NetworkErrorEventArgs.EventId, OnNetworkError); + GameEntry.Event.Subscribe(UnityGameFramework.Runtime.NetworkCustomErrorEventArgs.EventId, OnNetworkCustomError); + } + + /// + /// 关闭并清理网络频道辅助器。 + /// + public void Shutdown() + { + GameEntry.Event.Unsubscribe(UnityGameFramework.Runtime.NetworkConnectedEventArgs.EventId, OnNetworkConnected); + GameEntry.Event.Unsubscribe(UnityGameFramework.Runtime.NetworkClosedEventArgs.EventId, OnNetworkClosed); + GameEntry.Event.Unsubscribe(UnityGameFramework.Runtime.NetworkMissHeartBeatEventArgs.EventId, OnNetworkMissHeartBeat); + GameEntry.Event.Unsubscribe(UnityGameFramework.Runtime.NetworkErrorEventArgs.EventId, OnNetworkError); + GameEntry.Event.Unsubscribe(UnityGameFramework.Runtime.NetworkCustomErrorEventArgs.EventId, OnNetworkCustomError); + + m_NetworkChannel = null; + } + + /// + /// 准备进行连接。 + /// + public void PrepareForConnecting() + { + m_NetworkChannel.Socket.ReceiveBufferSize = 1024 * 64; + m_NetworkChannel.Socket.SendBufferSize = 1024 * 64; + } + + /// + /// 发送心跳消息包。 + /// + /// 是否发送心跳消息包成功。 + public bool SendHeartBeat() + { + m_NetworkChannel.Send(ReferencePool.Acquire()); + return true; + } + + /// + /// 序列化消息包。 + /// + /// 消息包类型。 + /// 要序列化的消息包。 + /// 要序列化的目标流。 + /// 是否序列化成功。 + public bool Serialize(T packet, Stream destination) where T : Packet + { + PacketBase packetImpl = packet as PacketBase; + if (packetImpl == null) + { + Log.Warning("Packet is invalid."); + return false; + } + + if (packetImpl.PacketType != PacketType.ClientToServer) + { + Log.Warning("Send packet invalid."); + return false; + } + + m_CachedStream.SetLength(m_CachedStream.Capacity); // 此行防止 Array.Copy 的数据无法写入 + m_CachedStream.Position = 0L; + + CSPacketHeader packetHeader = ReferencePool.Acquire(); + Serializer.Serialize(m_CachedStream, packetHeader); + ReferencePool.Release(packetHeader); + + Serializer.SerializeWithLengthPrefix(m_CachedStream, packet, PrefixStyle.Fixed32); + ReferencePool.Release((IReference)packet); + + m_CachedStream.WriteTo(destination); + return true; + } + + /// + /// 反序列化消息包头。 + /// + /// 要反序列化的来源流。 + /// 用户自定义错误数据。 + /// 反序列化后的消息包头。 + public IPacketHeader DeserializePacketHeader(Stream source, out object customErrorData) + { + // 注意:此函数并不在主线程调用! + customErrorData = null; + return (IPacketHeader)RuntimeTypeModel.Default.Deserialize(source, ReferencePool.Acquire(), typeof(SCPacketHeader)); + } + + /// + /// 反序列化消息包。 + /// + /// 消息包头。 + /// 要反序列化的来源流。 + /// 用户自定义错误数据。 + /// 反序列化后的消息包。 + public Packet DeserializePacket(IPacketHeader packetHeader, Stream source, out object customErrorData) + { + // 注意:此函数并不在主线程调用! + customErrorData = null; + + SCPacketHeader scPacketHeader = packetHeader as SCPacketHeader; + if (scPacketHeader == null) + { + Log.Warning("Packet header is invalid."); + return null; + } + + Packet packet = null; + if (scPacketHeader.IsValid) + { + Type packetType = GetServerToClientPacketType(scPacketHeader.Id); + if (packetType != null) + { + packet = (Packet)RuntimeTypeModel.Default.DeserializeWithLengthPrefix(source, ReferencePool.Acquire(packetType), packetType, PrefixStyle.Fixed32, 0); + } + else + { + Log.Warning("Can not deserialize packet for packet id '{0}'.", scPacketHeader.Id.ToString()); + } + } + else + { + Log.Warning("Packet header is invalid."); + } + + ReferencePool.Release(scPacketHeader); + return packet; + } + + private Type GetServerToClientPacketType(int id) + { + Type type = null; + if (m_ServerToClientPacketTypes.TryGetValue(id, out type)) + { + return type; + } + + return null; + } + + private void OnNetworkConnected(object sender, GameEventArgs e) + { + UnityGameFramework.Runtime.NetworkConnectedEventArgs ne = (UnityGameFramework.Runtime.NetworkConnectedEventArgs)e; + if (ne.NetworkChannel != m_NetworkChannel) + { + return; + } + + Log.Info("Network channel '{0}' connected, local address '{1}', remote address '{2}'.", ne.NetworkChannel.Name, ne.NetworkChannel.Socket.LocalEndPoint.ToString(), ne.NetworkChannel.Socket.RemoteEndPoint.ToString()); + } + + private void OnNetworkClosed(object sender, GameEventArgs e) + { + UnityGameFramework.Runtime.NetworkClosedEventArgs ne = (UnityGameFramework.Runtime.NetworkClosedEventArgs)e; + if (ne.NetworkChannel != m_NetworkChannel) + { + return; + } + + Log.Info("Network channel '{0}' closed.", ne.NetworkChannel.Name); + } + + private void OnNetworkMissHeartBeat(object sender, GameEventArgs e) + { + UnityGameFramework.Runtime.NetworkMissHeartBeatEventArgs ne = (UnityGameFramework.Runtime.NetworkMissHeartBeatEventArgs)e; + if (ne.NetworkChannel != m_NetworkChannel) + { + return; + } + + Log.Info("Network channel '{0}' miss heart beat '{1}' times.", ne.NetworkChannel.Name, ne.MissCount.ToString()); + + if (ne.MissCount < 2) + { + return; + } + + ne.NetworkChannel.Close(); + } + + private void OnNetworkError(object sender, GameEventArgs e) + { + UnityGameFramework.Runtime.NetworkErrorEventArgs ne = (UnityGameFramework.Runtime.NetworkErrorEventArgs)e; + if (ne.NetworkChannel != m_NetworkChannel) + { + return; + } + + Log.Info("Network channel '{0}' error, error code is '{1}', error message is '{2}'.", ne.NetworkChannel.Name, ne.ErrorCode.ToString(), ne.ErrorMessage); + + ne.NetworkChannel.Close(); + } + + private void OnNetworkCustomError(object sender, GameEventArgs e) + { + UnityGameFramework.Runtime.NetworkCustomErrorEventArgs ne = (UnityGameFramework.Runtime.NetworkCustomErrorEventArgs)e; + if (ne.NetworkChannel != m_NetworkChannel) + { + return; + } + } + } +} diff --git a/src-ref/Network/Packet/CSHeartBeat.cs b/src-ref/Network/Packet/CSHeartBeat.cs new file mode 100644 index 0000000..bcd9f7c --- /dev/null +++ b/src-ref/Network/Packet/CSHeartBeat.cs @@ -0,0 +1,32 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using ProtoBuf; +using System; + +namespace GeometryTD +{ + [Serializable, ProtoContract(Name = @"CSHeartBeat")] + public class CSHeartBeat : CSPacketBase + { + public CSHeartBeat() + { + } + + public override int Id + { + get + { + return 1; + } + } + + public override void Clear() + { + } + } +} diff --git a/src-ref/Network/Packet/SCHeartBeat.cs b/src-ref/Network/Packet/SCHeartBeat.cs new file mode 100644 index 0000000..0ae996b --- /dev/null +++ b/src-ref/Network/Packet/SCHeartBeat.cs @@ -0,0 +1,32 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using ProtoBuf; +using System; + +namespace GeometryTD +{ + [Serializable, ProtoContract(Name = @"SCHeartBeat")] + public class SCHeartBeat : SCPacketBase + { + public SCHeartBeat() + { + } + + public override int Id + { + get + { + return 2; + } + } + + public override void Clear() + { + } + } +} diff --git a/src-ref/Network/PacketBase.cs b/src-ref/Network/PacketBase.cs new file mode 100644 index 0000000..77032ed --- /dev/null +++ b/src-ref/Network/PacketBase.cs @@ -0,0 +1,32 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using GameFramework.Network; +using ProtoBuf; + +namespace GeometryTD +{ + public abstract class PacketBase : Packet, IExtensible + { + private IExtension m_ExtensionObject; + + public PacketBase() + { + m_ExtensionObject = null; + } + + public abstract PacketType PacketType + { + get; + } + + IExtension IExtensible.GetExtensionObject(bool createIfMissing) + { + return Extensible.GetExtensionObject(ref m_ExtensionObject, createIfMissing); + } + } +} diff --git a/src-ref/Network/PacketHandler/SCHeartBeatHandler.cs b/src-ref/Network/PacketHandler/SCHeartBeatHandler.cs new file mode 100644 index 0000000..77a7fc4 --- /dev/null +++ b/src-ref/Network/PacketHandler/SCHeartBeatHandler.cs @@ -0,0 +1,29 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using GameFramework.Network; +using UnityGameFramework.Runtime; + +namespace GeometryTD +{ + public class SCHeartBeatHandler : PacketHandlerBase + { + public override int Id + { + get + { + return 2; + } + } + + public override void Handle(object sender, Packet packet) + { + SCHeartBeat packetImpl = (SCHeartBeat)packet; + Log.Info("Receive packet '{0}'.", packetImpl.Id.ToString()); + } + } +} diff --git a/src-ref/Network/PacketHandlerBase.cs b/src-ref/Network/PacketHandlerBase.cs new file mode 100644 index 0000000..e6978bf --- /dev/null +++ b/src-ref/Network/PacketHandlerBase.cs @@ -0,0 +1,21 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using GameFramework.Network; + +namespace GeometryTD +{ + public abstract class PacketHandlerBase : IPacketHandler + { + public abstract int Id + { + get; + } + + public abstract void Handle(object sender, Packet packet); + } +} diff --git a/src-ref/Network/PacketHeaderBase.cs b/src-ref/Network/PacketHeaderBase.cs new file mode 100644 index 0000000..7c0f209 --- /dev/null +++ b/src-ref/Network/PacketHeaderBase.cs @@ -0,0 +1,46 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using GameFramework; +using GameFramework.Network; + +namespace GeometryTD +{ + public abstract class PacketHeaderBase : IPacketHeader, IReference + { + public abstract PacketType PacketType + { + get; + } + + public int Id + { + get; + set; + } + + public int PacketLength + { + get; + set; + } + + public bool IsValid + { + get + { + return PacketType != PacketType.Undefined && Id > 0 && PacketLength >= 0; + } + } + + public void Clear() + { + Id = 0; + PacketLength = 0; + } + } +} diff --git a/src-ref/Network/PacketType.cs b/src-ref/Network/PacketType.cs new file mode 100644 index 0000000..d981dff --- /dev/null +++ b/src-ref/Network/PacketType.cs @@ -0,0 +1,27 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +namespace GeometryTD +{ + public enum PacketType : byte + { + /// + /// 未定义。 + /// + Undefined = 0, + + /// + /// 客户端发往服务器的包。 + /// + ClientToServer, + + /// + /// 服务器发往客户端的包。 + /// + ServerToClient, + } +} diff --git a/src-ref/Network/SCPacketBase.cs b/src-ref/Network/SCPacketBase.cs new file mode 100644 index 0000000..5e62031 --- /dev/null +++ b/src-ref/Network/SCPacketBase.cs @@ -0,0 +1,20 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +namespace GeometryTD +{ + public abstract class SCPacketBase : PacketBase + { + public override PacketType PacketType + { + get + { + return PacketType.ServerToClient; + } + } + } +} diff --git a/src-ref/Network/SCPacketHeader.cs b/src-ref/Network/SCPacketHeader.cs new file mode 100644 index 0000000..4b51855 --- /dev/null +++ b/src-ref/Network/SCPacketHeader.cs @@ -0,0 +1,20 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +namespace GeometryTD +{ + public sealed class SCPacketHeader : PacketHeaderBase + { + public override PacketType PacketType + { + get + { + return PacketType.ServerToClient; + } + } + } +} diff --git a/src-ref/PoolObjectBase/HPBarItemObject.cs b/src-ref/PoolObjectBase/HPBarItemObject.cs new file mode 100644 index 0000000..fb7aa0c --- /dev/null +++ b/src-ref/PoolObjectBase/HPBarItemObject.cs @@ -0,0 +1,28 @@ +using GameFramework; +using GameFramework.ObjectPool; +using GeometryTD.UI; +using UnityEngine; + +namespace GeometryTD.PoolObjectBase +{ + public class HPBarItemObject : ObjectBase + { + public static HPBarItemObject Create(object target) + { + HPBarItemObject hpBarItemObject = ReferencePool.Acquire(); + hpBarItemObject.Initialize(target); + return hpBarItemObject; + } + + protected override void Release(bool isShutdown) + { + HPBarItem hpBarItem = (HPBarItem)Target; + if (hpBarItem == null) + { + return; + } + + Object.Destroy(hpBarItem.gameObject); + } + } +} \ No newline at end of file diff --git a/src-ref/PoolObjectBase/RepoItemObject.cs b/src-ref/PoolObjectBase/RepoItemObject.cs new file mode 100644 index 0000000..ac107cb --- /dev/null +++ b/src-ref/PoolObjectBase/RepoItemObject.cs @@ -0,0 +1,28 @@ +using GameFramework; +using GameFramework.ObjectPool; +using GeometryTD.UI; +using UnityEngine; + +namespace GeometryTD.PoolObjectBase +{ + public class RepoItemObject : ObjectBase + { + public static RepoItemObject Create(object target) + { + RepoItemObject itemObject = ReferencePool.Acquire(); + itemObject.Initialize(target); + return itemObject; + } + + protected override void Release(bool isShutdown) + { + RepoItem item = (RepoItem)Target; + if (item == null) + { + return; + } + + Object.Destroy(item.gameObject); + } + } +} diff --git a/src-ref/PoolObjectBase/TowerRepoItemObject.cs b/src-ref/PoolObjectBase/TowerRepoItemObject.cs new file mode 100644 index 0000000..ade0663 --- /dev/null +++ b/src-ref/PoolObjectBase/TowerRepoItemObject.cs @@ -0,0 +1,28 @@ +using GameFramework; +using GameFramework.ObjectPool; +using GeometryTD.UI; +using UnityEngine; + +namespace GeometryTD.PoolObjectBase +{ + public class TowerRepoItemObject : ObjectBase + { + public static TowerRepoItemObject Create(object target) + { + TowerRepoItemObject itemObject = ReferencePool.Acquire(); + itemObject.Initialize(target); + return itemObject; + } + + protected override void Release(bool isShutdown) + { + TowerRepoItem item = (TowerRepoItem)Target; + if (item == null) + { + return; + } + + Object.Destroy(item.gameObject); + } + } +} diff --git a/src-ref/Procedure/Base/ProcedureBase.cs b/src-ref/Procedure/Base/ProcedureBase.cs new file mode 100644 index 0000000..7745c7f --- /dev/null +++ b/src-ref/Procedure/Base/ProcedureBase.cs @@ -0,0 +1,19 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +namespace GeometryTD.Procedure +{ + public abstract class ProcedureBase : GameFramework.Procedure.ProcedureBase + { + // 获取流程是否使用原生对话框 + // 在一些特殊的流程(如游戏逻辑对话框资源更新完成前的流程)中,可以考虑调用原生对话框进行消息提示行为 + public abstract bool UseNativeDialog + { + get; + } + } +} diff --git a/src-ref/Procedure/Base/ProcedureChangeScene.cs b/src-ref/Procedure/Base/ProcedureChangeScene.cs new file mode 100644 index 0000000..9b96bee --- /dev/null +++ b/src-ref/Procedure/Base/ProcedureChangeScene.cs @@ -0,0 +1,149 @@ +using GameFramework.DataTable; +using GameFramework.Event; +using GeometryTD.CustomUtility; +using GeometryTD.DataTable; +using GeometryTD.Definition; +using UnityGameFramework.Runtime; +using ProcedureOwner = GameFramework.Fsm.IFsm; + +namespace GeometryTD.Procedure +{ + public class ProcedureChangeScene : ProcedureBase + { + private int _nextSceneId = 0; + private bool _isChangeSceneComplete = false; + private int _backgroundMusicId = 0; + + public override bool UseNativeDialog => false; + + protected override void OnEnter(ProcedureOwner procedureOwner) + { + base.OnEnter(procedureOwner); + + _isChangeSceneComplete = false; + + GameEntry.Event.Subscribe(LoadSceneSuccessEventArgs.EventId, OnLoadSceneSuccess); + GameEntry.Event.Subscribe(LoadSceneFailureEventArgs.EventId, OnLoadSceneFailure); + GameEntry.Event.Subscribe(LoadSceneUpdateEventArgs.EventId, OnLoadSceneUpdate); + GameEntry.Event.Subscribe(LoadSceneDependencyAssetEventArgs.EventId, OnLoadSceneDependencyAsset); + + // 停止所有声音 + GameEntry.Sound.StopAllLoadingSounds(); + GameEntry.Sound.StopAllLoadedSounds(); + + // 隐藏所有实体 + GameEntry.Entity.HideAllLoadingEntities(); + GameEntry.Entity.HideAllLoadedEntities(); + + // 卸载所有场景 + string[] loadedSceneAssetNames = GameEntry.Scene.GetLoadedSceneAssetNames(); + for (int i = 0; i < loadedSceneAssetNames.Length; i++) + { + GameEntry.Scene.UnloadScene(loadedSceneAssetNames[i]); + } + + // 还原游戏速度 + GameEntry.Base.ResetNormalGameSpeed(); + + _nextSceneId = procedureOwner.GetData("NextSceneId"); + + IDataTable dtScene = GameEntry.DataTable.GetDataTable(); + DRScene drScene = dtScene.GetDataRow(_nextSceneId); + if (drScene == null) + { + Log.Warning("Can not load scene '{0}' from data table.", _nextSceneId.ToString()); + return; + } + + GameEntry.Scene.LoadScene(AssetUtility.GetSceneAsset(drScene.AssetName), Constant.AssetPriority.SceneAsset, + this); + _backgroundMusicId = drScene.BackgroundMusicId; + } + + protected override void OnLeave(ProcedureOwner procedureOwner, bool isShutdown) + { + GameEntry.Event.Unsubscribe(LoadSceneSuccessEventArgs.EventId, OnLoadSceneSuccess); + GameEntry.Event.Unsubscribe(LoadSceneFailureEventArgs.EventId, OnLoadSceneFailure); + GameEntry.Event.Unsubscribe(LoadSceneUpdateEventArgs.EventId, OnLoadSceneUpdate); + GameEntry.Event.Unsubscribe(LoadSceneDependencyAssetEventArgs.EventId, OnLoadSceneDependencyAsset); + + base.OnLeave(procedureOwner, isShutdown); + } + + protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds) + { + base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds); + + if (!_isChangeSceneComplete) + { + return; + } + + SceneType sceneType = (SceneType)_nextSceneId; + switch (sceneType) + { + case SceneType.Menu: + ChangeState(procedureOwner); + break; + case SceneType.Main: + ChangeState(procedureOwner); + break; + default: + Log.Debug($"Scene {sceneType.ToString()} don't configure a procedure"); + break; + } + } + + private void OnLoadSceneSuccess(object sender, GameEventArgs e) + { + LoadSceneSuccessEventArgs ne = (LoadSceneSuccessEventArgs)e; + if (ne.UserData != this) + { + return; + } + + Log.Info("Load scene '{0}' OK.", ne.SceneAssetName); + + if (_backgroundMusicId > 0) + { + GameEntry.Sound.PlayMusic(_backgroundMusicId); + } + + _isChangeSceneComplete = true; + } + + private void OnLoadSceneFailure(object sender, GameEventArgs e) + { + LoadSceneFailureEventArgs ne = (LoadSceneFailureEventArgs)e; + if (ne.UserData != this) + { + return; + } + + Log.Error("Load scene '{0}' failure, error message '{1}'.", ne.SceneAssetName, ne.ErrorMessage); + } + + private void OnLoadSceneUpdate(object sender, GameEventArgs e) + { + LoadSceneUpdateEventArgs ne = (LoadSceneUpdateEventArgs)e; + if (ne.UserData != this) + { + return; + } + + Log.Info("Load scene '{0}' update, progress '{1}'.", ne.SceneAssetName, ne.Progress.ToString("P2")); + } + + private void OnLoadSceneDependencyAsset(object sender, GameEventArgs e) + { + LoadSceneDependencyAssetEventArgs ne = (LoadSceneDependencyAssetEventArgs)e; + if (ne.UserData != this) + { + return; + } + + Log.Info("Load scene '{0}' dependency asset '{1}', count '{2}/{3}'.", ne.SceneAssetName, + ne.DependencyAssetName, ne.LoadedCount.ToString(), ne.TotalCount.ToString()); + } + } +} diff --git a/src-ref/Procedure/Base/ProcedureCheckResources.cs b/src-ref/Procedure/Base/ProcedureCheckResources.cs new file mode 100644 index 0000000..765b7e4 --- /dev/null +++ b/src-ref/Procedure/Base/ProcedureCheckResources.cs @@ -0,0 +1,57 @@ +using UnityGameFramework.Runtime; +using ProcedureOwner = GameFramework.Fsm.IFsm; + +namespace GeometryTD.Procedure +{ + public class ProcedureCheckResources : ProcedureBase + { + private bool m_CheckResourcesComplete = false; + private bool m_NeedUpdateResources = false; + private int m_UpdateResourceCount = 0; + private long m_UpdateResourceTotalCompressedLength = 0L; + + public override bool UseNativeDialog => true; + + protected override void OnEnter(ProcedureOwner procedureOwner) + { + base.OnEnter(procedureOwner); + + m_CheckResourcesComplete = false; + m_NeedUpdateResources = false; + m_UpdateResourceCount = 0; + m_UpdateResourceTotalCompressedLength = 0L; + + GameEntry.Resource.CheckResources(OnCheckResourcesComplete); + } + + protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds) + { + base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds); + + if (!m_CheckResourcesComplete) + { + return; + } + + if (m_NeedUpdateResources) + { + procedureOwner.SetData("UpdateResourceCount", m_UpdateResourceCount); + procedureOwner.SetData("UpdateResourceTotalCompressedLength", m_UpdateResourceTotalCompressedLength); + ChangeState(procedureOwner); + } + else + { + ChangeState(procedureOwner); + } + } + + private void OnCheckResourcesComplete(int movedCount, int removedCount, int updateCount, long updateTotalLength, long updateTotalCompressedLength) + { + m_CheckResourcesComplete = true; + m_NeedUpdateResources = updateCount > 0; + m_UpdateResourceCount = updateCount; + m_UpdateResourceTotalCompressedLength = updateTotalCompressedLength; + Log.Info("Check resources complete, '{0}' resources need to update, compressed length is '{1}', uncompressed length is '{2}'.", updateCount.ToString(), updateTotalCompressedLength.ToString(), updateTotalLength.ToString()); + } + } +} diff --git a/src-ref/Procedure/Base/ProcedureCheckVersion.cs b/src-ref/Procedure/Base/ProcedureCheckVersion.cs new file mode 100644 index 0000000..b98f238 --- /dev/null +++ b/src-ref/Procedure/Base/ProcedureCheckVersion.cs @@ -0,0 +1,162 @@ +using GameFramework; +using GameFramework.Event; +using GameFramework.Resource; +using GeometryTD.Definition; +using GeometryTD.UI; +using UnityEngine; +using UnityGameFramework.Runtime; +using ProcedureOwner = GameFramework.Fsm.IFsm; + +namespace GeometryTD.Procedure +{ + public class ProcedureCheckVersion : ProcedureBase + { + private bool m_CheckVersionComplete = false; + private bool m_NeedUpdateVersion = false; + private VersionInfo m_VersionInfo = null; + + public override bool UseNativeDialog => true; + + protected override void OnEnter(ProcedureOwner procedureOwner) + { + base.OnEnter(procedureOwner); + + m_CheckVersionComplete = false; + m_NeedUpdateVersion = false; + m_VersionInfo = null; + + GameEntry.Event.Subscribe(WebRequestSuccessEventArgs.EventId, OnWebRequestSuccess); + GameEntry.Event.Subscribe(WebRequestFailureEventArgs.EventId, OnWebRequestFailure); + + // 向服务器请求版本信息 + GameEntry.WebRequest.AddWebRequest(Utility.Text.Format(GameEntry.BuiltinData.BuildInfo.CheckVersionUrl, GetPlatformPath()), this); + } + + protected override void OnLeave(ProcedureOwner procedureOwner, bool isShutdown) + { + GameEntry.Event.Unsubscribe(WebRequestSuccessEventArgs.EventId, OnWebRequestSuccess); + GameEntry.Event.Unsubscribe(WebRequestFailureEventArgs.EventId, OnWebRequestFailure); + + base.OnLeave(procedureOwner, isShutdown); + } + + protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds) + { + base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds); + + if (!m_CheckVersionComplete) + { + return; + } + + if (m_NeedUpdateVersion) + { + procedureOwner.SetData("VersionListLength", m_VersionInfo.VersionListLength); + procedureOwner.SetData("VersionListHashCode", m_VersionInfo.VersionListHashCode); + procedureOwner.SetData("VersionListCompressedLength", m_VersionInfo.VersionListCompressedLength); + procedureOwner.SetData("VersionListCompressedHashCode", m_VersionInfo.VersionListCompressedHashCode); + ChangeState(procedureOwner); + } + else + { + ChangeState(procedureOwner); + } + } + + private void GotoUpdateApp(object userData) + { + string url = null; +#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN + url = GameEntry.BuiltinData.BuildInfo.WindowsAppUrl; +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + url = GameEntry.BuiltinData.BuildInfo.MacOSAppUrl; +#elif UNITY_IOS + url = GameEntry.BuiltinData.BuildInfo.IOSAppUrl; +#elif UNITY_ANDROID + url = GameEntry.BuiltinData.BuildInfo.AndroidAppUrl; +#endif + if (!string.IsNullOrEmpty(url)) + { + Application.OpenURL(url); + } + } + + private void OnWebRequestSuccess(object sender, GameEventArgs e) + { + WebRequestSuccessEventArgs ne = (WebRequestSuccessEventArgs)e; + if (ne.UserData != this) + { + return; + } + + // 解析版本信息 + byte[] versionInfoBytes = ne.GetWebResponseBytes(); + string versionInfoString = Utility.Converter.GetString(versionInfoBytes); + m_VersionInfo = Utility.Json.ToObject(versionInfoString); + if (m_VersionInfo == null) + { + Log.Error("Parse VersionInfo failure."); + return; + } + + Log.Info("Latest game version is '{0} ({1})', local game version is '{2} ({3})'.", m_VersionInfo.LatestGameVersion, m_VersionInfo.InternalGameVersion.ToString(), Version.GameVersion, Version.InternalGameVersion.ToString()); + + if (m_VersionInfo.ForceUpdateGame) + { + // 需要强制更新游戏应用 + GameEntry.UI.OpenDialog(new DialogParams + { + Mode = 2, + Title = GameEntry.Localization.GetString("ForceUpdate.Title"), + Message = GameEntry.Localization.GetString("ForceUpdate.Message"), + ConfirmText = GameEntry.Localization.GetString("ForceUpdate.UpdateButton"), + OnClickConfirm = GotoUpdateApp, + CancelText = GameEntry.Localization.GetString("ForceUpdate.QuitButton"), + OnClickCancel = delegate (object userData) { UnityGameFramework.Runtime.GameEntry.Shutdown(ShutdownType.Quit); }, + }); + + return; + } + + // 设置资源更新下载地址 + GameEntry.Resource.UpdatePrefixUri = Utility.Path.GetRegularPath(m_VersionInfo.UpdatePrefixUri); + + m_CheckVersionComplete = true; + m_NeedUpdateVersion = GameEntry.Resource.CheckVersionList(m_VersionInfo.InternalResourceVersion) == CheckVersionListResult.NeedUpdate; + } + + private void OnWebRequestFailure(object sender, GameEventArgs e) + { + WebRequestFailureEventArgs ne = (WebRequestFailureEventArgs)e; + if (ne.UserData != this) + { + return; + } + + Log.Warning("Check version failure, error message is '{0}'.", ne.ErrorMessage); + } + + private string GetPlatformPath() + { + switch (Application.platform) + { + case RuntimePlatform.WindowsEditor: + case RuntimePlatform.WindowsPlayer: + return "Windows"; + + case RuntimePlatform.OSXEditor: + case RuntimePlatform.OSXPlayer: + return "MacOS"; + + case RuntimePlatform.IPhonePlayer: + return "IOS"; + + case RuntimePlatform.Android: + return "Android"; + + default: + throw new System.NotSupportedException(Utility.Text.Format("Platform '{0}' is not supported.", Application.platform)); + } + } + } +} diff --git a/src-ref/Procedure/Base/ProcedureInitResources.cs b/src-ref/Procedure/Base/ProcedureInitResources.cs new file mode 100644 index 0000000..9cae872 --- /dev/null +++ b/src-ref/Procedure/Base/ProcedureInitResources.cs @@ -0,0 +1,48 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using UnityGameFramework.Runtime; +using ProcedureOwner = GameFramework.Fsm.IFsm; + +namespace GeometryTD.Procedure +{ + public class ProcedureInitResources : ProcedureBase + { + private bool m_InitResourcesComplete = false; + + public override bool UseNativeDialog => true; + + protected override void OnEnter(ProcedureOwner procedureOwner) + { + base.OnEnter(procedureOwner); + + m_InitResourcesComplete = false; + + // 注意:使用单机模式并初始化资源前,需要先构建 AssetBundle 并复制到 StreamingAssets 中,否则会产生 HTTP 404 错误 + GameEntry.Resource.InitResources(OnInitResourcesComplete); + } + + protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds) + { + base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds); + + if (!m_InitResourcesComplete) + { + // 初始化资源未完成则继续等待 + return; + } + + ChangeState(procedureOwner); + } + + private void OnInitResourcesComplete() + { + m_InitResourcesComplete = true; + Log.Info("Init resources complete."); + } + } +} diff --git a/src-ref/Procedure/Base/ProcedureLaunch.cs b/src-ref/Procedure/Base/ProcedureLaunch.cs new file mode 100644 index 0000000..803b9fb --- /dev/null +++ b/src-ref/Procedure/Base/ProcedureLaunch.cs @@ -0,0 +1,129 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using GameFramework.Localization; +using System; +using GeometryTD; +using UnityGameFramework.Runtime; +using ProcedureOwner = GameFramework.Fsm.IFsm; + +namespace GeometryTD.Procedure +{ + public class ProcedureLaunch : ProcedureBase + { + public override bool UseNativeDialog => true; + + protected override void OnEnter(ProcedureOwner procedureOwner) + { + base.OnEnter(procedureOwner); + + // 构建信息:发布版本时,把一些数据以 Json 的格式写入 Assets/GameMain/Configs/BuildInfo.txt,供游戏逻辑读取 + GameEntry.BuiltinData.InitBuildInfo(); + + // 语言配置:设置当前使用的语言,如果不设置,则默认使用操作系统语言 + InitLanguageSettings(); + + // 变体配置:根据使用的语言,通知底层加载对应的资源变体 + InitCurrentVariant(); + + // 声音配置:根据用户配置数据,设置即将使用的声音选项 + InitSoundSettings(); + } + + protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds) + { + base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds); + + // 运行一帧即切换到 Splash 展示流程 + ChangeState(procedureOwner); + } + + private void InitLanguageSettings() + { + if (GameEntry.Base.EditorResourceMode && GameEntry.Base.EditorLanguage != Language.Unspecified) + { + // 编辑器资源模式直接使用 Inspector 上设置的语言 + return; + } + + Language language = GameEntry.Localization.Language; + if (GameEntry.Setting.HasSetting(Constant.Setting.Language)) + { + try + { + string languageString = GameEntry.Setting.GetString(Constant.Setting.Language); + language = (Language)Enum.Parse(typeof(Language), languageString); + } + catch + { + } + } + + if (language != Language.English + && language != Language.ChineseSimplified + && language != Language.ChineseTraditional + && language != Language.Korean) + { + // 若是暂不支持的语言,则使用英语 + language = Language.English; + + GameEntry.Setting.SetString(Constant.Setting.Language, language.ToString()); + GameEntry.Setting.Save(); + } + + GameEntry.Localization.Language = language; + Log.Info("Init language settings complete, current language is '{0}'.", language.ToString()); + } + + private void InitCurrentVariant() + { + if (GameEntry.Base.EditorResourceMode) + { + // 编辑器资源模式不使用 AssetBundle,也就没有变体了 + return; + } + + string currentVariant = null; + switch (GameEntry.Localization.Language) + { + case Language.English: + currentVariant = "en-us"; + break; + + case Language.ChineseSimplified: + currentVariant = "zh-cn"; + break; + + case Language.ChineseTraditional: + currentVariant = "zh-tw"; + break; + + case Language.Korean: + currentVariant = "ko-kr"; + break; + + default: + currentVariant = "zh-cn"; + break; + } + + GameEntry.Resource.SetCurrentVariant(currentVariant); + Log.Info("Init current variant complete."); + } + + private void InitSoundSettings() + { + GameEntry.Sound.Mute("Music", GameEntry.Setting.GetBool(Constant.Setting.MusicMuted, false)); + GameEntry.Sound.SetVolume("Music", GameEntry.Setting.GetFloat(Constant.Setting.MusicVolume, 0.3f)); + GameEntry.Sound.Mute("Sound", GameEntry.Setting.GetBool(Constant.Setting.SoundMuted, false)); + GameEntry.Sound.SetVolume("Sound", GameEntry.Setting.GetFloat(Constant.Setting.SoundVolume, 1f)); + GameEntry.Sound.Mute("UISound", GameEntry.Setting.GetBool(Constant.Setting.UISoundMuted, false)); + GameEntry.Sound.SetVolume("UISound", GameEntry.Setting.GetFloat(Constant.Setting.UISoundVolume, 1f)); + Log.Info("Init sound settings complete."); + } + } +} diff --git a/src-ref/Procedure/Base/ProcedurePreload.cs b/src-ref/Procedure/Base/ProcedurePreload.cs new file mode 100644 index 0000000..050ff44 --- /dev/null +++ b/src-ref/Procedure/Base/ProcedurePreload.cs @@ -0,0 +1,237 @@ +using GameFramework; +using GameFramework.Event; +using GameFramework.Resource; +using System.Collections.Generic; +using GeometryTD.CustomUtility; +using GeometryTD.DataTable; +using GeometryTD.Definition; +using TMPro; +using GeometryTD.UI; +using UnityEngine; +using UnityGameFramework.Runtime; +using ProcedureOwner = GameFramework.Fsm.IFsm; + +namespace GeometryTD.Procedure +{ + public class ProcedurePreload : ProcedureBase + { + public static readonly string[] DataTableNames = new string[] + { + "BaseComp", + "BearingComp", + "Enemy", + "Entity", + "Event", + "Level", + "LevelPhase", + "LevelSpawnEntry", + "Music", + "MuzzleComp", + "Scene", + "ShopPrice", + "Sound", + "Tag", + "RarityTagBudget", + "TagConfig", + "OutGameDropPool", + "UIForm", + "UISound", + }; + + private readonly Dictionary _loadedFlag = new(); + + public override bool UseNativeDialog => true; + + protected override void OnEnter(ProcedureOwner procedureOwner) + { + base.OnEnter(procedureOwner); + + GameEntry.Event.Subscribe(LoadConfigSuccessEventArgs.EventId, OnLoadConfigSuccess); + GameEntry.Event.Subscribe(LoadConfigFailureEventArgs.EventId, OnLoadConfigFailure); + GameEntry.Event.Subscribe(LoadDataTableSuccessEventArgs.EventId, OnLoadDataTableSuccess); + GameEntry.Event.Subscribe(LoadDataTableFailureEventArgs.EventId, OnLoadDataTableFailure); + GameEntry.Event.Subscribe(LoadDictionarySuccessEventArgs.EventId, OnLoadDictionarySuccess); + GameEntry.Event.Subscribe(LoadDictionaryFailureEventArgs.EventId, OnLoadDictionaryFailure); + + _loadedFlag.Clear(); + + PreloadResources(); + } + + protected override void OnLeave(ProcedureOwner procedureOwner, bool isShutdown) + { + GameEntry.Event.Unsubscribe(LoadConfigSuccessEventArgs.EventId, OnLoadConfigSuccess); + GameEntry.Event.Unsubscribe(LoadConfigFailureEventArgs.EventId, OnLoadConfigFailure); + GameEntry.Event.Unsubscribe(LoadDataTableSuccessEventArgs.EventId, OnLoadDataTableSuccess); + GameEntry.Event.Unsubscribe(LoadDataTableFailureEventArgs.EventId, OnLoadDataTableFailure); + GameEntry.Event.Unsubscribe(LoadDictionarySuccessEventArgs.EventId, OnLoadDictionarySuccess); + GameEntry.Event.Unsubscribe(LoadDictionaryFailureEventArgs.EventId, OnLoadDictionaryFailure); + + base.OnLeave(procedureOwner, isShutdown); + } + + protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds) + { + base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds); + + foreach (KeyValuePair loadedFlag in _loadedFlag) + { + if (!loadedFlag.Value) + { + return; + } + } + + procedureOwner.SetData("NextSceneId", (int)SceneType.Menu); + ChangeState(procedureOwner); + } + + private void PreloadResources() + { + // Preload configs + LoadConfig("DefaultConfig"); + + // Preload data tables + foreach (string dataTableName in DataTableNames) + { + LoadDataTable(dataTableName); + } + + // Preload dictionaries + //LoadDictionary("Default"); + + // Preload fonts + LoadFont("MainFont"); + LoadTMPFont("MainTMPFont"); + } + + private void LoadConfig(string configName) + { + string configAssetName = AssetUtility.GetConfigAsset(configName, false); + _loadedFlag.Add(configAssetName, false); + GameEntry.Config.ReadData(configAssetName, this); + } + + private void LoadDataTable(string dataTableName) + { + string dataTableAssetName = AssetUtility.GetDataTableAsset(dataTableName, false); + _loadedFlag.Add(dataTableAssetName, false); + GameEntry.DataTable.LoadDataTable(dataTableName, dataTableAssetName, this); + } + + private void LoadDictionary(string dictionaryName) + { + string dictionaryAssetName = AssetUtility.GetDictionaryAsset(dictionaryName, false); + _loadedFlag.Add(dictionaryAssetName, false); + GameEntry.Localization.ReadData(dictionaryAssetName, this); + } + + private void LoadFont(string fontName) + { + _loadedFlag.Add(Utility.Text.Format("Font.{0}", fontName), false); + GameEntry.Resource.LoadAsset(AssetUtility.GetFontAsset(fontName), Constant.AssetPriority.FontAsset, + new LoadAssetCallbacks( + (assetName, asset, duration, userData) => + { + _loadedFlag[Utility.Text.Format("Font.{0}", fontName)] = true; + UGuiForm.SetMainFont((Font)asset); + Log.Info("Load font '{0}' OK.", fontName); + }, + (assetName, status, errorMessage, userData) => + { + Log.Error("Can not load font '{0}' from '{1}' with error message '{2}'.", fontName, assetName, + errorMessage); + })); + } + + private void LoadTMPFont(string fontName) + { + _loadedFlag.Add(Utility.Text.Format("Font.{0}", fontName), false); + GameEntry.Resource.LoadAsset(AssetUtility.GetTMPFontAsset(fontName), Constant.AssetPriority.FontAsset, + new LoadAssetCallbacks( + (assetName, asset, duration, userData) => + { + _loadedFlag[Utility.Text.Format("Font.{0}", fontName)] = true; + UGuiForm.SetMainTMPFont((TMP_FontAsset)asset); + Log.Info("Load font '{0}' OK.", fontName); + }, + (assetName, status, errorMessage, userData) => + { + Log.Error("Can not load font '{0}' from '{1}' with error message '{2}'.", fontName, assetName, + errorMessage); + })); + } + + private void OnLoadConfigSuccess(object sender, GameEventArgs e) + { + LoadConfigSuccessEventArgs ne = (LoadConfigSuccessEventArgs)e; + if (ne.UserData != this) + { + return; + } + + _loadedFlag[ne.ConfigAssetName] = true; + Log.Info("Load config '{0}' OK.", ne.ConfigAssetName); + } + + private void OnLoadConfigFailure(object sender, GameEventArgs e) + { + LoadConfigFailureEventArgs ne = (LoadConfigFailureEventArgs)e; + if (ne.UserData != this) + { + return; + } + + Log.Error("Can not load config '{0}' from '{1}' with error message '{2}'.", ne.ConfigAssetName, + ne.ConfigAssetName, ne.ErrorMessage); + } + + private void OnLoadDataTableSuccess(object sender, GameEventArgs e) + { + LoadDataTableSuccessEventArgs ne = (LoadDataTableSuccessEventArgs)e; + if (ne.UserData != this) + { + return; + } + + _loadedFlag[ne.DataTableAssetName] = true; + Log.Info("Load data table '{0}' OK.", ne.DataTableAssetName); + } + + private void OnLoadDataTableFailure(object sender, GameEventArgs e) + { + LoadDataTableFailureEventArgs ne = (LoadDataTableFailureEventArgs)e; + if (ne.UserData != this) + { + return; + } + + Log.Error("Can not load data table '{0}' from '{1}' with error message '{2}'.", ne.DataTableAssetName, + ne.DataTableAssetName, ne.ErrorMessage); + } + + private void OnLoadDictionarySuccess(object sender, GameEventArgs e) + { + LoadDictionarySuccessEventArgs ne = (LoadDictionarySuccessEventArgs)e; + if (ne.UserData != this) + { + return; + } + + _loadedFlag[ne.DictionaryAssetName] = true; + Log.Info("Load dictionary '{0}' OK.", ne.DictionaryAssetName); + } + + private void OnLoadDictionaryFailure(object sender, GameEventArgs e) + { + LoadDictionaryFailureEventArgs ne = (LoadDictionaryFailureEventArgs)e; + if (ne.UserData != this) + { + return; + } + + Log.Error("Can not load dictionary '{0}' from '{1}' with error message '{2}'.", ne.DictionaryAssetName, + ne.DictionaryAssetName, ne.ErrorMessage); + } + } +} diff --git a/src-ref/Procedure/Base/ProcedureSplash.cs b/src-ref/Procedure/Base/ProcedureSplash.cs new file mode 100644 index 0000000..4adb376 --- /dev/null +++ b/src-ref/Procedure/Base/ProcedureSplash.cs @@ -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.Resource; +using UnityGameFramework.Runtime; +using ProcedureOwner = GameFramework.Fsm.IFsm; + +namespace GeometryTD.Procedure +{ + public class ProcedureSplash : ProcedureBase + { + public override bool UseNativeDialog => true; + + protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds) + { + base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds); + + // TODO: 这里可以播放一个 Splash 动画 + // ... + + if (GameEntry.Base.EditorResourceMode) + { + // 编辑器模式 + Log.Info("Editor resource mode detected."); + ChangeState(procedureOwner); + } + else if (GameEntry.Resource.ResourceMode == ResourceMode.Package) + { + // 单机模式 + Log.Info("Package resource mode detected."); + ChangeState(procedureOwner); + } + else + { + // 可更新模式 + Log.Info("Updatable resource mode detected."); + ChangeState(procedureOwner); + } + } + } +} diff --git a/src-ref/Procedure/Base/ProcedureUpdateResources.cs b/src-ref/Procedure/Base/ProcedureUpdateResources.cs new file mode 100644 index 0000000..78d0692 --- /dev/null +++ b/src-ref/Procedure/Base/ProcedureUpdateResources.cs @@ -0,0 +1,270 @@ +using GameFramework; +using GameFramework.Event; +using System.Collections.Generic; +using GeometryTD; +using GeometryTD.UI; +using UnityEngine; +using UnityGameFramework.Runtime; +using ProcedureOwner = GameFramework.Fsm.IFsm; + +namespace GeometryTD.Procedure +{ + public class ProcedureUpdateResources : ProcedureBase + { + private bool m_UpdateResourcesComplete = false; + private int m_UpdateCount = 0; + private long m_UpdateTotalCompressedLength = 0L; + private int m_UpdateSuccessCount = 0; + private List m_UpdateLengthData = new List(); + private UpdateResourceForm m_UpdateResourceForm = null; + + public override bool UseNativeDialog + { + get + { + return true; + } + } + + protected override void OnEnter(ProcedureOwner procedureOwner) + { + base.OnEnter(procedureOwner); + + m_UpdateResourcesComplete = false; + m_UpdateCount = procedureOwner.GetData("UpdateResourceCount"); + procedureOwner.RemoveData("UpdateResourceCount"); + m_UpdateTotalCompressedLength = procedureOwner.GetData("UpdateResourceTotalCompressedLength"); + procedureOwner.RemoveData("UpdateResourceTotalCompressedLength"); + m_UpdateSuccessCount = 0; + m_UpdateLengthData.Clear(); + m_UpdateResourceForm = null; + + GameEntry.Event.Subscribe(ResourceUpdateStartEventArgs.EventId, OnResourceUpdateStart); + GameEntry.Event.Subscribe(ResourceUpdateChangedEventArgs.EventId, OnResourceUpdateChanged); + GameEntry.Event.Subscribe(ResourceUpdateSuccessEventArgs.EventId, OnResourceUpdateSuccess); + GameEntry.Event.Subscribe(ResourceUpdateFailureEventArgs.EventId, OnResourceUpdateFailure); + + if (Application.internetReachability == NetworkReachability.ReachableViaCarrierDataNetwork) + { + GameEntry.UI.OpenDialog(new DialogParams + { + Mode = 2, + Title = GameEntry.Localization.GetString("UpdateResourceViaCarrierDataNetwork.Title"), + Message = GameEntry.Localization.GetString("UpdateResourceViaCarrierDataNetwork.Message"), + ConfirmText = GameEntry.Localization.GetString("UpdateResourceViaCarrierDataNetwork.UpdateButton"), + OnClickConfirm = StartUpdateResources, + CancelText = GameEntry.Localization.GetString("UpdateResourceViaCarrierDataNetwork.QuitButton"), + OnClickCancel = delegate (object userData) { UnityGameFramework.Runtime.GameEntry.Shutdown(ShutdownType.Quit); }, + }); + + return; + } + + StartUpdateResources(null); + } + + protected override void OnLeave(ProcedureOwner procedureOwner, bool isShutdown) + { + if (m_UpdateResourceForm != null) + { + Object.Destroy(m_UpdateResourceForm.gameObject); + m_UpdateResourceForm = null; + } + + GameEntry.Event.Unsubscribe(ResourceUpdateStartEventArgs.EventId, OnResourceUpdateStart); + GameEntry.Event.Unsubscribe(ResourceUpdateChangedEventArgs.EventId, OnResourceUpdateChanged); + GameEntry.Event.Unsubscribe(ResourceUpdateSuccessEventArgs.EventId, OnResourceUpdateSuccess); + GameEntry.Event.Unsubscribe(ResourceUpdateFailureEventArgs.EventId, OnResourceUpdateFailure); + + base.OnLeave(procedureOwner, isShutdown); + } + + protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds) + { + base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds); + + if (!m_UpdateResourcesComplete) + { + return; + } + + ChangeState(procedureOwner); + } + + private void StartUpdateResources(object userData) + { + if (m_UpdateResourceForm == null) + { + m_UpdateResourceForm = Object.Instantiate(GameEntry.BuiltinData.UpdateResourceFormTemplate); + } + + Log.Info("Start update resources..."); + GameEntry.Resource.UpdateResources(OnUpdateResourcesComplete); + } + + private void RefreshProgress() + { + long currentTotalUpdateLength = 0L; + for (int i = 0; i < m_UpdateLengthData.Count; i++) + { + currentTotalUpdateLength += m_UpdateLengthData[i].Length; + } + + float progressTotal = (float)currentTotalUpdateLength / m_UpdateTotalCompressedLength; + string descriptionText = GameEntry.Localization.GetString("UpdateResource.Tips", m_UpdateSuccessCount.ToString(), m_UpdateCount.ToString(), GetByteLengthString(currentTotalUpdateLength), GetByteLengthString(m_UpdateTotalCompressedLength), progressTotal, GetByteLengthString((int)GameEntry.Download.CurrentSpeed)); + m_UpdateResourceForm.SetProgress(progressTotal, descriptionText); + } + + private string GetByteLengthString(long byteLength) + { + if (byteLength < 1024L) // 2 ^ 10 + { + return Utility.Text.Format("{0} Bytes", byteLength); + } + + if (byteLength < 1048576L) // 2 ^ 20 + { + return Utility.Text.Format("{0:F2} KB", byteLength / 1024f); + } + + if (byteLength < 1073741824L) // 2 ^ 30 + { + return Utility.Text.Format("{0:F2} MB", byteLength / 1048576f); + } + + if (byteLength < 1099511627776L) // 2 ^ 40 + { + return Utility.Text.Format("{0:F2} GB", byteLength / 1073741824f); + } + + if (byteLength < 1125899906842624L) // 2 ^ 50 + { + return Utility.Text.Format("{0:F2} TB", byteLength / 1099511627776f); + } + + if (byteLength < 1152921504606846976L) // 2 ^ 60 + { + return Utility.Text.Format("{0:F2} PB", byteLength / 1125899906842624f); + } + + return Utility.Text.Format("{0:F2} EB", byteLength / 1152921504606846976f); + } + + private void OnUpdateResourcesComplete(GameFramework.Resource.IResourceGroup resourceGroup, bool result) + { + if (result) + { + m_UpdateResourcesComplete = true; + Log.Info("Update resources complete with no errors."); + } + else + { + Log.Error("Update resources complete with errors."); + } + } + + private void OnResourceUpdateStart(object sender, GameEventArgs e) + { + ResourceUpdateStartEventArgs ne = (ResourceUpdateStartEventArgs)e; + + for (int i = 0; i < m_UpdateLengthData.Count; i++) + { + if (m_UpdateLengthData[i].Name == ne.Name) + { + Log.Warning("Update resource '{0}' is invalid.", ne.Name); + m_UpdateLengthData[i].Length = 0; + RefreshProgress(); + return; + } + } + + m_UpdateLengthData.Add(new UpdateLengthData(ne.Name)); + } + + private void OnResourceUpdateChanged(object sender, GameEventArgs e) + { + ResourceUpdateChangedEventArgs ne = (ResourceUpdateChangedEventArgs)e; + + for (int i = 0; i < m_UpdateLengthData.Count; i++) + { + if (m_UpdateLengthData[i].Name == ne.Name) + { + m_UpdateLengthData[i].Length = ne.CurrentLength; + RefreshProgress(); + return; + } + } + + Log.Warning("Update resource '{0}' is invalid.", ne.Name); + } + + private void OnResourceUpdateSuccess(object sender, GameEventArgs e) + { + ResourceUpdateSuccessEventArgs ne = (ResourceUpdateSuccessEventArgs)e; + Log.Info("Update resource '{0}' success.", ne.Name); + + for (int i = 0; i < m_UpdateLengthData.Count; i++) + { + if (m_UpdateLengthData[i].Name == ne.Name) + { + m_UpdateLengthData[i].Length = ne.CompressedLength; + m_UpdateSuccessCount++; + RefreshProgress(); + return; + } + } + + Log.Warning("Update resource '{0}' is invalid.", ne.Name); + } + + private void OnResourceUpdateFailure(object sender, GameEventArgs e) + { + ResourceUpdateFailureEventArgs ne = (ResourceUpdateFailureEventArgs)e; + if (ne.RetryCount >= ne.TotalRetryCount) + { + Log.Error("Update resource '{0}' failure from '{1}' with error message '{2}', retry count '{3}'.", ne.Name, ne.DownloadUri, ne.ErrorMessage, ne.RetryCount.ToString()); + return; + } + else + { + Log.Info("Update resource '{0}' failure from '{1}' with error message '{2}', retry count '{3}'.", ne.Name, ne.DownloadUri, ne.ErrorMessage, ne.RetryCount.ToString()); + } + + for (int i = 0; i < m_UpdateLengthData.Count; i++) + { + if (m_UpdateLengthData[i].Name == ne.Name) + { + m_UpdateLengthData.Remove(m_UpdateLengthData[i]); + RefreshProgress(); + return; + } + } + + Log.Warning("Update resource '{0}' is invalid.", ne.Name); + } + + private class UpdateLengthData + { + private readonly string m_Name; + + public UpdateLengthData(string name) + { + m_Name = name; + } + + public string Name + { + get + { + return m_Name; + } + } + + public int Length + { + get; + set; + } + } + } +} diff --git a/src-ref/Procedure/Base/ProcedureUpdateVersion.cs b/src-ref/Procedure/Base/ProcedureUpdateVersion.cs new file mode 100644 index 0000000..8c2a393 --- /dev/null +++ b/src-ref/Procedure/Base/ProcedureUpdateVersion.cs @@ -0,0 +1,70 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using GameFramework.Resource; +using UnityGameFramework.Runtime; +using ProcedureOwner = GameFramework.Fsm.IFsm; + +namespace GeometryTD.Procedure +{ + public class ProcedureUpdateVersion : ProcedureBase + { + private bool m_UpdateVersionComplete = false; + private UpdateVersionListCallbacks m_UpdateVersionListCallbacks = null; + + public override bool UseNativeDialog + { + get + { + return true; + } + } + + protected override void OnInit(ProcedureOwner procedureOwner) + { + base.OnInit(procedureOwner); + + m_UpdateVersionListCallbacks = new UpdateVersionListCallbacks(OnUpdateVersionListSuccess, OnUpdateVersionListFailure); + } + + protected override void OnEnter(ProcedureOwner procedureOwner) + { + base.OnEnter(procedureOwner); + + m_UpdateVersionComplete = false; + + GameEntry.Resource.UpdateVersionList(procedureOwner.GetData("VersionListLength"), procedureOwner.GetData("VersionListHashCode"), procedureOwner.GetData("VersionListCompressedLength"), procedureOwner.GetData("VersionListCompressedHashCode"), m_UpdateVersionListCallbacks); + procedureOwner.RemoveData("VersionListLength"); + procedureOwner.RemoveData("VersionListHashCode"); + procedureOwner.RemoveData("VersionListCompressedLength"); + procedureOwner.RemoveData("VersionListCompressedHashCode"); + } + + protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds) + { + base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds); + + if (!m_UpdateVersionComplete) + { + return; + } + + ChangeState(procedureOwner); + } + + private void OnUpdateVersionListSuccess(string downloadPath, string downloadUri) + { + m_UpdateVersionComplete = true; + Log.Info("Update version list from '{0}' success.", downloadUri); + } + + private void OnUpdateVersionListFailure(string downloadUri, string errorMessage) + { + Log.Warning("Update version list from '{0}' failure, error message is '{1}'.", downloadUri, errorMessage); + } + } +} diff --git a/src-ref/Procedure/Base/ProcedureVerifyResources.cs b/src-ref/Procedure/Base/ProcedureVerifyResources.cs new file mode 100644 index 0000000..0618c0d --- /dev/null +++ b/src-ref/Procedure/Base/ProcedureVerifyResources.cs @@ -0,0 +1,77 @@ +using GameFramework.Event; +using GeometryTD; +using UnityGameFramework.Runtime; +using ProcedureOwner = GameFramework.Fsm.IFsm; + +namespace GeometryTD.Procedure +{ + public class ProcedureVerifyResources : ProcedureBase + { + private bool m_VerifyResourcesComplete = false; + + public override bool UseNativeDialog + { + get + { + return true; + } + } + + protected override void OnEnter(ProcedureOwner procedureOwner) + { + base.OnEnter(procedureOwner); + + GameEntry.Event.Subscribe(ResourceVerifyStartEventArgs.EventId, OnResourceVerifyStart); + GameEntry.Event.Subscribe(ResourceVerifySuccessEventArgs.EventId, OnResourceVerifySuccess); + GameEntry.Event.Subscribe(ResourceVerifyFailureEventArgs.EventId, OnResourceVerifyFailure); + + m_VerifyResourcesComplete = false; + GameEntry.Resource.VerifyResources(OnVerifyResourcesComplete); + } + + protected override void OnLeave(ProcedureOwner procedureOwner, bool isShutdown) + { + base.OnLeave(procedureOwner, isShutdown); + + GameEntry.Event.Unsubscribe(ResourceVerifyStartEventArgs.EventId, OnResourceVerifyStart); + GameEntry.Event.Unsubscribe(ResourceVerifySuccessEventArgs.EventId, OnResourceVerifySuccess); + GameEntry.Event.Unsubscribe(ResourceVerifyFailureEventArgs.EventId, OnResourceVerifyFailure); + } + + protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds) + { + base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds); + + if (!m_VerifyResourcesComplete) + { + return; + } + + ChangeState(procedureOwner); + } + + private void OnVerifyResourcesComplete(bool result) + { + m_VerifyResourcesComplete = true; + Log.Info("Verify resources complete, result is '{0}'.", result); + } + + private void OnResourceVerifyStart(object sender, GameEventArgs e) + { + ResourceVerifyStartEventArgs ne = (ResourceVerifyStartEventArgs)e; + Log.Info("Start verify resources, verify resource count '{0}', verify resource total length '{1}'.", ne.Count, ne.TotalLength); + } + + private void OnResourceVerifySuccess(object sender, GameEventArgs e) + { + ResourceVerifySuccessEventArgs ne = (ResourceVerifySuccessEventArgs)e; + Log.Info("Verify resource '{0}' success.", ne.Name); + } + + private void OnResourceVerifyFailure(object sender, GameEventArgs e) + { + ResourceVerifyFailureEventArgs ne = (ResourceVerifyFailureEventArgs)e; + Log.Warning("Verify resource '{0}' failure.", ne.Name); + } + } +} diff --git a/src-ref/Procedure/ProcedureMain/FixedRunNodeSequenceBuilder.cs b/src-ref/Procedure/ProcedureMain/FixedRunNodeSequenceBuilder.cs new file mode 100644 index 0000000..e17c66e --- /dev/null +++ b/src-ref/Procedure/ProcedureMain/FixedRunNodeSequenceBuilder.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using GeometryTD.Definition; + +namespace GeometryTD.Procedure +{ + public static class FixedRunNodeSequenceBuilder + { + private const int FixedNodeCount = 10; + private static readonly int[] PlainCombatLevelCycle = { 1, 2, 3, 1 }; + + public static IReadOnlyList Build(LevelThemeType themeType) + { + if (themeType != LevelThemeType.Plain) + { + return new List(); + } + + List nodeSeeds = new List(FixedNodeCount) + { + CreateNodeSeed(1, 0, RunNodeType.Combat, themeType, PlainCombatLevelCycle[0]), + CreateNodeSeed(2, 1, RunNodeType.Event, themeType, 0), + CreateNodeSeed(3, 2, RunNodeType.Combat, themeType, PlainCombatLevelCycle[1]), + CreateNodeSeed(4, 3, RunNodeType.Shop, themeType, 0), + CreateNodeSeed(5, 4, RunNodeType.Combat, themeType, PlainCombatLevelCycle[2]), + CreateNodeSeed(6, 5, RunNodeType.Event, themeType, 0), + CreateNodeSeed(7, 6, RunNodeType.Combat, themeType, PlainCombatLevelCycle[3]), + CreateNodeSeed(8, 7, RunNodeType.Shop, themeType, 0), + CreateNodeSeed(9, 8, RunNodeType.Event, themeType, 0), + CreateNodeSeed(10, 9, RunNodeType.BossCombat, themeType, 4) + }; + + return nodeSeeds; + } + + private static RunNodeSeed CreateNodeSeed( + int nodeId, + int sequenceIndex, + RunNodeType nodeType, + LevelThemeType themeType, + int linkedLevelId) + { + return new RunNodeSeed + { + NodeId = nodeId, + SequenceIndex = sequenceIndex, + NodeType = nodeType, + ThemeType = themeType, + LinkedLevelId = linkedLevelId + }; + } + } +} \ No newline at end of file diff --git a/src-ref/Procedure/ProcedureMain/ProcedureMain.cs b/src-ref/Procedure/ProcedureMain/ProcedureMain.cs new file mode 100644 index 0000000..8f81304 --- /dev/null +++ b/src-ref/Procedure/ProcedureMain/ProcedureMain.cs @@ -0,0 +1,468 @@ +using GameFramework.Event; +using GameFramework.Fsm; +using GameFramework.Procedure; +using System; +using GeometryTD.CustomEvent; +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using GeometryTD.Factory; +using GeometryTD.UI; +using UnityGameFramework.Runtime; + +namespace GeometryTD.Procedure +{ + public class ProcedureMain : ProcedureBase + { + private const int MaxParticipantTowerCount = 4; + + public override bool UseNativeDialog => false; + + private RepoFormUseCase _repoFormUseCase; + private NodeMapFormUseCase _nodeMapFormUseCase; + private RunState _currentRunState; + private ProcedureMainFlowPhase _flowPhase = ProcedureMainFlowPhase.Hub; + private bool _isRunCompleteDialogShown; + private bool _isReturnToMenuPending; + + #region FSM + + protected override void OnEnter(IFsm procedureOwner) + { + base.OnEnter(procedureOwner); + + GameEntry.Event.Subscribe(NodeCompleteEventArgs.EventId, OnNodeComplete); + GameEntry.Event.Subscribe(NodeEnterEventArgs.EventId, OnNodeEnter); + GameEntry.Event.Subscribe(NodeMapNodeEnterRequestedEventArgs.EventId, OnNodeMapNodeEnterRequested); + + GameEntry.TagRegistry.OnInit(); + GameEntry.EventNode.OnInit(); + GameEntry.CombatNode.OnInit(LevelThemeType.Plain); + GameEntry.ShopNode.OnInit(); + + string runId = Guid.NewGuid().ToString("N"); + int runSeed = RunStateFactory.CreateRunSeed(); + BackpackInventoryData initialInventory = InventorySeedUtility.CreateSampleInventory(runSeed); + GameEntry.PlayerInventory?.OnInit(initialInventory); + + _currentRunState = RunStateFactory.CreateFixedRun( + LevelThemeType.Plain, + GameEntry.PlayerInventory != null + ? GameEntry.PlayerInventory.GetInventorySnapshot() + : initialInventory, + runId, + runSeed); + + _repoFormUseCase = new RepoFormUseCase(); + GameEntry.UIRouter.BindUIUseCase(UIFormType.RepoForm, _repoFormUseCase); + + _nodeMapFormUseCase = new NodeMapFormUseCase(); + _nodeMapFormUseCase.SetRunState(_currentRunState); + GameEntry.UIRouter.BindUIUseCase(UIFormType.NodeMapForm, _nodeMapFormUseCase); + _isRunCompleteDialogShown = false; + _isReturnToMenuPending = false; + + EnterHubFlow(); + } + + protected override void OnUpdate(IFsm procedureOwner, float elapseSeconds, + float realElapseSeconds) + { + base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds); + + if (_isReturnToMenuPending) + { + _isReturnToMenuPending = false; + procedureOwner.SetData("NextSceneId", (int)SceneType.Menu); + ChangeState(procedureOwner); + return; + } + + GameEntry.CombatNode.OnUpdate(elapseSeconds, realElapseSeconds); + } + + protected override void OnLeave(IFsm procedureOwner, bool isShutdown) + { + GameEntry.CombatNode.OnShutdown(); + GameEntry.Event.Unsubscribe(NodeMapNodeEnterRequestedEventArgs.EventId, OnNodeMapNodeEnterRequested); + GameEntry.Event.Unsubscribe(NodeEnterEventArgs.EventId, OnNodeEnter); + GameEntry.Event.Unsubscribe(NodeCompleteEventArgs.EventId, OnNodeComplete); + GameEntry.UIRouter.CloseUI(UIFormType.RepoForm); + GameEntry.UIRouter.CloseUI(UIFormType.NodeMapForm); + GameEntry.UIRouter.CloseUI(UIFormType.MainForm); + GameEntry.UIRouter.CloseUI(UIFormType.DialogForm); + _repoFormUseCase = null; + _nodeMapFormUseCase = null; + _currentRunState = null; + _flowPhase = ProcedureMainFlowPhase.Hub; + _isRunCompleteDialogShown = false; + _isReturnToMenuPending = false; + + base.OnLeave(procedureOwner, isShutdown); + } + + #endregion + + + private void OnNodeEnter(object sender, GameEventArgs e) + { + if (!(e is NodeEnterEventArgs args)) + { + return; + } + + if (!string.IsNullOrWhiteSpace(args.RunId) && + _currentRunState != null && + !string.Equals(args.RunId, _currentRunState.RunId)) + { + Log.Warning( + "ProcedureMain.OnNodeEnter() ignored. EventRunId={0}, CurrentRunId={1}.", + args.RunId, + _currentRunState.RunId); + return; + } + + RunNodeState currentNode = _currentRunState?.CurrentNode; + if (!ProcedureMainNodeEventGuardService.MatchesCurrentNode( + _currentRunState, + args.NodeId, + args.NodeType, + args.SequenceIndex)) + { + Log.Warning( + "ProcedureMain.OnNodeEnter() node mismatch. EventNodeId={0}, EventNodeType={1}, EventSequenceIndex={2}; CurrentNodeId={3}, CurrentNodeType={4}, CurrentSequenceIndex={5}.", + args.NodeId, + args.NodeType, + args.SequenceIndex, + currentNode?.NodeId ?? 0, + currentNode?.NodeType ?? RunNodeType.None, + currentNode?.SequenceIndex ?? -1); + return; + } + + Log.Info( + "ProcedureMain.OnNodeEnter() accepted. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}.", + string.IsNullOrWhiteSpace(args.RunId) ? _currentRunState?.RunId : args.RunId, + args.NodeId, + args.NodeType, + args.SequenceIndex); + + EnterNodeFlow(); + } + + private void OnNodeComplete(object sender, GameEventArgs e) + { + if (!(e is NodeCompleteEventArgs args)) + { + return; + } + + if (!string.IsNullOrWhiteSpace(args.RunId) && + _currentRunState != null && + !string.Equals(args.RunId, _currentRunState.RunId)) + { + Log.Warning( + "ProcedureMain.OnNodeComplete() ignored. EventRunId={0}, CurrentRunId={1}.", + args.RunId, + _currentRunState.RunId); + return; + } + + RunNodeState currentNode = _currentRunState?.CurrentNode; + if (!ProcedureMainNodeEventGuardService.MatchesCurrentNode( + _currentRunState, + args.NodeId, + args.NodeType, + args.SequenceIndex)) + { + Log.Warning( + "ProcedureMain.OnNodeComplete() node mismatch. EventNodeId={0}, EventNodeType={1}, EventSequenceIndex={2}; CurrentNodeId={3}, CurrentNodeType={4}, CurrentSequenceIndex={5}.", + args.NodeId, + args.NodeType, + args.SequenceIndex, + currentNode?.NodeId ?? 0, + currentNode?.NodeType ?? RunNodeType.None, + currentNode?.SequenceIndex ?? -1); + return; + } + + Log.Info( + "ProcedureMain.OnNodeComplete() accepted. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}, CompletionStatus={4}, CombatWon={5}.", + string.IsNullOrWhiteSpace(args.RunId) ? _currentRunState?.RunId : args.RunId, + args.NodeId, + args.NodeType, + args.SequenceIndex, + args.CompletionStatus, + args.CombatWon); + + RunNodeCompletionSnapshot completionSnapshot = args.CompletionSnapshot != null + ? args.CompletionSnapshot.Clone() + : _currentRunState?.CreateCurrentNodeContext()?.CreateCompletionSnapshot(null); + BackpackInventoryData inventorySnapshot = completionSnapshot?.InventorySnapshot; + if (inventorySnapshot == null && GameEntry.PlayerInventory != null) + { + inventorySnapshot = GameEntry.PlayerInventory.GetInventorySnapshot(); + } + + if (completionSnapshot != null) + { + completionSnapshot.InventorySnapshot = inventorySnapshot; + } + + ProcedureMainParticipantTowerCleanupResult cleanupResult = + ProcedureMainParticipantTowerCleanupService.RemoveBrokenParticipantTowers( + inventorySnapshot, + MaxParticipantTowerCount); + if (cleanupResult.HasAnyRemovedTower && GameEntry.PlayerInventory != null) + { + GameEntry.PlayerInventory.ReplaceInventorySnapshot(inventorySnapshot); + } + + if (completionSnapshot != null) + { + completionSnapshot.InventorySnapshot = inventorySnapshot; + } + + ProcedureMainRunAdvanceResult advanceResult = + ProcedureMainRunFlowService.TryAdvanceRun(_currentRunState, args.CompletionStatus, completionSnapshot); + HandleRunAdvanceResult(advanceResult); + if (cleanupResult.HasAnyRemovedTower && + advanceResult != ProcedureMainRunAdvanceResult.RunCompleted) + { + OpenRemovedParticipantTowerDialog(cleanupResult); + } + } + + private void OnNodeMapNodeEnterRequested(object sender, GameEventArgs e) + { + if (!(sender is NodeMapForm) || !(e is NodeMapNodeEnterRequestedEventArgs args)) + { + return; + } + + RunNodeState currentNode = _currentRunState?.CurrentNode; + if (_currentRunState == null || + _flowPhase != ProcedureMainFlowPhase.Hub || + !_currentRunState.CanEnterCurrentNode || + currentNode == null) + { + Log.Warning( + "ProcedureMain.OnNodeMapNodeEnterRequested() ignored. FlowPhase={0}, HasEnterableNode={1}.", + _flowPhase, + _currentRunState != null && _currentRunState.CanEnterCurrentNode); + return; + } + + if (args.SequenceIndex != currentNode.SequenceIndex || args.NodeType != currentNode.NodeType) + { + Log.Warning( + "ProcedureMain.OnNodeMapNodeEnterRequested() ignored. Requested={0}#{1}, CurrentNode={2}#{3}.", + args.NodeType, + args.SequenceIndex, + currentNode.NodeType, + currentNode.SequenceIndex); + return; + } + + switch (currentNode.NodeType) + { + case RunNodeType.Combat: + case RunNodeType.BossCombat: + ProcedureMainCombatEntryValidationResult validationResult = + ProcedureMainCombatEntryValidationService.Validate( + GameEntry.PlayerInventory != null + ? GameEntry.PlayerInventory.GetInventorySnapshot() + : null); + if (!validationResult.CanEnterCombat) + { + LogCombatEntryBlocked(currentNode, validationResult); + OpenBlockedCombatDialog(validationResult); + return; + } + + if (GameEntry.CombatNode.CurrentThemeType != currentNode.ThemeType) + { + GameEntry.CombatNode.OnInit(currentNode.ThemeType); + } + + GameEntry.CombatNode.StartCombat( + currentNode.LinkedLevelId, + _currentRunState.CreateCurrentNodeContext()); + return; + case RunNodeType.Event: + GameEntry.EventNode.StartEvent(_currentRunState.CreateCurrentNodeContext()); + return; + case RunNodeType.Shop: + GameEntry.ShopNode.StartShop(_currentRunState.CreateCurrentNodeContext()); + return; + default: + Log.Warning("ProcedureMain.OnNodeMapNodeEnterRequested() encountered unsupported node type: {0}.", + currentNode.NodeType); + return; + } + } + + private void OpenBlockedCombatDialog(ProcedureMainCombatEntryValidationResult validationResult) + { + GameEntry.UIRouter.CloseUI(UIFormType.DialogForm); + GameEntry.UIRouter.OpenUI( + UIFormType.DialogForm, + ProcedureMainCombatEntryValidationService.BuildBlockedCombatDialogRawData(validationResult)); + } + + private void LogCombatEntryBlocked( + RunNodeState currentNode, + ProcedureMainCombatEntryValidationResult validationResult) + { + switch (validationResult?.BlockReason) + { + case ProcedureMainCombatEntryBlockReason.InventoryUnavailable: + Log.Warning( + "ProcedureMain blocked combat start. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}, Reason={4}.", + _currentRunState?.RunId, + currentNode?.NodeId ?? 0, + currentNode?.NodeType ?? RunNodeType.None, + currentNode?.SequenceIndex ?? -1, + ProcedureMainCombatEntryBlockReason.InventoryUnavailable); + return; + case ProcedureMainCombatEntryBlockReason.NoValidParticipantTower: + Log.Warning( + "ProcedureMain blocked combat start. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}, Reason={4}, InvalidParticipantTowers={5}.", + _currentRunState?.RunId, + currentNode?.NodeId ?? 0, + currentNode?.NodeType ?? RunNodeType.None, + currentNode?.SequenceIndex ?? -1, + ProcedureMainCombatEntryBlockReason.NoValidParticipantTower, + ProcedureMainCombatEntryValidationService.BuildInvalidParticipantTowerLog( + validationResult.ValidationSummary)); + return; + default: + Log.Warning( + "ProcedureMain blocked combat start. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}, Reason=Unknown.", + _currentRunState?.RunId, + currentNode?.NodeId ?? 0, + currentNode?.NodeType ?? RunNodeType.None, + currentNode?.SequenceIndex ?? -1); + return; + } + } + + private void HandleRunAdvanceResult(ProcedureMainRunAdvanceResult result) + { + switch (result) + { + case ProcedureMainRunAdvanceResult.NoChange: + return; + case ProcedureMainRunAdvanceResult.NodeException: + Log.Info( + "ProcedureMain current node entered exception state. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}.", + _currentRunState?.RunId, + _currentRunState?.CurrentNode?.NodeId ?? 0, + _currentRunState?.CurrentNode?.NodeType ?? RunNodeType.None, + _currentRunState?.CurrentNode?.SequenceIndex ?? -1); + EnterHubFlow(); + return; + case ProcedureMainRunAdvanceResult.AdvancedToNextNode: + LogNextNode(); + EnterHubFlow(); + return; + case ProcedureMainRunAdvanceResult.RunCompleted: + EnterRunCompletedPendingFinish(); + return; + default: + return; + } + } + + private void EnterHubFlow() + { + _flowPhase = ProcedureMainFlowPhase.Hub; + _nodeMapFormUseCase?.SetRunState(_currentRunState); + GameEntry.UIRouter.OpenUI(UIFormType.NodeMapForm); + GameEntry.UIRouter.OpenUI(UIFormType.MainForm); + } + + private void EnterNodeFlow() + { + _flowPhase = ProcedureMainFlowPhase.NodeActive; + CloseHubUI(); + } + + private void EnterRunCompletedPendingFinish() + { + _flowPhase = ProcedureMainFlowPhase.RunCompletedPendingFinish; + _nodeMapFormUseCase?.SetRunState(_currentRunState); + CloseHubUI(); + Log.Info("ProcedureMain run completed. RunId={0}", _currentRunState?.RunId); + + ProcedureMainRunCompletionResult result = + ProcedureMainRunCompletionService.TryEnterCompletedPendingFinish(_isRunCompleteDialogShown); + if (result == ProcedureMainRunCompletionResult.ShowCompletionDialog) + { + _isRunCompleteDialogShown = true; + OpenRunCompleteDialog(); + } + } + + private void CloseHubUI() + { + GameEntry.UIRouter.CloseUI(UIFormType.NodeMapForm); + GameEntry.UIRouter.CloseUI(UIFormType.MainForm); + } + + private void LogNextNode() + { + RunNodeState nextNode = _currentRunState?.CurrentNode; + if (nextNode == null) + { + return; + } + + Log.Info( + "ProcedureMain advanced run. RunId={0}, NextNodeType={1}, SequenceIndex={2}, LevelId={3}.", + _currentRunState.RunId, + nextNode.NodeType, + nextNode.SequenceIndex, + nextNode.LinkedLevelId); + } + + private void OpenRunCompleteDialog() + { + GameEntry.UIRouter.OpenUI(UIFormType.DialogForm, new DialogFormRawData + { + Mode = 1, + Title = "Run Complete", + Message = "Boss node completed. This run is finished and will return to the main menu.", + PauseGame = false, + ConfirmText = "Return to Menu", + OnClickConfirm = OnRunCompleteDialogConfirmed + }); + } + + private void OpenRemovedParticipantTowerDialog(ProcedureMainParticipantTowerCleanupResult cleanupResult) + { + GameEntry.UIRouter.CloseUI(UIFormType.DialogForm); + GameEntry.UIRouter.OpenUI( + UIFormType.DialogForm, + ProcedureMainParticipantTowerCleanupService.BuildRemovedTowerDialogRawData(cleanupResult)); + } + + private void OnRunCompleteDialogConfirmed(object userData) + { + _ = userData; + + ProcedureMainRunCompletionResult result = ProcedureMainRunCompletionService.TryConfirmReturnToMenu( + _flowPhase, + _isReturnToMenuPending); + if (result != ProcedureMainRunCompletionResult.ReturnToMenu) + { + Log.Warning( + "ProcedureMain ignored run completion confirm. FlowPhase={0}, ReturnPending={1}.", + _flowPhase, + _isReturnToMenuPending); + return; + } + + _isReturnToMenuPending = true; + } + } +} diff --git a/src-ref/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationResult.cs b/src-ref/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationResult.cs new file mode 100644 index 0000000..6173e64 --- /dev/null +++ b/src-ref/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationResult.cs @@ -0,0 +1,13 @@ +using GeometryTD.Definition; + +namespace GeometryTD.Procedure +{ + public sealed class ProcedureMainCombatEntryValidationResult + { + public bool CanEnterCombat => BlockReason == ProcedureMainCombatEntryBlockReason.None; + + public ProcedureMainCombatEntryBlockReason BlockReason { get; set; } + + public CombatParticipantTowerValidationSummary ValidationSummary { get; set; } + } +} \ No newline at end of file diff --git a/src-ref/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationService.cs b/src-ref/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationService.cs new file mode 100644 index 0000000..c116223 --- /dev/null +++ b/src-ref/Procedure/ProcedureMain/ProcedureMainCombatEntryValidationService.cs @@ -0,0 +1,123 @@ +using System.Text; +using GeometryTD.Definition; +using GeometryTD.UI; + +namespace GeometryTD.Procedure +{ + public static class ProcedureMainCombatEntryValidationService + { + public static ProcedureMainCombatEntryValidationResult Validate(BackpackInventoryData inventory) + { + if (inventory == null) + { + return new ProcedureMainCombatEntryValidationResult + { + BlockReason = ProcedureMainCombatEntryBlockReason.InventoryUnavailable, + ValidationSummary = new CombatParticipantTowerValidationSummary() + }; + } + + CombatParticipantTowerValidationSummary summary = + CombatParticipantTowerValidationService.ValidateParticipantTowers(inventory); + + return new ProcedureMainCombatEntryValidationResult + { + BlockReason = summary.HasAnyValidParticipantTower + ? ProcedureMainCombatEntryBlockReason.None + : ProcedureMainCombatEntryBlockReason.NoValidParticipantTower, + ValidationSummary = summary + }; + } + + public static string BuildInvalidParticipantTowerLog( + CombatParticipantTowerValidationSummary summary) + { + if (summary?.InvalidResults == null || summary.InvalidResults.Count <= 0) + { + return "none"; + } + + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < summary.InvalidResults.Count; i++) + { + CombatParticipantTowerValidationResult result = summary.InvalidResults[i]; + if (result == null) + { + continue; + } + + if (builder.Length > 0) + { + builder.Append(", "); + } + + builder.Append('#'); + builder.Append(result.TowerInstanceId); + builder.Append(':'); + builder.Append(result.FailureReason); + } + + return builder.Length > 0 ? builder.ToString() : "none"; + } + + public static DialogFormRawData BuildBlockedCombatDialogRawData( + ProcedureMainCombatEntryValidationResult validationResult) + { + return new DialogFormRawData + { + Mode = 1, + Title = "无法进入战斗", + Message = BuildBlockedCombatDialogMessage(validationResult), + PauseGame = false, + ConfirmText = "知道了" + }; + } + + private static string BuildBlockedCombatDialogMessage( + ProcedureMainCombatEntryValidationResult validationResult) + { + if (validationResult == null) + { + return "当前无法确认出战信息,请稍后重试。"; + } + + switch (validationResult.BlockReason) + { + case ProcedureMainCombatEntryBlockReason.InventoryUnavailable: + return "当前无法读取库存快照,暂时不能进入战斗。请重新进入本轮流程后再试。"; + case ProcedureMainCombatEntryBlockReason.NoValidParticipantTower: + return BuildNoValidParticipantTowerMessage(validationResult.ValidationSummary); + default: + return "当前出战校验未通过,暂时不能进入战斗。"; + } + } + + private static string BuildNoValidParticipantTowerMessage( + CombatParticipantTowerValidationSummary summary) + { + if (summary?.InvalidResults == null || summary.InvalidResults.Count <= 0) + { + return "参战区至少需要 1 座完整装配了枪口、轴承、底座的塔,才能进入战斗。"; + } + + StringBuilder builder = new StringBuilder(); + builder.Append("参战区没有可出战的完整塔。"); + for (int i = 0; i < summary.InvalidResults.Count; i++) + { + CombatParticipantTowerValidationResult result = summary.InvalidResults[i]; + if (result == null) + { + continue; + } + + builder.Append('\n'); + builder.Append("塔 #"); + builder.Append(result.TowerInstanceId); + builder.Append(' '); + builder.Append(CombatParticipantTowerValidationText.GetFailureReasonMessage(result.FailureReason)); + } + + return builder.ToString(); + } + } +} diff --git a/src-ref/Procedure/ProcedureMain/ProcedureMainNodeEventGuardService.cs b/src-ref/Procedure/ProcedureMain/ProcedureMainNodeEventGuardService.cs new file mode 100644 index 0000000..a342c3a --- /dev/null +++ b/src-ref/Procedure/ProcedureMain/ProcedureMainNodeEventGuardService.cs @@ -0,0 +1,22 @@ +namespace GeometryTD.Procedure +{ + public static class ProcedureMainNodeEventGuardService + { + public static bool MatchesCurrentNode( + RunState runState, + int nodeId, + RunNodeType nodeType, + int sequenceIndex) + { + RunNodeState currentNode = runState?.CurrentNode; + if (currentNode == null) + { + return false; + } + + return (nodeId <= 0 || nodeId == currentNode.NodeId) && + (nodeType == RunNodeType.None || nodeType == currentNode.NodeType) && + (sequenceIndex < 0 || sequenceIndex == currentNode.SequenceIndex); + } + } +} \ No newline at end of file diff --git a/src-ref/Procedure/ProcedureMain/ProcedureMainParticipantTowerCleanupService.cs b/src-ref/Procedure/ProcedureMain/ProcedureMainParticipantTowerCleanupService.cs new file mode 100644 index 0000000..cf551f9 --- /dev/null +++ b/src-ref/Procedure/ProcedureMain/ProcedureMainParticipantTowerCleanupService.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using GeometryTD.UI; + +namespace GeometryTD.Procedure +{ + public sealed class ProcedureMainParticipantTowerCleanupResult + { + public List RemovedResults { get; } = new(); + + public bool HasAnyRemovedTower => RemovedResults.Count > 0; + } + + public static class ProcedureMainParticipantTowerCleanupService + { + public static ProcedureMainParticipantTowerCleanupResult RemoveBrokenParticipantTowers( + BackpackInventoryData inventory, + int maxParticipantCount) + { + ProcedureMainParticipantTowerCleanupResult result = new ProcedureMainParticipantTowerCleanupResult(); + if (inventory == null) + { + return result; + } + + CombatParticipantTowerValidationSummary summary = + CombatParticipantTowerValidationService.ValidateParticipantTowers(inventory); + if (summary.InvalidResults == null || summary.InvalidResults.Count <= 0) + { + return result; + } + + HashSet removedTowerIds = new HashSet(); + for (int i = 0; i < summary.InvalidResults.Count; i++) + { + CombatParticipantTowerValidationResult invalidResult = summary.InvalidResults[i]; + if (invalidResult == null || + invalidResult.TowerInstanceId <= 0 || + !IsBrokenFailureReason(invalidResult.FailureReason) || + !removedTowerIds.Add(invalidResult.TowerInstanceId)) + { + continue; + } + + if (InventoryParticipantUtility.TryRemoveParticipantTower( + inventory, + invalidResult.TowerInstanceId, + maxParticipantCount)) + { + result.RemovedResults.Add(invalidResult); + } + } + + return result; + } + + public static DialogFormRawData BuildRemovedTowerDialogRawData( + ProcedureMainParticipantTowerCleanupResult cleanupResult) + { + return new DialogFormRawData + { + Mode = 1, + Title = "出战塔已损坏", + Message = BuildRemovedTowerDialogMessage(cleanupResult), + PauseGame = false, + ConfirmText = "知道了" + }; + } + + private static string BuildRemovedTowerDialogMessage( + ProcedureMainParticipantTowerCleanupResult cleanupResult) + { + if (cleanupResult == null || !cleanupResult.HasAnyRemovedTower) + { + return "当前没有需要移出参战区的损坏防御塔。"; + } + + System.Text.StringBuilder builder = new System.Text.StringBuilder(); + builder.Append("以下防御塔已损坏,已自动移出参战区:"); + for (int i = 0; i < cleanupResult.RemovedResults.Count; i++) + { + CombatParticipantTowerValidationResult removedResult = cleanupResult.RemovedResults[i]; + if (removedResult == null) + { + continue; + } + + builder.Append('\n'); + builder.Append("塔 #"); + builder.Append(removedResult.TowerInstanceId); + builder.Append(' '); + builder.Append(CombatParticipantTowerValidationText.GetFailureReasonMessage(removedResult.FailureReason)); + } + + return builder.ToString(); + } + + private static bool IsBrokenFailureReason(CombatParticipantTowerValidationFailureReason failureReason) + { + return failureReason == CombatParticipantTowerValidationFailureReason.BrokenMuzzleComponent || + failureReason == CombatParticipantTowerValidationFailureReason.BrokenBearingComponent || + failureReason == CombatParticipantTowerValidationFailureReason.BrokenBaseComponent; + } + } +} diff --git a/src-ref/Procedure/ProcedureMain/ProcedureMainRunCompletionService.cs b/src-ref/Procedure/ProcedureMain/ProcedureMainRunCompletionService.cs new file mode 100644 index 0000000..8559562 --- /dev/null +++ b/src-ref/Procedure/ProcedureMain/ProcedureMainRunCompletionService.cs @@ -0,0 +1,26 @@ +using GeometryTD.Definition; + +namespace GeometryTD.Procedure +{ + public static class ProcedureMainRunCompletionService + { + public static ProcedureMainRunCompletionResult TryEnterCompletedPendingFinish(bool isCompletionDialogShown) + { + return isCompletionDialogShown + ? ProcedureMainRunCompletionResult.NoChange + : ProcedureMainRunCompletionResult.ShowCompletionDialog; + } + + public static ProcedureMainRunCompletionResult TryConfirmReturnToMenu( + ProcedureMainFlowPhase flowPhase, + bool isReturnToMenuPending) + { + if (flowPhase != ProcedureMainFlowPhase.RunCompletedPendingFinish || isReturnToMenuPending) + { + return ProcedureMainRunCompletionResult.NoChange; + } + + return ProcedureMainRunCompletionResult.ReturnToMenu; + } + } +} \ No newline at end of file diff --git a/src-ref/Procedure/ProcedureMain/ProcedureMainRunFlowService.cs b/src-ref/Procedure/ProcedureMain/ProcedureMainRunFlowService.cs new file mode 100644 index 0000000..002231a --- /dev/null +++ b/src-ref/Procedure/ProcedureMain/ProcedureMainRunFlowService.cs @@ -0,0 +1,27 @@ +using GeometryTD.Definition; + +namespace GeometryTD.Procedure +{ + public static class ProcedureMainRunFlowService + { + public static ProcedureMainRunAdvanceResult TryAdvanceRun( + RunState runState, + RunNodeCompletionStatus completionStatus, + RunNodeCompletionSnapshot completionSnapshot) + { + if (!RunStateAdvanceService.TryCompleteCurrentNode(runState, completionStatus, completionSnapshot)) + { + return ProcedureMainRunAdvanceResult.NoChange; + } + + if (runState != null && runState.IsCompleted) + { + return ProcedureMainRunAdvanceResult.RunCompleted; + } + + return completionStatus == RunNodeCompletionStatus.Exception + ? ProcedureMainRunAdvanceResult.NodeException + : ProcedureMainRunAdvanceResult.AdvancedToNextNode; + } + } +} diff --git a/src-ref/Procedure/ProcedureMain/RunModel.cs b/src-ref/Procedure/ProcedureMain/RunModel.cs new file mode 100644 index 0000000..720d46d --- /dev/null +++ b/src-ref/Procedure/ProcedureMain/RunModel.cs @@ -0,0 +1,355 @@ +using System; +using System.Collections.Generic; +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using GeometryTD.Factory; + +namespace GeometryTD.Procedure +{ + public enum RunNodeType + { + None = 0, + Combat = 1, + Event = 2, + Shop = 3, + BossCombat = 4 + } + + public enum RunNodeStatus + { + Locked = 0, + Available = 1, + Completed = 2, + Exception = 3, + Skipped = 4 + } + + public enum RunNodeCompletionStatus + { + None = 0, + Completed = 1, + Exception = 2 + } + + [Serializable] + public sealed class RunNodeSeed + { + public int NodeId { get; set; } + public RunNodeType NodeType { get; set; } + public LevelThemeType ThemeType { get; set; } + public int LinkedLevelId { get; set; } + public int SequenceIndex { get; set; } = -1; + } + + [Serializable] + public sealed class RunNodeState + { + public int NodeId { get; internal set; } + public RunNodeType NodeType { get; internal set; } + public LevelThemeType ThemeType { get; internal set; } + public int LinkedLevelId { get; internal set; } + public RunNodeStatus Status { get; internal set; } + public int SequenceIndex { get; internal set; } + + internal RunNodeState Clone() + { + return new RunNodeState + { + NodeId = NodeId, + NodeType = NodeType, + ThemeType = ThemeType, + LinkedLevelId = LinkedLevelId, + Status = Status, + SequenceIndex = SequenceIndex + }; + } + } + + [Serializable] + public sealed class RunItemState + { + public int ItemId { get; set; } + public int StackCount { get; set; } + + internal RunItemState Clone() + { + return new RunItemState + { + ItemId = ItemId, + StackCount = StackCount + }; + } + } + + [Serializable] + public sealed class RunNodeExecutionContext + { + public string RunId { get; set; } + public int RunSeed { get; set; } + public int NodeId { get; set; } + public RunNodeType NodeType { get; set; } + public int SequenceIndex { get; set; } + public LevelThemeType ThemeType { get; set; } + public int ThemeStageIndex { get; set; } + public List CurrentThemePool { get; set; } = new List(); + public List ThemeHistory { get; set; } = new List(); + public int CurrentNodeContinueChallengeLayer { get; set; } + public List RunItems { get; set; } = new List(); + + internal RunNodeExecutionContext Clone() + { + return new RunNodeExecutionContext + { + RunId = RunId, + RunSeed = RunSeed, + NodeId = NodeId, + NodeType = NodeType, + SequenceIndex = SequenceIndex, + ThemeType = ThemeType, + ThemeStageIndex = ThemeStageIndex, + CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool), + ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory), + CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer, + RunItems = RunStateCloneUtility.CloneRunItems(RunItems) + }; + } + + public RunNodeCompletionSnapshot CreateCompletionSnapshot(BackpackInventoryData inventorySnapshot) + { + return new RunNodeCompletionSnapshot + { + InventorySnapshot = InventoryCloneUtility.CloneInventory(inventorySnapshot), + ThemeType = ThemeType, + ThemeStageIndex = ThemeStageIndex, + CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool), + ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory), + CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer, + RunItems = RunStateCloneUtility.CloneRunItems(RunItems) + }; + } + } + + [Serializable] + public sealed class RunNodeCompletionSnapshot + { + public BackpackInventoryData InventorySnapshot { get; set; } + public LevelThemeType ThemeType { get; set; } + public int ThemeStageIndex { get; set; } + public List CurrentThemePool { get; set; } = new List(); + public List ThemeHistory { get; set; } = new List(); + public int CurrentNodeContinueChallengeLayer { get; set; } + public List RunItems { get; set; } = new List(); + + internal RunNodeCompletionSnapshot Clone() + { + return new RunNodeCompletionSnapshot + { + InventorySnapshot = InventoryCloneUtility.CloneInventory(InventorySnapshot), + ThemeType = ThemeType, + ThemeStageIndex = ThemeStageIndex, + CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool), + ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory), + CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer, + RunItems = RunStateCloneUtility.CloneRunItems(RunItems) + }; + } + } + + [Serializable] + public sealed class RunState + { + private readonly List _nodes; + + internal RunState( + string runId, + int runSeed, + LevelThemeType themeType, + List nodes, + BackpackInventoryData runInventorySnapshot) + { + RunId = string.IsNullOrWhiteSpace(runId) ? Guid.NewGuid().ToString("N") : runId; + RunSeed = runSeed == 0 ? RunStateFactory.CreateRunSeed() : runSeed; + ThemeType = themeType; + _nodes = nodes ?? new List(); + RunInventorySnapshot = InventoryCloneUtility.CloneInventory(runInventorySnapshot); + ThemeStageIndex = 0; + CurrentThemePool = CreateDefaultThemeList(themeType); + ThemeHistory = CreateDefaultThemeList(themeType); + CurrentNodeContinueChallengeLayer = 0; + RunItems = new List(); + CurrentNodeIndex = _nodes.Count > 0 ? 0 : -1; + IsCompleted = _nodes.Count <= 0; + } + + public string RunId { get; internal set; } + + public int RunSeed { get; internal set; } + + public LevelThemeType ThemeType { get; internal set; } + + public int CurrentNodeIndex { get; internal set; } + + public bool IsCompleted { get; internal set; } + + public BackpackInventoryData RunInventorySnapshot { get; internal set; } + + public int ThemeStageIndex { get; internal set; } + + public List CurrentThemePool { get; internal set; } + + public List ThemeHistory { get; internal set; } + + public int CurrentNodeContinueChallengeLayer { get; internal set; } + + public List RunItems { get; internal set; } + + public IReadOnlyList Nodes => _nodes; + + public int NodeCount => _nodes.Count; + + public RunNodeState CurrentNode + { + get + { + if (CurrentNodeIndex < 0 || CurrentNodeIndex >= _nodes.Count) + { + return null; + } + + return _nodes[CurrentNodeIndex]; + } + } + + public bool CanEnterCurrentNode => !IsCompleted && CurrentNode != null && CurrentNode.Status == RunNodeStatus.Available; + + public RunNodeState GetNodeBySequenceIndex(int sequenceIndex) + { + if (sequenceIndex < 0) + { + return null; + } + + foreach (RunNodeState nodeState in _nodes) + { + if (nodeState.SequenceIndex == sequenceIndex) + { + return nodeState; + } + } + + return null; + } + + public static bool IsBossNode(RunNodeState nodeState) + { + return nodeState != null && nodeState.NodeType == RunNodeType.BossCombat; + } + + public int CompletedNodeCount + { + get + { + int count = 0; + foreach (var nodeState in _nodes) + { + if (nodeState.Status == RunNodeStatus.Completed) + { + count++; + } + } + + return count; + } + } + + internal void ReplaceInventorySnapshot(BackpackInventoryData inventorySnapshot) + { + RunInventorySnapshot = InventoryCloneUtility.CloneInventory(inventorySnapshot); + } + + public RunNodeExecutionContext CreateCurrentNodeContext() + { + RunNodeState currentNode = CurrentNode; + if (currentNode == null) + { + return null; + } + + return new RunNodeExecutionContext + { + RunId = RunId, + RunSeed = RunSeed, + NodeId = currentNode.NodeId, + NodeType = currentNode.NodeType, + SequenceIndex = currentNode.SequenceIndex, + ThemeType = ThemeType, + ThemeStageIndex = ThemeStageIndex, + CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool), + ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory), + CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer, + RunItems = RunStateCloneUtility.CloneRunItems(RunItems) + }; + } + + public void ApplyCompletionSnapshot(RunNodeCompletionSnapshot completionSnapshot) + { + RunInventorySnapshot = InventoryCloneUtility.CloneInventory(completionSnapshot.InventorySnapshot); + ThemeType = completionSnapshot.ThemeType; + ThemeStageIndex = completionSnapshot.ThemeStageIndex; + CurrentThemePool = RunStateCloneUtility.CloneThemeList(completionSnapshot.CurrentThemePool); + ThemeHistory = RunStateCloneUtility.CloneThemeList(completionSnapshot.ThemeHistory); + CurrentNodeContinueChallengeLayer = completionSnapshot.CurrentNodeContinueChallengeLayer; + RunItems = RunStateCloneUtility.CloneRunItems(completionSnapshot.RunItems); + } + + private static List CreateDefaultThemeList(LevelThemeType themeType) + { + List themes = new List(); + if (themeType != LevelThemeType.None) + { + themes.Add(themeType); + } + + return themes; + } + } + + internal static class RunStateCloneUtility + { + internal static List CloneThemeList(IReadOnlyList source) + { + List cloned = new List(); + if (source == null) + { + return cloned; + } + + for (int i = 0; i < source.Count; i++) + { + cloned.Add(source[i]); + } + + return cloned; + } + + internal static List CloneRunItems(IReadOnlyList source) + { + List cloned = new List(); + if (source == null) + { + return cloned; + } + + for (int i = 0; i < source.Count; i++) + { + RunItemState item = source[i]; + if (item != null) + { + cloned.Add(item.Clone()); + } + } + + return cloned; + } + } +} diff --git a/src-ref/Procedure/ProcedureMain/RunStateAdvanceService.cs b/src-ref/Procedure/ProcedureMain/RunStateAdvanceService.cs new file mode 100644 index 0000000..bca272d --- /dev/null +++ b/src-ref/Procedure/ProcedureMain/RunStateAdvanceService.cs @@ -0,0 +1,56 @@ +namespace GeometryTD.Procedure +{ + public static class RunStateAdvanceService + { + public static bool TryCompleteCurrentNode( + RunState runState, + RunNodeCompletionStatus completionStatus, + RunNodeCompletionSnapshot completionSnapshot) + { + if (runState == null || runState.IsCompleted) + { + return false; + } + + RunNodeState currentNode = runState.CurrentNode; + if (currentNode == null || currentNode.Status != RunNodeStatus.Available) + { + return false; + } + + runState.ApplyCompletionSnapshot(completionSnapshot); + + if (completionStatus == RunNodeCompletionStatus.Exception) + { + currentNode.Status = RunNodeStatus.Exception; + return true; + } + + if (completionStatus != RunNodeCompletionStatus.Completed) + { + return false; + } + + currentNode.Status = RunNodeStatus.Completed; + + int nextIndex = runState.CurrentNodeIndex + 1; + if (nextIndex >= runState.Nodes.Count) + { + runState.CurrentNodeContinueChallengeLayer = 0; + runState.CurrentNodeIndex = runState.Nodes.Count; + runState.IsCompleted = true; + return true; + } + + runState.CurrentNodeIndex = nextIndex; + runState.CurrentNodeContinueChallengeLayer = 0; + RunNodeState nextNode = runState.CurrentNode; + if (nextNode != null && nextNode.Status == RunNodeStatus.Locked) + { + nextNode.Status = RunNodeStatus.Available; + } + + return true; + } + } +} diff --git a/src-ref/Procedure/ProcedureMenu.cs b/src-ref/Procedure/ProcedureMenu.cs new file mode 100644 index 0000000..dd9354c --- /dev/null +++ b/src-ref/Procedure/ProcedureMenu.cs @@ -0,0 +1,59 @@ +using GameFramework.Fsm; +using GameFramework.Procedure; +using GeometryTD.Definition; +using GeometryTD.UI; +using UnityGameFramework.Runtime; + +namespace GeometryTD.Procedure +{ + public class ProcedureMenu : ProcedureBase + { + public override bool UseNativeDialog => false; + + public bool GameStart { get; set; } + + private MenuFormUseCase _menuFormUseCase; + + #region FSM + + protected override void OnInit(IFsm procedureOwner) + { + base.OnInit(procedureOwner); + } + + protected override void OnEnter(IFsm procedureOwner) + { + base.OnEnter(procedureOwner); + + GameStart = false; + + _menuFormUseCase = new MenuFormUseCase(); + GameEntry.UIRouter.BindUIUseCase(UIFormType.MenuForm, _menuFormUseCase); + GameEntry.UIRouter.OpenUI(UIFormType.MenuForm); + } + + protected override void OnUpdate(IFsm procedureOwner, float elapseSeconds, + float realElapseSeconds) + { + base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds); + + if (GameStart) + { + procedureOwner.SetData("NextSceneId", (int)SceneType.Main); + ChangeState(procedureOwner); + return; + } + + } + + protected override void OnLeave(IFsm procedureOwner, bool isShutdown) + { + GameEntry.UIRouter.CloseUI(UIFormType.MenuForm); + _menuFormUseCase = null; + + base.OnLeave(procedureOwner, isShutdown); + } + + #endregion + } +} diff --git a/src-ref/Procedure/ProcedureTest.cs b/src-ref/Procedure/ProcedureTest.cs new file mode 100644 index 0000000..1328863 --- /dev/null +++ b/src-ref/Procedure/ProcedureTest.cs @@ -0,0 +1,30 @@ +using GameFramework.Fsm; +using GameFramework.Procedure; + +namespace GeometryTD.Procedure +{ + public class ProcedureTest : ProcedureBase + { + public override bool UseNativeDialog => false; + + #region FSM + + protected override void OnEnter(IFsm procedureOwner) + { + base.OnEnter(procedureOwner); + } + + protected override void OnUpdate(IFsm procedureOwner, float elapseSeconds, + float realElapseSeconds) + { + base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds); + } + + protected override void OnLeave(IFsm procedureOwner, bool isShutdown) + { + base.OnLeave(procedureOwner, isShutdown); + } + + #endregion + } +} \ No newline at end of file diff --git a/src-ref/Scene/HideByBoundary.cs b/src-ref/Scene/HideByBoundary.cs new file mode 100644 index 0000000..bbcb8d9 --- /dev/null +++ b/src-ref/Scene/HideByBoundary.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using GeometryTD.Entity; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD +{ + public class HideByBoundary : MonoBehaviour + { + private void OnTriggerExit(Collider other) + { + GameObject go = other.gameObject; + EntityBase entity = go.GetComponent(); + if (entity == null) + { + Log.Warning("Unknown GameObject '{0}', you must use entity only.", go.name); + Destroy(go); + return; + } + + GameEntry.Entity.HideEntity(entity); + } + } +} diff --git a/src-ref/Scene/Map/House.cs b/src-ref/Scene/Map/House.cs new file mode 100644 index 0000000..5a03c3f --- /dev/null +++ b/src-ref/Scene/Map/House.cs @@ -0,0 +1,10 @@ +using UnityEngine; + +namespace GeometryTD.Map +{ + [DisallowMultipleComponent] + public class House : MonoBehaviour + { + public Vector3 Position => transform.position; + } +} diff --git a/src-ref/Scene/Map/MapCombatRuntimeBridge.cs b/src-ref/Scene/Map/MapCombatRuntimeBridge.cs new file mode 100644 index 0000000..8324626 --- /dev/null +++ b/src-ref/Scene/Map/MapCombatRuntimeBridge.cs @@ -0,0 +1,85 @@ +using System; +using GameFramework.Event; +using GeometryTD.CustomEvent; +using GeometryTD.Entity.EntityData; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.Map +{ + public sealed class MapCombatRuntimeBridge + { + private Func _tryConsumeCoin; + private Action _addCoin; + private bool _isCoinEventSubscribed; + + public int CurrentCoin { get; private set; } + + public void Initialize(MapEntityLoadContext loadContext, string mapName) + { + Reset(); + + MapData mapData = loadContext?.InitialMapData; + CurrentCoin = Mathf.Max(0, mapData != null ? mapData.InitialCoin : 0); + _tryConsumeCoin = loadContext?.TryConsumeCoin; + _addCoin = loadContext?.AddCoin; + + if (_tryConsumeCoin == null || _addCoin == null) + { + Log.Warning( + "Map combat runtime bridge has incomplete callbacks. Map='{0}', TryConsumeCoin={1}, AddCoin={2}.", + mapName, + _tryConsumeCoin != null, + _addCoin != null); + } + + GameEntry.Event.Subscribe(CombatCoinChangedEventArgs.EventId, OnCombatCoinChanged); + _isCoinEventSubscribed = true; + } + + public void Reset() + { + if (_isCoinEventSubscribed) + { + GameEntry.Event.Unsubscribe(CombatCoinChangedEventArgs.EventId, OnCombatCoinChanged); + _isCoinEventSubscribed = false; + } + + _tryConsumeCoin = null; + _addCoin = null; + CurrentCoin = 0; + } + + public bool TryConsumeCoin(int cost) + { + int requiredCoin = Mathf.Max(0, cost); + if (requiredCoin <= 0) + { + return true; + } + + return _tryConsumeCoin != null && _tryConsumeCoin.Invoke(requiredCoin); + } + + public void AddCoin(int coin) + { + int amount = Mathf.Max(0, coin); + if (amount <= 0) + { + return; + } + + _addCoin?.Invoke(amount); + } + + private void OnCombatCoinChanged(object sender, GameEventArgs e) + { + if (e is not CombatCoinChangedEventArgs args) + { + return; + } + + CurrentCoin = Mathf.Max(0, args.CurrentCoin); + } + } +} diff --git a/src-ref/Scene/Map/MapDataRefs.cs b/src-ref/Scene/Map/MapDataRefs.cs new file mode 100644 index 0000000..1aa6b9b --- /dev/null +++ b/src-ref/Scene/Map/MapDataRefs.cs @@ -0,0 +1,37 @@ +using UnityEngine; +using UnityEngine.Tilemaps; + +namespace GeometryTD.Map +{ + [DisallowMultipleComponent] + public class MapDataRefs : MonoBehaviour + { + [SerializeField] private Tilemap _tilemap = null; + [SerializeField] private Spawner[] _spawners = null; + [SerializeField] private House _house = null; + + public Tilemap Tilemap => _tilemap; + public Spawner[] Spawners => _spawners; + public House House => _house; + +#if UNITY_EDITOR + private void OnValidate() + { + if (_tilemap == null) + { + _tilemap = GetComponentInChildren(true); + } + + if (_spawners == null || _spawners.Length == 0) + { + _spawners = GetComponentsInChildren(true); + } + + if (_house == null) + { + _house = GetComponentInChildren(true); + } + } +#endif + } +} diff --git a/src-ref/Scene/Map/MapTopologyService.cs b/src-ref/Scene/Map/MapTopologyService.cs new file mode 100644 index 0000000..9a5f829 --- /dev/null +++ b/src-ref/Scene/Map/MapTopologyService.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using GeometryTD.Pathfinding; +using UnityEngine; +using UnityEngine.Tilemaps; +using UnityGameFramework.Runtime; + +namespace GeometryTD.Map +{ + public sealed class MapTopologyService + { + private const string PathTileName = "Path"; + private const string FoundationTileName = "Foundation"; + + private readonly List _pathCells = new(); + private readonly List _foundationCells = new(); + private readonly HashSet _pathCellSet = new(); + private readonly HashSet _foundationCellSet = new(); + private readonly IMapPathfinder _mapPathfinder = new GridMapPathfinder(); + private readonly List _pathCellBuffer = new(); + private readonly Dictionary _spawnerPathStartByRef = new(); + private readonly Dictionary> _defaultPathCellsBySpawner = new(); + + private bool _hasHousePathCell; + private Vector3Int _housePathCell; + + public IReadOnlyList PathCells => _pathCells; + public IReadOnlyList FoundationCells => _foundationCells; + + public void Refresh(Tilemap tilemap, Spawner[] spawners, House house, string mapName, int levelId) + { + Clear(); + if (tilemap == null) + { + return; + } + + BoundsInt bounds = tilemap.cellBounds; + foreach (Vector3Int cellPosition in bounds.allPositionsWithin) + { + TileBase tile = tilemap.GetTile(cellPosition); + if (tile == null || string.IsNullOrEmpty(tile.name)) + { + continue; + } + + if (string.Equals(tile.name, PathTileName, StringComparison.Ordinal)) + { + _pathCells.Add(cellPosition); + _pathCellSet.Add(cellPosition); + continue; + } + + if (string.Equals(tile.name, FoundationTileName, StringComparison.Ordinal)) + { + _foundationCells.Add(cellPosition); + _foundationCellSet.Add(cellPosition); + } + } + + RefreshPathCache(tilemap, spawners, house, mapName); + Log.Info( + "Map '{0}' initialized. LevelId={1}, PathCells={2}, FoundationCells={3}, Spawners={4}, House={5}, Routes={6}.", + mapName, + levelId, + _pathCells.Count, + _foundationCells.Count, + spawners != null ? spawners.Length : 0, + house != null ? house.name : "None", + _defaultPathCellsBySpawner.Count); + } + + public void Clear() + { + _pathCells.Clear(); + _foundationCells.Clear(); + _pathCellSet.Clear(); + _foundationCellSet.Clear(); + _pathCellBuffer.Clear(); + _hasHousePathCell = false; + _housePathCell = default; + _spawnerPathStartByRef.Clear(); + _defaultPathCellsBySpawner.Clear(); + } + + public bool IsPathCell(Vector3Int cellPosition) + { + return _pathCellSet.Contains(cellPosition); + } + + public bool IsFoundationCell(Vector3Int cellPosition) + { + return _foundationCellSet.Contains(cellPosition); + } + + public bool TryGetNearestPathCell(Tilemap tilemap, Vector3 worldPosition, out Vector3Int pathCell) + { + pathCell = default; + if (_pathCells.Count <= 0 || tilemap == null) + { + return false; + } + + Vector3Int directCell = tilemap.WorldToCell(worldPosition); + if (_pathCellSet.Contains(directCell)) + { + pathCell = directCell; + return true; + } + + float minDistance = float.MaxValue; + foreach (var candidate in _pathCells) + { + float distance = (tilemap.GetCellCenterWorld(candidate) - worldPosition).sqrMagnitude; + if (distance >= minDistance) + { + continue; + } + + minDistance = distance; + pathCell = candidate; + } + + return minDistance < float.MaxValue; + } + + public Vector3 GetPathCellCenterWorld(Tilemap tilemap, Vector3Int pathCell) + { + return tilemap != null ? tilemap.GetCellCenterWorld(pathCell) : Vector3.zero; + } + + public bool TryGetDefaultPathCells(Spawner spawner, out IReadOnlyList pathCells) + { + pathCells = null; + if (spawner == null) + { + return false; + } + + if (!_defaultPathCellsBySpawner.TryGetValue(spawner, out List cachedPathCells)) + { + return false; + } + + pathCells = cachedPathCells; + return true; + } + + public bool TryFindPathCells(Spawner spawner, IReadOnlyCollection blockedCells, + List pathResult) + { + if (pathResult == null) + { + return false; + } + + pathResult.Clear(); + if (spawner == null || !_hasHousePathCell) + { + return false; + } + + if (!_spawnerPathStartByRef.TryGetValue(spawner, out Vector3Int startCell)) + { + return false; + } + + return _mapPathfinder.TryFindPath(_pathCells, startCell, _housePathCell, blockedCells, pathResult); + } + + public bool TryFindPathWorldPoints(Tilemap tilemap, Spawner spawner, + IReadOnlyCollection blockedCells, + List worldPathResult) + { + if (worldPathResult == null) + { + return false; + } + + worldPathResult.Clear(); + if (tilemap == null) + { + return false; + } + + if (!TryFindPathCells(spawner, blockedCells, _pathCellBuffer)) + { + return false; + } + + foreach (Vector3Int pos in _pathCellBuffer) + { + worldPathResult.Add(GetPathCellCenterWorld(tilemap, pos)); + } + + return true; + } + + private void RefreshPathCache(Tilemap tilemap, Spawner[] spawners, House house, string mapName) + { + _hasHousePathCell = false; + _housePathCell = default; + _spawnerPathStartByRef.Clear(); + _defaultPathCellsBySpawner.Clear(); + + if (house == null) + { + Log.Warning("Map '{0}' has no house reference, path cache skipped.", mapName); + return; + } + + if (!TryGetNearestPathCell(tilemap, house.Position, out _housePathCell)) + { + Log.Warning("Map '{0}' house position can not map to a valid path cell.", mapName); + return; + } + + _hasHousePathCell = true; + if (spawners == null) + { + return; + } + + foreach (Spawner spawner in spawners) + { + if (spawner == null) + { + continue; + } + + if (!TryGetNearestPathCell(tilemap, spawner.Position, out Vector3Int startCell)) + { + Log.Warning("Map '{0}' spawner '{1}' can not map to a valid path cell.", mapName, spawner.name); + continue; + } + + _spawnerPathStartByRef[spawner] = startCell; + + List defaultPathCells = new(); + if (!_mapPathfinder.TryFindPath(_pathCells, startCell, _housePathCell, null, defaultPathCells)) + { + Log.Warning( + "Map '{0}' spawner '{1}' has no path to house cell {2}.", + mapName, + spawner.name, + _housePathCell); + continue; + } + + _defaultPathCellsBySpawner[spawner] = defaultPathCells; + } + } + } +} \ No newline at end of file diff --git a/src-ref/Scene/Map/Spawner.cs b/src-ref/Scene/Map/Spawner.cs new file mode 100644 index 0000000..45b51cd --- /dev/null +++ b/src-ref/Scene/Map/Spawner.cs @@ -0,0 +1,14 @@ +using UnityEngine; + +namespace GeometryTD.Map +{ + [DisallowMultipleComponent] + public class Spawner : MonoBehaviour + { + [SerializeField] private int _spawnOrder = 0; + + public int SpawnOrder => _spawnOrder; + + public Vector3 Position => transform.position; + } +} diff --git a/src-ref/Scene/Map/TowerPlacementService.cs b/src-ref/Scene/Map/TowerPlacementService.cs new file mode 100644 index 0000000..4d3a42b --- /dev/null +++ b/src-ref/Scene/Map/TowerPlacementService.cs @@ -0,0 +1,361 @@ +using System; +using System.Collections.Generic; +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using GeometryTD.Entity; +using GeometryTD.Entity.EntityData; +using UnityEngine; +using UnityEngine.Tilemaps; + +namespace GeometryTD.Map +{ + public sealed class TowerPlacementService + { + private const int DefaultTowerTypeId = 401; + private const int MinTowerLevel = 0; + private const int MaxTowerLevel = 4; + + private readonly Dictionary _towerEntityIdByFoundationCell = new(); + private readonly Dictionary _foundationCellByTowerEntityId = new(); + private readonly Dictionary _towerStatsByEntityId = new(); + private readonly Dictionary _towerLevelByEntityId = new(); + private readonly List _towerEntityIdBuffer = new(); + + public IReadOnlyDictionary TowerEntityIdByFoundationCell => _towerEntityIdByFoundationCell; + public IReadOnlyDictionary FoundationCellByTowerEntityId => _foundationCellByTowerEntityId; + + public bool IsTowerAtMaxLevel(int towerEntityId) + { + if (towerEntityId == 0) + { + return false; + } + + TowerStatsData towerStats = + _towerStatsByEntityId.TryGetValue(towerEntityId, out TowerStatsData cachedStats) + ? cachedStats + : null; + int currentLevel = GetTowerLevel(towerEntityId); + int maxLevel = ResolveMaxTowerLevel(towerStats); + return currentLevel >= maxLevel; + } + + public bool TryBuildTower(Vector3Int foundationCell, Func isFoundationCell, int buildIndex, + int[] buildTowerCosts, TowerStatsData buildTowerStats, Color muzzleColor, Color bearingColor, Color baseColor, int towerTypeId, Tilemap tilemap, Func tryConsumeCoin, + Action addCoin, + out int towerEntityId) + { + towerEntityId = 0; + if (isFoundationCell == null || !isFoundationCell(foundationCell)) + { + return false; + } + + if (_towerEntityIdByFoundationCell.ContainsKey(foundationCell)) + { + return false; + } + + TowerStatsData towerStats = CloneTowerStats(buildTowerStats); + if (towerStats == null) + { + return false; + } + + int buildCost = GetBuildTowerCost(buildTowerCosts, buildIndex); + if (tryConsumeCoin != null && !tryConsumeCoin(buildCost)) + { + return false; + } + + if (!TryShowTowerEntity(foundationCell, towerStats, muzzleColor, bearingColor, baseColor, towerTypeId, tilemap, out int newTowerEntityId)) + { + addCoin?.Invoke(buildCost); + return false; + } + + _towerEntityIdByFoundationCell[foundationCell] = newTowerEntityId; + _foundationCellByTowerEntityId[newTowerEntityId] = foundationCell; + _towerStatsByEntityId[newTowerEntityId] = CloneTowerStats(towerStats); + _towerLevelByEntityId[newTowerEntityId] = MinTowerLevel; + towerEntityId = newTowerEntityId; + return true; + } + + public bool TryUpgradeTower(int towerEntityId, int upgradeCost, int towerTypeId, Tilemap tilemap, + Func tryConsumeCoin, Action addCoin, out int resultTowerEntityId, + out Vector3Int foundationCell) + { + resultTowerEntityId = 0; + foundationCell = default; + if (towerEntityId == 0 || !_foundationCellByTowerEntityId.TryGetValue(towerEntityId, out foundationCell)) + { + return false; + } + + TowerStatsData towerStats = + _towerStatsByEntityId.TryGetValue(towerEntityId, out TowerStatsData cachedStats) + ? CloneTowerStats(cachedStats) + : BuildTowerStats(0); + int currentTowerLevel = GetTowerLevel(towerEntityId); + int maxTowerLevel = ResolveMaxTowerLevel(towerStats); + if (currentTowerLevel >= maxTowerLevel) + { + resultTowerEntityId = towerEntityId; + return false; + } + + int requiredUpgradeCost = Mathf.Max(0, upgradeCost); + if (tryConsumeCoin != null && !tryConsumeCoin(requiredUpgradeCost)) + { + resultTowerEntityId = towerEntityId; + return false; + } + + int nextTowerLevel = Mathf.Clamp(currentTowerLevel + 1, MinTowerLevel, maxTowerLevel); + if (!TryApplyTowerStats(towerEntityId, towerStats, nextTowerLevel)) + { + addCoin?.Invoke(requiredUpgradeCost); + resultTowerEntityId = towerEntityId; + return false; + } + + _towerStatsByEntityId[towerEntityId] = CloneTowerStats(towerStats); + _towerLevelByEntityId[towerEntityId] = nextTowerLevel; + resultTowerEntityId = towerEntityId; + return true; + } + + public bool TryDestroyTower(int towerEntityId, int destroyGain, Action addCoin, + out Vector3Int foundationCell) + { + foundationCell = default; + if (towerEntityId == 0 || !_foundationCellByTowerEntityId.TryGetValue(towerEntityId, out foundationCell)) + { + return false; + } + + HideTowerEntity(towerEntityId); + _towerEntityIdByFoundationCell.Remove(foundationCell); + _foundationCellByTowerEntityId.Remove(towerEntityId); + _towerStatsByEntityId.Remove(towerEntityId); + _towerLevelByEntityId.Remove(towerEntityId); + addCoin?.Invoke(Mathf.Max(0, destroyGain)); + return true; + } + + public void HideAndClearAllPlacedTowers() + { + _towerEntityIdBuffer.Clear(); + foreach (KeyValuePair pair in _foundationCellByTowerEntityId) + { + _towerEntityIdBuffer.Add(pair.Key); + } + + for (int i = 0; i < _towerEntityIdBuffer.Count; i++) + { + HideTowerEntity(_towerEntityIdBuffer[i]); + } + + _towerEntityIdByFoundationCell.Clear(); + _foundationCellByTowerEntityId.Clear(); + _towerStatsByEntityId.Clear(); + _towerLevelByEntityId.Clear(); + _towerEntityIdBuffer.Clear(); + } + + public void ClearTracking() + { + _towerEntityIdByFoundationCell.Clear(); + _foundationCellByTowerEntityId.Clear(); + _towerStatsByEntityId.Clear(); + _towerLevelByEntityId.Clear(); + _towerEntityIdBuffer.Clear(); + } + + private static int GetBuildTowerCost(int[] buildTowerCosts, int buildIndex) + { + if (buildTowerCosts == null || buildIndex < 0 || buildIndex >= buildTowerCosts.Length) + { + return 0; + } + + return Mathf.Max(0, buildTowerCosts[buildIndex]); + } + + private static bool TryShowTowerEntity(Vector3Int foundationCell, TowerStatsData towerStats, Color muzzleColor, Color bearingColor, Color baseColor, + int towerTypeId, + Tilemap tilemap, out int towerEntityId) + { + towerEntityId = 0; + if (GameEntry.Entity == null) + { + return false; + } + + int entityId = GameEntry.Entity.GenerateSerialId(); + int typeId = towerTypeId > 0 ? towerTypeId : DefaultTowerTypeId; + Vector3 towerPosition = tilemap != null ? tilemap.GetCellCenterWorld(foundationCell) : foundationCell; + towerPosition.z = 0f; + var towerData = new TowerData(entityId, typeId, towerPosition, Quaternion.identity, towerStats, + MinTowerLevel, muzzleColor, bearingColor, baseColor); + GameEntry.Entity.ShowDefenseTower(towerData); + + towerEntityId = entityId; + return true; + } + + private static void HideTowerEntity(int towerEntityId) + { + if (towerEntityId == 0 || GameEntry.Entity == null) + { + return; + } + + UnityGameFramework.Runtime.Entity towerEntity = GameEntry.Entity.GetEntity(towerEntityId); + if (towerEntity != null) + { + GameEntry.Entity.HideEntity(towerEntity); + } + } + + private static TowerStatsData BuildTowerStats(int buildIndex) + { + switch (buildIndex) + { + case 0: + return new TowerStatsData + { + AttackDamage = new[] { 200, 220, 240, 260, 300 }, + DamageRandomRate = 0f, + RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f }, + AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f }, + AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f }, + AttackMethodType = AttackMethodType.NormalBullet, + AttackPropertyType = AttackPropertyType.Physics, + TagRuntimes = Array.Empty(), + Tags = Array.Empty() + }; + case 1: + return new TowerStatsData + { + AttackDamage = new[] { 200, 220, 240, 260, 300 }, + DamageRandomRate = 0f, + RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f }, + AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f }, + AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f }, + AttackMethodType = AttackMethodType.NormalBullet, + AttackPropertyType = AttackPropertyType.Fire, + TagRuntimes = Array.Empty(), + Tags = Array.Empty() + }; + case 2: + return new TowerStatsData + { + AttackDamage = new[] { 200, 220, 240, 260, 300 }, + DamageRandomRate = 0f, + RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f }, + AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f }, + AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f }, + AttackMethodType = AttackMethodType.NormalBullet, + AttackPropertyType = AttackPropertyType.Ice, + TagRuntimes = Array.Empty(), + Tags = Array.Empty() + }; + case 3: + return new TowerStatsData + { + AttackDamage = new[] { 200, 220, 240, 260, 300 }, + DamageRandomRate = 0f, + RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f }, + AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f }, + AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f }, + AttackMethodType = AttackMethodType.NormalBullet, + AttackPropertyType = AttackPropertyType.Poison, + TagRuntimes = Array.Empty(), + Tags = Array.Empty() + }; + default: + return new TowerStatsData + { + AttackDamage = new[] { 200, 220, 240, 260, 300 }, + DamageRandomRate = 0f, + RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f }, + AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f }, + AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f }, + AttackMethodType = AttackMethodType.NormalBullet, + AttackPropertyType = AttackPropertyType.Physics, + TagRuntimes = Array.Empty(), + Tags = Array.Empty() + }; + } + } + + private static TowerStatsData CloneTowerStats(TowerStatsData source) + { + if (source == null) + { + return BuildTowerStats(0); + } + + TagType[] copiedTags = source.Tags != null ? (TagType[])source.Tags.Clone() : Array.Empty(); + return new TowerStatsData + { + AttackDamage = source.AttackDamage != null ? (int[])source.AttackDamage.Clone() : Array.Empty(), + DamageRandomRate = source.DamageRandomRate, + RotateSpeed = source.RotateSpeed != null ? (float[])source.RotateSpeed.Clone() : Array.Empty(), + AttackRange = source.AttackRange != null ? (float[])source.AttackRange.Clone() : Array.Empty(), + AttackSpeed = source.AttackSpeed != null ? (float[])source.AttackSpeed.Clone() : Array.Empty(), + AttackMethodType = source.AttackMethodType, + AttackPropertyType = source.AttackPropertyType, + TagRuntimes = InventoryCloneUtility.CloneTagRuntimes(source.TagRuntimes), + Tags = copiedTags + }; + } + + private int GetTowerLevel(int towerEntityId) + { + if (towerEntityId == 0 || !_towerLevelByEntityId.TryGetValue(towerEntityId, out int towerLevel)) + { + return MinTowerLevel; + } + + return Mathf.Clamp(towerLevel, MinTowerLevel, MaxTowerLevel); + } + + private static int ResolveMaxTowerLevel(TowerStatsData stats) + { + int maxCount = Mathf.Max( + GetLength(stats?.AttackDamage), + GetLength(stats?.RotateSpeed), + GetLength(stats?.AttackRange), + GetLength(stats?.AttackSpeed)); + if (maxCount <= 0) + { + return MinTowerLevel; + } + + return Mathf.Clamp(maxCount - 1, MinTowerLevel, MaxTowerLevel); + } + + private static bool TryApplyTowerStats(int towerEntityId, TowerStatsData towerStats, int towerLevel) + { + if (towerEntityId == 0 || towerStats == null || GameEntry.Entity == null) + { + return false; + } + + if (GameEntry.Entity.GetGameEntity(towerEntityId) is not TowerEntity towerEntity) + { + return false; + } + + return towerEntity.TryApplyStats(towerStats, towerLevel); + } + + private static int GetLength(T[] values) + { + return values != null ? values.Length : 0; + } + } +} diff --git a/src-ref/Scene/Map/TowerSelectionPresenter.cs b/src-ref/Scene/Map/TowerSelectionPresenter.cs new file mode 100644 index 0000000..89a60a2 --- /dev/null +++ b/src-ref/Scene/Map/TowerSelectionPresenter.cs @@ -0,0 +1,134 @@ +using System.Collections.Generic; +using GeometryTD.Entity; +using GeometryTD.UI; +using UnityEngine; + +namespace GeometryTD.Map +{ + public sealed class TowerSelectionPresenter + { + private bool _hasSelectedFoundationCell; + private Vector3Int _selectedFoundationCell; + private int _selectedTowerEntityId; + private int _attackRangeVisibleTowerEntityId; + + public void ApplySelectedObject(CombatSelectFormUserData userData) + { + if (userData == null) + { + ClearSelectedObject(); + return; + } + + switch (userData.ClickObjectType) + { + case CombatSelectClickObjectType.Foundation: + _hasSelectedFoundationCell = true; + _selectedFoundationCell = userData.CellPosition; + _selectedTowerEntityId = 0; + break; + case CombatSelectClickObjectType.Tower: + _hasSelectedFoundationCell = true; + _selectedFoundationCell = userData.CellPosition; + _selectedTowerEntityId = userData.TowerEntityId; + break; + default: + ClearSelectedObject(); + return; + } + + UpdateTowerAttackRangeDisplay(_selectedTowerEntityId); + } + + public void SelectTower(Vector3Int foundationCell, int towerEntityId) + { + _hasSelectedFoundationCell = true; + _selectedFoundationCell = foundationCell; + _selectedTowerEntityId = towerEntityId; + UpdateTowerAttackRangeDisplay(_selectedTowerEntityId); + } + + public bool TryGetSelectedFoundationCell(out Vector3Int foundationCell) + { + foundationCell = default; + if (!_hasSelectedFoundationCell) + { + return false; + } + + foundationCell = _selectedFoundationCell; + return true; + } + + public bool TryGetSelectedTower(IReadOnlyDictionary foundationCellByTowerEntityId, + out int towerEntityId, out Vector3Int foundationCell) + { + towerEntityId = 0; + foundationCell = default; + if (_selectedTowerEntityId == 0 || foundationCellByTowerEntityId == null) + { + return false; + } + + if (!foundationCellByTowerEntityId.TryGetValue(_selectedTowerEntityId, out foundationCell)) + { + return false; + } + + towerEntityId = _selectedTowerEntityId; + return true; + } + + public void ClearSelectedObject() + { + UpdateTowerAttackRangeDisplay(0); + _hasSelectedFoundationCell = false; + _selectedFoundationCell = default; + _selectedTowerEntityId = 0; + } + + private void UpdateTowerAttackRangeDisplay(int towerEntityId) + { + if (_attackRangeVisibleTowerEntityId != 0 && _attackRangeVisibleTowerEntityId != towerEntityId) + { + SetTowerAttackRangeVisible(_attackRangeVisibleTowerEntityId, false); + _attackRangeVisibleTowerEntityId = 0; + } + + if (towerEntityId == 0) + { + if (_attackRangeVisibleTowerEntityId != 0) + { + SetTowerAttackRangeVisible(_attackRangeVisibleTowerEntityId, false); + _attackRangeVisibleTowerEntityId = 0; + } + + return; + } + + if (SetTowerAttackRangeVisible(towerEntityId, true)) + { + _attackRangeVisibleTowerEntityId = towerEntityId; + } + } + + private static bool SetTowerAttackRangeVisible(int towerEntityId, bool visible) + { + if (towerEntityId == 0 || GameEntry.Entity == null) + { + return false; + } + + EntityBase gameEntity = GameEntry.Entity.GetGameEntity(towerEntityId); + if (gameEntity is not TowerEntity towerEntity) + { + return false; + } + + towerEntity.SetAttackRangeVisible(visible); + return true; + } + } + + +} diff --git a/src-ref/Scene/Pathfinding/GridMapPathfinder.cs b/src-ref/Scene/Pathfinding/GridMapPathfinder.cs new file mode 100644 index 0000000..f8e6ec9 --- /dev/null +++ b/src-ref/Scene/Pathfinding/GridMapPathfinder.cs @@ -0,0 +1,134 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace GeometryTD.Pathfinding +{ + public class GridMapPathfinder : IMapPathfinder + { + private static readonly Vector3Int[] NeighborOffsets = + { + new Vector3Int(1, 0, 0), + new Vector3Int(-1, 0, 0), + new Vector3Int(0, 1, 0), + new Vector3Int(0, -1, 0) + }; + + private readonly HashSet _walkableSet = new HashSet(); + private readonly Queue _openQueue = new Queue(); + private readonly Dictionary _cameFrom = new Dictionary(); + + public bool TryFindPath( + IReadOnlyCollection walkableCells, + Vector3Int startCell, + Vector3Int destinationCell, + IReadOnlyCollection blockedCells, + List pathResult) + { + if (pathResult == null) + { + return false; + } + + pathResult.Clear(); + if (walkableCells == null || walkableCells.Count <= 0) + { + return false; + } + + BuildWalkableSet(walkableCells); + if (!_walkableSet.Contains(startCell) || !_walkableSet.Contains(destinationCell)) + { + return false; + } + + _openQueue.Clear(); + _cameFrom.Clear(); + _openQueue.Enqueue(startCell); + _cameFrom[startCell] = startCell; + + while (_openQueue.Count > 0) + { + Vector3Int currentCell = _openQueue.Dequeue(); + if (currentCell == destinationCell) + { + BuildPath(destinationCell, pathResult); + return true; + } + + for (int i = 0; i < NeighborOffsets.Length; i++) + { + Vector3Int nextCell = currentCell + NeighborOffsets[i]; + if (!_walkableSet.Contains(nextCell) || _cameFrom.ContainsKey(nextCell)) + { + continue; + } + + if (IsBlocked(nextCell, blockedCells, startCell, destinationCell)) + { + continue; + } + + _cameFrom[nextCell] = currentCell; + _openQueue.Enqueue(nextCell); + } + } + + return false; + } + + private void BuildWalkableSet(IReadOnlyCollection walkableCells) + { + _walkableSet.Clear(); + foreach (Vector3Int cell in walkableCells) + { + _walkableSet.Add(cell); + } + } + + private void BuildPath(Vector3Int destinationCell, List pathResult) + { + pathResult.Clear(); + Vector3Int current = destinationCell; + while (true) + { + pathResult.Add(current); + Vector3Int parent = _cameFrom[current]; + if (parent == current) + { + break; + } + + current = parent; + } + + pathResult.Reverse(); + } + + private static bool IsBlocked( + Vector3Int cell, + IReadOnlyCollection blockedCells, + Vector3Int startCell, + Vector3Int destinationCell) + { + if (blockedCells == null || blockedCells.Count <= 0) + { + return false; + } + + if (cell == startCell || cell == destinationCell) + { + return false; + } + + foreach (Vector3Int blockedCell in blockedCells) + { + if (blockedCell == cell) + { + return true; + } + } + + return false; + } + } +} diff --git a/src-ref/Scene/Pathfinding/IMapPathfinder.cs b/src-ref/Scene/Pathfinding/IMapPathfinder.cs new file mode 100644 index 0000000..33340c7 --- /dev/null +++ b/src-ref/Scene/Pathfinding/IMapPathfinder.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace GeometryTD.Pathfinding +{ + public interface IMapPathfinder + { + bool TryFindPath( + IReadOnlyCollection walkableCells, + Vector3Int startCell, + Vector3Int destinationCell, + IReadOnlyCollection blockedCells, + List pathResult); + } +} diff --git a/src-ref/Sound/SoundExtension.cs b/src-ref/Sound/SoundExtension.cs new file mode 100644 index 0000000..f00ec4f --- /dev/null +++ b/src-ref/Sound/SoundExtension.cs @@ -0,0 +1,170 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using GeometryTD.Entity; +using GameFramework; +using GameFramework.DataTable; +using GameFramework.Sound; +using GeometryTD.CustomUtility; +using GeometryTD.DataTable; +using UnityGameFramework.Runtime; + +namespace GeometryTD +{ + public static class SoundExtension + { + private const float FadeVolumeDuration = 1f; + private static int? s_MusicSerialId = null; + + public static int? PlayMusic(this SoundComponent soundComponent, int musicId, object userData = null) + { + soundComponent.StopMusic(); + + IDataTable dtMusic = GameEntry.DataTable.GetDataTable(); + DRMusic drMusic = dtMusic.GetDataRow(musicId); + if (drMusic == null) + { + Log.Warning("Can not load music '{0}' from data table.", musicId.ToString()); + return null; + } + + PlaySoundParams playSoundParams = PlaySoundParams.Create(); + playSoundParams.Priority = 64; + playSoundParams.Loop = true; + playSoundParams.VolumeInSoundGroup = 1f; + playSoundParams.FadeInSeconds = FadeVolumeDuration; + playSoundParams.SpatialBlend = 0f; + s_MusicSerialId = soundComponent.PlaySound(AssetUtility.GetMusicAsset(drMusic.AssetName), "Music", Constant.AssetPriority.MusicAsset, playSoundParams, null, userData); + return s_MusicSerialId; + } + + public static void StopMusic(this SoundComponent soundComponent) + { + if (!s_MusicSerialId.HasValue) + { + return; + } + + soundComponent.StopSound(s_MusicSerialId.Value, FadeVolumeDuration); + s_MusicSerialId = null; + } + + public static int? PlaySound(this SoundComponent soundComponent, int soundId, EntityBase bindingEntity = null, object userData = null) + { + IDataTable dtSound = GameEntry.DataTable.GetDataTable(); + DRSound drSound = dtSound.GetDataRow(soundId); + if (drSound == null) + { + Log.Warning("Can not load sound '{0}' from data table.", soundId.ToString()); + return null; + } + + PlaySoundParams playSoundParams = PlaySoundParams.Create(); + playSoundParams.Priority = drSound.Priority; + playSoundParams.Loop = drSound.Loop; + playSoundParams.VolumeInSoundGroup = drSound.Volume; + playSoundParams.SpatialBlend = drSound.SpatialBlend; + return soundComponent.PlaySound(AssetUtility.GetSoundAsset(drSound.AssetName), "Sound", Constant.AssetPriority.SoundAsset, playSoundParams, bindingEntity != null ? bindingEntity.Entity : null, userData); + } + + public static int? PlayUISound(this SoundComponent soundComponent, int uiSoundId, object userData = null) + { + IDataTable dtUISound = GameEntry.DataTable.GetDataTable(); + DRUISound drUISound = dtUISound.GetDataRow(uiSoundId); + if (drUISound == null) + { + Log.Warning("Can not load UI sound '{0}' from data table.", uiSoundId.ToString()); + return null; + } + + PlaySoundParams playSoundParams = PlaySoundParams.Create(); + playSoundParams.Priority = drUISound.Priority; + playSoundParams.Loop = false; + playSoundParams.VolumeInSoundGroup = drUISound.Volume; + playSoundParams.SpatialBlend = 0f; + return soundComponent.PlaySound(AssetUtility.GetUISoundAsset(drUISound.AssetName), "UISound", Constant.AssetPriority.UISoundAsset, playSoundParams, userData); + } + + public static bool IsMuted(this SoundComponent soundComponent, string soundGroupName) + { + if (string.IsNullOrEmpty(soundGroupName)) + { + Log.Warning("Sound group is invalid."); + return true; + } + + ISoundGroup soundGroup = soundComponent.GetSoundGroup(soundGroupName); + if (soundGroup == null) + { + Log.Warning("Sound group '{0}' is invalid.", soundGroupName); + return true; + } + + return soundGroup.Mute; + } + + public static void Mute(this SoundComponent soundComponent, string soundGroupName, bool mute) + { + if (string.IsNullOrEmpty(soundGroupName)) + { + Log.Warning("Sound group is invalid."); + return; + } + + ISoundGroup soundGroup = soundComponent.GetSoundGroup(soundGroupName); + if (soundGroup == null) + { + Log.Warning("Sound group '{0}' is invalid.", soundGroupName); + return; + } + + soundGroup.Mute = mute; + + GameEntry.Setting.SetBool(Utility.Text.Format(Constant.Setting.SoundGroupMuted, soundGroupName), mute); + GameEntry.Setting.Save(); + } + + public static float GetVolume(this SoundComponent soundComponent, string soundGroupName) + { + if (string.IsNullOrEmpty(soundGroupName)) + { + Log.Warning("Sound group is invalid."); + return 0f; + } + + ISoundGroup soundGroup = soundComponent.GetSoundGroup(soundGroupName); + if (soundGroup == null) + { + Log.Warning("Sound group '{0}' is invalid.", soundGroupName); + return 0f; + } + + return soundGroup.Volume; + } + + public static void SetVolume(this SoundComponent soundComponent, string soundGroupName, float volume) + { + if (string.IsNullOrEmpty(soundGroupName)) + { + Log.Warning("Sound group is invalid."); + return; + } + + ISoundGroup soundGroup = soundComponent.GetSoundGroup(soundGroupName); + if (soundGroup == null) + { + Log.Warning("Sound group '{0}' is invalid.", soundGroupName); + return; + } + + soundGroup.Volume = volume; + + GameEntry.Setting.SetFloat(Utility.Text.Format(Constant.Setting.SoundGroupVolume, soundGroupName), volume); + GameEntry.Setting.Save(); + } + } +} diff --git a/src-ref/UI/Base/IUIFormController.cs b/src-ref/UI/Base/IUIFormController.cs new file mode 100644 index 0000000..e6ee781 --- /dev/null +++ b/src-ref/UI/Base/IUIFormController.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.UI +{ + public interface IUIFormController + { + int? OpenUI(object userData = null); + void CloseUI(); + void BindUseCase(IUIUseCase useCase); + } +} diff --git a/src-ref/UI/Base/IUIUseCase.cs b/src-ref/UI/Base/IUIUseCase.cs new file mode 100644 index 0000000..ad78f8a --- /dev/null +++ b/src-ref/UI/Base/IUIUseCase.cs @@ -0,0 +1,6 @@ +namespace GeometryTD.UI +{ + public interface IUIUseCase + { + } +} \ No newline at end of file diff --git a/src-ref/UI/Base/UGuiForm.cs b/src-ref/UI/Base/UGuiForm.cs new file mode 100644 index 0000000..c9b8856 --- /dev/null +++ b/src-ref/UI/Base/UGuiForm.cs @@ -0,0 +1,242 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using System.Collections; +using System.Collections.Generic; +using GeometryTD; +using TMPro; +using UnityEngine; +using UnityEngine.UI; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public abstract class UGuiForm : UIFormLogic + { + public const int DepthFactor = 100; + private const float FadeTime = 0.3f; + + private static Font _mainFont = null; + private static TMP_FontAsset _mainTMPFont = null; + private Canvas _cachedCanvas = null; + private CanvasGroup _canvasGroup = null; + private List _cachedCanvasContainer = new List(); + + public int OriginalDepth { get; private set; } + + public int Depth => _cachedCanvas.sortingOrder; + + public void Close() + { + Close(false); + } + + public void Close(bool ignoreFade) + { + StopAllCoroutines(); + + if (ignoreFade) + { + GameEntry.UI.CloseUIForm(this); + } + else + { + StartCoroutine(CloseCo(FadeTime)); + } + } + + public void PlayUISound(int uiSoundId) + { + GameEntry.Sound.PlayUISound(uiSoundId); + } + + public static void SetMainFont(Font mainFont) + { + if (mainFont == null) + { + Log.Error("Main font is invalid."); + return; + } + + _mainFont = mainFont; + } + + public static void SetMainTMPFont(TMP_FontAsset mainTMPFont) + { + if (mainTMPFont == null) + { + Log.Error("Main font is invalid."); + return; + } + + _mainTMPFont = mainTMPFont; + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnInit(object userData) +#else + protected internal override void OnInit(object userData) +#endif + { + base.OnInit(userData); + + _cachedCanvas = gameObject.GetOrAddComponent(); + _cachedCanvas.overrideSorting = true; + OriginalDepth = _cachedCanvas.sortingOrder; + + _canvasGroup = gameObject.GetOrAddComponent(); + + RectTransform rect = GetComponent(); + rect.anchorMin = Vector2.zero; + rect.anchorMax = Vector2.one; + rect.anchoredPosition = Vector2.zero; + rect.sizeDelta = Vector2.zero; + + gameObject.GetOrAddComponent(); + + if (_mainTMPFont == null) + { + Log.Warning("Main TMP font isn't set."); + } + else + { + var tmp_texts = GetComponentsInChildren(true); + foreach (var text in tmp_texts) + { + text.font = _mainTMPFont; + } + } + + if (_mainFont == null) + { + Log.Warning("Main font isn't set."); + } + else + { + var texts = GetComponentsInChildren(true); + foreach (var text in texts) + { + text.font = _mainFont; + } + } + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnRecycle() +#else + protected internal override void OnRecycle() +#endif + { + base.OnRecycle(); + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnOpen(object userData) +#else + protected internal override void OnOpen(object userData) +#endif + { + base.OnOpen(userData); + + _canvasGroup.alpha = 0f; + StopAllCoroutines(); + StartCoroutine(_canvasGroup.FadeToAlpha(1f, FadeTime)); + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnClose(bool isShutdown, object userData) +#else + protected internal override void OnClose(bool isShutdown, object userData) +#endif + { + base.OnClose(isShutdown, userData); + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnPause() +#else + protected internal override void OnPause() +#endif + { + base.OnPause(); + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnResume() +#else + protected internal override void OnResume() +#endif + { + base.OnResume(); + + _canvasGroup.alpha = 0f; + StopAllCoroutines(); + StartCoroutine(_canvasGroup.FadeToAlpha(1f, FadeTime)); + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnCover() +#else + protected internal override void OnCover() +#endif + { + base.OnCover(); + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnReveal() +#else + protected internal override void OnReveal() +#endif + { + base.OnReveal(); + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnRefocus(object userData) +#else + protected internal override void OnRefocus(object userData) +#endif + { + base.OnRefocus(userData); + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnUpdate(float elapseSeconds, float realElapseSeconds) +#else + protected internal override void OnUpdate(float elapseSeconds, float realElapseSeconds) +#endif + { + base.OnUpdate(elapseSeconds, realElapseSeconds); + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnDepthChanged(int uiGroupDepth, int depthInUIGroup) +#else + protected internal override void OnDepthChanged(int uiGroupDepth, int depthInUIGroup) +#endif + { + int oldDepth = Depth; + base.OnDepthChanged(uiGroupDepth, depthInUIGroup); + int deltaDepth = UGuiGroupHelper.DepthFactor * uiGroupDepth + DepthFactor * depthInUIGroup - oldDepth + + OriginalDepth; + GetComponentsInChildren(true, _cachedCanvasContainer); + for (int i = 0; i < _cachedCanvasContainer.Count; i++) + { + _cachedCanvasContainer[i].sortingOrder += deltaDepth; + } + + _cachedCanvasContainer.Clear(); + } + + private IEnumerator CloseCo(float duration) + { + yield return _canvasGroup.FadeToAlpha(0f, duration); + GameEntry.UI.CloseUIForm(this); + } + } +} \ No newline at end of file diff --git a/src-ref/UI/Base/UGuiGroupHelper.cs b/src-ref/UI/Base/UGuiGroupHelper.cs new file mode 100644 index 0000000..340a4f2 --- /dev/null +++ b/src-ref/UI/Base/UGuiGroupHelper.cs @@ -0,0 +1,53 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using UnityEngine; +using UnityEngine.UI; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + /// + /// uGUI 界面组辅助器。 + /// + public class UGuiGroupHelper : UIGroupHelperBase + { + public const int DepthFactor = 10000; + + private int m_Depth = 0; + private Canvas m_CachedCanvas = null; + + /// + /// 设置界面组深度。 + /// + /// 界面组深度。 + public override void SetDepth(int depth) + { + m_Depth = depth; + m_CachedCanvas.overrideSorting = true; + m_CachedCanvas.sortingOrder = DepthFactor * depth; + } + + private void Awake() + { + m_CachedCanvas = gameObject.GetOrAddComponent(); + gameObject.GetOrAddComponent(); + } + + private void Start() + { + m_CachedCanvas.overrideSorting = true; + m_CachedCanvas.sortingOrder = DepthFactor * m_Depth; + + RectTransform transform = GetComponent(); + transform.anchorMin = Vector2.zero; + transform.anchorMax = Vector2.one; + transform.anchoredPosition = Vector2.zero; + transform.sizeDelta = Vector2.zero; + } + } +} diff --git a/src-ref/UI/Base/UIContext.cs b/src-ref/UI/Base/UIContext.cs new file mode 100644 index 0000000..8461da3 --- /dev/null +++ b/src-ref/UI/Base/UIContext.cs @@ -0,0 +1,6 @@ +namespace GeometryTD.UI +{ + public class UIContext + { + } +} \ No newline at end of file diff --git a/src-ref/UI/Base/UIExtension.cs b/src-ref/UI/Base/UIExtension.cs new file mode 100644 index 0000000..192d01b --- /dev/null +++ b/src-ref/UI/Base/UIExtension.cs @@ -0,0 +1,182 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using GameFramework.DataTable; +using GameFramework.UI; +using System.Collections; +using GeometryTD.Procedure; +using GeometryTD; +using GeometryTD.CustomUtility; +using GeometryTD.DataTable; +using GeometryTD.Definition; +using UnityEngine; +using UnityEngine.UI; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public static class UIExtension + { + public static IEnumerator FadeToAlpha(this CanvasGroup canvasGroup, float alpha, float duration) + { + float time = 0f; + float originalAlpha = canvasGroup.alpha; + while (time < duration) + { + time += Time.deltaTime; + canvasGroup.alpha = Mathf.Lerp(originalAlpha, alpha, time / duration); + yield return new WaitForEndOfFrame(); + } + + canvasGroup.alpha = alpha; + } + + public static IEnumerator SmoothValue(this Slider slider, float value, float duration) + { + float time = 0f; + float originalValue = slider.value; + while (time < duration) + { + time += Time.deltaTime; + slider.value = Mathf.Lerp(originalValue, value, time / duration); + yield return new WaitForEndOfFrame(); + } + + slider.value = value; + } + + public static bool HasUIForm(this UIComponent uiComponent, UIFormType uiFormId, string uiGroupName = null) + { + return uiComponent.HasUIForm((int)uiFormId, uiGroupName); + } + + public static bool HasUIForm(this UIComponent uiComponent, int uiFormId, string uiGroupName = null) + { + IDataTable dtUIForm = GameEntry.DataTable.GetDataTable(); + DRUIForm drUIForm = dtUIForm.GetDataRow(uiFormId); + if (drUIForm == null) + { + return false; + } + + string assetName = AssetUtility.GetUIFormAsset(drUIForm.AssetName); + if (string.IsNullOrEmpty(uiGroupName)) + { + return uiComponent.HasUIForm(assetName); + } + + IUIGroup uiGroup = uiComponent.GetUIGroup(uiGroupName); + if (uiGroup == null) + { + return false; + } + + return uiGroup.HasUIForm(assetName); + } + + public static UGuiForm GetUIForm(this UIComponent uiComponent, UIFormType uiFormId, string uiGroupName = null) + { + return uiComponent.GetUIForm((int)uiFormId, uiGroupName); + } + + public static UGuiForm GetUIForm(this UIComponent uiComponent, int uiFormId, string uiGroupName = null) + { + IDataTable dtUIForm = GameEntry.DataTable.GetDataTable(); + DRUIForm drUIForm = dtUIForm.GetDataRow(uiFormId); + if (drUIForm == null) + { + return null; + } + + string assetName = AssetUtility.GetUIFormAsset(drUIForm.AssetName); + UIForm uiForm = null; + if (string.IsNullOrEmpty(uiGroupName)) + { + uiForm = uiComponent.GetUIForm(assetName); + if (uiForm == null) + { + return null; + } + + return (UGuiForm)uiForm.Logic; + } + + IUIGroup uiGroup = uiComponent.GetUIGroup(uiGroupName); + if (uiGroup == null) + { + return null; + } + + uiForm = (UIForm)uiGroup.GetUIForm(assetName); + if (uiForm == null) + { + return null; + } + + return (UGuiForm)uiForm.Logic; + } + + public static void CloseUIForm(this UIComponent uiComponent, UGuiForm uiForm) + { + uiComponent.CloseUIForm(uiForm.UIForm); + } + + public static int? OpenUIForm(this UIComponent uiComponent, UIFormType uiFormId, object userData = null) + { + return uiComponent.OpenUIForm((int)uiFormId, userData); + } + + public static int? OpenUIForm(this UIComponent uiComponent, int uiFormId, object userData = null) + { + IDataTable dtUIForm = GameEntry.DataTable.GetDataTable(); + DRUIForm drUIForm = dtUIForm.GetDataRow(uiFormId); + if (drUIForm == null) + { + Log.Warning("Can not load UI form '{0}' from data table.", uiFormId.ToString()); + return null; + } + + string assetName = AssetUtility.GetUIFormAsset(drUIForm.AssetName); + if (!drUIForm.AllowMultiInstance) + { + if (uiComponent.IsLoadingUIForm(assetName)) + { + return null; + } + + if (uiComponent.HasUIForm(assetName)) + { + return null; + } + } + + return uiComponent.OpenUIForm(assetName, drUIForm.UIGroupName, Constant.AssetPriority.UIFormAsset, + drUIForm.PauseCoveredUIForm, userData); + } + + public static void OpenDialog(this UIComponent uiComponent, DialogParams dialogParams) + { + if (((ProcedureBase)GameEntry.Procedure.CurrentProcedure).UseNativeDialog) + { + OpenNativeDialog(dialogParams); + } + else + { + uiComponent.OpenUIForm(UIFormType.DialogForm, dialogParams); + } + } + + private static void OpenNativeDialog(DialogParams dialogParams) + { + // TODO:这里应该弹出原生对话框,先简化实现为直接按确认按钮 + if (dialogParams.OnClickConfirm != null) + { + dialogParams.OnClickConfirm(dialogParams.UserData); + } + } + } +} \ No newline at end of file diff --git a/src-ref/UI/Base/UIFormControllerBase.cs b/src-ref/UI/Base/UIFormControllerBase.cs new file mode 100644 index 0000000..f75a063 --- /dev/null +++ b/src-ref/UI/Base/UIFormControllerBase.cs @@ -0,0 +1,10 @@ +namespace GeometryTD.UI +{ + public abstract class UIFormControllerBase : IUIFormController where TContext : UIContext + { + protected abstract int? OpenUIInternal(TContext context); + public abstract int? OpenUI(object userData = null); + public abstract void CloseUI(); + public abstract void BindUseCase(IUIUseCase useCase); + } +} diff --git a/src-ref/UI/Base/UIFormControllerCommonBase.cs b/src-ref/UI/Base/UIFormControllerCommonBase.cs new file mode 100644 index 0000000..442fb4d --- /dev/null +++ b/src-ref/UI/Base/UIFormControllerCommonBase.cs @@ -0,0 +1,183 @@ +using GeometryTD.Definition; +using GameFramework.Event; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public abstract class UIFormControllerCommonBase : UIFormControllerBase + where TContext : UIContext + where TForm : UGuiForm + { + private TContext _context; + private TForm _form; + private int? _formSerialId; + private bool _pendingRefresh; + private bool _isBindEvent; + + protected TContext Context => _context; + + protected TForm Form => _form; + + protected int? FormSerialId => _formSerialId; + + protected abstract UIFormType UIFormTypeId { get; } + + protected abstract void RefreshUI(TForm form, TContext context); + + protected virtual void SubscribeCustomEvents() + { + } + + protected virtual void UnsubscribeCustomEvents() + { + } + + protected virtual void CloseLoadedFormDirect(TForm form) + { + form.Close(); + } + + protected void SetContext(TContext context) + { + _context = context; + } + + protected void RefreshCurrentUI() + { + if (_context == null) + { + return; + } + + if (_form == null) + { + _pendingRefresh = true; + return; + } + + RefreshUI(_form, _context); + _pendingRefresh = false; + } + + protected override int? OpenUIInternal(TContext context) + { + if (context == null) + { + Log.Warning("{0}.OpenUI() context is null.", GetType().Name); + return null; + } + + _context = context; + + if (_form != null && _formSerialId.HasValue && GameEntry.UI.HasUIForm(_formSerialId.Value)) + { + RefreshUI(_form, _context); + return _formSerialId; + } + + CloseUI(); + _pendingRefresh = true; + SubscribeEvents(); + _formSerialId = GameEntry.UI.OpenUIForm(UIFormTypeId, _context); + return _formSerialId; + } + + public override void CloseUI() + { + _pendingRefresh = false; + UnsubscribeEvents(); + + if (_formSerialId.HasValue) + { + if (GameEntry.UI.HasUIForm(_formSerialId.Value)) + { + GameEntry.UI.CloseUIForm(_formSerialId.Value); + } + + _form = null; + _formSerialId = null; + return; + } + + if (_form != null) + { + CloseLoadedFormDirect(_form); + _form = null; + } + } + + private void SubscribeEvents() + { + if (_isBindEvent) + { + return; + } + + GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess); + GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete); + SubscribeCustomEvents(); + + _isBindEvent = true; + } + + private void UnsubscribeEvents() + { + if (!_isBindEvent) + { + return; + } + + GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess); + GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete); + UnsubscribeCustomEvents(); + + _isBindEvent = false; + } + + private void OpenUIFormSuccess(object sender, GameEventArgs e) + { + if (!(e is OpenUIFormSuccessEventArgs args)) + { + return; + } + + if (!_formSerialId.HasValue) + { + return; + } + + if (args.UIForm == null || args.UIForm.SerialId != _formSerialId.Value || args.UserData != _context) + { + return; + } + + _form = args.UIForm.Logic as TForm; + if (_form == null) + { + Log.Warning("{0} open success but form logic is invalid.", GetType().Name); + return; + } + + if (_pendingRefresh) + { + RefreshCurrentUI(); + } + } + + private void CloseUIFormComplete(object sender, GameEventArgs e) + { + if (!(e is CloseUIFormCompleteEventArgs args)) + { + return; + } + + if (args.SerialId != _formSerialId) + { + return; + } + + _form = null; + _formSerialId = null; + } + } +} diff --git a/src-ref/UI/Combat/Context/CombatFinishFormContext.cs b/src-ref/UI/Combat/Context/CombatFinishFormContext.cs new file mode 100644 index 0000000..3a7046f --- /dev/null +++ b/src-ref/UI/Combat/Context/CombatFinishFormContext.cs @@ -0,0 +1,10 @@ +namespace GeometryTD.UI +{ + public class CombatFinishFormContext : UIContext + { + public string EnemyKilledText; + public string GoldGainedText; + public RepoItemContext[] RewardItems; + public bool CanReturn; + } +} diff --git a/src-ref/UI/Combat/Context/CombatInfoFormContext.cs b/src-ref/UI/Combat/Context/CombatInfoFormContext.cs new file mode 100644 index 0000000..7d071a4 --- /dev/null +++ b/src-ref/UI/Combat/Context/CombatInfoFormContext.cs @@ -0,0 +1,13 @@ +namespace GeometryTD.UI +{ + public class CombatInfoFormContext : UIContext + { + public string LevelMetaText; + public string LevelPhaseText; + public string CoinText; + public string BaseHpText; + public string EnemyHpRateText; + public bool CanPause; + public bool CanEnd; + } +} diff --git a/src-ref/UI/Combat/Context/CombatSelectActionType.cs b/src-ref/UI/Combat/Context/CombatSelectActionType.cs new file mode 100644 index 0000000..af0372c --- /dev/null +++ b/src-ref/UI/Combat/Context/CombatSelectActionType.cs @@ -0,0 +1,10 @@ +namespace GeometryTD.UI +{ + public enum CombatSelectActionType : byte + { + None = 0, + BuildTower = 1, + UpgradeTower = 2, + DestroyTower = 3 + } +} diff --git a/src-ref/UI/Combat/Context/CombatSelectFormContext.cs b/src-ref/UI/Combat/Context/CombatSelectFormContext.cs new file mode 100644 index 0000000..800945f --- /dev/null +++ b/src-ref/UI/Combat/Context/CombatSelectFormContext.cs @@ -0,0 +1,15 @@ +using UnityEngine; + +namespace GeometryTD.UI +{ + public class CombatSelectFormContext : UIContext + { + public bool IsVisible; + public Vector2 ScreenPosition; + public bool ShowBuildArea; + public bool ShowFuncArea; + public TowerSelectItemContext[] BuildItems; + public TowerSelectItemContext UpgradeItem; + public TowerSelectItemContext DestroyItem; + } +} diff --git a/src-ref/UI/Combat/Context/TowerSelectItemContext.cs b/src-ref/UI/Combat/Context/TowerSelectItemContext.cs new file mode 100644 index 0000000..f6e5d47 --- /dev/null +++ b/src-ref/UI/Combat/Context/TowerSelectItemContext.cs @@ -0,0 +1,15 @@ +using UnityEngine; + +namespace GeometryTD.UI +{ + public class TowerSelectItemContext : UIContext + { + public Sprite Icon; + public string PriceText; + public bool IsVisible; + public bool IsInteractable; + public CombatSelectActionType ActionType; + public int ActionIndex; + public TowerIconAreaContext TowerIconAreaContext; + } +} diff --git a/src-ref/UI/Combat/ContextBuilder/CombatFinishFormController.ContextBuilder.cs b/src-ref/UI/Combat/ContextBuilder/CombatFinishFormController.ContextBuilder.cs new file mode 100644 index 0000000..f925c2b --- /dev/null +++ b/src-ref/UI/Combat/ContextBuilder/CombatFinishFormController.ContextBuilder.cs @@ -0,0 +1,269 @@ +using System.Collections.Generic; +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.UI +{ + public partial class CombatFinishFormController + { + private CombatFinishFormContext BuildContext(CombatFinishFormRawData rawData) + { + _itemDescSeedMap.Clear(); + if (rawData == null) + { + return null; + } + + return new CombatFinishFormContext + { + EnemyKilledText = rawData.DefeatedEnemyCount.ToString(), + GoldGainedText = rawData.GainedGold.ToString(), + RewardItems = BuildRewardItems(rawData.RewardInventory), + CanReturn = rawData.CanReturn + }; + } + + private RepoItemContext[] BuildRewardItems(BackpackInventoryData inventory) + { + if (inventory == null) + { + return System.Array.Empty(); + } + + Dictionary muzzleMap = BuildComponentMap(inventory.MuzzleComponents); + Dictionary bearingMap = BuildComponentMap(inventory.BearingComponents); + Dictionary baseMap = BuildComponentMap(inventory.BaseComponents); + List itemContexts = new List(); + + if (inventory.Towers != null) + { + foreach (TowerItemData tower in inventory.Towers) + { + if (tower == null) + { + continue; + } + + itemContexts.Add(new RepoItemContext + { + InstanceId = tower.InstanceId, + CanDrag = false, + EnduranceRate01 = ItemDescUtility.ResolveTowerEnduranceRate(tower, muzzleMap, bearingMap, baseMap), + ClickActionType = RepoItemClickActionType.OpenDetail, + ComponentSlotType = TowerCompSlotType.None, + IconAreaContext = BuildTowerIconContext(tower, muzzleMap, bearingMap, baseMap) + }); + + AddItemDescSeed( + tower.InstanceId, + tower.Name, + "Tower", + ItemDescUtility.BuildTowerDesc(tower, muzzleMap, bearingMap, baseMap) ?? string.Empty, + tower.Stats?.Tags, + tower.Stats?.TagRuntimes); + } + } + + if (inventory.MuzzleComponents != null) + { + foreach (MuzzleCompItemData item in inventory.MuzzleComponents) + { + if (item == null) + { + continue; + } + + itemContexts.Add(new RepoItemContext + { + InstanceId = item.InstanceId, + CanDrag = false, + EnduranceRate01 = ItemDescUtility.ResolveComponentEnduranceRate(item), + ClickActionType = RepoItemClickActionType.OpenDetail, + ComponentSlotType = TowerCompSlotType.Muzzle, + IconAreaContext = BuildIconAreaContext(item) + }); + + AddItemDescSeed( + item.InstanceId, + item.Name, + BuildComponentTypeText(item.SlotType), + ItemDescUtility.BuildMuzzleDesc(item) ?? string.Empty, + item.Tags, + null); + } + } + + if (inventory.BearingComponents != null) + { + foreach (BearingCompItemData item in inventory.BearingComponents) + { + if (item == null) + { + continue; + } + + itemContexts.Add(new RepoItemContext + { + InstanceId = item.InstanceId, + CanDrag = false, + EnduranceRate01 = ItemDescUtility.ResolveComponentEnduranceRate(item), + ClickActionType = RepoItemClickActionType.OpenDetail, + ComponentSlotType = TowerCompSlotType.Bearing, + IconAreaContext = BuildIconAreaContext(item) + }); + + AddItemDescSeed( + item.InstanceId, + item.Name, + BuildComponentTypeText(item.SlotType), + ItemDescUtility.BuildBearingDesc(item) ?? string.Empty, + item.Tags, + null); + } + } + + if (inventory.BaseComponents != null) + { + foreach (BaseCompItemData item in inventory.BaseComponents) + { + if (item == null) + { + continue; + } + + itemContexts.Add(new RepoItemContext + { + InstanceId = item.InstanceId, + CanDrag = false, + EnduranceRate01 = ItemDescUtility.ResolveComponentEnduranceRate(item), + ClickActionType = RepoItemClickActionType.OpenDetail, + ComponentSlotType = TowerCompSlotType.Base, + IconAreaContext = BuildIconAreaContext(item) + }); + + AddItemDescSeed( + item.InstanceId, + item.Name, + BuildComponentTypeText(item.SlotType), + ItemDescUtility.BuildBaseDesc(item) ?? string.Empty, + item.Tags, + null); + } + } + + return itemContexts.ToArray(); + } + + private static TowerIconAreaContext BuildTowerIconContext( + TowerItemData tower, + IReadOnlyDictionary muzzleMap, + IReadOnlyDictionary bearingMap, + IReadOnlyDictionary baseMap) + { + if (tower == null) + { + return null; + } + + return new TowerIconAreaContext + { + Rarity = tower.Rarity, + MuzzleColor = ResolveComponentColor(tower.MuzzleComponentInstanceId, muzzleMap), + BearingColor = ResolveComponentColor(tower.BearingComponentInstanceId, bearingMap), + BaseColor = ResolveComponentColor(tower.BaseComponentInstanceId, baseMap) + }; + } + + private static Color ResolveComponentColor(long instanceId, IReadOnlyDictionary componentMap) + where TComp : TowerCompItemData + { + if (instanceId > 0 && componentMap != null && componentMap.TryGetValue(instanceId, out TComp comp) && + comp != null) + { + return IconColorGenerator.GenerateForComponent(comp); + } + + return Color.white; + } + + private void AddItemDescSeed( + long itemId, + string title, + string typeText, + string description, + TagType[] tags, + TagRuntimeData[] tagRuntimes) + { + if (itemId <= 0) + { + return; + } + + _itemDescSeedMap[itemId] = new ItemDescSeed + { + Title = string.IsNullOrWhiteSpace(title) ? $"Item {itemId}" : title, + TypeText = typeText ?? string.Empty, + Description = description ?? string.Empty, + Tags = tags, + TagRuntimes = tagRuntimes + }; + } + + private static string BuildComponentTypeText(TowerCompSlotType slotType) + { + return slotType switch + { + TowerCompSlotType.Muzzle => "Muzzle Component", + TowerCompSlotType.Bearing => "Bearing Component", + TowerCompSlotType.Base => "Base Component", + TowerCompSlotType.Accessory => "Accessory", + _ => "Component" + }; + } + + private static Dictionary BuildComponentMap(IReadOnlyList items) + where TComp : TowerCompItemData + { + Dictionary map = new Dictionary(); + if (items == null) + { + return map; + } + + foreach (TComp item in items) + { + if (item == null || item.InstanceId <= 0) + { + continue; + } + + map[item.InstanceId] = item; + } + + return map; + } + + private static IconAreaContext BuildIconAreaContext(TowerCompItemData item) + { + if (item == null) + { + return new IconAreaContext + { + ComponentSlotType = TowerCompSlotType.None, + Rarity = RarityType.None, + Color = Color.white, + Icon = null + }; + } + + return new IconAreaContext + { + ComponentSlotType = item.SlotType, + Rarity = item.Rarity, + Color = IconColorGenerator.GenerateForComponent(item), + Icon = null + }; + } + } +} diff --git a/src-ref/UI/Combat/ContextBuilder/CombatInfoFormController.ContextBuilder.cs b/src-ref/UI/Combat/ContextBuilder/CombatInfoFormController.ContextBuilder.cs new file mode 100644 index 0000000..af3b8cf --- /dev/null +++ b/src-ref/UI/Combat/ContextBuilder/CombatInfoFormController.ContextBuilder.cs @@ -0,0 +1,76 @@ +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.UI +{ + public partial class CombatInfoFormController + { + private static CombatInfoFormContext BuildContext(CombatInfoFormRawData rawData) + { + if (rawData == null) + { + return null; + } + + return new CombatInfoFormContext + { + LevelMetaText = BuildLevelMetaText(rawData.LevelThemeType, rawData.LevelId), + LevelPhaseText = BuildPhaseText(rawData.CurrentPhaseIndex, rawData.TotalPhaseCount), + CoinText = BuildCoinText(rawData.Coin), + BaseHpText = BuildBaseHpText(rawData.BaseHp, rawData.BaseHpMax), + EnemyHpRateText = BuildEnemyHpRateText(rawData.EnemyHpRateMultiplier), + CanPause = rawData.CanPause, + CanEnd = rawData.CanEnd + }; + } + + private static string BuildLevelMetaText(LevelThemeType themeType, int levelId) + { + if (levelId <= 0) + { + return "Map -"; + } + + return $"{themeType} Map.{levelId}"; + } + + private static string BuildPhaseText(int currentPhaseIndex, int totalPhaseCount) + { + int phaseIndex = currentPhaseIndex < 0 ? 0 : currentPhaseIndex; + if (totalPhaseCount <= 0) + { + return $"{phaseIndex}/?"; + } + + return $"Wave: {phaseIndex}/{totalPhaseCount}"; + } + + private static string BuildCoinText(int coin) + { + if (coin < 0) + { + return "Coin: -"; + } + + return $"Coin: {coin}"; + } + + private static string BuildBaseHpText(int baseHp, int baseHpMax) + { + if (baseHpMax <= 0) + { + return "\u57FA\u5730\uFF1A-"; + } + + int clampedHp = Mathf.Clamp(baseHp, 0, baseHpMax); + int percent = Mathf.RoundToInt((float)clampedHp / baseHpMax * 100f); + return $"\u57FA\u5730\uFF1A{percent}%"; + } + + private static string BuildEnemyHpRateText(int enemyHpRateMultiplier) + { + int resolvedMultiplier = enemyHpRateMultiplier > 0 ? enemyHpRateMultiplier : 1; + return $"\u96BE\u5EA6\uFF1A{resolvedMultiplier}x"; + } + } +} diff --git a/src-ref/UI/Combat/ContextBuilder/CombatSelectFormController.ContextBuilder.cs b/src-ref/UI/Combat/ContextBuilder/CombatSelectFormController.ContextBuilder.cs new file mode 100644 index 0000000..78c5ae4 --- /dev/null +++ b/src-ref/UI/Combat/ContextBuilder/CombatSelectFormController.ContextBuilder.cs @@ -0,0 +1,77 @@ +using UnityEngine; + +namespace GeometryTD.UI +{ + public partial class CombatSelectFormController + { + private static CombatSelectFormContext BuildContext(CombatSelectFormRawData rawData) + { + if (rawData == null) + { + return null; + } + + return new CombatSelectFormContext + { + IsVisible = rawData.IsVisible, + ScreenPosition = rawData.ScreenPosition, + ShowBuildArea = rawData.IsVisible && rawData.DisplayMode == CombatSelectDisplayMode.Build, + ShowFuncArea = rawData.IsVisible && rawData.DisplayMode == CombatSelectDisplayMode.Func, + BuildItems = BuildItemContexts(rawData.BuildItems), + UpgradeItem = BuildItemContext(rawData.UpgradeItem), + DestroyItem = BuildItemContext(rawData.DestroyItem) + }; + } + + private static TowerSelectItemContext[] BuildItemContexts(TowerSelectItemRawData[] itemRawData) + { + if (itemRawData == null || itemRawData.Length <= 0) + { + return System.Array.Empty(); + } + + TowerSelectItemContext[] contexts = new TowerSelectItemContext[itemRawData.Length]; + for (int i = 0; i < itemRawData.Length; i++) + { + contexts[i] = BuildItemContext(itemRawData[i]); + } + + return contexts; + } + + private static TowerSelectItemContext BuildItemContext(TowerSelectItemRawData rawData) + { + if (rawData == null) + { + return null; + } + + return new TowerSelectItemContext + { + Icon = rawData.Icon, + PriceText = BuildPriceText(rawData.Price, rawData.IsGain), + IsVisible = rawData.IsVisible, + IsInteractable = rawData.IsInteractable, + ActionType = rawData.ActionType, + ActionIndex = rawData.ActionIndex, + TowerIconAreaContext = new TowerIconAreaContext + { + BaseColor = rawData.BaseColor, + BearingColor = rawData.BearingColor, + MuzzleColor = rawData.MuzzleColor + } + }; + } + + private static string BuildPriceText(int price, bool isGain) + { + int positivePrice = Mathf.Max(0, price); + if (isGain) + { + return $"+{positivePrice}"; + } + + return positivePrice.ToString(); + } + } +} diff --git a/src-ref/UI/Combat/Controller/CombatFinishFormController.cs b/src-ref/UI/Combat/Controller/CombatFinishFormController.cs new file mode 100644 index 0000000..ab25580 --- /dev/null +++ b/src-ref/UI/Combat/Controller/CombatFinishFormController.cs @@ -0,0 +1,152 @@ +using System.Collections.Generic; +using GameFramework.Event; +using GeometryTD.CustomEvent; +using GeometryTD.Definition; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public partial class CombatFinishFormController : UIFormControllerCommonBase + { + private CombatFinishFormUseCase _useCase; + private readonly Dictionary _itemDescSeedMap = new Dictionary(); + + private sealed class ItemDescSeed + { + public string Title; + public string TypeText; + public string Description; + public TagType[] Tags; + public TagRuntimeData[] TagRuntimes; + } + + protected override UIFormType UIFormTypeId => UIFormType.CombatFinishForm; + + protected override void RefreshUI(CombatFinishForm form, CombatFinishFormContext context) + { + form.RefreshUI(context); + } + + protected override void SubscribeCustomEvents() + { + GameEntry.Event.Subscribe(CombatFinishReturnEventArgs.EventId, OnCombatFinishReturnButtonClicked); + GameEntry.Event.Subscribe(RepoItemClickedEventArgs.EventId, OnRepoItemClicked); + } + + protected override void UnsubscribeCustomEvents() + { + GameEntry.Event.Unsubscribe(CombatFinishReturnEventArgs.EventId, OnCombatFinishReturnButtonClicked); + GameEntry.Event.Unsubscribe(RepoItemClickedEventArgs.EventId, OnRepoItemClicked); + } + + public override int? OpenUI(object userData = null) + { + if (userData is CombatFinishFormContext context) + { + return OpenUIInternal(context); + } + + if (userData is CombatFinishFormRawData rawDataFromUserData) + { + return OpenUI(rawDataFromUserData); + } + + if (userData != null) + { + Log.Warning("CombatFinishFormController.OpenUI() userData type is invalid."); + return null; + } + + if (_useCase == null) + { + Log.Error("CombatFinishFormController.OpenUI() useCase is null."); + return null; + } + + CombatFinishFormRawData rawData = _useCase.CreateInitialModel(); + return OpenUI(rawData); + } + + public int? OpenUI(CombatFinishFormRawData rawData) + { + CombatFinishFormContext context = BuildContext(rawData); + return OpenUIInternal(context); + } + + public override void BindUseCase(IUIUseCase useCase) + { + if (!(useCase is CombatFinishFormUseCase combatFinishFormUseCase)) + { + Log.Error("CombatFinishFormController.BindUseCase() useCase is invalid."); + return; + } + + _useCase = combatFinishFormUseCase; + } + + #region Event Handlers + + private void OnCombatFinishReturnButtonClicked(object sender, GameEventArgs e) + { + if (!IsEventFromCurrentForm(sender) || !(e is CombatFinishReturnEventArgs)) + { + return; + } + + _useCase?.TryReturnToMenu(); + } + + private void OnRepoItemClicked(object sender, GameEventArgs e) + { + if (!IsEventFromCurrentForm(sender)) + { + return; + } + + if (!(e is RepoItemClickedEventArgs args)) + { + return; + } + + if (!_itemDescSeedMap.TryGetValue(args.ItemId, out ItemDescSeed seed)) + { + return; + } + + GameEntry.UIRouter.OpenUI(UIFormType.ItemDescForm, new ItemDescFormRawData + { + Title = seed.Title, + TypeText = seed.TypeText, + Description = seed.Description ?? string.Empty, + Price = 0, + ScreenPosition = args.ScreenPosition, + Tags = seed.Tags, + TagRuntimes = seed.TagRuntimes + }); + } + + #endregion + + private bool IsEventFromCurrentForm(object sender) + { + if (Form == null) + { + return false; + } + + if (ReferenceEquals(sender, Form)) + { + return true; + } + + if (sender is Component component) + { + CombatFinishForm ownerForm = component.GetComponentInParent(); + return ownerForm == Form; + } + + return false; + } + } +} diff --git a/src-ref/UI/Combat/Controller/CombatInfoFormController.cs b/src-ref/UI/Combat/Controller/CombatInfoFormController.cs new file mode 100644 index 0000000..eea0a9e --- /dev/null +++ b/src-ref/UI/Combat/Controller/CombatInfoFormController.cs @@ -0,0 +1,181 @@ +using GeometryTD.CustomEvent; +using GeometryTD.Definition; +using GameFramework.Event; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public partial class CombatInfoFormController : UIFormControllerCommonBase + { + private CombatInfoFormUseCase _useCase; + + protected override UIFormType UIFormTypeId => UIFormType.CombatInfoForm; + + protected override void RefreshUI(CombatInfoForm form, CombatInfoFormContext context) + { + form.RefreshUI(context); + } + + protected override void SubscribeCustomEvents() + { + GameEntry.Event.Subscribe(CombatPauseEventArgs.EventId, OnCombatPauseButtonClicked); + GameEntry.Event.Subscribe(CombatEndEventArgs.EventId, OnCombatEndButtonClicked); + GameEntry.Event.Subscribe(CombatDebugFailEventArgs.EventId, OnCombatDebugFailButtonClicked); + GameEntry.Event.Subscribe(CombatProcessEventArgs.EventId, OnCombatProcessChanged); + GameEntry.Event.Subscribe(CombatCoinChangedEventArgs.EventId, OnCombatCoinChanged); + GameEntry.Event.Subscribe(CombatBaseHpChangedEventArgs.EventId, OnCombatBaseHpChanged); + GameEntry.Event.Subscribe(CombatEnemyHpRateChangedEventArgs.EventId, OnCombatEnemyHpRateChanged); + } + + protected override void UnsubscribeCustomEvents() + { + GameEntry.Event.Unsubscribe(CombatPauseEventArgs.EventId, OnCombatPauseButtonClicked); + GameEntry.Event.Unsubscribe(CombatEndEventArgs.EventId, OnCombatEndButtonClicked); + GameEntry.Event.Unsubscribe(CombatDebugFailEventArgs.EventId, OnCombatDebugFailButtonClicked); + GameEntry.Event.Unsubscribe(CombatProcessEventArgs.EventId, OnCombatProcessChanged); + GameEntry.Event.Unsubscribe(CombatCoinChangedEventArgs.EventId, OnCombatCoinChanged); + GameEntry.Event.Unsubscribe(CombatBaseHpChangedEventArgs.EventId, OnCombatBaseHpChanged); + GameEntry.Event.Unsubscribe(CombatEnemyHpRateChangedEventArgs.EventId, OnCombatEnemyHpRateChanged); + } + + public int? OpenUI(CombatInfoFormRawData rawData) + { + CombatInfoFormContext context = BuildContext(rawData); + return OpenUIInternal(context); + } + + public override int? OpenUI(object userData = null) + { + if (userData is CombatInfoFormContext context) + { + return OpenUIInternal(context); + } + + if (userData is CombatInfoFormRawData rawDataFromUserData) + { + return OpenUI(rawDataFromUserData); + } + + if (userData != null) + { + Log.Warning("CombatInfoFormController.OpenUI() userData type is invalid."); + return null; + } + + if (_useCase == null) + { + Log.Error("CombatInfoFormController.OpenUI() useCase is null."); + return null; + } + + CombatInfoFormRawData rawData = _useCase.CreateInitialModel(); + return OpenUI(rawData); + } + + public override void BindUseCase(IUIUseCase useCase) + { + if (!(useCase is CombatInfoFormUseCase combatInfoFormUseCase)) + { + Log.Error("CombatInfoFormController.BindUseCase() useCase is invalid."); + return; + } + + _useCase = combatInfoFormUseCase; + } + + private void OnCombatPauseButtonClicked(object sender, GameEventArgs e) + { + if (!(sender is CombatInfoForm) || !(e is CombatPauseEventArgs)) + { + return; + } + + GameEntry.UIRouter.OpenUI(UIFormType.DialogForm, new DialogFormRawData + { + Mode = 1, + Title = "Pause", + Message = "Game paused.", + PauseGame = true, + ConfirmText = "Continue" + }); + } + + private void OnCombatEndButtonClicked(object sender, GameEventArgs e) + { + if (!(sender is CombatInfoForm) || !(e is CombatEndEventArgs)) + { + return; + } + + _useCase?.TryEndCombat(); + } + + private void OnCombatDebugFailButtonClicked(object sender, GameEventArgs e) + { + if (!(sender is CombatInfoForm) || !(e is CombatDebugFailEventArgs)) + { + return; + } + + _useCase?.TryDebugFail(); + } + + private void OnCombatProcessChanged(object sender, GameEventArgs e) + { + if (!(e is CombatProcessEventArgs)) + { + return; + } + + RefreshFromUseCase(); + } + + private void OnCombatCoinChanged(object sender, GameEventArgs e) + { + if (!(e is CombatCoinChangedEventArgs)) + { + return; + } + + RefreshFromUseCase(); + } + + private void OnCombatBaseHpChanged(object sender, GameEventArgs e) + { + if (!(e is CombatBaseHpChangedEventArgs)) + { + return; + } + + RefreshFromUseCase(); + } + + private void OnCombatEnemyHpRateChanged(object sender, GameEventArgs e) + { + if (!(e is CombatEnemyHpRateChangedEventArgs)) + { + return; + } + + RefreshFromUseCase(); + } + + private void RefreshFromUseCase() + { + if (_useCase == null) + { + return; + } + + CombatInfoFormRawData rawData = _useCase.TryRefresh(); + CombatInfoFormContext context = BuildContext(rawData); + if (context == null) + { + return; + } + + SetContext(context); + RefreshCurrentUI(); + } + } +} diff --git a/src-ref/UI/Combat/Controller/CombatSelectFormController.cs b/src-ref/UI/Combat/Controller/CombatSelectFormController.cs new file mode 100644 index 0000000..4f9972f --- /dev/null +++ b/src-ref/UI/Combat/Controller/CombatSelectFormController.cs @@ -0,0 +1,223 @@ +using GameFramework.Event; +using GeometryTD.CustomEvent; +using GeometryTD.Definition; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public partial class CombatSelectFormController : UIFormControllerCommonBase + { + private CombatSelectFormUseCase _useCase; + + protected override UIFormType UIFormTypeId => UIFormType.CombatSelectForm; + + protected override void RefreshUI(CombatSelectForm form, CombatSelectFormContext context) + { + form.RefreshUI(context); + } + + protected override void SubscribeCustomEvents() + { + GameEntry.Event.Subscribe(CombatSelectItemClickEventArgs.EventId, OnSelectItemClicked); + GameEntry.Event.Subscribe(CombatCoinChangedEventArgs.EventId, OnCombatCoinChanged); + } + + protected override void UnsubscribeCustomEvents() + { + GameEntry.Event.Unsubscribe(CombatSelectItemClickEventArgs.EventId, OnSelectItemClicked); + GameEntry.Event.Unsubscribe(CombatCoinChangedEventArgs.EventId, OnCombatCoinChanged); + } + + public int? OpenUI(CombatSelectFormRawData rawData) + { + CombatSelectFormContext context = BuildContext(rawData); + if (context == null || !context.IsVisible) + { + CloseUI(); + return null; + } + + return OpenUIInternal(context); + } + + public override int? OpenUI(object userData = null) + { + if (userData is CombatSelectFormUserData clickUserData) + { + return OpenUI(clickUserData); + } + + if (userData is CombatSelectFormContext context) + { + if (context == null || !context.IsVisible) + { + CloseUI(); + return null; + } + + return OpenUIInternal(context); + } + + if (userData is CombatSelectFormRawData rawDataFromUserData) + { + return OpenUI(rawDataFromUserData); + } + + if (userData != null) + { + Log.Warning("CombatSelectFormController.OpenUI() userData type is invalid."); + return null; + } + + if (_useCase == null) + { + Log.Error("CombatSelectFormController.OpenUI() useCase is null."); + return null; + } + + CombatSelectFormRawData rawData = _useCase.CreateInitialModel(); + return OpenUI(rawData); + } + + public int? OpenUI(CombatSelectFormUserData userData) + { + if (_useCase == null) + { + Log.Error("CombatSelectFormController.OpenUI() useCase is null."); + return null; + } + + if (userData == null) + { + _useCase.Hide(); + CloseUI(); + return null; + } + + switch (userData.ClickObjectType) + { + case CombatSelectClickObjectType.Foundation: + _useCase.SetUpgradeVisible(true); + _useCase.ShowForFoundation(userData.ScreenPosition); + break; + case CombatSelectClickObjectType.Tower: + _useCase.SetUpgradePrice(userData.UpgradeCost); + _useCase.SetUpgradeVisible(!userData.IsTowerAtMaxLevel); + _useCase.SetDestroyGain(userData.DestroyGain); + _useCase.ShowForTower(userData.ScreenPosition); + break; + default: + _useCase.Hide(); + CloseUI(); + return null; + } + + return OpenUI(_useCase.TryRefresh()); + } + + public override void BindUseCase(IUIUseCase useCase) + { + if (!(useCase is CombatSelectFormUseCase combatSelectFormUseCase)) + { + Log.Error("CombatSelectFormController.BindUseCase() useCase is invalid."); + return; + } + + _useCase = combatSelectFormUseCase; + } + + public int? ShowForFoundation(Vector2 screenPosition) + { + if (_useCase == null) + { + Log.Error("CombatSelectFormController.ShowForFoundation() useCase is null."); + return null; + } + + _useCase.SetUpgradeVisible(true); + _useCase.ShowForFoundation(screenPosition); + return OpenUI(_useCase.TryRefresh()); + } + + public int? ShowForTower(Vector2 screenPosition, int upgradeCost, int destroyGain) + { + if (_useCase == null) + { + Log.Error("CombatSelectFormController.ShowForTower() useCase is null."); + return null; + } + + _useCase.SetUpgradePrice(upgradeCost); + _useCase.SetUpgradeVisible(true); + _useCase.SetDestroyGain(destroyGain); + _useCase.ShowForTower(screenPosition); + return OpenUI(_useCase.TryRefresh()); + } + + public void HideSelection() + { + _useCase?.Hide(); + CloseUI(); + } + + public void RefreshFromUseCase() + { + if (_useCase == null) + { + return; + } + + CombatSelectFormRawData rawData = _useCase.TryRefresh(); + CombatSelectFormContext context = BuildContext(rawData); + if (context == null || !context.IsVisible) + { + CloseUI(); + return; + } + + SetContext(context); + RefreshCurrentUI(); + } + + private void OnSelectItemClicked(object sender, GameEventArgs e) + { + if (!(e is CombatSelectItemClickEventArgs args)) + { + return; + } + + if (_useCase == null) + { + return; + } + + if (Form != null && sender is Component component) + { + CombatSelectForm ownerForm = component.GetComponentInParent(); + if (ownerForm != Form) + { + return; + } + } + + _useCase.TryExecuteAction(args.ActionType, args.ActionIndex); + RefreshFromUseCase(); + } + + private void OnCombatCoinChanged(object sender, GameEventArgs e) + { + if (!(e is CombatCoinChangedEventArgs)) + { + return; + } + + if (Form == null) + { + return; + } + + RefreshFromUseCase(); + } + } +} diff --git a/src-ref/UI/Combat/RawData/CombatFinishFormRawData.cs b/src-ref/UI/Combat/RawData/CombatFinishFormRawData.cs new file mode 100644 index 0000000..b9173d5 --- /dev/null +++ b/src-ref/UI/Combat/RawData/CombatFinishFormRawData.cs @@ -0,0 +1,12 @@ +using GeometryTD.Definition; + +namespace GeometryTD.UI +{ + public class CombatFinishFormRawData + { + public int DefeatedEnemyCount; + public int GainedGold; + public BackpackInventoryData RewardInventory; + public bool CanReturn; + } +} diff --git a/src-ref/UI/Combat/RawData/CombatInfoFormRawData.cs b/src-ref/UI/Combat/RawData/CombatInfoFormRawData.cs new file mode 100644 index 0000000..d23d8f2 --- /dev/null +++ b/src-ref/UI/Combat/RawData/CombatInfoFormRawData.cs @@ -0,0 +1,18 @@ +using GeometryTD.Definition; + +namespace GeometryTD.UI +{ + public class CombatInfoFormRawData + { + public LevelThemeType LevelThemeType; + public int LevelId; + public int CurrentPhaseIndex; + public int TotalPhaseCount; + public int Coin; + public int BaseHp; + public int BaseHpMax; + public int EnemyHpRateMultiplier; + public bool CanPause; + public bool CanEnd; + } +} diff --git a/src-ref/UI/Combat/RawData/CombatSelectClickObjectType.cs b/src-ref/UI/Combat/RawData/CombatSelectClickObjectType.cs new file mode 100644 index 0000000..5937200 --- /dev/null +++ b/src-ref/UI/Combat/RawData/CombatSelectClickObjectType.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.UI +{ + public enum CombatSelectClickObjectType : byte + { + None = 0, + Foundation = 1, + Tower = 2 + } +} diff --git a/src-ref/UI/Combat/RawData/CombatSelectDisplayMode.cs b/src-ref/UI/Combat/RawData/CombatSelectDisplayMode.cs new file mode 100644 index 0000000..e500bd2 --- /dev/null +++ b/src-ref/UI/Combat/RawData/CombatSelectDisplayMode.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.UI +{ + public enum CombatSelectDisplayMode : byte + { + Hidden = 0, + Build = 1, + Func = 2 + } +} diff --git a/src-ref/UI/Combat/RawData/CombatSelectFormRawData.cs b/src-ref/UI/Combat/RawData/CombatSelectFormRawData.cs new file mode 100644 index 0000000..587b9e3 --- /dev/null +++ b/src-ref/UI/Combat/RawData/CombatSelectFormRawData.cs @@ -0,0 +1,14 @@ +using UnityEngine; + +namespace GeometryTD.UI +{ + public class CombatSelectFormRawData + { + public bool IsVisible; + public Vector2 ScreenPosition; + public CombatSelectDisplayMode DisplayMode; + public TowerSelectItemRawData[] BuildItems; + public TowerSelectItemRawData UpgradeItem; + public TowerSelectItemRawData DestroyItem; + } +} diff --git a/src-ref/UI/Combat/RawData/CombatSelectFormUserData.cs b/src-ref/UI/Combat/RawData/CombatSelectFormUserData.cs new file mode 100644 index 0000000..6dfc15d --- /dev/null +++ b/src-ref/UI/Combat/RawData/CombatSelectFormUserData.cs @@ -0,0 +1,16 @@ +using UnityEngine; + +namespace GeometryTD.UI +{ + public class CombatSelectFormUserData + { + public CombatSelectClickObjectType ClickObjectType; + public Vector2 ScreenPosition; + public Vector3 WorldPosition; + public Vector3Int CellPosition; + public int TowerEntityId; + public bool IsTowerAtMaxLevel; + public int UpgradeCost; + public int DestroyGain; + } +} diff --git a/src-ref/UI/Combat/RawData/TowerSelectItemRawData.cs b/src-ref/UI/Combat/RawData/TowerSelectItemRawData.cs new file mode 100644 index 0000000..e02daef --- /dev/null +++ b/src-ref/UI/Combat/RawData/TowerSelectItemRawData.cs @@ -0,0 +1,19 @@ +using UnityEngine; + +namespace GeometryTD.UI +{ + public class TowerSelectItemRawData + { + public Sprite Icon; + public int Price; + public bool IsGain; + public bool IsVisible; + public bool IsInteractable; + public CombatSelectActionType ActionType; + public int ActionIndex; + public bool UseTowerLayers; + public Color BaseColor = Color.white; + public Color BearingColor = Color.white; + public Color MuzzleColor = Color.white; + } +} diff --git a/src-ref/UI/Combat/UseCase/CombatFinishFormUseCase.cs b/src-ref/UI/Combat/UseCase/CombatFinishFormUseCase.cs new file mode 100644 index 0000000..7753947 --- /dev/null +++ b/src-ref/UI/Combat/UseCase/CombatFinishFormUseCase.cs @@ -0,0 +1,62 @@ +using GeometryTD.CustomComponent; +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.UI +{ + public class CombatFinishFormUseCase : IUIUseCase + { + private ICombatSchedulerPort _combatSchedulerPort; + private CombatSettlementContext _settlementContext; + private bool _isSummaryPrepared; + + public CombatFinishFormRawData CreateInitialModel() + { + return BuildModel(); + } + + public CombatFinishFormRawData TryRefresh() + { + return BuildModel(); + } + + internal void SetSummary(CombatSettlementContext settlementContext) + { + _settlementContext = settlementContext; + _isSummaryPrepared = true; + } + + internal CombatFinishFormUseCase(ICombatSchedulerPort combatSchedulerPort) + { + _combatSchedulerPort = combatSchedulerPort; + } + + public bool TryReturnToMenu() + { + return _combatSchedulerPort != null && _combatSchedulerPort.OnCombatFinishReturnRequested(); + } + + private CombatFinishFormRawData BuildModel() + { + if (!_isSummaryPrepared) + { + _settlementContext = null; + } + + BackpackInventoryData rewardInventory = _settlementContext?.Summary.RewardInventory; + if (rewardInventory == null) + { + rewardInventory = InventorySeedUtility.CreateSampleInventory(); + } + + return new CombatFinishFormRawData + { + DefeatedEnemyCount = Mathf.Max(0, _settlementContext?.Summary.DefeatedEnemyCount ?? 0), + GainedGold = Mathf.Max(0, _settlementContext?.Summary.GainedGold ?? 0), + RewardInventory = rewardInventory, + CanReturn = true + }; + } + } +} diff --git a/src-ref/UI/Combat/UseCase/CombatInfoFormUseCase.cs b/src-ref/UI/Combat/UseCase/CombatInfoFormUseCase.cs new file mode 100644 index 0000000..0cd6edd --- /dev/null +++ b/src-ref/UI/Combat/UseCase/CombatInfoFormUseCase.cs @@ -0,0 +1,96 @@ +using System; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.UI +{ + public class CombatInfoFormUseCase : IUIUseCase + { + private Func _modelProvider; + private Func _tryEndCombat; + private Func _tryDebugFail; + + public CombatInfoFormRawData CreateInitialModel() + { + return BuildModel(); + } + + public CombatInfoFormRawData TryRefresh() + { + return BuildModel(); + } + + public void Configure(Func modelProvider, Func tryEndCombat, Func tryDebugFail) + { + _modelProvider = modelProvider; + _tryEndCombat = tryEndCombat; + _tryDebugFail = tryDebugFail; + } + + public bool TryEndCombat() + { + if (_tryEndCombat == null) + { + return false; + } + + return _tryEndCombat.Invoke(); + } + + public bool TryDebugFail() + { + if (_tryDebugFail == null) + { + return false; + } + + return _tryDebugFail.Invoke(); + } + + private CombatInfoFormRawData BuildModel() + { + return _modelProvider != null ? _modelProvider.Invoke() : null; + } + + public static CombatInfoFormRawData BuildRawData( + LevelThemeType themeType, + int levelId, + int currentPhaseIndex, + int totalPhaseCount, + int coin, + int baseHp, + int baseHpMax, + bool canEnd) + { + return new CombatInfoFormRawData + { + LevelThemeType = themeType, + LevelId = levelId, + CurrentPhaseIndex = Mathf.Max(0, currentPhaseIndex), + TotalPhaseCount = Mathf.Max(0, totalPhaseCount), + Coin = Mathf.Max(0, coin), + BaseHp = Mathf.Max(0, baseHp), + BaseHpMax = Mathf.Max(0, baseHpMax), + EnemyHpRateMultiplier = ResolveEnemyHpRateMultiplier(currentPhaseIndex, totalPhaseCount), + CanPause = true, + CanEnd = canEnd + }; + } + + private static int ResolveEnemyHpRateMultiplier(int currentPhaseIndex, int totalPhaseCount) + { + if (currentPhaseIndex <= 0 || totalPhaseCount <= 0) + { + return 1; + } + + int completedLoopCount = Mathf.Max(0, (currentPhaseIndex - 1) / totalPhaseCount); + if (completedLoopCount >= 30) + { + return int.MaxValue; + } + + return 1 << completedLoopCount; + } + } +} diff --git a/src-ref/UI/Combat/UseCase/CombatSelectFormUseCase.cs b/src-ref/UI/Combat/UseCase/CombatSelectFormUseCase.cs new file mode 100644 index 0000000..445b073 --- /dev/null +++ b/src-ref/UI/Combat/UseCase/CombatSelectFormUseCase.cs @@ -0,0 +1,337 @@ +using System; +using UnityEngine; + +namespace GeometryTD.UI +{ + public class CombatSelectFormUseCase : IUIUseCase + { + private const int BuildOptionCount = 4; + + private readonly Option[] _buildOptions = new Option[BuildOptionCount]; + private readonly Option _upgradeOption = new Option(CombatSelectActionType.UpgradeTower, 0, true); + private readonly Option _destroyOption = new Option(CombatSelectActionType.DestroyTower, 0, false); + + private Func _coinProvider; + private bool _isVisible; + private CombatSelectDisplayMode _displayMode = CombatSelectDisplayMode.Hidden; + private Vector2 _screenPosition; + + public CombatSelectFormUseCase() + { + _coinProvider = DefaultCoinProvider; + + for (int i = 0; i < BuildOptionCount; i++) + { + _buildOptions[i] = new Option(CombatSelectActionType.BuildTower, i, true); + } + } + + public CombatSelectFormRawData CreateInitialModel() + { + return BuildModel(); + } + + public CombatSelectFormRawData TryRefresh() + { + return BuildModel(); + } + + public void SetCoinProvider(Func coinProvider) + { + _coinProvider = coinProvider ?? DefaultCoinProvider; + } + + public void SetBuildAction(int buildIndex, Func buildAction, int cost, Sprite icon = null) + { + SetBuildAction(buildIndex, buildAction, cost, icon, false, Color.white, Color.white, Color.white); + } + + public void SetBuildAction( + int buildIndex, + Func buildAction, + int cost, + Sprite icon, + bool useTowerLayers, + Color baseColor, + Color bearingColor, + Color muzzleColor) + { + if (buildIndex < 0 || buildIndex >= _buildOptions.Length) + { + return; + } + + Option option = _buildOptions[buildIndex]; + option.Action = buildAction; + option.Price = Mathf.Max(0, cost); + option.Icon = icon; + option.IsGain = false; + option.IsVisible = true; + option.UseTowerLayers = useTowerLayers; + option.BaseColor = baseColor; + option.BearingColor = bearingColor; + option.MuzzleColor = muzzleColor; + } + + public void SetBuildAction(int buildIndex, Action buildAction, int cost, Sprite icon = null) + { + if (buildAction == null) + { + SetBuildAction(buildIndex, (Func)null, cost, icon); + return; + } + + SetBuildAction( + buildIndex, + () => + { + buildAction.Invoke(); + return true; + }, + cost, + icon); + } + + public void SetBuildVisible(int buildIndex, bool visible) + { + if (buildIndex < 0 || buildIndex >= _buildOptions.Length) + { + return; + } + + _buildOptions[buildIndex].IsVisible = visible; + } + + public void SetUpgradeAction(Func upgradeAction, int cost, Sprite icon = null) + { + _upgradeOption.Action = upgradeAction; + _upgradeOption.Price = Mathf.Max(0, cost); + _upgradeOption.Icon = icon; + _upgradeOption.IsGain = false; + _upgradeOption.IsVisible = true; + _upgradeOption.UseTowerLayers = false; + } + + public void SetUpgradeAction(Action upgradeAction, int cost, Sprite icon = null) + { + if (upgradeAction == null) + { + SetUpgradeAction((Func)null, cost, icon); + return; + } + + SetUpgradeAction( + () => + { + upgradeAction.Invoke(); + return true; + }, + cost, + icon); + } + + public void SetDestroyAction(Func destroyAction, int gain, Sprite icon = null) + { + _destroyOption.Action = destroyAction; + _destroyOption.Price = Mathf.Max(0, gain); + _destroyOption.Icon = icon; + _destroyOption.IsGain = true; + _destroyOption.IsVisible = true; + _destroyOption.UseTowerLayers = false; + } + + public void SetDestroyAction(Action destroyAction, int gain, Sprite icon = null) + { + if (destroyAction == null) + { + SetDestroyAction((Func)null, gain, icon); + return; + } + + SetDestroyAction( + () => + { + destroyAction.Invoke(); + return true; + }, + gain, + icon); + } + + public void SetUpgradePrice(int cost) + { + _upgradeOption.Price = Mathf.Max(0, cost); + } + + public void SetUpgradeVisible(bool visible) + { + _upgradeOption.IsVisible = visible; + } + + public void SetDestroyGain(int gain) + { + _destroyOption.Price = Mathf.Max(0, gain); + _destroyOption.IsGain = true; + } + + public void ShowForFoundation(Vector2 screenPosition) + { + _screenPosition = screenPosition; + _displayMode = CombatSelectDisplayMode.Build; + _isVisible = true; + } + + public void ShowForTower(Vector2 screenPosition) + { + _screenPosition = screenPosition; + _displayMode = CombatSelectDisplayMode.Func; + _isVisible = true; + } + + public void Hide() + { + _isVisible = false; + _displayMode = CombatSelectDisplayMode.Hidden; + } + + public bool TryExecuteAction(CombatSelectActionType actionType, int actionIndex) + { + if (!_isVisible) + { + return false; + } + + Option option = GetOption(actionType, actionIndex); + if (option == null || !option.IsVisible) + { + return false; + } + + int currentCoin = Mathf.Max(0, _coinProvider != null ? _coinProvider.Invoke() : 0); + if (!CanExecute(option, currentCoin)) + { + return false; + } + + if (option.Action == null) + { + return false; + } + + bool result = option.Action.Invoke(); + if (result) + { + Hide(); + } + + return result; + } + + private CombatSelectFormRawData BuildModel() + { + int currentCoin = Mathf.Max(0, _coinProvider != null ? _coinProvider.Invoke() : 0); + TowerSelectItemRawData[] buildItems = new TowerSelectItemRawData[_buildOptions.Length]; + for (int i = 0; i < _buildOptions.Length; i++) + { + buildItems[i] = BuildOptionRawData(_buildOptions[i], currentCoin); + } + + return new CombatSelectFormRawData + { + IsVisible = _isVisible, + ScreenPosition = _screenPosition, + DisplayMode = _displayMode, + BuildItems = buildItems, + UpgradeItem = BuildOptionRawData(_upgradeOption, currentCoin), + DestroyItem = BuildOptionRawData(_destroyOption, currentCoin) + }; + } + + private Option GetOption(CombatSelectActionType actionType, int actionIndex) + { + switch (actionType) + { + case CombatSelectActionType.BuildTower: + if (actionIndex < 0 || actionIndex >= _buildOptions.Length) + { + return null; + } + + return _buildOptions[actionIndex]; + case CombatSelectActionType.UpgradeTower: + return _upgradeOption; + case CombatSelectActionType.DestroyTower: + return _destroyOption; + default: + return null; + } + } + + private static int DefaultCoinProvider() + { + return 0; + } + + private static bool CanExecute(Option option, int currentCoin) + { + if (option == null) + { + return false; + } + + if (option.RequiresAffordable && currentCoin < option.Price) + { + return false; + } + + return true; + } + + private static TowerSelectItemRawData BuildOptionRawData(Option option, int currentCoin) + { + if (option == null) + { + return null; + } + + return new TowerSelectItemRawData + { + Icon = option.Icon, + Price = option.Price, + IsGain = option.IsGain, + IsVisible = option.IsVisible, + IsInteractable = option.Action != null && CanExecute(option, currentCoin), + ActionType = option.ActionType, + ActionIndex = option.ActionIndex, + UseTowerLayers = option.UseTowerLayers, + BaseColor = option.BaseColor, + BearingColor = option.BearingColor, + MuzzleColor = option.MuzzleColor + }; + } + + private sealed class Option + { + public readonly CombatSelectActionType ActionType; + public readonly int ActionIndex; + public readonly bool RequiresAffordable; + + public int Price; + public bool IsGain; + public bool IsVisible = true; + public Sprite Icon; + public Func Action; + public bool UseTowerLayers; + public Color BaseColor = Color.white; + public Color BearingColor = Color.white; + public Color MuzzleColor = Color.white; + + public Option(CombatSelectActionType actionType, int actionIndex, bool requiresAffordable) + { + ActionType = actionType; + ActionIndex = actionIndex; + RequiresAffordable = requiresAffordable; + } + } + } +} diff --git a/src-ref/UI/Combat/View/CombatFinishForm.cs b/src-ref/UI/Combat/View/CombatFinishForm.cs new file mode 100644 index 0000000..55e5969 --- /dev/null +++ b/src-ref/UI/Combat/View/CombatFinishForm.cs @@ -0,0 +1,173 @@ +using System.Collections.Generic; +using GameFramework.ObjectPool; +using GeometryTD.CustomEvent; +using GeometryTD.PoolObjectBase; +using TMPro; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class CombatFinishForm : UGuiForm + { + [SerializeField] private TMP_Text _enemyKilledText; + [SerializeField] private TMP_Text _goldGainedText; + [SerializeField] private Transform _itemGainedParent; + [SerializeField] private RepoItem _itemTemplate; + [SerializeField] private int _instancePoolCapacity = 16; + + private readonly List _activeItems = new List(); + private IObjectPool _itemPool; + private string _poolName; + private CombatFinishFormContext _context; + + public void RefreshUI(CombatFinishFormContext context) + { + _context = context; + + if (_enemyKilledText != null) + { + _enemyKilledText.text = context?.EnemyKilledText ?? "0"; + } + + if (_goldGainedText != null) + { + _goldGainedText.text = context?.GoldGainedText ?? "0"; + } + + RefreshRewardItems(context?.RewardItems); + } + + public void OnReturnButtonClick() + { + if (_context != null && !_context.CanReturn) + { + return; + } + + GameEntry.Event.Fire(this, CombatFinishReturnEventArgs.Create()); + } + + protected override void OnOpen(object userData) + { + base.OnOpen(userData); + + if (userData is CombatFinishFormContext context) + { + RefreshUI(context); + return; + } + + Log.Warning("CombatFinishForm requires CombatFinishFormContext as userData."); + } + + protected override void OnClose(bool isShutdown, object userData) + { + _context = null; + + if (_enemyKilledText != null) + { + _enemyKilledText.text = string.Empty; + } + + if (_goldGainedText != null) + { + _goldGainedText.text = string.Empty; + } + + ClearRewardItems(); + base.OnClose(isShutdown, userData); + } + + private void OnDestroy() + { + ClearRewardItems(); + _itemPool?.ReleaseAllUnused(); + } + + private void EnsurePool() + { + if (_itemPool != null) + { + return; + } + + _poolName = $"CombatFinishRepoItemPool_{GetInstanceID()}"; + _itemPool = GameEntry.ObjectPool.CreateSingleSpawnObjectPool(_poolName, _instancePoolCapacity); + } + + private void RefreshRewardItems(RepoItemContext[] rewardItems) + { + ClearRewardItems(); + + if (rewardItems == null || rewardItems.Length <= 0) + { + return; + } + + EnsurePool(); + + for (int i = 0; i < rewardItems.Length; i++) + { + RepoItem item = CreateOrSpawnItem(); + if (item == null) + { + continue; + } + + item.gameObject.SetActive(true); + item.OnInit(rewardItems[i]); + _activeItems.Add(item); + } + } + + private void ClearRewardItems() + { + for (int i = _activeItems.Count - 1; i >= 0; i--) + { + RepoItem item = _activeItems[i]; + if (item == null) + { + continue; + } + + item.OnReset(); + item.gameObject.SetActive(false); + _itemPool?.Unspawn(item); + } + + _activeItems.Clear(); + } + + private RepoItem CreateOrSpawnItem() + { + if (_itemGainedParent == null) + { + Log.Warning("CombatFinishForm requires an item gained parent."); + return null; + } + + if (_itemPool == null) + { + return null; + } + + RepoItemObject itemObject = _itemPool.Spawn(); + RepoItem item = itemObject != null ? (RepoItem)itemObject.Target : null; + if (item == null) + { + if (_itemTemplate == null) + { + Log.Warning("CombatFinishForm requires an item template."); + return null; + } + + item = Instantiate(_itemTemplate); + _itemPool.Register(RepoItemObject.Create(item), true); + } + + item.transform.SetParent(_itemGainedParent, false); + return item; + } + } +} diff --git a/src-ref/UI/Combat/View/CombatInfoForm.cs b/src-ref/UI/Combat/View/CombatInfoForm.cs new file mode 100644 index 0000000..8d3c1d8 --- /dev/null +++ b/src-ref/UI/Combat/View/CombatInfoForm.cs @@ -0,0 +1,136 @@ +using GeometryTD.CustomEvent; +using TMPro; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class CombatInfoForm : UGuiForm + { + [SerializeField] private TMP_Text _levelMetaText; + + [SerializeField] private TMP_Text _levelPhaseText; + + [SerializeField] private TMP_Text _coinText; + + [SerializeField] private TMP_Text _baseHpText; + + [SerializeField] private TMP_Text _enemyHpRateText; + + [SerializeField] private CommonButton _pauseButton; + + [SerializeField] private CommonButton _endButton; + + private CombatInfoFormContext _context; + + public void RefreshUI(CombatInfoFormContext context) + { + _context = context; + + if (_levelMetaText != null) + { + _levelMetaText.text = context?.LevelMetaText ?? string.Empty; + } + + if (_levelPhaseText != null) + { + _levelPhaseText.text = context?.LevelPhaseText ?? string.Empty; + } + + if (_coinText != null) + { + _coinText.text = context?.CoinText ?? "0"; + } + + if (_baseHpText != null) + { + _baseHpText.text = context?.BaseHpText ?? string.Empty; + } + + if (_enemyHpRateText != null) + { + _enemyHpRateText.text = context?.EnemyHpRateText ?? string.Empty; + } + + if (_pauseButton != null) + { + _pauseButton.Interactive = context?.CanPause ?? false; + } + + if (_endButton != null) + { + _endButton.gameObject.SetActive(context?.CanEnd ?? false); + } + } + + public void OnPauseButtonClick() + { + if (_context != null && !_context.CanPause) + { + return; + } + + GameEntry.Event.Fire(this, CombatPauseEventArgs.Create()); + } + + public void OnEndButtonClick() + { + if (_context != null && !_context.CanEnd) + { + return; + } + + GameEntry.Event.Fire(this, CombatEndEventArgs.Create()); + } + + public void OnFailButtonClick() + { + GameEntry.Event.Fire(this, CombatDebugFailEventArgs.Create()); + } + + protected override void OnOpen(object userData) + { + base.OnOpen(userData); + + if (userData is CombatInfoFormContext context) + { + RefreshUI(context); + return; + } + + Log.Warning("CombatInfoForm requires CombatInfoFormContext as userData."); + } + + protected override void OnClose(bool isShutdown, object userData) + { + _context = null; + + if (_levelMetaText != null) + { + _levelMetaText.text = string.Empty; + } + + if (_levelPhaseText != null) + { + _levelPhaseText.text = string.Empty; + } + + if (_coinText != null) + { + _coinText.text = string.Empty; + } + + if (_baseHpText != null) + { + _baseHpText.text = string.Empty; + } + + if (_enemyHpRateText != null) + { + _enemyHpRateText.text = string.Empty; + } + + base.OnClose(isShutdown, userData); + } + } +} diff --git a/src-ref/UI/Combat/View/CombatSelectBuildArea.cs b/src-ref/UI/Combat/View/CombatSelectBuildArea.cs new file mode 100644 index 0000000..737ca79 --- /dev/null +++ b/src-ref/UI/Combat/View/CombatSelectBuildArea.cs @@ -0,0 +1,60 @@ +using UnityEngine; + +namespace GeometryTD.UI +{ + public class CombatSelectBuildArea : MonoBehaviour + { + [SerializeField] private TowerSelectItem[] _towerBuildItems; + + public void OnInit(TowerSelectItemContext[] itemContexts) + { + if (_towerBuildItems == null) + { + return; + } + + for (int i = 0; i < _towerBuildItems.Length; i++) + { + TowerSelectItem item = _towerBuildItems[i]; + if (item == null) + { + continue; + } + + TowerSelectItemContext itemContext = + itemContexts != null && i < itemContexts.Length ? itemContexts[i] : null; + bool isVisible = itemContext != null && itemContext.IsVisible; + + item.gameObject.SetActive(isVisible); + if (isVisible) + { + item.OnInit(itemContext); + } + else + { + item.OnReset(); + } + } + } + + public void OnReset() + { + if (_towerBuildItems == null) + { + return; + } + + for (int i = 0; i < _towerBuildItems.Length; i++) + { + TowerSelectItem item = _towerBuildItems[i]; + if (item == null) + { + continue; + } + + item.OnReset(); + item.gameObject.SetActive(false); + } + } + } +} diff --git a/src-ref/UI/Combat/View/CombatSelectForm.cs b/src-ref/UI/Combat/View/CombatSelectForm.cs new file mode 100644 index 0000000..a1bb5e3 --- /dev/null +++ b/src-ref/UI/Combat/View/CombatSelectForm.cs @@ -0,0 +1,141 @@ +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class CombatSelectForm : UGuiForm + { + [SerializeField] private RectTransform _content; + + [SerializeField] private CombatSelectBuildArea _buildArea; + + [SerializeField] private CombatSelectFuncArea _funcArea; + + private CombatSelectFormContext _context; + + public void RefreshUI(CombatSelectFormContext context) + { + _context = context; + EnsureContentReference(); + + if (_content == null) + { + Log.Warning("CombatSelectForm content is missing."); + return; + } + + bool isVisible = context != null && context.IsVisible; + _content.gameObject.SetActive(isVisible); + if (!isVisible) + { + _buildArea?.OnReset(); + _funcArea?.OnReset(); + return; + } + + _content.anchoredPosition = ResolveContentAnchoredPosition(context.ScreenPosition); + + if (_buildArea != null) + { + bool showBuildArea = context.ShowBuildArea; + _buildArea.gameObject.SetActive(showBuildArea); + if (showBuildArea) + { + _buildArea.OnInit(context.BuildItems); + } + else + { + _buildArea.OnReset(); + } + } + + if (_funcArea != null) + { + bool showFuncArea = context.ShowFuncArea; + _funcArea.gameObject.SetActive(showFuncArea); + if (showFuncArea) + { + _funcArea.OnInit(context.UpgradeItem, context.DestroyItem); + } + else + { + _funcArea.OnReset(); + } + } + } + + protected override void OnOpen(object userData) + { + base.OnOpen(userData); + + if (userData is CombatSelectFormContext context) + { + RefreshUI(context); + return; + } + + Log.Warning("CombatSelectForm requires CombatSelectFormContext as userData."); + RefreshUI(null); + } + + protected override void OnClose(bool isShutdown, object userData) + { + _context = null; + _buildArea?.OnReset(); + _funcArea?.OnReset(); + + if (_content != null) + { + _content.gameObject.SetActive(false); + } + + base.OnClose(isShutdown, userData); + } + + private void Awake() + { + EnsureContentReference(); + } + + private void EnsureContentReference() + { + if (_content != null) + { + return; + } + + Transform contentTransform = transform.Find("Content"); + _content = contentTransform as RectTransform; + } + + private Vector2 ResolveContentAnchoredPosition(Vector2 screenPosition) + { + if (_content == null) + { + return Vector2.zero; + } + + RectTransform parentRect = _content.parent as RectTransform; + if (parentRect == null) + { + return screenPosition; + } + + Canvas canvas = _content.GetComponentInParent(); + Canvas rootCanvas = canvas != null ? canvas.rootCanvas : null; + Camera uiCamera = null; + if (rootCanvas != null && rootCanvas.renderMode != RenderMode.ScreenSpaceOverlay) + { + uiCamera = rootCanvas.worldCamera != null ? rootCanvas.worldCamera : Camera.main; + } + + if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRect, screenPosition, uiCamera, + out Vector2 localPoint)) + { + return Vector2.zero; + } + + return localPoint; + } + } +} diff --git a/src-ref/UI/Combat/View/CombatSelectFuncArea.cs b/src-ref/UI/Combat/View/CombatSelectFuncArea.cs new file mode 100644 index 0000000..caad082 --- /dev/null +++ b/src-ref/UI/Combat/View/CombatSelectFuncArea.cs @@ -0,0 +1,53 @@ +using UnityEngine; + +namespace GeometryTD.UI +{ + public class CombatSelectFuncArea : MonoBehaviour + { + [SerializeField] private TowerSelectItem _upgradeItem; + + [SerializeField] private TowerSelectItem _destroyItem; + + public void OnInit(TowerSelectItemContext upgradeItemContext, TowerSelectItemContext destroyItemContext) + { + RefreshItem(_upgradeItem, upgradeItemContext); + RefreshItem(_destroyItem, destroyItemContext); + } + + public void OnReset() + { + ResetItem(_upgradeItem); + ResetItem(_destroyItem); + } + + private static void RefreshItem(TowerSelectItem item, TowerSelectItemContext context) + { + if (item == null) + { + return; + } + + bool isVisible = context != null && context.IsVisible; + item.gameObject.SetActive(isVisible); + if (isVisible) + { + item.OnInit(context); + } + else + { + item.OnReset(); + } + } + + private static void ResetItem(TowerSelectItem item) + { + if (item == null) + { + return; + } + + item.OnReset(); + item.gameObject.SetActive(false); + } + } +} diff --git a/src-ref/UI/Combat/View/TowerSelectItem.cs b/src-ref/UI/Combat/View/TowerSelectItem.cs new file mode 100644 index 0000000..d7d8cf6 --- /dev/null +++ b/src-ref/UI/Combat/View/TowerSelectItem.cs @@ -0,0 +1,118 @@ +using TMPro; +using UnityEngine; +using UnityEngine.UI; +using GeometryTD.CustomEvent; + +namespace GeometryTD.UI +{ + public class TowerSelectItem : MonoBehaviour + { + [SerializeField] private Image _icon; + + [SerializeField] private TMP_Text _price; + + [SerializeField] private CommonButton _button; + + [SerializeField] private TowerIconArea _towerIconArea; + + private TowerSelectItemContext _context; + + private Sprite _defaultIcon; + + private bool _hasCachedDefaultIcon; + + public void OnInit(TowerSelectItemContext context) + { + CacheDefaultIcon(); + _context = context; + + if (context.ActionType == CombatSelectActionType.BuildTower) + { + _icon?.gameObject.SetActive(false); + _towerIconArea?.OnInit(_context.TowerIconAreaContext); + + if (_price != null) + { + _price.text = context.PriceText ?? string.Empty; + } + + if (_button != null) + { + _button.Interactive = context.IsInteractable; + } + } + else if (context.ActionType is CombatSelectActionType.UpgradeTower or CombatSelectActionType.DestroyTower) + { + if (_icon != null) + { + _icon.sprite = _context.Icon ?? _defaultIcon; + _icon.gameObject.SetActive(true); + } + + _towerIconArea?.OnReset(); + + if (_price != null) + { + _price.text = context.PriceText ?? string.Empty; + } + + if (_button != null) + { + _button.Interactive = context.IsInteractable; + } + } + else + { + _icon?.gameObject.SetActive(false); + _towerIconArea?.OnReset(); + } + } + + public void OnReset() + { + _context = null; + CacheDefaultIcon(); + + if (_icon != null) + { + _icon.sprite = _defaultIcon; + _icon.enabled = _icon.sprite != null; + } + + _towerIconArea.OnReset(); + + if (_price != null) + { + _price.text = string.Empty; + } + + if (_button != null) + { + _button.Interactive = false; + } + } + + public void OnClick() + { + if (_context == null || !_context.IsInteractable) + { + return; + } + + GameEntry.Event.Fire( + this, + CombatSelectItemClickEventArgs.Create(_context.ActionType, _context.ActionIndex)); + } + + private void CacheDefaultIcon() + { + if (_hasCachedDefaultIcon || _icon == null) + { + return; + } + + _defaultIcon = _icon.sprite; + _hasCachedDefaultIcon = true; + } + } +} \ No newline at end of file diff --git a/src-ref/UI/CommonButton.cs b/src-ref/UI/CommonButton.cs new file mode 100644 index 0000000..3f7b494 --- /dev/null +++ b/src-ref/UI/CommonButton.cs @@ -0,0 +1,108 @@ +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.EventSystems; +using UnityEngine.Serialization; + +namespace GeometryTD.UI +{ + public class CommonButton : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerDownHandler, + IPointerUpHandler + { + private const float FadeTime = 0.3f; + private const float OnHoverAlpha = 0.7f; + private const float OnClickAlpha = 0.6f; + + [FormerlySerializedAs("m_OnHover")] [SerializeField] + private UnityEvent _onHover = null; + + [FormerlySerializedAs("m_OnClick")] [SerializeField] + private UnityEvent _onClick = null; + + [SerializeField] private UnityEvent _onHoverEnd = null; + + [SerializeField] private bool _allowFade = true; + + private bool _interactive = true; + + public bool Interactive + { + get => _interactive; + set + { + _interactive = value; + if (!value) + { + StopAllCoroutines(); + _canvasGroup.alpha = 1f; + } + } + } + + private CanvasGroup _canvasGroup = null; + + private void Awake() + { + _canvasGroup = gameObject.GetOrAddComponent(); + } + + private void OnDisable() + { + _canvasGroup.alpha = 1f; + } + + public void OnPointerEnter(PointerEventData eventData) + { + if (!Interactive) return; + + if (eventData.button != PointerEventData.InputButton.Left) + { + return; + } + + StopAllCoroutines(); + if (_allowFade) + StartCoroutine(_canvasGroup.FadeToAlpha(OnHoverAlpha, FadeTime)); + _onHover.Invoke(); + } + + public void OnPointerExit(PointerEventData eventData) + { + if (!Interactive) return; + + if (eventData.button != PointerEventData.InputButton.Left) + { + return; + } + + StopAllCoroutines(); + if (_allowFade) + StartCoroutine(_canvasGroup.FadeToAlpha(1f, FadeTime)); + _onHoverEnd?.Invoke(); + } + + public void OnPointerDown(PointerEventData eventData) + { + if (!Interactive) return; + + if (eventData.button != PointerEventData.InputButton.Left) + { + return; + } + + _canvasGroup.alpha = OnClickAlpha; + _onClick.Invoke(); + } + + public void OnPointerUp(PointerEventData eventData) + { + if (!Interactive) return; + + if (eventData.button != PointerEventData.InputButton.Left) + { + return; + } + + _canvasGroup.alpha = OnHoverAlpha; + } + } +} \ No newline at end of file diff --git a/src-ref/UI/DialogParams.cs b/src-ref/UI/DialogParams.cs new file mode 100644 index 0000000..1157bbf --- /dev/null +++ b/src-ref/UI/DialogParams.cs @@ -0,0 +1,116 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using GameFramework; + +namespace GeometryTD.UI +{ + /// + /// 对话框显示数据。 + /// + public class DialogParams + { + /// + /// 模式,即按钮数量。取值 1、2、3。 + /// + public int Mode + { + get; + set; + } + + /// + /// 标题。 + /// + public string Title + { + get; + set; + } + + /// + /// 消息内容。 + /// + public string Message + { + get; + set; + } + + /// + /// 弹出窗口时是否暂停游戏。 + /// + public bool PauseGame + { + get; + set; + } + + /// + /// 确认按钮文本。 + /// + public string ConfirmText + { + get; + set; + } + + /// + /// 确定按钮回调。 + /// + public GameFrameworkAction OnClickConfirm + { + get; + set; + } + + /// + /// 取消按钮文本。 + /// + public string CancelText + { + get; + set; + } + + /// + /// 取消按钮回调。 + /// + public GameFrameworkAction OnClickCancel + { + get; + set; + } + + /// + /// 中立按钮文本。 + /// + public string OtherText + { + get; + set; + } + + /// + /// 其它按钮回调。 + /// + public GameFrameworkAction OnClickOther + { + get; + set; + } + + /// + /// 用户自定义数据。 + /// + public string UserData + { + get; + set; + } + } +} diff --git a/src-ref/UI/Game/Context/CombineAreaContext.cs b/src-ref/UI/Game/Context/CombineAreaContext.cs new file mode 100644 index 0000000..63c1896 --- /dev/null +++ b/src-ref/UI/Game/Context/CombineAreaContext.cs @@ -0,0 +1,6 @@ +namespace GeometryTD.UI +{ + public class CombineAreaContext + { + } +} diff --git a/src-ref/UI/Game/Context/CompAreaContext.cs b/src-ref/UI/Game/Context/CompAreaContext.cs new file mode 100644 index 0000000..f53fb44 --- /dev/null +++ b/src-ref/UI/Game/Context/CompAreaContext.cs @@ -0,0 +1,8 @@ +namespace GeometryTD.UI +{ + public class CompAreaContext + { + public RepoItemContext[] ComponentItems; + public TowerRepoItemContext[] TowerItems; + } +} diff --git a/src-ref/UI/Game/Context/EventFormContext.cs b/src-ref/UI/Game/Context/EventFormContext.cs new file mode 100644 index 0000000..ea39583 --- /dev/null +++ b/src-ref/UI/Game/Context/EventFormContext.cs @@ -0,0 +1,12 @@ +using GeometryTD.UI; + +namespace GeometryTD.UI +{ + public class EventFormContext : UIContext + { + public int EventId; + public string Title; + public string Description; + public EventOptionItemContext[] OptionItems; + } +} diff --git a/src-ref/UI/Game/Context/EventOptionItemContext.cs b/src-ref/UI/Game/Context/EventOptionItemContext.cs new file mode 100644 index 0000000..856fded --- /dev/null +++ b/src-ref/UI/Game/Context/EventOptionItemContext.cs @@ -0,0 +1,10 @@ +namespace GeometryTD.UI +{ + public class EventOptionItemContext + { + public int OptionIndex; + public string OptionText; + public bool IsSelectable; + public string BlockedReason; + } +} diff --git a/src-ref/UI/Game/Context/MainFormContext.cs b/src-ref/UI/Game/Context/MainFormContext.cs new file mode 100644 index 0000000..b74efcc --- /dev/null +++ b/src-ref/UI/Game/Context/MainFormContext.cs @@ -0,0 +1,6 @@ +namespace GeometryTD.UI +{ + public class MainFormContext : UIContext + { + } +} diff --git a/src-ref/UI/Game/Context/NodeItemContext.cs b/src-ref/UI/Game/Context/NodeItemContext.cs new file mode 100644 index 0000000..db22b88 --- /dev/null +++ b/src-ref/UI/Game/Context/NodeItemContext.cs @@ -0,0 +1,16 @@ +using GeometryTD.Procedure; + +namespace GeometryTD.UI +{ + public sealed class NodeItemContext : UIContext + { + public int NodeId; + public int SequenceIndex; + public RunNodeType NodeType; + public bool IsLocked; + public bool IsCurrent; + public bool IsCompleted; + public bool IsException; + public bool CanClick; + } +} diff --git a/src-ref/UI/Game/Context/NodeMapFormContext.cs b/src-ref/UI/Game/Context/NodeMapFormContext.cs new file mode 100644 index 0000000..ece9752 --- /dev/null +++ b/src-ref/UI/Game/Context/NodeMapFormContext.cs @@ -0,0 +1,11 @@ +namespace GeometryTD.UI +{ + public sealed class NodeMapFormContext : UIContext + { + public string Title; + public string ProgressText; + public string CurrentNodeText; + public bool IsRunCompleted; + public NodeItemContext[] NodeItems; + } +} diff --git a/src-ref/UI/Game/Context/ParticipantAreaContext.cs b/src-ref/UI/Game/Context/ParticipantAreaContext.cs new file mode 100644 index 0000000..8bb52bc --- /dev/null +++ b/src-ref/UI/Game/Context/ParticipantAreaContext.cs @@ -0,0 +1,8 @@ +namespace GeometryTD.UI +{ + public class ParticipantAreaContext : UIContext + { + public TowerRepoItemContext[] TowerItems; + public int MaxCount; + } +} diff --git a/src-ref/UI/Game/Context/RepoFormContext.cs b/src-ref/UI/Game/Context/RepoFormContext.cs new file mode 100644 index 0000000..0931b88 --- /dev/null +++ b/src-ref/UI/Game/Context/RepoFormContext.cs @@ -0,0 +1,16 @@ +namespace GeometryTD.UI +{ + public class RepoFormContext : UIContext + { + public string GoldText; + public RepoFormState State; + public bool ShowSellModeButton; + public string SellModeButtonText; + public bool ShowCombineArea; + public bool ShowSellArea; + public CombineAreaContext CombineAreaContext; + public SellAreaContext SellAreaContext; + public CompAreaContext CompAreaContext; + public ParticipantAreaContext ParticipantAreaContext; + } +} diff --git a/src-ref/UI/Game/Context/RepoFormState.cs b/src-ref/UI/Game/Context/RepoFormState.cs new file mode 100644 index 0000000..57a6f4c --- /dev/null +++ b/src-ref/UI/Game/Context/RepoFormState.cs @@ -0,0 +1,8 @@ +namespace GeometryTD.UI +{ + public enum RepoFormState + { + Assemble = 0, + Sell = 1 + } +} diff --git a/src-ref/UI/Game/Context/RepoItemClickActionType.cs b/src-ref/UI/Game/Context/RepoItemClickActionType.cs new file mode 100644 index 0000000..ae1db0b --- /dev/null +++ b/src-ref/UI/Game/Context/RepoItemClickActionType.cs @@ -0,0 +1,8 @@ +namespace GeometryTD.UI +{ + public enum RepoItemClickActionType : byte + { + OpenDetail = 0, + RemoveParticipant = 1 + } +} diff --git a/src-ref/UI/Game/Context/RepoItemContext.cs b/src-ref/UI/Game/Context/RepoItemContext.cs new file mode 100644 index 0000000..31caa47 --- /dev/null +++ b/src-ref/UI/Game/Context/RepoItemContext.cs @@ -0,0 +1,19 @@ +using System; +using GeometryTD.Definition; + +namespace GeometryTD.UI +{ + [Serializable] + public class RepoItemContext + { + public long InstanceId; + public bool CanDrag; + public float EnduranceRate01; + public RepoItemClickActionType ClickActionType; + public TowerCompSlotType ComponentSlotType; + public bool IsSellMode; + public bool IsSellable = true; + public bool IsSellSelected; + public IconAreaContext IconAreaContext; + } +} diff --git a/src-ref/UI/Game/Context/SellAreaContext.cs b/src-ref/UI/Game/Context/SellAreaContext.cs new file mode 100644 index 0000000..2b7c00c --- /dev/null +++ b/src-ref/UI/Game/Context/SellAreaContext.cs @@ -0,0 +1,10 @@ +namespace GeometryTD.UI +{ + public class SellAreaContext + { + public string TotalPriceText; + public bool CanConfirmSell; + public RepoItemContext[] ComponentItems; + public TowerRepoItemContext[] TowerItems; + } +} diff --git a/src-ref/UI/Game/Context/TowerRepoItemContext.cs b/src-ref/UI/Game/Context/TowerRepoItemContext.cs new file mode 100644 index 0000000..83d8832 --- /dev/null +++ b/src-ref/UI/Game/Context/TowerRepoItemContext.cs @@ -0,0 +1,9 @@ +using System; + +namespace GeometryTD.UI +{ + [Serializable] + public class TowerRepoItemContext : RepoItemContext + { + } +} diff --git a/src-ref/UI/Game/ContextBuilder/NodeMapFormController.ContextBuilder.cs b/src-ref/UI/Game/ContextBuilder/NodeMapFormController.ContextBuilder.cs new file mode 100644 index 0000000..f81f21a --- /dev/null +++ b/src-ref/UI/Game/ContextBuilder/NodeMapFormController.ContextBuilder.cs @@ -0,0 +1,41 @@ +using GeometryTD.Definition; +using GeometryTD.Procedure; + +namespace GeometryTD.UI +{ + public sealed partial class NodeMapFormController + { + private static NodeMapFormContext BuildContext(NodeMapFormRawData rawData) + { + NodeItemContext[] nodeItemContexts = System.Array.Empty(); + if (rawData?.Nodes != null && rawData.Nodes.Count > 0) + { + nodeItemContexts = new NodeItemContext[rawData.Nodes.Count]; + for (int i = 0; i < rawData.Nodes.Count; i++) + { + NodeMapNodeRawData node = rawData.Nodes[i]; + nodeItemContexts[i] = new NodeItemContext + { + NodeId = node?.NodeId ?? 0, + SequenceIndex = node?.SequenceIndex ?? i, + NodeType = node?.NodeType ?? RunNodeType.None, + IsLocked = node != null && node.Status == RunNodeStatus.Locked, + IsCurrent = node != null && node.IsCurrentNode, + IsCompleted = node != null && node.Status == RunNodeStatus.Completed, + IsException = node != null && node.Status == RunNodeStatus.Exception, + CanClick = node != null && node.CanEnter + }; + } + } + + return new NodeMapFormContext + { + Title = rawData?.Title ?? "节点地图", + ProgressText = rawData?.ProgressText ?? "0 / 0", + CurrentNodeText = rawData?.CurrentNodeText ?? "当前节点: 无", + IsRunCompleted = rawData != null && rawData.IsRunCompleted, + NodeItems = nodeItemContexts + }; + } + } +} diff --git a/src-ref/UI/Game/ContextBuilder/RepoFormController.ContextBuilder.cs b/src-ref/UI/Game/ContextBuilder/RepoFormController.ContextBuilder.cs new file mode 100644 index 0000000..c8e8c08 --- /dev/null +++ b/src-ref/UI/Game/ContextBuilder/RepoFormController.ContextBuilder.cs @@ -0,0 +1,497 @@ +using System; +using System.Collections.Generic; +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.UI +{ + public partial class RepoFormController + { + private RepoFormContext BuildContext(RepoFormRawData rawData) + { + _itemClickActionMap.Clear(); + _itemDescSeedMap.Clear(); + _compAreaTowerIds.Clear(); + if (rawData?.Inventory == null) + { + _participantTowerIds.Clear(); + return null; + } + + Dictionary muzzleMap = BuildComponentMap(rawData.Inventory.MuzzleComponents); + Dictionary bearingMap = BuildComponentMap(rawData.Inventory.BearingComponents); + Dictionary baseMap = BuildComponentMap(rawData.Inventory.BaseComponents); + Dictionary towerMap = BuildTowerMap(rawData.Inventory.Towers); + HashSet selectedSellItemIds = BuildSelectedSellItemSet(rawData.SelectedSellItemIds); + bool isSellState = rawData.State == RepoFormState.Sell; + + List componentItems = new List(); + List towerItems = new List(); + List selectedSellComponentItems = new List(); + List selectedSellTowerItems = new List(); + + if (rawData.Inventory.Towers != null) + { + foreach (TowerItemData tower in rawData.Inventory.Towers) + { + if (tower == null) + { + continue; + } + + bool isParticipantTower = rawData.Inventory.ParticipantTowerInstanceIds != null && + rawData.Inventory.ParticipantTowerInstanceIds.Contains(tower.InstanceId); + bool isSellSelected = isSellState && selectedSellItemIds.Contains(tower.InstanceId); + + TowerRepoItemContext towerContext = BuildTowerRepoItemContext( + tower, + muzzleMap, + bearingMap, + baseMap, + RepoItemClickActionType.OpenDetail, + !isSellState, + isSellState, + !isParticipantTower, + isSellSelected, + false); + AddTowerItemContext(towerItems, towerContext); + if (isSellSelected) + { + selectedSellTowerItems.Add(towerContext); + } + + AddItemDescSeed( + tower.InstanceId, + tower.Name, + "Tower", + ItemDescUtility.BuildTowerDesc(tower, muzzleMap, bearingMap, baseMap) ?? string.Empty, + tower.Stats?.Tags, + tower.Stats?.TagRuntimes); + } + } + + if (rawData.Inventory.MuzzleComponents != null) + { + foreach (MuzzleCompItemData item in rawData.Inventory.MuzzleComponents) + { + if (item == null || item.IsAssembledIntoTower) + { + continue; + } + + RepoItemContext componentContext = BuildSellableComponentContext( + item, + TowerCompSlotType.Muzzle, + isSellState, + selectedSellItemIds.Contains(item.InstanceId)); + AddComponentItemContext(componentItems, componentContext); + if (componentContext.IsSellSelected) + { + selectedSellComponentItems.Add(componentContext); + } + + AddItemDescSeed( + item.InstanceId, + item.Name, + BuildComponentTypeText(item.SlotType), + ItemDescUtility.BuildMuzzleDesc(item) ?? string.Empty, + item.Tags, + null); + } + } + + if (rawData.Inventory.BearingComponents != null) + { + foreach (BearingCompItemData item in rawData.Inventory.BearingComponents) + { + if (item == null || item.IsAssembledIntoTower) + { + continue; + } + + RepoItemContext componentContext = BuildSellableComponentContext( + item, + TowerCompSlotType.Bearing, + isSellState, + selectedSellItemIds.Contains(item.InstanceId)); + AddComponentItemContext(componentItems, componentContext); + if (componentContext.IsSellSelected) + { + selectedSellComponentItems.Add(componentContext); + } + + AddItemDescSeed( + item.InstanceId, + item.Name, + BuildComponentTypeText(item.SlotType), + ItemDescUtility.BuildBearingDesc(item) ?? string.Empty, + item.Tags, + null); + } + } + + if (rawData.Inventory.BaseComponents != null) + { + foreach (BaseCompItemData item in rawData.Inventory.BaseComponents) + { + if (item == null || item.IsAssembledIntoTower) + { + continue; + } + + RepoItemContext componentContext = BuildSellableComponentContext( + item, + TowerCompSlotType.Base, + isSellState, + selectedSellItemIds.Contains(item.InstanceId)); + AddComponentItemContext(componentItems, componentContext); + if (componentContext.IsSellSelected) + { + selectedSellComponentItems.Add(componentContext); + } + + AddItemDescSeed( + item.InstanceId, + item.Name, + BuildComponentTypeText(item.SlotType), + ItemDescUtility.BuildBaseDesc(item) ?? string.Empty, + item.Tags, + null); + } + } + + ParticipantAreaContext participantAreaContext = + BuildParticipantAreaContext(rawData.Inventory, towerMap, muzzleMap, bearingMap, baseMap); + + return new RepoFormContext + { + GoldText = $"金币: {rawData.Inventory.Gold}", + State = rawData.State, + ShowSellModeButton = rawData.State == RepoFormState.Assemble, + SellModeButtonText = "出售模式", + ShowCombineArea = rawData.State == RepoFormState.Assemble, + ShowSellArea = rawData.State == RepoFormState.Sell, + CombineAreaContext = new CombineAreaContext(), + SellAreaContext = new SellAreaContext + { + TotalPriceText = $"总价值: {rawData.SelectedSellTotalPrice}", + CanConfirmSell = rawData.State == RepoFormState.Sell && rawData.SelectedSellItemCount > 0, + ComponentItems = selectedSellComponentItems.ToArray(), + TowerItems = selectedSellTowerItems.ToArray() + }, + CompAreaContext = new CompAreaContext + { + ComponentItems = componentItems.ToArray(), + TowerItems = towerItems.ToArray() + }, + ParticipantAreaContext = participantAreaContext + }; + } + + private static RepoItemContext BuildSellableComponentContext( + TowerCompItemData item, + TowerCompSlotType slotType, + bool isSellState, + bool isSellSelected) + { + return new RepoItemContext + { + InstanceId = item.InstanceId, + CanDrag = !isSellState, + EnduranceRate01 = ItemDescUtility.ResolveComponentEnduranceRate(item), + ClickActionType = RepoItemClickActionType.OpenDetail, + ComponentSlotType = slotType, + IsSellMode = isSellState, + IsSellable = true, + IsSellSelected = isSellState && isSellSelected, + IconAreaContext = BuildIconAreaContext(item) + }; + } + + private void AddComponentItemContext(List items, RepoItemContext itemContext) + { + if (itemContext == null) + { + return; + } + + items.Add(itemContext); + _itemClickActionMap[itemContext.InstanceId] = itemContext.ClickActionType; + } + + private void AddTowerItemContext(List items, TowerRepoItemContext itemContext) + { + if (itemContext == null) + { + return; + } + + items.Add(itemContext); + _itemClickActionMap[itemContext.InstanceId] = itemContext.ClickActionType; + _compAreaTowerIds.Add(itemContext.InstanceId); + } + + private void AddItemDescSeed( + long itemId, + string title, + string typeText, + string description, + TagType[] tags, + TagRuntimeData[] tagRuntimes) + { + if (itemId <= 0) + { + return; + } + + _itemDescSeedMap[itemId] = new ItemDescSeed + { + Title = string.IsNullOrWhiteSpace(title) ? $"Item {itemId}" : title, + TypeText = typeText ?? string.Empty, + Description = description ?? string.Empty, + Tags = tags, + TagRuntimes = tagRuntimes + }; + } + + private static string BuildComponentTypeText(TowerCompSlotType slotType) + { + return slotType switch + { + TowerCompSlotType.Muzzle => "Muzzle Component", + TowerCompSlotType.Bearing => "Bearing Component", + TowerCompSlotType.Base => "Base Component", + TowerCompSlotType.Accessory => "Accessory", + _ => "Component" + }; + } + + private static Dictionary BuildComponentMap(IReadOnlyList items) + where TComp : TowerCompItemData + { + Dictionary map = new Dictionary(); + if (items == null) + { + return map; + } + + foreach (TComp item in items) + { + if (item == null || item.InstanceId <= 0) + { + continue; + } + + map[item.InstanceId] = item; + } + + return map; + } + + private static HashSet BuildSelectedSellItemSet(IReadOnlyList selectedSellItemIds) + { + HashSet selectedIds = new HashSet(); + if (selectedSellItemIds == null) + { + return selectedIds; + } + + for (int i = 0; i < selectedSellItemIds.Count; i++) + { + long itemId = selectedSellItemIds[i]; + if (itemId > 0) + { + selectedIds.Add(itemId); + } + } + + return selectedIds; + } + + private static Dictionary BuildTowerMap(IReadOnlyList towers) + { + Dictionary map = new Dictionary(); + if (towers == null) + { + return map; + } + + foreach (TowerItemData tower in towers) + { + if (tower == null || tower.InstanceId <= 0) + { + continue; + } + + map[tower.InstanceId] = tower; + } + + return map; + } + + private ParticipantAreaContext BuildParticipantAreaContext( + BackpackInventoryData inventory, + IReadOnlyDictionary towerMap, + IReadOnlyDictionary muzzleMap, + IReadOnlyDictionary bearingMap, + IReadOnlyDictionary baseMap) + { + _participantTowerIds.Clear(); + List participantItems = new List(); + if (inventory?.ParticipantTowerInstanceIds != null && towerMap != null) + { + foreach (long towerId in inventory.ParticipantTowerInstanceIds) + { + if (towerId <= 0) + { + continue; + } + + if (!towerMap.TryGetValue(towerId, out TowerItemData tower) || tower == null) + { + continue; + } + + if (!_participantTowerIds.Add(towerId)) + { + continue; + } + + TowerRepoItemContext towerContext = BuildTowerRepoItemContext( + tower, + muzzleMap, + bearingMap, + baseMap, + RepoItemClickActionType.RemoveParticipant, + false, + false, + false, + false, + false); + if (towerContext != null) + { + participantItems.Add(towerContext); + _itemClickActionMap[towerContext.InstanceId] = towerContext.ClickActionType; + } + } + } + + return new ParticipantAreaContext + { + TowerItems = participantItems.ToArray(), + MaxCount = MaxParticipantCount + }; + } + + private void ApplyParticipantSelection() + { + if (Form == null || _compAreaTowerIds.Count <= 0) + { + return; + } + + if (Context != null && Context.State == RepoFormState.Sell) + { + return; + } + + foreach (long towerId in _compAreaTowerIds) + { + Form.SetRepoItemSelected(towerId, _participantTowerIds.Contains(towerId)); + } + } + + private void RefreshParticipantAreaOnly() + { + if (_useCase == null || Form == null) + { + return; + } + + RepoFormContext latestContext = BuildContext(_useCase.CreateInitialModel()); + if (latestContext?.ParticipantAreaContext == null) + { + return; + } + + Form.RefreshGoldText(latestContext.GoldText); + Form.RefreshParticipantArea(latestContext.ParticipantAreaContext); + ApplyParticipantSelection(); + } + + private static TowerRepoItemContext BuildTowerRepoItemContext( + TowerItemData tower, + IReadOnlyDictionary muzzleMap, + IReadOnlyDictionary bearingMap, + IReadOnlyDictionary baseMap, + RepoItemClickActionType clickActionType, + bool canDrag, + bool isSellMode, + bool isSellable, + bool isSellSelected, + bool highlightSelected) + { + if (tower == null) + { + return null; + } + + return new TowerRepoItemContext + { + InstanceId = tower.InstanceId, + CanDrag = canDrag, + EnduranceRate01 = ItemDescUtility.ResolveTowerEnduranceRate(tower, muzzleMap, bearingMap, baseMap), + ClickActionType = clickActionType, + ComponentSlotType = TowerCompSlotType.None, + IsSellMode = isSellMode, + IsSellable = isSellable, + IsSellSelected = isSellSelected || highlightSelected, + IconAreaContext = new TowerIconAreaContext + { + Rarity = tower.Rarity, + MuzzleColor = ResolveComponentColor(tower.MuzzleComponentInstanceId, muzzleMap), + BearingColor = ResolveComponentColor(tower.BearingComponentInstanceId, bearingMap), + BaseColor = ResolveComponentColor(tower.BaseComponentInstanceId, baseMap) + } + }; + } + + private static IconAreaContext BuildIconAreaContext(TowerCompItemData item) + { + if (item == null) + { + return new IconAreaContext + { + ComponentSlotType = TowerCompSlotType.None, + Rarity = RarityType.None, + Color = Color.white, + Icon = null + }; + } + + return new IconAreaContext + { + ComponentSlotType = item.SlotType, + Rarity = item.Rarity, + Color = IconColorGenerator.GenerateForComponent(item), + Icon = null + }; + } + + private static Color ResolveComponentColor(long instanceId, + IReadOnlyDictionary componentMap) + where TComp : TowerCompItemData + { + if (instanceId > 0 && + componentMap != null && + componentMap.TryGetValue(instanceId, out TComp component) && + component != null) + { + return IconColorGenerator.GenerateForComponent(component); + } + + return Color.white; + } + } +} diff --git a/src-ref/UI/Game/ContextBuilder/RewardSelectFormController.ContextBuilder.cs b/src-ref/UI/Game/ContextBuilder/RewardSelectFormController.ContextBuilder.cs new file mode 100644 index 0000000..995e3be --- /dev/null +++ b/src-ref/UI/Game/ContextBuilder/RewardSelectFormController.ContextBuilder.cs @@ -0,0 +1,142 @@ +using System; +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.UI +{ + public partial class RewardSelectFormController + { + private static RewardSelectFormContext BuildContext(RewardSelectFormRawData rawData) + { + if (rawData == null) + { + return null; + } + + return new RewardSelectFormContext + { + TipText = string.IsNullOrWhiteSpace(rawData.TipText) ? "Select one reward" : rawData.TipText, + RefreshButtonText = BuildRefreshButtonText(rawData.RefreshCost, rawData.CanRefresh), + CanRefresh = rawData.CanRefresh, + CanGiveUp = rawData.CanGiveUp, + RewardItems = BuildRewardItemContexts(rawData.RewardItems) + }; + } + + private static string BuildRefreshButtonText(int refreshCost, bool canRefresh) + { + if (!canRefresh) + { + return "Refreshed"; + } + + if (refreshCost <= 0) + { + return "Refresh"; + } + + return $"Refresh -{refreshCost}"; + } + + private static RewardItemContext[] BuildRewardItemContexts(RewardSelectItemRawData[] rawItems) + { + if (rawItems == null || rawItems.Length <= 0) + { + return Array.Empty(); + } + + RewardItemContext[] contexts = new RewardItemContext[rawItems.Length]; + for (int i = 0; i < rawItems.Length; i++) + { + RewardSelectItemRawData rawItem = rawItems[i]; + if (rawItem == null) + { + continue; + } + + contexts[i] = new RewardItemContext + { + Index = i, + IconArea = new IconAreaContext + { + ComponentSlotType = rawItem.SlotType, + Icon = rawItem.Icon, + Rarity = rawItem.Rarity, + Color = ResolveIconColor(rawItem.SourceItem) + }, + Title = rawItem.Title ?? string.Empty, + TypeText = ResolveTypeText(rawItem.TypeText, rawItem.SlotType), + Description = BuildDescription(rawItem), + Tags = BuildTagContexts(rawItem), + }; + } + + return contexts; + } + + private static string BuildDescription(RewardSelectItemRawData rawItem) + { + string baseDescription = rawItem.Description; + string tagDescription = TagDisplayUtility.BuildTagDescriptionText(rawItem.Tags); + if (string.IsNullOrWhiteSpace(tagDescription)) + { + return baseDescription; + } + + if (string.IsNullOrWhiteSpace(baseDescription)) + { + return tagDescription; + } + + return $"{baseDescription}\n{tagDescription}"; + } + + private static Color ResolveIconColor(TowerCompItemData sourceItem) + { + if (sourceItem == null) + { + return Color.white; + } + + return IconColorGenerator.GenerateForComponent(sourceItem); + } + + private static string ResolveTypeText(string typeText, TowerCompSlotType slotType) + { + if (!string.IsNullOrWhiteSpace(typeText)) + { + return typeText; + } + + return slotType switch + { + TowerCompSlotType.Muzzle => "Muzzle Component", + TowerCompSlotType.Bearing => "Bearing Component", + TowerCompSlotType.Base => "Base Component", + TowerCompSlotType.Accessory => "Accessory", + _ => "Component" + }; + } + + private static TagItemContext[] BuildTagContexts(RewardSelectItemRawData rawItem) + { + string[] tagTexts = TagDisplayUtility.BuildTagTexts(rawItem?.Tags); + if (tagTexts == null || tagTexts.Length <= 0) + { + return Array.Empty(); + } + + TagItemContext[] contexts = new TagItemContext[tagTexts.Length]; + for (int i = 0; i < tagTexts.Length; i++) + { + contexts[i] = new TagItemContext + { + TagName = tagTexts[i] ?? string.Empty + }; + } + + return contexts; + } + } +} \ No newline at end of file diff --git a/src-ref/UI/Game/Controller/EventFormController.cs b/src-ref/UI/Game/Controller/EventFormController.cs new file mode 100644 index 0000000..92493f2 --- /dev/null +++ b/src-ref/UI/Game/Controller/EventFormController.cs @@ -0,0 +1,129 @@ +using GeometryTD.Definition; +using GeometryTD.CustomEvent; +using GameFramework.Event; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class EventFormController : UIFormControllerCommonBase + { + private EventFormUseCase m_UseCase; + + protected override UIFormType UIFormTypeId => UIFormType.EventForm; + + protected override void RefreshUI(EventForm form, EventFormContext context) + { + form.RefreshUI(context); + } + + protected override void SubscribeCustomEvents() + { + GameEntry.Event.Subscribe(EventOptionItemSelectedEventArgs.EventId, OnEventOptionItemSelected); + } + + protected override void UnsubscribeCustomEvents() + { + GameEntry.Event.Unsubscribe(EventOptionItemSelectedEventArgs.EventId, OnEventOptionItemSelected); + } + + public override int? OpenUI(object userData = null) + { + if (userData is EventFormContext eventFormContext) + { + return OpenUIInternal(eventFormContext); + } + + if (userData is EventFormRawData rawDataFromUserData) + { + return OpenUI(rawDataFromUserData); + } + + if (userData != null) + { + Log.Warning("EventFormController.OpenUI() userData type is invalid."); + return null; + } + + if (m_UseCase == null) + { + Log.Error("EventFormController.OpenUI() useCase is null."); + return null; + } + + EventFormRawData rawData = m_UseCase.CreateInitialModel(); + return OpenUI(rawData); + } + + public int? OpenUI(EventFormRawData rawData) + { + EventFormContext context = BuildContext(rawData); + return OpenUIInternal(context); + } + + public override void BindUseCase(IUIUseCase useCase) + { + if (!(useCase is EventFormUseCase eventFormUseCase)) + { + Log.Error("EventFormController.BindUseCase() useCase is invalid."); + return; + } + + m_UseCase = eventFormUseCase; + } + + private static EventFormContext BuildContext(EventFormRawData rawData) + { + if (rawData == null) + { + return null; + } + + EventOptionItemContext[] options = new EventOptionItemContext[4]; + for (int i = 0; i < options.Length; i++) + { + string optionText = string.Empty; + bool isSelectable = false; + string blockedReason = string.Empty; + if (rawData.OptionItems != null && i < rawData.OptionItems.Length && + rawData.OptionItems[i] != null) + { + optionText = rawData.OptionItems[i].OptionText; + isSelectable = rawData.OptionItems[i].IsSelectable; + blockedReason = rawData.OptionItems[i].BlockedReason; + } + + options[i] = new EventOptionItemContext + { + OptionIndex = i, + OptionText = optionText, + IsSelectable = isSelectable, + BlockedReason = blockedReason + }; + } + + return new EventFormContext + { + EventId = rawData.EventId, + Title = rawData.Title, + Description = rawData.Description, + OptionItems = options + }; + } + + private void OnEventOptionItemSelected(object sender, GameEventArgs e) + { + if (!(e is EventOptionItemSelectedEventArgs args)) + { + return; + } + + if (m_UseCase == null) + { + Log.Warning("EventFormController.OnOptionSelected() useCase is null."); + return; + } + + m_UseCase.TrySelectOption(args.SelectedItemId); + } + } +} diff --git a/src-ref/UI/Game/Controller/MainFormController.cs b/src-ref/UI/Game/Controller/MainFormController.cs new file mode 100644 index 0000000..7ba1f4f --- /dev/null +++ b/src-ref/UI/Game/Controller/MainFormController.cs @@ -0,0 +1,79 @@ +using GeometryTD.CustomEvent; +using GeometryTD.Definition; +using GameFramework.Event; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class MainFormController : UIFormControllerCommonBase + { + protected override UIFormType UIFormTypeId => UIFormType.MainForm; + + protected override void RefreshUI(MainForm form, MainFormContext context) + { + form.RefreshUI(context); + } + + protected override void SubscribeCustomEvents() + { + GameEntry.Event.Subscribe(ReturnButtonClickedEventArgs.EventId, OnReturnButtonClicked); + GameEntry.Event.Subscribe(RepoButtonClickedEventArgs.EventId, OnRepoButtonClicked); + } + + protected override void UnsubscribeCustomEvents() + { + GameEntry.Event.Unsubscribe(ReturnButtonClickedEventArgs.EventId, OnReturnButtonClicked); + GameEntry.Event.Unsubscribe(RepoButtonClickedEventArgs.EventId, OnRepoButtonClicked); + } + + public override int? OpenUI(object userData = null) + { + if (userData is MainFormContext context) + { + return OpenUIInternal(context); + } + + if (userData != null) + { + Log.Warning("MainFormController.OpenUI() userData type is invalid."); + return null; + } + + return OpenUIInternal(BuildDefaultContext()); + } + + public override void BindUseCase(IUIUseCase useCase) + { + if (useCase != null) + { + Log.Warning("MainFormController does not use a use case."); + } + } + + private static MainFormContext BuildDefaultContext() + { + return new MainFormContext(); + } + + private void OnReturnButtonClicked(object sender, GameEventArgs e) + { + if (!(sender is MainForm) || !(e is ReturnButtonClickedEventArgs)) + { + return; + } + + //TODO:游戏 UI 返回菜单 UI + //GameEntry.UIRouter.CloseUI(UIFormType.MainForm); + } + + private void OnRepoButtonClicked(object sender, GameEventArgs e) + { + if (!(sender is MainForm) || !(e is RepoButtonClickedEventArgs)) + { + return; + } + + GameEntry.UIRouter.OpenUI(UIFormType.RepoForm); + } + } +} \ No newline at end of file diff --git a/src-ref/UI/Game/Controller/NodeMapFormController.cs b/src-ref/UI/Game/Controller/NodeMapFormController.cs new file mode 100644 index 0000000..07fa774 --- /dev/null +++ b/src-ref/UI/Game/Controller/NodeMapFormController.cs @@ -0,0 +1,130 @@ +using GeometryTD.CustomEvent; +using GeometryTD.Definition; +using GeometryTD.Procedure; +using GameFramework.Event; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public sealed partial class NodeMapFormController : UIFormControllerCommonBase + { + private NodeMapFormUseCase _useCase; + + protected override UIFormType UIFormTypeId => UIFormType.NodeMapForm; + + protected override void RefreshUI(NodeMapForm form, NodeMapFormContext context) + { + form.RefreshUI(context); + } + + protected override void SubscribeCustomEvents() + { + GameEntry.Event.Subscribe(NodeMapNodeClickEventArgs.EventId, OnNodeClicked); + } + + protected override void UnsubscribeCustomEvents() + { + GameEntry.Event.Unsubscribe(NodeMapNodeClickEventArgs.EventId, OnNodeClicked); + } + + public override int? OpenUI(object userData = null) + { + if (userData is NodeMapFormContext context) + { + return OpenUIInternal(context); + } + + if (userData is NodeMapFormRawData rawDataFromUserData) + { + return OpenUI(rawDataFromUserData); + } + + if (userData != null) + { + Log.Warning("NodeMapFormController.OpenUI() userData type is invalid."); + return null; + } + + if (_useCase == null) + { + Log.Error("NodeMapFormController.OpenUI() useCase is null."); + return null; + } + + return OpenUI(_useCase.CreateInitialModel()); + } + + public int? OpenUI(NodeMapFormRawData rawData) + { + return OpenUIInternal(BuildContext(rawData)); + } + + public override void BindUseCase(IUIUseCase useCase) + { + if (!(useCase is NodeMapFormUseCase nodeMapFormUseCase)) + { + Log.Error("NodeMapFormController.BindUseCase() useCase is invalid."); + return; + } + + _useCase = nodeMapFormUseCase; + } + + private void OnNodeClicked(object sender, GameEventArgs e) + { + if (!IsEventFromCurrentForm(sender) || !(e is NodeMapNodeClickEventArgs args) || _useCase == null) + { + return; + } + + if (!_useCase.TryRequestEnterNode(args.SequenceIndex, out NodeMapNodeRawData selectedNode, out NodeMapFormRawData latestRawData)) + { + if (latestRawData != null) + { + SetContext(BuildContext(latestRawData)); + RefreshCurrentUI(); + } + + return; + } + + if (latestRawData != null) + { + SetContext(BuildContext(latestRawData)); + RefreshCurrentUI(); + } + + if (Form != null && selectedNode != null) + { + GameEntry.Event.Fire(Form, NodeMapNodeEnterRequestedEventArgs.Create( + selectedNode.RunId, + selectedNode.NodeId, + selectedNode.NodeType, + selectedNode.SequenceIndex)); + } + } + + private bool IsEventFromCurrentForm(object sender) + { + if (Form == null) + { + return false; + } + + if (ReferenceEquals(sender, Form)) + { + return true; + } + + if (sender is Component component) + { + NodeMapForm ownerForm = component.GetComponentInParent(); + return ownerForm == Form; + } + + return false; + } + + } +} diff --git a/src-ref/UI/Game/Controller/RepoFormController.cs b/src-ref/UI/Game/Controller/RepoFormController.cs new file mode 100644 index 0000000..45e319b --- /dev/null +++ b/src-ref/UI/Game/Controller/RepoFormController.cs @@ -0,0 +1,425 @@ +using System.Collections.Generic; +using GeometryTD.CustomEvent; +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using GameFramework.Event; +using GeometryTD.CustomComponent; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public partial class RepoFormController : UIFormControllerCommonBase + { + private const int MaxParticipantCount = 4; + private RepoFormUseCase _useCase; + private readonly Dictionary _itemClickActionMap = + new Dictionary(); + private readonly Dictionary _itemDescSeedMap = new Dictionary(); + private readonly HashSet _participantTowerIds = new HashSet(); + private readonly HashSet _compAreaTowerIds = new HashSet(); + + private sealed class ItemDescSeed + { + public string Title; + public string TypeText; + public string Description; + public TagType[] Tags; + public TagRuntimeData[] TagRuntimes; + } + + protected override UIFormType UIFormTypeId => UIFormType.RepoForm; + + protected override void RefreshUI(RepoForm form, RepoFormContext context) + { + form.RefreshUI(context); + ApplyParticipantSelection(); + } + + protected override void SubscribeCustomEvents() + { + GameEntry.Event.Subscribe(RepoItemClickedEventArgs.EventId, OnRepoItemClicked); + GameEntry.Event.Subscribe(RepoItemDragEndedEventArgs.EventId, OnRepoItemDragEnded); + GameEntry.Event.Subscribe(CombineSlotClickedEventArgs.EventId, OnCombineSlotClicked); + GameEntry.Event.Subscribe(RepoCombineRequestedEventArgs.EventId, OnRepoCombineRequested); + GameEntry.Event.Subscribe(RepoParticipantAssignRequestedEventArgs.EventId, OnRepoParticipantAssignRequested); + GameEntry.Event.Subscribe(RepoFormReturnEventArgs.EventId, OnRepoFormReturn); + GameEntry.Event.Subscribe(RepoSellModeToggleRequestedEventArgs.EventId, OnRepoSellModeToggleRequested); + GameEntry.Event.Subscribe(RepoSellCancelRequestedEventArgs.EventId, OnRepoSellCancelRequested); + GameEntry.Event.Subscribe(RepoSellConfirmRequestedEventArgs.EventId, OnRepoSellConfirmRequested); + } + + protected override void UnsubscribeCustomEvents() + { + GameEntry.Event.Unsubscribe(RepoItemClickedEventArgs.EventId, OnRepoItemClicked); + GameEntry.Event.Unsubscribe(RepoItemDragEndedEventArgs.EventId, OnRepoItemDragEnded); + GameEntry.Event.Unsubscribe(CombineSlotClickedEventArgs.EventId, OnCombineSlotClicked); + GameEntry.Event.Unsubscribe(RepoCombineRequestedEventArgs.EventId, OnRepoCombineRequested); + GameEntry.Event.Unsubscribe(RepoParticipantAssignRequestedEventArgs.EventId, OnRepoParticipantAssignRequested); + GameEntry.Event.Unsubscribe(RepoFormReturnEventArgs.EventId, OnRepoFormReturn); + GameEntry.Event.Unsubscribe(RepoSellModeToggleRequestedEventArgs.EventId, OnRepoSellModeToggleRequested); + GameEntry.Event.Unsubscribe(RepoSellCancelRequestedEventArgs.EventId, OnRepoSellCancelRequested); + GameEntry.Event.Unsubscribe(RepoSellConfirmRequestedEventArgs.EventId, OnRepoSellConfirmRequested); + } + + public override int? OpenUI(object userData = null) + { + if (userData is RepoFormContext repoFormContext) + { + return OpenUIInternal(repoFormContext); + } + + if (userData is RepoFormRawData rawDataFromUserData) + { + return OpenUI(rawDataFromUserData); + } + + if (userData != null) + { + Log.Warning("RepoFormController.OpenUI() userData type is invalid."); + return null; + } + + if (_useCase == null) + { + Log.Error("RepoFormController.OpenUI() useCase is null."); + return null; + } + + RepoFormRawData rawData = _useCase.CreateInitialModel(); + return OpenUI(rawData); + } + + public int? OpenUI(RepoFormRawData rawData) + { + RepoFormContext context = BuildContext(rawData); + return OpenUIInternal(context); + } + + public override void BindUseCase(IUIUseCase useCase) + { + if (!(useCase is RepoFormUseCase repoFormUseCase)) + { + Log.Error("RepoFormController.BindUseCase() useCase is invalid."); + return; + } + + _useCase = repoFormUseCase; + } + + #region Event Handlers + + private void OnRepoItemClicked(object sender, GameEventArgs e) + { + if (!IsEventFromCurrentForm(sender)) + { + return; + } + + if (!(e is RepoItemClickedEventArgs args)) + { + return; + } + + RepoItemClickActionType clickActionType = RepoItemClickActionType.OpenDetail; + if (_itemClickActionMap.TryGetValue(args.ItemId, out RepoItemClickActionType mappedAction)) + { + clickActionType = mappedAction; + } + + if (args.ItemId <= 0) + { + return; + } + + if (_useCase != null && _useCase.State == RepoFormState.Sell) + { + if (_useCase.TryToggleSellSelection( + args.ItemId, + out RepoFormRawData sellModeRawData, + out PlayerInventorySaleFailureReason failureReason)) + { + SetContext(BuildContext(sellModeRawData)); + RefreshCurrentUI(); + } + else if (failureReason == PlayerInventorySaleFailureReason.ParticipantTower) + { + OpenSellBlockedDialog("参战防御塔不能直接出售,请先移出参战区。"); + } + + return; + } + + if (clickActionType == RepoItemClickActionType.RemoveParticipant) + { + if (_useCase == null || Form == null) + { + return; + } + + if (!_useCase.TryRemoveParticipantTower(args.ItemId)) + { + return; + } + + RefreshParticipantAreaOnly(); + return; + } + + if (!_itemDescSeedMap.TryGetValue(args.ItemId, out ItemDescSeed seed)) + { + return; + } + + GameEntry.UIRouter.OpenUI(UIFormType.ItemDescForm, new ItemDescFormRawData + { + Title = seed.Title, + TypeText = seed.TypeText, + Description = seed.Description ?? string.Empty, + Price = 0, + ScreenPosition = args.ScreenPosition, + Tags = seed.Tags, + TagRuntimes = seed.TagRuntimes + }); + } + + private void OnRepoItemDragEnded(object sender, GameEventArgs e) + { + if (!IsEventFromCurrentForm(sender)) + { + return; + } + + if (Context != null && Context.State == RepoFormState.Sell) + { + return; + } + + if (!(e is RepoItemDragEndedEventArgs args)) + { + return; + } + + if (Form == null || !_itemClickActionMap.ContainsKey(args.ItemId)) + { + return; + } + + bool isSelected = _compAreaTowerIds.Contains(args.ItemId) + ? _participantTowerIds.Contains(args.ItemId) + : args.Assigned; + Form.SetRepoItemSelected(args.ItemId, isSelected); + } + + private void OnCombineSlotClicked(object sender, GameEventArgs e) + { + if (!IsEventFromCurrentForm(sender)) + { + return; + } + + if (Context != null && Context.State == RepoFormState.Sell) + { + return; + } + + if (!(e is CombineSlotClickedEventArgs args)) + { + return; + } + + if (Form == null) + { + return; + } + + if (Form.TryClearCombineSlot(args.SlotIndex, out long removedItemId) && removedItemId > 0) + { + Form.SetRepoItemSelected(removedItemId, false); + } + } + + private void OnRepoFormReturn(object sender, GameEventArgs e) + { + if (!IsEventFromCurrentForm(sender) || !(e is RepoFormReturnEventArgs)) + { + return; + } + + if (Form == null) + { + return; + } + + CloseUI(); + } + + private void OnRepoCombineRequested(object sender, GameEventArgs e) + { + if (!IsEventFromCurrentForm(sender)) + { + return; + } + + if (!(e is RepoCombineRequestedEventArgs args)) + { + return; + } + + if (_useCase == null || Form == null) + { + return; + } + + if (!_useCase.TryAssembleTower(args.MuzzleItemId, args.BearingItemId, args.BaseItemId)) + { + return; + } + + RepoFormRawData latestRawData = _useCase.CreateInitialModel(); + RepoFormContext latestContext = BuildContext(latestRawData); + if (latestContext != null) + { + SetContext(latestContext); + RefreshCurrentUI(); + } + } + + private void OnRepoParticipantAssignRequested(object sender, GameEventArgs e) + { + if (!IsEventFromCurrentForm(sender)) + { + return; + } + + if (Context != null && Context.State == RepoFormState.Sell) + { + return; + } + + if (!(e is RepoParticipantAssignRequestedEventArgs args)) + { + return; + } + + if (_useCase == null || Form == null || args.TowerItemId <= 0) + { + return; + } + + ParticipantTowerAssignResult result = _useCase.TryAddParticipantTower(args.TowerItemId); + if (result == null || !result.IsSuccess) + { + if (result != null) + { + Log.Warning( + "RepoFormController denied participant assignment. TowerId={0}, FailureReason={1}, ValidationFailureReason={2}.", + result.TowerInstanceId, + result.FailureReason, + result.ValidationFailureReason); + GameEntry.UIRouter.OpenUI( + UIFormType.DialogForm, + RepoParticipantAssignDialogUtility.BuildDialog(result, MaxParticipantCount)); + } + + return; + } + + RefreshParticipantAreaOnly(); + } + + private void OnRepoSellModeToggleRequested(object sender, GameEventArgs e) + { + if (!IsEventFromCurrentForm(sender) || !(e is RepoSellModeToggleRequestedEventArgs) || _useCase == null) + { + return; + } + + if (_useCase.State != RepoFormState.Assemble) + { + return; + } + + RepoFormRawData rawData = _useCase.EnterSellState(); + SetContext(BuildContext(rawData)); + RefreshCurrentUI(); + } + + private void OnRepoSellCancelRequested(object sender, GameEventArgs e) + { + if (!IsEventFromCurrentForm(sender) || !(e is RepoSellCancelRequestedEventArgs) || _useCase == null) + { + return; + } + + if (_useCase.State != RepoFormState.Sell) + { + return; + } + + SetContext(BuildContext(_useCase.ExitSellState())); + RefreshCurrentUI(); + } + + private void OnRepoSellConfirmRequested(object sender, GameEventArgs e) + { + if (!IsEventFromCurrentForm(sender) || !(e is RepoSellConfirmRequestedEventArgs) || _useCase == null) + { + return; + } + + if (!_useCase.TryConfirmSellSelection(out RepoFormRawData rawData, out PlayerInventorySaleResult result)) + { + if (result != null && result.FailureReason == PlayerInventorySaleFailureReason.ParticipantTower) + { + OpenSellBlockedDialog("参战防御塔不能直接出售,请先移出参战区。"); + } + + return; + } + + SetContext(BuildContext(rawData)); + RefreshCurrentUI(); + + if (GameEntry.UI.GetUIForm(UIFormType.ShopForm) != null) + { + GameEntry.UIRouter.OpenUI(UIFormType.ShopForm); + } + } + + private bool IsEventFromCurrentForm(object sender) + { + if (Form == null) + { + return false; + } + + if (ReferenceEquals(sender, Form)) + { + return true; + } + + if (sender is Component component) + { + RepoForm ownerForm = component.GetComponentInParent(); + return ownerForm == Form; + } + + return false; + } + + private static void OpenSellBlockedDialog(string message) + { + GameEntry.UIRouter.OpenUI(UIFormType.DialogForm, new DialogFormRawData + { + Mode = 1, + Title = "无法出售", + Message = message, + PauseGame = false, + ConfirmText = "知道了" + }); + } + + #endregion + } +} diff --git a/src-ref/UI/Game/RawData/EventFormRawData.cs b/src-ref/UI/Game/RawData/EventFormRawData.cs new file mode 100644 index 0000000..5b9f5a9 --- /dev/null +++ b/src-ref/UI/Game/RawData/EventFormRawData.cs @@ -0,0 +1,18 @@ +namespace GeometryTD.UI +{ + public class EventFormRawData + { + public int EventId; + public string Title; + public string Description; + public EventOptionRawData[] OptionItems; + } + + public sealed class EventOptionRawData + { + public int OptionIndex; + public string OptionText; + public bool IsSelectable; + public string BlockedReason; + } +} diff --git a/src-ref/UI/Game/RawData/NodeMapFormRawData.cs b/src-ref/UI/Game/RawData/NodeMapFormRawData.cs new file mode 100644 index 0000000..30ac6b8 --- /dev/null +++ b/src-ref/UI/Game/RawData/NodeMapFormRawData.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using GeometryTD.Procedure; + +namespace GeometryTD.UI +{ + public sealed class NodeMapFormRawData + { + public string RunId; + public string Title; + public string ProgressText; + public string CurrentNodeText; + public bool IsRunCompleted; + public List Nodes; + } + + public sealed class NodeMapNodeRawData + { + public string RunId; + public int NodeId; + public int SequenceIndex; + public RunNodeType NodeType; + public RunNodeStatus Status; + public bool IsCurrentNode; + public bool CanEnter; + public string Title; + public string StatusText; + } +} diff --git a/src-ref/UI/Game/RawData/RepoFormRawData.cs b/src-ref/UI/Game/RawData/RepoFormRawData.cs new file mode 100644 index 0000000..fa01232 --- /dev/null +++ b/src-ref/UI/Game/RawData/RepoFormRawData.cs @@ -0,0 +1,13 @@ +using GeometryTD.Definition; + +namespace GeometryTD.UI +{ + public class RepoFormRawData + { + public BackpackInventoryData Inventory; + public RepoFormState State; + public long[] SelectedSellItemIds; + public int SelectedSellItemCount; + public int SelectedSellTotalPrice; + } +} diff --git a/src-ref/UI/Game/RepoParticipantAssignDialogUtility.cs b/src-ref/UI/Game/RepoParticipantAssignDialogUtility.cs new file mode 100644 index 0000000..39da8af --- /dev/null +++ b/src-ref/UI/Game/RepoParticipantAssignDialogUtility.cs @@ -0,0 +1,45 @@ +using GeometryTD.Definition; + +namespace GeometryTD.UI +{ + public static class RepoParticipantAssignDialogUtility + { + public static DialogFormRawData BuildDialog( + ParticipantTowerAssignResult result, + int maxParticipantCount) + { + return new DialogFormRawData + { + Mode = 1, + Title = "无法加入参战区", + Message = BuildMessage(result, maxParticipantCount), + PauseGame = false, + ConfirmText = "知道了" + }; + } + + private static string BuildMessage( + ParticipantTowerAssignResult result, + int maxParticipantCount) + { + if (result == null) + { + return "当前无法加入参战区,请稍后重试。"; + } + + switch (result.FailureReason) + { + case ParticipantTowerAssignFailureReason.TowerMissing: + return $"塔 #{result.TowerInstanceId} 已不存在,无法加入参战区。"; + case ParticipantTowerAssignFailureReason.InvalidTower: + return $"塔 #{result.TowerInstanceId} {CombatParticipantTowerValidationText.GetFailureReasonMessage(result.ValidationFailureReason)}"; + case ParticipantTowerAssignFailureReason.AlreadyAssigned: + return $"塔 #{result.TowerInstanceId} 已在参战区中。"; + case ParticipantTowerAssignFailureReason.ParticipantAreaFull: + return $"参战区已满,最多只能放入 {maxParticipantCount} 座塔。"; + default: + return "当前无法加入参战区,请稍后重试。"; + } + } + } +} diff --git a/src-ref/UI/Game/UseCase/EventFormUseCase.cs b/src-ref/UI/Game/UseCase/EventFormUseCase.cs new file mode 100644 index 0000000..39ea5b7 --- /dev/null +++ b/src-ref/UI/Game/UseCase/EventFormUseCase.cs @@ -0,0 +1,106 @@ +using GeometryTD.Definition; +using GeometryTD.Procedure; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class EventFormUseCase : IUIUseCase + { + private EventItem _currentEvent; + private RunNodeExecutionContext _currentContext; + private readonly EventOptionExecutor _executor = new EventOptionExecutor(); + + public void BindEvent( + EventItem eventItem, + RunNodeExecutionContext context) + { + _currentEvent = eventItem; + _currentContext = context != null ? context.Clone() : null; + } + + public void Clear() + { + _currentEvent = null; + _currentContext = null; + } + + public EventFormRawData CreateInitialModel() + { + if (_currentEvent == null) + { + return null; + } + + return new EventFormRawData + { + EventId = _currentEvent.Id, + Title = _currentEvent.Title, + Description = _currentEvent.Description, + OptionItems = BuildOptionItems() + }; + } + + public bool TrySelectOption(int optionIndex) + { + if (_currentEvent == null || _currentEvent.Options == null) + { + return false; + } + + if (optionIndex < 0 || optionIndex >= _currentEvent.Options.Length) + { + Log.Warning("EventFormUseCase.TrySelectOption() option index is invalid: {0}", optionIndex); + return false; + } + + BackpackInventoryData workingInventory = GameEntry.PlayerInventory != null + ? GameEntry.PlayerInventory.GetInventorySnapshot() + : new BackpackInventoryData(); + EventOptionExecutionResult executionResult = _executor.Execute( + _currentEvent, + optionIndex, + _currentContext, + workingInventory); + if (!executionResult.IsAccepted) + { + Log.Warning( + "EventFormUseCase.TrySelectOption() rejected. EventId={0}, OptionIndex={1}, Reason={2}", + _currentEvent.Id, + optionIndex, + executionResult.FailureReason); + return false; + } + + if (GameEntry.PlayerInventory != null) + { + GameEntry.PlayerInventory.ReplaceInventorySnapshot(workingInventory); + } + + GameEntry.EventNode.EndEvent(); + return true; + } + + private EventOptionRawData[] BuildOptionItems() + { + EventOption[] options = _currentEvent.Options ?? System.Array.Empty(); + EventOptionRawData[] result = new EventOptionRawData[options.Length]; + BackpackInventoryData inventory = GameEntry.PlayerInventory != null + ? GameEntry.PlayerInventory.GetInventorySnapshot() + : new BackpackInventoryData(); + for (int i = 0; i < options.Length; i++) + { + EventOption option = options[i]; + EventOptionAvailability availability = _executor.EvaluateOption(option, inventory); + result[i] = new EventOptionRawData + { + OptionIndex = i, + OptionText = option?.OptionText ?? string.Empty, + IsSelectable = availability.IsSelectable, + BlockedReason = availability.BlockedReason + }; + } + + return result; + } + } +} diff --git a/src-ref/UI/Game/UseCase/NodeMapFormUseCase.cs b/src-ref/UI/Game/UseCase/NodeMapFormUseCase.cs new file mode 100644 index 0000000..c47cebc --- /dev/null +++ b/src-ref/UI/Game/UseCase/NodeMapFormUseCase.cs @@ -0,0 +1,156 @@ +using System.Collections.Generic; +using GeometryTD.Procedure; + +namespace GeometryTD.UI +{ + public sealed class NodeMapFormUseCase : IUIUseCase + { + private RunState _runState; + + public void SetRunState(RunState runState) + { + _runState = runState; + } + + public NodeMapFormRawData CreateInitialModel() + { + return BuildRawData(_runState); + } + + public bool TryRequestEnterNode(int sequenceIndex, out NodeMapNodeRawData selectedNode, out NodeMapFormRawData latestRawData) + { + latestRawData = BuildRawData(_runState); + selectedNode = null; + + if (_runState == null || _runState.IsCompleted) + { + return false; + } + + RunNodeState currentNode = _runState.CurrentNode; + if (currentNode == null || currentNode.SequenceIndex != sequenceIndex || !_runState.CanEnterCurrentNode) + { + return false; + } + + RunNodeState targetNode = _runState.GetNodeBySequenceIndex(sequenceIndex); + if (targetNode == null || targetNode.Status != RunNodeStatus.Available) + { + return false; + } + + selectedNode = BuildNodeRawData(_runState.RunId, targetNode, currentNode.SequenceIndex); + return selectedNode != null && selectedNode.CanEnter; + } + + private static NodeMapFormRawData BuildRawData(RunState runState) + { + if (runState == null) + { + return new NodeMapFormRawData + { + Title = "节点地图", + ProgressText = "0 / 0", + CurrentNodeText = "当前节点: 无", + IsRunCompleted = true, + Nodes = new List() + }; + } + + List nodes = new List(runState.NodeCount); + int currentSequenceIndex = runState.CurrentNode != null ? runState.CurrentNode.SequenceIndex : -1; + foreach (RunNodeState node in runState.Nodes) + { + NodeMapNodeRawData nodeRawData = BuildNodeRawData(runState.RunId, node, currentSequenceIndex); + if (nodeRawData != null) + { + nodes.Add(nodeRawData); + } + } + + string currentNodeText = runState.IsCompleted + ? "当前节点: 已完成" + : $"当前节点: {BuildNodeTitle(runState.CurrentNode)}"; + + return new NodeMapFormRawData + { + RunId = runState.RunId, + Title = "节点地图", + ProgressText = $"{runState.CompletedNodeCount} / {runState.NodeCount}", + CurrentNodeText = currentNodeText, + IsRunCompleted = runState.IsCompleted, + Nodes = nodes + }; + } + + private static NodeMapNodeRawData BuildNodeRawData(string runId, RunNodeState node, int currentSequenceIndex) + { + if (node == null) + { + return null; + } + + bool isCurrentNode = node.SequenceIndex == currentSequenceIndex; + bool canEnter = isCurrentNode && node.Status == RunNodeStatus.Available; + + return new NodeMapNodeRawData + { + RunId = runId, + NodeId = node.NodeId, + SequenceIndex = node.SequenceIndex, + NodeType = node.NodeType, + Status = node.Status, + IsCurrentNode = isCurrentNode, + CanEnter = canEnter, + Title = BuildNodeTitle(node), + StatusText = BuildStatusText(node.Status, canEnter) + }; + } + + private static string BuildNodeTitle(RunNodeState node) + { + return node == null ? "未知节点" : BuildNodeTitle(node.NodeType); + } + + private static string BuildNodeTitle(RunNodeType nodeType) + { + switch (nodeType) + { + case RunNodeType.Combat: + return "战斗"; + case RunNodeType.Event: + return "事件"; + case RunNodeType.Shop: + return "商店"; + case RunNodeType.BossCombat: + return "Boss"; + default: + return "未知节点"; + } + } + + private static string BuildStatusText(RunNodeStatus status, bool canEnter) + { + if (canEnter) + { + return "当前"; + } + + switch (status) + { + case RunNodeStatus.Locked: + return "未解锁"; + case RunNodeStatus.Available: + return "可进入"; + case RunNodeStatus.Completed: + return "已完成"; + case RunNodeStatus.Exception: + return "异常"; + case RunNodeStatus.Skipped: + return "跳过"; + default: + return string.Empty; + } + } + } +} diff --git a/src-ref/UI/Game/UseCase/RepoFormUseCase.cs b/src-ref/UI/Game/UseCase/RepoFormUseCase.cs new file mode 100644 index 0000000..7142f7f --- /dev/null +++ b/src-ref/UI/Game/UseCase/RepoFormUseCase.cs @@ -0,0 +1,209 @@ +using System.Collections.Generic; +using GeometryTD.CustomComponent; +using GeometryTD.CustomUtility; +using GeometryTD.Definition; + +namespace GeometryTD.UI +{ + public class RepoFormUseCase : IUIUseCase + { + private const int MaxParticipantCount = 4; + private BackpackInventoryData _fallbackInventory; + private readonly HashSet _selectedSellItemIds = new HashSet(); + private RepoFormState _state = RepoFormState.Assemble; + + public RepoFormRawData CreateInitialModel() + { + BackpackInventoryData sample = GameEntry.PlayerInventory != null + ? GameEntry.PlayerInventory.GetInventorySnapshot() + : GetOrCreateFallbackInventory(); + return new RepoFormRawData + { + Inventory = sample, + State = _state, + SelectedSellItemIds = BuildSelectedSellItemArray(), + SelectedSellItemCount = _selectedSellItemIds.Count, + SelectedSellTotalPrice = ResolveSelectedSellTotalPrice() + }; + } + + public RepoFormState State => _state; + + public bool TryAssembleTower(long muzzleItemId, long bearingItemId, long baseItemId) + { + if (_state == RepoFormState.Sell) + { + return false; + } + + if (GameEntry.PlayerInventory == null) + { + return false; + } + + return GameEntry.PlayerInventory.TryAssembleTower( + muzzleItemId, + bearingItemId, + baseItemId, + out _); + } + + public ParticipantTowerAssignResult TryAddParticipantTower(long towerItemId) + { + if (_state == RepoFormState.Sell) + { + return new ParticipantTowerAssignResult + { + TowerInstanceId = towerItemId, + FailureReason = ParticipantTowerAssignFailureReason.ParticipantAreaFull + }; + } + + if (GameEntry.PlayerInventory == null) + { + BackpackInventoryData fallbackInventory = GetOrCreateFallbackInventory(); + return InventoryParticipantUtility.TryAddParticipantTower( + fallbackInventory, + towerItemId, + MaxParticipantCount); + } + + return GameEntry.PlayerInventory.TryAddParticipantTower(towerItemId, MaxParticipantCount); + } + + public bool TryRemoveParticipantTower(long towerItemId) + { + if (_state == RepoFormState.Sell) + { + return false; + } + + if (GameEntry.PlayerInventory == null) + { + BackpackInventoryData fallbackInventory = GetOrCreateFallbackInventory(); + return InventoryParticipantUtility.TryRemoveParticipantTower( + fallbackInventory, + towerItemId, + MaxParticipantCount); + } + + return GameEntry.PlayerInventory.TryRemoveParticipantTower(towerItemId); + } + + public RepoFormRawData EnterSellState() + { + _state = RepoFormState.Sell; + _selectedSellItemIds.Clear(); + return CreateInitialModel(); + } + + public RepoFormRawData ExitSellState() + { + _state = RepoFormState.Assemble; + _selectedSellItemIds.Clear(); + return CreateInitialModel(); + } + + public bool TryToggleSellSelection( + long itemId, + out RepoFormRawData rawData, + out PlayerInventorySaleFailureReason failureReason) + { + rawData = null; + failureReason = PlayerInventorySaleFailureReason.None; + if (_state != RepoFormState.Sell || itemId <= 0 || GameEntry.PlayerInventory == null) + { + failureReason = PlayerInventorySaleFailureReason.InvalidSelection; + return false; + } + + if (_selectedSellItemIds.Contains(itemId)) + { + _selectedSellItemIds.Remove(itemId); + rawData = CreateInitialModel(); + return true; + } + + if (!GameEntry.PlayerInventory.TryGetSaleCandidate(itemId, out PlayerInventorySaleCandidate candidate) || + candidate == null) + { + failureReason = PlayerInventorySaleFailureReason.ItemNotFound; + return false; + } + + if (!candidate.IsSellable) + { + failureReason = candidate.FailureReason; + return false; + } + + _selectedSellItemIds.Add(itemId); + rawData = CreateInitialModel(); + return true; + } + + public bool TryConfirmSellSelection(out RepoFormRawData rawData, out PlayerInventorySaleResult result) + { + rawData = null; + result = null; + if (_state != RepoFormState.Sell || _selectedSellItemIds.Count <= 0 || GameEntry.PlayerInventory == null) + { + return false; + } + + if (!GameEntry.PlayerInventory.TrySellItems(_selectedSellItemIds, out PlayerInventorySaleResult saleResult) || + saleResult == null || + !saleResult.IsSuccess) + { + result = saleResult; + rawData = CreateInitialModel(); + return false; + } + + _selectedSellItemIds.Clear(); + result = saleResult; + rawData = CreateInitialModel(); + return true; + } + + private BackpackInventoryData GetOrCreateFallbackInventory() + { + _fallbackInventory ??= InventorySeedUtility.CreateSampleInventory(); + InventoryParticipantUtility.NormalizeParticipantState(_fallbackInventory, MaxParticipantCount); + return _fallbackInventory; + } + + private long[] BuildSelectedSellItemArray() + { + if (_selectedSellItemIds.Count <= 0) + { + return System.Array.Empty(); + } + + long[] itemIds = new long[_selectedSellItemIds.Count]; + _selectedSellItemIds.CopyTo(itemIds); + return itemIds; + } + + private int ResolveSelectedSellTotalPrice() + { + if (_state != RepoFormState.Sell || _selectedSellItemIds.Count <= 0 || GameEntry.PlayerInventory == null) + { + return 0; + } + + int total = 0; + foreach (long itemId in _selectedSellItemIds) + { + if (GameEntry.PlayerInventory.TryGetSaleCandidate(itemId, out PlayerInventorySaleCandidate candidate) && + candidate != null && + candidate.IsSellable) + { + total += candidate.Price; + } + } + + return total; + } + } +} diff --git a/src-ref/UI/Game/View/CombineArea.cs b/src-ref/UI/Game/View/CombineArea.cs new file mode 100644 index 0000000..6a29d81 --- /dev/null +++ b/src-ref/UI/Game/View/CombineArea.cs @@ -0,0 +1,169 @@ +using GeometryTD.CustomEvent; +using GeometryTD.Definition; +using UnityEngine; +using UnityEngine.EventSystems; + +namespace GeometryTD.UI +{ + public class CombineArea : MonoBehaviour, IDropHandler + { + [SerializeField] private CombineSlotItem[] _slots; + private bool _isInteractionEnabled = true; + + public void OnInit(CombineAreaContext context) + { + if (_slots == null) + { + return; + } + + for (int i = 0; i < _slots.Length; i++) + { + if (_slots[i] != null) + { + _slots[i].OnInit(i); + } + } + } + + public void OnReset() + { + if (_slots == null) + { + return; + } + + foreach (var slot in _slots) + { + if (slot != null) + { + slot.OnReset(); + } + } + } + + public void OnCombineClick() + { + if (!_isInteractionEnabled) + { + return; + } + + if (!TryGetBoundItemId(TowerCompSlotType.Muzzle, out long muzzleItemId) || + !TryGetBoundItemId(TowerCompSlotType.Bearing, out long bearingItemId) || + !TryGetBoundItemId(TowerCompSlotType.Base, out long baseItemId)) + { + return; + } + + GameEntry.Event.Fire(this, RepoCombineRequestedEventArgs.Create(muzzleItemId, bearingItemId, baseItemId)); + } + + public bool TryAssignItem(RepoItemContext itemContext) + { + if (!_isInteractionEnabled) + { + return false; + } + + if (itemContext == null || itemContext.ComponentSlotType == TowerCompSlotType.None) + { + return false; + } + + CombineSlotItem targetSlot = FindSlot(itemContext.ComponentSlotType); + if (targetSlot == null || targetSlot.HasItem) + { + return false; + } + + targetSlot.BindItem(itemContext); + return true; + } + + public bool TryClearSlot(int slotIndex, out long removedItemId) + { + removedItemId = 0; + if (_slots == null || slotIndex < 0 || slotIndex >= _slots.Length) + { + return false; + } + + CombineSlotItem slot = _slots[slotIndex]; + if (slot == null || !slot.HasItem) + { + return false; + } + + removedItemId = slot.BoundItemId; + slot.ClearSlot(); + return true; + } + + public void OnDrop(PointerEventData eventData) + { + if (!_isInteractionEnabled) + { + return; + } + + if (eventData == null) + { + return; + } + + GameObject pointerDrag = eventData.pointerDrag; + if (pointerDrag == null) + { + return; + } + + IRepoDragItemView dragItem = pointerDrag.GetComponent() ?? + pointerDrag.GetComponentInParent(); + if (dragItem == null) + { + return; + } + + bool assigned = TryAssignItem(dragItem.ComponentContext); + dragItem.SetDropResult(assigned); + } + + public void SetInteractionEnabled(bool enabled) + { + _isInteractionEnabled = enabled; + } + + private CombineSlotItem FindSlot(TowerCompSlotType slotType) + { + if (_slots == null) + { + return null; + } + + for (int i = 0; i < _slots.Length; i++) + { + CombineSlotItem slot = _slots[i]; + if (slot != null && slot.AcceptSlotType == slotType) + { + return slot; + } + } + + return null; + } + + private bool TryGetBoundItemId(TowerCompSlotType slotType, out long boundItemId) + { + boundItemId = 0; + CombineSlotItem slot = FindSlot(slotType); + if (slot == null || !slot.HasItem || slot.BoundItemId <= 0) + { + return false; + } + + boundItemId = slot.BoundItemId; + return true; + } + } +} diff --git a/src-ref/UI/Game/View/CombineSlotItem.cs b/src-ref/UI/Game/View/CombineSlotItem.cs new file mode 100644 index 0000000..c58e83e --- /dev/null +++ b/src-ref/UI/Game/View/CombineSlotItem.cs @@ -0,0 +1,69 @@ +using GeometryTD.CustomEvent; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.UI +{ + public class CombineSlotItem : MonoBehaviour + { + [SerializeField] private TowerCompSlotType _acceptSlotType = TowerCompSlotType.None; + + [SerializeField] private IconArea _iconArea; + + [SerializeField] private CommonButton _button; + + private long _boundItemId; + + private int _slotIndex = -1; + + public TowerCompSlotType AcceptSlotType => _acceptSlotType; + + public bool HasItem => _boundItemId > 0; + + public int SlotIndex => _slotIndex; + + public long BoundItemId => _boundItemId; + + public void OnInit(int slotIndex) + { + _slotIndex = slotIndex; + ClearSlot(); + _iconArea.OnReset(); + } + + public void BindItem(RepoItemContext itemContext) + { + _boundItemId = itemContext?.InstanceId ?? 0; + _button.Interactive = true; + + if (itemContext != null) + { + _iconArea.OnInit(itemContext.IconAreaContext); + } + } + + public void ClearSlot() + { + _boundItemId = 0; + _button.Interactive = false; + _iconArea.OnReset(); + } + + public void OnReset() + { + _slotIndex = -1; + ClearSlot(); + _iconArea.OnReset(); + } + + public void OnClick() + { + if (_slotIndex < 0) + { + return; + } + + GameEntry.Event.Fire(this, CombineSlotClickedEventArgs.Create(_slotIndex)); + } + } +} \ No newline at end of file diff --git a/src-ref/UI/Game/View/CompArea.cs b/src-ref/UI/Game/View/CompArea.cs new file mode 100644 index 0000000..a9abd90 --- /dev/null +++ b/src-ref/UI/Game/View/CompArea.cs @@ -0,0 +1,203 @@ +using System.Collections.Generic; +using GameFramework.ObjectPool; +using GeometryTD.PoolObjectBase; +using UnityEngine; +using UnityEngine.Serialization; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class CompArea : MonoBehaviour + { + [FormerlySerializedAs("m_Content")] [SerializeField] private Transform _content; + [FormerlySerializedAs("m_ItemTemplate")] [SerializeField] private RepoItem _itemTemplate; + [SerializeField] private TowerRepoItem _towerItemTemplate; + [FormerlySerializedAs("m_InstancePoolCapacity")] [SerializeField] private int _instancePoolCapacity = 64; + + private readonly List _activeComponentItems = new List(); + private readonly List _activeTowerItems = new List(); + private readonly Dictionary _componentItemMap = new Dictionary(); + private readonly Dictionary _towerItemMap = new Dictionary(); + private IObjectPool _componentItemPool; + private IObjectPool _towerItemPool; + private string _componentPoolName; + private string _towerPoolName; + + public void OnInit(CompAreaContext context) + { + if (context == null) + { + OnReset(); + return; + } + + EnsurePool(); + OnReset(); + + if (context.ComponentItems != null) + { + foreach (RepoItemContext itemContext in context.ComponentItems) + { + RepoItem item = CreateOrSpawnComponentItem(); + if (item == null) + { + continue; + } + + item.gameObject.SetActive(true); + item.OnInit(itemContext); + _activeComponentItems.Add(item); + _componentItemMap[itemContext.InstanceId] = item; + } + } + + if (context.TowerItems != null) + { + foreach (TowerRepoItemContext itemContext in context.TowerItems) + { + TowerRepoItem item = CreateOrSpawnTowerItem(); + if (item == null) + { + continue; + } + + item.gameObject.SetActive(true); + item.OnInit(itemContext); + _activeTowerItems.Add(item); + _towerItemMap[itemContext.InstanceId] = item; + } + } + } + + public void OnReset() + { + for (int i = _activeComponentItems.Count - 1; i >= 0; i--) + { + RepoItem item = _activeComponentItems[i]; + if (item == null) + { + continue; + } + + item.OnReset(); + item.gameObject.SetActive(false); + _componentItemPool?.Unspawn(item); + } + + for (int i = _activeTowerItems.Count - 1; i >= 0; i--) + { + TowerRepoItem item = _activeTowerItems[i]; + if (item == null) + { + continue; + } + + item.OnReset(); + item.gameObject.SetActive(false); + _towerItemPool?.Unspawn(item); + } + + _activeComponentItems.Clear(); + _activeTowerItems.Clear(); + _componentItemMap.Clear(); + _towerItemMap.Clear(); + } + + private void OnDestroy() + { + OnReset(); + _componentItemPool?.ReleaseAllUnused(); + _towerItemPool?.ReleaseAllUnused(); + } + + private void EnsurePool() + { + if (_componentItemPool == null) + { + _componentPoolName = $"RepoItemPool_{GetInstanceID()}"; + _componentItemPool = + GameEntry.ObjectPool.CreateSingleSpawnObjectPool(_componentPoolName, + _instancePoolCapacity); + } + + if (_towerItemPool == null) + { + _towerPoolName = $"TowerRepoItemPool_{GetInstanceID()}"; + _towerItemPool = + GameEntry.ObjectPool.CreateSingleSpawnObjectPool(_towerPoolName, + _instancePoolCapacity); + } + } + + private RepoItem CreateOrSpawnComponentItem() + { + if (_componentItemPool == null) + { + return null; + } + + RepoItemObject itemObject = _componentItemPool.Spawn(); + RepoItem item = itemObject != null ? (RepoItem)itemObject.Target : null; + if (item == null) + { + if (_itemTemplate == null) + { + Log.Warning("CompArea requires a component item template."); + return null; + } + + item = Instantiate(_itemTemplate); + _componentItemPool.Register(RepoItemObject.Create(item), true); + } + + if (_content != null) + { + item.transform.SetParent(_content, false); + } + + return item; + } + + private TowerRepoItem CreateOrSpawnTowerItem() + { + if (_towerItemPool == null) + { + return null; + } + + TowerRepoItemObject itemObject = _towerItemPool.Spawn(); + TowerRepoItem item = itemObject != null ? (TowerRepoItem)itemObject.Target : null; + if (item == null) + { + if (_towerItemTemplate == null) + { + Log.Warning("CompArea requires a tower item template."); + return null; + } + + item = Instantiate(_towerItemTemplate); + _towerItemPool.Register(TowerRepoItemObject.Create(item), true); + } + + if (_content != null) + { + item.transform.SetParent(_content, false); + } + + return item; + } + + public void SetItemSelected(long itemId, bool isSelected) + { + if (_componentItemMap.TryGetValue(itemId, out RepoItem componentItem)) + { + componentItem.SetSelected(isSelected); + } + + if (_towerItemMap.TryGetValue(itemId, out TowerRepoItem towerItem)) + { + towerItem.SetSelected(isSelected); + } + } + } +} diff --git a/src-ref/UI/Game/View/EventForm.cs b/src-ref/UI/Game/View/EventForm.cs new file mode 100644 index 0000000..b32cb47 --- /dev/null +++ b/src-ref/UI/Game/View/EventForm.cs @@ -0,0 +1,82 @@ +using TMPro; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class EventForm : UGuiForm + { + [SerializeField] private TMP_Text _titleText; + [SerializeField] private TMP_Text _descriptionText; + [SerializeField] private EventOptionItem[] _optionItems; + + private EventFormContext _context; + + public void RefreshUI(EventFormContext context) + { + _context = context; + + if (_titleText != null) + { + _titleText.text = context?.Title ?? string.Empty; + } + + if (_descriptionText != null) + { + _descriptionText.text = context?.Description ?? string.Empty; + } + + for (int i = 0; i < _optionItems.Length; i++) + { + EventOptionItemContext optionContext = null; + if (context?.OptionItems != null && i < context.OptionItems.Length) + { + optionContext = context.OptionItems[i]; + } + + if (_optionItems[i] != null) + { + _optionItems[i].OnInit(optionContext); + } + } + } + + protected override void OnOpen(object userData) + { + base.OnOpen(userData); + + if (userData is EventFormContext context) + { + RefreshUI(context); + return; + } + + Log.Warning("EventForm requires EventFormContext as userData."); + } + + protected override void OnClose(bool isShutdown, object userData) + { + _context = null; + + if (_titleText != null) + { + _titleText.text = string.Empty; + } + + if (_descriptionText != null) + { + _descriptionText.text = string.Empty; + } + + foreach (var item in _optionItems) + { + if (item != null) + { + item.OnReset(); + } + } + + base.OnClose(isShutdown, userData); + } + } +} \ No newline at end of file diff --git a/src-ref/UI/Game/View/EventOptionItem.cs b/src-ref/UI/Game/View/EventOptionItem.cs new file mode 100644 index 0000000..0275ae9 --- /dev/null +++ b/src-ref/UI/Game/View/EventOptionItem.cs @@ -0,0 +1,85 @@ +using GeometryTD.CustomEvent; +using TMPro; +using UnityEngine; + +namespace GeometryTD.UI +{ + public class EventOptionItem : MonoBehaviour + { + [SerializeField] private TMP_Text _optionText; + + private static readonly Color SelectableTextColor = Color.white; + private static readonly Color BlockedTextColor = new Color(0.75f, 0.75f, 0.75f, 1f); + + private int _optionIndex; + private bool _isSelectable; + private CommonButton _commonButton; + + private void Awake() + { + _commonButton = GetComponent(); + } + + public void OnInit(EventOptionItemContext context) + { + if (context == null) + { + OnReset(); + gameObject.SetActive(false); + return; + } + + _optionIndex = context.OptionIndex; + _isSelectable = context.IsSelectable; + if (_optionText != null) + { + _optionText.text = BuildDisplayText(context); + _optionText.color = _isSelectable ? SelectableTextColor : BlockedTextColor; + } + + if (_commonButton != null) + { + _commonButton.Interactive = _isSelectable; + } + + gameObject.SetActive(!string.IsNullOrEmpty(context.OptionText)); + } + + public void OnReset() + { + _optionIndex = -1; + _isSelectable = false; + if (_optionText != null) + { + _optionText.text = string.Empty; + _optionText.color = SelectableTextColor; + } + + if (_commonButton != null) + { + _commonButton.Interactive = true; + } + } + + public void OnClick() + { + if (_optionIndex < 0 || !_isSelectable) + { + return; + } + + GameEntry.Event.Fire(this, EventOptionItemSelectedEventArgs.Create(_optionIndex)); + } + + private static string BuildDisplayText(EventOptionItemContext context) + { + string optionText = context.OptionText ?? string.Empty; + if (context.IsSelectable || string.IsNullOrEmpty(context.BlockedReason)) + { + return optionText; + } + + return $"{optionText}\n{context.BlockedReason}"; + } + } +} diff --git a/src-ref/UI/Game/View/IRepoDragItemView.cs b/src-ref/UI/Game/View/IRepoDragItemView.cs new file mode 100644 index 0000000..f7d3018 --- /dev/null +++ b/src-ref/UI/Game/View/IRepoDragItemView.cs @@ -0,0 +1,19 @@ +using GeometryTD.Definition; + +namespace GeometryTD.UI +{ + public interface IRepoDragItemView + { + long InstanceId { get; } + + bool CanDrag { get; } + + TowerCompSlotType ComponentSlotType { get; } + + RepoItemClickActionType ClickActionType { get; } + + RepoItemContext ComponentContext { get; } + + void SetDropResult(bool assigned); + } +} diff --git a/src-ref/UI/Game/View/MainForm.cs b/src-ref/UI/Game/View/MainForm.cs new file mode 100644 index 0000000..e956e01 --- /dev/null +++ b/src-ref/UI/Game/View/MainForm.cs @@ -0,0 +1,49 @@ +using GeometryTD.CustomEvent; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class MainForm : UGuiForm + { + private MainFormContext _context; + + public void RefreshUI(MainFormContext context) + { + _context = context; + if (_context == null) + { + return; + } + } + + public void OnReturnButtonClick() + { + GameEntry.Event.Fire(this, ReturnButtonClickedEventArgs.Create()); + } + + public void OnRepoButtonClick() + { + GameEntry.Event.Fire(this, RepoButtonClickedEventArgs.Create()); + } + + protected override void OnOpen(object userData) + { + base.OnOpen(userData); + + if (userData is MainFormContext context) + { + RefreshUI(context); + return; + } + + Log.Warning("MainForm requires MainFormContext as userData."); + } + + protected override void OnClose(bool isShutdown, object userData) + { + _context = null; + + base.OnClose(isShutdown, userData); + } + } +} \ No newline at end of file diff --git a/src-ref/UI/Game/View/NodeItem.cs b/src-ref/UI/Game/View/NodeItem.cs new file mode 100644 index 0000000..bf190c4 --- /dev/null +++ b/src-ref/UI/Game/View/NodeItem.cs @@ -0,0 +1,121 @@ +using UnityEngine; +using UnityEngine.UI; +using GeometryTD.CustomEvent; +using GeometryTD.Procedure; + +namespace GeometryTD.UI +{ + public class NodeItem : MonoBehaviour + { + private const string CombatIconAssetName = "Combat"; + private const string EventIconAssetName = "Event"; + private const string ShopIconAssetName = "Shop"; + private const string BossIconAssetName = "BossCombat"; + + [SerializeField] private Image _icon; + [SerializeField] private Image _bg; + [SerializeField] private CommonButton _button; + + [SerializeField] private Color _lockedColor = Color.gray; + [SerializeField] private Color _activeColor = Color.cyan; + [SerializeField] private Color _passedColor = Color.green; + [SerializeField] private Color _exceptionColor = Color.red; + [SerializeField] private Color _defaultIconColor = Color.white; + [SerializeField] private Color _lockedIconColor = Color.gray; + + private NodeItemContext _context; + private string _requestedIconAssetName; + + public void RefreshUI(NodeItemContext context) + { + _context = context; + + if (_button != null) + { + _button.Interactive = context != null && context.CanClick; + } + + Color targetColor = _lockedColor; + if (context == null) + { + targetColor = _lockedColor; + } + else if (context.IsCompleted) + { + targetColor = _passedColor; + } + else if (context.IsException) + { + targetColor = _exceptionColor; + } + else if (context.IsCurrent) + { + targetColor = _activeColor; + } + + if (_bg != null) + { + _bg.color = targetColor; + } + + if (_icon != null) + { + RefreshIcon(context != null ? context.NodeType : RunNodeType.None); + _icon.color = context != null && !context.IsLocked ? _defaultIconColor : _lockedIconColor; + } + } + + public void OnClick() + { + if (_context == null || !_context.CanClick) + { + return; + } + + GameEntry.Event.Fire(this, NodeMapNodeClickEventArgs.Create(_context.SequenceIndex)); + } + + private void RefreshIcon(RunNodeType nodeType) + { + if (_icon == null) + { + return; + } + + string assetName = ResolveIconAssetName(nodeType); + _requestedIconAssetName = assetName; + if (string.IsNullOrWhiteSpace(assetName) || GameEntry.SpriteCache == null) + { + _icon.sprite = null; + return; + } + + GameEntry.SpriteCache.GetSprite(assetName, sprite => + { + if (_icon == null || _requestedIconAssetName != assetName) + { + return; + } + + _icon.sprite = sprite; + }); + } + + private static string ResolveIconAssetName(RunNodeType nodeType) + { + switch (nodeType) + { + case RunNodeType.Combat: + return CombatIconAssetName; + case RunNodeType.Event: + return EventIconAssetName; + case RunNodeType.Shop: + return ShopIconAssetName; + case RunNodeType.BossCombat: + return BossIconAssetName; + default: + return null; + } + } + } +} diff --git a/src-ref/UI/Game/View/NodeMapForm.cs b/src-ref/UI/Game/View/NodeMapForm.cs new file mode 100644 index 0000000..0d08bad --- /dev/null +++ b/src-ref/UI/Game/View/NodeMapForm.cs @@ -0,0 +1,89 @@ +using TMPro; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class NodeMapForm : UGuiForm + { + [SerializeField] private TMP_Text _titleText; + [SerializeField] private TMP_Text _progressText; + [SerializeField] private TMP_Text _currentNodeText; + [SerializeField] private NodeItem[] _nodeItems; + + private NodeMapFormContext _context; + + public void RefreshUI(NodeMapFormContext context) + { + _context = context; + if (_titleText != null) + { + _titleText.text = context?.Title ?? string.Empty; + } + + if (_progressText != null) + { + _progressText.text = context?.ProgressText ?? string.Empty; + } + + if (_currentNodeText != null) + { + _currentNodeText.text = context?.CurrentNodeText ?? string.Empty; + } + + if (_nodeItems == null) + { + return; + } + + for (int i = 0; i < _nodeItems.Length; i++) + { + NodeItemContext itemContext = + context?.NodeItems != null && i < context.NodeItems.Length ? context.NodeItems[i] : null; + _nodeItems[i]?.RefreshUI(itemContext); + } + } + + protected override void OnOpen(object userData) + { + base.OnOpen(userData); + + if (userData is NodeMapFormContext context) + { + RefreshUI(context); + return; + } + + Log.Warning("NodeMapForm requires NodeMapFormContext as userData."); + } + + protected override void OnClose(bool isShutdown, object userData) + { + _context = null; + if (_titleText != null) + { + _titleText.text = string.Empty; + } + + if (_progressText != null) + { + _progressText.text = string.Empty; + } + + if (_currentNodeText != null) + { + _currentNodeText.text = string.Empty; + } + + if (_nodeItems != null) + { + for (int i = 0; i < _nodeItems.Length; i++) + { + _nodeItems[i]?.RefreshUI(null); + } + } + + base.OnClose(isShutdown, userData); + } + } +} diff --git a/src-ref/UI/Game/View/ParticipantArea.cs b/src-ref/UI/Game/View/ParticipantArea.cs new file mode 100644 index 0000000..cfe0c81 --- /dev/null +++ b/src-ref/UI/Game/View/ParticipantArea.cs @@ -0,0 +1,194 @@ +using System.Collections.Generic; +using GameFramework.ObjectPool; +using GeometryTD.CustomEvent; +using GeometryTD.Definition; +using GeometryTD.PoolObjectBase; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class ParticipantArea : MonoBehaviour, IDropHandler + { + [SerializeField] private Transform _content; + [SerializeField] private TowerRepoItem _towerItemTemplate; + [SerializeField] private int _instancePoolCapacity = 8; + private bool _isInteractionEnabled = true; + + private readonly List _activeItems = new List(); + private readonly HashSet _boundItemIds = new HashSet(); + private IObjectPool _itemPool; + private string _poolName; + private int _maxCount = 4; + + public void OnInit(ParticipantAreaContext context) + { + OnReset(); + EnsurePool(); + + _maxCount = Mathf.Max(1, context != null ? context.MaxCount : 4); + if (context?.TowerItems == null || context.TowerItems.Length <= 0) + { + return; + } + + for (int i = 0; i < context.TowerItems.Length; i++) + { + TowerRepoItemContext itemContext = context.TowerItems[i]; + if (itemContext == null || itemContext.InstanceId <= 0) + { + continue; + } + + TowerRepoItem item = CreateOrSpawnItem(); + if (item == null) + { + continue; + } + + item.gameObject.SetActive(true); + item.OnInit(itemContext); + _activeItems.Add(item); + _boundItemIds.Add(itemContext.InstanceId); + } + } + + public void OnReset() + { + for (int i = _activeItems.Count - 1; i >= 0; i--) + { + TowerRepoItem item = _activeItems[i]; + if (item == null) + { + continue; + } + + item.OnReset(); + item.gameObject.SetActive(false); + _itemPool?.Unspawn(item); + } + + _activeItems.Clear(); + _boundItemIds.Clear(); + _maxCount = 4; + } + + private void OnDestroy() + { + OnReset(); + _itemPool?.ReleaseAllUnused(); + } + + public bool CanAssign(IRepoDragItemView dragItem) + { + if (!_isInteractionEnabled) + { + return false; + } + + if (dragItem == null || dragItem.InstanceId <= 0) + { + return false; + } + + if (dragItem.ComponentSlotType != TowerCompSlotType.None) + { + return false; + } + + if (_boundItemIds.Contains(dragItem.InstanceId)) + { + return false; + } + + if (_boundItemIds.Count >= _maxCount) + { + return false; + } + + return true; + } + + public void OnDrop(PointerEventData eventData) + { + if (!_isInteractionEnabled) + { + return; + } + + if (eventData == null) + { + return; + } + + GameObject pointerDrag = eventData.pointerDrag; + if (pointerDrag == null) + { + return; + } + + IRepoDragItemView dragItem = pointerDrag.GetComponent() ?? + pointerDrag.GetComponentInParent(); + if (dragItem == null) + { + return; + } + + bool assigned = CanAssign(dragItem); + dragItem.SetDropResult(assigned); + if (!assigned) + { + return; + } + + GameEntry.Event.Fire(this, RepoParticipantAssignRequestedEventArgs.Create(dragItem.InstanceId)); + } + + public void SetInteractionEnabled(bool enabled) + { + _isInteractionEnabled = enabled; + } + + private void EnsurePool() + { + if (_itemPool != null) + { + return; + } + + _poolName = $"ParticipantTowerRepoItemPool_{GetInstanceID()}"; + _itemPool = + GameEntry.ObjectPool.CreateSingleSpawnObjectPool(_poolName, _instancePoolCapacity); + } + + private TowerRepoItem CreateOrSpawnItem() + { + if (_itemPool == null) + { + return null; + } + + TowerRepoItemObject itemObject = _itemPool.Spawn(); + TowerRepoItem item = itemObject != null ? (TowerRepoItem)itemObject.Target : null; + if (item == null) + { + if (_towerItemTemplate == null) + { + Log.Warning("ParticipantArea requires a tower item template."); + return null; + } + + item = Instantiate(_towerItemTemplate); + _itemPool.Register(TowerRepoItemObject.Create(item), true); + } + + if (_content != null) + { + item.transform.SetParent(_content, false); + } + + return item; + } + } +} diff --git a/src-ref/UI/Game/View/RepoForm.cs b/src-ref/UI/Game/View/RepoForm.cs new file mode 100644 index 0000000..2cb19bd --- /dev/null +++ b/src-ref/UI/Game/View/RepoForm.cs @@ -0,0 +1,136 @@ +using GeometryTD.CustomEvent; +using TMPro; +using UnityEngine; +using UnityEngine.UI; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class RepoForm : UGuiForm + { + [SerializeField] private CombineArea _combineArea; + [SerializeField] private CompArea _compArea; + [SerializeField] private SellArea _sellArea; + [SerializeField] private ParticipantArea _participantArea; + [SerializeField] private TMP_Text _goldText; + [SerializeField] private CommonButton _sellModeButton; + [SerializeField] private TMP_Text _sellModeButtonText; + + public void RefreshUI(RepoFormContext context) + { + if (context == null) + { + return; + } + + RefreshGoldText(context.GoldText); + RefreshStateUI(context); + + _combineArea?.OnInit(context.CombineAreaContext); + _compArea?.OnInit(context.CompAreaContext); + _sellArea?.OnInit(context.SellAreaContext); + _participantArea?.OnInit(context.ParticipantAreaContext); + + if (_combineArea != null) + { + _combineArea.gameObject.SetActive(context.ShowCombineArea); + _combineArea.SetInteractionEnabled(context.State == RepoFormState.Assemble); + } + + if (_sellArea != null) + { + _sellArea.gameObject.SetActive(context.ShowSellArea); + } + + _participantArea?.SetInteractionEnabled(context.State == RepoFormState.Assemble); + } + + public void RefreshGoldText(string goldText) + { + if (_goldText != null) + { + _goldText.text = goldText ?? string.Empty; + } + } + + public bool TryAssignItemToCombineArea(RepoItemContext itemContext) + { + if (_combineArea == null) + { + return false; + } + + return _combineArea.TryAssignItem(itemContext); + } + + public bool TryClearCombineSlot(int slotIndex, out long removedItemId) + { + removedItemId = 0; + if (_combineArea == null) + { + return false; + } + + return _combineArea.TryClearSlot(slotIndex, out removedItemId); + } + + public void SetRepoItemSelected(long itemId, bool isSelected) + { + _compArea?.SetItemSelected(itemId, isSelected); + } + + public void RefreshParticipantArea(ParticipantAreaContext context) + { + _participantArea?.OnInit(context); + } + + protected override void OnOpen(object userData) + { + base.OnOpen(userData); + if (userData is RepoFormContext context) + { + RefreshUI(context); + return; + } + + Log.Warning("RepoForm requires RepoFormContext as userData."); + } + + protected override void OnClose(bool isShutdown, object userData) + { + if (_goldText != null) + { + _goldText.text = string.Empty; + } + + _combineArea?.OnReset(); + _compArea?.OnReset(); + _sellArea?.OnReset(); + _participantArea?.OnReset(); + base.OnClose(isShutdown, userData); + } + + public void OnReturnButtonClick() + { + GameEntry.Event.Fire(this, RepoFormReturnEventArgs.Create()); + } + + public void OnSellModeButtonClick() + { + GameEntry.Event.Fire(this, RepoSellModeToggleRequestedEventArgs.Create()); + } + + private void RefreshStateUI(RepoFormContext context) + { + if (_sellModeButton != null) + { + _sellModeButton.gameObject.SetActive(context != null && context.ShowSellModeButton); + } + + if (_sellModeButtonText != null) + { + _sellModeButtonText.text = context?.SellModeButtonText ?? "出售模式"; + } + } + } +} diff --git a/src-ref/UI/Game/View/RepoItem.cs b/src-ref/UI/Game/View/RepoItem.cs new file mode 100644 index 0000000..3f0f562 --- /dev/null +++ b/src-ref/UI/Game/View/RepoItem.cs @@ -0,0 +1,381 @@ +using GeometryTD.CustomEvent; +using GeometryTD.Definition; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; + +namespace GeometryTD.UI +{ + public class RepoItem : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IRepoDragItemView + { + [SerializeField] private Image _bgImage; + + [SerializeField] private IconArea _iconArea; + + private static readonly Color SelectedColor = new Color32(255, 216, 102, 255); + private static readonly Color DisabledSellColor = new Color32(96, 96, 96, 255); + private static readonly Color EmptyEnduranceColor = new Color(0.8f, 0f, 0f, 1f); + private static readonly Color FullEnduranceColor = new Color(0f, 0.8f, 0f, 1f); + private static readonly Vector2 DefaultDragGhostSize = new Vector2(64f, 64f); + + [SerializeField] private RepoItemContext _context; + private CanvasGroup _canvasGroup; + private RectTransform _dragRoot; + protected GameObject _dragGhostObject; + protected RectTransform _dragGhostRect; + private bool _isSelected; + private bool _isDragging; + private bool _dropHandled; + private bool _dropAssigned; + + public RepoItemContext Context => _context; + + public long InstanceId => _context != null ? _context.InstanceId : 0; + + bool IRepoDragItemView.CanDrag => _context != null && _context.CanDrag; + + public TowerCompSlotType ComponentSlotType => + _context != null ? _context.ComponentSlotType : TowerCompSlotType.None; + + public RepoItemClickActionType ClickActionType => + _context != null ? _context.ClickActionType : RepoItemClickActionType.OpenDetail; + + public RepoItemContext ComponentContext => _context; + + protected IconArea IconArea => _iconArea; + + protected Image BackgroundImage => _bgImage; + + protected RectTransform DragRoot => _dragRoot; + + private void Awake() + { + _canvasGroup = GetComponent(); + if (_canvasGroup == null) + { + _canvasGroup = gameObject.AddComponent(); + } + } + + private void OnDisable() + { + ResetDragState(); + } + + public virtual void OnInit(RepoItemContext context) + { + if (context == null) + { + OnReset(); + return; + } + + _context = context; + _iconArea.OnInit(context.IconAreaContext); + + SetSelected(context.IsSellSelected); + ResetDragState(); + } + + public virtual void OnReset() + { + _context = null; + _iconArea.OnReset(); + + SetSelected(false); + ResetDragState(); + } + + public void SetSelected(bool isSelected) + { + _isSelected = isSelected; + RefreshBackgroundColor(); + } + + private void RefreshBackgroundColor() + { + if (_bgImage == null) + { + return; + } + + if (_isSelected) + { + _bgImage.color = SelectedColor; + return; + } + + if (_context != null && _context.IsSellMode && !_context.IsSellable) + { + _bgImage.color = DisabledSellColor; + return; + } + + float enduranceRate = _context != null ? Mathf.Clamp01(_context.EnduranceRate01) : 1f; + _bgImage.color = Color.Lerp(EmptyEnduranceColor, FullEnduranceColor, enduranceRate); + } + + public void OnClick() + { + if (_context == null) + { + return; + } + + if (_isDragging) + { + return; + } + + GameEntry.Event.Fire(this, RepoItemClickedEventArgs.Create(_context.InstanceId, ResolveScreenPosition())); + } + + public void OnBeginDrag(PointerEventData eventData) + { + if (!CanStartDrag(eventData)) + { + return; + } + + _dragRoot = ResolveDragRoot(); + if (_dragRoot == null) + { + return; + } + + _isDragging = true; + _dropHandled = false; + _dropAssigned = false; + + CloseItemDescFormIfOpen(); + + if (!CreateDragGhost()) + { + _isDragging = false; + _dragRoot = null; + return; + } + + if (_canvasGroup != null) + { + _canvasGroup.blocksRaycasts = false; + } + + SetSelected(true); + UpdateDragGhostPosition(eventData); + } + + public void OnDrag(PointerEventData eventData) + { + if (!_isDragging) + { + return; + } + + UpdateDragGhostPosition(eventData); + } + + public void OnEndDrag(PointerEventData eventData) + { + if (!_isDragging) + { + return; + } + + bool assigned = _dropHandled && _dropAssigned; + _isDragging = false; + + if (_canvasGroup != null) + { + _canvasGroup.blocksRaycasts = true; + } + + DestroyDragGhost(); + _dragRoot = null; + + if (_context == null) + { + return; + } + + GameEntry.Event.Fire(this, RepoItemDragEndedEventArgs.Create(_context.InstanceId, assigned)); + } + + public void SetDropResult(bool assigned) + { + if (!_isDragging) + { + return; + } + + _dropHandled = true; + _dropAssigned = assigned; + } + + private bool CanStartDrag(PointerEventData eventData) + { + if (_context == null || _isDragging) + { + return false; + } + + if (_context.IsSellMode) + { + return false; + } + + if (_isSelected && _context.ComponentSlotType != TowerCompSlotType.None) + { + return false; + } + + if (!_context.CanDrag) + { + return false; + } + + if (eventData == null || eventData.button != PointerEventData.InputButton.Left) + { + return false; + } + + return true; + } + + private static void CloseItemDescFormIfOpen() + { + var itemDescForm = GameEntry.UI.GetUIForm(UIFormType.ItemDescForm); + if (itemDescForm != null) + { + GameEntry.UI.CloseUIForm(itemDescForm); + } + } + + private Vector2 ResolveScreenPosition() + { + RectTransform rectTransform = transform as RectTransform; + if (rectTransform == null) + { + return Vector2.zero; + } + + Canvas canvas = GetComponentInParent(); + Canvas rootCanvas = canvas != null ? canvas.rootCanvas : null; + Camera uiCamera = null; + if (rootCanvas != null && rootCanvas.renderMode != RenderMode.ScreenSpaceOverlay) + { + uiCamera = rootCanvas.worldCamera != null ? rootCanvas.worldCamera : Camera.main; + } + + return RectTransformUtility.WorldToScreenPoint(uiCamera, rectTransform.position); + } + + private RectTransform ResolveDragRoot() + { + Canvas canvas = GetComponentInParent(); + if (canvas == null || canvas.rootCanvas == null) + { + return null; + } + + return canvas.rootCanvas.transform as RectTransform; + } + + protected virtual bool CreateDragGhost() + { + DestroyDragGhost(); + + if (_dragRoot == null) + { + return false; + } + + Sprite iconSprite = _iconArea != null ? _iconArea.CurrentIconSprite : null; + Color iconColor = _iconArea != null ? _iconArea.CurrentIconColor : Color.white; + Material iconMaterial = _iconArea != null ? _iconArea.CurrentIconMaterial : null; + Vector2 iconSize = _iconArea != null ? _iconArea.CurrentIconSize : Vector2.zero; + + if (iconSprite == null && _bgImage != null) + { + iconSprite = _bgImage.sprite; + iconColor = _bgImage.color; + iconMaterial = _bgImage.material; + iconSize = _bgImage.rectTransform.rect.size; + } + + if (iconSprite == null) + { + return false; + } + + _dragGhostObject = new GameObject("RepoItemDragGhost", typeof(RectTransform), typeof(CanvasGroup), + typeof(Image)); + _dragGhostObject.layer = gameObject.layer; + + _dragGhostRect = _dragGhostObject.GetComponent(); + _dragGhostRect.SetParent(_dragRoot, false); + _dragGhostRect.anchorMin = new Vector2(0.5f, 0.5f); + _dragGhostRect.anchorMax = new Vector2(0.5f, 0.5f); + _dragGhostRect.pivot = new Vector2(0.5f, 0.5f); + + if (iconSize.x <= 0f || iconSize.y <= 0f) + { + iconSize = DefaultDragGhostSize; + } + + _dragGhostRect.sizeDelta = iconSize; + + Image ghostImage = _dragGhostObject.GetComponent(); + ghostImage.raycastTarget = false; + ghostImage.sprite = iconSprite; + ghostImage.color = iconColor; + ghostImage.material = iconMaterial; + ghostImage.preserveAspect = true; + + CanvasGroup ghostCanvasGroup = _dragGhostObject.GetComponent(); + ghostCanvasGroup.blocksRaycasts = false; + ghostCanvasGroup.interactable = false; + + _dragGhostObject.transform.SetAsLastSibling(); + return true; + } + + protected virtual void UpdateDragGhostPosition(PointerEventData eventData) + { + if (_dragGhostRect == null || _dragRoot == null || eventData == null) + { + return; + } + + if (RectTransformUtility.ScreenPointToLocalPointInRectangle(_dragRoot, eventData.position, + eventData.pressEventCamera, out Vector2 localPoint)) + { + _dragGhostRect.anchoredPosition = localPoint; + } + } + + protected virtual void DestroyDragGhost() + { + if (_dragGhostObject != null) + { + Destroy(_dragGhostObject); + _dragGhostObject = null; + _dragGhostRect = null; + } + } + + private void ResetDragState() + { + _isDragging = false; + _dropHandled = false; + _dropAssigned = false; + _dragRoot = null; + DestroyDragGhost(); + + if (_canvasGroup != null) + { + _canvasGroup.blocksRaycasts = true; + } + } + } +} diff --git a/src-ref/UI/Game/View/SellArea.cs b/src-ref/UI/Game/View/SellArea.cs new file mode 100644 index 0000000..688f46f --- /dev/null +++ b/src-ref/UI/Game/View/SellArea.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using GeometryTD.CustomEvent; +using TMPro; +using UnityEngine; + +namespace GeometryTD.UI +{ + public class SellArea : MonoBehaviour + { + [SerializeField] private Transform _content; + [SerializeField] private RepoItem _itemTemplate; + [SerializeField] private TowerRepoItem _towerItemTemplate; + [SerializeField] private TMP_Text _totalPrice; + + private readonly List _activeComponentItems = new List(); + private readonly List _activeTowerItems = new List(); + private SellAreaContext _context; + + public void OnInit(SellAreaContext context) + { + OnReset(); + _context = context; + + if (_totalPrice != null) + { + _totalPrice.text = context?.TotalPriceText ?? string.Empty; + } + + if (context == null || _content == null) + { + return; + } + + if (context.ComponentItems != null) + { + foreach (RepoItemContext itemContext in context.ComponentItems) + { + if (itemContext == null || _itemTemplate == null) + { + continue; + } + + RepoItem item = Instantiate(_itemTemplate, _content); + item.gameObject.SetActive(true); + item.OnInit(itemContext); + _activeComponentItems.Add(item); + } + } + + if (context.TowerItems != null) + { + foreach (TowerRepoItemContext itemContext in context.TowerItems) + { + if (itemContext == null || _towerItemTemplate == null) + { + continue; + } + + TowerRepoItem item = Instantiate(_towerItemTemplate, _content); + item.gameObject.SetActive(true); + item.OnInit(itemContext); + _activeTowerItems.Add(item); + } + } + } + + public void OnReset() + { + for (int i = _activeComponentItems.Count - 1; i >= 0; i--) + { + RepoItem item = _activeComponentItems[i]; + if (item != null) + { + Destroy(item.gameObject); + } + } + + for (int i = _activeTowerItems.Count - 1; i >= 0; i--) + { + TowerRepoItem item = _activeTowerItems[i]; + if (item != null) + { + Destroy(item.gameObject); + } + } + + _activeComponentItems.Clear(); + _activeTowerItems.Clear(); + _context = null; + + if (_totalPrice != null) + { + _totalPrice.text = string.Empty; + } + } + + public void OnCancelButtonClick() + { + GameEntry.Event.Fire(this, RepoSellCancelRequestedEventArgs.Create()); + } + + public void OnConfirmButtonClick() + { + if (_context == null || !_context.CanConfirmSell) + { + return; + } + + GameEntry.Event.Fire(this, RepoSellConfirmRequestedEventArgs.Create()); + } + } +} diff --git a/src-ref/UI/Game/View/TowerRepoItem.cs b/src-ref/UI/Game/View/TowerRepoItem.cs new file mode 100644 index 0000000..3648827 --- /dev/null +++ b/src-ref/UI/Game/View/TowerRepoItem.cs @@ -0,0 +1,109 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace GeometryTD.UI +{ + public class TowerRepoItem : RepoItem + { + private static readonly Vector2 DefaultTowerDragGhostSize = new Vector2(64f, 64f); + + public new TowerRepoItemContext Context => base.Context as TowerRepoItemContext; + + public void OnInit(TowerRepoItemContext context) + { + base.OnInit(context); + } + + protected override bool CreateDragGhost() + { + DestroyDragGhost(); + + if (DragRoot == null) + { + return false; + } + + TowerIconArea towerIconArea = IconArea as TowerIconArea; + if (towerIconArea == null) + { + return base.CreateDragGhost(); + } + + _dragGhostObject = new GameObject("TowerRepoItemDragGhost", typeof(RectTransform), typeof(CanvasGroup)); + _dragGhostObject.layer = gameObject.layer; + + _dragGhostRect = _dragGhostObject.GetComponent(); + _dragGhostRect.SetParent(DragRoot, false); + _dragGhostRect.anchorMin = new Vector2(0.5f, 0.5f); + _dragGhostRect.anchorMax = new Vector2(0.5f, 0.5f); + _dragGhostRect.pivot = new Vector2(0.5f, 0.5f); + + Vector2 iconSize = IconArea != null ? IconArea.CurrentIconSize : Vector2.zero; + if (iconSize.x <= 0f || iconSize.y <= 0f) + { + iconSize = BackgroundImage != null ? BackgroundImage.rectTransform.rect.size : DefaultTowerDragGhostSize; + } + + if (iconSize.x <= 0f || iconSize.y <= 0f) + { + iconSize = DefaultTowerDragGhostSize; + } + + _dragGhostRect.sizeDelta = iconSize; + + bool hasAnyLayer = false; + hasAnyLayer |= TryCreateLayer("Base", towerIconArea.BaseIconSprite, towerIconArea.BaseIconColor, + towerIconArea.BaseIconMaterial, _dragGhostRect, 0); + hasAnyLayer |= TryCreateLayer("Bearing", towerIconArea.BearingIconSprite, towerIconArea.BearingIconColor, + towerIconArea.BearingIconMaterial, _dragGhostRect, 1); + hasAnyLayer |= TryCreateLayer("Muzzle", towerIconArea.MuzzleIconSprite, towerIconArea.MuzzleIconColor, + towerIconArea.MuzzleIconMaterial, _dragGhostRect, 2); + + if (!hasAnyLayer) + { + DestroyDragGhost(); + return base.CreateDragGhost(); + } + + CanvasGroup ghostCanvasGroup = _dragGhostObject.GetComponent(); + ghostCanvasGroup.blocksRaycasts = false; + ghostCanvasGroup.interactable = false; + + _dragGhostObject.transform.SetAsLastSibling(); + return true; + } + + private static bool TryCreateLayer( + string layerName, + Sprite sprite, + Color color, + Material material, + RectTransform parent, + int siblingIndex) + { + if (parent == null || sprite == null) + { + return false; + } + + GameObject layerObject = new GameObject($"TowerGhost{layerName}", typeof(RectTransform), typeof(Image)); + layerObject.layer = parent.gameObject.layer; + + RectTransform layerRect = layerObject.GetComponent(); + layerRect.SetParent(parent, false); + layerRect.anchorMin = Vector2.zero; + layerRect.anchorMax = Vector2.one; + layerRect.offsetMin = Vector2.zero; + layerRect.offsetMax = Vector2.zero; + layerRect.SetSiblingIndex(siblingIndex); + + Image layerImage = layerObject.GetComponent(); + layerImage.raycastTarget = false; + layerImage.sprite = sprite; + layerImage.color = color; + layerImage.material = material; + layerImage.preserveAspect = true; + return true; + } + } +} diff --git a/src-ref/UI/General/Context/DialogFormContext.cs b/src-ref/UI/General/Context/DialogFormContext.cs new file mode 100644 index 0000000..6056363 --- /dev/null +++ b/src-ref/UI/General/Context/DialogFormContext.cs @@ -0,0 +1,19 @@ +using GameFramework; + +namespace GeometryTD.UI +{ + public class DialogFormContext : UIContext + { + public int Mode; + public string Title; + public string Message; + public bool PauseGame; + public string ConfirmText; + public GameFrameworkAction OnClickConfirm; + public string CancelText; + public GameFrameworkAction OnClickCancel; + public string OtherText; + public GameFrameworkAction OnClickOther; + public object UserData; + } +} diff --git a/src-ref/UI/General/Context/IconAreaContext.cs b/src-ref/UI/General/Context/IconAreaContext.cs new file mode 100644 index 0000000..ec79155 --- /dev/null +++ b/src-ref/UI/General/Context/IconAreaContext.cs @@ -0,0 +1,15 @@ +using System; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.UI +{ + [Serializable] + public class IconAreaContext : UIContext + { + public RarityType Rarity; + public TowerCompSlotType ComponentSlotType; + public Color Color = Color.white; + public Sprite Icon; + } +} diff --git a/src-ref/UI/General/Context/ItemDescFormContext.cs b/src-ref/UI/General/Context/ItemDescFormContext.cs new file mode 100644 index 0000000..8a4fab5 --- /dev/null +++ b/src-ref/UI/General/Context/ItemDescFormContext.cs @@ -0,0 +1,14 @@ +using UnityEngine; + +namespace GeometryTD.UI +{ + public class ItemDescFormContext : UIContext + { + public string Title; + public string TypeText; + public string Description; + public int Price; + public Vector2 ScreenPosition; + public TagItemContext[] Tags; + } +} diff --git a/src-ref/UI/General/Context/RewardItemContext.cs b/src-ref/UI/General/Context/RewardItemContext.cs new file mode 100644 index 0000000..892d240 --- /dev/null +++ b/src-ref/UI/General/Context/RewardItemContext.cs @@ -0,0 +1,12 @@ +namespace GeometryTD.UI +{ + public class RewardItemContext : UIContext + { + public int Index; + public IconAreaContext IconArea; + public string Title; + public string TypeText; + public string Description; + public TagItemContext[] Tags; + } +} diff --git a/src-ref/UI/General/Context/RewardSelectFormContext.cs b/src-ref/UI/General/Context/RewardSelectFormContext.cs new file mode 100644 index 0000000..0bfc190 --- /dev/null +++ b/src-ref/UI/General/Context/RewardSelectFormContext.cs @@ -0,0 +1,11 @@ +namespace GeometryTD.UI +{ + public class RewardSelectFormContext : UIContext + { + public string TipText; + public string RefreshButtonText; + public bool CanRefresh; + public bool CanGiveUp; + public RewardItemContext[] RewardItems; + } +} \ No newline at end of file diff --git a/src-ref/UI/General/Context/TagItemContext.cs b/src-ref/UI/General/Context/TagItemContext.cs new file mode 100644 index 0000000..ff8ae6e --- /dev/null +++ b/src-ref/UI/General/Context/TagItemContext.cs @@ -0,0 +1,11 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace GeometryTD.UI +{ + public class TagItemContext : UIContext + { + public string TagName; + } +} \ No newline at end of file diff --git a/src-ref/UI/General/Context/TowerIconAreaContext.cs b/src-ref/UI/General/Context/TowerIconAreaContext.cs new file mode 100644 index 0000000..a3c2da3 --- /dev/null +++ b/src-ref/UI/General/Context/TowerIconAreaContext.cs @@ -0,0 +1,21 @@ +using System; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.UI +{ + [Serializable] + public class TowerIconAreaContext : IconAreaContext + { + public Color MuzzleColor = Color.white; + public Color BearingColor = Color.white; + public Color BaseColor = Color.white; + + public TowerIconAreaContext() + { + ComponentSlotType = TowerCompSlotType.None; + Color = Color.white; + Icon = null; + } + } +} diff --git a/src-ref/UI/General/Controller/DialogFormController.cs b/src-ref/UI/General/Controller/DialogFormController.cs new file mode 100644 index 0000000..440514b --- /dev/null +++ b/src-ref/UI/General/Controller/DialogFormController.cs @@ -0,0 +1,78 @@ +using GeometryTD.Definition; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class DialogFormController : UIFormControllerCommonBase + { + protected override UIFormType UIFormTypeId => UIFormType.DialogForm; + + protected override void RefreshUI(DialogForm form, DialogFormContext context) + { + form.RefreshUI(context); + } + + protected override void CloseLoadedFormDirect(DialogForm form) + { + GameEntry.UI.CloseUIForm(form); + } + + private static DialogFormContext BuildContext(DialogFormRawData rawData) + { + if (rawData == null) + { + return null; + } + + return new DialogFormContext + { + Mode = rawData.Mode, + Title = rawData.Title, + Message = rawData.Message, + PauseGame = rawData.PauseGame, + ConfirmText = rawData.ConfirmText, + OnClickConfirm = rawData.OnClickConfirm, + CancelText = rawData.CancelText, + OnClickCancel = rawData.OnClickCancel, + OtherText = rawData.OtherText, + OnClickOther = rawData.OnClickOther, + UserData = rawData.UserData + }; + } + + public int? OpenUI(DialogFormRawData rawData) + { + DialogFormContext context = BuildContext(rawData); + return OpenUIInternal(context); + } + + public override int? OpenUI(object userData = null) + { + if (userData is DialogFormContext context) + { + return OpenUIInternal(context); + } + + if (userData is DialogFormRawData rawData) + { + return OpenUI(rawData); + } + + if (userData != null) + { + Log.Warning("DialogFormController.OpenUI() userData type is invalid."); + return null; + } + + return OpenUIInternal(Context); + } + + public override void BindUseCase(IUIUseCase useCase) + { + if (useCase != null) + { + Log.Warning("DialogFormController does not use a use case."); + } + } + } +} diff --git a/src-ref/UI/General/Controller/ItemDescFormController.cs b/src-ref/UI/General/Controller/ItemDescFormController.cs new file mode 100644 index 0000000..ad13ff1 --- /dev/null +++ b/src-ref/UI/General/Controller/ItemDescFormController.cs @@ -0,0 +1,121 @@ +using GameFramework.Event; +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class ItemDescFormController : UIFormControllerCommonBase< + ItemDescFormContext, ItemDescForm> + { + protected override UIFormType UIFormTypeId => UIFormType.ItemDescForm; + + + protected override void RefreshUI(ItemDescForm form, ItemDescFormContext context) + { + form.RefreshUI(context); + } + + protected override void CloseLoadedFormDirect(ItemDescForm form) + { + GameEntry.UI.CloseUIForm(form); + } + + private static ItemDescFormContext BuildContext(ItemDescFormRawData rawData) + { + if (rawData == null) + { + return null; + } + + return new ItemDescFormContext + { + Title = rawData.Title, + TypeText = rawData.TypeText, + Description = BuildDescription(rawData), + Price = rawData.Price, + ScreenPosition = rawData.ScreenPosition, + Tags = BuildTags(rawData) + }; + } + + private static string BuildDescription(ItemDescFormRawData rawData) + { + string baseDescription = rawData?.Description ?? string.Empty; + string tagDescription = rawData?.TagRuntimes != null && rawData.TagRuntimes.Length > 0 + ? TagDisplayUtility.BuildTagDescriptionText(rawData.TagRuntimes) + : TagDisplayUtility.BuildTagDescriptionText(rawData?.Tags); + if (string.IsNullOrWhiteSpace(tagDescription)) + { + return baseDescription; + } + + if (string.IsNullOrWhiteSpace(baseDescription)) + { + return tagDescription; + } + + return $"{baseDescription}\n{tagDescription}"; + } + + private static TagItemContext[] BuildTags(ItemDescFormRawData rawData) + { + string[] tagTexts = rawData?.TagRuntimes != null && rawData.TagRuntimes.Length > 0 + ? TagDisplayUtility.BuildTagTexts(rawData.TagRuntimes) + : rawData?.Tags != null + ? TagDisplayUtility.BuildTagTexts(rawData.Tags) + : null; + + if (tagTexts == null || tagTexts.Length <= 0) + { + return System.Array.Empty(); + } + + TagItemContext[] contexts = new TagItemContext[tagTexts.Length]; + for (int i = 0; i < tagTexts.Length; i++) + { + contexts[i] = new TagItemContext + { + TagName = tagTexts[i] ?? string.Empty + }; + } + + return contexts; + } + + public int? OpenUI(ItemDescFormRawData rawData) + { + ItemDescFormContext context = BuildContext(rawData); + return OpenUIInternal(context); + } + + public override int? OpenUI(object userData = null) + { + if (userData is ItemDescFormContext context) + { + return OpenUIInternal(context); + } + + if (userData is ItemDescFormRawData rawData) + { + return OpenUI(rawData); + } + + if (userData != null) + { + Log.Warning("ItemDescFormController.OpenUI() userData type is invalid."); + return null; + } + + return OpenUIInternal(Context); + } + + public override void BindUseCase(IUIUseCase useCase) + { + if (!(useCase is ItemDescFormUseCase)) + { + Log.Error("ItemDescForm.BindUseCase() useCase is invalid."); + } + } + } +} diff --git a/src-ref/UI/General/Controller/RewardSelectFormController.cs b/src-ref/UI/General/Controller/RewardSelectFormController.cs new file mode 100644 index 0000000..950da46 --- /dev/null +++ b/src-ref/UI/General/Controller/RewardSelectFormController.cs @@ -0,0 +1,162 @@ +using GeometryTD.CustomEvent; +using GeometryTD.Definition; +using GameFramework.Event; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public partial class RewardSelectFormController : UIFormControllerCommonBase + { + private RewardSelectFormUseCase _useCase; + + protected override UIFormType UIFormTypeId => UIFormType.RewardSelectForm; + + protected override void RefreshUI(RewardSelectForm form, RewardSelectFormContext context) + { + form.RefreshUI(context); + } + + protected override void SubscribeCustomEvents() + { + GameEntry.Event.Subscribe(RewardSelectItemSelectedEventArgs.EventId, OnRewardSelected); + GameEntry.Event.Subscribe(RewardSelectRefreshEventArgs.EventId, OnRefreshClicked); + GameEntry.Event.Subscribe(RewardSelectGiveUpEventArgs.EventId, OnGiveUpClicked); + } + + protected override void UnsubscribeCustomEvents() + { + GameEntry.Event.Unsubscribe(RewardSelectItemSelectedEventArgs.EventId, OnRewardSelected); + GameEntry.Event.Unsubscribe(RewardSelectRefreshEventArgs.EventId, OnRefreshClicked); + GameEntry.Event.Unsubscribe(RewardSelectGiveUpEventArgs.EventId, OnGiveUpClicked); + } + + public override int? OpenUI(object userData = null) + { + if (userData is RewardSelectFormContext context) + { + return OpenUIInternal(context); + } + + if (userData is RewardSelectFormRawData rawDataFromUserData) + { + return OpenUI(rawDataFromUserData); + } + + if (userData != null) + { + Log.Warning("RewardSelectFormController.OpenUI() userData type is invalid."); + return null; + } + + if (_useCase == null) + { + Log.Error("RewardSelectFormController.OpenUI() useCase is null."); + return null; + } + + RewardSelectFormRawData rawData = _useCase.CreateInitialModel(); + return OpenUI(rawData); + } + + public int? OpenUI(RewardSelectFormRawData rawData) + { + RewardSelectFormContext context = BuildContext(rawData); + return OpenUIInternal(context); + } + + public override void BindUseCase(IUIUseCase useCase) + { + if (!(useCase is RewardSelectFormUseCase rewardSelectUseCase)) + { + Log.Error("RewardSelectFormController.BindUseCase() useCase is invalid."); + return; + } + + _useCase = rewardSelectUseCase; + } + + private void OnRewardSelected(object sender, GameEventArgs e) + { + if (!IsEventFromCurrentForm(sender) || !(e is RewardSelectItemSelectedEventArgs args)) + { + return; + } + + if (_useCase == null) + { + return; + } + + RewardSelectFormRawData nextRawData = _useCase.SelectReward(args.SelectedIndex); + if (nextRawData == null) + { + CloseUI(); + return; + } + + OpenUI(nextRawData); + } + + private void OnRefreshClicked(object sender, GameEventArgs e) + { + if (!IsEventFromCurrentForm(sender) || !(e is RewardSelectRefreshEventArgs)) + { + return; + } + + if (_useCase == null) + { + return; + } + + RewardSelectFormRawData nextRawData = _useCase.TryRotateSelection(); + if (nextRawData == null) + { + CloseUI(); + return; + } + + OpenUI(nextRawData); + } + + private void OnGiveUpClicked(object sender, GameEventArgs e) + { + if (!IsEventFromCurrentForm(sender) || !(e is RewardSelectGiveUpEventArgs)) + { + return; + } + + if (_useCase == null) + { + return; + } + + if (_useCase.TryGiveUp()) + { + CloseUI(); + } + } + + private bool IsEventFromCurrentForm(object sender) + { + if (Form == null) + { + return false; + } + + if (ReferenceEquals(sender, Form)) + { + return true; + } + + if (sender is Component component) + { + RewardSelectForm ownerForm = component.GetComponentInParent(); + return ownerForm == Form; + } + + return false; + } + } +} diff --git a/src-ref/UI/General/RawData/DialogFormRawData.cs b/src-ref/UI/General/RawData/DialogFormRawData.cs new file mode 100644 index 0000000..f55d9f9 --- /dev/null +++ b/src-ref/UI/General/RawData/DialogFormRawData.cs @@ -0,0 +1,109 @@ +using GameFramework; + +namespace GeometryTD.UI +{ + /// + /// 对话框显示数据。 + /// + public class DialogFormRawData + { + /// + /// 模式,即按钮数量。取值 1、2、3。 + /// + public int Mode + { + get; + set; + } + + /// + /// 标题。 + /// + public string Title + { + get; + set; + } + + /// + /// 消息内容。 + /// + public string Message + { + get; + set; + } + + /// + /// 弹出窗口时是否暂停游戏。 + /// + public bool PauseGame + { + get; + set; + } + + /// + /// 确认按钮文本。 + /// + public string ConfirmText + { + get; + set; + } + + /// + /// 确定按钮回调。 + /// + public GameFrameworkAction OnClickConfirm + { + get; + set; + } + + /// + /// 取消按钮文本。 + /// + public string CancelText + { + get; + set; + } + + /// + /// 取消按钮回调。 + /// + public GameFrameworkAction OnClickCancel + { + get; + set; + } + + /// + /// 中立按钮文本。 + /// + public string OtherText + { + get; + set; + } + + /// + /// 其它按钮回调。 + /// + public GameFrameworkAction OnClickOther + { + get; + set; + } + + /// + /// 用户自定义数据。 + /// + public object UserData + { + get; + set; + } + } +} diff --git a/src-ref/UI/General/RawData/ItemDescFormRawData.cs b/src-ref/UI/General/RawData/ItemDescFormRawData.cs new file mode 100644 index 0000000..439bf12 --- /dev/null +++ b/src-ref/UI/General/RawData/ItemDescFormRawData.cs @@ -0,0 +1,16 @@ +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.UI +{ + public class ItemDescFormRawData + { + public string Title; + public string TypeText; + public string Description; + public int Price; + public Vector2 ScreenPosition; + public TagType[] Tags; + public TagRuntimeData[] TagRuntimes; + } +} diff --git a/src-ref/UI/General/RawData/RewardSelectFormRawData.cs b/src-ref/UI/General/RawData/RewardSelectFormRawData.cs new file mode 100644 index 0000000..0c972c8 --- /dev/null +++ b/src-ref/UI/General/RawData/RewardSelectFormRawData.cs @@ -0,0 +1,11 @@ +namespace GeometryTD.UI +{ + public class RewardSelectFormRawData + { + public string TipText; + public RewardSelectItemRawData[] RewardItems; + public int RefreshCost; + public bool CanRefresh; + public bool CanGiveUp; + } +} diff --git a/src-ref/UI/General/RawData/RewardSelectItemRawData.cs b/src-ref/UI/General/RawData/RewardSelectItemRawData.cs new file mode 100644 index 0000000..553e129 --- /dev/null +++ b/src-ref/UI/General/RawData/RewardSelectItemRawData.cs @@ -0,0 +1,17 @@ +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.UI +{ + public class RewardSelectItemRawData + { + public TowerCompSlotType SlotType; + public string Title; + public string TypeText; + public string Description; + public RarityType Rarity; + public TagType[] Tags; + public Sprite Icon; + public TowerCompItemData SourceItem; + } +} diff --git a/src-ref/UI/General/RawData/RewardSelectItemRawDataBuilder.cs b/src-ref/UI/General/RawData/RewardSelectItemRawDataBuilder.cs new file mode 100644 index 0000000..e7fdf2c --- /dev/null +++ b/src-ref/UI/General/RawData/RewardSelectItemRawDataBuilder.cs @@ -0,0 +1,56 @@ +using System; +using GeometryTD.CustomUtility; +using GeometryTD.Definition; + +namespace GeometryTD.UI +{ + public static class RewardSelectItemRawDataBuilder + { + public static RewardSelectItemRawData Build(TowerCompItemData item) + { + return new RewardSelectItemRawData + { + SlotType = item.SlotType, + Title = item.Name, + TypeText = BuildTypeText(item.SlotType), + Description = BuildDescription(item), + Rarity = item.Rarity, + Tags = item.Tags != null ? (TagType[])item.Tags.Clone() : Array.Empty(), + Icon = null, + SourceItem = item + }; + } + + private static string BuildTypeText(TowerCompSlotType slotType) + { + return slotType switch + { + TowerCompSlotType.Muzzle => "Muzzle Component", + TowerCompSlotType.Bearing => "Bearing Component", + TowerCompSlotType.Base => "Base Component", + TowerCompSlotType.Accessory => "Accessory", + _ => "Component" + }; + } + + private static string BuildDescription(TowerCompItemData item) + { + if (item is MuzzleCompItemData muzzle) + { + return ItemDescUtility.BuildMuzzleDesc(muzzle); + } + + if (item is BearingCompItemData bearing) + { + return ItemDescUtility.BuildBearingDesc(bearing); + } + + if (item is BaseCompItemData baseComp) + { + return ItemDescUtility.BuildBaseDesc(baseComp); + } + + return string.Empty; + } + } +} diff --git a/src-ref/UI/General/UseCase/ItemDescFormUseCase.cs b/src-ref/UI/General/UseCase/ItemDescFormUseCase.cs new file mode 100644 index 0000000..8cc4672 --- /dev/null +++ b/src-ref/UI/General/UseCase/ItemDescFormUseCase.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.UI +{ + public class ItemDescFormUseCase : IUIUseCase + { + public ItemDescFormUseCase() + { + } + } +} \ No newline at end of file diff --git a/src-ref/UI/General/UseCase/RewardSelectFormUseCase.cs b/src-ref/UI/General/UseCase/RewardSelectFormUseCase.cs new file mode 100644 index 0000000..b64cc43 --- /dev/null +++ b/src-ref/UI/General/UseCase/RewardSelectFormUseCase.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.UI +{ + public class RewardSelectFormUseCase : IUIUseCase + { + private readonly List _rewardPool = new List(); + + private RewardSelectFormRawData _currentModel; + private Action _onRewardSelected; + private Action _onGiveUp; + + private int _displayCount = 3; + private int _refreshCost; + private bool _allowRotateOnce = true; + private bool _allowGiveUp = true; + private bool _hasRotated; + private int _selectionOffset; + private string _tipText = "Select one reward"; + + // RewardSelectForm keeps a fixed reward pool for the current node. + // The "refresh" action only rotates which slice of that pool is shown so save/load + // can reopen the same node with the exact same reward contents. + public void ConfigureRewardPool( + IReadOnlyList rewardPool, + int displayCount = 3, + int refreshCost = 0, + bool allowRotateOnce = true, + bool allowGiveUp = true, + string tipText = null) + { + _rewardPool.Clear(); + if (rewardPool != null) + { + foreach (var item in rewardPool) + { + if (item == null) + { + continue; + } + + _rewardPool.Add(item); + } + } + + _displayCount = Mathf.Max(1, displayCount); + _refreshCost = Mathf.Max(0, refreshCost); + _allowRotateOnce = allowRotateOnce; + _allowGiveUp = allowGiveUp; + _tipText = string.IsNullOrWhiteSpace(tipText) ? "Select one reward" : tipText; + _hasRotated = false; + _selectionOffset = 0; + _currentModel = null; + } + + public void ConfigureRewardCandidates( + IReadOnlyList rewardCandidates, + int displayCount = 3, + int refreshCost = 0, + bool allowRotateOnce = true, + bool allowGiveUp = true, + string tipText = null) + { + _rewardPool.Clear(); + if (rewardCandidates != null) + { + foreach (var item in rewardCandidates) + { + if (item == null) + { + continue; + } + + _rewardPool.Add(RewardSelectItemRawDataBuilder.Build(item)); + } + } + + _displayCount = Mathf.Max(1, displayCount); + _refreshCost = Mathf.Max(0, refreshCost); + _allowRotateOnce = allowRotateOnce; + _allowGiveUp = allowGiveUp; + _tipText = string.IsNullOrWhiteSpace(tipText) ? "Select one reward" : tipText; + _hasRotated = false; + _selectionOffset = 0; + _currentModel = null; + } + + public void SetCallbacks(Action onRewardSelected, Action onGiveUp = null) + { + _onRewardSelected = onRewardSelected; + _onGiveUp = onGiveUp; + } + + public RewardSelectFormRawData CreateInitialModel() + { + _hasRotated = false; + _currentModel = BuildModel(); + return _currentModel; + } + + public RewardSelectFormRawData TryRotateSelection() + { + if (_currentModel == null) + { + return null; + } + + if (!CanRotateSelectionInternal()) + { + return _currentModel; + } + + if (!TryConsumeRotateCost()) + { + _currentModel.CanRefresh = false; + return _currentModel; + } + + _hasRotated = true; + if (_rewardPool.Count > 0) + { + _selectionOffset = (_selectionOffset + _displayCount) % _rewardPool.Count; + } + + _currentModel = BuildModel(); + return _currentModel; + } + + public RewardSelectFormRawData SelectReward(int selectedIndex) + { + if (_currentModel?.RewardItems == null || _currentModel.RewardItems.Length <= 0) + { + return null; + } + + if (selectedIndex < 0 || selectedIndex >= _currentModel.RewardItems.Length) + { + return _currentModel; + } + + RewardSelectItemRawData selectedReward = _currentModel.RewardItems[selectedIndex]; + _onRewardSelected?.Invoke(selectedReward); + + _currentModel = null; + return null; + } + + public bool TryGiveUp() + { + if (!_allowGiveUp) + { + return false; + } + + _onGiveUp?.Invoke(); + _currentModel = null; + return true; + } + + private RewardSelectFormRawData BuildModel() + { + RewardSelectItemRawData[] selectedRewards = SelectRewards(); + + return new RewardSelectFormRawData + { + TipText = _tipText, + RewardItems = selectedRewards, + RefreshCost = _refreshCost, + CanRefresh = selectedRewards.Length > 0 && CanRotateSelectionInternal(), + CanGiveUp = _allowGiveUp + }; + } + + private RewardSelectItemRawData[] SelectRewards() + { + if (_rewardPool.Count <= 0) + { + return Array.Empty(); + } + + int finalCount = Mathf.Clamp(_displayCount, 1, _rewardPool.Count); + RewardSelectItemRawData[] results = new RewardSelectItemRawData[finalCount]; + for (int i = 0; i < finalCount; i++) + { + int sourceIndex = (_selectionOffset + i) % _rewardPool.Count; + RewardSelectItemRawData source = _rewardPool[sourceIndex]; + results[i] = source; + } + + return results; + } + + private bool CanRotateSelectionInternal() + { + if (!_allowRotateOnce || _hasRotated) + { + return false; + } + + return CanPayRotateCost(); + } + + private bool CanPayRotateCost() + { + if (_refreshCost <= 0) + { + return true; + } + + if (GameEntry.PlayerInventory == null) + { + return true; + } + + return GameEntry.PlayerInventory.Gold >= _refreshCost; + } + + private bool TryConsumeRotateCost() + { + if (_refreshCost <= 0) + { + return true; + } + + if (GameEntry.PlayerInventory == null) + { + return true; + } + + return GameEntry.PlayerInventory.TryConsumeGold(_refreshCost); + } + } +} diff --git a/src-ref/UI/General/View/DialogForm.cs b/src-ref/UI/General/View/DialogForm.cs new file mode 100644 index 0000000..88ba9b2 --- /dev/null +++ b/src-ref/UI/General/View/DialogForm.cs @@ -0,0 +1,208 @@ +using GameFramework; +using TMPro; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class DialogForm : UGuiForm + { + [SerializeField] private TMP_Text _titleText = null; + [SerializeField] private TMP_Text _messageText = null; + [SerializeField] private GameObject[] _modeObjects = null; + [SerializeField] private TMP_Text[] _confirmTexts = null; + [SerializeField] private TMP_Text[] _cancelTexts = null; + [SerializeField] private TMP_Text[] _otherTexts = null; + + private int _dialogMode = 1; + private bool _pauseGame = false; + private object _userData = null; + private GameFrameworkAction _onClickConfirmGFAction = null; + private GameFrameworkAction _onClickCancelGFAction = null; + private GameFrameworkAction _onClickOtherGFAction = null; + private DialogFormContext _context = null; + + public int DialogMode => _dialogMode; + + public bool PauseGame => _pauseGame; + + public object UserData => _userData; + + public void OnConfirmButtonClick() + { + Close(); + + if (_onClickConfirmGFAction != null) + { + _onClickConfirmGFAction(_userData); + } + } + + public void OnCancelButtonClick() + { + Close(); + + if (_onClickCancelGFAction != null) + { + _onClickCancelGFAction(_userData); + } + } + + public void OnOtherButtonClick() + { + Close(); + + if (_onClickOtherGFAction != null) + { + _onClickOtherGFAction(_userData); + } + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnOpen(object userData) +#else + protected internal override void OnOpen(object userData) +#endif + { + base.OnOpen(userData); + + if (userData is DialogFormContext context) + { + RefreshUI(context); + return; + } + + if (userData is DialogFormRawData dialogParams) + { + RefreshUI(BuildContext(dialogParams)); + return; + } + + Log.Warning("DialogForm requires DialogFormContext or DialogParams as userData."); + } + +#if UNITY_2017_3_OR_NEWER + protected override void OnClose(bool isShutdown, object userData) +#else + protected internal override void OnClose(bool isShutdown, object userData) +#endif + { + if (_pauseGame) + { + GameEntry.Base.ResumeGame(); + } + + _dialogMode = 1; + _titleText.text = string.Empty; + _messageText.text = string.Empty; + _pauseGame = false; + _userData = null; + _context = null; + + RefreshConfirmText(string.Empty); + _onClickConfirmGFAction = null; + + RefreshCancelText(string.Empty); + _onClickCancelGFAction = null; + + RefreshOtherText(string.Empty); + _onClickOtherGFAction = null; + + base.OnClose(isShutdown, userData); + } + + public void RefreshUI(DialogFormContext context) + { + if (context == null) + { + Log.Warning("DialogForm context is invalid."); + return; + } + + _context = context; + + _dialogMode = context.Mode; + RefreshDialogMode(); + + _titleText.text = context.Title; + _messageText.text = context.Message; + + _pauseGame = context.PauseGame; + RefreshPauseGame(); + + _userData = context.UserData; + + RefreshConfirmText(context.ConfirmText); + _onClickConfirmGFAction = context.OnClickConfirm; + + RefreshCancelText(context.CancelText); + _onClickCancelGFAction = context.OnClickCancel; + + RefreshOtherText(context.OtherText); + _onClickOtherGFAction = context.OnClickOther; + } + + private static DialogFormContext BuildContext(DialogFormRawData rawData) + { + if (rawData == null) + { + return null; + } + + return new DialogFormContext + { + Mode = rawData.Mode, + Title = rawData.Title, + Message = rawData.Message, + PauseGame = rawData.PauseGame, + ConfirmText = rawData.ConfirmText, + OnClickConfirm = rawData.OnClickConfirm, + CancelText = rawData.CancelText, + OnClickCancel = rawData.OnClickCancel, + OtherText = rawData.OtherText, + OnClickOther = rawData.OnClickOther, + UserData = rawData.UserData + }; + } + + private void RefreshDialogMode() + { + for (int i = 1; i <= _modeObjects.Length; i++) + { + _modeObjects[i - 1].SetActive(i == _dialogMode); + } + } + + private void RefreshPauseGame() + { + if (_pauseGame) + { + GameEntry.Base.PauseGame(); + } + } + + private void RefreshConfirmText(string confirmText) + { + foreach (var text in _confirmTexts) + { + text.text = confirmText; + } + } + + private void RefreshCancelText(string cancelText) + { + foreach (var text in _cancelTexts) + { + text.text = cancelText; + } + } + + private void RefreshOtherText(string otherText) + { + foreach (var text in _otherTexts) + { + text.text = otherText; + } + } + } +} \ No newline at end of file diff --git a/src-ref/UI/General/View/IconArea.cs b/src-ref/UI/General/View/IconArea.cs new file mode 100644 index 0000000..ba73959 --- /dev/null +++ b/src-ref/UI/General/View/IconArea.cs @@ -0,0 +1,105 @@ +using GeometryTD.Definition; +using UnityEngine; +using UnityEngine.UI; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class IconArea : MonoBehaviour + { + [SerializeField] private Image _board; + + [SerializeField] private Image _icon; + + private IconAreaContext _context; + + public virtual Sprite CurrentIconSprite => _icon != null ? _icon.sprite : null; + + public virtual Color CurrentIconColor => _icon != null ? _icon.color : Color.white; + + public virtual Vector2 CurrentIconSize => _icon != null ? _icon.rectTransform.rect.size : Vector2.zero; + + public virtual Material CurrentIconMaterial => _icon != null ? _icon.material : null; + + public virtual RectTransform IconRectTransform => _icon != null ? _icon.rectTransform : null; + + public virtual void OnInit(IconAreaContext context) + { + if (context == null) + { + Log.Error("Icon area need IconAreaContext."); + return; + } + + _context = context; + + switch (context.ComponentSlotType) + { + case TowerCompSlotType.Base: + case TowerCompSlotType.Bearing: + case TowerCompSlotType.Muzzle: + GameEntry.SpriteCache.GetSprite(context.ComponentSlotType.ToString(), SetIcon); + break; + default: + SetIcon(_context.Icon); + break; + } + + SetRarity(_context.Rarity); + SetIconColor(_context.Color); + SetIconVisible(true); + } + + public virtual void SetIcon(Sprite sprite) + { + if (_icon != null) + { + _icon.sprite = sprite; + } + } + + public virtual void SetRarity(RarityType rarity) + { + if (_board == null) + { + return; + } + + _board.color = rarity switch + { + RarityType.White => Color.white, + RarityType.Green => Color.green, + RarityType.Blue => Color.blue, + RarityType.Purple => Color.magenta, + RarityType.Red => Color.red, + _ => Color.clear, + }; + } + + public virtual void SetIconColor(Color color) + { + if (_icon == null) + { + return; + } + + _icon.color = color; + } + + public virtual void SetIconVisible(bool visible) + { + if (_icon != null) + { + _icon.enabled = visible; + } + } + + public virtual void OnReset() + { + SetIcon(null); + SetRarity(RarityType.None); + SetIconColor(Color.clear); + SetIconVisible(true); + } + } +} diff --git a/src-ref/UI/General/View/ItemDescForm.cs b/src-ref/UI/General/View/ItemDescForm.cs new file mode 100644 index 0000000..7d879b2 --- /dev/null +++ b/src-ref/UI/General/View/ItemDescForm.cs @@ -0,0 +1,392 @@ +using System.Collections.Generic; +using TMPro; +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.UI; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class ItemDescForm : UGuiForm + { + [SerializeField] private RectTransform _content; + + [SerializeField] private TMP_Text _itemTitle; + + [SerializeField] private TMP_Text _itemTypeText; + + [SerializeField] private RectTransform _descScrollView; + + [SerializeField] private TMP_Text _itemDescription; + + [SerializeField] private TMP_Text _itemPrice; + + [SerializeField] private Transform _tagAreaParent; + + [SerializeField] private GameObject _tagItemPrefab; + + [SerializeField] private bool _autoResizeHeight = true; + + [SerializeField] private float _fixedTopHeight = 150f; + + [SerializeField] private float _fixedBottomHeight = 150f; + + [SerializeField] private float _fixedWidth = 300f; + + [SerializeField] private float _maxDescriptionViewportHeight = 0f; + + [SerializeField] private float _screenEdgePadding = 0f; + + [SerializeField] private float _sideGap = 16f; + + [SerializeField] private float _anchorItemWidth = 0f; + + private readonly List _runtimeTagItems = new List(); + + private ItemDescFormContext _context; + private Vector2 _targetPos; + private bool _isOpened; + + public void RefreshUI(ItemDescFormContext context) + { + if (context == null) + { + Log.Warning("ItemDescForm context is invalid."); + return; + } + + _context = context; + + _targetPos = ConvertScreenToAnchored(_context.ScreenPosition); + + if (_content != null) + { + _content.anchoredPosition3D = _targetPos; + } + + if (_itemTitle != null) _itemTitle.text = _context.Title ?? string.Empty; + if (_itemTypeText != null) _itemTypeText.text = _context.TypeText ?? string.Empty; + if (_itemDescription != null) _itemDescription.text = _context.Description ?? string.Empty; + if (_itemPrice != null) _itemPrice.text = $"Price: {_context.Price} Gold"; + + RefreshTags(_context.Tags); + + ResizeToFitContent(); + ApplySideAnchoredTargetPos(); + } + + protected override void OnOpen(object userData) + { + base.OnOpen(userData); + _isOpened = true; + + if (!(userData is ItemDescFormContext context)) + { + Log.Error("ItemDescFormContext is invalid."); + return; + } + + RefreshUI(context); + } + + protected override void OnClose(bool isShutdown, object userData) + { + _isOpened = false; + ClearTags(); + _context = null; + base.OnClose(isShutdown, userData); + } + + public void OnBlankAreaClick() + { + CloseSelf(); + } + + public void OnBackgroundClick() + { + CloseSelf(); + } + + private void Update() + { + if (!_isOpened || _content == null) + { + return; + } + + if (!TryGetPointerDownScreenPosition(out Vector2 screenPoint)) + { + return; + } + + if (IsPointerInContent(screenPoint)) + { + return; + } + + CloseSelf(); + } + + private void ResizeToFitContent() + { + if (!_autoResizeHeight || _content == null) + { + return; + } + + if (_itemDescription == null) + { + return; + } + + _itemDescription.ForceMeshUpdate(); + float descriptionHeight = Mathf.Max(0f, _itemDescription.preferredHeight); + SetRectHeight(_itemDescription.rectTransform, descriptionHeight); + + RectTransform scrollContent = _itemDescription.rectTransform.parent as RectTransform; + if (scrollContent != null) + { + SetRectHeight(scrollContent, descriptionHeight); + } + + float viewportHeight = descriptionHeight; + if (_descScrollView != null) + { + viewportHeight = Mathf.Min(descriptionHeight, ResolveMaxDescriptionViewportHeight()); + SetRectHeight(_descScrollView, viewportHeight); + + if (_descScrollView.TryGetComponent(out ScrollRect scrollRect)) + { + scrollRect.verticalNormalizedPosition = 1f; + } + } + + float targetHeight = _fixedTopHeight + viewportHeight + _fixedBottomHeight; + + Vector2 size = new Vector2(_fixedWidth, targetHeight); + _content.sizeDelta = size; + } + + private float ResolveMaxDescriptionViewportHeight() + { + float maxViewportHeight = _maxDescriptionViewportHeight; + RectTransform parent = _content.parent as RectTransform; + if (parent != null) + { + float availableHeight = parent.rect.height - _fixedTopHeight - _fixedBottomHeight + - _screenEdgePadding * 2f; + availableHeight = Mathf.Max(0f, availableHeight); + maxViewportHeight = maxViewportHeight > 0f + ? Mathf.Min(maxViewportHeight, availableHeight) + : availableHeight; + } + + return maxViewportHeight > 0f ? maxViewportHeight : float.MaxValue; + } + + private static void SetRectHeight(RectTransform rectTransform, float height) + { + if (rectTransform == null) + { + return; + } + + Vector2 size = rectTransform.sizeDelta; + size.y = height; + rectTransform.sizeDelta = size; + } + + private void ApplySideAnchoredTargetPos() + { + if (_content == null) + { + return; + } + + RectTransform parent = _content.parent as RectTransform; + if (parent == null) + { + return; + } + + Vector2 size = _content.sizeDelta; + if (size.x <= 0f || size.y <= 0f) + { + size = _content.rect.size; + } + + Vector2 pivot = _content.pivot; + float halfWidth = parent.rect.width * 0.5f; + float halfHeight = parent.rect.height * 0.5f; + + float minX = -halfWidth + size.x * pivot.x + _screenEdgePadding; + float maxX = halfWidth - size.x * (1f - pivot.x) - _screenEdgePadding; + float minY = -halfHeight + size.y * pivot.y + _screenEdgePadding; + float maxY = halfHeight - size.y * (1f - pivot.y) - _screenEdgePadding; + + float clampedY = minY <= maxY ? Mathf.Clamp(_targetPos.y, minY, maxY) : 0f; + + float anchorHalfWidth = Mathf.Max(0f, _anchorItemWidth * 0.5f); + float sideGap = Mathf.Max(0f, _sideGap); + float horizontalOffset = anchorHalfWidth + size.x * 0.5f + sideGap; + + float leftCandidateX = _targetPos.x - horizontalOffset; + float rightCandidateX = _targetPos.x + horizontalOffset; + + float finalX; + if (IsInsideBounds(leftCandidateX, minX, maxX)) + { + finalX = leftCandidateX; + } + else if (IsInsideBounds(rightCandidateX, minX, maxX)) + { + finalX = rightCandidateX; + } + else + { + finalX = minX <= maxX ? Mathf.Clamp(leftCandidateX, minX, maxX) : 0f; + } + + _content.anchoredPosition = new Vector2(finalX, clampedY); + } + + private Vector2 ConvertScreenToAnchored(Vector2 screenPosition) + { + if (_content == null) + { + return screenPosition; + } + + RectTransform parent = _content.parent as RectTransform; + if (parent == null) + { + return screenPosition; + } + + Canvas canvas = _content.GetComponentInParent(); + Canvas rootCanvas = canvas != null ? canvas.rootCanvas : null; + Camera uiCamera = null; + if (rootCanvas != null && rootCanvas.renderMode != RenderMode.ScreenSpaceOverlay) + { + uiCamera = rootCanvas.worldCamera != null ? rootCanvas.worldCamera : Camera.main; + } + + if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(parent, screenPosition, uiCamera, + out Vector2 localPoint)) + { + return screenPosition; + } + + return localPoint; + } + + private static bool IsInsideBounds(float value, float min, float max) + { + return value >= min && value <= max; + } + + private void RefreshTags(TagItemContext[] tags) + { + ClearTags(); + + if (_tagAreaParent == null || _tagItemPrefab == null || tags == null || tags.Length <= 0) + { + return; + } + + if (_tagItemPrefab.scene.IsValid() && _tagItemPrefab.transform.IsChildOf(_tagAreaParent)) + { + _tagItemPrefab.SetActive(false); + } + + for (int i = 0; i < tags.Length; i++) + { + TagItemContext tagContext = tags[i]; + if (tagContext == null || string.IsNullOrWhiteSpace(tagContext.TagName)) + { + continue; + } + + GameObject tagGo = Instantiate(_tagItemPrefab, _tagAreaParent); + tagGo.SetActive(true); + + if (tagGo.TryGetComponent(out TagItem tagItem)) + { + tagItem.OnInit(tagContext); + } + else + { + TMP_Text tagText = tagGo.GetComponentInChildren(true); + if (tagText != null) + { + tagText.text = tagContext.TagName; + } + } + + _runtimeTagItems.Add(tagGo); + } + } + + private void ClearTags() + { + for (int i = _runtimeTagItems.Count - 1; i >= 0; i--) + { + GameObject tagGo = _runtimeTagItems[i]; + if (tagGo != null) + { + Destroy(tagGo); + } + } + + _runtimeTagItems.Clear(); + } + + private void CloseSelf() + { + GameEntry.UI.CloseUIForm(this); + } + + private bool IsPointerInContent(Vector2 screenPoint) + { + Canvas canvas = _content.GetComponentInParent(); + Canvas rootCanvas = canvas != null ? canvas.rootCanvas : null; + Camera uiCamera = null; + if (rootCanvas != null && rootCanvas.renderMode != RenderMode.ScreenSpaceOverlay) + { + uiCamera = rootCanvas.worldCamera != null ? rootCanvas.worldCamera : Camera.main; + } + + return RectTransformUtility.RectangleContainsScreenPoint(_content, screenPoint, uiCamera); + } + + private static bool TryGetPointerDownScreenPosition(out Vector2 screenPoint) + { + Touchscreen touchscreen = Touchscreen.current; + if (touchscreen != null) + { + var touches = touchscreen.touches; + for (int i = 0; i < touches.Count; i++) + { + var touch = touches[i]; + if (!touch.press.wasPressedThisFrame) + { + continue; + } + + screenPoint = touch.position.ReadValue(); + return true; + } + } + + Mouse mouse = Mouse.current; + if (mouse != null && mouse.leftButton.wasPressedThisFrame) + { + screenPoint = mouse.position.ReadValue(); + return true; + } + + screenPoint = default; + return false; + } + } +} diff --git a/src-ref/UI/General/View/RewardItem.cs b/src-ref/UI/General/View/RewardItem.cs new file mode 100644 index 0000000..ee35844 --- /dev/null +++ b/src-ref/UI/General/View/RewardItem.cs @@ -0,0 +1,230 @@ +using System.Collections.Generic; +using GeometryTD.CustomEvent; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace GeometryTD.UI +{ + public class RewardItem : MonoBehaviour + { + [SerializeField] private IconArea _iconArea; + + [SerializeField] private TMP_Text _titleText; + + [SerializeField] private TMP_Text _typeText; + + [SerializeField] private TMP_Text _descriptionText; + + [SerializeField] private RectTransform _descriptionScrollView; + + [SerializeField] private RectTransform _descriptionContent; + + [SerializeField] private float _maxDescriptionViewportHeight = 0f; + + [SerializeField] private Transform _tagItemParent; + + [SerializeField] private TagItem _tagItemTemplate; + + private readonly List _runtimeTagItems = new List(); + + private RewardItemContext _context; + + + public void OnInit(RewardItemContext context) + { + if (context == null) + { + OnReset(); + gameObject.SetActive(false); + return; + } + + _context = context; + gameObject.SetActive(true); + + if (_iconArea != null) + { + if (context.IconArea != null) + { + _iconArea.OnInit(context.IconArea); + } + else + { + _iconArea.OnReset(); + } + } + + if (_titleText != null) + { + _titleText.text = context.Title ?? string.Empty; + } + + if (_typeText != null) + { + _typeText.text = context.TypeText ?? string.Empty; + } + + if (_descriptionText != null) + { + _descriptionText.text = context.Description ?? string.Empty; + } + + ResizeDescriptionArea(); + + RefreshTags(context.Tags); + } + + public void OnReset() + { + _context = null; + + if (_iconArea != null) + { + _iconArea.OnReset(); + } + + if (_titleText != null) + { + _titleText.text = string.Empty; + } + + if (_typeText != null) + { + _typeText.text = string.Empty; + } + + if (_descriptionText != null) + { + _descriptionText.text = string.Empty; + } + + ResetDescriptionScroll(); + + ClearTags(); + } + + public void OnClick() + { + if (_context == null) + { + return; + } + + GameEntry.Event.Fire(this, RewardSelectItemSelectedEventArgs.Create(_context.Index)); + } + + private void RefreshTags(TagItemContext[] tagContexts) + { + ClearTags(); + + if (_tagItemParent == null || tagContexts == null || tagContexts.Length <= 0) + { + return; + } + + TagItem template = ResolveTagTemplate(); + if (template == null) + { + return; + } + + for (int i = 0; i < tagContexts.Length; i++) + { + TagItemContext tagContext = tagContexts[i]; + if (tagContext == null || string.IsNullOrWhiteSpace(tagContext.TagName)) + { + continue; + } + + TagItem tagItem = Instantiate(template, _tagItemParent); + tagItem.gameObject.SetActive(true); + tagItem.OnInit(tagContext); + _runtimeTagItems.Add(tagItem); + } + } + + private void ResizeDescriptionArea() + { + if (_descriptionText == null) + { + return; + } + + _descriptionText.ForceMeshUpdate(); + float descriptionHeight = Mathf.Max(0f, _descriptionText.preferredHeight); + SetRectHeight(_descriptionText.rectTransform, descriptionHeight); + + if (_descriptionContent != null) + { + SetRectHeight(_descriptionContent, descriptionHeight); + } + + if (_descriptionScrollView != null) + { + float viewportHeight = _maxDescriptionViewportHeight > 0f + ? Mathf.Min(descriptionHeight, _maxDescriptionViewportHeight) + : descriptionHeight; + SetRectHeight(_descriptionScrollView, viewportHeight); + } + + ResetDescriptionScroll(); + } + + private void ResetDescriptionScroll() + { + if (_descriptionScrollView != null && + _descriptionScrollView.TryGetComponent(out ScrollRect scrollRect)) + { + scrollRect.verticalNormalizedPosition = 1f; + } + } + + private static void SetRectHeight(RectTransform rectTransform, float height) + { + if (rectTransform == null) + { + return; + } + + Vector2 size = rectTransform.sizeDelta; + size.y = height; + rectTransform.sizeDelta = size; + } + + private TagItem ResolveTagTemplate() + { + if (_tagItemTemplate != null) + { + return _tagItemTemplate; + } + + if (_tagItemParent == null) + { + return null; + } + + _tagItemTemplate = _tagItemParent.GetComponentInChildren(true); + if (_tagItemTemplate != null) + { + _tagItemTemplate.gameObject.SetActive(false); + } + + return _tagItemTemplate; + } + + private void ClearTags() + { + for (int i = _runtimeTagItems.Count - 1; i >= 0; i--) + { + TagItem tagItem = _runtimeTagItems[i]; + if (tagItem != null) + { + Destroy(tagItem.gameObject); + } + } + + _runtimeTagItems.Clear(); + } + } +} diff --git a/src-ref/UI/General/View/RewardSelectForm.cs b/src-ref/UI/General/View/RewardSelectForm.cs new file mode 100644 index 0000000..e12bfee --- /dev/null +++ b/src-ref/UI/General/View/RewardSelectForm.cs @@ -0,0 +1,167 @@ +using GeometryTD.CustomEvent; +using TMPro; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class RewardSelectForm : UGuiForm + { + [SerializeField] private Transform _selectItemParent; + + [SerializeField] private RewardItem[] _rewardItems; + + [SerializeField] private TMP_Text _tipText; + + [SerializeField] private TMP_Text _refreshButtonText; + + [SerializeField] private CommonButton _refreshButton; + + [SerializeField] private CommonButton _giveUpButton; + + private RewardSelectFormContext _context; + + public void OnInit(RewardSelectFormContext context) + { + if (context == null) + { + Log.Error("RewardSelectForm need Context."); + return; + } + + RefreshUI(context); + } + + public void RefreshUI(RewardSelectFormContext context) + { + _context = context; + EnsureRewardItems(); + + if (_tipText != null) + { + _tipText.text = context?.TipText ?? string.Empty; + } + + if (_refreshButtonText != null) + { + _refreshButtonText.text = context?.RefreshButtonText ?? string.Empty; + } + + if (_refreshButton != null) + { + _refreshButton.Interactive = context?.CanRefresh ?? false; + } + + if (_giveUpButton != null) + { + _giveUpButton.Interactive = context?.CanGiveUp ?? false; + } + + if (_rewardItems == null || _rewardItems.Length <= 0) + { + return; + } + + for (int i = 0; i < _rewardItems.Length; i++) + { + RewardItem rewardItem = _rewardItems[i]; + if (rewardItem == null) + { + continue; + } + + RewardItemContext itemContext = null; + if (context?.RewardItems != null && i < context.RewardItems.Length) + { + itemContext = context.RewardItems[i]; + } + + rewardItem.OnInit(itemContext); + } + } + + public void OnRefreshButtonClick() + { + if (_context != null && !_context.CanRefresh) + { + return; + } + + GameEntry.Event.Fire(this, RewardSelectRefreshEventArgs.Create()); + } + + public void OnGiveUpButtonClick() + { + if (_context != null && !_context.CanGiveUp) + { + return; + } + + GameEntry.Event.Fire(this, RewardSelectGiveUpEventArgs.Create()); + } + + protected override void OnOpen(object userData) + { + base.OnOpen(userData); + + if (userData is RewardSelectFormContext context) + { + RefreshUI(context); + return; + } + + Log.Warning("RewardSelectForm requires RewardSelectFormContext as userData."); + } + + protected override void OnClose(bool isShutdown, object userData) + { + _context = null; + + if (_tipText != null) + { + _tipText.text = string.Empty; + } + + if (_refreshButtonText != null) + { + _refreshButtonText.text = string.Empty; + } + + if (_rewardItems != null) + { + for (int i = 0; i < _rewardItems.Length; i++) + { + RewardItem rewardItem = _rewardItems[i]; + if (rewardItem == null) + { + continue; + } + + rewardItem.OnReset(); + rewardItem.gameObject.SetActive(false); + } + } + + base.OnClose(isShutdown, userData); + } + + private void EnsureRewardItems() + { + if (_rewardItems != null && _rewardItems.Length > 0) + { + return; + } + + if (_selectItemParent != null) + { + _rewardItems = _selectItemParent.GetComponentsInChildren(true); + } + + if (_rewardItems == null || _rewardItems.Length <= 0) + { + _rewardItems = GetComponentsInChildren(true); + } + } + } + +} diff --git a/src-ref/UI/General/View/TagItem.cs b/src-ref/UI/General/View/TagItem.cs new file mode 100644 index 0000000..8016f3c --- /dev/null +++ b/src-ref/UI/General/View/TagItem.cs @@ -0,0 +1,27 @@ +using TMPro; +using UnityEngine; + +namespace GeometryTD.UI +{ + public class TagItem : MonoBehaviour + { + [SerializeField] private RectTransform _bgRect; + + [SerializeField] private TMP_Text _tagName; + + [SerializeField] private float _bgExtraWidth = 12f; + + private TagItemContext _context; + + public void OnInit(TagItemContext context) + { + _context = context; + _tagName.text = _context?.TagName ?? string.Empty; + + _tagName.ForceMeshUpdate(); + var targetWidth = _tagName.preferredWidth + _bgExtraWidth; + _bgRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, targetWidth); + } + } + +} diff --git a/src-ref/UI/General/View/TowerIconArea.cs b/src-ref/UI/General/View/TowerIconArea.cs new file mode 100644 index 0000000..0d339f2 --- /dev/null +++ b/src-ref/UI/General/View/TowerIconArea.cs @@ -0,0 +1,72 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace GeometryTD.UI +{ + public class TowerIconArea : IconArea + { + [SerializeField] private Image _baseIcon; + + [SerializeField] private Image _bearingIcon; + + [SerializeField] private Image _muzzleIcon; + + public Sprite BaseIconSprite => _baseIcon != null ? _baseIcon.sprite : null; + + public Sprite BearingIconSprite => _bearingIcon != null ? _bearingIcon.sprite : null; + + public Sprite MuzzleIconSprite => _muzzleIcon != null ? _muzzleIcon.sprite : null; + + public Color BaseIconColor => _baseIcon != null ? _baseIcon.color : Color.white; + + public Color BearingIconColor => _bearingIcon != null ? _bearingIcon.color : Color.white; + + public Color MuzzleIconColor => _muzzleIcon != null ? _muzzleIcon.color : Color.white; + + public Material BaseIconMaterial => _baseIcon != null ? _baseIcon.material : null; + + public Material BearingIconMaterial => _bearingIcon != null ? _bearingIcon.material : null; + + public Material MuzzleIconMaterial => _muzzleIcon != null ? _muzzleIcon.material : null; + + public override void OnInit(IconAreaContext context) + { + if (!(context is TowerIconAreaContext towerContext)) + { + base.OnInit(context); + return; + } + + SetRarity(towerContext.Rarity); + SetIconVisible(false); + + SetLayerColor(_baseIcon, towerContext.BaseColor); + SetLayerColor(_bearingIcon, towerContext.BearingColor); + SetLayerColor(_muzzleIcon, towerContext.MuzzleColor); + } + + public void OnInit(TowerIconAreaContext context) + { + OnInit((IconAreaContext)context); + } + + public override void OnReset() + { + base.OnReset(); + SetLayerColor(_baseIcon, Color.clear); + SetLayerColor(_bearingIcon, Color.clear); + SetLayerColor(_muzzleIcon, Color.clear); + SetIconVisible(false); + } + + private static void SetLayerColor(Image icon, Color color) + { + if (icon == null) + { + return; + } + + icon.color = color; + } + } +} diff --git a/src-ref/UI/HPBarItem.cs b/src-ref/UI/HPBarItem.cs new file mode 100644 index 0000000..c096e6a --- /dev/null +++ b/src-ref/UI/HPBarItem.cs @@ -0,0 +1,122 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using System.Collections; +using GeometryTD.Entity; +using GeometryTD.UI; +using UnityEngine; +using UnityEngine.UI; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class HPBarItem : MonoBehaviour + { + private const float AnimationSeconds = 0.3f; + private const float KeepSeconds = 0.4f; + private const float FadeOutSeconds = 0.3f; + + [SerializeField] + private Slider m_HPBar = null; + + private Canvas m_ParentCanvas = null; + private RectTransform m_CachedTransform = null; + private CanvasGroup m_CachedCanvasGroup = null; + private EntityBase m_Owner = null; + private int m_OwnerId = 0; + + public EntityBase Owner + { + get + { + return m_Owner; + } + } + + public void Init(EntityBase owner, Canvas parentCanvas, float fromHPRatio, float toHPRatio) + { + if (owner == null) + { + Log.Error("Owner is invalid."); + return; + } + + m_ParentCanvas = parentCanvas; + + gameObject.SetActive(true); + StopAllCoroutines(); + + m_CachedCanvasGroup.alpha = 1f; + if (m_Owner != owner || m_OwnerId != owner.Id) + { + m_HPBar.value = fromHPRatio; + m_Owner = owner; + m_OwnerId = owner.Id; + } + + Refresh(); + + StartCoroutine(HPBarCo(toHPRatio, AnimationSeconds, KeepSeconds, FadeOutSeconds)); + } + + public bool Refresh() + { + if (m_CachedCanvasGroup.alpha <= 0f) + { + return false; + } + + if (m_Owner != null && Owner.Available && Owner.Id == m_OwnerId) + { + Vector3 worldPosition = m_Owner.CachedTransform.position + Vector3.forward; + Vector3 screenPosition = GameEntry.Scene.MainCamera.WorldToScreenPoint(worldPosition); + + Vector2 position; + if (RectTransformUtility.ScreenPointToLocalPointInRectangle((RectTransform)m_ParentCanvas.transform, screenPosition, + m_ParentCanvas.worldCamera, out position)) + { + m_CachedTransform.localPosition = position; + } + } + + return true; + } + + public void Reset() + { + StopAllCoroutines(); + m_CachedCanvasGroup.alpha = 1f; + m_HPBar.value = 1f; + m_Owner = null; + gameObject.SetActive(false); + } + + private void Awake() + { + m_CachedTransform = GetComponent(); + if (m_CachedTransform == null) + { + Log.Error("RectTransform is invalid."); + return; + } + + m_CachedCanvasGroup = GetComponent(); + if (m_CachedCanvasGroup == null) + { + Log.Error("CanvasGroup is invalid."); + return; + } + } + + private IEnumerator HPBarCo(float value, float animationDuration, float keepDuration, float fadeOutDuration) + { + yield return m_HPBar.SmoothValue(value, animationDuration); + yield return new WaitForSeconds(keepDuration); + yield return m_CachedCanvasGroup.FadeToAlpha(0f, fadeOutDuration); + } + } +} diff --git a/src-ref/UI/Menu/Context/MenuFormContext.cs b/src-ref/UI/Menu/Context/MenuFormContext.cs new file mode 100644 index 0000000..8ce0643 --- /dev/null +++ b/src-ref/UI/Menu/Context/MenuFormContext.cs @@ -0,0 +1,13 @@ +namespace GeometryTD.UI +{ + public class MenuFormContext : UIContext + { + public string TitleText { get; set; } + + public string StartButtonText { get; set; } + + public string SettingsButtonText { get; set; } + + public string ExitButtonText { get; set; } + } +} diff --git a/src-ref/UI/Menu/Context/TestMenuFormContext.cs b/src-ref/UI/Menu/Context/TestMenuFormContext.cs new file mode 100644 index 0000000..4dc8ae6 --- /dev/null +++ b/src-ref/UI/Menu/Context/TestMenuFormContext.cs @@ -0,0 +1,9 @@ +using GeometryTD.Procedure; + +namespace GeometryTD.UI +{ + public class TestMenuFormContext : UIContext + { + public RunNodeType CurrentNodeType { get; set; } + } +} diff --git a/src-ref/UI/Menu/Controller/MenuFormController.cs b/src-ref/UI/Menu/Controller/MenuFormController.cs new file mode 100644 index 0000000..5cdb8a5 --- /dev/null +++ b/src-ref/UI/Menu/Controller/MenuFormController.cs @@ -0,0 +1,129 @@ +using GeometryTD.CustomEvent; +using GeometryTD.Definition; +using GeometryTD.Procedure; +using GameFramework.Event; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class MenuFormController : UIFormControllerCommonBase + { + private MenuFormUseCase _useCase; + + protected override UIFormType UIFormTypeId => UIFormType.MenuForm; + + protected override void RefreshUI(MenuForm form, MenuFormContext context) + { + form.RefreshUI(context); + } + + protected override void SubscribeCustomEvents() + { + GameEntry.Event.Subscribe(MenuStartRequestedEventArgs.EventId, OnStartRequested); + GameEntry.Event.Subscribe(MenuSettingsRequestedEventArgs.EventId, OnSettingsRequested); + GameEntry.Event.Subscribe(MenuExitRequestedEventArgs.EventId, OnExitRequested); + } + + protected override void UnsubscribeCustomEvents() + { + GameEntry.Event.Unsubscribe(MenuStartRequestedEventArgs.EventId, OnStartRequested); + GameEntry.Event.Unsubscribe(MenuSettingsRequestedEventArgs.EventId, OnSettingsRequested); + GameEntry.Event.Unsubscribe(MenuExitRequestedEventArgs.EventId, OnExitRequested); + } + + public override int? OpenUI(object userData = null) + { + if (userData is MenuFormContext context) + { + return OpenUIInternal(context); + } + + if (userData is MenuFormRawData rawDataFromUserData) + { + return OpenUI(rawDataFromUserData); + } + + if (userData != null) + { + Log.Warning("MenuFormController.OpenUI() userData type is invalid."); + return null; + } + + if (_useCase == null) + { + Log.Error("MenuFormController.OpenUI() useCase is null."); + return null; + } + + return OpenUI(_useCase.CreateInitialModel()); + } + + public int? OpenUI(MenuFormRawData rawData) + { + return OpenUIInternal(BuildContext(rawData)); + } + + public override void BindUseCase(IUIUseCase useCase) + { + if (!(useCase is MenuFormUseCase menuFormUseCase)) + { + Log.Error("MenuFormController.BindUseCase() useCase is invalid."); + return; + } + + _useCase = menuFormUseCase; + } + + private void OnStartRequested(object sender, GameEventArgs e) + { + if (!IsCurrentFormEvent(sender) || !(e is MenuStartRequestedEventArgs)) + { + return; + } + + if (GameEntry.Procedure.CurrentProcedure is ProcedureMenu procedureMenu) + { + procedureMenu.GameStart = true; + return; + } + + Log.Warning("MenuFormController received start request outside ProcedureMenu."); + } + + private void OnSettingsRequested(object sender, GameEventArgs e) + { + if (!IsCurrentFormEvent(sender) || !(e is MenuSettingsRequestedEventArgs)) + { + return; + } + + Log.Warning("Menu settings button click is TODO."); + } + + private void OnExitRequested(object sender, GameEventArgs e) + { + if (!IsCurrentFormEvent(sender) || !(e is MenuExitRequestedEventArgs)) + { + return; + } + + UnityGameFramework.Runtime.GameEntry.Shutdown(ShutdownType.Quit); + } + + private bool IsCurrentFormEvent(object sender) + { + return Form != null && ReferenceEquals(sender, Form); + } + + private static MenuFormContext BuildContext(MenuFormRawData rawData) + { + return new MenuFormContext + { + TitleText = rawData?.TitleText ?? string.Empty, + StartButtonText = rawData?.StartButtonText ?? string.Empty, + SettingsButtonText = rawData?.SettingsButtonText ?? string.Empty, + ExitButtonText = rawData?.ExitButtonText ?? string.Empty + }; + } + } +} diff --git a/src-ref/UI/Menu/Controller/TestMenuFormController.cs b/src-ref/UI/Menu/Controller/TestMenuFormController.cs new file mode 100644 index 0000000..eea3db3 --- /dev/null +++ b/src-ref/UI/Menu/Controller/TestMenuFormController.cs @@ -0,0 +1,57 @@ +using GeometryTD.CustomEvent; +using GeometryTD.Definition; +using GeometryTD.Procedure; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class TestMenuFormController : UIFormControllerCommonBase + { + protected override UIFormType UIFormTypeId => UIFormType.TestMenuForm; + + protected override void RefreshUI(TestMenuForm form, TestMenuFormContext context) + { + form.RefreshUI(context); + } + + protected override void SubscribeCustomEvents() + { + } + + protected override void UnsubscribeCustomEvents() + { + } + + public override int? OpenUI(object userData = null) + { + if (userData is TestMenuFormContext context) + { + return OpenUIInternal(context); + } + + if (userData != null) + { + Log.Warning("TestMenuFormController.OpenUI() userData type is invalid."); + return null; + } + + return OpenUIInternal(BuildDefaultContext()); + } + + public override void BindUseCase(IUIUseCase useCase) + { + if (useCase != null) + { + Log.Warning("TestMenuFormController does not use a use case."); + } + } + + private static TestMenuFormContext BuildDefaultContext() + { + return new TestMenuFormContext + { + CurrentNodeType = RunNodeType.None + }; + } + } +} diff --git a/src-ref/UI/Menu/RawData/MenuFormRawData.cs b/src-ref/UI/Menu/RawData/MenuFormRawData.cs new file mode 100644 index 0000000..4e865ab --- /dev/null +++ b/src-ref/UI/Menu/RawData/MenuFormRawData.cs @@ -0,0 +1,13 @@ +namespace GeometryTD.UI +{ + public class MenuFormRawData + { + public string TitleText { get; set; } + + public string StartButtonText { get; set; } + + public string SettingsButtonText { get; set; } + + public string ExitButtonText { get; set; } + } +} diff --git a/src-ref/UI/Menu/UseCase/MenuFormUseCase.cs b/src-ref/UI/Menu/UseCase/MenuFormUseCase.cs new file mode 100644 index 0000000..f3cdaae --- /dev/null +++ b/src-ref/UI/Menu/UseCase/MenuFormUseCase.cs @@ -0,0 +1,16 @@ +namespace GeometryTD.UI +{ + public class MenuFormUseCase : IUIUseCase + { + public MenuFormRawData CreateInitialModel() + { + return new MenuFormRawData + { + TitleText = "GeometryTD", + StartButtonText = "Start", + SettingsButtonText = "Settings", + ExitButtonText = "Exit" + }; + } + } +} diff --git a/src-ref/UI/Menu/View/MenuForm.cs b/src-ref/UI/Menu/View/MenuForm.cs new file mode 100644 index 0000000..5f1c55e --- /dev/null +++ b/src-ref/UI/Menu/View/MenuForm.cs @@ -0,0 +1,76 @@ +using GeometryTD.CustomEvent; +using TMPro; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class MenuForm : UGuiForm + { + [SerializeField] private TMP_Text _titleText; + [SerializeField] private TMP_Text _startButtonText; + [SerializeField] private TMP_Text _settingsButtonText; + [SerializeField] private TMP_Text _exitButtonText; + + private MenuFormContext _context; + + public void RefreshUI(MenuFormContext context) + { + _context = context; + if (_context == null) + { + return; + } + + SetText(_titleText, _context.TitleText); + SetText(_startButtonText, _context.StartButtonText); + SetText(_settingsButtonText, _context.SettingsButtonText); + SetText(_exitButtonText, _context.ExitButtonText); + } + + public void OnStartButtonClick() + { + GameEntry.Event.Fire(this, MenuStartRequestedEventArgs.Create()); + } + + public void OnSettingsButtonClick() + { + GameEntry.Event.Fire(this, MenuSettingsRequestedEventArgs.Create()); + } + + public void OnExitButtonClick() + { + GameEntry.Event.Fire(this, MenuExitRequestedEventArgs.Create()); + } + + protected override void OnOpen(object userData) + { + base.OnOpen(userData); + + if (userData is MenuFormContext context) + { + RefreshUI(context); + return; + } + + Log.Warning("MenuForm requires MenuFormContext as userData."); + } + + protected override void OnClose(bool isShutdown, object userData) + { + _context = null; + + base.OnClose(isShutdown, userData); + } + + private static void SetText(TMP_Text textComponent, string value) + { + if (textComponent == null) + { + return; + } + + textComponent.text = value ?? string.Empty; + } + } +} diff --git a/src-ref/UI/Menu/View/TestMenuForm.cs b/src-ref/UI/Menu/View/TestMenuForm.cs new file mode 100644 index 0000000..2c60bc4 --- /dev/null +++ b/src-ref/UI/Menu/View/TestMenuForm.cs @@ -0,0 +1,80 @@ +using GeometryTD.CustomEvent; +using GeometryTD.Procedure; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class TestMenuForm : UGuiForm + { + [SerializeField] private GameObject combatButtonObject; + [SerializeField] private GameObject eventButtonObject; + [SerializeField] private GameObject shopButtonObject; + + private TestMenuFormContext _context; + + public void RefreshUI(TestMenuFormContext context) + { + _context = context; + if (_context == null) + { + SetButtonVisible(combatButtonObject, false); + SetButtonVisible(eventButtonObject, false); + SetButtonVisible(shopButtonObject, false); + return; + } + + SetButtonVisible( + combatButtonObject, + _context.CurrentNodeType == RunNodeType.Combat || _context.CurrentNodeType == RunNodeType.BossCombat); + SetButtonVisible(eventButtonObject, _context.CurrentNodeType == RunNodeType.Event); + SetButtonVisible(shopButtonObject, _context.CurrentNodeType == RunNodeType.Shop); + } + + public void OnCombatButtonClick() + { + GameEntry.Event.Fire(this, TestMenuNodeClickEventArgs.Create(TestMenuNodeType.Combat)); + } + + public void OnEventButtonClick() + { + GameEntry.Event.Fire(this, TestMenuNodeClickEventArgs.Create(TestMenuNodeType.Event)); + } + + public void OnShopButtonClick() + { + GameEntry.Event.Fire(this, TestMenuNodeClickEventArgs.Create(TestMenuNodeType.Shop)); + } + + protected override void OnOpen(object userData) + { + base.OnOpen(userData); + + if (userData is TestMenuFormContext context) + { + RefreshUI(context); + return; + } + + Log.Warning("TestMenuForm requires TestMenuFormContext as userData."); + } + + protected override void OnClose(bool isShutdown, object userData) + { + _context = null; + SetButtonVisible(combatButtonObject, false); + SetButtonVisible(eventButtonObject, false); + SetButtonVisible(shopButtonObject, false); + + base.OnClose(isShutdown, userData); + } + + private static void SetButtonVisible(GameObject buttonObject, bool visible) + { + if (buttonObject != null) + { + buttonObject.SetActive(visible); + } + } + } +} diff --git a/src-ref/UI/Shop/Context/GoodsItemContext.cs b/src-ref/UI/Shop/Context/GoodsItemContext.cs new file mode 100644 index 0000000..909e052 --- /dev/null +++ b/src-ref/UI/Shop/Context/GoodsItemContext.cs @@ -0,0 +1,14 @@ +namespace GeometryTD.UI +{ + public sealed class GoodsItemContext : UIContext + { + public int GoodsIndex; + public string Title; + public string TypeText; + public string Description; + public string[] TagTexts; + public string PurchaseButtonText; + public bool CanPurchase; + public IconAreaContext IconAreaContext; + } +} diff --git a/src-ref/UI/Shop/Context/ShopFormContext.cs b/src-ref/UI/Shop/Context/ShopFormContext.cs new file mode 100644 index 0000000..b47827c --- /dev/null +++ b/src-ref/UI/Shop/Context/ShopFormContext.cs @@ -0,0 +1,8 @@ +namespace GeometryTD.UI +{ + public sealed class ShopFormContext : UIContext + { + public string GoldText; + public GoodsItemContext[] GoodsItems; + } +} diff --git a/src-ref/UI/Shop/ContextBuilder/ShopFormController.ContextBuilder.cs b/src-ref/UI/Shop/ContextBuilder/ShopFormController.ContextBuilder.cs new file mode 100644 index 0000000..546749f --- /dev/null +++ b/src-ref/UI/Shop/ContextBuilder/ShopFormController.ContextBuilder.cs @@ -0,0 +1,59 @@ +using GeometryTD.CustomUtility; + +namespace GeometryTD.UI +{ + public sealed partial class ShopFormController + { + private static ShopFormContext BuildContext(ShopFormRawData rawData) + { + GoodsItemContext[] goodsItemContexts = System.Array.Empty(); + if (rawData?.GoodsItems != null && rawData.GoodsItems.Count > 0) + { + goodsItemContexts = new GoodsItemContext[rawData.GoodsItems.Count]; + for (int i = 0; i < rawData.GoodsItems.Count; i++) + { + GoodsItemRawData item = rawData.GoodsItems[i]; + goodsItemContexts[i] = new GoodsItemContext + { + GoodsIndex = item?.GoodsIndex ?? i, + Title = item?.Title ?? string.Empty, + TypeText = item?.TypeText ?? string.Empty, + Description = BuildDescription(item), + TagTexts = BuildTagTexts(item), + PurchaseButtonText = item != null && item.IsPurchased ? "已购买" : $"购买 {item?.Price ?? 0}", + CanPurchase = item != null && !item.IsPurchased, + IconAreaContext = item?.IconAreaContext + }; + } + } + + return new ShopFormContext + { + GoldText = $"金币: {rawData?.PlayerGold ?? 0}", + GoodsItems = goodsItemContexts + }; + } + + private static string BuildDescription(GoodsItemRawData rawData) + { + string baseDescription = rawData?.Description ?? string.Empty; + string tagDescription = TagDisplayUtility.BuildTagDescriptionText(rawData?.Tags); + if (string.IsNullOrWhiteSpace(tagDescription)) + { + return baseDescription; + } + + if (string.IsNullOrWhiteSpace(baseDescription)) + { + return tagDescription; + } + + return $"{baseDescription}\n{tagDescription}"; + } + + private static string[] BuildTagTexts(GoodsItemRawData rawData) + { + return TagDisplayUtility.BuildTagTexts(rawData?.Tags); + } + } +} diff --git a/src-ref/UI/Shop/Controller/ShopFormController.cs b/src-ref/UI/Shop/Controller/ShopFormController.cs new file mode 100644 index 0000000..08fa6e1 --- /dev/null +++ b/src-ref/UI/Shop/Controller/ShopFormController.cs @@ -0,0 +1,137 @@ +using GeometryTD.CustomEvent; +using GeometryTD.Definition; +using GameFramework.Event; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public sealed partial class ShopFormController : UIFormControllerCommonBase + { + private ShopFormUseCase _useCase; + + protected override UIFormType UIFormTypeId => UIFormType.ShopForm; + + protected override void RefreshUI(ShopForm form, ShopFormContext context) + { + form.RefreshUI(context); + } + + protected override void SubscribeCustomEvents() + { + GameEntry.Event.Subscribe(ShopPurchaseRequestedEventArgs.EventId, OnPurchaseRequested); + GameEntry.Event.Subscribe(ShopExitRequestedEventArgs.EventId, OnExitRequested); + GameEntry.Event.Subscribe(ShopInventoryRequestedEventArgs.EventId, OnInventoryRequested); + } + + protected override void UnsubscribeCustomEvents() + { + GameEntry.Event.Unsubscribe(ShopPurchaseRequestedEventArgs.EventId, OnPurchaseRequested); + GameEntry.Event.Unsubscribe(ShopExitRequestedEventArgs.EventId, OnExitRequested); + GameEntry.Event.Unsubscribe(ShopInventoryRequestedEventArgs.EventId, OnInventoryRequested); + } + + public override int? OpenUI(object userData = null) + { + if (userData is ShopFormContext context) + { + return OpenUIInternal(context); + } + + if (userData is ShopFormRawData rawDataFromUserData) + { + return OpenUI(rawDataFromUserData); + } + + if (userData != null) + { + Log.Warning("ShopFormController.OpenUI() userData type is invalid."); + return null; + } + + if (_useCase == null) + { + Log.Error("ShopFormController.OpenUI() useCase is null."); + return null; + } + + ShopFormRawData rawData = _useCase.CreateInitialModel(); + return OpenUI(rawData); + } + + public int? OpenUI(ShopFormRawData rawData) + { + ShopFormContext context = BuildContext(rawData); + return OpenUIInternal(context); + } + + public override void BindUseCase(IUIUseCase useCase) + { + if (!(useCase is ShopFormUseCase shopFormUseCase)) + { + Log.Error("ShopFormController.BindUseCase() useCase is invalid."); + return; + } + + _useCase = shopFormUseCase; + } + + private void OnPurchaseRequested(object sender, GameEventArgs e) + { + if (!IsEventFromCurrentForm(sender) || !(e is ShopPurchaseRequestedEventArgs args) || _useCase == null) + { + return; + } + + if (!_useCase.TryPurchase(args.GoodsIndex, out ShopFormRawData rawData)) + { + return; + } + + SetContext(BuildContext(rawData)); + RefreshCurrentUI(); + } + + private void OnExitRequested(object sender, GameEventArgs e) + { + if (!IsEventFromCurrentForm(sender) || !(e is ShopExitRequestedEventArgs)) + { + return; + } + + GameEntry.ShopNode.EndShop(); + } + + private void OnInventoryRequested(object sender, GameEventArgs e) + { + if (!IsEventFromCurrentForm(sender) || !(e is ShopInventoryRequestedEventArgs)) + { + return; + } + + GameEntry.UIRouter.OpenUI(UIFormType.RepoForm); + } + + private bool IsEventFromCurrentForm(object sender) + { + if (Form == null) + { + return false; + } + + if (ReferenceEquals(sender, Form)) + { + return true; + } + + if (sender is Component component) + { + ShopForm ownerForm = component.GetComponentInParent(); + return ownerForm == Form; + } + + return false; + } + + } +} diff --git a/src-ref/UI/Shop/RawData/GoodsItemRawData.cs b/src-ref/UI/Shop/RawData/GoodsItemRawData.cs new file mode 100644 index 0000000..3751b5d --- /dev/null +++ b/src-ref/UI/Shop/RawData/GoodsItemRawData.cs @@ -0,0 +1,17 @@ +using GeometryTD.Definition; + +namespace GeometryTD.UI +{ + public sealed class GoodsItemRawData + { + public int GoodsIndex; + public string Title; + public string TypeText; + public string Description; + public int Price; + public TagType[] Tags; + public IconAreaContext IconAreaContext; + public TowerCompItemData SourceItem; + public bool IsPurchased; + } +} diff --git a/src-ref/UI/Shop/RawData/ShopFormRawData.cs b/src-ref/UI/Shop/RawData/ShopFormRawData.cs new file mode 100644 index 0000000..c3c2c95 --- /dev/null +++ b/src-ref/UI/Shop/RawData/ShopFormRawData.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace GeometryTD.UI +{ + public sealed class ShopFormRawData + { + public int PlayerGold; + public List GoodsItems; + } +} diff --git a/src-ref/UI/Shop/UseCase/ShopFormUseCase.cs b/src-ref/UI/Shop/UseCase/ShopFormUseCase.cs new file mode 100644 index 0000000..6f86eba --- /dev/null +++ b/src-ref/UI/Shop/UseCase/ShopFormUseCase.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public sealed class ShopFormUseCase : IUIUseCase + { + private const int GoodsCount = 4; + + private readonly List _currentGoods = new List(GoodsCount); + + public bool PrepareForOpen(int runSeed = 0, int sequenceIndex = -1) + { + if (GameEntry.InventoryGeneration == null) + { + Log.Warning("ShopFormUseCase.PrepareForOpen() inventory generation component is null."); + return false; + } + + _currentGoods.Clear(); + _currentGoods.AddRange(GameEntry.InventoryGeneration.BuildShopGoods(GoodsCount, runSeed, sequenceIndex)); + return _currentGoods.Count == GoodsCount; + } + + public ShopFormRawData CreateInitialModel() + { + return new ShopFormRawData + { + PlayerGold = GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.Gold : 0, + GoodsItems = new List(_currentGoods) + }; + } + + public bool TryPurchase(int goodsIndex, out ShopFormRawData updatedRawData) + { + updatedRawData = null; + if (goodsIndex < 0 || goodsIndex >= _currentGoods.Count) + { + Log.Warning("ShopFormUseCase.TryPurchase() goods index is invalid: {0}", goodsIndex); + return false; + } + + if (GameEntry.PlayerInventory == null) + { + Log.Warning("ShopFormUseCase.TryPurchase() player inventory is null."); + return false; + } + + GoodsItemRawData goodsItem = _currentGoods[goodsIndex]; + if (goodsItem == null || goodsItem.SourceItem == null) + { + return false; + } + + if (goodsItem.IsPurchased) + { + Log.Warning("ShopFormUseCase.TryPurchase() goods item {0} already purchased.", goodsIndex); + return false; + } + + if (!GameEntry.PlayerInventory.TryPurchaseComponent(goodsItem.SourceItem, goodsItem.Price)) + { + Log.Warning("ShopFormUseCase.TryPurchase() failed. Purchase command was rejected for goods item {0}.", goodsIndex); + return false; + } + + goodsItem.IsPurchased = true; + updatedRawData = CreateInitialModel(); + return true; + } + + } +} diff --git a/src-ref/UI/Shop/View/GoodsItem.cs b/src-ref/UI/Shop/View/GoodsItem.cs new file mode 100644 index 0000000..94dbd2e --- /dev/null +++ b/src-ref/UI/Shop/View/GoodsItem.cs @@ -0,0 +1,195 @@ +using GeometryTD.CustomEvent; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace GeometryTD.UI +{ + public class GoodsItem : MonoBehaviour + { + [SerializeField] private IconArea _iconArea; + + [SerializeField] private TMP_Text _titleText; + + [SerializeField] private TMP_Text _typeText; + + [SerializeField] private TMP_Text _descriptionText; + + [SerializeField] private RectTransform _descriptionScrollView; + + [SerializeField] private RectTransform _descriptionContent; + + [SerializeField] private float _maxDescriptionViewportHeight = 0f; + + [SerializeField] private Transform _tagsParent; + + [SerializeField] private TMP_Text _purchaseButtonText; + + [SerializeField] private GameObject _tagItemPrefab; + + [SerializeField] private int _index; + + private GoodsItemContext _context; + + public void OnInit(GoodsItemContext context) + { + _context = context; + _index = context?.GoodsIndex ?? -1; + + _iconArea?.OnInit(context?.IconAreaContext ?? new IconAreaContext()); + + if (_titleText != null) + { + _titleText.text = context?.Title ?? string.Empty; + } + + if (_typeText != null) + { + _typeText.text = context?.TypeText ?? string.Empty; + } + + if (_descriptionText != null) + { + _descriptionText.text = context?.Description ?? string.Empty; + } + + ResizeDescriptionArea(); + + if (_purchaseButtonText != null) + { + _purchaseButtonText.text = context?.PurchaseButtonText ?? string.Empty; + } + + RefreshTags(context?.TagTexts); + } + + public void OnReset() + { + _context = null; + _index = -1; + _iconArea?.OnReset(); + + if (_titleText != null) + { + _titleText.text = string.Empty; + } + + if (_typeText != null) + { + _typeText.text = string.Empty; + } + + if (_descriptionText != null) + { + _descriptionText.text = string.Empty; + } + + ResetDescriptionScroll(); + + if (_purchaseButtonText != null) + { + _purchaseButtonText.text = string.Empty; + } + + ClearTags(); + } + + public void OnPurchaseButtonClick() + { + if (_context == null || !_context.CanPurchase || _index < 0) + { + return; + } + + GameEntry.Event.Fire(this, ShopPurchaseRequestedEventArgs.Create(_index)); + } + + private void RefreshTags(string[] tagTexts) + { + ClearTags(); + if (_tagsParent == null || _tagItemPrefab == null || tagTexts == null) + { + return; + } + + for (int i = 0; i < tagTexts.Length; i++) + { + if (string.IsNullOrEmpty(tagTexts[i])) + { + continue; + } + + GameObject tagItemObject = Instantiate(_tagItemPrefab, _tagsParent); + TagItem tagItem = tagItemObject.GetComponent(); + if (tagItem != null) + { + tagItem.OnInit(new TagItemContext + { + TagName = tagTexts[i] + }); + } + } + } + + private void ResizeDescriptionArea() + { + if (_descriptionText == null) + { + return; + } + + _descriptionText.ForceMeshUpdate(); + float descriptionHeight = Mathf.Max(0f, _descriptionText.preferredHeight); + SetRectHeight(_descriptionText.rectTransform, descriptionHeight); + + if (_descriptionContent != null) + { + SetRectHeight(_descriptionContent, descriptionHeight); + } + + if (_descriptionScrollView != null) + { + float viewportHeight = _maxDescriptionViewportHeight > 0f + ? Mathf.Min(descriptionHeight, _maxDescriptionViewportHeight) + : descriptionHeight; + SetRectHeight(_descriptionScrollView, viewportHeight); + } + + ResetDescriptionScroll(); + } + + private void ResetDescriptionScroll() + { + if (_descriptionScrollView != null && + _descriptionScrollView.TryGetComponent(out ScrollRect scrollRect)) + { + scrollRect.verticalNormalizedPosition = 1f; + } + } + + private static void SetRectHeight(RectTransform rectTransform, float height) + { + if (rectTransform == null) + { + return; + } + + Vector2 size = rectTransform.sizeDelta; + size.y = height; + rectTransform.sizeDelta = size; + } + + private void ClearTags() + { + if (_tagsParent == null) + { + return; + } + + for (int i = _tagsParent.childCount - 1; i >= 0; i--) + { + Destroy(_tagsParent.GetChild(i).gameObject); + } + } + } +} diff --git a/src-ref/UI/Shop/View/ShopForm.cs b/src-ref/UI/Shop/View/ShopForm.cs new file mode 100644 index 0000000..67a5bc1 --- /dev/null +++ b/src-ref/UI/Shop/View/ShopForm.cs @@ -0,0 +1,94 @@ +using GeometryTD.CustomEvent; +using TMPro; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.UI +{ + public class ShopForm : UGuiForm + { + [SerializeField] private TMP_Text _goldText; + + [SerializeField] private GoodsItem[] _goldItems; + + private ShopFormContext _context; + + public void RefreshUI(ShopFormContext context) + { + _context = context; + if (_goldText != null) + { + _goldText.text = context?.GoldText ?? string.Empty; + } + + RefreshGoodsItems(context?.GoodsItems); + } + + public void OnInventoryButtonClick() + { + GameEntry.Event.Fire(this, ShopInventoryRequestedEventArgs.Create()); + } + + public void OnExitButtonClick() + { + GameEntry.Event.Fire(this, ShopExitRequestedEventArgs.Create()); + } + + protected override void OnOpen(object userData) + { + base.OnOpen(userData); + if (userData is ShopFormContext context) + { + RefreshUI(context); + return; + } + + Log.Warning("ShopForm requires ShopFormContext as userData."); + } + + protected override void OnClose(bool isShutdown, object userData) + { + _context = null; + if (_goldText != null) + { + _goldText.text = string.Empty; + } + + if (_goldItems != null) + { + foreach (var item in _goldItems) + { + item?.OnReset(); + } + } + + base.OnClose(isShutdown, userData); + } + + private void RefreshGoodsItems(GoodsItemContext[] goodsItems) + { + if (_goldItems == null) + { + return; + } + + for (int i = 0; i < _goldItems.Length; i++) + { + GoodsItem goodsItem = _goldItems[i]; + if (goodsItem == null) + { + continue; + } + + if (goodsItems != null && i < goodsItems.Length && goodsItems[i] != null) + { + goodsItem.OnInit(goodsItems[i]); + } + else + { + goodsItem.OnReset(); + } + } + } + } +} \ No newline at end of file diff --git a/src-ref/UI/Templates/GameScene/Context/DisplayItemContext.cs b/src-ref/UI/Templates/GameScene/Context/DisplayItemContext.cs new file mode 100644 index 0000000..2b69980 --- /dev/null +++ b/src-ref/UI/Templates/GameScene/Context/DisplayItemContext.cs @@ -0,0 +1,11 @@ +// using Definition.Enum; +// +// namespace UI +// { +// public class DisplayItemContext : UIContext +// { +// public string IconAssetName; +// public ItemRarity Rarity; +// public bool IsWeapon; +// } +// } \ No newline at end of file diff --git a/src-ref/UI/Templates/GameScene/Context/DisplayItemInfoFormContext.cs b/src-ref/UI/Templates/GameScene/Context/DisplayItemInfoFormContext.cs new file mode 100644 index 0000000..697b02b --- /dev/null +++ b/src-ref/UI/Templates/GameScene/Context/DisplayItemInfoFormContext.cs @@ -0,0 +1,18 @@ +// using Definition.Enum; +// using UnityEngine; +// +// namespace UI +// { +// public class DisplayItemInfoFormContext : UIContext +// { +// public int Index; +// public string IconAssetName; +// public string Title; +// public string TypeText; +// public ItemRarity Rarity; +// public string Description; +// public int Price; +// public bool IsWeapon; +// public Vector3 TargetPos; +// } +// } \ No newline at end of file diff --git a/src-ref/UI/Templates/GameScene/Context/DisplayListAreaContext.cs b/src-ref/UI/Templates/GameScene/Context/DisplayListAreaContext.cs new file mode 100644 index 0000000..2cf4243 --- /dev/null +++ b/src-ref/UI/Templates/GameScene/Context/DisplayListAreaContext.cs @@ -0,0 +1,14 @@ +// using System.Collections; +// using System.Collections.Generic; +// using UnityEngine; +// +// namespace UI +// { +// public class DisplayListAreaContext : UIContext +// { +// public string Title; +// public int CurrentCount; +// public int MaxCount = -1; +// public DisplayItemContext[] ItemContexts; +// } +// } \ No newline at end of file diff --git a/src-ref/UI/Templates/GameScene/Context/GoodsItemContext.cs b/src-ref/UI/Templates/GameScene/Context/GoodsItemContext.cs new file mode 100644 index 0000000..e74d35e --- /dev/null +++ b/src-ref/UI/Templates/GameScene/Context/GoodsItemContext.cs @@ -0,0 +1,15 @@ +// using Definition.Enum; +// using UnityEngine; +// +// namespace UI +// { +// public class GoodsItemContext : UIContext +// { +// public string Title; +// public ItemRarity Rarity; +// public string Type; +// public Sprite Icon; +// public string Description; +// public int Price; +// } +// } \ No newline at end of file diff --git a/src-ref/UI/Templates/GameScene/Context/HudFormContext.cs b/src-ref/UI/Templates/GameScene/Context/HudFormContext.cs new file mode 100644 index 0000000..24dd0d8 --- /dev/null +++ b/src-ref/UI/Templates/GameScene/Context/HudFormContext.cs @@ -0,0 +1,10 @@ +// using System.Collections; +// using System.Collections.Generic; +// using UnityEngine; +// +// namespace UI +// { +// public class HudFormContext : UIContext +// { +// } +// } \ No newline at end of file diff --git a/src-ref/UI/Templates/GameScene/Context/LevelUpFormContext.cs b/src-ref/UI/Templates/GameScene/Context/LevelUpFormContext.cs new file mode 100644 index 0000000..4caf8cd --- /dev/null +++ b/src-ref/UI/Templates/GameScene/Context/LevelUpFormContext.cs @@ -0,0 +1,10 @@ +// using System.Collections.Generic; +// +// namespace UI +// { +// public class LevelUpFormContext : UIContext +// { +// public List Props; +// public int RefreshPrice; +// } +// } diff --git a/src-ref/UI/Templates/GameScene/Context/LevelUpRewardItemContext.cs b/src-ref/UI/Templates/GameScene/Context/LevelUpRewardItemContext.cs new file mode 100644 index 0000000..b2d794c --- /dev/null +++ b/src-ref/UI/Templates/GameScene/Context/LevelUpRewardItemContext.cs @@ -0,0 +1,14 @@ +// using Definition.Enum; +// using UnityEngine; +// +// namespace UI +// { +// public class LevelUpRewardItemContext : UIContext +// { +// public string Title; +// public Sprite Icon; +// public ItemRarity ItemRarity; +// public string Description; +// public string IconAssetName; +// } +// } diff --git a/src-ref/UI/Templates/GameScene/Context/ShopFormContext.cs b/src-ref/UI/Templates/GameScene/Context/ShopFormContext.cs new file mode 100644 index 0000000..397a025 --- /dev/null +++ b/src-ref/UI/Templates/GameScene/Context/ShopFormContext.cs @@ -0,0 +1,15 @@ +// using System.Collections.Generic; +// +// namespace UI +// { +// public class ShopFormContext : UIContext +// { +// public int CurrentLevel; +// public int RefreshPrice; +// public int PlayerCoin; +// public List GoodsItems; +// public DisplayListAreaContext PropListContext; +// public DisplayListAreaContext WeaponListContext; +// public float WeaponRecycleRate = 0.3f; +// } +// } diff --git a/src-ref/UI/Templates/GameScene/Controller/DisplayItemInfoFormController.cs b/src-ref/UI/Templates/GameScene/Controller/DisplayItemInfoFormController.cs new file mode 100644 index 0000000..9052df5 --- /dev/null +++ b/src-ref/UI/Templates/GameScene/Controller/DisplayItemInfoFormController.cs @@ -0,0 +1,116 @@ +// using CustomEvent; +// using Definition.Enum; +// using GameFramework.Event; +// using UnityGameFramework.Runtime; +// +// namespace UI +// { +// public class DisplayItemInfoFormController : UIFormControllerCommonBase< +// DisplayItemInfoFormContext, DisplayItemInfoForm> +// { +// protected override UIFormType UIFormTypeId => UIFormType.DisplayItemInfoForm; +// +// private bool _locked = false; +// +// protected override void SubscribeCustomEvents() +// { +// GameEntry.Event.Subscribe(DisplayItemInfoLockEventArgs.EventId, DisplayItemInfoLock); +// GameEntry.Event.Subscribe(DisplayItemInfoHideEventArgs.EventId, DisplayItemInfoHide); +// } +// +// protected override void UnsubscribeCustomEvents() +// { +// GameEntry.Event.Unsubscribe(DisplayItemInfoLockEventArgs.EventId, DisplayItemInfoLock); +// GameEntry.Event.Unsubscribe(DisplayItemInfoHideEventArgs.EventId, DisplayItemInfoHide); +// } +// +// protected override void RefreshUI(DisplayItemInfoForm form, DisplayItemInfoFormContext context) +// { +// form.RefreshUI(context); +// } +// +// protected override void CloseLoadedFormDirect(DisplayItemInfoForm form) +// { +// GameEntry.UI.CloseUIForm(form); +// } +// +// private static DisplayItemInfoFormContext BuildContext(DisplayItemInfoFormRawData rawData) +// { +// if (rawData == null) +// { +// return null; +// } +// +// return new DisplayItemInfoFormContext +// { +// Index = rawData.Index, +// IconAssetName = rawData.IconAssetName, +// Title = rawData.Title, +// Rarity = rawData.Rarity, +// TypeText = rawData.TypeText, +// Description = rawData.Description, +// Price = rawData.Price, +// IsWeapon = rawData.IsWeapon, +// TargetPos = rawData.TargetPos +// }; +// } +// +// public int? OpenUI(DisplayItemInfoFormRawData rawData) +// { +// _locked = false; +// DisplayItemInfoFormContext context = BuildContext(rawData); +// return OpenUIInternal(context); +// } +// +// public override int? OpenUI(object userData = null) +// { +// _locked = false; +// if (userData is DisplayItemInfoFormContext context) +// { +// return OpenUIInternal(context); +// } +// +// if (userData is DisplayItemInfoFormRawData rawData) +// { +// return OpenUI(rawData); +// } +// +// if (userData != null) +// { +// Log.Warning("DisplayItemInfoFormController.OpenUI() userData type is invalid."); +// return null; +// } +// +// return OpenUIInternal(Context); +// } +// +// public override void BindUseCase(IUIUseCase useCase) +// { +// if (!(useCase is DisplayItemInfoFormUseCase)) +// { +// Log.Error("DisplayItemInfoForm.BindUseCase() useCase is invalid."); +// } +// } +// +// #region Event Handlers +// +// private void DisplayItemInfoLock(object sender, GameEventArgs e) +// { +// if (!(e is DisplayItemInfoLockEventArgs)) return; +// +// _locked = true; +// } +// +// private void DisplayItemInfoHide(object sender, GameEventArgs e) +// { +// if (!(e is DisplayItemInfoHideEventArgs args)) return; +// +// if (!args.Force && _locked && sender is not DisplayItemInfoForm) return; +// +// GameEntry.UIRouter.CloseUI(UIFormType.DisplayItemInfoForm); +// _locked = false; +// } +// +// #endregion +// } +// } \ No newline at end of file diff --git a/src-ref/UI/Templates/GameScene/Controller/HudFormController.cs b/src-ref/UI/Templates/GameScene/Controller/HudFormController.cs new file mode 100644 index 0000000..ab4cd34 --- /dev/null +++ b/src-ref/UI/Templates/GameScene/Controller/HudFormController.cs @@ -0,0 +1,46 @@ +// using Definition.Enum; +// using UnityGameFramework.Runtime; +// +// namespace UI +// { +// public class HudFormController : UIFormControllerCommonBase +// { +// protected override UIFormType UIFormTypeId => UIFormType.HudForm; +// +// protected override void RefreshUI(HudForm form, HudFormContext context) +// { +// form.RefreshUI(context); +// } +// +// private static HudFormContext BuildHudFormContext() +// { +// return new HudFormContext(); +// } +// +// public override int? OpenUI(object userData = null) +// { +// if (userData is HudFormContext context) +// { +// return OpenUIInternal(context); +// } +// +// if (userData != null) +// { +// Log.Warning("HudFormController.OpenUI() userData type is invalid."); +// return null; +// } +// +// return OpenUIInternal(BuildHudFormContext()); +// } +// +// public int? OpenUI(HudFormContext context) +// { +// return OpenUIInternal(context); +// } +// +// public override void BindUseCase(IUIUseCase useCase) +// { +// Log.Info("HudFormController doesn't need UseCase"); +// } +// } +// } diff --git a/src-ref/UI/Templates/GameScene/Controller/LevelUpFormController.cs b/src-ref/UI/Templates/GameScene/Controller/LevelUpFormController.cs new file mode 100644 index 0000000..ff9ed2a --- /dev/null +++ b/src-ref/UI/Templates/GameScene/Controller/LevelUpFormController.cs @@ -0,0 +1,169 @@ +// using System.Collections.Generic; +// using CustomEvent; +// using Definition.Enum; +// using CustomUtility; +// using GameFramework.Event; +// using UnityGameFramework.Runtime; +// +// namespace UI +// { +// public class LevelUpFormController : UIFormControllerCommonBase +// { +// private LevelUpFormUseCase _useCase; +// +// protected override UIFormType UIFormTypeId => UIFormType.LevelUpForm; +// +// protected override void RefreshUI(LevelUpForm form, LevelUpFormContext context) +// { +// form.RefreshUI(context); +// } +// +// protected override void SubscribeCustomEvents() +// { +// GameEntry.Event.Subscribe(RefreshEventArgs.EventId, OnRefresh); +// GameEntry.Event.Subscribe(LevelUpPropSelectedEventArgs.EventId, OnLevelUpPropSelected); +// } +// +// protected override void UnsubscribeCustomEvents() +// { +// GameEntry.Event.Unsubscribe(RefreshEventArgs.EventId, OnRefresh); +// GameEntry.Event.Unsubscribe(LevelUpPropSelectedEventArgs.EventId, OnLevelUpPropSelected); +// } +// +// private static LevelUpFormContext BuildContext(LevelUpFormRawData rawData) +// { +// if (rawData == null || rawData.Rewards == null) +// { +// return null; +// } +// +// List props = new List(rawData.Rewards.Count); +// foreach (var reward in rawData.Rewards) +// { +// if (reward == null) +// { +// continue; +// } +// +// props.Add(new LevelUpRewardItemContext +// { +// Title = reward.Title, +// Icon = null, +// ItemRarity = reward.Rarity, +// Description = ItemDescUtility.CreatePropDescription(reward.Modifiers), +// IconAssetName = reward.IconAssetName +// }); +// } +// +// return new LevelUpFormContext +// { +// RefreshPrice = rawData.RefreshPrice, +// Props = props +// }; +// } +// +// public override int? OpenUI(object userData = null) +// { +// if (userData is LevelUpFormContext context) +// { +// return OpenUIInternal(context); +// } +// +// if (userData is LevelUpFormRawData rawDataFromUserData) +// { +// return OpenUI(rawDataFromUserData); +// } +// +// if (userData != null) +// { +// Log.Warning("LevelUpFormController.OpenUI() userData type is invalid."); +// return null; +// } +// +// if (_useCase == null) +// { +// Log.Error("LevelUpFormController.OpenUI() useCase is null."); +// return null; +// } +// +// LevelUpFormRawData rawData = _useCase.CreateInitialModel(); +// return OpenUI(rawData); +// } +// +// public int? OpenUI(LevelUpFormRawData rawData) +// { +// LevelUpFormContext context = BuildContext(rawData); +// return OpenUIInternal(context); +// } +// +// public override void BindUseCase(IUIUseCase useCase) +// { +// if (!(useCase is LevelUpFormUseCase levelUpFormUseCase)) +// { +// Log.Error("LevelUpForm.BindUseCase() useCase is invalid."); +// return; +// } +// +// _useCase = levelUpFormUseCase; +// } +// +// private void SelectReward(int selectedIndex) +// { +// if (_useCase == null) +// { +// Log.Error("LevelUpFormController.OpenUI() useCase is null."); +// return; +// } +// +// LevelUpFormRawData rawData = _useCase.SelectReward(selectedIndex); +// if (rawData == null) +// { +// return; +// } +// +// OpenUI(rawData); +// } +// +// private void RefreshRewardList(int refreshCost) +// { +// if (_useCase == null) +// { +// Log.Error("LevelUpFormController.OpenUI() useCase is null."); +// return; +// } +// +// LevelUpFormRawData rawData = _useCase.TryRefresh(refreshCost); +// if (rawData == null) +// { +// return; +// } +// +// OpenUI(rawData); +// } +// +// private void OnRefresh(object sender, GameEventArgs e) +// { +// if (!(sender is LevelUpForm)) +// { +// return; +// } +// +// if (!(e is RefreshEventArgs args)) +// { +// return; +// } +// +// RefreshRewardList(args.Cost); +// } +// +// private void OnLevelUpPropSelected(object sender, GameEventArgs e) +// { +// if (!(e is LevelUpPropSelectedEventArgs args)) +// { +// return; +// } +// +// SelectReward(args.SelectedId); +// } +// } +// } diff --git a/src-ref/UI/Templates/GameScene/Controller/ShopFormController.cs b/src-ref/UI/Templates/GameScene/Controller/ShopFormController.cs new file mode 100644 index 0000000..68da533 --- /dev/null +++ b/src-ref/UI/Templates/GameScene/Controller/ShopFormController.cs @@ -0,0 +1,411 @@ +// using System.Collections.Generic; +// using CustomEvent; +// using Definition.DataStruct; +// using Definition.Enum; +// using Entity; +// using CustomUtility; +// using Entity.Weapon; +// using GameFramework.Event; +// using UnityEngine; +// using UnityGameFramework.Runtime; +// +// namespace UI +// { +// public class ShopFormController : UIFormControllerCommonBase +// { +// private ShopFormUseCase _useCase; +// private ShopFormRawData _rawData; +// +// protected override UIFormType UIFormTypeId => UIFormType.ShopForm; +// +// protected override void RefreshUI(ShopForm form, ShopFormContext context) +// { +// form.RefreshUI(context); +// } +// +// protected override void SubscribeCustomEvents() +// { +// GameEntry.Event.Subscribe(RefreshEventArgs.EventId, Refresh); +// GameEntry.Event.Subscribe(ShopPurchaseEventArgs.EventId, ShopPurchase); +// GameEntry.Event.Subscribe(ShopWeaponRecycleEventArgs.EventId, WeaponRecycle); +// GameEntry.Event.Subscribe(ShopContinueEventArgs.EventId, ShopContinue); +// GameEntry.Event.Subscribe(DisplayItemShowEventArgs.EventId, DisplayItemShow); +// } +// +// protected override void UnsubscribeCustomEvents() +// { +// GameEntry.Event.Unsubscribe(RefreshEventArgs.EventId, Refresh); +// GameEntry.Event.Unsubscribe(ShopPurchaseEventArgs.EventId, ShopPurchase); +// GameEntry.Event.Unsubscribe(ShopWeaponRecycleEventArgs.EventId, WeaponRecycle); +// GameEntry.Event.Unsubscribe(ShopContinueEventArgs.EventId, ShopContinue); +// GameEntry.Event.Unsubscribe(DisplayItemShowEventArgs.EventId, DisplayItemShow); +// } +// +// #region BuildContext +// +// private ShopFormContext BuildContext(ShopFormRawData rawData) +// { +// if (rawData == null) +// { +// return null; +// } +// +// _rawData = rawData; +// +// return new ShopFormContext +// { +// CurrentLevel = rawData.CurrentLevel, +// RefreshPrice = rawData.RefreshPrice, +// PlayerCoin = rawData.PlayerCoin, +// GoodsItems = rawData.GoodsItems, +// PropListContext = +// BuildDisplayListAreaContext(DisplayListAreaType.Prop, rawData.PropItems, rawData.PropMaxCount), +// WeaponListContext = BuildDisplayListAreaContext(DisplayListAreaType.Weapon, rawData.WeaponItems, +// rawData.WeaponMaxCount) +// }; +// } +// +// private static DisplayListAreaContext BuildDisplayListAreaContext(DisplayListAreaType listType, +// IReadOnlyList items, +// int maxCount) +// { +// string title = GetDisplayListTitle(listType); +// if (items == null) +// { +// return new DisplayListAreaContext +// { +// Title = title, +// CurrentCount = 0, +// MaxCount = maxCount, +// ItemContexts = System.Array.Empty() +// }; +// } +// +// DisplayItemContext[] itemContexts = new DisplayItemContext[items.Count]; +// switch (listType) +// { +// case DisplayListAreaType.Weapon: +// if (items is IReadOnlyList weapons) +// { +// for (int i = 0; i < weapons.Count; i++) +// { +// WeaponBase weapon = weapons[i]; +// if (weapon == null) break; +// itemContexts[i] = BuildWeaponItem(weapon); +// } +// } +// +// break; +// +// case DisplayListAreaType.Prop: +// if (items is IReadOnlyList propItems) +// { +// for (int i = 0; i < propItems.Count; i++) +// { +// PropItem propItem = propItems[i]; +// if (propItem == null) break; +// itemContexts[i] = BuildPropItem(propItem); +// } +// } +// +// break; +// } +// +// int currentCount = itemContexts.Length; +// return new DisplayListAreaContext +// { +// Title = title, +// CurrentCount = currentCount, +// MaxCount = maxCount, +// ItemContexts = itemContexts +// }; +// } +// +// private static string GetDisplayListTitle(DisplayListAreaType listType) +// { +// return listType switch +// { +// DisplayListAreaType.Weapon => "武器", +// DisplayListAreaType.Prop => "道具", +// _ => string.Empty +// }; +// } +// +// private static DisplayItemContext BuildPropItem(PropItem propItem) +// { +// string iconAssetName = null; +// ItemRarity rarity = ItemRarity.None; +// +// if (propItem != null) +// { +// iconAssetName = propItem.IconAssetName; +// rarity = propItem.Rarity; +// } +// +// return new DisplayItemContext +// { +// IconAssetName = iconAssetName, +// Rarity = rarity, +// IsWeapon = false +// }; +// } +// +// private static DisplayItemContext BuildWeaponItem(WeaponBase weaponBase) +// { +// string iconAssetName = null; +// ItemRarity rarity = ItemRarity.None; +// +// if (weaponBase != null && weaponBase.WeaponData != null) +// { +// iconAssetName = weaponBase.WeaponData.IconAssetName; +// rarity = weaponBase.WeaponData.Rarity; +// } +// +// return new DisplayItemContext +// { +// IconAssetName = iconAssetName, +// Rarity = rarity, +// IsWeapon = true +// }; +// } +// +// private static void AppendDisplayItemContext(DisplayListAreaContext listContext, DisplayItemContext newItem) +// { +// if (listContext == null || newItem == null) +// { +// return; +// } +// +// int oldCount = listContext.ItemContexts != null ? listContext.ItemContexts.Length : 0; +// DisplayItemContext[] newContexts = new DisplayItemContext[oldCount + 1]; +// if (oldCount > 0) +// { +// System.Array.Copy(listContext.ItemContexts, newContexts, oldCount); +// } +// +// newContexts[oldCount] = newItem; +// listContext.ItemContexts = newContexts; +// listContext.CurrentCount = oldCount + 1; +// } +// +// #endregion +// +// #region UI Methods +// +// public int? OpenUI(ShopFormRawData rawData) +// { +// ShopFormContext context = BuildContext(rawData); +// return OpenUIInternal(context); +// } +// +// public override int? OpenUI(object userData = null) +// { +// if (userData is ShopFormContext context) +// { +// return OpenUIInternal(context); +// } +// +// if (userData is ShopFormRawData rawDataFromUserData) +// { +// return OpenUI(rawDataFromUserData); +// } +// +// if (userData != null) +// { +// Log.Warning("ShopFormController.OpenUI() userData type is invalid."); +// return null; +// } +// +// if (_useCase == null) +// { +// Log.Error("ShopForm.OpenUI():: useCase is null."); +// return null; +// } +// +// ShopFormRawData rawData = _useCase.CreateInitialModel(); +// return OpenUI(rawData); +// } +// +// public override void BindUseCase(IUIUseCase useCase) +// { +// if (!(useCase is ShopFormUseCase shopFormUseCase)) +// { +// Log.Error("LevelUpForm.BindUseCase() useCase is invalid."); +// return; +// } +// +// _useCase = shopFormUseCase; +// } +// +// #endregion +// +// #region Service +// +// private void RefreshGoodsItems(ShopRefreshResult result) +// { +// if (Context == null || result == null) +// { +// return; +// } +// +// Context.GoodsItems = result.GoodsItems; +// Context.RefreshPrice = result.RefreshPrice; +// +// if (Form == null) +// { +// return; +// } +// +// Form.RefreshGoodsItems(result.GoodsItems); +// Form.RefreshRefreshPrice(result.RefreshPrice); +// } +// +// private void ApplyGoodsPurchased(ShopPurchaseResult result) +// { +// if (Context == null || result == null) +// { +// return; +// } +// +// if (Context.GoodsItems != null && result.GoodsIndex >= 0 && result.GoodsIndex < Context.GoodsItems.Count) +// { +// Context.GoodsItems[result.GoodsIndex] = null; +// } +// +// if (result.DisplayItem != null) +// { +// if (result.DisplayItem.IsWeapon) +// { +// AppendDisplayItemContext(Context.WeaponListContext, result.DisplayItem); +// } +// else +// { +// AppendDisplayItemContext(Context.PropListContext, result.DisplayItem); +// } +// } +// +// Form?.ApplyGoodsPurchased(result.GoodsIndex, result.DisplayItem); +// } +// +// #endregion +// +// #region Event Handlers +// +// private void Refresh(object sender, GameEventArgs e) +// { +// if (!(sender is ShopForm)) +// { +// return; +// } +// +// if (!(e is RefreshEventArgs args)) +// { +// return; +// } +// +// ShopRefreshResult result = _useCase.TryRefresh(args.Cost); +// if (result == null) +// { +// return; +// } +// +// RefreshGoodsItems(result); +// } +// +// private void ShopPurchase(object sender, GameEventArgs e) +// { +// if (!(sender is ShopForm)) +// { +// return; +// } +// +// if (!(e is ShopPurchaseEventArgs args)) +// { +// return; +// } +// +// ShopPurchaseResult result = _useCase.TryPurchase(args.GoodsIndex); +// if (result == null) +// { +// return; +// } +// +// ApplyGoodsPurchased(result); +// } +// +// private void ShopContinue(object sender, GameEventArgs e) +// { +// if (!(sender is ShopForm)) +// { +// return; +// } +// +// if (!(e is ShopContinueEventArgs)) +// { +// return; +// } +// +// _useCase?.Continue(); +// } +// +// private void DisplayItemShow(object sender, GameEventArgs e) +// { +// if (!(e is DisplayItemShowEventArgs args) || _rawData == null) return; +// +// DisplayItemInfoFormRawData rawData = new(); +// rawData.TargetPos = args.TargetPos; +// rawData.Index = args.Index; +// if (args.IsWeapon) +// { +// var weaponData = _rawData.WeaponItems[args.Index].WeaponData; +// rawData.IconAssetName = weaponData.IconAssetName; +// rawData.Title = weaponData.Title; +// rawData.Rarity = weaponData.Rarity; +// rawData.TypeText = "武器"; +// rawData.Description = ItemDescUtility.CreateWeaponDescription(weaponData); +// rawData.Price = Mathf.FloorToInt(weaponData.Price * Context.WeaponRecycleRate); +// rawData.IsWeapon = true; +// } +// else +// { +// var propItem = _rawData.PropItems[args.Index]; +// rawData.IconAssetName = propItem.IconAssetName; +// rawData.Title = propItem.Title; +// rawData.Rarity = propItem.Rarity; +// rawData.TypeText = "道具"; +// rawData.Description = ItemDescUtility.CreatePropDescription(propItem); +// rawData.Price = 0; +// rawData.IsWeapon = false; +// } +// +// GameEntry.UIRouter.OpenUI(UIFormType.DisplayItemInfoForm, rawData); +// } +// +// private void WeaponRecycle(object sender, GameEventArgs e) +// { +// if (!(e is ShopWeaponRecycleEventArgs args)) return; +// +// if (_useCase == null || Context == null) +// { +// return; +// } +// +// bool success = _useCase.TryRecycleWeapon(args.Index, args.Price); +// if (!success) +// { +// return; +// } +// +// if (Context.WeaponListContext != null) +// { +// int currentCount = Mathf.Max(0, Context.WeaponListContext.CurrentCount - 1); +// Context.WeaponListContext.CurrentCount = currentCount; +// } +// +// Form?.RemoveWeaponDisplayItem(args.Index); +// GameEntry.Event.Fire(this, DisplayItemInfoHideEventArgs.Create(true)); +// } +// +// #endregion +// } +// } diff --git a/src-ref/UI/Templates/GameScene/RawData/DisplayItemInfoFormRawData.cs b/src-ref/UI/Templates/GameScene/RawData/DisplayItemInfoFormRawData.cs new file mode 100644 index 0000000..b8de396 --- /dev/null +++ b/src-ref/UI/Templates/GameScene/RawData/DisplayItemInfoFormRawData.cs @@ -0,0 +1,20 @@ +// using System.Collections; +// using System.Collections.Generic; +// using Definition.Enum; +// using UnityEngine; +// +// namespace UI +// { +// public class DisplayItemInfoFormRawData +// { +// public int Index; +// public string IconAssetName; +// public string Title; +// public string TypeText; +// public ItemRarity Rarity; +// public string Description; +// public int Price; +// public bool IsWeapon; +// public Vector3 TargetPos; +// } +// } \ No newline at end of file diff --git a/src-ref/UI/Templates/GameScene/RawData/LevelUpFormRawData.cs b/src-ref/UI/Templates/GameScene/RawData/LevelUpFormRawData.cs new file mode 100644 index 0000000..4b53f65 --- /dev/null +++ b/src-ref/UI/Templates/GameScene/RawData/LevelUpFormRawData.cs @@ -0,0 +1,11 @@ +// using System.Collections.Generic; +// using DataTable; +// +// namespace UI +// { +// public class LevelUpFormRawData +// { +// public List Rewards; +// public int RefreshPrice; +// } +// } \ No newline at end of file diff --git a/src-ref/UI/Templates/GameScene/RawData/ShopFormRawData.cs b/src-ref/UI/Templates/GameScene/RawData/ShopFormRawData.cs new file mode 100644 index 0000000..a9c8e67 --- /dev/null +++ b/src-ref/UI/Templates/GameScene/RawData/ShopFormRawData.cs @@ -0,0 +1,18 @@ +// using System.Collections.Generic; +// using Definition.DataStruct; +// using Entity.Weapon; +// +// namespace UI +// { +// public class ShopFormRawData +// { +// public int CurrentLevel; +// public int RefreshPrice; +// public int PlayerCoin; +// public List GoodsItems; +// public IReadOnlyList PropItems; +// public int PropMaxCount = -1; +// public IReadOnlyList WeaponItems; +// public int WeaponMaxCount = -1; +// } +// } diff --git a/src-ref/UI/Templates/GameScene/UseCase/DisplayItemInfoFormUseCase.cs b/src-ref/UI/Templates/GameScene/UseCase/DisplayItemInfoFormUseCase.cs new file mode 100644 index 0000000..7ab1c39 --- /dev/null +++ b/src-ref/UI/Templates/GameScene/UseCase/DisplayItemInfoFormUseCase.cs @@ -0,0 +1,75 @@ +// using Definition.DataStruct; +// using Entity; +// using CustomUtility; +// using Entity.Weapon; +// using Procedure; +// +// namespace UI +// { +// public class DisplayItemInfoFormUseCase : IUIUseCase +// { +// private readonly ProcedureGame _procedureGame; +// +// private Player Player => _procedureGame != null ? _procedureGame.Player : null; +// +// public DisplayItemInfoFormUseCase(ProcedureGame procedureGame) +// { +// _procedureGame = procedureGame; +// } +// +// public DisplayItemInfoFormRawData CreateModel(int index, bool isWeapon) +// { +// if (index < 0 || Player == null) +// { +// return null; +// } +// +// if (isWeapon) +// { +// var weapons = Player.Weapons; +// if (weapons == null || index >= weapons.Count) +// { +// return null; +// } +// +// WeaponBase weapon = weapons[index]; +// if (weapon == null || weapon.WeaponData == null) +// { +// return null; +// } +// +// return new DisplayItemInfoFormRawData +// { +// IconAssetName = weapon.WeaponData.IconAssetName, +// Title = weapon.WeaponData.Title, +// TypeText = "Weapon", +// Description = ItemDescUtility.CreateWeaponDescription(weapon), +// Price = 0, +// IsWeapon = true +// }; +// } +// +// var props = Player.Props; +// if (props == null || index >= props.Count) +// { +// return null; +// } +// +// PropItem prop = props[index]; +// if (prop == null) +// { +// return null; +// } +// +// return new DisplayItemInfoFormRawData +// { +// IconAssetName = prop.IconAssetName, +// Title = prop.Title, +// TypeText = "Prop", +// Description = ItemDescUtility.CreatePropDescription(prop), +// Price = 0, +// IsWeapon = false +// }; +// } +// } +// } \ No newline at end of file diff --git a/src-ref/UI/Templates/GameScene/UseCase/LevelUpFormUseCase.cs b/src-ref/UI/Templates/GameScene/UseCase/LevelUpFormUseCase.cs new file mode 100644 index 0000000..6accafa --- /dev/null +++ b/src-ref/UI/Templates/GameScene/UseCase/LevelUpFormUseCase.cs @@ -0,0 +1,195 @@ +// using System.Collections.Generic; +// using System.Linq; +// using DataTable; +// using Definition.DataStruct; +// using Definition.Enum; +// using Entity; +// using Procedure; +// using UnityEngine; +// using UnityGameFramework.Runtime; +// using CustomUtility; +// using GameFramework.DataTable; +// +// namespace UI +// { +// internal class LevelUpFormUseCase : IUIUseCase +// { +// private const int BaseRefreshPrice = 2; +// private readonly GameStateLevelUp _gameStateLevelUp; +// private readonly DRLevelUpReward[] _allRewards; +// private readonly IDataTable _levelRarityTable; +// private readonly Dictionary> _rewardsByRarity = new(); +// private readonly List _validRewards = new(); +// +// private int _refreshCount; +// private LevelUpFormRawData _currentModel; +// +// private readonly Player _player; +// +// public LevelUpFormUseCase(Player player, GameStateLevelUp gameStateLevelUp) +// { +// _player = player; +// _gameStateLevelUp = gameStateLevelUp; +// _allRewards = GameEntry.DataTable.GetDataTable().ToArray(); +// _levelRarityTable = GameEntry.DataTable.GetDataTable(); +// +// BuildRewardRarityPools(); +// } +// +// public LevelUpFormRawData CreateInitialModel(int count = 4) +// { +// _refreshCount = 0; +// +// if (_gameStateLevelUp == null || _player.PendingLevelPoints <= 0) +// { +// _currentModel = null; +// return null; +// } +// +// _currentModel = BuildModel(BaseRefreshPrice, count); +// return _currentModel; +// } +// +// public LevelUpFormRawData TryRefresh(int refreshCost, int count = 4) +// { +// if (_currentModel == null) +// { +// return null; +// } +// +// if (_player == null || _player.Coin < refreshCost) +// { +// return null; +// } +// +// _player.Coin -= refreshCost; +// +// _refreshCount++; +// _currentModel = BuildModel((_refreshCount + 1) * BaseRefreshPrice, count); +// return _currentModel; +// } +// +// public LevelUpFormRawData SelectReward(int selectedIndex, int count = 4) +// { +// if (_currentModel?.Rewards == null) +// { +// return null; +// } +// +// if (selectedIndex < 0 || selectedIndex >= _currentModel.Rewards.Count) +// { +// Log.Warning("LevelUpFormUseCase::SelectReward: Invalid index"); +// return null; +// } +// +// ApplyReward(_currentModel.Rewards[selectedIndex]); +// +// if (--_player.PendingLevelPoints > 0) +// { +// _refreshCount = 0; +// _currentModel = BuildModel(BaseRefreshPrice, count); +// return _currentModel; +// } +// +// _gameStateLevelUp.IsCompleted = true; +// _currentModel = null; +// return null; +// } +// +// private LevelUpFormRawData BuildModel(int refreshPrice, int count) +// { +// if (_allRewards == null || _allRewards.Length == 0) +// { +// Log.Error("LevelUpFormUseCase::BuildModel(): _allRewards is empty"); +// return null; +// } +// +// int finalCount = count > 0 ? System.Math.Min(count, _allRewards.Length) : _allRewards.Length; +// List selections = new List(finalCount); +// +// int currentLevel = _player != null ? _player.CurrentLevel : 1; +// for (int i = 0; i < finalCount; i++) +// { +// ItemRarity rarity = RarityUtility.SelectRarityForLevel(_levelRarityTable, currentLevel); +// DRLevelUpReward reward = PickRewardByRarity(rarity); +// if (reward == null) +// { +// Log.Warning("LevelUpFormUseCase::BuildModel(): No available reward for selection."); +// break; +// } +// +// selections.Add(reward); +// } +// +// return new LevelUpFormRawData +// { +// Rewards = selections, +// RefreshPrice = refreshPrice +// }; +// } +// +// private void ApplyReward(DRLevelUpReward reward) +// { +// if (reward == null) +// { +// return; +// } +// +// if (_player == null) +// { +// Log.Warning("LevelUpFormUseCase::ApplyReward: Player is null"); +// return; +// } +// +// if (reward.Modifiers == null || reward.Modifiers.Length == 0) +// { +// Log.Warning("LevelUpFormUseCase::ApplyReward: Reward modifiers are empty"); +// return; +// } +// +// _player.AddProp(new PropItem(reward.Modifiers, reward.Rarity, reward.Title, reward.IconAssetName)); +// } +// +// private void BuildRewardRarityPools() +// { +// _rewardsByRarity.Clear(); +// _validRewards.Clear(); +// +// if (_allRewards == null) return; +// +// foreach (DRLevelUpReward reward in _allRewards) +// { +// if (reward == null) +// { +// continue; +// } +// +// ItemRarity rarity = reward.Rarity; +// _validRewards.Add(reward); +// +// if (!_rewardsByRarity.TryGetValue(rarity, out List list)) +// { +// list = new List(); +// _rewardsByRarity.Add(rarity, list); +// } +// +// list.Add(reward); +// } +// } +// +// private DRLevelUpReward PickRewardByRarity(ItemRarity rarity) +// { +// if (_rewardsByRarity.TryGetValue(rarity, out List list) && list.Count > 0) +// { +// return list[Random.Range(0, list.Count)]; +// } +// +// if (_validRewards.Count > 0) +// { +// return _validRewards[Random.Range(0, _validRewards.Count)]; +// } +// +// return null; +// } +// } +// } diff --git a/src-ref/UI/Templates/GameScene/UseCase/ShopFormUseCase.cs b/src-ref/UI/Templates/GameScene/UseCase/ShopFormUseCase.cs new file mode 100644 index 0000000..bcfe5f5 --- /dev/null +++ b/src-ref/UI/Templates/GameScene/UseCase/ShopFormUseCase.cs @@ -0,0 +1,444 @@ +// using System.Collections.Generic; +// using System.Linq; +// using DataTable; +// using Definition.DataStruct; +// using Definition.Enum; +// using Entity; +// using Entity.EntityData; +// using CustomUtility; +// using GameFramework.DataTable; +// using Procedure; +// using UnityEngine; +// using UnityGameFramework.Runtime; +// using Random = UnityEngine.Random; +// +// namespace UI +// { +// internal sealed class ShopRefreshResult +// { +// public List GoodsItems; +// public int RefreshPrice; +// } +// +// internal sealed class ShopPurchaseResult +// { +// public int GoodsIndex; +// public DisplayItemContext DisplayItem; +// } +// +// internal class ShopFormUseCase : IUIUseCase +// { +// private readonly ProcedureGame _procedureGame; +// private readonly GameStateShop _gameStateShop; +// +// private readonly DRGoods[] _allGoods; +// private readonly IDataTable _propDataTable; +// private readonly IDataTable _weaponDataTable; +// private readonly IDataTable _levelRarityTable; +// +// private readonly List _validGoods = new(); +// private readonly Dictionary> _goodsByRarity = new(); +// +// private readonly List _selections = new(); +// private int _refreshTime; +// +// private Player Player => _procedureGame.Player; +// +// private sealed class ShopGoodsSelection +// { +// public GoodsType GoodsType; +// public int GoodsTypeId; +// public int Price; +// } +// +// public ShopFormUseCase(ProcedureGame procedureGame, GameStateShop gameStateShop) +// { +// _procedureGame = procedureGame; +// _gameStateShop = gameStateShop; +// _allGoods = GameEntry.DataTable.GetDataTable().ToArray(); +// _propDataTable = GameEntry.DataTable.GetDataTable(); +// _weaponDataTable = GameEntry.DataTable.GetDataTable(); +// _levelRarityTable = GameEntry.DataTable.GetDataTable(); +// +// BuildGoodsRarityPools(); +// } +// +// public ShopFormRawData CreateInitialModel() +// { +// _refreshTime = 0; +// +// Player player = Player; +// if (_procedureGame == null || player == null) +// { +// return null; +// } +// +// int currentLevel = _procedureGame.CurrentLevel; +// return new ShopFormRawData +// { +// CurrentLevel = currentLevel, +// RefreshPrice = currentLevel, +// PlayerCoin = player.Coin, +// GoodsItems = BuildRandomGoodsItems(4), +// PropItems = player.Props, +// PropMaxCount = -1, +// WeaponItems = player.Weapons, +// WeaponMaxCount = player.WeaponCapacity +// }; +// } +// +// public ShopRefreshResult TryRefresh(int cost) +// { +// Player player = Player; +// if (player == null || _procedureGame == null || player.Coin < cost) +// { +// return null; +// } +// +// player.Coin -= cost; +// +// int refreshPrice = (_refreshTime + 1) * _procedureGame.CurrentLevel; +// _refreshTime++; +// +// return new ShopRefreshResult +// { +// GoodsItems = BuildRandomGoodsItems(4), +// RefreshPrice = refreshPrice +// }; +// } +// +// public ShopPurchaseResult TryPurchase(int goodsIndex) +// { +// if (goodsIndex < 0 || goodsIndex >= _selections.Count) +// { +// Log.Warning("ShopFormUseCase::TryPurchase: Invalid index"); +// return null; +// } +// +// ShopGoodsSelection selection = _selections[goodsIndex]; +// if (selection == null) +// { +// Log.Warning("ShopFormUseCase::TryPurchase: Item already purchased."); +// return null; +// } +// +// Player player = Player; +// if (player == null || player.Coin < selection.Price) +// { +// return null; +// } +// +// DisplayItemContext displayItem = ApplyGoodsPurchase(selection); +// if (displayItem == null) +// { +// return null; +// } +// +// player.Coin -= selection.Price; +// _selections[goodsIndex] = null; +// +// return new ShopPurchaseResult +// { +// GoodsIndex = goodsIndex, +// DisplayItem = displayItem +// }; +// } +// +// public void Continue() +// { +// _gameStateShop.ShopFinish = true; +// } +// +// private List BuildRandomGoodsItems(int count) +// { +// if (_allGoods == null || _allGoods.Length == 0) +// { +// Log.Error("ShopFormUseCase::BuildRandomGoodsItems(): _allGoods is empty"); +// return null; +// } +// +// int finalCount = Mathf.Min(count, _allGoods.Length); +// List goodsItems = new List(finalCount); +// _selections.Clear(); +// +// int currentLevel = _procedureGame != null ? _procedureGame.CurrentLevel : 1; +// +// for (int i = 0; i < finalCount; i++) +// { +// ItemRarity rarity = RarityUtility.SelectRarityForLevel(_levelRarityTable, currentLevel); +// DRGoods drGoods = PickGoodsByRarity(rarity); +// if (drGoods == null) +// { +// Log.Warning("ShopFormUseCase::BuildRandomGoodsItems: No available goods for selection."); +// break; +// } +// +// GoodsItemContext goodsItem = new GoodsItemContext(); +// int price; +// +// if (drGoods.GoodsType == GoodsType.Prop) +// { +// DRProp drProp = _propDataTable.GetDataRow(drGoods.GoodsTypeId); +// if (drProp == null) +// { +// Log.Warning($"ShopFormUseCase::BuildRandomGoodsItems: Missing DRProp, id = {drGoods.GoodsTypeId}"); +// continue; +// } +// +// price = CalculateRandomizedPrice(drProp.Price, drProp.PriceRandomPercent); +// goodsItem.Title = drProp.Title; +// goodsItem.Type = "Prop"; +// goodsItem.Rarity = drProp.Rarity; +// GameEntry.SpriteCache.GetSprite(drProp.IconAssetName, sprite => goodsItem.Icon = sprite); +// goodsItem.Description = ItemDescUtility.CreatePropDescription(drProp.Modifiers); +// } +// else if (drGoods.GoodsType == GoodsType.Weapon) +// { +// DRWeapon drWeapon = _weaponDataTable.GetDataRow(drGoods.GoodsTypeId); +// if (drWeapon == null) +// { +// Log.Warning($"ShopFormUseCase::BuildRandomGoodsItems: Missing DRWeapon, id = {drGoods.GoodsTypeId}"); +// continue; +// } +// +// price = CalculateRandomizedPrice(drWeapon.Price, drWeapon.PriceRandomPercent); +// goodsItem.Title = drWeapon.Title; +// goodsItem.Type = "Weapon"; +// goodsItem.Rarity = drWeapon.Rarity; +// GameEntry.SpriteCache.GetSprite(drWeapon.IconAssetName, sprite => goodsItem.Icon = sprite); +// goodsItem.Description = ItemDescUtility.CreateWeaponDescription(drWeapon); +// } +// else +// { +// Log.Warning($"ShopFormUseCase::BuildRandomGoodsItems: Unsupported goods type = {drGoods.GoodsType}"); +// continue; +// } +// +// goodsItem.Price = price; +// +// _selections.Add(new ShopGoodsSelection +// { +// GoodsType = drGoods.GoodsType, +// GoodsTypeId = drGoods.GoodsTypeId, +// Price = price +// }); +// +// goodsItems.Add(goodsItem); +// } +// +// return goodsItems; +// } +// +// private void BuildGoodsRarityPools() +// { +// _goodsByRarity.Clear(); +// _validGoods.Clear(); +// +// if (_allGoods == null) return; +// +// foreach (DRGoods goods in _allGoods) +// { +// if (goods == null) continue; +// +// if (!TryGetGoodsRarity(goods, out ItemRarity rarity)) +// { +// continue; +// } +// +// _validGoods.Add(goods); +// +// if (!_goodsByRarity.TryGetValue(rarity, out List list)) +// { +// list = new List(); +// _goodsByRarity.Add(rarity, list); +// } +// +// list.Add(goods); +// } +// } +// +// private DRGoods PickGoodsByRarity(ItemRarity rarity) +// { +// if (_goodsByRarity.TryGetValue(rarity, out List list) && list.Count > 0) +// { +// return list[Random.Range(0, list.Count)]; +// } +// +// if (_validGoods.Count > 0) +// { +// return _validGoods[Random.Range(0, _validGoods.Count)]; +// } +// +// return null; +// } +// +// private bool TryGetGoodsRarity(DRGoods goods, out ItemRarity rarity) +// { +// rarity = ItemRarity.None; +// if (goods == null) +// { +// return false; +// } +// +// if (goods.GoodsType == GoodsType.Prop) +// { +// DRProp drProp = _propDataTable != null ? _propDataTable.GetDataRow(goods.GoodsTypeId) : null; +// if (drProp == null) +// { +// Log.Warning($"ShopFormUseCase::TryGetGoodsRarity: Missing DRProp, id = {goods.GoodsTypeId}"); +// return false; +// } +// +// rarity = drProp.Rarity; +// return true; +// } +// +// if (goods.GoodsType == GoodsType.Weapon) +// { +// DRWeapon drWeapon = _weaponDataTable != null ? _weaponDataTable.GetDataRow(goods.GoodsTypeId) : null; +// if (drWeapon == null) +// { +// Log.Warning($"ShopFormUseCase::TryGetGoodsRarity: Missing DRWeapon, id = {goods.GoodsTypeId}"); +// return false; +// } +// +// rarity = drWeapon.Rarity; +// return true; +// } +// +// Log.Warning($"ShopFormUseCase::TryGetGoodsRarity: Unsupported goods type = {goods.GoodsType}"); +// return false; +// } +// +// private static int CalculateRandomizedPrice(int basePrice, float priceRandomPercent) +// { +// float normalizedPercent = priceRandomPercent > 1f ? priceRandomPercent / 100f : priceRandomPercent; +// normalizedPercent = Mathf.Max(0f, normalizedPercent); +// float offsetPercent = Random.Range(-normalizedPercent, normalizedPercent); +// int finalPrice = Mathf.RoundToInt(basePrice * (1f + offsetPercent)); +// return Mathf.Max(1, finalPrice); +// } +// +// private DisplayItemContext ApplyGoodsPurchase(ShopGoodsSelection goods) +// { +// if (goods == null || Player == null) +// { +// return null; +// } +// +// if (goods.GoodsType == GoodsType.Prop) +// { +// DRProp drProp = _propDataTable != null ? _propDataTable.GetDataRow(goods.GoodsTypeId) : null; +// if (drProp == null || drProp.Modifiers == null || drProp.Modifiers.Length == 0) +// { +// Log.Warning("ShopFormUseCase::ApplyGoodsPurchase: Prop modifiers are empty"); +// return null; +// } +// +// Player.AddProp(new PropItem(drProp.Modifiers, drProp.Rarity, drProp.Title, drProp.IconAssetName)); +// return new DisplayItemContext +// { +// IconAssetName = drProp.IconAssetName, +// Rarity = drProp.Rarity, +// IsWeapon = false +// }; +// } +// +// if (goods.GoodsType == GoodsType.Weapon) +// { +// if (Player.Weapons != null && Player.Weapons.Count >= Player.WeaponCapacity) +// { +// Log.Warning("ShopFormUseCase::ApplyGoodsPurchase: Weapon capacity is full."); +// return null; +// } +// +// DRWeapon drWeapon = _weaponDataTable != null ? _weaponDataTable.GetDataRow(goods.GoodsTypeId) : null; +// if (drWeapon == null) +// { +// Log.Warning($"ShopFormUseCase::ApplyGoodsPurchase: Missing DRWeapon, id = {goods.GoodsTypeId}"); +// return null; +// } +// +// var weaponData = CreateWeaponData(goods.GoodsTypeId); +// if (weaponData == null) +// { +// Log.Warning( +// $"ShopFormUseCase::ApplyGoodsPurchase: Unsupported weapon type id = {goods.GoodsTypeId}"); +// return null; +// } +// +// GameEntry.Entity.ShowWeapon(weaponData); +// +// return new DisplayItemContext +// { +// IconAssetName = drWeapon.IconAssetName, +// Rarity = drWeapon.Rarity, +// IsWeapon = true +// }; +// } +// +// return null; +// } +// +// private WeaponData CreateWeaponData(int weaponTypeId) +// { +// int entityId = GameEntry.Entity.GenerateSerialId(); +// int ownerId = Player.Id; +// CampType ownerCamp = CampType.Player; +// +// WeaponType weaponType = (WeaponType)weaponTypeId; +// switch (weaponType) +// { +// case WeaponType.WeaponKnife: +// return new WeaponKnifeData(entityId, ownerId, ownerCamp); +// case WeaponType.WeaponHandgun: +// return new WeaponHandgunData(entityId, ownerId, ownerCamp); +// case WeaponType.WeaponSlash: +// return new WeaponSlashData(entityId, ownerId, ownerCamp); +// case WeaponType.WeaponLightning: +// return new WeaponLightningData(entityId, ownerId, ownerCamp); +// default: +// return null; +// } +// } +// +// public bool TryRecycleWeapon(int argsIndex, int argsPrice) +// { +// Player player = Player; +// if (player == null) +// { +// return false; +// } +// +// var weapons = player.Weapons; +// if (weapons == null || weapons.Count <= 1) +// { +// Log.Warning("ShopFormUseCase::TryRecycleWeapon: Can not recycle the last weapon."); +// return false; +// } +// +// if (argsIndex < 0 || argsIndex >= weapons.Count) +// { +// Log.Warning($"ShopFormUseCase::TryRecycleWeapon: Invalid weapon index = {argsIndex}"); +// return false; +// } +// +// var weapon = weapons[argsIndex]; +// if (weapon == null) +// { +// Log.Warning("ShopFormUseCase::TryRecycleWeapon: Weapon is null."); +// return false; +// } +// +// int recyclePrice = Mathf.Max(0, argsPrice); +// if (!player.RemoveWeapon(weapon)) +// { +// Log.Warning("ShopFormUseCase::TryRecycleWeapon: Remove weapon failed."); +// return false; +// } +// +// player.Coin += recyclePrice; +// return true; +// } +// } +// } diff --git a/src-ref/UI/Templates/GameScene/View/DisplayItem.cs b/src-ref/UI/Templates/GameScene/View/DisplayItem.cs new file mode 100644 index 0000000..27f613c --- /dev/null +++ b/src-ref/UI/Templates/GameScene/View/DisplayItem.cs @@ -0,0 +1,71 @@ +// using CustomEvent; +// using UnityEngine; +// using UnityEngine.UI; +// +// namespace UI +// { +// public class DisplayItem : MonoBehaviour +// { +// [SerializeField] private IconArea _iconArea; +// +// [SerializeField] private RectTransform _itemRect; +// +// private DisplayItemContext _context; +// +// private int _index = -1; +// +// public void SetIndex(int index) +// { +// _index = index; +// } +// +// public void OnInit(DisplayItemContext context, int index) +// { +// if (context == null) return; +// +// _context = context; +// _index = index; +// +// if (_iconArea != null) +// { +// string iconName = _context.IconAssetName; +// GameEntry.SpriteCache.GetSprite(iconName, sprite => { _iconArea.OnInit(sprite, context.Rarity); }); +// } +// } +// +// public void OnReset() +// { +// _context = null; +// _index = -1; +// +// if (_iconArea != null) +// { +// _iconArea.OnReset(); +// } +// } +// +// public void OnItemInfoShow() +// { +// if (_index < 0) return; +// if (_itemRect == null) return; +// +// Rect rect = _itemRect.rect; +// Vector3 targetPos = _itemRect.TransformPoint(new Vector3( +// (rect.xMin + rect.xMax) / 2, +// rect.yMax, +// 0f) +// ); +// GameEntry.Event.Fire(this, DisplayItemShowEventArgs.Create(_index, _context.IsWeapon, targetPos)); +// } +// +// public void OnItemInfoLock() +// { +// GameEntry.Event.Fire(this, DisplayItemInfoLockEventArgs.Create()); +// } +// +// public void OnItemInfoHide() +// { +// GameEntry.Event.Fire(this, DisplayItemInfoHideEventArgs.Create()); +// } +// } +// } \ No newline at end of file diff --git a/src-ref/UI/Templates/GameScene/View/DisplayItemInfoForm.cs b/src-ref/UI/Templates/GameScene/View/DisplayItemInfoForm.cs new file mode 100644 index 0000000..29796f5 --- /dev/null +++ b/src-ref/UI/Templates/GameScene/View/DisplayItemInfoForm.cs @@ -0,0 +1,205 @@ +// using CustomEvent; +// using TMPro; +// using UnityEngine; +// using UnityGameFramework.Runtime; +// +// namespace UI +// { +// public class DisplayItemInfoForm : UGuiForm +// { +// [SerializeField] private RectTransform _content; +// +// [SerializeField] private IconArea _iconArea; +// +// [SerializeField] private TMP_Text _itemTitle; +// +// [SerializeField] private TMP_Text _itemTypeText; +// +// [SerializeField] private TMP_Text _itemDescription; +// +// [SerializeField] private CommonButton _recycleButton; +// +// [SerializeField] private TMP_Text _itemPrice; +// +// [SerializeField] private bool _autoResizeHeight = true; +// +// [SerializeField] private float _fixedTopHeight = 150f; +// +// [SerializeField] private float _fixedBottomHeight = 150f; +// +// [SerializeField] private float _fixedWidth = 300f; +// +// [SerializeField] private float _screenEdgePadding = 0f; +// +// private DisplayItemInfoFormContext _context; +// private Vector3 _targetPos; +// +// public void RefreshUI(DisplayItemInfoFormContext context) +// { +// if (context == null) +// { +// Log.Warning("DisplayItemInfoForm context is invalid."); +// return; +// } +// +// _context = context; +// +// _targetPos = ConvertWorldToAnchored(_context.TargetPos); +// Log.Info($"{_context.TargetPos}->{_targetPos}"); +// +// if (_content != null) +// { +// _content.anchoredPosition3D = _targetPos; +// } +// +// if (_itemTitle != null) _itemTitle.text = _context.Title ?? string.Empty; +// if (_itemTypeText != null) _itemTypeText.text = _context.TypeText ?? string.Empty; +// if (_itemDescription != null) _itemDescription.text = _context.Description ?? string.Empty; +// if (_itemPrice != null) _itemPrice.text = $"{_context.Price} "; +// +// if (_recycleButton != null) +// { +// _recycleButton.gameObject.SetActive(_context.IsWeapon); +// } +// +// if (_iconArea != null) +// { +// if (!string.IsNullOrEmpty(_context.IconAssetName)) +// { +// string iconAssetName = _context.IconAssetName; +// GameEntry.SpriteCache.GetSprite(iconAssetName, sprite => +// { +// if (_context != null && _context.IconAssetName == iconAssetName) +// { +// _iconArea.OnInit(sprite, context.Rarity); +// ResizeToFitContent(); +// ApplyClampedTargetPos(); +// } +// }); +// } +// } +// +// ResizeToFitContent(); +// ApplyClampedTargetPos(); +// } +// +// protected override void OnOpen(object userData) +// { +// base.OnOpen(userData); +// +// if (!(userData is DisplayItemInfoFormContext context)) +// { +// Log.Error("DisplayItemInfoFormContext is invalid."); +// return; +// } +// +// RefreshUI(context); +// } +// +// protected override void OnClose(bool isShutdown, object userData) +// { +// _context = null; +// base.OnClose(isShutdown, userData); +// } +// +// private void ResizeToFitContent() +// { +// if (!_autoResizeHeight || _content == null) +// { +// return; +// } +// +// if (_itemDescription == null) +// { +// return; +// } +// +// _itemDescription.ForceMeshUpdate(); +// var descriptionSize = _itemDescription.rectTransform.sizeDelta; +// descriptionSize.y = _itemDescription.preferredHeight; +// _itemDescription.rectTransform.sizeDelta = descriptionSize; +// float descriptionHeight = Mathf.Max(0f, _itemDescription.preferredHeight); +// float targetHeight = _fixedTopHeight + descriptionHeight + _fixedBottomHeight; +// +// Vector2 size = new Vector2(_fixedWidth, targetHeight); +// _content.sizeDelta = size; +// } +// +// private void ApplyClampedTargetPos() +// { +// if (_content == null) +// { +// return; +// } +// +// RectTransform parent = _content.parent as RectTransform; +// if (parent == null) +// { +// return; +// } +// +// Vector2 size = _content.sizeDelta; +// if (size.x <= 0f || size.y <= 0f) +// { +// size = _content.rect.size; +// } +// +// Vector2 pivot = _content.pivot; +// float halfWidth = parent.rect.width * 0.5f; +// float halfHeight = parent.rect.height * 0.5f; +// +// float minX = -halfWidth + size.x * pivot.x + _screenEdgePadding; +// float maxX = halfWidth - size.x * (1f - pivot.x) - _screenEdgePadding; +// float minY = -halfHeight + size.y * pivot.y + _screenEdgePadding; +// float maxY = halfHeight - size.y * (1f - pivot.y) - _screenEdgePadding; +// +// float clampedX = minX <= maxX ? Mathf.Clamp(_targetPos.x, minX, maxX) : 0f; +// float clampedY = minY <= maxY ? Mathf.Clamp(_targetPos.y, minY, maxY) : 0f; +// +// _content.anchoredPosition3D = new Vector3(clampedX, clampedY, _targetPos.z); +// } +// +// private Vector3 ConvertWorldToAnchored(Vector3 worldPos) +// { +// if (_content == null) +// { +// return worldPos; +// } +// +// RectTransform parent = _content.parent as RectTransform; +// if (parent == null) +// { +// return worldPos; +// } +// +// Canvas canvas = _content.GetComponentInParent(); +// Canvas rootCanvas = canvas != null ? canvas.rootCanvas : null; +// Camera uiCamera = null; +// if (rootCanvas != null && rootCanvas.renderMode != RenderMode.ScreenSpaceOverlay) +// { +// uiCamera = rootCanvas.worldCamera != null ? rootCanvas.worldCamera : Camera.main; +// } +// +// Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(uiCamera, worldPos); +// if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(parent, screenPoint, uiCamera, +// out Vector2 localPoint)) +// { +// return worldPos; +// } +// +// float z = _content.anchoredPosition3D.z; +// return new Vector3(localPoint.x, localPoint.y, z); +// } +// +// public void OnRecycleButtonClick() +// { +// if (_context == null || !_context.IsWeapon) return; +// GameEntry.Event.Fire(this, ShopWeaponRecycleEventArgs.Create(_context.Index, _context.Price)); +// } +// +// public void OnCancelButtonClick() +// { +// GameEntry.Event.Fire(this, DisplayItemInfoHideEventArgs.Create()); +// } +// } +// } diff --git a/src-ref/UI/Templates/GameScene/View/DisplayItemObject.cs b/src-ref/UI/Templates/GameScene/View/DisplayItemObject.cs new file mode 100644 index 0000000..1348d62 --- /dev/null +++ b/src-ref/UI/Templates/GameScene/View/DisplayItemObject.cs @@ -0,0 +1,27 @@ +// using GameFramework; +// using GameFramework.ObjectPool; +// using UnityEngine; +// +// namespace UI +// { +// public class DisplayItemObject : ObjectBase +// { +// public static DisplayItemObject Create(object target) +// { +// DisplayItemObject displayItemObject = ReferencePool.Acquire(); +// displayItemObject.Initialize(target); +// return displayItemObject; +// } +// +// protected override void Release(bool isShutdown) +// { +// DisplayItem item = (DisplayItem)Target; +// if (item == null) +// { +// return; +// } +// +// Object.Destroy(item.gameObject); +// } +// } +// } diff --git a/src-ref/UI/Templates/GameScene/View/DisplayListArea.cs b/src-ref/UI/Templates/GameScene/View/DisplayListArea.cs new file mode 100644 index 0000000..51efa4c --- /dev/null +++ b/src-ref/UI/Templates/GameScene/View/DisplayListArea.cs @@ -0,0 +1,285 @@ +// using System; +// using System.Collections.Generic; +// using GameFramework.ObjectPool; +// using TMPro; +// using UnityEngine; +// using UnityEngine.UI; +// using UnityGameFramework.Runtime; +// +// namespace UI +// { +// public enum DisplayListAreaType : byte +// { +// None = 0, +// Prop = 1, +// Weapon = 2 +// } +// +// public class DisplayListArea : MonoBehaviour +// { +// [SerializeField] private TMP_Text _titleText; +// +// [SerializeField] private TMP_Text _countText; +// +// [SerializeField] private DisplayItem _displayItemTemplate; +// +// [SerializeField] private Transform _displayItemRoot; +// +// [SerializeField] private Vector2 _startPosition = Vector2.zero; +// +// [SerializeField] private float _itemSize = 150f; +// +// [SerializeField] private float _itemSpacing = 10f; +// +// [SerializeField] private int _fixedColumnCount = 0; +// +// [SerializeField] private int _instancePoolCapacity = 32; +// +// private DisplayListAreaContext _context; +// +// private IObjectPool _displayItemObjectPool; +// private const string DisplayItemPoolName = "DisplayItem"; +// +// private readonly List _activeItems = new List(); +// +// public void OnInit(DisplayListAreaContext context) +// { +// if (context == null) +// { +// Log.Warning("DisplayListArea context is invalid."); +// return; +// } +// +// EnsurePool(); +// _context = context; +// +// if (_titleText != null) _titleText.text = context.Title; +// UpdateCountText(context.CurrentCount, context.MaxCount); +// +// int itemCount = context.ItemContexts?.Length ?? 0; +// int targetCount = Mathf.Min(context.CurrentCount, itemCount); +// EnsureItemCount(targetCount); +// for (int i = 0; i < targetCount; i++) +// { +// _activeItems[i].OnInit(context.ItemContexts[i], i); +// } +// +// LayoutItems(); +// } +// +// public void OnReset() +// { +// _context = null; +// +// for (int i = _activeItems.Count - 1; i >= 0; i--) +// { +// HideItem(_activeItems[i]); +// } +// } +// +// public void OnDestroy() +// { +// _displayItemObjectPool?.ReleaseAllUnused(); +// } +// +// public DisplayItem AddItem(DisplayItemContext itemContext) +// { +// EnsurePool(); +// if (_displayItemObjectPool == null) +// { +// return null; +// } +// +// DisplayItem item = CreateItem(); +// if (item == null) +// { +// return null; +// } +// +// int index = _activeItems.Count; +// _activeItems.Add(item); +// item.OnInit(itemContext, index); +// item.SetIndex(index); +// +// int columnCount = _fixedColumnCount > 0 ? _fixedColumnCount : CalculateColumnCount(GetRootWidth()); +// LayoutItem(item, index, columnCount); +// +// int currentCount = _activeItems.Count; +// int maxCount = _context != null ? _context.MaxCount : -1; +// if (_context != null) +// { +// _context.CurrentCount = currentCount; +// } +// +// UpdateCountText(currentCount, maxCount); +// +// return item; +// } +// +// public bool RemoveItemAt(int index) +// { +// if (index < 0 || index >= _activeItems.Count) +// { +// return false; +// } +// +// HideItem(_activeItems[index]); +// LayoutItems(); +// +// int currentCount = _activeItems.Count; +// int maxCount = _context != null ? _context.MaxCount : -1; +// if (_context != null) +// { +// _context.CurrentCount = currentCount; +// } +// +// UpdateCountText(currentCount, maxCount); +// +// return true; +// } +// +// private void EnsurePool() +// { +// if (_displayItemObjectPool != null) return; +// +// if (_displayItemRoot == null) +// { +// _displayItemRoot = transform; +// } +// +// GridLayoutGroup gridLayout = _displayItemRoot.GetComponent(); +// if (gridLayout != null) +// { +// gridLayout.enabled = false; +// } +// +// if (GameEntry.ObjectPool.HasObjectPool(DisplayItemPoolName)) +// { +// _displayItemObjectPool = GameEntry.ObjectPool.GetObjectPool(DisplayItemPoolName); +// return; +// } +// +// if (_displayItemTemplate == null) +// { +// Debug.LogError("DisplayListArea requires a DisplayItem template."); +// return; +// } +// +// _displayItemObjectPool = +// GameEntry.ObjectPool.CreateSingleSpawnObjectPool(DisplayItemPoolName, +// _instancePoolCapacity); +// } +// +// private void EnsureItemCount(int count) +// { +// if (_displayItemObjectPool == null) return; +// +// for (int i = _activeItems.Count; i < count; i++) +// { +// DisplayItem item = CreateItem(); +// if (item == null) +// { +// break; +// } +// +// _activeItems.Add(item); +// } +// +// for (int i = _activeItems.Count - 1; i >= count; i--) +// { +// HideItem(_activeItems[i]); +// } +// } +// +// private DisplayItem CreateItem() +// { +// DisplayItem item = null; +// DisplayItemObject itemObject = _displayItemObjectPool.Spawn(); +// if (itemObject != null) +// { +// item = (DisplayItem)itemObject.Target; +// } +// else +// { +// if (_displayItemTemplate == null) +// { +// Debug.LogError("DisplayListArea requires a DisplayItem template."); +// return null; +// } +// +// item = Instantiate(_displayItemTemplate); +// _displayItemObjectPool.Register(DisplayItemObject.Create(item), true); +// } +// +// if (_displayItemRoot != null) +// { +// Transform itemTransform = item.transform; +// itemTransform.SetParent(_displayItemRoot, false); +// } +// +// item.gameObject.SetActive(true); +// return item; +// } +// +// private void HideItem(DisplayItem item) +// { +// if (item == null) return; +// item.OnReset(); +// _activeItems.Remove(item); +// item.gameObject.SetActive(false); +// _displayItemObjectPool?.Unspawn(item); +// } +// +// private void LayoutItems() +// { +// if (_displayItemRoot == null) return; +// +// int columnCount = _fixedColumnCount > 0 ? _fixedColumnCount : CalculateColumnCount(GetRootWidth()); +// +// for (int i = 0; i < _activeItems.Count; i++) +// { +// DisplayItem item = _activeItems[i]; +// item.SetIndex(i); +// LayoutItem(item, i, columnCount); +// } +// } +// +// private void LayoutItem(DisplayItem item, int index, int columnCount) +// { +// if (item == null) return; +// RectTransform itemRect = item.GetComponent(); +// if (itemRect == null) return; +// +// float step = _itemSize + _itemSpacing; +// int row = index / columnCount; +// int col = index % columnCount; +// +// itemRect.anchorMin = new Vector2(0f, 1f); +// itemRect.anchorMax = new Vector2(0f, 1f); +// itemRect.pivot = new Vector2(0f, 1f); +// itemRect.sizeDelta = new Vector2(_itemSize, _itemSize); +// itemRect.anchoredPosition = _startPosition + new Vector2(col * step, -row * step); +// } +// +// private float GetRootWidth() +// { +// RectTransform rootRect = _displayItemRoot as RectTransform; +// return rootRect != null ? rootRect.rect.width : 0f; +// } +// +// private int CalculateColumnCount(float rootWidth) +// { +// if (rootWidth <= 0f) return 1; +// +// float step = _itemSize + _itemSpacing; +// int columns = Mathf.FloorToInt((rootWidth + _itemSpacing) / step); +// return Mathf.Max(1, columns); +// } +// +// private void UpdateCountText(int currentCount, int maxCount) +// { +// if (_countText == null) return; +// _countText.text = maxCount == -1 ? $"({currentCount})" : $"({currentCount}/{maxCount})"; +// } +// } +// } diff --git a/src-ref/UI/Templates/GameScene/View/HudForm.cs b/src-ref/UI/Templates/GameScene/View/HudForm.cs new file mode 100644 index 0000000..9c51f99 --- /dev/null +++ b/src-ref/UI/Templates/GameScene/View/HudForm.cs @@ -0,0 +1,142 @@ +// using CustomComponent; +// using CustomEvent; +// using GameFramework.Event; +// using TMPro; +// using UnityEngine; +// using UnityEngine.UI; +// +// namespace UI +// { +// public class HudForm : UGuiForm +// { +// [SerializeField] private float _sliderFadeDuration = 0.5f; +// +// [SerializeField] private Slider _hpSlider; +// +// [SerializeField] private TMP_Text _hpText; +// +// private float _currentHpPercent = 0; +// private Coroutine _hpSliderFadeCoroutine; +// +// [SerializeField] private Slider _expSlider; +// +// private float _currentExpPercent = 0; +// private Coroutine _expSliderFadeCoroutine; +// +// [SerializeField] private TMP_Text _expText; +// +// private int _playerCurrentLevel = 1; +// +// [SerializeField] private TMP_Text _coinText; +// private int _currentCoin = 0; +// +// [SerializeField] private TMP_Text _levelTimeLeftText; +// private int _currentTimeLeft = 0; +// +// [SerializeField] private TMP_Text _enemyCountText; +// private EnemyManagerComponent _enemy; +// +// public void RefreshUI(HudFormContext hudFormContext) +// { +// } +// +// #region FSM +// +// protected override void OnInit(object userData) +// { +// base.OnInit(userData); +// +// GameEntry.Event.Subscribe(PlayerHealthChangeEventArgs.EventId, PlayerHpChange); +// GameEntry.Event.Subscribe(PlayerExpChangeEventArgs.EventId, PlayerExpChange); +// GameEntry.Event.Subscribe(PlayerCoinChangeEventArgs.EventId, PlayerCoinChange); +// GameEntry.Event.Subscribe(PlayerLevelUpEventArgs.EventId, PlayerLevelUp); +// GameEntry.Event.Subscribe(LevelProcessEventArgs.EventId, LevelProcess); +// +// _enemy = GameEntry.EnemyManager; +// } +// +// protected override void OnClose(bool isShutdown, object userData) +// { +// GameEntry.Event.Unsubscribe(PlayerLevelUpEventArgs.EventId, PlayerLevelUp); +// GameEntry.Event.Unsubscribe(PlayerHealthChangeEventArgs.EventId, PlayerHpChange); +// GameEntry.Event.Unsubscribe(PlayerExpChangeEventArgs.EventId, PlayerExpChange); +// GameEntry.Event.Unsubscribe(PlayerCoinChangeEventArgs.EventId, PlayerCoinChange); +// GameEntry.Event.Unsubscribe(PlayerLevelUpEventArgs.EventId, PlayerLevelUp); +// +// base.OnClose(isShutdown, userData); +// } +// +// protected override void OnUpdate(float elapseSeconds, float realElapseSeconds) +// { +// base.OnUpdate(elapseSeconds, realElapseSeconds); +// if (_enemy == null) +// { +// _enemy = GameEntry.EnemyManager; +// } +// +// if (_enemy != null) +// { +// _enemyCountText.text = $"Enemy: {_enemy.CurrentEnemyCount}"; +// } +// } +// +// #endregion +// +// #region Event Handlers +// +// private void PlayerHpChange(object sender, GameEventArgs e) +// { +// if (!(e is PlayerHealthChangeEventArgs args)) return; +// +// _hpText.text = $"{args.CurrentHealth}/{args.MaxHealth}"; +// +// float percent = (float)args.CurrentHealth / args.MaxHealth; +// if (Mathf.Approximately(_currentHpPercent, percent)) return; +// +// _currentHpPercent = percent; +// +// if (_hpSliderFadeCoroutine != null) StopCoroutine(_hpSliderFadeCoroutine); +// _hpSliderFadeCoroutine = StartCoroutine(_hpSlider.SmoothValue(percent, _sliderFadeDuration)); +// } +// +// private void PlayerExpChange(object sender, GameEventArgs e) +// { +// if (!(e is PlayerExpChangeEventArgs args)) return; +// float percent = (float)args.CurrentExp / args.MaxExp; +// if (Mathf.Approximately(_currentExpPercent, percent)) return; +// +// _currentExpPercent = percent; +// +// if (_expSliderFadeCoroutine != null) StopCoroutine(_expSliderFadeCoroutine); +// _expSliderFadeCoroutine = StartCoroutine(_expSlider.SmoothValue(percent, _sliderFadeDuration)); +// } +// +// private void PlayerLevelUp(object sender, GameEventArgs e) +// { +// if (!(e is PlayerLevelUpEventArgs)) return; +// +// _playerCurrentLevel++; +// _expText.text = $"LV.{_playerCurrentLevel}"; +// } +// +// private void PlayerCoinChange(object sender, GameEventArgs e) +// { +// if (!(e is PlayerCoinChangeEventArgs args)) return; +// if (_currentCoin == args.CoinCount) return; +// +// _coinText.text = $" {args.CoinCount}"; +// _currentCoin = args.CoinCount; +// } +// +// private void LevelProcess(object sender, GameEventArgs e) +// { +// if (!(e is LevelProcessEventArgs args)) return; +// if (_currentTimeLeft == args.LevelTimeLeft) return; +// +// _currentTimeLeft = args.LevelTimeLeft; +// _levelTimeLeftText.text = _currentTimeLeft.ToString(); +// } +// +// #endregion +// } +// } \ No newline at end of file diff --git a/src-ref/UI/Templates/GameScene/View/LevelUpForm.cs b/src-ref/UI/Templates/GameScene/View/LevelUpForm.cs new file mode 100644 index 0000000..b130005 --- /dev/null +++ b/src-ref/UI/Templates/GameScene/View/LevelUpForm.cs @@ -0,0 +1,70 @@ +// using CustomEvent; +// using TMPro; +// using UnityEngine; +// using UnityGameFramework.Runtime; +// +// namespace UI +// { +// public class LevelUpForm : UGuiForm +// { +// [SerializeField] private LevelUpRewardItem[] _propItems; +// +// [SerializeField] private TMP_Text _refreshButtonText; +// +// private LevelUpFormContext _context; +// +// public void RefreshUI(LevelUpFormContext context) +// { +// _context = context; +// +// foreach (var propItem in _propItems) +// { +// propItem.gameObject.SetActive(false); +// } +// +// if (_context.Props == null) return; +// int count = Mathf.Min(_propItems.Length, _context.Props.Count); +// for (int i = 0; i < count; i++) +// { +// _propItems[i].gameObject.SetActive(true); +// _propItems[i].Init(_context.Props[i]); +// } +// +// //刷新 -20 +// _refreshButtonText.text = $"刷新 -{context.RefreshPrice} "; +// } +// +// #region FSM +// +// protected override void OnOpen(object userData) +// { +// base.OnOpen(userData); +// +// if (userData is LevelUpFormContext context) +// { +// RefreshUI(context); +// return; +// } +// +// Log.Warning("LevelUpForm requires LevelUpFormContext as userData."); +// } +// +// protected override void OnClose(bool isShutdown, object userData) +// { +// _context = null; +// base.OnClose(isShutdown, userData); +// } +// +// #endregion +// +// public void OnSelectButtonClick(int index) +// { +// GameEntry.Event.Fire(this, LevelUpPropSelectedEventArgs.Create(index)); +// } +// +// public void OnRefreshButtonClick() +// { +// GameEntry.Event.Fire(this, RefreshEventArgs.Create(_context.RefreshPrice)); +// } +// } +// } diff --git a/src-ref/UI/Templates/GameScene/View/LevelUpRewardItem.cs b/src-ref/UI/Templates/GameScene/View/LevelUpRewardItem.cs new file mode 100644 index 0000000..eeea0a4 --- /dev/null +++ b/src-ref/UI/Templates/GameScene/View/LevelUpRewardItem.cs @@ -0,0 +1,47 @@ +// using TMPro; +// using UnityEngine; +// using UnityEngine.UI; +// using UnityGameFramework.Runtime; +// +// namespace UI +// { +// public class LevelUpRewardItem : MonoBehaviour +// { +// [SerializeField] private IconArea _iconArea; +// +// [SerializeField] private TMP_Text _titleText; +// +// [SerializeField] private TMP_Text _typeText; +// +// [SerializeField] private TMP_Text _descriptionText; +// +// private LevelUpRewardItemContext _context; +// +// +// public void Init(LevelUpRewardItemContext context) +// { +// if (context == null) +// { +// Log.Error("LevelUpPropContext context is invalid."); +// return; +// } +// +// _context = context; +// +// if (_titleText != null) _titleText.text = context.Title; +// if (_descriptionText != null) _descriptionText.text = context.Description; +// if (_iconArea != null) _iconArea.OnInit(context.Icon, context.ItemRarity); +// +// LoadIcon(_context.IconAssetName); +// } +// +// private void LoadIcon(string iconAssetName) +// { +// if (_iconArea == null) return; +// +// if (string.IsNullOrEmpty(iconAssetName)) return; +// +// GameEntry.SpriteCache.GetSprite(iconAssetName, sprite => _iconArea.SetIcon(sprite)); +// } +// } +// } \ No newline at end of file diff --git a/src-ref/UI/Templates/GameScene/View/ShopForm.cs b/src-ref/UI/Templates/GameScene/View/ShopForm.cs new file mode 100644 index 0000000..6d38f66 --- /dev/null +++ b/src-ref/UI/Templates/GameScene/View/ShopForm.cs @@ -0,0 +1,192 @@ +// using System.Collections.Generic; +// using CustomEvent; +// using GameFramework.Event; +// using TMPro; +// using UnityEngine; +// using UnityGameFramework.Runtime; +// +// namespace UI +// { +// public class ShopForm : UGuiForm +// { +// #region Property +// +// [SerializeField] private TMP_Text _titleText; +// +// [SerializeField] private TMP_Text _continueButtonText; +// +// [SerializeField] private TMP_Text _refreshPriceText; +// +// [SerializeField] private TMP_Text _playerCoinText; +// private int _currentCoin; +// +// [SerializeField] private ShopGoodsItem[] _goodsItems; +// +// [SerializeField] private DisplayListArea _propListArea; +// +// [SerializeField] private DisplayListArea _weaponListArea; +// +// private ShopFormContext _context; +// +// #endregion +// +// +// public void RefreshUI(ShopFormContext context) +// { +// _context = context; +// +// _titleText.text = $"商店 (Lv.{context.CurrentLevel})"; +// _continueButtonText.text = $"继续 (Lv.{context.CurrentLevel + 1})"; +// RefreshRefreshPrice(context.RefreshPrice); +// _playerCoinText.text = $" {context.PlayerCoin}"; +// +// RefreshGoodsItems(context.GoodsItems); +// +// if (_propListArea != null && context.PropListContext != null) +// { +// _propListArea.OnInit(context.PropListContext); +// } +// +// if (_weaponListArea != null && context.WeaponListContext != null) +// { +// _weaponListArea.OnInit(context.WeaponListContext); +// } +// } +// +// internal void RefreshGoodsItems(List goodsItems) +// { +// foreach (var item in _goodsItems) +// { +// item.gameObject.SetActive(false); +// } +// +// if (goodsItems == null) +// { +// return; +// } +// +// int count = Mathf.Min(_goodsItems.Length, goodsItems.Count); +// for (int i = 0; i < count; i++) +// { +// _goodsItems[i].Init(goodsItems[i]); +// _goodsItems[i].gameObject.SetActive(true); +// } +// } +// +// internal void RefreshRefreshPrice(int refreshPrice) +// { +// _refreshPriceText.text = $"刷新 -{refreshPrice} "; +// } +// +// internal void ApplyGoodsPurchased(int goodsIndex, DisplayItemContext displayItem) +// { +// SetGoodsItemVisible(goodsIndex, false); +// +// if (displayItem == null) +// { +// return; +// } +// +// if (displayItem.IsWeapon) +// { +// AddWeaponDisplayItem(displayItem); +// } +// else +// { +// AddPropDisplayItem(displayItem); +// } +// } +// +// private void SetGoodsItemVisible(int index, bool visible) +// { +// if (_goodsItems == null || index < 0 || index >= _goodsItems.Length) +// { +// Log.Warning("ShopForm.SetGoodsItemVisible: Invalid index."); +// return; +// } +// +// _goodsItems[index].gameObject.SetActive(visible); +// } +// +// private void AddPropDisplayItem(DisplayItemContext context) +// { +// if (_propListArea == null || context == null) return; +// _propListArea.AddItem(context); +// } +// +// private void AddWeaponDisplayItem(DisplayItemContext context) +// { +// if (_weaponListArea == null || context == null) return; +// _weaponListArea.AddItem(context); +// } +// +// internal void RemoveWeaponDisplayItem(int index) +// { +// if (_weaponListArea == null) return; +// _weaponListArea.RemoveItemAt(index); +// } +// +// #region ButtonClick +// +// public void OnContinueButtonClick() +// { +// GameEntry.Event.Fire(this, ShopContinueEventArgs.Create()); +// } +// +// public void OnPurchaseButtonClick(int index) +// { +// GameEntry.Event.Fire(this, ShopPurchaseEventArgs.Create(index)); +// } +// +// public void OnRefreshButtonClick() +// { +// GameEntry.Event.Fire(this, RefreshEventArgs.Create(_context.RefreshPrice)); +// } +// +// #endregion +// +// #region FSM +// +// protected override void OnOpen(object userData) +// { +// base.OnOpen(userData); +// +// GameEntry.Event.Subscribe(PlayerCoinChangeEventArgs.EventId, OnPlayerCoinChange); +// +// if (userData is ShopFormContext context) +// { +// RefreshUI(context); +// return; +// } +// +// Log.Warning("ShopForm requires ShopFormContext as userData."); +// } +// +// protected override void OnClose(bool isShutdown, object userData) +// { +// _context = null; +// +// GameEntry.Event.Unsubscribe(PlayerCoinChangeEventArgs.EventId, OnPlayerCoinChange); +// +// _propListArea?.OnReset(); +// _weaponListArea?.OnReset(); +// +// base.OnClose(isShutdown, userData); +// } +// +// #endregion +// +// #region Event Handlers +// +// private void OnPlayerCoinChange(object sender, GameEventArgs e) +// { +// if (!(e is PlayerCoinChangeEventArgs args)) return; +// if (args.CoinCount == _currentCoin) return; +// +// _currentCoin = args.CoinCount; +// _playerCoinText.text = $" {_currentCoin}"; +// } +// +// #endregion +// } +// } diff --git a/src-ref/UI/Templates/MenuScene/Context/RoleItemContext.cs b/src-ref/UI/Templates/MenuScene/Context/RoleItemContext.cs new file mode 100644 index 0000000..d6d68be --- /dev/null +++ b/src-ref/UI/Templates/MenuScene/Context/RoleItemContext.cs @@ -0,0 +1,8 @@ +// namespace UI +// { +// public class RoleItemContext : UIContext +// { +// public int RoleId; +// public string IconName; +// } +// } diff --git a/src-ref/UI/Templates/MenuScene/Context/RolePropertyAreaContext.cs b/src-ref/UI/Templates/MenuScene/Context/RolePropertyAreaContext.cs new file mode 100644 index 0000000..3f04cda --- /dev/null +++ b/src-ref/UI/Templates/MenuScene/Context/RolePropertyAreaContext.cs @@ -0,0 +1,8 @@ +// namespace UI +// { +// public class RolePropertyAreaContext : UIContext +// { +// public string RoleName; +// public string InitialPropertyText; +// } +// } diff --git a/src-ref/UI/Templates/MenuScene/Context/SelectRoleFormContext.cs b/src-ref/UI/Templates/MenuScene/Context/SelectRoleFormContext.cs new file mode 100644 index 0000000..7a6888d --- /dev/null +++ b/src-ref/UI/Templates/MenuScene/Context/SelectRoleFormContext.cs @@ -0,0 +1,9 @@ +// namespace UI +// { +// public class SelectRoleFormContext : UIContext +// { +// public RoleItemContext[] RoleItemContexts; +// public RolePropertyAreaContext RolePropertyAreaContext; +// public int[] RoleIds; +// } +// } diff --git a/src-ref/UI/Templates/MenuScene/Context/StartMenuFormContext.cs b/src-ref/UI/Templates/MenuScene/Context/StartMenuFormContext.cs new file mode 100644 index 0000000..da5f5d3 --- /dev/null +++ b/src-ref/UI/Templates/MenuScene/Context/StartMenuFormContext.cs @@ -0,0 +1,6 @@ +// namespace UI +// { +// public class StartMenuFormContext : UIContext +// { +// } +// } diff --git a/src-ref/UI/Templates/MenuScene/Controller/SelectRoleFormController.cs b/src-ref/UI/Templates/MenuScene/Controller/SelectRoleFormController.cs new file mode 100644 index 0000000..ff702f0 --- /dev/null +++ b/src-ref/UI/Templates/MenuScene/Controller/SelectRoleFormController.cs @@ -0,0 +1,156 @@ +// using CustomEvent; +// using Definition.Enum; +// using GameFramework.Event; +// using UnityGameFramework.Runtime; +// +// namespace UI +// { +// public class SelectRoleFormController : UIFormControllerCommonBase +// { +// private SelectRoleFormUseCase _useCase; +// +// protected override UIFormType UIFormTypeId => UIFormType.SelectRoleForm; +// +// protected override void RefreshUI(SelectRoleForm form, SelectRoleFormContext context) +// { +// form.RefreshUI(context); +// } +// +// protected override void SubscribeCustomEvents() +// { +// GameEntry.Event.Subscribe(MenuSelectRoleReturnEventArgs.EventId, OnMenuSelectRoleReturn); +// GameEntry.Event.Subscribe(MenuSelectRoleSelectedEventArgs.EventId, OnMenuSelectRoleSelected); +// GameEntry.Event.Subscribe(MenuSelectRoleConfirmEventArgs.EventId, OnMenuSelectRoleConfirm); +// } +// +// protected override void UnsubscribeCustomEvents() +// { +// GameEntry.Event.Unsubscribe(MenuSelectRoleReturnEventArgs.EventId, OnMenuSelectRoleReturn); +// GameEntry.Event.Unsubscribe(MenuSelectRoleSelectedEventArgs.EventId, OnMenuSelectRoleSelected); +// GameEntry.Event.Unsubscribe(MenuSelectRoleConfirmEventArgs.EventId, OnMenuSelectRoleConfirm); +// } +// +// private static SelectRoleFormContext BuildContext(SelectRoleFormRawData rawData) +// { +// if (rawData == null) +// { +// return null; +// } +// +// int count = rawData.RoleIds?.Length ?? 0; +// RoleItemContext[] roleItems = new RoleItemContext[count]; +// for (int i = 0; i < count; i++) +// { +// string iconName = rawData.RoleIconNames != null && i < rawData.RoleIconNames.Length +// ? rawData.RoleIconNames[i] +// : null; +// +// roleItems[i] = new RoleItemContext +// { +// RoleId = rawData.RoleIds[i], +// IconName = iconName +// }; +// } +// +// RolePropertyAreaContext propertyContext = null; +// if (rawData.SelectedRoleId >= 0) +// { +// propertyContext = new RolePropertyAreaContext +// { +// RoleName = rawData.SelectedRoleName, +// InitialPropertyText = rawData.SelectedRoleInitialPropertyText +// }; +// } +// +// return new SelectRoleFormContext +// { +// RoleItemContexts = roleItems, +// RolePropertyAreaContext = propertyContext, +// RoleIds = rawData.RoleIds +// }; +// } +// +// public override int? OpenUI(object userData = null) +// { +// if (userData is SelectRoleFormContext selectRoleFormContext) +// { +// return OpenUIInternal(selectRoleFormContext); +// } +// +// if (userData != null) +// { +// Log.Warning("SelectRoleFormController.OpenUI() userData type is invalid."); +// return null; +// } +// +// if (_useCase == null) +// { +// Log.Error("SelectRoleFormController.OpenUI() useCase is null."); +// return null; +// } +// +// SelectRoleFormRawData rawData = _useCase.CreateModel(); +// SelectRoleFormContext context = BuildContext(rawData); +// return OpenUIInternal(context); +// } +// +// public override void BindUseCase(IUIUseCase useCase) +// { +// if (!(useCase is SelectRoleFormUseCase selectRoleUseCase)) +// { +// Log.Error("SelectRoleUseCase.BindUseCase() useCase is invalid."); +// return; +// } +// +// _useCase = selectRoleUseCase; +// } +// +// public void UpdateShowRole(RolePropertyAreaContext rolePropertyAreaContext) +// { +// if (Context != null) +// { +// Context.RolePropertyAreaContext = rolePropertyAreaContext; +// } +// +// Form?.UpdateShowRole(rolePropertyAreaContext); +// } +// +// private void OnMenuSelectRoleReturn(object sender, GameEventArgs e) +// { +// if (!(sender is SelectRoleForm) || !(e is MenuSelectRoleReturnEventArgs)) +// { +// return; +// } +// +// CloseUI(); +// } +// +// private void OnMenuSelectRoleSelected(object sender, GameEventArgs e) +// { +// if (!(e is MenuSelectRoleSelectedEventArgs args)) +// { +// return; +// } +// +// SelectRoleFormRawData rawData = _useCase != null ? _useCase.SelectRole(args.RoleId) : null; +// SelectRoleFormContext context = BuildContext(rawData); +// if (context == null) +// { +// return; +// } +// +// SetContext(context); +// UpdateShowRole(context.RolePropertyAreaContext); +// } +// +// private void OnMenuSelectRoleConfirm(object sender, GameEventArgs e) +// { +// if (!(e is MenuSelectRoleConfirmEventArgs)) +// { +// return; +// } +// +// _useCase?.ConfirmSelectedRole(); +// } +// } +// } diff --git a/src-ref/UI/Templates/MenuScene/Controller/StartMenuFormController.cs b/src-ref/UI/Templates/MenuScene/Controller/StartMenuFormController.cs new file mode 100644 index 0000000..ea4a6c2 --- /dev/null +++ b/src-ref/UI/Templates/MenuScene/Controller/StartMenuFormController.cs @@ -0,0 +1,137 @@ +// using CustomEvent; +// using Definition.Enum; +// using GameFramework.Event; +// using UnityGameFramework.Runtime; +// +// namespace UI +// { +// public class StartMenuFormController : UIFormControllerCommonBase +// { +// protected override UIFormType UIFormTypeId => UIFormType.StartMenuForm; +// +// protected override void RefreshUI(StartMenuForm form, StartMenuFormContext context) +// { +// form.RefreshUI(context); +// } +// +// protected override void SubscribeCustomEvents() +// { +// GameEntry.Event.Subscribe(MenuStartGameEventArgs.EventId, OnMenuStartGameButtonClick); +// GameEntry.Event.Subscribe(MenuFileButtonClickEventArgs.EventId, OnMenuFileButtonClick); +// GameEntry.Event.Subscribe(MenuGuideButtonClickEventArgs.EventId, OnMenuGuideButtonClick); +// GameEntry.Event.Subscribe(MenuSettingButtonClickEventArgs.EventId, OnMenuSettingButtonClick); +// GameEntry.Event.Subscribe(MenuQuitButtonClickEventArgs.EventId, OnMenuQuitButtonClick); +// GameEntry.Event.Subscribe(MenuAboutButtonClickEventArgs.EventId, OnMenuAboutButtonClick); +// } +// +// protected override void UnsubscribeCustomEvents() +// { +// GameEntry.Event.Unsubscribe(MenuStartGameEventArgs.EventId, OnMenuStartGameButtonClick); +// GameEntry.Event.Unsubscribe(MenuFileButtonClickEventArgs.EventId, OnMenuFileButtonClick); +// GameEntry.Event.Unsubscribe(MenuGuideButtonClickEventArgs.EventId, OnMenuGuideButtonClick); +// GameEntry.Event.Unsubscribe(MenuSettingButtonClickEventArgs.EventId, OnMenuSettingButtonClick); +// GameEntry.Event.Unsubscribe(MenuQuitButtonClickEventArgs.EventId, OnMenuQuitButtonClick); +// GameEntry.Event.Unsubscribe(MenuAboutButtonClickEventArgs.EventId, OnMenuAboutButtonClick); +// } +// +// private static StartMenuFormContext BuildStartMenuFormContext() +// { +// return new StartMenuFormContext(); +// } +// +// public override int? OpenUI(object userData = null) +// { +// if (userData is StartMenuFormContext context) +// { +// return OpenUIInternal(context); +// } +// +// if (userData != null) +// { +// Log.Warning("StartMenuFormController.OpenUI() userData type is invalid."); +// return null; +// } +// +// return OpenUIInternal(BuildStartMenuFormContext()); +// } +// +// public int? OpenUI(StartMenuFormContext context) +// { +// return OpenUIInternal(context); +// } +// +// public override void BindUseCase(IUIUseCase useCase) +// { +// Log.Info("StartMenuForm doesn't need UseCase"); +// } +// +// private void OnMenuStartGameButtonClick(object sender, GameEventArgs e) +// { +// if (!(sender is StartMenuForm) || !(e is MenuStartGameEventArgs)) +// { +// return; +// } +// +// GameEntry.UIRouter.OpenUI(UIFormType.SelectRoleForm); +// } +// +// private void OnMenuFileButtonClick(object sender, GameEventArgs e) +// { +// if (!(sender is StartMenuForm) || !(e is MenuFileButtonClickEventArgs)) +// { +// return; +// } +// +// Log.Warning("Menu file button click is not implemented."); +// } +// +// private void OnMenuGuideButtonClick(object sender, GameEventArgs e) +// { +// if (!(sender is StartMenuForm) || !(e is MenuGuideButtonClickEventArgs)) +// { +// return; +// } +// +// Log.Warning("Menu guide button click is not implemented."); +// } +// +// private void OnMenuSettingButtonClick(object sender, GameEventArgs e) +// { +// if (!(sender is StartMenuForm) || !(e is MenuSettingButtonClickEventArgs)) +// { +// return; +// } +// +// GameEntry.UIRouter.OpenUI(UIFormType.SettingForm); +// } +// +// private void OnMenuQuitButtonClick(object sender, GameEventArgs e) +// { +// if (!(sender is StartMenuForm) || !(e is MenuQuitButtonClickEventArgs)) +// { +// return; +// } +// +// GameEntry.UIRouter.OpenUI(UIFormType.DialogForm, new DialogFormRawData +// { +// Mode = 2, +// Title = GameEntry.Localization.GetString("AskQuitGame.Title"), +// Message = GameEntry.Localization.GetString("AskQuitGame.Message"), +// OnClickConfirm = delegate(object userData) +// { +// UnityGameFramework.Runtime.GameEntry.Shutdown(ShutdownType.Quit); +// } +// }); +// } +// +// private void OnMenuAboutButtonClick(object sender, GameEventArgs e) +// { +// if (!(sender is StartMenuForm) || !(e is MenuAboutButtonClickEventArgs)) +// { +// return; +// } +// +// Log.Warning("Menu about button click is not implemented."); +// } +// } +// } diff --git a/src-ref/UI/Templates/MenuScene/RawData/SelectRoleFormRawData.cs b/src-ref/UI/Templates/MenuScene/RawData/SelectRoleFormRawData.cs new file mode 100644 index 0000000..53efae5 --- /dev/null +++ b/src-ref/UI/Templates/MenuScene/RawData/SelectRoleFormRawData.cs @@ -0,0 +1,11 @@ +namespace UI +{ + public class SelectRoleFormRawData + { + public int[] RoleIds; + public string[] RoleIconNames; + public int SelectedRoleId; + public string SelectedRoleName; + public string SelectedRoleInitialPropertyText; + } +} diff --git a/src-ref/UI/Templates/MenuScene/RawData/StartMenuFormRawData.cs b/src-ref/UI/Templates/MenuScene/RawData/StartMenuFormRawData.cs new file mode 100644 index 0000000..b72806b --- /dev/null +++ b/src-ref/UI/Templates/MenuScene/RawData/StartMenuFormRawData.cs @@ -0,0 +1,7 @@ +namespace UI +{ + public class StartMenuFormRawData + { + + } +} \ No newline at end of file diff --git a/src-ref/UI/Templates/MenuScene/UseCase/SelectRoleFormUseCase.cs b/src-ref/UI/Templates/MenuScene/UseCase/SelectRoleFormUseCase.cs new file mode 100644 index 0000000..a4d7ec3 --- /dev/null +++ b/src-ref/UI/Templates/MenuScene/UseCase/SelectRoleFormUseCase.cs @@ -0,0 +1,99 @@ +// using System.Text; +// using DataTable; +// using GameFramework.DataTable; +// using Procedure; +// using UnityEngine; +// +// namespace UI +// { +// public class SelectRoleFormUseCase : IUIUseCase +// { +// private readonly IDataTable _roleDataTable; +// +// private readonly ProcedureStartMenu _procedureStartMenu; +// +// public int SelectedRoleId { get; private set; } +// +// public SelectRoleFormUseCase(ProcedureStartMenu procedureStartMenu) +// { +// _procedureStartMenu = procedureStartMenu; +// _roleDataTable = GameEntry.DataTable.GetDataTable(); +// } +// +// public SelectRoleFormRawData CreateModel() +// { +// return BuildModel(0); +// } +// +// public SelectRoleFormRawData SelectRole(int roleId) +// { +// SelectedRoleId = roleId; +// return BuildModel(roleId); +// } +// +// public bool ConfirmSelectedRole() +// { +// DRRole[] roles = _roleDataTable != null ? _roleDataTable.GetAllDataRows() : null; +// if (roles == null || roles.Length == 0) +// { +// return false; +// } +// +// if (SelectedRoleId < 0 || _roleDataTable.GetDataRow(SelectedRoleId) == null) +// { +// int randomIndex = Random.Range(0, roles.Length); +// SelectedRoleId = roles[randomIndex].Id; +// } +// +// bool result = SelectedRoleId >= 0; +// if (result) +// { +// _procedureStartMenu.StartGame(SelectedRoleId); +// } +// +// return result; +// } +// +// private SelectRoleFormRawData BuildModel(int selectedRoleId) +// { +// DRRole[] roles = _roleDataTable.GetAllDataRows(); +// int count = _roleDataTable.Count; +// int[] roleIds = new int[count]; +// string[] iconNames = new string[count]; +// for (int i = 0; i < count; i++) +// { +// roleIds[i] = roles[i].Id; +// iconNames[i] = roles[i].IconName; +// } +// +// DRRole selectedRole = selectedRoleId > 0 ? _roleDataTable.GetDataRow(selectedRoleId) : null; +// return new SelectRoleFormRawData +// { +// RoleIds = roleIds, +// RoleIconNames = iconNames, +// SelectedRoleId = selectedRole?.Id ?? -1, +// SelectedRoleName = selectedRole?.RoleName, +// SelectedRoleInitialPropertyText = selectedRole != null +// ? BuildRoleInitialPropertyText(selectedRole) +// : null +// }; +// } +// +// private static string BuildRoleInitialPropertyText(DRRole role) +// { +// if (role == null || role.InitialProperties == null || role.InitialProperties.Length == 0) +// { +// return string.Empty; +// } +// +// StringBuilder sb = new StringBuilder(); +// for (int i = 0; i < role.InitialProperties.Length; i++) +// { +// sb.Append(role.InitialProperties[i]); +// sb.Append('\n'); +// } +// +// return sb.ToString(); +// } +// } +// } \ No newline at end of file diff --git a/src-ref/UI/Templates/MenuScene/View/RoleItem.cs b/src-ref/UI/Templates/MenuScene/View/RoleItem.cs new file mode 100644 index 0000000..1ca9cc3 --- /dev/null +++ b/src-ref/UI/Templates/MenuScene/View/RoleItem.cs @@ -0,0 +1,40 @@ +// using CustomEvent; +// using UnityEngine; +// using UnityEngine.UI; +// +// namespace UI +// { +// public class RoleItem : MonoBehaviour +// { +// [SerializeField] private Image _roleImage = null; +// +// private RoleItemContext _context = null; +// +// public void OnInit(RoleItemContext context) +// { +// _context = context; +// if (_context == null || string.IsNullOrEmpty(_context.IconName)) +// { +// return; +// } +// +// GameEntry.SpriteCache.GetSprite(_context.IconName, sprite => _roleImage.sprite = sprite); +// } +// +// public void OnReset() +// { +// _context = null; +// } +// +// public void UpdateShowRole() +// { +// int roleId = _context?.RoleId ?? -1; +// GameEntry.Event.Fire(this, MenuSelectRoleSelectedEventArgs.Create(roleId)); +// } +// +// public void OnConfirmRoleClick() +// { +// GameEntry.Event.Fire(this, MenuSelectRoleConfirmEventArgs.Create()); +// } +// } +// } diff --git a/src-ref/UI/Templates/MenuScene/View/RolePropertyArea.cs b/src-ref/UI/Templates/MenuScene/View/RolePropertyArea.cs new file mode 100644 index 0000000..ff8b679 --- /dev/null +++ b/src-ref/UI/Templates/MenuScene/View/RolePropertyArea.cs @@ -0,0 +1,37 @@ +// using TMPro; +// using UnityEngine; +// using UnityEngine.UI; +// +// namespace UI +// { +// public class RolePropertyArea : MonoBehaviour +// { +// [SerializeField] private TMP_Text _roleNameText; +// +// [SerializeField] private RawImage _roleAvatar; +// +// [SerializeField] private TMP_Text _roleInitialPropertyText; +// +// [SerializeField] private Transform _roleInitialItemParent; +// +// private RolePropertyAreaContext _context; +// +// public void OnInit(RolePropertyAreaContext context) +// { +// _context = context; +// if (_context == null) +// { +// OnReset(); +// return; +// } +// +// _roleNameText.text = _context.RoleName; +// _roleInitialPropertyText.text = _context.InitialPropertyText; +// } +// +// public void OnReset(object userData = null) +// { +// _context = null; +// } +// } +// } diff --git a/src-ref/UI/Templates/MenuScene/View/SelectRoleForm.cs b/src-ref/UI/Templates/MenuScene/View/SelectRoleForm.cs new file mode 100644 index 0000000..e8296b8 --- /dev/null +++ b/src-ref/UI/Templates/MenuScene/View/SelectRoleForm.cs @@ -0,0 +1,100 @@ +// using System; +// using CustomEvent; +// using UnityEngine; +// using UnityGameFramework.Runtime; +// +// namespace UI +// { +// public class SelectRoleForm : UGuiForm +// { +// [SerializeField] private GameObject _randomRoleCard = null; +// +// [SerializeField] private GameObject _roleCard = null; +// +// [SerializeField] private RolePropertyArea _propertyArea = null; +// +// private SelectRoleFormContext _context = null; +// +// private RoleItem[] _roleItems = null; +// +// public void RefreshUI(SelectRoleFormContext context) +// { +// _context = context; +// +// int itemIndex = 1, roleIndex = 0; +// while (itemIndex < _roleItems.Length) +// { +// if (roleIndex < _context.RoleItemContexts.Length) +// { +// _roleItems[itemIndex].OnInit(_context.RoleItemContexts[roleIndex]); +// _roleItems[itemIndex].gameObject.SetActive(true); +// } +// else +// { +// _roleItems[itemIndex].gameObject.SetActive(false); +// _roleItems[itemIndex].OnReset(); +// } +// +// itemIndex++; +// roleIndex++; +// } +// +// UpdateShowRole(_context.RolePropertyAreaContext); +// } +// +// #region FSM +// +// protected override void OnInit(object userData) +// { +// base.OnInit(userData); +// _roleItems = this.transform.GetComponentsInChildren(); +// } +// +// protected override void OnOpen(object userData) +// { +// base.OnOpen(userData); +// +// if (userData is SelectRoleFormContext context) +// { +// RefreshUI(context); +// return; +// } +// +// Log.Warning("SelectRoleForm requires SelectRoleFormContext as userData."); +// } +// +// protected override void OnClose(bool isShutdown, object userData) +// { +// base.OnClose(isShutdown, userData); +// +// _context = null; +// foreach (var item in _roleItems) +// { +// item.OnReset(); +// } +// } +// +// #endregion +// +// public void UpdateShowRole(RolePropertyAreaContext propertyAreaContext) +// { +// if (propertyAreaContext == null) +// { +// _randomRoleCard.SetActive(true); +// _roleCard.SetActive(false); +// _propertyArea.OnReset(); +// } +// else +// { +// _randomRoleCard.SetActive(false); +// _propertyArea.OnInit(propertyAreaContext); +// _roleCard.SetActive(true); +// } +// } +// +// public void OnReturnButtonClick() +// { +// GameEntry.Event.Fire(this, MenuSelectRoleReturnEventArgs.Create()); +// } +// } +// } \ No newline at end of file diff --git a/src-ref/UI/Templates/MenuScene/View/SettingForm.cs b/src-ref/UI/Templates/MenuScene/View/SettingForm.cs new file mode 100644 index 0000000..461ef4c --- /dev/null +++ b/src-ref/UI/Templates/MenuScene/View/SettingForm.cs @@ -0,0 +1,189 @@ +// //------------------------------------------------------------ +// // Game Framework +// // Copyright © 2013-2021 Jiang Yin. All rights reserved. +// // Homepage: https://gameframework.cn/ +// // Feedback: mailto:ellan@gameframework.cn +// //------------------------------------------------------------ +// +// using Definition; +// using GameFramework.Localization; +// using StarForce; +// using UnityEngine; +// using UnityEngine.UI; +// using UnityGameFramework.Runtime; +// +// namespace UI +// { +// public class SettingForm : UGuiForm +// { +// [SerializeField] private Toggle _musicMuteToggle = null; +// [SerializeField] private Slider _musicVolumeSlider = null; +// +// [SerializeField] private Toggle _soundMuteToggle = null; +// [SerializeField] private Slider _soundVolumeSlider = null; +// +// [SerializeField] private Toggle _uiSoundMuteToggle = null; +// [SerializeField] private Slider _uiSoundVolumeSlider = null; +// +// [SerializeField] private CanvasGroup _languageTipsCanvasGroup = null; +// +// [SerializeField] private Toggle _englishToggle = null; +// [SerializeField] private Toggle _chineseSimplifiedToggle = null; +// [SerializeField] private Toggle _chineseTraditionalToggle = null; +// [SerializeField] private Toggle _koreanToggle = null; +// +// private Language _selectedLanguage = Language.Unspecified; +// +// public void OnMusicMuteChanged(bool isOn) +// { +// GameEntry.Sound.Mute("Music", !isOn); +// _musicVolumeSlider.gameObject.SetActive(isOn); +// } +// +// public void OnMusicVolumeChanged(float volume) +// { +// GameEntry.Sound.SetVolume("Music", volume); +// } +// +// public void OnSoundMuteChanged(bool isOn) +// { +// GameEntry.Sound.Mute("Sound", !isOn); +// _soundVolumeSlider.gameObject.SetActive(isOn); +// } +// +// public void OnSoundVolumeChanged(float volume) +// { +// GameEntry.Sound.SetVolume("Sound", volume); +// } +// +// public void OnUISoundMuteChanged(bool isOn) +// { +// GameEntry.Sound.Mute("UISound", !isOn); +// _uiSoundVolumeSlider.gameObject.SetActive(isOn); +// } +// +// public void OnUISoundVolumeChanged(float volume) +// { +// GameEntry.Sound.SetVolume("UISound", volume); +// } +// +// public void OnEnglishSelected(bool isOn) +// { +// if (!isOn) +// { +// return; +// } +// +// _selectedLanguage = Language.English; +// RefreshLanguageTips(); +// } +// +// public void OnChineseSimplifiedSelected(bool isOn) +// { +// if (!isOn) +// { +// return; +// } +// +// _selectedLanguage = Language.ChineseSimplified; +// RefreshLanguageTips(); +// } +// +// public void OnChineseTraditionalSelected(bool isOn) +// { +// if (!isOn) +// { +// return; +// } +// +// _selectedLanguage = Language.ChineseTraditional; +// RefreshLanguageTips(); +// } +// +// public void OnKoreanSelected(bool isOn) +// { +// if (!isOn) +// { +// return; +// } +// +// _selectedLanguage = Language.Korean; +// RefreshLanguageTips(); +// } +// +// public void OnSubmitButtonClick() +// { +// if (_selectedLanguage == GameEntry.Localization.Language) +// { +// Close(); +// return; +// } +// +// GameEntry.Setting.SetString(Constant.Setting.Language, _selectedLanguage.ToString()); +// GameEntry.Setting.Save(); +// +// GameEntry.Sound.StopMusic(); +// UnityGameFramework.Runtime.GameEntry.Shutdown(ShutdownType.Restart); +// } +// +// #if UNITY_2017_3_OR_NEWER +// protected override void OnOpen(object userData) +// #else +// protected internal override void OnOpen(object userData) +// #endif +// { +// base.OnOpen(userData); +// +// _musicMuteToggle.isOn = !GameEntry.Sound.IsMuted("Music"); +// _musicVolumeSlider.value = GameEntry.Sound.GetVolume("Music"); +// +// _soundMuteToggle.isOn = !GameEntry.Sound.IsMuted("Sound"); +// _soundVolumeSlider.value = GameEntry.Sound.GetVolume("Sound"); +// +// _uiSoundMuteToggle.isOn = !GameEntry.Sound.IsMuted("UISound"); +// _uiSoundVolumeSlider.value = GameEntry.Sound.GetVolume("UISound"); +// +// _selectedLanguage = GameEntry.Localization.Language; +// switch (_selectedLanguage) +// { +// case Language.English: +// _englishToggle.isOn = true; +// break; +// +// case Language.ChineseSimplified: +// _chineseSimplifiedToggle.isOn = true; +// break; +// +// case Language.ChineseTraditional: +// _chineseTraditionalToggle.isOn = true; +// break; +// +// case Language.Korean: +// _koreanToggle.isOn = true; +// break; +// +// default: +// break; +// } +// } +// +// #if UNITY_2017_3_OR_NEWER +// protected override void OnUpdate(float elapseSeconds, float realElapseSeconds) +// #else +// protected internal override void OnUpdate(float elapseSeconds, float realElapseSeconds) +// #endif +// { +// base.OnUpdate(elapseSeconds, realElapseSeconds); +// +// if (_languageTipsCanvasGroup.gameObject.activeSelf) +// { +// _languageTipsCanvasGroup.alpha = 0.5f + 0.5f * Mathf.Sin(Mathf.PI * Time.time); +// } +// } +// +// private void RefreshLanguageTips() +// { +// _languageTipsCanvasGroup.gameObject.SetActive(_selectedLanguage != GameEntry.Localization.Language); +// } +// } +// } \ No newline at end of file diff --git a/src-ref/UI/Templates/MenuScene/View/StartMenuForm.cs b/src-ref/UI/Templates/MenuScene/View/StartMenuForm.cs new file mode 100644 index 0000000..e3823f0 --- /dev/null +++ b/src-ref/UI/Templates/MenuScene/View/StartMenuForm.cs @@ -0,0 +1,64 @@ +// using CustomEvent; +// using UnityGameFramework.Runtime; +// +// namespace UI +// { +// public class StartMenuForm : UGuiForm +// { +// public void RefreshUI(StartMenuFormContext context) +// { +// } +// +// #region Lifecycle +// +// protected override void OnOpen(object userData) +// { +// base.OnOpen(userData); +// +// if (userData is StartMenuFormContext context) +// { +// RefreshUI(context); +// return; +// } +// +// Log.Warning("StartMenuForm requires StartMenuFormContext as userData."); +// } +// +// protected override void OnClose(bool isShutdown, object userData) +// { +// base.OnClose(isShutdown, userData); +// } +// +// #endregion +// +// public void OnStartButtonClick() +// { +// GameEntry.Event.Fire(this, MenuStartGameEventArgs.Create()); +// } +// +// public void OnFileButtonClick() +// { +// GameEntry.Event.Fire(this, MenuFileButtonClickEventArgs.Create()); +// } +// +// public void OnGuideButtonClick() +// { +// GameEntry.Event.Fire(this, MenuGuideButtonClickEventArgs.Create()); +// } +// +// public void OnSettingButtonClick() +// { +// GameEntry.Event.Fire(this, MenuSettingButtonClickEventArgs.Create()); +// } +// +// public void OnQuitButtonClick() +// { +// GameEntry.Event.Fire(this, MenuQuitButtonClickEventArgs.Create()); +// } +// +// public void OnAboutButtonClick() +// { +// GameEntry.Event.Fire(this, MenuAboutButtonClickEventArgs.Create()); +// } +// } +// } diff --git a/src-ref/UI/UpdateResourceForm.cs b/src-ref/UI/UpdateResourceForm.cs new file mode 100644 index 0000000..cf53d81 --- /dev/null +++ b/src-ref/UI/UpdateResourceForm.cs @@ -0,0 +1,30 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace GeometryTD.UI +{ + public class UpdateResourceForm : MonoBehaviour + { + [SerializeField] + private Text m_DescriptionText = null; + + [SerializeField] + private Slider m_ProgressSlider = null; + + private void Start() + { + + } + + private void Update() + { + + } + + public void SetProgress(float progress, string description) + { + m_ProgressSlider.value = progress; + m_DescriptionText.text = description; + } + } +} diff --git a/src-ref/Utility/AssetUtility.cs b/src-ref/Utility/AssetUtility.cs new file mode 100644 index 0000000..a3a047c --- /dev/null +++ b/src-ref/Utility/AssetUtility.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using GameFramework; + +namespace GeometryTD.CustomUtility +{ + public static class AssetUtility + { + public static string GetConfigAsset(string assetName, bool fromBytes) + { + return Utility.Text.Format("Assets/GameMain/Configs/{0}.{1}", assetName, fromBytes ? "bytes" : "txt"); + } + + public static string GetDataTableAsset(string assetName, bool fromBytes) + { + return Utility.Text.Format("Assets/GameMain/DataTables/{0}.{1}", assetName, fromBytes ? "bytes" : "txt"); + } + + public static string GetDictionaryAsset(string assetName, bool fromBytes) + { + return Utility.Text.Format("Assets/GameMain/Localization/{0}/Dictionaries/{1}.{2}", + GameEntry.Localization.Language, assetName, fromBytes ? "bytes" : "xml"); + } + + public static string GetFontAsset(string assetName) + { + return Utility.Text.Format("Assets/GameMain/Fonts/{0}.ttf", assetName); + } + + public static string GetTMPFontAsset(string assetName) + { + return Utility.Text.Format("Assets/GameMain/Fonts/{0}.asset", assetName); + } + + public static string GetSceneAsset(string assetName) + { + return Utility.Text.Format("Assets/GameMain/Scenes/{0}.unity", assetName); + } + + public static string GetMusicAsset(string assetName) + { + return Utility.Text.Format("Assets/GameMain/Music/{0}.mp3", assetName); + } + + public static string GetSoundAsset(string assetName) + { + return Utility.Text.Format("Assets/GameMain/Sounds/{0}.wav", assetName); + } + + public static string GetEntityAsset(string assetName) + { + return Utility.Text.Format("Assets/GameMain/Entities/{0}.prefab", assetName); + } + + public static string GetUIFormAsset(string assetName) + { + return Utility.Text.Format("Assets/GameMain/UI/UIForms/{0}.prefab", assetName); + } + + public static string GetUISoundAsset(string assetName) + { + return Utility.Text.Format("Assets/GameMain/UI/UISounds/{0}.wav", assetName); + } + + public static string GetLevelMapAsset(string assetName) + { + return Utility.Text.Format("Assets/GameMain/Entities/LevelMap/Level{0}.prefab", assetName); + } + + public static string GetTextureAsset(string assetName) + { + // Assets/GameMain/Textures/Capsule.png + return Utility.Text.Format("Assets/GameMain/Textures/{0}.png", assetName); + } + } +} \ No newline at end of file diff --git a/src-ref/Utility/EnumUtility.cs b/src-ref/Utility/EnumUtility.cs new file mode 100644 index 0000000..c2cda9e --- /dev/null +++ b/src-ref/Utility/EnumUtility.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using UnityGameFramework.Runtime; + +namespace GeometryTD.CustomUtility +{ + public static class EnumUtility where T : struct, System.Enum + { + private static readonly Dictionary _enumCache = new(); + + public static T Get(string value) + { + if (!_enumCache.TryGetValue(value, out T result)) + { + if (System.Enum.TryParse(value, true, out result)) + { + _enumCache[value] = result; + } + else + { + Log.Error($"Enum 解析失败:类型:{typeof(T).Name} 不包含值 {value}"); + } + } + + return result; + } + } +} diff --git a/src-ref/Utility/IconColorGenerator.cs b/src-ref/Utility/IconColorGenerator.cs new file mode 100644 index 0000000..9d7c8f2 --- /dev/null +++ b/src-ref/Utility/IconColorGenerator.cs @@ -0,0 +1,443 @@ +using GameFramework.DataTable; +using GeometryTD.DataTable; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.CustomUtility +{ + public static class IconColorGenerator + { + private const int StatCount = 5; + private const float MinSaturation = 0.4f; + private const float MinValue = 0.6f; + private const float TinyValue = 0.0001f; + + private static readonly float[] HueMap = { 0f, 30f, 200f, 120f, 270f }; + + private static StatRange _attackDamageRange = new StatRange(1f, 300f); + private static StatRange _damageRandomRateRange = new StatRange(0f, 0.3f); + private static StatRange _rotateSpeedRange = new StatRange(10f, 240f); + private static StatRange _attackRangeRange = new StatRange(2f, 8f); + private static StatRange _attackSpeedRange = new StatRange(0.5f, 4f); + + private static bool s_HasMuzzleRange; + private static bool s_HasBearingRange; + private static bool s_HasBaseRange; + + public static Color GenerateForComponent(TowerCompItemData item) + { + if (item == null) + { + return Color.white; + } + + float attackDamage = 0f; + float damageRandomRate = 0f; + float rotateSpeed = 0f; + float attackRange = 0f; + float attackSpeed = 0f; + + if (item is MuzzleCompItemData muzzle) + { + attackDamage = ResolveArrayValue(muzzle.AttackDamage); + damageRandomRate = Mathf.Max(0f, muzzle.DamageRandomRate); + } + else if (item is BearingCompItemData bearing) + { + rotateSpeed = ResolveArrayValue(bearing.RotateSpeed); + attackRange = ResolveArrayValue(bearing.AttackRange); + } + else if (item is BaseCompItemData baseComp) + { + attackSpeed = ResolveArrayValue(baseComp.AttackSpeed); + } + + return GenerateColor( + attackDamage, + damageRandomRate, + rotateSpeed, + attackRange, + attackSpeed, + item.SlotType); + } + + public static Color GenerateForTower(TowerItemData tower) + { + if (tower?.Stats == null) + { + return Color.white; + } + + TowerStatsData stats = tower.Stats; + return GenerateColor( + ResolveArrayValue(stats.AttackDamage), + stats.DamageRandomRate, + ResolveArrayValue(stats.RotateSpeed), + ResolveArrayValue(stats.AttackRange), + ResolveArrayValue(stats.AttackSpeed), + TowerCompSlotType.None); + } + + private static Color GenerateColor( + float attackDamage, + float damageRandomRate, + float rotateSpeed, + float attackRange, + float attackSpeed, + TowerCompSlotType slotType) + { + TryRefreshRangesFromDataTables(); + + float[] normalizedStats = new float[StatCount]; + normalizedStats[0] = Normalize(attackDamage, _attackDamageRange); + normalizedStats[1] = Normalize(damageRandomRate, _damageRandomRateRange); + normalizedStats[2] = Normalize(rotateSpeed, _rotateSpeedRange); + normalizedStats[3] = Normalize(attackRange, _attackRangeRange); + normalizedStats[4] = Normalize(attackSpeed, _attackSpeedRange); + + FindTopTwoIndexes(normalizedStats, out int primaryIndex, out int secondaryIndex); + + float denominator = normalizedStats[primaryIndex] + normalizedStats[secondaryIndex] + TinyValue; + float blendWeight = normalizedStats[secondaryIndex] / denominator; + float hue = LerpHue(HueMap[primaryIndex], HueMap[secondaryIndex], blendWeight); + hue = NormalizeHue(hue + ResolveSlotHueOffset(slotType)); + + float purity = normalizedStats[primaryIndex] - normalizedStats[secondaryIndex]; + float saturation = Mathf.Lerp(0.45f, 0.9f, Mathf.Clamp01(purity * 1.5f)); + saturation = Mathf.Max(saturation, MinSaturation); + + float intensity = 0f; + for (int i = 0; i < normalizedStats.Length; i++) + { + intensity += normalizedStats[i]; + } + + float value = Mathf.Lerp(0.55f, 0.95f, intensity / StatCount); + value = Mathf.Max(value, MinValue); + + Color color = Color.HSVToRGB(hue / 360f, saturation, value); + color.a = 1f; + return color; + } + + private static void TryRefreshRangesFromDataTables() + { + if (GameEntry.DataTable == null) + { + return; + } + + if (!s_HasMuzzleRange && + TryResolveMuzzleRange(out StatRange attackDamageRange, out StatRange damageRandomRateRange)) + { + _attackDamageRange = attackDamageRange; + _damageRandomRateRange = damageRandomRateRange; + s_HasMuzzleRange = true; + } + + if (!s_HasBearingRange && + TryResolveBearingRange(out StatRange rotateSpeedRange, out StatRange attackRangeRange)) + { + _rotateSpeedRange = rotateSpeedRange; + _attackRangeRange = attackRangeRange; + s_HasBearingRange = true; + } + + if (!s_HasBaseRange && TryResolveBaseRange(out StatRange attackSpeedRange)) + { + _attackSpeedRange = attackSpeedRange; + s_HasBaseRange = true; + } + } + + private static bool TryResolveMuzzleRange(out StatRange attackDamageRange, out StatRange damageRandomRateRange) + { + attackDamageRange = _attackDamageRange; + damageRandomRateRange = _damageRandomRateRange; + + IDataTable dataTable = GameEntry.DataTable.GetDataTable(); + if (dataTable == null || dataTable.Count <= 0) + { + return false; + } + + DRMuzzleComp[] rows = dataTable.GetAllDataRows(); + if (rows == null || rows.Length <= 0) + { + return false; + } + + bool hasAttackDamage = false; + bool hasDamageRandomRate = false; + float minAttackDamage = float.MaxValue; + float maxAttackDamage = float.MinValue; + float minDamageRandomRate = float.MaxValue; + float maxDamageRandomRate = float.MinValue; + + for (int i = 0; i < rows.Length; i++) + { + DRMuzzleComp row = rows[i]; + if (row == null) + { + continue; + } + + IncludeValues(row.AttackDamage, ref minAttackDamage, ref maxAttackDamage, ref hasAttackDamage); + IncludeValue(Mathf.Max(0f, row.DamageRandomRate), ref minDamageRandomRate, ref maxDamageRandomRate, + ref hasDamageRandomRate); + } + + if (!hasAttackDamage || !hasDamageRandomRate) + { + return false; + } + + attackDamageRange = BuildRange(minAttackDamage, maxAttackDamage, _attackDamageRange); + damageRandomRateRange = BuildRange(minDamageRandomRate, maxDamageRandomRate, _damageRandomRateRange); + return true; + } + + private static bool TryResolveBearingRange(out StatRange rotateSpeedRange, out StatRange attackRangeRange) + { + rotateSpeedRange = _rotateSpeedRange; + attackRangeRange = _attackRangeRange; + + IDataTable dataTable = GameEntry.DataTable.GetDataTable(); + if (dataTable == null || dataTable.Count <= 0) + { + return false; + } + + DRBearingComp[] rows = dataTable.GetAllDataRows(); + if (rows == null || rows.Length <= 0) + { + return false; + } + + bool hasRotateSpeed = false; + bool hasAttackRange = false; + float minRotateSpeed = float.MaxValue; + float maxRotateSpeed = float.MinValue; + float minAttackRange = float.MaxValue; + float maxAttackRange = float.MinValue; + + for (int i = 0; i < rows.Length; i++) + { + DRBearingComp row = rows[i]; + if (row == null) + { + continue; + } + + IncludeValues(row.RotateSpeed, ref minRotateSpeed, ref maxRotateSpeed, ref hasRotateSpeed); + IncludeValues(row.AttackRange, ref minAttackRange, ref maxAttackRange, ref hasAttackRange); + } + + if (!hasRotateSpeed || !hasAttackRange) + { + return false; + } + + rotateSpeedRange = BuildRange(minRotateSpeed, maxRotateSpeed, _rotateSpeedRange); + attackRangeRange = BuildRange(minAttackRange, maxAttackRange, _attackRangeRange); + return true; + } + + private static bool TryResolveBaseRange(out StatRange attackSpeedRange) + { + attackSpeedRange = _attackSpeedRange; + + IDataTable dataTable = GameEntry.DataTable.GetDataTable(); + if (dataTable == null || dataTable.Count <= 0) + { + return false; + } + + DRBaseComp[] rows = dataTable.GetAllDataRows(); + if (rows == null || rows.Length <= 0) + { + return false; + } + + bool hasAttackSpeed = false; + float minAttackSpeed = float.MaxValue; + float maxAttackSpeed = float.MinValue; + + for (int i = 0; i < rows.Length; i++) + { + DRBaseComp row = rows[i]; + if (row == null) + { + continue; + } + + IncludeValues(row.AttackSpeed, ref minAttackSpeed, ref maxAttackSpeed, ref hasAttackSpeed); + } + + if (!hasAttackSpeed) + { + return false; + } + + attackSpeedRange = BuildRange(minAttackSpeed, maxAttackSpeed, _attackSpeedRange); + return true; + } + + private static void FindTopTwoIndexes(float[] values, out int primaryIndex, out int secondaryIndex) + { + primaryIndex = 0; + secondaryIndex = 1; + + if (values[secondaryIndex] > values[primaryIndex]) + { + (primaryIndex, secondaryIndex) = (secondaryIndex, primaryIndex); + } + + for (int i = 2; i < values.Length; i++) + { + float value = values[i]; + if (value > values[primaryIndex]) + { + secondaryIndex = primaryIndex; + primaryIndex = i; + } + else if (value > values[secondaryIndex]) + { + secondaryIndex = i; + } + } + } + + private static float ResolveSlotHueOffset(TowerCompSlotType slotType) + { + return slotType switch + { + TowerCompSlotType.Bearing => 15f, + TowerCompSlotType.Base => -15f, + _ => 0f + }; + } + + private static float ResolveArrayValue(int[] values) + { + if (values == null || values.Length <= 0) + { + return 0f; + } + + return Mathf.Max(0f, values[0]); + } + + private static float ResolveArrayValue(float[] values) + { + if (values == null || values.Length <= 0) + { + return 0f; + } + + return Mathf.Max(0f, values[0]); + } + + private static void IncludeValues(int[] values, ref float min, ref float max, ref bool hasValue) + { + if (values == null || values.Length <= 0) + { + return; + } + + for (int i = 0; i < values.Length; i++) + { + IncludeValue(Mathf.Max(0f, values[i]), ref min, ref max, ref hasValue); + } + } + + private static void IncludeValues(float[] values, ref float min, ref float max, ref bool hasValue) + { + if (values == null || values.Length <= 0) + { + return; + } + + for (int i = 0; i < values.Length; i++) + { + IncludeValue(Mathf.Max(0f, values[i]), ref min, ref max, ref hasValue); + } + } + + private static void IncludeValue(float value, ref float min, ref float max, ref bool hasValue) + { + if (float.IsNaN(value) || float.IsInfinity(value)) + { + return; + } + + if (!hasValue) + { + min = value; + max = value; + hasValue = true; + return; + } + + if (value < min) + { + min = value; + } + + if (value > max) + { + max = value; + } + } + + private static float Normalize(float value, StatRange range) + { + float denominator = Mathf.Max(TinyValue, range.Max - range.Min); + return Mathf.Clamp01((value - range.Min) / denominator); + } + + private static StatRange BuildRange(float min, float max, StatRange fallback) + { + if (float.IsNaN(min) || float.IsInfinity(min) || float.IsNaN(max) || float.IsInfinity(max)) + { + return fallback; + } + + if (max - min < TinyValue) + { + return fallback; + } + + return new StatRange(min, max); + } + + private static float LerpHue(float fromDegree, float toDegree, float t) + { + float delta = Mathf.DeltaAngle(fromDegree, toDegree); + return fromDegree + delta * Mathf.Clamp01(t); + } + + private static float NormalizeHue(float hueDegree) + { + float normalized = hueDegree % 360f; + if (normalized < 0f) + { + normalized += 360f; + } + + return normalized; + } + + private readonly struct StatRange + { + public StatRange(float min, float max) + { + Min = min; + Max = max; + } + + public float Min { get; } + public float Max { get; } + } + } +} \ No newline at end of file diff --git a/src-ref/Utility/InventoryCloneUtility.cs b/src-ref/Utility/InventoryCloneUtility.cs new file mode 100644 index 0000000..1cb124f --- /dev/null +++ b/src-ref/Utility/InventoryCloneUtility.cs @@ -0,0 +1,236 @@ +using System; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.CustomUtility +{ + public static class InventoryCloneUtility + { + public static BackpackInventoryData CloneInventory(BackpackInventoryData source) + { + BackpackInventoryData cloned = new BackpackInventoryData(); + if (source == null) + { + return cloned; + } + + cloned.Gold = Mathf.Max(0, source.Gold); + + if (source.MuzzleComponents != null) + { + for (int i = 0; i < source.MuzzleComponents.Count; i++) + { + MuzzleCompItemData item = source.MuzzleComponents[i]; + if (item != null) + { + cloned.MuzzleComponents.Add(CloneMuzzleComp(item)); + } + } + } + + if (source.BearingComponents != null) + { + for (int i = 0; i < source.BearingComponents.Count; i++) + { + BearingCompItemData item = source.BearingComponents[i]; + if (item != null) + { + cloned.BearingComponents.Add(CloneBearingComp(item)); + } + } + } + + if (source.BaseComponents != null) + { + for (int i = 0; i < source.BaseComponents.Count; i++) + { + BaseCompItemData item = source.BaseComponents[i]; + if (item != null) + { + cloned.BaseComponents.Add(CloneBaseComp(item)); + } + } + } + + if (source.Towers != null) + { + for (int i = 0; i < source.Towers.Count; i++) + { + TowerItemData item = source.Towers[i]; + if (item != null) + { + cloned.Towers.Add(CloneTower(item)); + } + } + } + + if (source.ParticipantTowerInstanceIds != null) + { + for (int i = 0; i < source.ParticipantTowerInstanceIds.Count; i++) + { + long id = source.ParticipantTowerInstanceIds[i]; + if (id > 0) + { + cloned.ParticipantTowerInstanceIds.Add(id); + } + } + } + + return cloned; + } + + public static MuzzleCompItemData CloneMuzzleComp(MuzzleCompItemData source) + { + if (source == null) + { + return null; + } + + return new MuzzleCompItemData + { + InstanceId = source.InstanceId, + ConfigId = source.ConfigId, + Name = source.Name, + Rarity = source.Rarity, + Endurance = source.Endurance, + IsAssembledIntoTower = source.IsAssembledIntoTower, + Constraint = source.Constraint, + Tags = CloneTags(source.Tags), + AttackDamage = CloneIntArray(source.AttackDamage), + DamageRandomRate = source.DamageRandomRate, + AttackMethodType = source.AttackMethodType + }; + } + + public static BearingCompItemData CloneBearingComp(BearingCompItemData source) + { + if (source == null) + { + return null; + } + + return new BearingCompItemData + { + InstanceId = source.InstanceId, + ConfigId = source.ConfigId, + Name = source.Name, + Rarity = source.Rarity, + Endurance = source.Endurance, + IsAssembledIntoTower = source.IsAssembledIntoTower, + Constraint = source.Constraint, + Tags = CloneTags(source.Tags), + RotateSpeed = CloneFloatArray(source.RotateSpeed), + AttackRange = CloneFloatArray(source.AttackRange) + }; + } + + public static BaseCompItemData CloneBaseComp(BaseCompItemData source) + { + if (source == null) + { + return null; + } + + return new BaseCompItemData + { + InstanceId = source.InstanceId, + ConfigId = source.ConfigId, + Name = source.Name, + Rarity = source.Rarity, + Endurance = source.Endurance, + IsAssembledIntoTower = source.IsAssembledIntoTower, + Constraint = source.Constraint, + Tags = CloneTags(source.Tags), + AttackSpeed = CloneFloatArray(source.AttackSpeed), + AttackPropertyType = source.AttackPropertyType + }; + } + + public static TowerItemData CloneTower(TowerItemData source) + { + if (source == null) + { + return null; + } + + return new TowerItemData + { + InstanceId = source.InstanceId, + Name = source.Name, + Rarity = source.Rarity, + IsParticipatingInCombat = source.IsParticipatingInCombat, + MuzzleComponentInstanceId = source.MuzzleComponentInstanceId, + BearingComponentInstanceId = source.BearingComponentInstanceId, + BaseComponentInstanceId = source.BaseComponentInstanceId, + Stats = CloneTowerStats(source.Stats), + ComposedIconSprite = source.ComposedIconSprite, + ComposedIconKey = source.ComposedIconKey + }; + } + + public static TowerStatsData CloneTowerStats(TowerStatsData source) + { + if (source == null) + { + return new TowerStatsData(); + } + + return new TowerStatsData + { + AttackDamage = CloneIntArray(source.AttackDamage), + DamageRandomRate = source.DamageRandomRate, + RotateSpeed = CloneFloatArray(source.RotateSpeed), + AttackRange = CloneFloatArray(source.AttackRange), + AttackSpeed = CloneFloatArray(source.AttackSpeed), + AttackMethodType = source.AttackMethodType, + AttackPropertyType = source.AttackPropertyType, + TagRuntimes = CloneTagRuntimes(source.TagRuntimes), + Tags = CloneTags(source.Tags) + }; + } + + public static int[] CloneIntArray(int[] source) + { + return source != null ? (int[])source.Clone() : Array.Empty(); + } + + public static float[] CloneFloatArray(float[] source) + { + return source != null ? (float[])source.Clone() : Array.Empty(); + } + + public static TagType[] CloneTags(TagType[] source) + { + return source != null ? (TagType[])source.Clone() : Array.Empty(); + } + + public static TagRuntimeData[] CloneTagRuntimes(TagRuntimeData[] source) + { + if (source == null || source.Length <= 0) + { + return Array.Empty(); + } + + 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; + } + } + + +} + diff --git a/src-ref/Utility/InventoryParticipantUtility.cs b/src-ref/Utility/InventoryParticipantUtility.cs new file mode 100644 index 0000000..19f9dea --- /dev/null +++ b/src-ref/Utility/InventoryParticipantUtility.cs @@ -0,0 +1,180 @@ +using System.Collections.Generic; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.CustomUtility +{ + public static class InventoryParticipantUtility + { + public static ParticipantTowerAssignResult TryAddParticipantTower( + BackpackInventoryData inventory, + long towerInstanceId, + int maxCount) + { + if (inventory == null || towerInstanceId <= 0) + { + return new ParticipantTowerAssignResult + { + TowerInstanceId = towerInstanceId, + FailureReason = ParticipantTowerAssignFailureReason.TowerMissing, + ValidationFailureReason = CombatParticipantTowerValidationFailureReason.None + }; + } + + int resolvedMaxCount = Mathf.Max(1, maxCount); + NormalizeParticipantState(inventory, resolvedMaxCount); + CombatParticipantTowerValidationResult validationResult = + CombatParticipantTowerValidationService.ValidateTower(inventory, towerInstanceId); + if (!validationResult.IsValid) + { + return new ParticipantTowerAssignResult + { + TowerInstanceId = towerInstanceId, + FailureReason = validationResult.FailureReason == CombatParticipantTowerValidationFailureReason.TowerMissing + ? ParticipantTowerAssignFailureReason.TowerMissing + : ParticipantTowerAssignFailureReason.InvalidTower, + ValidationFailureReason = validationResult.FailureReason + }; + } + + inventory.ParticipantTowerInstanceIds ??= new List(); + if (inventory.ParticipantTowerInstanceIds.Contains(towerInstanceId)) + { + if (validationResult.Tower != null) + { + validationResult.Tower.IsParticipatingInCombat = true; + } + + return new ParticipantTowerAssignResult + { + TowerInstanceId = towerInstanceId, + FailureReason = ParticipantTowerAssignFailureReason.AlreadyAssigned, + ValidationFailureReason = CombatParticipantTowerValidationFailureReason.None + }; + } + + if (inventory.ParticipantTowerInstanceIds.Count >= resolvedMaxCount) + { + return new ParticipantTowerAssignResult + { + TowerInstanceId = towerInstanceId, + FailureReason = ParticipantTowerAssignFailureReason.ParticipantAreaFull, + ValidationFailureReason = CombatParticipantTowerValidationFailureReason.None + }; + } + + inventory.ParticipantTowerInstanceIds.Add(towerInstanceId); + if (validationResult.Tower != null) + { + validationResult.Tower.IsParticipatingInCombat = true; + } + + return new ParticipantTowerAssignResult + { + TowerInstanceId = towerInstanceId, + FailureReason = ParticipantTowerAssignFailureReason.None, + ValidationFailureReason = CombatParticipantTowerValidationFailureReason.None + }; + } + + public static bool TryRemoveParticipantTower(BackpackInventoryData inventory, long towerInstanceId, int maxCount) + { + if (inventory == null || towerInstanceId <= 0) + { + return false; + } + + NormalizeParticipantState(inventory, maxCount); + if (inventory.ParticipantTowerInstanceIds == null) + { + return false; + } + + bool removed = inventory.ParticipantTowerInstanceIds.Remove(towerInstanceId); + if (!removed) + { + return false; + } + + if (TryGetTowerById(inventory, towerInstanceId, out TowerItemData tower)) + { + tower.IsParticipatingInCombat = false; + } + + return true; + } + + public static void NormalizeParticipantState(BackpackInventoryData inventory, int maxCount) + { + if (inventory == null) + { + return; + } + + int resolvedMaxCount = Mathf.Max(1, maxCount); + inventory.ParticipantTowerInstanceIds ??= new List(); + Dictionary towerMap = new Dictionary(); + if (inventory.Towers != null) + { + for (int i = 0; i < inventory.Towers.Count; i++) + { + TowerItemData tower = inventory.Towers[i]; + if (tower == null || tower.InstanceId <= 0) + { + continue; + } + + tower.IsParticipatingInCombat = false; + towerMap[tower.InstanceId] = tower; + } + } + + HashSet uniqueIds = new HashSet(); + List normalizedIds = new List(inventory.ParticipantTowerInstanceIds.Count); + for (int i = 0; i < inventory.ParticipantTowerInstanceIds.Count; i++) + { + if (normalizedIds.Count >= resolvedMaxCount) + { + break; + } + + long towerId = inventory.ParticipantTowerInstanceIds[i]; + if (towerId <= 0 || !uniqueIds.Add(towerId)) + { + continue; + } + + if (!towerMap.TryGetValue(towerId, out TowerItemData tower)) + { + continue; + } + + tower.IsParticipatingInCombat = true; + normalizedIds.Add(towerId); + } + + inventory.ParticipantTowerInstanceIds = normalizedIds; + } + + public 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; + } + } +} diff --git a/src-ref/Utility/InventorySeedUtility.cs b/src-ref/Utility/InventorySeedUtility.cs new file mode 100644 index 0000000..25fa744 --- /dev/null +++ b/src-ref/Utility/InventorySeedUtility.cs @@ -0,0 +1,198 @@ +using GeometryTD.Definition; + +namespace GeometryTD.CustomUtility +{ + public static class InventorySeedUtility + { + private static readonly TagType[] FirePool = { TagType.Fire }; + private static readonly TagType[] IceFreezePool = { TagType.Ice, TagType.FreezeMask }; + private static readonly TagType[] CritPiercePool = { TagType.Pierce, TagType.Crit }; + private static readonly TagType[] IceShatterPool = { TagType.Ice, TagType.Shatter }; + private static readonly TagType[] PierceOverpenetratePool = { TagType.Pierce, TagType.Overpenetrate }; + private static readonly TagType[] IceAbsoluteZeroPool = { TagType.Ice, TagType.AbsoluteZero }; + private static readonly TagType[] PierceExecutionPool = { TagType.Pierce, TagType.Execution }; + + public static BackpackInventoryData CreateSampleInventory(int runSeed = 0) + { + BackpackInventoryData inventory = new BackpackInventoryData + { + Gold = 500 + }; + + MuzzleCompItemData muzzle = new MuzzleCompItemData + { + InstanceId = 10001, + ConfigId = 1, + Name = "元素枪口", + Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Blue), + Endurance = 90f, + IsAssembledIntoTower = true, + AttackDamage = new[] { 200, 300, 400, 500, 800 }, + DamageRandomRate = 0.05f, + AttackMethodType = AttackMethodType.NormalBullet, + Constraint = string.Empty, + Tags = ResolveSeedTags(FirePool, RarityType.Blue, runSeed, 10001, 1) + }; + + BearingCompItemData bearing = new BearingCompItemData + { + InstanceId = 20001, + ConfigId = 1, + Name = "元素轴承", + Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Blue), + Endurance = 1f, + IsAssembledIntoTower = true, + RotateSpeed = new[] { 100f, 120f, 130f, 140f, 150f }, + AttackRange = new[] { 3f, 4f, 5f, 6f, 8f }, + Constraint = string.Empty, + Tags = ResolveSeedTags(FirePool, RarityType.Blue, runSeed, 20001, 1) + }; + + BaseCompItemData baseComp = new BaseCompItemData + { + InstanceId = 30001, + ConfigId = 1, + Name = "元素底座", + Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Blue), + Endurance = 88f, + IsAssembledIntoTower = true, + AttackSpeed = new[] { 1f, 2f, 3f, 3.5f, 0.7f }, + AttackPropertyType = AttackPropertyType.Fire, + Constraint = string.Empty, + Tags = ResolveSeedTags(FirePool, RarityType.Blue, runSeed, 30001, 1) + }; + + TowerItemData tower = new TowerItemData + { + InstanceId = 90001, + Name = "测试防御塔-A", + Rarity = InventoryRarityRuleService.ResolveTowerRarity( + muzzle.Rarity, + bearing.Rarity, + baseComp.Rarity), + IsParticipatingInCombat = true, + MuzzleComponentInstanceId = muzzle.InstanceId, + BearingComponentInstanceId = bearing.InstanceId, + BaseComponentInstanceId = baseComp.InstanceId, + Stats = new TowerStatsData + { + AttackDamage = new[] { 200, 220, 240, 260, 300 }, + DamageRandomRate = 0f, + RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f }, + AttackRange = new[] { 4.5f, 4.5f, 4.5f, 4.5f, 4.5f }, + AttackSpeed = new[] { 1.0f, 1.2f, 1.3f, 1.4f, 0.5f }, + AttackMethodType = AttackMethodType.NormalBullet, + AttackPropertyType = AttackPropertyType.Fire, + TagRuntimes = TowerTagAggregationService.AggregateTowerTags( + muzzle.Tags, + bearing.Tags, + baseComp.Tags) + } + }; + tower.Stats.Tags = TowerTagAggregationService.FlattenUniqueTags(tower.Stats.TagRuntimes); + + inventory.MuzzleComponents.Add(muzzle); + inventory.BaseComponents.Add(baseComp); + inventory.BearingComponents.Add(bearing); + inventory.Towers.Add(tower); + + inventory.MuzzleComponents.Add(new MuzzleCompItemData + { + InstanceId = 10002, + ConfigId = 2, + Name = "控制枪口", + Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Blue), + Endurance = 80, + IsAssembledIntoTower = false, + AttackDamage = new[] { 200, 300, 400, 500, 600 }, + DamageRandomRate = 0.01f, + AttackMethodType = AttackMethodType.NormalBullet, + Constraint = string.Empty, + Tags = ResolveSeedTags(IceFreezePool, RarityType.Blue, runSeed, 10002, 2) + }); + + inventory.MuzzleComponents.Add(new MuzzleCompItemData + { + InstanceId = 10003, + ConfigId = 3, + Name = "穿透枪口", + Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Purple), + Endurance = 1f, + IsAssembledIntoTower = false, + AttackDamage = new[] { 50, 55, 60, 80, 90 }, + DamageRandomRate = 0.02f, + AttackMethodType = AttackMethodType.NormalBullet, + Constraint = string.Empty, + Tags = ResolveSeedTags(CritPiercePool, RarityType.Purple, runSeed, 10003, 3) + }); + + inventory.BearingComponents.Add(new BearingCompItemData + { + InstanceId = 20002, + ConfigId = 2, + Name = "控制轴承", + Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Blue), + Endurance = 20, + IsAssembledIntoTower = false, + RotateSpeed = new[] { 200f, 250f, 300f, 320f, 350f }, + AttackRange = new[] { 6f, 6.5f, 7f, 8f, 8f }, + Constraint = string.Empty, + Tags = ResolveSeedTags(IceShatterPool, RarityType.Blue, runSeed, 20002, 2) + }); + + inventory.BearingComponents.Add(new BearingCompItemData + { + InstanceId = 20003, + ConfigId = 3, + Name = "穿透轴承", + Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Purple), + Endurance = 96f, + IsAssembledIntoTower = false, + RotateSpeed = new[] { 60f, 70f, 80f, 90f, 100f }, + AttackRange = new[] { 4f, 4.5f, 5f, 5.5f, 6f }, + Constraint = string.Empty, + Tags = ResolveSeedTags(PierceOverpenetratePool, RarityType.Purple, runSeed, 20003, 3) + }); + + inventory.BaseComponents.Add(new BaseCompItemData + { + InstanceId = 30002, + ConfigId = 2, + Name = "控制底座", + Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Blue), + Endurance = 50f, + IsAssembledIntoTower = false, + AttackSpeed = new[] { 4f, 4.2f, 4.4f, 4.6f, 4.8f }, + AttackPropertyType = AttackPropertyType.Ice, + Constraint = string.Empty, + Tags = ResolveSeedTags(IceAbsoluteZeroPool, RarityType.Blue, runSeed, 30002, 2) + }); + + inventory.BaseComponents.Add(new BaseCompItemData + { + InstanceId = 30003, + ConfigId = 3, + Name = "穿透底座", + Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Purple), + Endurance = 30f, + IsAssembledIntoTower = false, + AttackSpeed = new[] { 1f, 1f, 1f, 1f, 1f }, + AttackPropertyType = AttackPropertyType.Physics, + Constraint = string.Empty, + Tags = ResolveSeedTags(PierceExecutionPool, RarityType.Purple, runSeed, 30003, 3) + }); + + inventory.ParticipantTowerInstanceIds.Add(90001); + + return inventory; + } + + private static TagType[] ResolveSeedTags(TagType[] possibleTags, RarityType rarity, int runSeed, long instanceId, int configId) + { + return ComponentTagGenerationService.ResolveComponentTags( + possibleTags, + rarity, + InventoryTagRandomContext.CreateSeed(runSeed, instanceId, configId)); + } + } +} diff --git a/src-ref/Utility/ItemDescUtility.cs b/src-ref/Utility/ItemDescUtility.cs new file mode 100644 index 0000000..5fa5888 --- /dev/null +++ b/src-ref/Utility/ItemDescUtility.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.Text; +using GeometryTD.Definition; + +namespace GeometryTD.CustomUtility +{ + public static class ItemDescUtility + { + public static string BuildTowerDesc(TowerStatsData towerData) + { + StringBuilder sb = new StringBuilder(); + + // MuzzleComp + sb.Append("攻击伤害:"); + for (int i = 0; i < towerData.AttackDamage.Length; i++) + { + if (i != 0) sb.Append("|"); + sb.Append(towerData.AttackDamage[i]); + } + sb.Append('\n'); + + sb.Append($"伤害浮动:{towerData.DamageRandomRate:P0}\n"); + + sb.Append($"攻击方式:{ConvertAttackMethod(towerData.AttackMethodType)}\n"); + + // BearingComp + sb.Append("旋转速度:"); + for (int i = 0; i < towerData.RotateSpeed.Length; i++) + { + if (i != 0) sb.Append("|"); + sb.Append(towerData.RotateSpeed[i]); + } + sb.Append('\n'); + + sb.Append("攻击范围:"); + for (int i = 0; i < towerData.AttackRange.Length; i++) + { + if (i != 0) sb.Append("|"); + sb.Append(towerData.AttackRange[i]); + } + sb.Append('\n'); + + // BaseComp + sb.Append("攻击速度:"); + for (int i = 0; i < towerData.AttackSpeed.Length; i++) + { + if (i != 0) sb.Append("|"); + sb.Append(towerData.AttackSpeed[i]); + } + sb.Append('\n'); + + sb.Append($"伤害属性:{ConvertAttackProperty(towerData.AttackPropertyType)}\n"); + + return sb.ToString(); + } + + public static string BuildTowerDesc( + TowerItemData tower, + IReadOnlyDictionary muzzleMap, + IReadOnlyDictionary bearingMap, + IReadOnlyDictionary baseMap) + { + if (tower == null) + { + return string.Empty; + } + + StringBuilder sb = new StringBuilder(); + sb.Append(BuildTowerDesc(tower.Stats ?? new TowerStatsData())); + float enduranceRate = ResolveTowerEnduranceRate(tower, muzzleMap, bearingMap, baseMap); + sb.AppendLine($"平均耐久: {enduranceRate * 100f:0.#}"); + CombatParticipantTowerValidationFailureReason failureReason = + ResolveTowerValidationFailureReason(tower, muzzleMap, bearingMap, baseMap); + if (failureReason != CombatParticipantTowerValidationFailureReason.None) + { + sb.AppendLine("状态:已损坏"); + sb.Append($"参战限制:{CombatParticipantTowerValidationText.GetFailureReasonMessage(failureReason)}"); + } + + return sb.ToString(); + } + + public static float ResolveComponentEnduranceRate(TowerCompItemData item) + { + if (item == null) + { + return 1f; + } + + return UnityEngine.Mathf.Clamp01(item.Endurance / 100f); + } + + public static float ResolveTowerEnduranceRate( + TowerItemData tower, + IReadOnlyDictionary muzzleMap, + IReadOnlyDictionary bearingMap, + IReadOnlyDictionary baseMap) + { + if (tower == null) + { + return 1f; + } + + float sum = 0f; + int count = 0; + if (muzzleMap != null && muzzleMap.TryGetValue(tower.MuzzleComponentInstanceId, out MuzzleCompItemData muzzle)) + { + sum += ResolveComponentEnduranceRate(muzzle); + count++; + } + + if (bearingMap != null && bearingMap.TryGetValue(tower.BearingComponentInstanceId, out BearingCompItemData bearing)) + { + sum += ResolveComponentEnduranceRate(bearing); + count++; + } + + if (baseMap != null && baseMap.TryGetValue(tower.BaseComponentInstanceId, out BaseCompItemData baseComp)) + { + sum += ResolveComponentEnduranceRate(baseComp); + count++; + } + + if (count <= 0) + { + return 1f; + } + + return UnityEngine.Mathf.Clamp01(sum / count); + } + + public static string BuildMuzzleDesc(MuzzleCompItemData muzzleData) + { + StringBuilder sb = new StringBuilder(); + + sb.Append("攻击伤害:"); + for (int i = 0; i < muzzleData.AttackDamage.Length; i++) + { + if (i != 0) sb.Append("|"); + sb.Append(muzzleData.AttackDamage[i]); + } + sb.Append('\n'); + + sb.AppendLine($"伤害浮动:{muzzleData.DamageRandomRate:P0}"); + + sb.AppendLine($"攻击方式:{ConvertAttackMethod(muzzleData.AttackMethodType)}"); + + sb.AppendLine($"当前耐久:{muzzleData.Endurance}"); + AppendComponentBrokenStatus(sb, muzzleData); + + return sb.ToString(); + } + + public static string BuildBearingDesc(BearingCompItemData bearingData) + { + StringBuilder sb = new StringBuilder(); + + sb.Append("旋转速度:"); + for (int i = 0; i < bearingData.RotateSpeed.Length; i++) + { + if (i != 0) sb.Append("|"); + sb.Append(bearingData.RotateSpeed[i]); + } + sb.Append('\n'); + + sb.Append("攻击范围:"); + for (int i = 0; i < bearingData.AttackRange.Length; i++) + { + if (i != 0) sb.Append("|"); + sb.Append(bearingData.AttackRange[i]); + } + sb.Append('\n'); + + sb.AppendLine($"当前耐久:{bearingData.Endurance}"); + AppendComponentBrokenStatus(sb, bearingData); + + return sb.ToString(); + } + + public static string BuildBaseDesc(BaseCompItemData baseData) + { + StringBuilder sb = new StringBuilder(); + + sb.Append("攻击速度:"); + for (int i = 0; i < baseData.AttackSpeed.Length; i++) + { + if (i != 0) sb.Append("|"); + sb.Append(baseData.AttackSpeed[i]); + } + sb.Append('\n'); + + sb.AppendLine($"伤害属性:{ConvertAttackProperty(baseData.AttackPropertyType)}"); + + sb.AppendLine($"当前耐久:{baseData.Endurance}"); + AppendComponentBrokenStatus(sb, baseData); + + return sb.ToString(); + } + + private static void AppendComponentBrokenStatus(StringBuilder sb, TowerCompItemData component) + { + if (sb == null || component == null || component.Endurance > 0f) + { + return; + } + + sb.AppendLine("状态:已损坏,无法参战。"); + } + + private static CombatParticipantTowerValidationFailureReason ResolveTowerValidationFailureReason( + TowerItemData tower, + IReadOnlyDictionary muzzleMap, + IReadOnlyDictionary bearingMap, + IReadOnlyDictionary baseMap) + { + if (tower == null) + { + return CombatParticipantTowerValidationFailureReason.TowerMissing; + } + + if (muzzleMap == null || !muzzleMap.TryGetValue(tower.MuzzleComponentInstanceId, out MuzzleCompItemData muzzle)) + { + return CombatParticipantTowerValidationFailureReason.MissingMuzzleComponent; + } + + if (muzzle.Endurance <= 0f) + { + return CombatParticipantTowerValidationFailureReason.BrokenMuzzleComponent; + } + + if (bearingMap == null || !bearingMap.TryGetValue(tower.BearingComponentInstanceId, out BearingCompItemData bearing)) + { + return CombatParticipantTowerValidationFailureReason.MissingBearingComponent; + } + + if (bearing.Endurance <= 0f) + { + return CombatParticipantTowerValidationFailureReason.BrokenBearingComponent; + } + + if (baseMap == null || !baseMap.TryGetValue(tower.BaseComponentInstanceId, out BaseCompItemData baseComp)) + { + return CombatParticipantTowerValidationFailureReason.MissingBaseComponent; + } + + if (baseComp.Endurance <= 0f) + { + return CombatParticipantTowerValidationFailureReason.BrokenBaseComponent; + } + + return CombatParticipantTowerValidationFailureReason.None; + } + + public static string ConvertAttackMethod(AttackMethodType type) + { + return type switch + { + AttackMethodType.None => "无效", + AttackMethodType.NormalBullet => "发送子弹", + AttackMethodType.Range => "范围攻击", + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + } + + public static string ConvertAttackProperty(AttackPropertyType type) + { + return type switch + { + AttackPropertyType.None => "无效", + AttackPropertyType.Physics => "物理", //yellow + AttackPropertyType.Fire => "火", //red + AttackPropertyType.Water => "水", //cyan + AttackPropertyType.Earth => "自然", //lime + AttackPropertyType.Poison => "毒", //green + AttackPropertyType.Ice => "冰", //darkblue + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + } + } +} + diff --git a/src-ref/Utility/JsonNetUtility.cs b/src-ref/Utility/JsonNetUtility.cs new file mode 100644 index 0000000..abe08ce --- /dev/null +++ b/src-ref/Utility/JsonNetUtility.cs @@ -0,0 +1,26 @@ +using System; +using Newtonsoft.Json; + +namespace GeometryTD.CustomUtility +{ + /// + /// Newtonsoft.Json 函数集辅助器。 + /// + public class JsonNetUtility : GameFramework.Utility.Json.IJsonHelper + { + public string ToJson(object obj) + { + return JsonConvert.SerializeObject(obj); + } + + public T ToObject(string json) + { + return JsonConvert.DeserializeObject(json); + } + + public object ToObject(Type objectType, string json) + { + return JsonConvert.DeserializeObject(json, objectType); + } + } +} \ No newline at end of file diff --git a/src-ref/Utility/ShopPriceRuleService.cs b/src-ref/Utility/ShopPriceRuleService.cs new file mode 100644 index 0000000..69ec2b2 --- /dev/null +++ b/src-ref/Utility/ShopPriceRuleService.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using GameFramework.DataTable; +using GeometryTD.DataTable; +using GeometryTD.Definition; +using UnityEngine; +using Random = System.Random; + +namespace GeometryTD.CustomUtility +{ + public static class ShopPriceRuleService + { + public static int ResolveRandomBuyPrice(IReadOnlyList shopPriceRows, RarityType rarity, Random random) + { + if (!TryFindPriceRow(shopPriceRows, rarity, out DRShopPrice row) || row == null) + { + return 0; + } + + int min = Mathf.Max(0, row.MinPrice); + int max = Mathf.Max(min, row.MaxPrice); + return random != null ? random.Next(min, max + 1) : min; + } + + public static int ResolveComponentSalePrice(TowerCompItemData component, IDataTable shopPriceTable = null) + { + if (component == null) + { + return 0; + } + + return ResolveBasePrice(component.Rarity, shopPriceTable); + } + + public static bool TryResolveTowerSalePrice( + TowerItemData tower, + BackpackInventoryData inventory, + out int price, + IDataTable shopPriceTable = null) + { + price = 0; + if (tower == null || inventory == null) + { + return false; + } + + if (!TryGetComponentById(inventory.MuzzleComponents, tower.MuzzleComponentInstanceId, out MuzzleCompItemData muzzleComp) || + !TryGetComponentById(inventory.BearingComponents, tower.BearingComponentInstanceId, out BearingCompItemData bearingComp) || + !TryGetComponentById(inventory.BaseComponents, tower.BaseComponentInstanceId, out BaseCompItemData baseComp)) + { + return false; + } + + price = ResolveComponentSalePrice(muzzleComp, shopPriceTable) + + ResolveComponentSalePrice(bearingComp, shopPriceTable) + + ResolveComponentSalePrice(baseComp, shopPriceTable); + return price > 0; + } + + public static int ResolveBasePrice(RarityType rarity, IDataTable shopPriceTable = null) + { + IDataTable resolvedTable = shopPriceTable ?? GameEntry.DataTable.GetDataTable(); + if (resolvedTable == null) + { + return 0; + } + + DRShopPrice[] rows = resolvedTable.GetAllDataRows(); + if (!TryFindPriceRow(rows, rarity, out DRShopPrice row) || row == null) + { + return 0; + } + + int min = Mathf.Max(0, row.MinPrice); + int max = Mathf.Max(min, row.MaxPrice); + return Mathf.RoundToInt((min + max) * 0.5f); + } + + private static bool TryFindPriceRow(IReadOnlyList rows, RarityType rarity, out DRShopPrice result) + { + result = null; + if (rows == null) + { + return false; + } + + for (int i = 0; i < rows.Count; i++) + { + DRShopPrice row = rows[i]; + if (row != null && row.Rarity == rarity) + { + result = row; + return true; + } + } + + return false; + } + + private static bool TryGetComponentById(IReadOnlyList 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; + } + } +} diff --git a/src-ref/Utility/TowerComposedIconCacheUtility.cs b/src-ref/Utility/TowerComposedIconCacheUtility.cs new file mode 100644 index 0000000..480147a --- /dev/null +++ b/src-ref/Utility/TowerComposedIconCacheUtility.cs @@ -0,0 +1,130 @@ +using System.Collections.Generic; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.CustomUtility +{ + public static class TowerComposedIconCacheUtility + { + private const string MuzzleAssetName = "Muzzle"; + private const string BearingAssetName = "Bearing"; + private const string BaseAssetName = "Base"; + + private static Sprite _muzzleSprite; + private static Sprite _bearingSprite; + private static Sprite _baseSprite; + private static bool s_RequestedMuzzle; + private static bool s_RequestedBearing; + private static bool s_RequestedBase; + + public static Sprite ResolveTowerIconSprite( + TowerItemData tower, + IReadOnlyDictionary muzzleMap, + IReadOnlyDictionary bearingMap, + IReadOnlyDictionary baseMap) + { + if (tower == null) + { + return null; + } + + if (!TryGetComponents(tower, muzzleMap, bearingMap, baseMap, + out MuzzleCompItemData muzzleComp, + out BearingCompItemData bearingComp, + out BaseCompItemData baseComp)) + { + return null; + } + + Color muzzleColor = IconColorGenerator.GenerateForComponent(muzzleComp); + Color bearingColor = IconColorGenerator.GenerateForComponent(bearingComp); + Color baseColor = IconColorGenerator.GenerateForComponent(baseComp); + + string cacheKey = TowerIconComposeUtility.BuildCacheKey( + tower.MuzzleComponentInstanceId, + tower.BearingComponentInstanceId, + tower.BaseComponentInstanceId, + muzzleColor, + bearingColor, + baseColor); + + if (tower.ComposedIconSprite != null && string.Equals(tower.ComposedIconKey, cacheKey)) + { + return tower.ComposedIconSprite; + } + + EnsureBaseSpritesRequested(); + if (_muzzleSprite == null || _bearingSprite == null || _baseSprite == null) + { + return null; + } + + Sprite composedSprite = TowerIconComposeUtility.Compose( + _muzzleSprite, + muzzleColor, + _bearingSprite, + bearingColor, + _baseSprite, + baseColor); + + if (composedSprite == null) + { + return null; + } + + tower.ComposedIconSprite = composedSprite; + tower.ComposedIconKey = cacheKey; + return composedSprite; + } + + private static bool TryGetComponents( + TowerItemData tower, + IReadOnlyDictionary muzzleMap, + IReadOnlyDictionary bearingMap, + IReadOnlyDictionary baseMap, + out MuzzleCompItemData muzzleComp, + out BearingCompItemData bearingComp, + out BaseCompItemData baseComp) + { + muzzleComp = null; + bearingComp = null; + baseComp = null; + + if (tower == null || muzzleMap == null || bearingMap == null || baseMap == null) + { + return false; + } + + return muzzleMap.TryGetValue(tower.MuzzleComponentInstanceId, out muzzleComp) && + bearingMap.TryGetValue(tower.BearingComponentInstanceId, out bearingComp) && + baseMap.TryGetValue(tower.BaseComponentInstanceId, out baseComp) && + muzzleComp != null && bearingComp != null && baseComp != null; + } + + private static void EnsureBaseSpritesRequested() + { + if (GameEntry.SpriteCache == null) + { + return; + } + + if (!s_RequestedMuzzle) + { + s_RequestedMuzzle = true; + GameEntry.SpriteCache.GetSprite(MuzzleAssetName, sprite => { _muzzleSprite = sprite; }); + } + + if (!s_RequestedBearing) + { + s_RequestedBearing = true; + GameEntry.SpriteCache.GetSprite(BearingAssetName, sprite => { _bearingSprite = sprite; }); + } + + if (!s_RequestedBase) + { + s_RequestedBase = true; + GameEntry.SpriteCache.GetSprite(BaseAssetName, sprite => { _baseSprite = sprite; }); + } + } + } +} diff --git a/src-ref/Utility/TowerIconComposeUtility.cs b/src-ref/Utility/TowerIconComposeUtility.cs new file mode 100644 index 0000000..256f55e --- /dev/null +++ b/src-ref/Utility/TowerIconComposeUtility.cs @@ -0,0 +1,148 @@ +using System; +using System.Globalization; +using UnityEngine; + +namespace GeometryTD.CustomUtility +{ + public static class TowerIconComposeUtility + { + public static Sprite Compose( + Sprite muzzleSprite, + Color muzzleColor, + Sprite bearingSprite, + Color bearingColor, + Sprite baseSprite, + Color baseColor) + { + if (muzzleSprite == null || bearingSprite == null || baseSprite == null) + { + return null; + } + + Sprite referenceSprite = muzzleSprite ?? bearingSprite ?? baseSprite; + if (referenceSprite == null) + { + return null; + } + + int width = Mathf.RoundToInt(referenceSprite.rect.width); + int height = Mathf.RoundToInt(referenceSprite.rect.height); + if (width <= 0 || height <= 0) + { + return null; + } + + return ComposeWithRenderTexture( + muzzleSprite, + muzzleColor, + bearingSprite, + bearingColor, + baseSprite, + baseColor, + width, + height, + referenceSprite); + } + + public static string BuildCacheKey( + long muzzleId, + long bearingId, + long baseId, + Color muzzleColor, + Color bearingColor, + Color baseColor) + { + return string.Format( + CultureInfo.InvariantCulture, + "v2|{0}|{1}|{2}|{3}|{4}|{5}", + muzzleId, + bearingId, + baseId, + ToColorKey(muzzleColor), + ToColorKey(bearingColor), + ToColorKey(baseColor)); + } + + private static Sprite ComposeWithRenderTexture( + Sprite muzzleSprite, + Color muzzleColor, + Sprite bearingSprite, + Color bearingColor, + Sprite baseSprite, + Color baseColor, + int width, + int height, + Sprite referenceSprite) + { + RenderTexture rt = RenderTexture.GetTemporary(width, height, 0, RenderTextureFormat.ARGB32); + RenderTexture previous = RenderTexture.active; + try + { + RenderTexture.active = rt; + GL.PushMatrix(); + GL.LoadPixelMatrix(0f, width, 0f, height); + GL.Clear(true, true, Color.clear); + + DrawSpriteToRenderTarget(baseSprite, baseColor, width, height); + DrawSpriteToRenderTarget(bearingSprite, bearingColor, width, height); + DrawSpriteToRenderTarget(muzzleSprite, muzzleColor, width, height); + + GL.PopMatrix(); + + Texture2D composedTexture = new Texture2D(width, height, TextureFormat.RGBA32, false); + composedTexture.ReadPixels(new Rect(0f, 0f, width, height), 0, 0); + composedTexture.Apply(false, false); + return CreateSprite(composedTexture, referenceSprite); + } + catch (Exception) + { + return null; + } + finally + { + RenderTexture.active = previous; + RenderTexture.ReleaseTemporary(rt); + } + } + + private static void DrawSpriteToRenderTarget(Sprite sprite, Color color, int width, int height) + { + if (sprite == null || sprite.texture == null) + { + return; + } + + Rect textureRect = sprite.textureRect; + Texture texture = sprite.texture; + Rect uvRect = new Rect( + textureRect.x / texture.width, + textureRect.y / texture.height, + textureRect.width / texture.width, + textureRect.height / texture.height); + + Graphics.DrawTexture(new Rect(0f, 0f, width, height), texture, uvRect, 0, 0, 0, 0, color); + } + + private static Sprite CreateSprite(Texture2D texture, Sprite referenceSprite) + { + if (texture == null || referenceSprite == null) + { + return null; + } + + float width = Mathf.Max(1f, referenceSprite.rect.width); + float height = Mathf.Max(1f, referenceSprite.rect.height); + Vector2 pivot = new Vector2(referenceSprite.pivot.x / width, referenceSprite.pivot.y / height); + return Sprite.Create(texture, new Rect(0f, 0f, texture.width, texture.height), pivot, referenceSprite.pixelsPerUnit); + } + + private static string ToColorKey(Color color) + { + Color32 c = color; + return c.r.ToString("X2", CultureInfo.InvariantCulture) + + c.g.ToString("X2", CultureInfo.InvariantCulture) + + c.b.ToString("X2", CultureInfo.InvariantCulture) + + c.a.ToString("X2", CultureInfo.InvariantCulture); + } + } +} diff --git a/src-ref/Utility/WebUtility.cs b/src-ref/Utility/WebUtility.cs new file mode 100644 index 0000000..88e32c1 --- /dev/null +++ b/src-ref/Utility/WebUtility.cs @@ -0,0 +1,24 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +using System; + +namespace GeometryTD.CustomUtility +{ + public static class WebUtility + { + public static string EscapeString(string stringToEscape) + { + return Uri.EscapeDataString(stringToEscape); + } + + public static string UnescapeString(string stringToUnescape) + { + return Uri.UnescapeDataString(stringToUnescape); + } + } +} diff --git a/src/Class1.cs b/src/Class1.cs new file mode 100644 index 0000000..1b76cce --- /dev/null +++ b/src/Class1.cs @@ -0,0 +1,9 @@ +using GameFramework; + +namespace GeometryTD.Core +{ + public class Class1 + { + + } +} diff --git a/src/Geometry-Tower-Defense-Base.csproj b/src/Geometry-Tower-Defense-Base.csproj new file mode 100644 index 0000000..6262a3f --- /dev/null +++ b/src/Geometry-Tower-Defense-Base.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.1 + GeometryTD.Core + disable + + + + + $(MSBuildThisFileDirectory)Libraries\GameFramework.dll + + + + diff --git a/src/Geometry-Tower-Defense-Base.sln b/src/Geometry-Tower-Defense-Base.sln new file mode 100644 index 0000000..8cd1403 --- /dev/null +++ b/src/Geometry-Tower-Defense-Base.sln @@ -0,0 +1,65 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeometryTD.Domain", "GeometryTD.Domain\GeometryTD.Domain.csproj", "{EE1392E2-D7A5-40E1-AA86-736D3FD321AF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeometryTD.Infrastructure", "GeometryTD.Infrastructure\GeometryTD.Infrastructure.csproj", "{F0BA47D0-1D6E-4786-BB7D-DCFC9EB4482F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeometryTD.Presentation", "GeometryTD.Presentation\GeometryTD.Presentation.csproj", "{694B789E-0595-4513-90EE-0982D5096D09}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EE1392E2-D7A5-40E1-AA86-736D3FD321AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE1392E2-D7A5-40E1-AA86-736D3FD321AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE1392E2-D7A5-40E1-AA86-736D3FD321AF}.Debug|x64.ActiveCfg = Debug|Any CPU + {EE1392E2-D7A5-40E1-AA86-736D3FD321AF}.Debug|x64.Build.0 = Debug|Any CPU + {EE1392E2-D7A5-40E1-AA86-736D3FD321AF}.Debug|x86.ActiveCfg = Debug|Any CPU + {EE1392E2-D7A5-40E1-AA86-736D3FD321AF}.Debug|x86.Build.0 = Debug|Any CPU + {EE1392E2-D7A5-40E1-AA86-736D3FD321AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE1392E2-D7A5-40E1-AA86-736D3FD321AF}.Release|Any CPU.Build.0 = Release|Any CPU + {EE1392E2-D7A5-40E1-AA86-736D3FD321AF}.Release|x64.ActiveCfg = Release|Any CPU + {EE1392E2-D7A5-40E1-AA86-736D3FD321AF}.Release|x64.Build.0 = Release|Any CPU + {EE1392E2-D7A5-40E1-AA86-736D3FD321AF}.Release|x86.ActiveCfg = Release|Any CPU + {EE1392E2-D7A5-40E1-AA86-736D3FD321AF}.Release|x86.Build.0 = Release|Any CPU + {F0BA47D0-1D6E-4786-BB7D-DCFC9EB4482F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0BA47D0-1D6E-4786-BB7D-DCFC9EB4482F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0BA47D0-1D6E-4786-BB7D-DCFC9EB4482F}.Debug|x64.ActiveCfg = Debug|Any CPU + {F0BA47D0-1D6E-4786-BB7D-DCFC9EB4482F}.Debug|x64.Build.0 = Debug|Any CPU + {F0BA47D0-1D6E-4786-BB7D-DCFC9EB4482F}.Debug|x86.ActiveCfg = Debug|Any CPU + {F0BA47D0-1D6E-4786-BB7D-DCFC9EB4482F}.Debug|x86.Build.0 = Debug|Any CPU + {F0BA47D0-1D6E-4786-BB7D-DCFC9EB4482F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0BA47D0-1D6E-4786-BB7D-DCFC9EB4482F}.Release|Any CPU.Build.0 = Release|Any CPU + {F0BA47D0-1D6E-4786-BB7D-DCFC9EB4482F}.Release|x64.ActiveCfg = Release|Any CPU + {F0BA47D0-1D6E-4786-BB7D-DCFC9EB4482F}.Release|x64.Build.0 = Release|Any CPU + {F0BA47D0-1D6E-4786-BB7D-DCFC9EB4482F}.Release|x86.ActiveCfg = Release|Any CPU + {F0BA47D0-1D6E-4786-BB7D-DCFC9EB4482F}.Release|x86.Build.0 = Release|Any CPU + {694B789E-0595-4513-90EE-0982D5096D09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {694B789E-0595-4513-90EE-0982D5096D09}.Debug|Any CPU.Build.0 = Debug|Any CPU + {694B789E-0595-4513-90EE-0982D5096D09}.Debug|x64.ActiveCfg = Debug|Any CPU + {694B789E-0595-4513-90EE-0982D5096D09}.Debug|x64.Build.0 = Debug|Any CPU + {694B789E-0595-4513-90EE-0982D5096D09}.Debug|x86.ActiveCfg = Debug|Any CPU + {694B789E-0595-4513-90EE-0982D5096D09}.Debug|x86.Build.0 = Debug|Any CPU + {694B789E-0595-4513-90EE-0982D5096D09}.Release|Any CPU.ActiveCfg = Release|Any CPU + {694B789E-0595-4513-90EE-0982D5096D09}.Release|Any CPU.Build.0 = Release|Any CPU + {694B789E-0595-4513-90EE-0982D5096D09}.Release|x64.ActiveCfg = Release|Any CPU + {694B789E-0595-4513-90EE-0982D5096D09}.Release|x64.Build.0 = Release|Any CPU + {694B789E-0595-4513-90EE-0982D5096D09}.Release|x86.ActiveCfg = Release|Any CPU + {694B789E-0595-4513-90EE-0982D5096D09}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FEEACB36-CF3E-42C8-9228-C5314170622C} + EndGlobalSection +EndGlobal diff --git a/src/GeometryTD.Domain/Definition/DataStruct/BuildInfo.cs b/src/GeometryTD.Domain/Definition/DataStruct/BuildInfo.cs new file mode 100644 index 0000000..658b102 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/DataStruct/BuildInfo.cs @@ -0,0 +1,54 @@ +//------------------------------------------------------------ +// Game Framework +// Copyright © 2013-2021 Jiang Yin. All rights reserved. +// Homepage: https://gameframework.cn/ +// Feedback: mailto:ellan@gameframework.cn +//------------------------------------------------------------ + +namespace GeometryTD.Definition +{ + public class BuildInfo + { + public string GameVersion + { + get; + set; + } + + public int InternalGameVersion + { + get; + set; + } + + public string CheckVersionUrl + { + get; + set; + } + + public string WindowsAppUrl + { + get; + set; + } + + public string MacOSAppUrl + { + get; + set; + } + + public string IOSAppUrl + { + get; + set; + } + + public string AndroidAppUrl + { + get; + set; + } + } +} diff --git a/src/GeometryTD.Domain/Definition/DataStruct/ImpactData.cs b/src/GeometryTD.Domain/Definition/DataStruct/ImpactData.cs new file mode 100644 index 0000000..1db0f0b --- /dev/null +++ b/src/GeometryTD.Domain/Definition/DataStruct/ImpactData.cs @@ -0,0 +1,24 @@ +using System.Runtime.InteropServices; + +namespace GeometryTD.Definition +{ + [StructLayout(LayoutKind.Auto)] + public struct ImpactData + { + public ImpactData(CampType camp, int hp, int attack, int defense) + { + Camp = camp; + HP = hp; + Attack = attack; + Defense = defense; + } + + public CampType Camp { get; } + + public int HP { get; } + + public int Attack { get; } + + public int Defense { get; } + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Definition/DataStruct/RunItemState.cs b/src/GeometryTD.Domain/Definition/DataStruct/RunItemState.cs new file mode 100644 index 0000000..d59917c --- /dev/null +++ b/src/GeometryTD.Domain/Definition/DataStruct/RunItemState.cs @@ -0,0 +1,20 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class RunItemState + { + public int ItemId { get; set; } + public int StackCount { get; set; } + + internal RunItemState Clone() + { + return new RunItemState + { + ItemId = ItemId, + StackCount = StackCount + }; + } + } +} diff --git a/src/GeometryTD.Domain/Definition/DataStruct/RunNodeCompletionSnapshot.cs b/src/GeometryTD.Domain/Definition/DataStruct/RunNodeCompletionSnapshot.cs new file mode 100644 index 0000000..7b9b3b7 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/DataStruct/RunNodeCompletionSnapshot.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class RunNodeCompletionSnapshot + { + public int Gold { get; set; } + public LevelThemeType ThemeType { get; set; } + public int ThemeStageIndex { get; set; } + public List CurrentThemePool { get; set; } = new List(); + public List ThemeHistory { get; set; } = new List(); + public int CurrentNodeContinueChallengeLayer { get; set; } + public List RunItems { get; set; } = new List(); + + internal RunNodeCompletionSnapshot Clone() + { + return new RunNodeCompletionSnapshot + { + Gold = Gold, + ThemeType = ThemeType, + ThemeStageIndex = ThemeStageIndex, + CurrentThemePool = new List(CurrentThemePool), + ThemeHistory = new List(ThemeHistory), + CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer, + RunItems = CloneRunItems(RunItems) + }; + } + + private static List CloneRunItems(List source) + { + var cloned = new List(); + if (source == null) return cloned; + foreach (var item in source) + { + if (item != null) cloned.Add(item.Clone()); + } + return cloned; + } + } +} diff --git a/src/GeometryTD.Domain/Definition/DataStruct/TowerCompItemData.cs b/src/GeometryTD.Domain/Definition/DataStruct/TowerCompItemData.cs new file mode 100644 index 0000000..6ba31d2 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/DataStruct/TowerCompItemData.cs @@ -0,0 +1,99 @@ +using System; + +namespace GeometryTD.Definition +{ + /// + /// 背包内组件实例基类(非 DataTable,表示玩家持有物)。 + /// + [Serializable] + public abstract class TowerCompItemData + { + /// + /// 组件实例唯一 Id。 + /// + public long InstanceId { get; set; } + + /// + /// 组件配置 Id(对应 DataTable Id)。 + /// + public int ConfigId { get; set; } + + /// + /// 组件槽位类型。 + /// + public TowerCompSlotType SlotType { get; protected set; } + + /// + /// 组件名称。 + /// + public string Name { get; set; } + + /// + /// 组件品质。 + /// + public RarityType Rarity { get; set; } + + /// + /// 组件当前耐久(0~100)。 + /// + public float Endurance { get; set; } = 100f; + + public bool IsAssembledIntoTower { get; set; } + + /// + /// 组件约束(先沿用 DataTable 原定义)。 + /// + public string Constraint { get; set; } + + /// + /// 组件当前 Tag(实例态)。 + /// + public TagType[] Tags { get; set; } + } + + [Serializable] + public sealed class MuzzleCompItemData : TowerCompItemData + { + public MuzzleCompItemData() + { + SlotType = TowerCompSlotType.Muzzle; + } + + public int[] AttackDamage { get; set; } + public float DamageRandomRate { get; set; } + public AttackMethodType AttackMethodType { get; set; } + } + + [Serializable] + public sealed class BearingCompItemData : TowerCompItemData + { + public BearingCompItemData() + { + SlotType = TowerCompSlotType.Bearing; + } + + public float[] RotateSpeed { get; set; } + public float[] AttackRange { get; set; } + } + + [Serializable] + public sealed class BaseCompItemData : TowerCompItemData + { + public BaseCompItemData() + { + SlotType = TowerCompSlotType.Base; + } + + public float[] AttackSpeed { get; set; } + public AttackPropertyType AttackPropertyType { get; set; } + } + + [Serializable] + public sealed class AccessoryItemData : TowerCompItemData + { + public AccessoryItemData() + { + SlotType = TowerCompSlotType.Accessory; + } + } +} diff --git a/src/GeometryTD.Domain/Definition/DataStruct/VersionInfo.cs b/src/GeometryTD.Domain/Definition/DataStruct/VersionInfo.cs new file mode 100644 index 0000000..24cd43a --- /dev/null +++ b/src/GeometryTD.Domain/Definition/DataStruct/VersionInfo.cs @@ -0,0 +1,68 @@ +namespace GeometryTD.Definition +{ + public class VersionInfo + { + // 是否需要强制更新游戏应用 + public bool ForceUpdateGame + { + get; + set; + } + + // 最新的游戏版本号 + public string LatestGameVersion + { + get; + set; + } + + // 最新的游戏内部版本号 + public int InternalGameVersion + { + get; + set; + } + + // 最新的资源内部版本号 + public int InternalResourceVersion + { + get; + set; + } + + // 资源更新下载地址 + public string UpdatePrefixUri + { + get; + set; + } + + // 资源版本列表长度 + public int VersionListLength + { + get; + set; + } + + // 资源版本列表哈希值 + public int VersionListHashCode + { + get; + set; + } + + // 资源版本列表压缩后长度 + public int VersionListCompressedLength + { + get; + set; + } + + // 资源版本列表压缩后哈希值 + public int VersionListCompressedHashCode + { + get; + set; + } + } +} diff --git a/src/GeometryTD.Domain/Definition/Enum/AttackMethodType.cs b/src/GeometryTD.Domain/Definition/Enum/AttackMethodType.cs new file mode 100644 index 0000000..78d4b0f --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/AttackMethodType.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public enum AttackMethodType : byte + { + None = 0, + NormalBullet = 1, + Range + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Definition/Enum/AttackPropertyType.cs b/src/GeometryTD.Domain/Definition/Enum/AttackPropertyType.cs new file mode 100644 index 0000000..5a0f3d9 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/AttackPropertyType.cs @@ -0,0 +1,13 @@ +namespace GeometryTD.Definition +{ + public enum AttackPropertyType : byte + { + None = 0, + Physics = 1, + Fire = 2, + Water = 3, + Earth = 4, + Poison = 5, + Ice = 6 + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Definition/Enum/CampType.cs b/src/GeometryTD.Domain/Definition/Enum/CampType.cs new file mode 100644 index 0000000..ddba1c9 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/CampType.cs @@ -0,0 +1,40 @@ +namespace GeometryTD.Definition +{ + /// + /// 阵营类型。 + /// + public enum CampType : byte + { + Unknown = 0, + + /// + /// 第一玩家阵营。 + /// + Player, + + /// + /// 第一敌人阵营。 + /// + Enemy, + + /// + /// 第一中立阵营。 + /// + Neutral, + + /// + /// 第二玩家阵营。 + /// + Player2, + + /// + /// 第二敌人阵营。 + /// + Enemy2, + + /// + /// 第二中立阵营 + /// + Neutral2, + } +} diff --git a/src/GeometryTD.Domain/Definition/Enum/CombatSelectActionType.cs b/src/GeometryTD.Domain/Definition/Enum/CombatSelectActionType.cs new file mode 100644 index 0000000..87c041d --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/CombatSelectActionType.cs @@ -0,0 +1,10 @@ +namespace GeometryTD.Definition +{ + public enum CombatSelectActionType : byte + { + None = 0, + BuildTower = 1, + UpgradeTower = 2, + DestroyTower = 3 + } +} diff --git a/src/GeometryTD.Domain/Definition/Enum/EntryType.cs b/src/GeometryTD.Domain/Definition/Enum/EntryType.cs new file mode 100644 index 0000000..e29a644 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/EntryType.cs @@ -0,0 +1,13 @@ +namespace GeometryTD.Definition +{ + /// + /// 出怪条目类型 + /// + public enum EntryType : byte + { + None = 0, + Stream = 1, + Burst = 2, + Boss = 3 + } +} diff --git a/src/GeometryTD.Domain/Definition/Enum/EventEffectType.cs b/src/GeometryTD.Domain/Definition/Enum/EventEffectType.cs new file mode 100644 index 0000000..0231272 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/EventEffectType.cs @@ -0,0 +1,21 @@ +namespace GeometryTD.Definition +{ + public enum EventEffectType + { + None, + + AddGold, + RemoveGold, + + AddRandomComps, + RemoveRandomComps, + + AddRandomCompsEndurance, + RemoveRandomCompsEndurance, + + AddRandomTowersEndurance, + DamageRandomTowersEndurance, + + TransformComponents, + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Definition/Enum/EventRequirementType.cs b/src/GeometryTD.Domain/Definition/Enum/EventRequirementType.cs new file mode 100644 index 0000000..eb02abd --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/EventRequirementType.cs @@ -0,0 +1,11 @@ +namespace GeometryTD.Definition +{ + public enum EventRequirementType + { + None, + GoldAtLeast, + CompCountAtLeast, + TowerCountAtLeast, + HasRelic, + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Definition/Enum/InventoryTagSourceType.cs b/src/GeometryTD.Domain/Definition/Enum/InventoryTagSourceType.cs new file mode 100644 index 0000000..2f21259 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/InventoryTagSourceType.cs @@ -0,0 +1,11 @@ +namespace GeometryTD.Definition +{ + public enum InventoryTagSourceType : byte + { + Seed = 1, + Shop = 2, + Drop = 3, + Reward = 4, + Event = 5, + } +} diff --git a/src/GeometryTD.Domain/Definition/Enum/LevelThemeType.cs b/src/GeometryTD.Domain/Definition/Enum/LevelThemeType.cs new file mode 100644 index 0000000..8efa818 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/LevelThemeType.cs @@ -0,0 +1,10 @@ +namespace GeometryTD.Definition +{ + public enum LevelThemeType + { + None, + Plain, + Volcano, + Mountain + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Definition/Enum/LevelVictoryType.cs b/src/GeometryTD.Domain/Definition/Enum/LevelVictoryType.cs new file mode 100644 index 0000000..b58db43 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/LevelVictoryType.cs @@ -0,0 +1,10 @@ +namespace GeometryTD.Definition +{ + public enum LevelVictoryType + { + None, + PhasesCleared, + BossDead, + TimeElapsed + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Definition/Enum/PhaseEndType.cs b/src/GeometryTD.Domain/Definition/Enum/PhaseEndType.cs new file mode 100644 index 0000000..a8b1e2e --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/PhaseEndType.cs @@ -0,0 +1,13 @@ +namespace GeometryTD.Definition +{ + /// + /// 关卡阶段结束类型 + /// + public enum PhaseEndType : byte + { + None = 0, + TimeElapsed = 1, + EnemiesCleared = 2, + BossDead = 3 + } +} diff --git a/src/GeometryTD.Domain/Definition/Enum/ProcedureMainCombatEntryBlockReason.cs b/src/GeometryTD.Domain/Definition/Enum/ProcedureMainCombatEntryBlockReason.cs new file mode 100644 index 0000000..a3feef7 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/ProcedureMainCombatEntryBlockReason.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public enum ProcedureMainCombatEntryBlockReason + { + None = 0, + InventoryUnavailable = 1, + NoValidParticipantTower = 2 + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Definition/Enum/ProcedureMainFlowPhase.cs b/src/GeometryTD.Domain/Definition/Enum/ProcedureMainFlowPhase.cs new file mode 100644 index 0000000..d9305ba --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/ProcedureMainFlowPhase.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public enum ProcedureMainFlowPhase + { + Hub = 0, + NodeActive = 1, + RunCompletedPendingFinish = 2 + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Definition/Enum/ProcedureMainRunAdvanceResult.cs b/src/GeometryTD.Domain/Definition/Enum/ProcedureMainRunAdvanceResult.cs new file mode 100644 index 0000000..6fe78cc --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/ProcedureMainRunAdvanceResult.cs @@ -0,0 +1,10 @@ +namespace GeometryTD.Definition +{ + public enum ProcedureMainRunAdvanceResult + { + NoChange = 0, + NodeException = 1, + AdvancedToNextNode = 2, + RunCompleted = 3 + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Definition/Enum/ProcedureMainRunCompletionResult.cs b/src/GeometryTD.Domain/Definition/Enum/ProcedureMainRunCompletionResult.cs new file mode 100644 index 0000000..f5fe2f6 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/ProcedureMainRunCompletionResult.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public enum ProcedureMainRunCompletionResult + { + NoChange = 0, + ShowCompletionDialog = 1, + ReturnToMenu = 2 + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Definition/Enum/RarityType.cs b/src/GeometryTD.Domain/Definition/Enum/RarityType.cs new file mode 100644 index 0000000..3cd10df --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/RarityType.cs @@ -0,0 +1,12 @@ +namespace GeometryTD.Definition +{ + public enum RarityType + { + None = 0, + White = 1, + Green = 2, + Blue = 3, + Purple = 4, + Red = 5 + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Definition/Enum/RelationType.cs b/src/GeometryTD.Domain/Definition/Enum/RelationType.cs new file mode 100644 index 0000000..cd56fc9 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/RelationType.cs @@ -0,0 +1,28 @@ +namespace GeometryTD.Definition +{ + /// + /// 关系类型。 + /// + public enum RelationType : byte + { + /// + /// 未知的。 + /// + Unknown, + + /// + /// 友好的。 + /// + Friendly, + + /// + /// 中立的。 + /// + Neutral, + + /// + /// 敌对的。 + /// + Hostile + } +} diff --git a/src/GeometryTD.Domain/Definition/Enum/RepoItemClickActionType.cs b/src/GeometryTD.Domain/Definition/Enum/RepoItemClickActionType.cs new file mode 100644 index 0000000..8e74193 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/RepoItemClickActionType.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public enum RepoItemClickActionType + { + OpenDetail, + RemoveParticipant, + + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Definition/Enum/RunNodeCompletionStatus.cs b/src/GeometryTD.Domain/Definition/Enum/RunNodeCompletionStatus.cs new file mode 100644 index 0000000..f3d9309 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/RunNodeCompletionStatus.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public enum RunNodeCompletionStatus + { + None = 0, + Completed = 1, + Exception = 2 + } +} diff --git a/src/GeometryTD.Domain/Definition/Enum/RunNodeStatus.cs b/src/GeometryTD.Domain/Definition/Enum/RunNodeStatus.cs new file mode 100644 index 0000000..269dae6 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/RunNodeStatus.cs @@ -0,0 +1,11 @@ +namespace GeometryTD.Definition +{ + public enum RunNodeStatus + { + Locked = 0, + Available = 1, + Completed = 2, + Exception = 3, + Skipped = 4 + } +} diff --git a/src/GeometryTD.Domain/Definition/Enum/RunNodeType.cs b/src/GeometryTD.Domain/Definition/Enum/RunNodeType.cs new file mode 100644 index 0000000..684393e --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/RunNodeType.cs @@ -0,0 +1,11 @@ +namespace GeometryTD.Definition +{ + public enum RunNodeType + { + None = 0, + Combat = 1, + Event = 2, + Shop = 3, + BossCombat = 4 + } +} diff --git a/src/GeometryTD.Domain/Definition/Enum/SceneType.cs b/src/GeometryTD.Domain/Definition/Enum/SceneType.cs new file mode 100644 index 0000000..9a96df7 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/SceneType.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public enum SceneType : byte + { + Launch = 0, + Menu = 1, + Main = 2, + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Definition/Enum/TagCategory.cs b/src/GeometryTD.Domain/Definition/Enum/TagCategory.cs new file mode 100644 index 0000000..2cf6a22 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/TagCategory.cs @@ -0,0 +1,12 @@ +namespace GeometryTD.Definition +{ + public enum TagCategory : byte + { + None = 0, + Status = 1, + NumericModifier = 2, + AttackShape = 3, + // Enhances another applied status on the same hit, but does not create its own enemy-held state. + StatusModifier = 4 + } +} diff --git a/src/GeometryTD.Domain/Definition/Enum/TagTriggerPhase.cs b/src/GeometryTD.Domain/Definition/Enum/TagTriggerPhase.cs new file mode 100644 index 0000000..cfa9d7f --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/TagTriggerPhase.cs @@ -0,0 +1,11 @@ +namespace GeometryTD.Definition +{ + public enum TagTriggerPhase : byte + { + None = 0, + OnBeforeHit = 1, + OnHit = 2, + OnAfterHit = 3, + OnKill = 4 + } +} diff --git a/src/GeometryTD.Domain/Definition/Enum/TagType.cs b/src/GeometryTD.Domain/Definition/Enum/TagType.cs new file mode 100644 index 0000000..28c0bec --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/TagType.cs @@ -0,0 +1,31 @@ +namespace GeometryTD.Definition +{ + public enum TagType : byte + { + None = 0, + + /// + /// 元素 + /// + Fire, + BurnSpread, + IgniteBurst, + Inferno, + + /// + /// 控制 + /// + Ice, + FreezeMask, + Shatter, + AbsoluteZero, + + /// + /// 穿透 + /// + Pierce, + Crit, + Overpenetrate, + Execution, + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Definition/Enum/TowerCompSlotType.cs b/src/GeometryTD.Domain/Definition/Enum/TowerCompSlotType.cs new file mode 100644 index 0000000..10737c8 --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/TowerCompSlotType.cs @@ -0,0 +1,11 @@ +namespace GeometryTD.Definition +{ + public enum TowerCompSlotType : byte + { + None = 0, + Muzzle = 1, + Bearing = 2, + Base = 3, + Accessory = 4, + } +} diff --git a/src/GeometryTD.Domain/Definition/Enum/UIFormType.cs b/src/GeometryTD.Domain/Definition/Enum/UIFormType.cs new file mode 100644 index 0000000..3d7553f --- /dev/null +++ b/src/GeometryTD.Domain/Definition/Enum/UIFormType.cs @@ -0,0 +1,85 @@ +namespace GeometryTD.Definition +{ + /// + /// 界面编号。 + /// + public enum UIFormType : byte + { + Undefined = 0, + + /// + /// 弹出框。 + /// + DialogForm = 1, + + /// + /// 主菜单。 + /// + MenuForm = 100, + + /// + /// 设置。 + /// + SettingForm = 101, + + /// + /// 关于。 + /// + AboutForm = 102, + + /// + /// 主界面。 + /// + MainForm = 110, + + /// + /// 仓库界面。 + /// + RepoForm = 111, + + /// + /// 节点地图界面。 + /// + NodeMapForm = 112, + + /// + /// 道具详细信息界面。 + /// + ItemDescForm = 113, + + /// + /// 奖励三选一界面。 + /// + RewardSelectForm = 114, + + /// + /// 事件节点界面。 + /// + EventForm = 130, + + /// + /// 战斗信息界面。 + /// + CombatInfoForm = 140, + + /// + /// 战斗结算界面。 + /// + CombatFinishForm = 141, + + /// + /// 战斗选择界面 + /// + CombatSelectForm = 142, + + /// + /// 商店界面。 + /// + ShopForm = 150, + + /// + /// 测试菜单。 + /// + TestMenuForm = 200, + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Event/Combat/CombatBaseHpChangedEventArgs.cs b/src/GeometryTD.Domain/Event/Combat/CombatBaseHpChangedEventArgs.cs new file mode 100644 index 0000000..7486e02 --- /dev/null +++ b/src/GeometryTD.Domain/Event/Combat/CombatBaseHpChangedEventArgs.cs @@ -0,0 +1,37 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombatBaseHpChangedEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatBaseHpChangedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int CurrentBaseHp { get; private set; } + public int DeltaBaseHp { get; private set; } + + + public CombatBaseHpChangedEventArgs() + { + CurrentBaseHp = 100; + DeltaBaseHp = 0; + } + + public static CombatBaseHpChangedEventArgs Create(int currentBaseHp, int deltaBaseHp = 0) + { + var args = ReferencePool.Acquire(); + args.CurrentBaseHp = currentBaseHp; + args.DeltaBaseHp = deltaBaseHp; + + return args; + } + + public override void Clear() + { + CurrentBaseHp = 100; + DeltaBaseHp = 0; + } + } +} diff --git a/src/GeometryTD.Domain/Event/Combat/CombatCoinChangedEventArgs.cs b/src/GeometryTD.Domain/Event/Combat/CombatCoinChangedEventArgs.cs new file mode 100644 index 0000000..f0b173c --- /dev/null +++ b/src/GeometryTD.Domain/Event/Combat/CombatCoinChangedEventArgs.cs @@ -0,0 +1,37 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombatCoinChangedEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatCoinChangedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int CurrentCoin { get; private set; } + public int DeltaCoin { get; private set; } + + + public CombatCoinChangedEventArgs() + { + CurrentCoin = 0; + DeltaCoin = 0; + } + + public static CombatCoinChangedEventArgs Create(int currentCoin, int deltaCoin = 0) + { + var args = ReferencePool.Acquire(); + args.CurrentCoin = currentCoin; + args.DeltaCoin = deltaCoin; + + return args; + } + + public override void Clear() + { + CurrentCoin = 0; + DeltaCoin = 0; + } + } +} diff --git a/src/GeometryTD.Domain/Event/Combat/CombatDebugFailEventArgs.cs b/src/GeometryTD.Domain/Event/Combat/CombatDebugFailEventArgs.cs new file mode 100644 index 0000000..ae32419 --- /dev/null +++ b/src/GeometryTD.Domain/Event/Combat/CombatDebugFailEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombatDebugFailEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatDebugFailEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static CombatDebugFailEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src/GeometryTD.Domain/Event/Combat/CombatEndEventArgs.cs b/src/GeometryTD.Domain/Event/Combat/CombatEndEventArgs.cs new file mode 100644 index 0000000..b2a9589 --- /dev/null +++ b/src/GeometryTD.Domain/Event/Combat/CombatEndEventArgs.cs @@ -0,0 +1,25 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombatEndEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatEndEventArgs).GetHashCode(); + + public override int Id => EventId; + + public CombatEndEventArgs() + { + } + + public static CombatEndEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Event/Combat/CombatEnemyHpRateChangedEventArgs.cs b/src/GeometryTD.Domain/Event/Combat/CombatEnemyHpRateChangedEventArgs.cs new file mode 100644 index 0000000..b21d2fc --- /dev/null +++ b/src/GeometryTD.Domain/Event/Combat/CombatEnemyHpRateChangedEventArgs.cs @@ -0,0 +1,31 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombatEnemyHpRateChangedEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatEnemyHpRateChangedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int EnemyHpRateMultiplier { get; private set; } + + public CombatEnemyHpRateChangedEventArgs() + { + EnemyHpRateMultiplier = 1; + } + + public static CombatEnemyHpRateChangedEventArgs Create(int enemyHpRateMultiplier) + { + var args = ReferencePool.Acquire(); + args.EnemyHpRateMultiplier = enemyHpRateMultiplier > 0 ? enemyHpRateMultiplier : 1; + return args; + } + + public override void Clear() + { + EnemyHpRateMultiplier = 1; + } + } +} diff --git a/src/GeometryTD.Domain/Event/Combat/CombatFinishReturnEventArgs.cs b/src/GeometryTD.Domain/Event/Combat/CombatFinishReturnEventArgs.cs new file mode 100644 index 0000000..452de0d --- /dev/null +++ b/src/GeometryTD.Domain/Event/Combat/CombatFinishReturnEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombatFinishReturnEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatFinishReturnEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static CombatFinishReturnEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src/GeometryTD.Domain/Event/Combat/CombatPauseEventArgs.cs b/src/GeometryTD.Domain/Event/Combat/CombatPauseEventArgs.cs new file mode 100644 index 0000000..97f0bdd --- /dev/null +++ b/src/GeometryTD.Domain/Event/Combat/CombatPauseEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombatPauseEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatPauseEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static CombatPauseEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src/GeometryTD.Domain/Event/Combat/CombatProcessEventArgs.cs b/src/GeometryTD.Domain/Event/Combat/CombatProcessEventArgs.cs new file mode 100644 index 0000000..fa5c36e --- /dev/null +++ b/src/GeometryTD.Domain/Event/Combat/CombatProcessEventArgs.cs @@ -0,0 +1,37 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombatProcessEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatProcessEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int CurrentPhase { get; private set; } + + public int TotalPhase { get; private set; } + + public CombatProcessEventArgs() + { + CurrentPhase = 0; + TotalPhase = 0; + } + + public static CombatProcessEventArgs Create(int currentPhase, int totalPhase) + { + var args = ReferencePool.Acquire(); + args.CurrentPhase = currentPhase; + args.TotalPhase = totalPhase; + + return args; + } + + public override void Clear() + { + CurrentPhase = 0; + TotalPhase = 0; + } + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Event/Combat/CombatSelectItemClickEventArgs.cs b/src/GeometryTD.Domain/Event/Combat/CombatSelectItemClickEventArgs.cs new file mode 100644 index 0000000..dfec0cf --- /dev/null +++ b/src/GeometryTD.Domain/Event/Combat/CombatSelectItemClickEventArgs.cs @@ -0,0 +1,31 @@ +using GameFramework; +using GameFramework.Event; +using GeometryTD.Definition; + +namespace GeometryTD.Domain.CustomEvent +{ + public class CombatSelectItemClickEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatSelectItemClickEventArgs).GetHashCode(); + + public override int Id => EventId; + + public CombatSelectActionType ActionType { get; private set; } = CombatSelectActionType.None; + + public int ActionIndex { get; private set; } = -1; + + public static CombatSelectItemClickEventArgs Create(CombatSelectActionType actionType, int actionIndex) + { + CombatSelectItemClickEventArgs args = ReferencePool.Acquire(); + args.ActionType = actionType; + args.ActionIndex = actionIndex; + return args; + } + + public override void Clear() + { + ActionType = CombatSelectActionType.None; + ActionIndex = -1; + } + } +} diff --git a/src/GeometryTD.Domain/Event/EventForm/EventOptionItemSelectedEventArgs.cs b/src/GeometryTD.Domain/Event/EventForm/EventOptionItemSelectedEventArgs.cs new file mode 100644 index 0000000..d7b10d8 --- /dev/null +++ b/src/GeometryTD.Domain/Event/EventForm/EventOptionItemSelectedEventArgs.cs @@ -0,0 +1,27 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class EventOptionItemSelectedEventArgs : GameEventArgs + { + public static int EventId => typeof(EventOptionItemSelectedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int SelectedItemId { get; private set; } = -1; + + public static EventOptionItemSelectedEventArgs Create(int selectedItemId) + { + var args = ReferencePool.Acquire(); + args.SelectedItemId = selectedItemId; + + return args; + } + + public override void Clear() + { + SelectedItemId = -1; + } + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Event/Game/NodeCompleteEventArgs.cs b/src/GeometryTD.Domain/Event/Game/NodeCompleteEventArgs.cs new file mode 100644 index 0000000..d321c86 --- /dev/null +++ b/src/GeometryTD.Domain/Event/Game/NodeCompleteEventArgs.cs @@ -0,0 +1,68 @@ +using GameFramework; +using GameFramework.Event; +using GeometryTD.Definition; + +namespace GeometryTD.Domain.CustomEvent +{ + public class NodeCompleteEventArgs : GameEventArgs + { + public static int EventId = typeof(NodeCompleteEventArgs).GetHashCode(); + + public override int Id => EventId; + + public string RunId { get; private set; } + + public int NodeId { get; private set; } + + public RunNodeType NodeType { get; private set; } + + public int SequenceIndex { get; private set; } + + public RunNodeCompletionStatus CompletionStatus { get; private set; } + + public bool CombatWon { get; private set; } + + public RunNodeCompletionSnapshot CompletionSnapshot { get; private set; } + + public NodeCompleteEventArgs() + { + } + + public static NodeCompleteEventArgs Create() + { + return Create(null, 0, RunNodeType.None, -1, RunNodeCompletionStatus.Completed, true, null); + } + + public static NodeCompleteEventArgs Create( + string runId, + int nodeId, + RunNodeType nodeType, + int sequenceIndex, + RunNodeCompletionStatus completionStatus, + bool combatWon, + RunNodeCompletionSnapshot completionSnapshot) + { + var args = ReferencePool.Acquire(); + args.RunId = runId; + args.NodeId = nodeId; + args.NodeType = nodeType; + args.SequenceIndex = sequenceIndex; + args.CompletionStatus = completionStatus; + args.CombatWon = combatWon; + args.CompletionSnapshot = completionSnapshot != null ? completionSnapshot.Clone() : null; + + return args; + } + + public override void Clear() + { + RunId = null; + NodeId = 0; + NodeType = RunNodeType.None; + SequenceIndex = -1; + CompletionStatus = RunNodeCompletionStatus.None; + CombatWon = false; + CompletionSnapshot = null; + } + } +} diff --git a/src/GeometryTD.Domain/Event/Game/NodeEnterEventArgs.cs b/src/GeometryTD.Domain/Event/Game/NodeEnterEventArgs.cs new file mode 100644 index 0000000..1174a76 --- /dev/null +++ b/src/GeometryTD.Domain/Event/Game/NodeEnterEventArgs.cs @@ -0,0 +1,49 @@ +using GameFramework; +using GameFramework.Event; +using GeometryTD.Definition; + +namespace GeometryTD.Domain.CustomEvent +{ + public class NodeEnterEventArgs : GameEventArgs + { + public static int EventId = typeof(NodeEnterEventArgs).GetHashCode(); + + public override int Id => EventId; + + public string RunId { get; private set; } + + public int NodeId { get; private set; } + + public RunNodeType NodeType { get; private set; } + + public int SequenceIndex { get; private set; } + + public NodeEnterEventArgs() + { + } + + public static NodeEnterEventArgs Create() + { + return Create(null, 0, RunNodeType.None, -1); + } + + public static NodeEnterEventArgs Create(string runId, int nodeId, RunNodeType nodeType, int sequenceIndex) + { + var args = ReferencePool.Acquire(); + args.RunId = runId; + args.NodeId = nodeId; + args.NodeType = nodeType; + args.SequenceIndex = sequenceIndex; + + return args; + } + + public override void Clear() + { + RunId = null; + NodeId = 0; + NodeType = RunNodeType.None; + SequenceIndex = -1; + } + } +} diff --git a/src/GeometryTD.Domain/Event/Game/NodeMapNodeClickEventArgs.cs b/src/GeometryTD.Domain/Event/Game/NodeMapNodeClickEventArgs.cs new file mode 100644 index 0000000..8b3fb8e --- /dev/null +++ b/src/GeometryTD.Domain/Event/Game/NodeMapNodeClickEventArgs.cs @@ -0,0 +1,26 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public sealed class NodeMapNodeClickEventArgs : GameEventArgs + { + public static int EventId => typeof(NodeMapNodeClickEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int SequenceIndex { get; private set; } + + public static NodeMapNodeClickEventArgs Create(int sequenceIndex) + { + NodeMapNodeClickEventArgs args = ReferencePool.Acquire(); + args.SequenceIndex = sequenceIndex; + return args; + } + + public override void Clear() + { + SequenceIndex = -1; + } + } +} diff --git a/src/GeometryTD.Domain/Event/Game/NodeMapNodeEnterRequestedEventArgs.cs b/src/GeometryTD.Domain/Event/Game/NodeMapNodeEnterRequestedEventArgs.cs new file mode 100644 index 0000000..53f4256 --- /dev/null +++ b/src/GeometryTD.Domain/Event/Game/NodeMapNodeEnterRequestedEventArgs.cs @@ -0,0 +1,43 @@ +using GameFramework; +using GameFramework.Event; +using GeometryTD.Definition; + +namespace GeometryTD.Domain.CustomEvent +{ + public sealed class NodeMapNodeEnterRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(NodeMapNodeEnterRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public string RunId { get; private set; } + + public int NodeId { get; private set; } + + public RunNodeType NodeType { get; private set; } + + public int SequenceIndex { get; private set; } + + public static NodeMapNodeEnterRequestedEventArgs Create( + string runId, + int nodeId, + RunNodeType nodeType, + int sequenceIndex) + { + NodeMapNodeEnterRequestedEventArgs args = ReferencePool.Acquire(); + args.RunId = runId; + args.NodeId = nodeId; + args.NodeType = nodeType; + args.SequenceIndex = sequenceIndex; + return args; + } + + public override void Clear() + { + RunId = null; + NodeId = 0; + NodeType = RunNodeType.None; + SequenceIndex = -1; + } + } +} diff --git a/src/GeometryTD.Domain/Event/Game/TestMenuNodeClickEventArgs.cs b/src/GeometryTD.Domain/Event/Game/TestMenuNodeClickEventArgs.cs new file mode 100644 index 0000000..df7f777 --- /dev/null +++ b/src/GeometryTD.Domain/Event/Game/TestMenuNodeClickEventArgs.cs @@ -0,0 +1,33 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public enum TestMenuNodeType : byte + { + Combat = 1, + Event = 2, + Shop = 3 + } + + public class TestMenuNodeClickEventArgs : GameEventArgs + { + public static int EventId => typeof(TestMenuNodeClickEventArgs).GetHashCode(); + + public override int Id => EventId; + + public TestMenuNodeType NodeType { get; private set; } + + public static TestMenuNodeClickEventArgs Create(TestMenuNodeType nodeType) + { + TestMenuNodeClickEventArgs args = ReferencePool.Acquire(); + args.NodeType = nodeType; + return args; + } + + public override void Clear() + { + NodeType = 0; + } + } +} diff --git a/src/GeometryTD.Domain/Event/General/RewardSelectGiveUpEventArgs.cs b/src/GeometryTD.Domain/Event/General/RewardSelectGiveUpEventArgs.cs new file mode 100644 index 0000000..9289192 --- /dev/null +++ b/src/GeometryTD.Domain/Event/General/RewardSelectGiveUpEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class RewardSelectGiveUpEventArgs : GameEventArgs + { + public static int EventId => typeof(RewardSelectGiveUpEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static RewardSelectGiveUpEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src/GeometryTD.Domain/Event/General/RewardSelectItemSelectedEventArgs.cs b/src/GeometryTD.Domain/Event/General/RewardSelectItemSelectedEventArgs.cs new file mode 100644 index 0000000..80d46ba --- /dev/null +++ b/src/GeometryTD.Domain/Event/General/RewardSelectItemSelectedEventArgs.cs @@ -0,0 +1,26 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class RewardSelectItemSelectedEventArgs : GameEventArgs + { + public static int EventId => typeof(RewardSelectItemSelectedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int SelectedIndex { get; private set; } = -1; + + public static RewardSelectItemSelectedEventArgs Create(int selectedIndex) + { + RewardSelectItemSelectedEventArgs args = ReferencePool.Acquire(); + args.SelectedIndex = selectedIndex; + return args; + } + + public override void Clear() + { + SelectedIndex = -1; + } + } +} diff --git a/src/GeometryTD.Domain/Event/General/RewardSelectRefreshEventArgs.cs b/src/GeometryTD.Domain/Event/General/RewardSelectRefreshEventArgs.cs new file mode 100644 index 0000000..aaa01df --- /dev/null +++ b/src/GeometryTD.Domain/Event/General/RewardSelectRefreshEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class RewardSelectRefreshEventArgs : GameEventArgs + { + public static int EventId => typeof(RewardSelectRefreshEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static RewardSelectRefreshEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src/GeometryTD.Domain/Event/MainForm/RepoButtonClickedEventArgs.cs b/src/GeometryTD.Domain/Event/MainForm/RepoButtonClickedEventArgs.cs new file mode 100644 index 0000000..9d57b2f --- /dev/null +++ b/src/GeometryTD.Domain/Event/MainForm/RepoButtonClickedEventArgs.cs @@ -0,0 +1,27 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class RepoButtonClickedEventArgs : GameEventArgs + { + public static int EventId = typeof(RepoButtonClickedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public RepoButtonClickedEventArgs() + { + } + + public static RepoButtonClickedEventArgs Create() + { + var args = ReferencePool.Acquire(); + + return args; + } + + public override void Clear() + { + } + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Event/MainForm/ReturnButtonClickedEventArgs.cs b/src/GeometryTD.Domain/Event/MainForm/ReturnButtonClickedEventArgs.cs new file mode 100644 index 0000000..02ee195 --- /dev/null +++ b/src/GeometryTD.Domain/Event/MainForm/ReturnButtonClickedEventArgs.cs @@ -0,0 +1,27 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class ReturnButtonClickedEventArgs : GameEventArgs + { + public static int EventId = typeof(ReturnButtonClickedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public ReturnButtonClickedEventArgs() + { + } + + public static ReturnButtonClickedEventArgs Create() + { + var args = ReferencePool.Acquire(); + + return args; + } + + public override void Clear() + { + } + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Event/Menu/MenuExitRequestedEventArgs.cs b/src/GeometryTD.Domain/Event/Menu/MenuExitRequestedEventArgs.cs new file mode 100644 index 0000000..ec44241 --- /dev/null +++ b/src/GeometryTD.Domain/Event/Menu/MenuExitRequestedEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class MenuExitRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(MenuExitRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static MenuExitRequestedEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src/GeometryTD.Domain/Event/Menu/MenuSettingsRequestedEventArgs.cs b/src/GeometryTD.Domain/Event/Menu/MenuSettingsRequestedEventArgs.cs new file mode 100644 index 0000000..6d9db5e --- /dev/null +++ b/src/GeometryTD.Domain/Event/Menu/MenuSettingsRequestedEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class MenuSettingsRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(MenuSettingsRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static MenuSettingsRequestedEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src/GeometryTD.Domain/Event/Menu/MenuStartRequestedEventArgs.cs b/src/GeometryTD.Domain/Event/Menu/MenuStartRequestedEventArgs.cs new file mode 100644 index 0000000..76ffb38 --- /dev/null +++ b/src/GeometryTD.Domain/Event/Menu/MenuStartRequestedEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class MenuStartRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(MenuStartRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static MenuStartRequestedEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src/GeometryTD.Domain/Event/RepoForm/CombineSlotClickedEventArgs.cs b/src/GeometryTD.Domain/Event/RepoForm/CombineSlotClickedEventArgs.cs new file mode 100644 index 0000000..6d9c780 --- /dev/null +++ b/src/GeometryTD.Domain/Event/RepoForm/CombineSlotClickedEventArgs.cs @@ -0,0 +1,26 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombineSlotClickedEventArgs : GameEventArgs + { + public static int EventId => typeof(CombineSlotClickedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int SlotIndex { get; private set; } = -1; + + public static CombineSlotClickedEventArgs Create(int slotIndex) + { + CombineSlotClickedEventArgs args = ReferencePool.Acquire(); + args.SlotIndex = slotIndex; + return args; + } + + public override void Clear() + { + SlotIndex = -1; + } + } +} diff --git a/src/GeometryTD.Domain/Event/RepoForm/RepoCombineRequestedEventArgs.cs b/src/GeometryTD.Domain/Event/RepoForm/RepoCombineRequestedEventArgs.cs new file mode 100644 index 0000000..2fef81e --- /dev/null +++ b/src/GeometryTD.Domain/Event/RepoForm/RepoCombineRequestedEventArgs.cs @@ -0,0 +1,32 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public sealed class RepoCombineRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(RepoCombineRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public long MuzzleItemId { get; private set; } + public long BearingItemId { get; private set; } + public long BaseItemId { get; private set; } + + public static RepoCombineRequestedEventArgs Create(long muzzleItemId, long bearingItemId, long baseItemId) + { + RepoCombineRequestedEventArgs args = ReferencePool.Acquire(); + args.MuzzleItemId = muzzleItemId; + args.BearingItemId = bearingItemId; + args.BaseItemId = baseItemId; + return args; + } + + public override void Clear() + { + MuzzleItemId = 0; + BearingItemId = 0; + BaseItemId = 0; + } + } +} diff --git a/src/GeometryTD.Domain/Event/RepoForm/RepoFormReturnEventArgs.cs b/src/GeometryTD.Domain/Event/RepoForm/RepoFormReturnEventArgs.cs new file mode 100644 index 0000000..d9fa41e --- /dev/null +++ b/src/GeometryTD.Domain/Event/RepoForm/RepoFormReturnEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class RepoFormReturnEventArgs : GameEventArgs + { + public static int EventId => typeof(RepoFormReturnEventArgs).GetHashCode(); + + public override int Id => EventId; + + public override void Clear() + { + } + + public static RepoFormReturnEventArgs Create() + { + return ReferencePool.Acquire(); + } + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/Event/RepoForm/RepoItemClickedEventArgs.cs b/src/GeometryTD.Domain/Event/RepoForm/RepoItemClickedEventArgs.cs new file mode 100644 index 0000000..93d85a4 --- /dev/null +++ b/src/GeometryTD.Domain/Event/RepoForm/RepoItemClickedEventArgs.cs @@ -0,0 +1,31 @@ +using GameFramework; +using GameFramework.Event; +using System.Numerics; + +namespace GeometryTD.Domain.CustomEvent +{ + public sealed class RepoItemClickedEventArgs : GameEventArgs + { + public static int EventId => typeof(RepoItemClickedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public long ItemId { get; private set; } + + public Vector2 ScreenPosition { get; private set; } + + public static RepoItemClickedEventArgs Create(long itemId, Vector2 screenPosition) + { + RepoItemClickedEventArgs args = ReferencePool.Acquire(); + args.ItemId = itemId; + args.ScreenPosition = screenPosition; + return args; + } + + public override void Clear() + { + ItemId = 0; + ScreenPosition = Vector2.Zero; + } + } +} diff --git a/src/GeometryTD.Domain/Event/RepoForm/RepoItemDragEndedEventArgs.cs b/src/GeometryTD.Domain/Event/RepoForm/RepoItemDragEndedEventArgs.cs new file mode 100644 index 0000000..8d8a8e0 --- /dev/null +++ b/src/GeometryTD.Domain/Event/RepoForm/RepoItemDragEndedEventArgs.cs @@ -0,0 +1,30 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class RepoItemDragEndedEventArgs : GameEventArgs + { + public static int EventId => typeof(RepoItemDragEndedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public long ItemId { get; private set; } + + public bool Assigned { get; private set; } + + public static RepoItemDragEndedEventArgs Create(long itemId, bool assigned) + { + RepoItemDragEndedEventArgs args = ReferencePool.Acquire(); + args.ItemId = itemId; + args.Assigned = assigned; + return args; + } + + public override void Clear() + { + ItemId = 0; + Assigned = false; + } + } +} diff --git a/src/GeometryTD.Domain/Event/RepoForm/RepoParticipantAssignRequestedEventArgs.cs b/src/GeometryTD.Domain/Event/RepoForm/RepoParticipantAssignRequestedEventArgs.cs new file mode 100644 index 0000000..7f5e344 --- /dev/null +++ b/src/GeometryTD.Domain/Event/RepoForm/RepoParticipantAssignRequestedEventArgs.cs @@ -0,0 +1,27 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public sealed class RepoParticipantAssignRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(RepoParticipantAssignRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public long TowerItemId { get; private set; } + + public static RepoParticipantAssignRequestedEventArgs Create(long towerItemId) + { + RepoParticipantAssignRequestedEventArgs args = + ReferencePool.Acquire(); + args.TowerItemId = towerItemId; + return args; + } + + public override void Clear() + { + TowerItemId = 0; + } + } +} diff --git a/src/GeometryTD.Domain/Event/RepoForm/RepoSellCancelRequestedEventArgs.cs b/src/GeometryTD.Domain/Event/RepoForm/RepoSellCancelRequestedEventArgs.cs new file mode 100644 index 0000000..5ad03b0 --- /dev/null +++ b/src/GeometryTD.Domain/Event/RepoForm/RepoSellCancelRequestedEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public sealed class RepoSellCancelRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(RepoSellCancelRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static RepoSellCancelRequestedEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src/GeometryTD.Domain/Event/RepoForm/RepoSellConfirmRequestedEventArgs.cs b/src/GeometryTD.Domain/Event/RepoForm/RepoSellConfirmRequestedEventArgs.cs new file mode 100644 index 0000000..ce0557b --- /dev/null +++ b/src/GeometryTD.Domain/Event/RepoForm/RepoSellConfirmRequestedEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public sealed class RepoSellConfirmRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(RepoSellConfirmRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static RepoSellConfirmRequestedEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src/GeometryTD.Domain/Event/RepoForm/RepoSellModeToggleRequestedEventArgs.cs b/src/GeometryTD.Domain/Event/RepoForm/RepoSellModeToggleRequestedEventArgs.cs new file mode 100644 index 0000000..e96c3e0 --- /dev/null +++ b/src/GeometryTD.Domain/Event/RepoForm/RepoSellModeToggleRequestedEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public sealed class RepoSellModeToggleRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(RepoSellModeToggleRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static RepoSellModeToggleRequestedEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src/GeometryTD.Domain/Event/Shop/ShopExitRequestedEventArgs.cs b/src/GeometryTD.Domain/Event/Shop/ShopExitRequestedEventArgs.cs new file mode 100644 index 0000000..7af3669 --- /dev/null +++ b/src/GeometryTD.Domain/Event/Shop/ShopExitRequestedEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public sealed class ShopExitRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(ShopExitRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static ShopExitRequestedEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src/GeometryTD.Domain/Event/Shop/ShopInventoryRequestedEventArgs.cs b/src/GeometryTD.Domain/Event/Shop/ShopInventoryRequestedEventArgs.cs new file mode 100644 index 0000000..b299c12 --- /dev/null +++ b/src/GeometryTD.Domain/Event/Shop/ShopInventoryRequestedEventArgs.cs @@ -0,0 +1,21 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public sealed class ShopInventoryRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(ShopInventoryRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public static ShopInventoryRequestedEventArgs Create() + { + return ReferencePool.Acquire(); + } + + public override void Clear() + { + } + } +} diff --git a/src/GeometryTD.Domain/Event/Shop/ShopPurchaseRequestedEventArgs.cs b/src/GeometryTD.Domain/Event/Shop/ShopPurchaseRequestedEventArgs.cs new file mode 100644 index 0000000..eece0c2 --- /dev/null +++ b/src/GeometryTD.Domain/Event/Shop/ShopPurchaseRequestedEventArgs.cs @@ -0,0 +1,26 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public sealed class ShopPurchaseRequestedEventArgs : GameEventArgs + { + public static int EventId => typeof(ShopPurchaseRequestedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int GoodsIndex { get; private set; } + + public static ShopPurchaseRequestedEventArgs Create(int goodsIndex) + { + ShopPurchaseRequestedEventArgs args = ReferencePool.Acquire(); + args.GoodsIndex = goodsIndex; + return args; + } + + public override void Clear() + { + GoodsIndex = -1; + } + } +} diff --git a/src/GeometryTD.Domain/GeometryTD.Domain.csproj b/src/GeometryTD.Domain/GeometryTD.Domain.csproj new file mode 100644 index 0000000..bac7fc5 --- /dev/null +++ b/src/GeometryTD.Domain/GeometryTD.Domain.csproj @@ -0,0 +1,19 @@ + + + + + netstandard2.1 + GeometryTD.Domain + latest + disable + disable + + + + + $(MSBuildThisFileDirectory)/../Libraries/GameFramework.dll + False + + + + diff --git a/src/GeometryTD.Domain/UI/Base/IUIFormController.cs b/src/GeometryTD.Domain/UI/Base/IUIFormController.cs new file mode 100644 index 0000000..e6ee781 --- /dev/null +++ b/src/GeometryTD.Domain/UI/Base/IUIFormController.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.UI +{ + public interface IUIFormController + { + int? OpenUI(object userData = null); + void CloseUI(); + void BindUseCase(IUIUseCase useCase); + } +} diff --git a/src/GeometryTD.Domain/UI/Base/IUIUseCase.cs b/src/GeometryTD.Domain/UI/Base/IUIUseCase.cs new file mode 100644 index 0000000..ad78f8a --- /dev/null +++ b/src/GeometryTD.Domain/UI/Base/IUIUseCase.cs @@ -0,0 +1,6 @@ +namespace GeometryTD.UI +{ + public interface IUIUseCase + { + } +} \ No newline at end of file diff --git a/src/GeometryTD.Domain/UI/Base/UIContext.cs b/src/GeometryTD.Domain/UI/Base/UIContext.cs new file mode 100644 index 0000000..8461da3 --- /dev/null +++ b/src/GeometryTD.Domain/UI/Base/UIContext.cs @@ -0,0 +1,6 @@ +namespace GeometryTD.UI +{ + public class UIContext + { + } +} \ No newline at end of file diff --git a/src/GeometryTD.Infrastructure/GeometryTD.Infrastructure.csproj b/src/GeometryTD.Infrastructure/GeometryTD.Infrastructure.csproj new file mode 100644 index 0000000..91e1d1a --- /dev/null +++ b/src/GeometryTD.Infrastructure/GeometryTD.Infrastructure.csproj @@ -0,0 +1,23 @@ + + + + + netstandard2.1 + GeometryTD.Infrastructure + latest + disable + disable + + + + + $(MSBuildThisFileDirectory)/../Libraries/GameFramework.dll + False + + + + + + + + diff --git a/src/GeometryTD.Presentation/GeometryTD.Presentation.csproj b/src/GeometryTD.Presentation/GeometryTD.Presentation.csproj new file mode 100644 index 0000000..9ec59a8 --- /dev/null +++ b/src/GeometryTD.Presentation/GeometryTD.Presentation.csproj @@ -0,0 +1,16 @@ + + + + + netstandard2.1 + GeometryTD.Presentation + latest + disable + disable + + + + + + + diff --git a/src/GeometryTD.slnx b/src/GeometryTD.slnx new file mode 100644 index 0000000..ba788ff --- /dev/null +++ b/src/GeometryTD.slnx @@ -0,0 +1,2 @@ + + diff --git a/src/Libraries/GameFramework.dll b/src/Libraries/GameFramework.dll new file mode 100644 index 0000000000000000000000000000000000000000..b178b95961756f7e079ca04916fe8bf42ff7e24e GIT binary patch literal 415744 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 L|9jUGDe(URx|3*l literal 0 HcmV?d00001 diff --git a/src/Libraries/GameFramework.dll.meta b/src/Libraries/GameFramework.dll.meta new file mode 100644 index 0000000..4dc4b87 --- /dev/null +++ b/src/Libraries/GameFramework.dll.meta @@ -0,0 +1,19 @@ +fileFormatVersion: 2 +guid: 535b3169b00c6744aaef208fc713a195 +PluginImporter: + externalObjects: {} + serializedVersion: 1 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + platformData: + Any: + enabled: 1 + settings: {} + Editor: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Libraries/GameFramework.xml b/src/Libraries/GameFramework.xml new file mode 100644 index 0000000..b64dca1 --- /dev/null +++ b/src/Libraries/GameFramework.xml @@ -0,0 +1,27039 @@ + + + + GameFramework + + + +

+ 数据提供者。 + + 数据提供者的持有者的类型。 + + + + 初始化数据提供者的新实例。 + + 数据提供者的持有者。 + + + + 获取缓冲二进制流的大小。 + + + + + 读取数据成功事件。 + + + + + 读取数据失败事件。 + + + + + 读取数据更新事件。 + + + + + 读取数据时加载依赖资源事件。 + + + + + 确保二进制流缓存分配足够大小的内存并缓存。 + + 要确保二进制流缓存分配内存的大小。 + + + + 释放缓存的二进制流。 + + + + + 读取数据。 + + 内容资源名称。 + + + + 读取数据。 + + 内容资源名称。 + 加载数据资源的优先级。 + + + + 读取数据。 + + 内容资源名称。 + 用户自定义数据。 + + + + 读取数据。 + + 内容资源名称。 + 加载数据资源的优先级。 + 用户自定义数据。 + + + + 解析内容。 + + 要解析的内容字符串。 + 是否解析内容成功。 + + + + 解析内容。 + + 要解析的内容字符串。 + 用户自定义数据。 + 是否解析内容成功。 + + + + 解析内容。 + + 要解析的内容二进制流。 + 是否解析内容成功。 + + + + 解析内容。 + + 要解析的内容二进制流。 + 用户自定义数据。 + 是否解析内容成功。 + + + + 解析内容。 + + 要解析的内容二进制流。 + 内容二进制流的起始位置。 + 内容二进制流的长度。 + 是否解析内容成功。 + + + + 解析内容。 + + 要解析的内容二进制流。 + 内容二进制流的起始位置。 + 内容二进制流的长度。 + 用户自定义数据。 + 是否解析内容成功。 + + + + 设置资源管理器。 + + 资源管理器。 + + + + 设置数据提供者辅助器。 + + 数据提供者辅助器。 + + + + 数据提供者创建器。 + + + + + 获取缓冲二进制流的大小。 + + 数据提供者的持有者的类型。 + 缓冲二进制流的大小。 + + + + 确保二进制流缓存分配足够大小的内存并缓存。 + + 数据提供者的持有者的类型。 + 要确保二进制流缓存分配内存的大小。 + + + + 释放缓存的二进制流。 + + 数据提供者的持有者的类型。 + + + + 创建数据提供者。 + + 数据提供者的持有者的类型。 + 数据提供者的持有者。 + 资源管理器。 + 数据提供者辅助器。 + 创建的数据提供者。 + + + + 数据提供者接口。 + + 数据提供者的持有者的类型。 + + + + 读取数据成功事件。 + + + + + 读取数据失败事件。 + + + + + 读取数据更新事件。 + + + + + 读取数据时加载依赖资源事件。 + + + + + 读取数据。 + + 内容资源名称。 + + + + 读取数据。 + + 内容资源名称。 + 加载数据资源的优先级。 + + + + 读取数据。 + + 内容资源名称。 + 用户自定义数据。 + + + + 读取数据。 + + 内容资源名称。 + 加载数据资源的优先级。 + 用户自定义数据。 + + + + 解析内容。 + + 要解析的内容字符串。 + 是否解析内容成功。 + + + + 解析内容。 + + 要解析的内容字符串。 + 用户自定义数据。 + 是否解析内容成功。 + + + + 解析内容。 + + 要解析的内容二进制流。 + 是否解析内容成功。 + + + + 解析内容。 + + 要解析的内容二进制流。 + 用户自定义数据。 + 是否解析内容成功。 + + + + 解析内容。 + + 要解析的内容二进制流。 + 内容二进制流的起始位置。 + 内容二进制流的长度。 + 是否解析内容成功。 + + + + 解析内容。 + + 要解析的内容二进制流。 + 内容二进制流的起始位置。 + 内容二进制流的长度。 + 用户自定义数据。 + 是否解析内容成功。 + + + + 数据提供者辅助器接口。 + + + + + 读取数据。 + + 数据提供者的持有者。 + 内容资源名称。 + 内容资源。 + 用户自定义数据。 + 是否读取数据成功。 + + + + 读取数据。 + + 数据提供者的持有者。 + 内容资源名称。 + 内容二进制流。 + 内容二进制流的起始位置。 + 内容二进制流的长度。 + 用户自定义数据。 + 是否读取数据成功。 + + + + 解析内容。 + + 数据提供者的持有者。 + 要解析的内容字符串。 + 用户自定义数据。 + 是否解析内容成功。 + + + + 解析内容。 + + 数据提供者的持有者。 + 要解析的内容二进制流。 + 内容二进制流的起始位置。 + 内容二进制流的长度。 + 用户自定义数据。 + 是否解析内容成功。 + + + + 释放内容资源。 + + 数据提供者的持有者。 + 要释放的内容资源。 + + + + 读取数据时加载依赖资源事件。 + + + + + 初始化读取数据时加载依赖资源事件的新实例。 + + + + + 获取内容资源名称。 + + + + + 获取被加载的依赖资源名称。 + + + + + 获取当前已加载依赖资源数量。 + + + + + 获取总共加载依赖资源数量。 + + + + + 获取用户自定义数据。 + + + + + 创建读取数据时加载依赖资源事件。 + + 内容资源名称。 + 被加载的依赖资源名称。 + 当前已加载依赖资源数量。 + 总共加载依赖资源数量。 + 用户自定义数据。 + 创建的读取数据时加载依赖资源事件。 + + + + 清理读取数据时加载依赖资源事件。 + + + + + 读取数据失败事件。 + + + + + 初始化读取数据失败事件的新实例。 + + + + + 获取内容资源名称。 + + + + + 获取错误信息。 + + + + + 获取用户自定义数据。 + + + + + 创建读取数据失败事件。 + + 内容资源名称。 + 错误信息。 + 用户自定义数据。 + 创建的读取数据失败事件。 + + + + 清理读取数据失败事件。 + + + + + 读取数据成功事件。 + + + + + 初始化读取数据成功事件的新实例。 + + + + + 获取内容资源名称。 + + + + + 获取加载持续时间。 + + + + + 获取用户自定义数据。 + + + + + 创建读取数据成功事件。 + + 内容资源名称。 + 加载持续时间。 + 用户自定义数据。 + 创建的读取数据成功事件。 + + + + 清理读取数据成功事件。 + + + + + 读取数据更新事件。 + + + + + 初始化读取数据更新事件的新实例。 + + + + + 获取内容资源名称。 + + + + + 获取读取数据进度。 + + + + + 获取用户自定义数据。 + + + + + 创建读取数据更新事件。 + + 内容资源名称。 + 读取数据进度。 + 用户自定义数据。 + 创建的读取数据更新事件。 + + + + 清理读取数据更新事件。 + + + + + 类型和名称的组合值。 + + + + + 初始化类型和名称的组合值的新实例。 + + 类型。 + + + + 初始化类型和名称的组合值的新实例。 + + 类型。 + 名称。 + + + + 获取类型。 + + + + + 获取名称。 + + + + + 获取类型和名称的组合值字符串。 + + 类型和名称的组合值字符串。 + + + + 获取对象的哈希值。 + + 对象的哈希值。 + + + + 比较对象是否与自身相等。 + + 要比较的对象。 + 被比较的对象是否与自身相等。 + + + + 比较对象是否与自身相等。 + + 要比较的对象。 + 被比较的对象是否与自身相等。 + + + + 判断两个对象是否相等。 + + 值 a。 + 值 b。 + 两个对象是否相等。 + + + + 判断两个对象是否不相等。 + + 值 a。 + 值 b。 + 两个对象是否不相等。 + + + + 事件基类。 + + + + + 获取类型编号。 + + + + + 事件池。 + + 事件类型。 + + + + 初始化事件池的新实例。 + + 事件池模式。 + + + + 获取事件处理函数的数量。 + + + + + 获取事件数量。 + + + + + 事件池轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理事件池。 + + + + + 清理事件。 + + + + + 获取事件处理函数的数量。 + + 事件类型编号。 + 事件处理函数的数量。 + + + + 检查是否存在事件处理函数。 + + 事件类型编号。 + 要检查的事件处理函数。 + 是否存在事件处理函数。 + + + + 订阅事件处理函数。 + + 事件类型编号。 + 要订阅的事件处理函数。 + + + + 取消订阅事件处理函数。 + + 事件类型编号。 + 要取消订阅的事件处理函数。 + + + + 设置默认事件处理函数。 + + 要设置的默认事件处理函数。 + + + + 抛出事件,这个操作是线程安全的,即使不在主线程中抛出,也可保证在主线程中回调事件处理函数,但事件会在抛出后的下一帧分发。 + + 事件源。 + 事件参数。 + + + + 抛出事件立即模式,这个操作不是线程安全的,事件会立刻分发。 + + 事件源。 + 事件参数。 + + + + 处理事件结点。 + + 事件源。 + 事件参数。 + + + + 事件结点。 + + + + + 事件池模式。 + + + + + 默认事件池模式,即必须存在有且只有一个事件处理函数。 + + + + + 允许不存在事件处理函数。 + + + + + 允许存在多个事件处理函数。 + + + + + 允许存在重复的事件处理函数。 + + + + + 封装一个方法,该方法不具有参数并且不返回值。 + + + + + 封装一个方法,该方法只有一个参数并且不返回值。 + + 此委托封装的方法的参数类型。 + 此委托封装的方法的参数。 + + + + 封装一个方法,该方法具有两个参数并且不返回值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + + + + 封装一个方法,该方法具有三个参数并且不返回值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + + + + 封装一个方法,该方法具有四个参数并且不返回值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + + + + 封装一个方法,该方法具有五个参数并且不返回值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + + + + 封装一个方法,该方法具有六个参数并且不返回值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + + + + 封装一个方法,该方法具有七个参数并且不返回值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + + + + 封装一个方法,该方法具有八个参数并且不返回值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第八个参数的类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的第八个参数。 + + + + 封装一个方法,该方法具有九个参数并且不返回值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第八个参数的类型。 + 此委托封装的方法的第九个参数的类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的第八个参数。 + 此委托封装的方法的第九个参数。 + + + + 封装一个方法,该方法具有十个参数并且不返回值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第八个参数的类型。 + 此委托封装的方法的第九个参数的类型。 + 此委托封装的方法的第十个参数的类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的第八个参数。 + 此委托封装的方法的第九个参数。 + 此委托封装的方法的第十个参数。 + + + + 封装一个方法,该方法具有十一个参数并且不返回值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第八个参数的类型。 + 此委托封装的方法的第九个参数的类型。 + 此委托封装的方法的第十个参数的类型。 + 此委托封装的方法的第十一个参数的类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的第八个参数。 + 此委托封装的方法的第九个参数。 + 此委托封装的方法的第十个参数。 + 此委托封装的方法的第十一个参数。 + + + + 封装一个方法,该方法具有十二个参数并且不返回值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第八个参数的类型。 + 此委托封装的方法的第九个参数的类型。 + 此委托封装的方法的第十个参数的类型。 + 此委托封装的方法的第十一个参数的类型。 + 此委托封装的方法的第十二个参数的类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的第八个参数。 + 此委托封装的方法的第九个参数。 + 此委托封装的方法的第十个参数。 + 此委托封装的方法的第十一个参数。 + 此委托封装的方法的第十二个参数。 + + + + 封装一个方法,该方法具有十三个参数并且不返回值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第八个参数的类型。 + 此委托封装的方法的第九个参数的类型。 + 此委托封装的方法的第十个参数的类型。 + 此委托封装的方法的第十一个参数的类型。 + 此委托封装的方法的第十二个参数的类型。 + 此委托封装的方法的第十三个参数的类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的第八个参数。 + 此委托封装的方法的第九个参数。 + 此委托封装的方法的第十个参数。 + 此委托封装的方法的第十一个参数。 + 此委托封装的方法的第十二个参数。 + 此委托封装的方法的第十三个参数。 + + + + 封装一个方法,该方法具有十四个参数并且不返回值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第八个参数的类型。 + 此委托封装的方法的第九个参数的类型。 + 此委托封装的方法的第十个参数的类型。 + 此委托封装的方法的第十一个参数的类型。 + 此委托封装的方法的第十二个参数的类型。 + 此委托封装的方法的第十三个参数的类型。 + 此委托封装的方法的第十四个参数的类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的第八个参数。 + 此委托封装的方法的第九个参数。 + 此委托封装的方法的第十个参数。 + 此委托封装的方法的第十一个参数。 + 此委托封装的方法的第十二个参数。 + 此委托封装的方法的第十三个参数。 + 此委托封装的方法的第十四个参数。 + + + + 封装一个方法,该方法具有十五个参数并且不返回值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第八个参数的类型。 + 此委托封装的方法的第九个参数的类型。 + 此委托封装的方法的第十个参数的类型。 + 此委托封装的方法的第十一个参数的类型。 + 此委托封装的方法的第十二个参数的类型。 + 此委托封装的方法的第十三个参数的类型。 + 此委托封装的方法的第十四个参数的类型。 + 此委托封装的方法的第十五个参数的类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的第八个参数。 + 此委托封装的方法的第九个参数。 + 此委托封装的方法的第十个参数。 + 此委托封装的方法的第十一个参数。 + 此委托封装的方法的第十二个参数。 + 此委托封装的方法的第十三个参数。 + 此委托封装的方法的第十四个参数。 + 此委托封装的方法的第十五个参数。 + + + + 封装一个方法,该方法具有十六个参数并且不返回值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第八个参数的类型。 + 此委托封装的方法的第九个参数的类型。 + 此委托封装的方法的第十个参数的类型。 + 此委托封装的方法的第十一个参数的类型。 + 此委托封装的方法的第十二个参数的类型。 + 此委托封装的方法的第十三个参数的类型。 + 此委托封装的方法的第十四个参数的类型。 + 此委托封装的方法的第十五个参数的类型。 + 此委托封装的方法的第十六个参数的类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的第八个参数。 + 此委托封装的方法的第九个参数。 + 此委托封装的方法的第十个参数。 + 此委托封装的方法的第十一个参数。 + 此委托封装的方法的第十二个参数。 + 此委托封装的方法的第十三个参数。 + 此委托封装的方法的第十四个参数。 + 此委托封装的方法的第十五个参数。 + 此委托封装的方法的第十六个参数。 + + + + 游戏框架入口。 + + + + + 所有游戏框架模块轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理所有游戏框架模块。 + + + + + 获取游戏框架模块。 + + 要获取的游戏框架模块类型。 + 要获取的游戏框架模块。 + 如果要获取的游戏框架模块不存在,则自动创建该游戏框架模块。 + + + + 获取游戏框架模块。 + + 要获取的游戏框架模块类型。 + 要获取的游戏框架模块。 + 如果要获取的游戏框架模块不存在,则自动创建该游戏框架模块。 + + + + 创建游戏框架模块。 + + 要创建的游戏框架模块类型。 + 要创建的游戏框架模块。 + + + + 游戏框架中包含事件数据的类的基类。 + + + + + 初始化游戏框架中包含事件数据的类的新实例。 + + + + + 清理引用。 + + + + + 游戏框架异常类。 + + + + + 初始化游戏框架异常类的新实例。 + + + + + 使用指定错误消息初始化游戏框架异常类的新实例。 + + 描述错误的消息。 + + + + 使用指定错误消息和对作为此异常原因的内部异常的引用来初始化游戏框架异常类的新实例。 + + 解释异常原因的错误消息。 + 导致当前异常的异常。如果 innerException 参数不为空引用,则在处理内部异常的 catch 块中引发当前异常。 + + + + 用序列化数据初始化游戏框架异常类的新实例。 + + 存有有关所引发异常的序列化的对象数据。 + 包含有关源或目标的上下文信息。 + + + + 封装一个方法,该方法不具有参数,但却返回 TResult 参数指定的类型的值。 + + 此委托封装的方法的返回值类型。 + 此委托封装的方法的返回值。 + + + + 封装一个方法,该方法具有一个参数,并返回 TResult 参数所指定的类型的值。 + + 此委托封装的方法的参数类型。 + 此委托封装的方法的返回值类型。 + 此委托封装的方法的参数。 + 此委托封装的方法的返回值。 + + + + 封装一个方法,该方法具有两个参数,并返回 TResult 参数所指定的类型的值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的返回值类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的返回值。 + + + + 封装一个方法,该方法具有三个参数,并返回 TResult 参数所指定的类型的值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的返回值类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的返回值。 + + + + 封装一个方法,该方法具有四个参数,并返回 TResult 参数所指定的类型的值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的返回值类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的返回值。 + + + + 封装一个方法,该方法具有五个参数,并返回 TResult 参数所指定的类型的值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的返回值类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的返回值。 + + + + 封装一个方法,该方法具有六个参数,并返回 TResult 参数所指定的类型的值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的返回值类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的返回值。 + + + + 封装一个方法,该方法具有七个参数,并返回 TResult 参数所指定的类型的值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的返回值类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的返回值。 + + + + 封装一个方法,该方法具有八个参数,并返回 TResult 参数所指定的类型的值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第八个参数的类型。 + 此委托封装的方法的返回值类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的第八个参数。 + 此委托封装的方法的返回值。 + + + + 封装一个方法,该方法具有九个参数,并返回 TResult 参数所指定的类型的值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第八个参数的类型。 + 此委托封装的方法的第九个参数的类型。 + 此委托封装的方法的返回值类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的第八个参数。 + 此委托封装的方法的第九个参数。 + 此委托封装的方法的返回值。 + + + + 封装一个方法,该方法具有十个参数,并返回 TResult 参数所指定的类型的值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第八个参数的类型。 + 此委托封装的方法的第九个参数的类型。 + 此委托封装的方法的第十个参数的类型。 + 此委托封装的方法的返回值类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的第八个参数。 + 此委托封装的方法的第九个参数。 + 此委托封装的方法的第十个参数。 + 此委托封装的方法的返回值。 + + + + 封装一个方法,该方法具有十一个参数,并返回 TResult 参数所指定的类型的值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第八个参数的类型。 + 此委托封装的方法的第九个参数的类型。 + 此委托封装的方法的第十个参数的类型。 + 此委托封装的方法的第十一个参数的类型。 + 此委托封装的方法的返回值类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的第八个参数。 + 此委托封装的方法的第九个参数。 + 此委托封装的方法的第十个参数。 + 此委托封装的方法的第十一个参数。 + 此委托封装的方法的返回值。 + + + + 封装一个方法,该方法具有十二个参数,并返回 TResult 参数所指定的类型的值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第八个参数的类型。 + 此委托封装的方法的第九个参数的类型。 + 此委托封装的方法的第十个参数的类型。 + 此委托封装的方法的第十一个参数的类型。 + 此委托封装的方法的第十二个参数的类型。 + 此委托封装的方法的返回值类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的第八个参数。 + 此委托封装的方法的第九个参数。 + 此委托封装的方法的第十个参数。 + 此委托封装的方法的第十一个参数。 + 此委托封装的方法的第十二个参数。 + 此委托封装的方法的返回值。 + + + + 封装一个方法,该方法具有十三个参数,并返回 TResult 参数所指定的类型的值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第八个参数的类型。 + 此委托封装的方法的第九个参数的类型。 + 此委托封装的方法的第十个参数的类型。 + 此委托封装的方法的第十一个参数的类型。 + 此委托封装的方法的第十二个参数的类型。 + 此委托封装的方法的第十三个参数的类型。 + 此委托封装的方法的返回值类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的第八个参数。 + 此委托封装的方法的第九个参数。 + 此委托封装的方法的第十个参数。 + 此委托封装的方法的第十一个参数。 + 此委托封装的方法的第十二个参数。 + 此委托封装的方法的第十三个参数。 + 此委托封装的方法的返回值。 + + + + 封装一个方法,该方法具有十四个参数,并返回 TResult 参数所指定的类型的值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第八个参数的类型。 + 此委托封装的方法的第九个参数的类型。 + 此委托封装的方法的第十个参数的类型。 + 此委托封装的方法的第十一个参数的类型。 + 此委托封装的方法的第十二个参数的类型。 + 此委托封装的方法的第十三个参数的类型。 + 此委托封装的方法的第十四个参数的类型。 + 此委托封装的方法的返回值类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的第八个参数。 + 此委托封装的方法的第九个参数。 + 此委托封装的方法的第十个参数。 + 此委托封装的方法的第十一个参数。 + 此委托封装的方法的第十二个参数。 + 此委托封装的方法的第十三个参数。 + 此委托封装的方法的第十四个参数。 + 此委托封装的方法的返回值。 + + + + 封装一个方法,该方法具有十五个参数,并返回 TResult 参数所指定的类型的值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第八个参数的类型。 + 此委托封装的方法的第九个参数的类型。 + 此委托封装的方法的第十个参数的类型。 + 此委托封装的方法的第十一个参数的类型。 + 此委托封装的方法的第十二个参数的类型。 + 此委托封装的方法的第十三个参数的类型。 + 此委托封装的方法的第十四个参数的类型。 + 此委托封装的方法的第十五个参数的类型。 + 此委托封装的方法的返回值类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的第八个参数。 + 此委托封装的方法的第九个参数。 + 此委托封装的方法的第十个参数。 + 此委托封装的方法的第十一个参数。 + 此委托封装的方法的第十二个参数。 + 此委托封装的方法的第十三个参数。 + 此委托封装的方法的第十四个参数。 + 此委托封装的方法的第十五个参数。 + 此委托封装的方法的返回值。 + + + + 封装一个方法,该方法具有十六个参数,并返回 TResult 参数所指定的类型的值。 + + 此委托封装的方法的第一个参数的类型。 + 此委托封装的方法的第二个参数的类型。 + 此委托封装的方法的第三个参数的类型。 + 此委托封装的方法的第四个参数的类型。 + 此委托封装的方法的第五个参数的类型。 + 此委托封装的方法的第六个参数的类型。 + 此委托封装的方法的第七个参数的类型。 + 此委托封装的方法的第八个参数的类型。 + 此委托封装的方法的第九个参数的类型。 + 此委托封装的方法的第十个参数的类型。 + 此委托封装的方法的第十一个参数的类型。 + 此委托封装的方法的第十二个参数的类型。 + 此委托封装的方法的第十三个参数的类型。 + 此委托封装的方法的第十四个参数的类型。 + 此委托封装的方法的第十五个参数的类型。 + 此委托封装的方法的第十六个参数的类型。 + 此委托封装的方法的返回值类型。 + 此委托封装的方法的第一个参数。 + 此委托封装的方法的第二个参数。 + 此委托封装的方法的第三个参数。 + 此委托封装的方法的第四个参数。 + 此委托封装的方法的第五个参数。 + 此委托封装的方法的第六个参数。 + 此委托封装的方法的第七个参数。 + 此委托封装的方法的第八个参数。 + 此委托封装的方法的第九个参数。 + 此委托封装的方法的第十个参数。 + 此委托封装的方法的第十一个参数。 + 此委托封装的方法的第十二个参数。 + 此委托封装的方法的第十三个参数。 + 此委托封装的方法的第十四个参数。 + 此委托封装的方法的第十五个参数。 + 此委托封装的方法的第十六个参数。 + 此委托封装的方法的返回值。 + + + + 游戏框架链表类。 + + 指定链表的元素类型。 + + + + 初始化游戏框架链表类的新实例。 + + + + + 获取链表中实际包含的结点数量。 + + + + + 获取链表结点缓存数量。 + + + + + 获取链表的第一个结点。 + + + + + 获取链表的最后一个结点。 + + + + + 获取一个值,该值指示 ICollection`1 是否为只读。 + + + + + 获取可用于同步对 ICollection 的访问的对象。 + + + + + 获取一个值,该值指示是否同步对 ICollection 的访问(线程安全)。 + + + + + 在链表中指定的现有结点后添加包含指定值的新结点。 + + 指定的现有结点。 + 指定值。 + 包含指定值的新结点。 + + + + 在链表中指定的现有结点后添加指定的新结点。 + + 指定的现有结点。 + 指定的新结点。 + + + + 在链表中指定的现有结点前添加包含指定值的新结点。 + + 指定的现有结点。 + 指定值。 + 包含指定值的新结点。 + + + + 在链表中指定的现有结点前添加指定的新结点。 + + 指定的现有结点。 + 指定的新结点。 + + + + 在链表的开头处添加包含指定值的新结点。 + + 指定值。 + 包含指定值的新结点。 + + + + 在链表的开头处添加指定的新结点。 + + 指定的新结点。 + + + + 在链表的结尾处添加包含指定值的新结点。 + + 指定值。 + 包含指定值的新结点。 + + + + 在链表的结尾处添加指定的新结点。 + + 指定的新结点。 + + + + 从链表中移除所有结点。 + + + + + 清除链表结点缓存。 + + + + + 确定某值是否在链表中。 + + 指定值。 + 某值是否在链表中。 + + + + 从目标数组的指定索引处开始将整个链表复制到兼容的一维数组。 + + 一维数组,它是从链表复制的元素的目标。数组必须具有从零开始的索引。 + array 中从零开始的索引,从此处开始复制。 + + + + 从特定的 ICollection 索引开始,将数组的元素复制到一个数组中。 + + 一维数组,它是从 ICollection 复制的元素的目标。数组必须具有从零开始的索引。 + array 中从零开始的索引,从此处开始复制。 + + + + 查找包含指定值的第一个结点。 + + 要查找的指定值。 + 包含指定值的第一个结点。 + + + + 查找包含指定值的最后一个结点。 + + 要查找的指定值。 + 包含指定值的最后一个结点。 + + + + 从链表中移除指定值的第一个匹配项。 + + 指定值。 + 是否移除成功。 + + + + 从链表中移除指定的结点。 + + 指定的结点。 + + + + 移除位于链表开头处的结点。 + + + + + 移除位于链表结尾处的结点。 + + + + + 返回循环访问集合的枚举数。 + + 循环访问集合的枚举数。 + + + + 将值添加到 ICollection`1 的结尾处。 + + 要添加的值。 + + + + 返回循环访问集合的枚举数。 + + 循环访问集合的枚举数。 + + + + 返回循环访问集合的枚举数。 + + 循环访问集合的枚举数。 + + + + 循环访问集合的枚举数。 + + + + + 获取当前结点。 + + + + + 获取当前的枚举数。 + + + + + 清理枚举数。 + + + + + 获取下一个结点。 + + 返回下一个结点。 + + + + 重置枚举数。 + + + + + 游戏框架链表范围。 + + 指定链表范围的元素类型。 + + + + 初始化游戏框架链表范围的新实例。 + + 链表范围的开始结点。 + 链表范围的终结标记结点。 + + + + 获取链表范围是否有效。 + + + + + 获取链表范围的开始结点。 + + + + + 获取链表范围的终结标记结点。 + + + + + 获取链表范围的结点数量。 + + + + + 检查是否包含指定值。 + + 要检查的值。 + 是否包含指定值。 + + + + 返回循环访问集合的枚举数。 + + 循环访问集合的枚举数。 + + + + 返回循环访问集合的枚举数。 + + 循环访问集合的枚举数。 + + + + 返回循环访问集合的枚举数。 + + 循环访问集合的枚举数。 + + + + 循环访问集合的枚举数。 + + + + + 获取当前结点。 + + + + + 获取当前的枚举数。 + + + + + 清理枚举数。 + + + + + 获取下一个结点。 + + 返回下一个结点。 + + + + 重置枚举数。 + + + + + 游戏框架模块抽象类。 + + + + + 获取游戏框架模块优先级。 + + 优先级较高的模块会优先轮询,并且关闭操作会后进行。 + + + + 游戏框架模块轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理游戏框架模块。 + + + + + 游戏框架多值字典类。 + + 指定多值字典的主键类型。 + 指定多值字典的值类型。 + + + + 初始化游戏框架多值字典类的新实例。 + + + + + 获取多值字典中实际包含的主键数量。 + + + + + 获取多值字典中指定主键的范围。 + + 指定的主键。 + 指定主键的范围。 + + + + 清理多值字典。 + + + + + 检查多值字典中是否包含指定主键。 + + 要检查的主键。 + 多值字典中是否包含指定主键。 + + + + 检查多值字典中是否包含指定值。 + + 要检查的主键。 + 要检查的值。 + 多值字典中是否包含指定值。 + + + + 尝试获取多值字典中指定主键的范围。 + + 指定的主键。 + 指定主键的范围。 + 是否获取成功。 + + + + 向指定的主键增加指定的值。 + + 指定的主键。 + 指定的值。 + + + + 从指定的主键中移除指定的值。 + + 指定的主键。 + 指定的值。 + 是否移除成功。 + + + + 从指定的主键中移除所有的值。 + + 指定的主键。 + 是否移除成功。 + + + + 返回循环访问集合的枚举数。 + + 循环访问集合的枚举数。 + + + + 返回循环访问集合的枚举数。 + + 循环访问集合的枚举数。 + + + + 返回循环访问集合的枚举数。 + + 循环访问集合的枚举数。 + + + + 循环访问集合的枚举数。 + + + + + 获取当前结点。 + + + + + 获取当前的枚举数。 + + + + + 清理枚举数。 + + + + + 获取下一个结点。 + + 返回下一个结点。 + + + + 重置枚举数。 + + + + + 游戏框架序列化器基类。 + + 要序列化的数据类型。 + + + + 初始化游戏框架序列化器基类的新实例。 + + + + + 序列化回调函数。 + + 目标流。 + 要序列化的数据。 + 是否序列化数据成功。 + + + + 反序列化回调函数。 + + 指定流。 + 反序列化的数据。 + + + + 尝试从指定流获取指定键的值回调函数。 + + 指定流。 + 指定键。 + 指定键的值。 + 是否从指定流获取指定键的值成功。 + + + + 注册序列化回调函数。 + + 序列化回调函数的版本。 + 序列化回调函数。 + + + + 注册反序列化回调函数。 + + 反序列化回调函数的版本。 + 反序列化回调函数。 + + + + 注册尝试从指定流获取指定键的值回调函数。 + + 尝试从指定流获取指定键的值回调函数的版本。 + 尝试从指定流获取指定键的值回调函数。 + + + + 序列化数据到目标流中。 + + 目标流。 + 要序列化的数据。 + 是否序列化数据成功。 + + + + 序列化数据到目标流中。 + + 目标流。 + 要序列化的数据。 + 序列化回调函数的版本。 + 是否序列化数据成功。 + + + + 从指定流反序列化数据。 + + 指定流。 + 反序列化的数据。 + + + + 尝试从指定流获取指定键的值。 + + 指定流。 + 指定键。 + 指定键的值。 + 是否从指定流获取指定键的值成功。 + + + + 获取数据头标识。 + + 数据头标识。 + + + + 游戏框架日志类。 + + + + + 设置游戏框架日志辅助器。 + + 要设置的游戏框架日志辅助器。 + + + + 打印调试级别日志,用于记录调试类日志信息。 + + 日志内容。 + + + + 打印调试级别日志,用于记录调试类日志信息。 + + 日志内容。 + + + + 打印调试级别日志,用于记录调试类日志信息。 + + 日志参数的类型。 + 日志格式。 + 日志参数。 + + + + 打印调试级别日志,用于记录调试类日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + + + + 打印调试级别日志,用于记录调试类日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + + + + 打印调试级别日志,用于记录调试类日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + + + + 打印调试级别日志,用于记录调试类日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + + + + 打印调试级别日志,用于记录调试类日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + + + + 打印调试级别日志,用于记录调试类日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + + + + 打印调试级别日志,用于记录调试类日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + + + + 打印调试级别日志,用于记录调试类日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + + + + 打印调试级别日志,用于记录调试类日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + + + + 打印调试级别日志,用于记录调试类日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + + + + 打印调试级别日志,用于记录调试类日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + + + + 打印调试级别日志,用于记录调试类日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + + + + 打印调试级别日志,用于记录调试类日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志参数 14 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + 日志参数 14。 + + + + 打印调试级别日志,用于记录调试类日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志参数 14 的类型。 + 日志参数 15 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + 日志参数 14。 + 日志参数 15。 + + + + 打印调试级别日志,用于记录调试类日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志参数 14 的类型。 + 日志参数 15 的类型。 + 日志参数 16 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + 日志参数 14。 + 日志参数 15。 + 日志参数 16。 + + + + 打印信息级别日志,用于记录程序正常运行日志信息。 + + 日志内容。 + + + + 打印信息级别日志,用于记录程序正常运行日志信息。 + + 日志内容。 + + + + 打印信息级别日志,用于记录程序正常运行日志信息。 + + 日志参数的类型。 + 日志格式。 + 日志参数。 + + + + 打印信息级别日志,用于记录程序正常运行日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + + + + 打印信息级别日志,用于记录程序正常运行日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + + + + 打印信息级别日志,用于记录程序正常运行日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + + + + 打印信息级别日志,用于记录程序正常运行日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + + + + 打印信息级别日志,用于记录程序正常运行日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + + + + 打印信息级别日志,用于记录程序正常运行日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + + + + 打印信息级别日志,用于记录程序正常运行日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + + + + 打印信息级别日志,用于记录程序正常运行日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + + + + 打印信息级别日志,用于记录程序正常运行日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + + + + 打印信息级别日志,用于记录程序正常运行日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + + + + 打印信息级别日志,用于记录程序正常运行日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + + + + 打印信息级别日志,用于记录程序正常运行日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + + + + 打印信息级别日志,用于记录程序正常运行日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志参数 14 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + 日志参数 14。 + + + + 打印信息级别日志,用于记录程序正常运行日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志参数 14 的类型。 + 日志参数 15 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + 日志参数 14。 + 日志参数 15。 + + + + 打印信息级别日志,用于记录程序正常运行日志信息。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志参数 14 的类型。 + 日志参数 15 的类型。 + 日志参数 16 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + 日志参数 14。 + 日志参数 15。 + 日志参数 16。 + + + + 打印警告级别日志,建议在发生局部功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志内容。 + + + + 打印警告级别日志,建议在发生局部功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志内容。 + + + + 打印警告级别日志,建议在发生局部功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数的类型。 + 日志格式。 + 日志参数。 + + + + 打印警告级别日志,建议在发生局部功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + + + + 打印警告级别日志,建议在发生局部功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + + + + 打印警告级别日志,建议在发生局部功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + + + + 打印警告级别日志,建议在发生局部功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + + + + 打印警告级别日志,建议在发生局部功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + + + + 打印警告级别日志,建议在发生局部功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + + + + 打印警告级别日志,建议在发生局部功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + + + + 打印警告级别日志,建议在发生局部功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + + + + 打印警告级别日志,建议在发生局部功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + + + + 打印警告级别日志,建议在发生局部功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + + + + 打印警告级别日志,建议在发生局部功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + + + + 打印警告级别日志,建议在发生局部功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + + + + 打印警告级别日志,建议在发生局部功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志参数 14 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + 日志参数 14。 + + + + 打印警告级别日志,建议在发生局部功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志参数 14 的类型。 + 日志参数 15 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + 日志参数 14。 + 日志参数 15。 + + + + 打印警告级别日志,建议在发生局部功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志参数 14 的类型。 + 日志参数 15 的类型。 + 日志参数 16 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + 日志参数 14。 + 日志参数 15。 + 日志参数 16。 + + + + 打印错误级别日志,建议在发生功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志内容。 + + + + 打印错误级别日志,建议在发生功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志内容。 + + + + 打印错误级别日志,建议在发生功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数的类型。 + 日志格式。 + 日志参数。 + + + + 打印错误级别日志,建议在发生功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + + + + 打印错误级别日志,建议在发生功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + + + + 打印错误级别日志,建议在发生功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + + + + 打印错误级别日志,建议在发生功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + + + + 打印错误级别日志,建议在发生功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + + + + 打印错误级别日志,建议在发生功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + + + + 打印错误级别日志,建议在发生功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + + + + 打印错误级别日志,建议在发生功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + + + + 打印错误级别日志,建议在发生功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + + + + 打印错误级别日志,建议在发生功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + + + + 打印错误级别日志,建议在发生功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + + + + 打印错误级别日志,建议在发生功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + + + + 打印错误级别日志,建议在发生功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志参数 14 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + 日志参数 14。 + + + + 打印错误级别日志,建议在发生功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志参数 14 的类型。 + 日志参数 15 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + 日志参数 14。 + 日志参数 15。 + + + + 打印错误级别日志,建议在发生功能逻辑错误,但尚不会导致游戏崩溃或异常时使用。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志参数 14 的类型。 + 日志参数 15 的类型。 + 日志参数 16 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + 日志参数 14。 + 日志参数 15。 + 日志参数 16。 + + + + 打印严重错误级别日志,建议在发生严重错误,可能导致游戏崩溃或异常时使用,此时应尝试重启进程或重建游戏框架。 + + 日志内容。 + + + + 打印严重错误级别日志,建议在发生严重错误,可能导致游戏崩溃或异常时使用,此时应尝试重启进程或重建游戏框架。 + + 日志内容。 + + + + 打印严重错误级别日志,建议在发生严重错误,可能导致游戏崩溃或异常时使用,此时应尝试重启进程或重建游戏框架。 + + 日志参数的类型。 + 日志格式。 + 日志参数。 + + + + 打印严重错误级别日志,建议在发生严重错误,可能导致游戏崩溃或异常时使用,此时应尝试重启进程或重建游戏框架。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + + + + 打印严重错误级别日志,建议在发生严重错误,可能导致游戏崩溃或异常时使用,此时应尝试重启进程或重建游戏框架。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + + + + 打印严重错误级别日志,建议在发生严重错误,可能导致游戏崩溃或异常时使用,此时应尝试重启进程或重建游戏框架。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + + + + 打印严重错误级别日志,建议在发生严重错误,可能导致游戏崩溃或异常时使用,此时应尝试重启进程或重建游戏框架。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + + + + 打印严重错误级别日志,建议在发生严重错误,可能导致游戏崩溃或异常时使用,此时应尝试重启进程或重建游戏框架。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + + + + 打印严重错误级别日志,建议在发生严重错误,可能导致游戏崩溃或异常时使用,此时应尝试重启进程或重建游戏框架。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + + + + 打印严重错误级别日志,建议在发生严重错误,可能导致游戏崩溃或异常时使用,此时应尝试重启进程或重建游戏框架。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + + + + 打印严重错误级别日志,建议在发生严重错误,可能导致游戏崩溃或异常时使用,此时应尝试重启进程或重建游戏框架。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + + + + 打印严重错误级别日志,建议在发生严重错误,可能导致游戏崩溃或异常时使用,此时应尝试重启进程或重建游戏框架。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + + + + 打印严重错误级别日志,建议在发生严重错误,可能导致游戏崩溃或异常时使用,此时应尝试重启进程或重建游戏框架。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + + + + 打印严重错误级别日志,建议在发生严重错误,可能导致游戏崩溃或异常时使用,此时应尝试重启进程或重建游戏框架。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + + + + 打印严重错误级别日志,建议在发生严重错误,可能导致游戏崩溃或异常时使用,此时应尝试重启进程或重建游戏框架。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + + + + 打印严重错误级别日志,建议在发生严重错误,可能导致游戏崩溃或异常时使用,此时应尝试重启进程或重建游戏框架。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志参数 14 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + 日志参数 14。 + + + + 打印严重错误级别日志,建议在发生严重错误,可能导致游戏崩溃或异常时使用,此时应尝试重启进程或重建游戏框架。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志参数 14 的类型。 + 日志参数 15 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + 日志参数 14。 + 日志参数 15。 + + + + 打印严重错误级别日志,建议在发生严重错误,可能导致游戏崩溃或异常时使用,此时应尝试重启进程或重建游戏框架。 + + 日志参数 1 的类型。 + 日志参数 2 的类型。 + 日志参数 3 的类型。 + 日志参数 4 的类型。 + 日志参数 5 的类型。 + 日志参数 6 的类型。 + 日志参数 7 的类型。 + 日志参数 8 的类型。 + 日志参数 9 的类型。 + 日志参数 10 的类型。 + 日志参数 11 的类型。 + 日志参数 12 的类型。 + 日志参数 13 的类型。 + 日志参数 14 的类型。 + 日志参数 15 的类型。 + 日志参数 16 的类型。 + 日志格式。 + 日志参数 1。 + 日志参数 2。 + 日志参数 3。 + 日志参数 4。 + 日志参数 5。 + 日志参数 6。 + 日志参数 7。 + 日志参数 8。 + 日志参数 9。 + 日志参数 10。 + 日志参数 11。 + 日志参数 12。 + 日志参数 13。 + 日志参数 14。 + 日志参数 15。 + 日志参数 16。 + + + + 游戏框架日志辅助器接口。 + + + + + 记录日志。 + + 游戏框架日志等级。 + 日志内容。 + + + + 游戏框架日志等级。 + + + + + 调试。 + + + + + 信息。 + + + + + 警告。 + + + + + 错误。 + + + + + 严重错误。 + + + + + 引用接口。 + + + + + 清理引用。 + + + + + 引用池。 + + + + + 获取或设置是否开启强制检查。 + + + + + 获取引用池的数量。 + + + + + 获取所有引用池的信息。 + + 所有引用池的信息。 + + + + 清除所有引用池。 + + + + + 从引用池获取引用。 + + 引用类型。 + 引用。 + + + + 从引用池获取引用。 + + 引用类型。 + 引用。 + + + + 将引用归还引用池。 + + 引用。 + + + + 向引用池中追加指定数量的引用。 + + 引用类型。 + 追加数量。 + + + + 向引用池中追加指定数量的引用。 + + 引用类型。 + 追加数量。 + + + + 从引用池中移除指定数量的引用。 + + 引用类型。 + 移除数量。 + + + + 从引用池中移除指定数量的引用。 + + 引用类型。 + 移除数量。 + + + + 从引用池中移除所有的引用。 + + 引用类型。 + + + + 从引用池中移除所有的引用。 + + 引用类型。 + + + + 引用池信息。 + + + + + 初始化引用池信息的新实例。 + + 引用池类型。 + 未使用引用数量。 + 正在使用引用数量。 + 获取引用数量。 + 归还引用数量。 + 增加引用数量。 + 移除引用数量。 + + + + 获取引用池类型。 + + + + + 获取未使用引用数量。 + + + + + 获取正在使用引用数量。 + + + + + 获取获取引用数量。 + + + + + 获取归还引用数量。 + + + + + 获取增加引用数量。 + + + + + 获取移除引用数量。 + + + + + 任务代理接口。 + + 任务类型。 + + + + 获取任务。 + + + + + 初始化任务代理。 + + + + + 任务代理轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理任务代理。 + + + + + 开始处理任务。 + + 要处理的任务。 + 开始处理任务的状态。 + + + + 停止正在处理的任务并重置任务代理。 + + + + + 开始处理任务的状态。 + + + + + 可以立刻处理完成此任务。 + + + + + 可以继续处理此任务。 + + + + + 不能继续处理此任务,需等待其它任务执行完成。 + + + + + 不能继续处理此任务,出现未知错误。 + + + + + 任务基类。 + + + + + 任务默认优先级。 + + + + + 初始化任务基类的新实例。 + + + + + 获取任务的序列编号。 + + + + + 获取任务的标签。 + + + + + 获取任务的优先级。 + + + + + 获取任务的用户自定义数据。 + + + + + 获取或设置任务是否完成。 + + + + + 获取任务描述。 + + + + + 初始化任务基类。 + + 任务的序列编号。 + 任务的标签。 + 任务的优先级。 + 任务的用户自定义数据。 + + + + 清理任务基类。 + + + + + 任务信息。 + + + + + 初始化任务信息的新实例。 + + 任务的序列编号。 + 任务的标签。 + 任务的优先级。 + 任务的用户自定义数据。 + 任务状态。 + 任务描述。 + + + + 获取任务信息是否有效。 + + + + + 获取任务的序列编号。 + + + + + 获取任务的标签。 + + + + + 获取任务的优先级。 + + + + + 获取任务的用户自定义数据。 + + + + + 获取任务状态。 + + + + + 获取任务描述。 + + + + + 任务池。 + + 任务类型。 + + + + 初始化任务池的新实例。 + + + + + 获取或设置任务池是否被暂停。 + + + + + 获取任务代理总数量。 + + + + + 获取可用任务代理数量。 + + + + + 获取工作中任务代理数量。 + + + + + 获取等待任务数量。 + + + + + 任务池轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理任务池。 + + + + + 增加任务代理。 + + 要增加的任务代理。 + + + + 根据任务的序列编号获取任务的信息。 + + 要获取信息的任务的序列编号。 + 任务的信息。 + + + + 根据任务的标签获取任务的信息。 + + 要获取信息的任务的标签。 + 任务的信息。 + + + + 根据任务的标签获取任务的信息。 + + 要获取信息的任务的标签。 + 任务的信息。 + + + + 获取所有任务的信息。 + + 所有任务的信息。 + + + + 获取所有任务的信息。 + + 所有任务的信息。 + + + + 增加任务。 + + 要增加的任务。 + + + + 根据任务的序列编号移除任务。 + + 要移除任务的序列编号。 + 是否移除任务成功。 + + + + 根据任务的标签移除任务。 + + 要移除任务的标签。 + 移除任务的数量。 + + + + 移除所有任务。 + + 移除任务的数量。 + + + + 任务状态。 + + + + + 未开始。 + + + + + 执行中。 + + + + + 完成。 + + + + + 变量。 + + 变量类型。 + + + + 初始化变量的新实例。 + + + + + 获取变量类型。 + + + + + 获取或设置变量值。 + + + + + 获取变量值。 + + 变量值。 + + + + 设置变量值。 + + 变量值。 + + + + 清理变量值。 + + + + + 获取变量字符串。 + + 变量字符串。 + + + + 变量。 + + + + + 初始化变量的新实例。 + + + + + 获取变量类型。 + + + + + 获取变量值。 + + 变量值。 + + + + 设置变量值。 + + 变量值。 + + + + 清理变量值。 + + + + + 版本号类。 + + + + + 获取游戏框架版本号。 + + + + + 获取游戏版本号。 + + + + + 获取内部游戏版本号。 + + + + + 设置版本号辅助器。 + + 要设置的版本号辅助器。 + + + + 版本号辅助器接口。 + + + + + 获取游戏版本号。 + + + + + 获取内部游戏版本号。 + + + + + 全局配置管理器。 + + + + + 初始化全局配置管理器的新实例。 + + + + + 获取全局配置项数量。 + + + + + 获取缓冲二进制流的大小。 + + + + + 读取全局配置成功事件。 + + + + + 读取全局配置失败事件。 + + + + + 读取全局配置更新事件。 + + + + + 读取全局配置时加载依赖资源事件。 + + + + + 全局配置管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理全局配置管理器。 + + + + + 设置资源管理器。 + + 资源管理器。 + + + + 设置全局配置数据提供者辅助器。 + + 全局配置数据提供者辅助器。 + + + + 设置全局配置辅助器。 + + 全局配置辅助器。 + + + + 确保二进制流缓存分配足够大小的内存并缓存。 + + 要确保二进制流缓存分配内存的大小。 + + + + 释放缓存的二进制流。 + + + + + 读取全局配置。 + + 全局配置资源名称。 + + + + 读取全局配置。 + + 全局配置资源名称。 + 加载全局配置资源的优先级。 + + + + 读取全局配置。 + + 全局配置资源名称。 + 用户自定义数据。 + + + + 读取全局配置。 + + 全局配置资源名称。 + 加载全局配置资源的优先级。 + 用户自定义数据。 + + + + 解析全局配置。 + + 要解析的全局配置字符串。 + 是否解析全局配置成功。 + + + + 解析全局配置。 + + 要解析的全局配置字符串。 + 用户自定义数据。 + 是否解析全局配置成功。 + + + + 解析全局配置。 + + 要解析的全局配置二进制流。 + 是否解析全局配置成功。 + + + + 解析全局配置。 + + 要解析的全局配置二进制流。 + 用户自定义数据。 + 是否解析全局配置成功。 + + + + 解析全局配置。 + + 要解析的全局配置二进制流。 + 全局配置二进制流的起始位置。 + 全局配置二进制流的长度。 + 是否解析全局配置成功。 + + + + 解析全局配置。 + + 要解析的全局配置二进制流。 + 全局配置二进制流的起始位置。 + 全局配置二进制流的长度。 + 用户自定义数据。 + 是否解析全局配置成功。 + + + + 检查是否存在指定全局配置项。 + + 要检查全局配置项的名称。 + 指定的全局配置项是否存在。 + + + + 从指定全局配置项中读取布尔值。 + + 要获取全局配置项的名称。 + 读取的布尔值。 + + + + 从指定全局配置项中读取布尔值。 + + 要获取全局配置项的名称。 + 当指定的全局配置项不存在时,返回此默认值。 + 读取的布尔值。 + + + + 从指定全局配置项中读取整数值。 + + 要获取全局配置项的名称。 + 读取的整数值。 + + + + 从指定全局配置项中读取整数值。 + + 要获取全局配置项的名称。 + 当指定的全局配置项不存在时,返回此默认值。 + 读取的整数值。 + + + + 从指定全局配置项中读取浮点数值。 + + 要获取全局配置项的名称。 + 读取的浮点数值。 + + + + 从指定全局配置项中读取浮点数值。 + + 要获取全局配置项的名称。 + 当指定的全局配置项不存在时,返回此默认值。 + 读取的浮点数值。 + + + + 从指定全局配置项中读取字符串值。 + + 要获取全局配置项的名称。 + 读取的字符串值。 + + + + 从指定全局配置项中读取字符串值。 + + 要获取全局配置项的名称。 + 当指定的全局配置项不存在时,返回此默认值。 + 读取的字符串值。 + + + + 增加指定全局配置项。 + + 要增加全局配置项的名称。 + 全局配置项的值。 + 是否增加全局配置项成功。 + + + + 增加指定全局配置项。 + + 要增加全局配置项的名称。 + 全局配置项布尔值。 + 全局配置项整数值。 + 全局配置项浮点数值。 + 全局配置项字符串值。 + 是否增加全局配置项成功。 + + + + 移除指定全局配置项。 + + 要移除全局配置项的名称。 + + + + 清空所有全局配置项。 + + + + + 全局配置辅助器接口。 + + + + + 全局配置管理器接口。 + + + + + 获取全局配置项数量。 + + + + + 获取缓冲二进制流的大小。 + + + + + 设置资源管理器。 + + 资源管理器。 + + + + 设置全局配置数据提供者辅助器。 + + 全局配置数据提供者辅助器。 + + + + 设置全局配置辅助器。 + + 全局配置辅助器。 + + + + 确保二进制流缓存分配足够大小的内存并缓存。 + + 要确保二进制流缓存分配内存的大小。 + + + + 释放缓存的二进制流。 + + + + + 检查是否存在指定全局配置项。 + + 要检查全局配置项的名称。 + 指定的全局配置项是否存在。 + + + + 从指定全局配置项中读取布尔值。 + + 要获取全局配置项的名称。 + 读取的布尔值。 + + + + 从指定全局配置项中读取布尔值。 + + 要获取全局配置项的名称。 + 当指定的全局配置项不存在时,返回此默认值。 + 读取的布尔值。 + + + + 从指定全局配置项中读取整数值。 + + 要获取全局配置项的名称。 + 读取的整数值。 + + + + 从指定全局配置项中读取整数值。 + + 要获取全局配置项的名称。 + 当指定的全局配置项不存在时,返回此默认值。 + 读取的整数值。 + + + + 从指定全局配置项中读取浮点数值。 + + 要获取全局配置项的名称。 + 读取的浮点数值。 + + + + 从指定全局配置项中读取浮点数值。 + + 要获取全局配置项的名称。 + 当指定的全局配置项不存在时,返回此默认值。 + 读取的浮点数值。 + + + + 从指定全局配置项中读取字符串值。 + + 要获取全局配置项的名称。 + 读取的字符串值。 + + + + 从指定全局配置项中读取字符串值。 + + 要获取全局配置项的名称。 + 当指定的全局配置项不存在时,返回此默认值。 + 读取的字符串值。 + + + + 增加指定全局配置项。 + + 要增加全局配置项的名称。 + 全局配置项的值。 + 是否增加全局配置项成功。 + + + + 增加指定全局配置项。 + + 要增加全局配置项的名称。 + 全局配置项布尔值。 + 全局配置项整数值。 + 全局配置项浮点数值。 + 全局配置项字符串值。 + 是否增加全局配置项成功。 + + + + 移除指定全局配置项。 + + 要移除全局配置项的名称。 + 是否移除全局配置项成功。 + + + + 清空所有全局配置项。 + + + + + 数据结点管理器。 + + + + + 初始化数据结点管理器的新实例。 + + + + + 获取根数据结点。 + + + + + 数据结点管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理数据结点管理器。 + + + + + 根据类型获取数据结点的数据。 + + 要获取的数据类型。 + 相对于 node 的查找路径。 + 指定类型的数据。 + + + + 获取数据结点的数据。 + + 相对于 node 的查找路径。 + 数据结点的数据。 + + + + 根据类型获取数据结点的数据。 + + 要获取的数据类型。 + 相对于 node 的查找路径。 + 查找起始结点。 + 指定类型的数据。 + + + + 获取数据结点的数据。 + + 相对于 node 的查找路径。 + 查找起始结点。 + 数据结点的数据。 + + + + 设置数据结点的数据。 + + 要设置的数据类型。 + 相对于 node 的查找路径。 + 要设置的数据。 + + + + 设置数据结点的数据。 + + 相对于 node 的查找路径。 + 要设置的数据。 + + + + 设置数据结点的数据。 + + 要设置的数据类型。 + 相对于 node 的查找路径。 + 要设置的数据。 + 查找起始结点。 + + + + 设置数据结点的数据。 + + 相对于 node 的查找路径。 + 要设置的数据。 + 查找起始结点。 + + + + 获取数据结点。 + + 相对于 node 的查找路径。 + 指定位置的数据结点,如果没有找到,则返回空。 + + + + 获取数据结点。 + + 相对于 node 的查找路径。 + 查找起始结点。 + 指定位置的数据结点,如果没有找到,则返回空。 + + + + 获取或增加数据结点。 + + 相对于 node 的查找路径。 + 指定位置的数据结点,如果没有找到,则创建相应的数据结点。 + + + + 获取或增加数据结点。 + + 相对于 node 的查找路径。 + 查找起始结点。 + 指定位置的数据结点,如果没有找到,则增加相应的数据结点。 + + + + 移除数据结点。 + + 相对于 node 的查找路径。 + + + + 移除数据结点。 + + 相对于 node 的查找路径。 + 查找起始结点。 + + + + 移除所有数据结点。 + + + + + 数据结点路径切分工具函数。 + + 要切分的数据结点路径。 + 切分后的字符串数组。 + + + + 数据结点。 + + + + + 创建数据结点。 + + 数据结点名称。 + 父数据结点。 + 创建的数据结点。 + + + + 获取数据结点的名称。 + + + + + 获取数据结点的完整名称。 + + + + + 获取父数据结点。 + + + + + 获取子数据结点的数量。 + + + + + 根据类型获取数据结点的数据。 + + 要获取的数据类型。 + 指定类型的数据。 + + + + 获取数据结点的数据。 + + 数据结点数据。 + + + + 设置数据结点的数据。 + + 要设置的数据类型。 + 要设置的数据。 + + + + 设置数据结点的数据。 + + 要设置的数据。 + + + + 根据索引检查是否存在子数据结点。 + + 子数据结点的索引。 + 是否存在子数据结点。 + + + + 根据名称检查是否存在子数据结点。 + + 子数据结点名称。 + 是否存在子数据结点。 + + + + 根据索引获取子数据结点。 + + 子数据结点的索引。 + 指定索引的子数据结点,如果索引越界,则返回空。 + + + + 根据名称获取子数据结点。 + + 子数据结点名称。 + 指定名称的子数据结点,如果没有找到,则返回空。 + + + + 根据名称获取或增加子数据结点。 + + 子数据结点名称。 + 指定名称的子数据结点,如果对应名称的子数据结点已存在,则返回已存在的子数据结点,否则增加子数据结点。 + + + + 获取所有子数据结点。 + + 所有子数据结点。 + + + + 获取所有子数据结点。 + + 所有子数据结点。 + + + + 根据索引移除子数据结点。 + + 子数据结点的索引位置。 + + + + 根据名称移除子数据结点。 + + 子数据结点名称。 + + + + 获取数据结点字符串。 + + 数据结点字符串。 + + + + 获取数据字符串。 + + 数据字符串。 + + + + 检测数据结点名称是否合法。 + + 要检测的数据结点名称。 + 是否是合法的数据结点名称。 + + + + 数据结点接口。 + + + + + 获取数据结点的名称。 + + + + + 获取数据结点的完整名称。 + + + + + 获取父数据结点。 + + + + + 获取子数据结点的数量。 + + + + + 根据类型获取数据结点的数据。 + + 要获取的数据类型。 + 指定类型的数据。 + + + + 获取数据结点的数据。 + + 数据结点数据。 + + + + 设置数据结点的数据。 + + 要设置的数据类型。 + 要设置的数据。 + + + + 设置数据结点的数据。 + + 要设置的数据。 + + + + 根据索引检查是否存在子数据结点。 + + 子数据结点的索引。 + 是否存在子数据结点。 + + + + 根据名称检查是否存在子数据结点。 + + 子数据结点名称。 + 是否存在子数据结点。 + + + + 根据索引获取子数据结点。 + + 子数据结点的索引。 + 指定索引的子数据结点,如果索引越界,则返回空。 + + + + 根据名称获取子数据结点。 + + 子数据结点名称。 + 指定名称的子数据结点,如果没有找到,则返回空。 + + + + 根据名称获取或增加子数据结点。 + + 子数据结点名称。 + 指定名称的子数据结点,如果对应名称的子数据结点已存在,则返回已存在的子数据结点,否则增加子数据结点。 + + + + 获取所有子数据结点。 + + 所有子数据结点。 + + + + 获取所有子数据结点。 + + 所有子数据结点。 + + + + 根据索引移除子数据结点。 + + 子数据结点的索引。 + + + + 根据名称移除子数据结点。 + + 子数据结点名称。 + + + + 移除当前数据结点的数据和所有子数据结点。 + + + + + 获取数据结点字符串。 + + 数据结点字符串。 + + + + 获取数据字符串。 + + 数据字符串。 + + + + 数据结点管理器接口。 + + + + + 获取根数据结点。 + + + + + 根据类型获取数据结点的数据。 + + 要获取的数据类型。 + 相对于 node 的查找路径。 + 指定类型的数据。 + + + + 获取数据结点的数据。 + + 相对于 node 的查找路径。 + 数据结点的数据。 + + + + 根据类型获取数据结点的数据。 + + 要获取的数据类型。 + 相对于 node 的查找路径。 + 查找起始结点。 + 指定类型的数据。 + + + + 获取数据结点的数据。 + + 相对于 node 的查找路径。 + 查找起始结点。 + 数据结点的数据。 + + + + 设置数据结点的数据。 + + 要设置的数据类型。 + 相对于 node 的查找路径。 + 要设置的数据。 + + + + 设置数据结点的数据。 + + 相对于 node 的查找路径。 + 要设置的数据。 + + + + 设置数据结点的数据。 + + 要设置的数据类型。 + 相对于 node 的查找路径。 + 要设置的数据。 + 查找起始结点。 + + + + 设置数据结点的数据。 + + 相对于 node 的查找路径。 + 要设置的数据。 + 查找起始结点。 + + + + 获取数据结点。 + + 相对于 node 的查找路径。 + 指定位置的数据结点,如果没有找到,则返回空。 + + + + 获取数据结点。 + + 相对于 node 的查找路径。 + 查找起始结点。 + 指定位置的数据结点,如果没有找到,则返回空。 + + + + 获取或增加数据结点。 + + 相对于 node 的查找路径。 + 指定位置的数据结点,如果没有找到,则创建相应的数据结点。 + + + + 获取或增加数据结点。 + + 相对于 node 的查找路径。 + 查找起始结点。 + 指定位置的数据结点,如果没有找到,则创建相应的数据结点。 + + + + 移除数据结点。 + + 相对于 node 的查找路径。 + + + + 移除数据结点。 + + 相对于 node 的查找路径。 + 查找起始结点。 + + + + 移除所有数据结点。 + + + + + 数据表基类。 + + + + + 初始化数据表基类的新实例。 + + + + + 初始化数据表基类的新实例。 + + 数据表名称。 + + + + 获取数据表名称。 + + + + + 获取数据表完整名称。 + + + + + 获取数据表行的类型。 + + + + + 获取数据表行数。 + + + + + 读取数据表成功事件。 + + + + + 读取数据表失败事件。 + + + + + 读取数据表更新事件。 + + + + + 读取数据表时加载依赖资源事件。 + + + + + 读取数据表。 + + 数据表资源名称。 + + + + 读取数据表。 + + 数据表资源名称。 + 加载数据表资源的优先级。 + + + + 读取数据表。 + + 数据表资源名称。 + 用户自定义数据。 + + + + 读取数据表。 + + 数据表资源名称。 + 加载数据表资源的优先级。 + 用户自定义数据。 + + + + 解析数据表。 + + 要解析的数据表字符串。 + 是否解析数据表成功。 + + + + 解析数据表。 + + 要解析的数据表字符串。 + 用户自定义数据。 + 是否解析数据表成功。 + + + + 解析数据表。 + + 要解析的数据表二进制流。 + 是否解析数据表成功。 + + + + 解析数据表。 + + 要解析的数据表二进制流。 + 用户自定义数据。 + 是否解析数据表成功。 + + + + 解析数据表。 + + 要解析的数据表二进制流。 + 数据表二进制流的起始位置。 + 数据表二进制流的长度。 + 是否解析数据表成功。 + + + + 解析数据表。 + + 要解析的数据表二进制流。 + 数据表二进制流的起始位置。 + 数据表二进制流的长度。 + 用户自定义数据。 + 是否解析数据表成功。 + + + + 检查是否存在数据表行。 + + 数据表行的编号。 + 是否存在数据表行。 + + + + 增加数据表行。 + + 要解析的数据表行字符串。 + 用户自定义数据。 + 是否增加数据表行成功。 + + + + 增加数据表行。 + + 要解析的数据表行二进制流。 + 数据表行二进制流的起始位置。 + 数据表行二进制流的长度。 + 用户自定义数据。 + 是否增加数据表行成功。 + + + + 移除指定数据表行。 + + 要移除数据表行的编号。 + 是否移除数据表行成功。 + + + + 清空所有数据表行。 + + + + + 设置资源管理器。 + + 资源管理器。 + + + + 设置数据提供者辅助器。 + + 数据提供者辅助器。 + + + + 关闭并清理数据表。 + + + + + 数据表管理器。 + + + + + 初始化数据表管理器的新实例。 + + + + + 获取数据表数量。 + + + + + 获取缓冲二进制流的大小。 + + + + + 数据表管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理数据表管理器。 + + + + + 设置资源管理器。 + + 资源管理器。 + + + + 设置数据表数据提供者辅助器。 + + 数据表数据提供者辅助器。 + + + + 设置数据表辅助器。 + + 数据表辅助器。 + + + + 确保二进制流缓存分配足够大小的内存并缓存。 + + 要确保二进制流缓存分配内存的大小。 + + + + 释放缓存的二进制流。 + + + + + 是否存在数据表。 + + 数据表行的类型。 + 是否存在数据表。 + + + + 是否存在数据表。 + + 数据表行的类型。 + 是否存在数据表。 + + + + 是否存在数据表。 + + 数据表行的类型。 + 数据表名称。 + 是否存在数据表。 + + + + 是否存在数据表。 + + 数据表行的类型。 + 数据表名称。 + 是否存在数据表。 + + + + 获取数据表。 + + 数据表行的类型。 + 要获取的数据表。 + + + + 获取数据表。 + + 数据表行的类型。 + 要获取的数据表。 + + + + 获取数据表。 + + 数据表行的类型。 + 数据表名称。 + 要获取的数据表。 + + + + 获取数据表。 + + 数据表行的类型。 + 数据表名称。 + 要获取的数据表。 + + + + 获取所有数据表。 + + 所有数据表。 + + + + 获取所有数据表。 + + 所有数据表。 + + + + 创建数据表。 + + 数据表行的类型。 + 要创建的数据表。 + + + + 创建数据表。 + + 数据表行的类型。 + 要创建的数据表。 + + + + 创建数据表。 + + 数据表行的类型。 + 数据表名称。 + 要创建的数据表。 + + + + 创建数据表。 + + 数据表行的类型。 + 数据表名称。 + 要创建的数据表。 + + + + 销毁数据表。 + + 数据表行的类型。 + + + + 销毁数据表。 + + 数据表行的类型。 + 是否销毁数据表成功。 + + + + 销毁数据表。 + + 数据表行的类型。 + 数据表名称。 + + + + 销毁数据表。 + + 数据表行的类型。 + 数据表名称。 + 是否销毁数据表成功。 + + + + 销毁数据表。 + + 数据表行的类型。 + 要销毁的数据表。 + 是否销毁数据表成功。 + + + + 销毁数据表。 + + 要销毁的数据表。 + 是否销毁数据表成功。 + + + + 数据表。 + + 数据表行的类型。 + + + + 初始化数据表的新实例。 + + 数据表名称。 + + + + 获取数据表行的类型。 + + + + + 获取数据表行数。 + + + + + 获取数据表行。 + + 数据表行的编号。 + 数据表行。 + + + + 获取编号最小的数据表行。 + + + + + 获取编号最大的数据表行。 + + + + + 检查是否存在数据表行。 + + 数据表行的编号。 + 是否存在数据表行。 + + + + 检查是否存在数据表行。 + + 要检查的条件。 + 是否存在数据表行。 + + + + 获取数据表行。 + + 数据表行的编号。 + 数据表行。 + + + + 获取符合条件的数据表行。 + + 要检查的条件。 + 符合条件的数据表行。 + 当存在多个符合条件的数据表行时,仅返回第一个符合条件的数据表行。 + + + + 获取符合条件的数据表行。 + + 要检查的条件。 + 符合条件的数据表行。 + + + + 获取符合条件的数据表行。 + + 要检查的条件。 + 符合条件的数据表行。 + + + + 获取排序后的数据表行。 + + 要排序的条件。 + 排序后的数据表行。 + + + + 获取排序后的数据表行。 + + 要排序的条件。 + 排序后的数据表行。 + + + + 获取排序后的符合条件的数据表行。 + + 要检查的条件。 + 要排序的条件。 + 排序后的符合条件的数据表行。 + + + + 获取排序后的符合条件的数据表行。 + + 要检查的条件。 + 要排序的条件。 + 排序后的符合条件的数据表行。 + + + + 获取所有数据表行。 + + 所有数据表行。 + + + + 获取所有数据表行。 + + 所有数据表行。 + + + + 增加数据表行。 + + 要解析的数据表行字符串。 + 用户自定义数据。 + 是否增加数据表行成功。 + + + + 增加数据表行。 + + 要解析的数据表行二进制流。 + 数据表行二进制流的起始位置。 + 数据表行二进制流的长度。 + 用户自定义数据。 + 是否增加数据表行成功。 + + + + 移除指定数据表行。 + + 要移除数据表行的编号。 + 是否移除数据表行成功。 + + + + 清空所有数据表行。 + + + + + 返回循环访问集合的枚举数。 + + 循环访问集合的枚举数。 + + + + 返回循环访问集合的枚举数。 + + 循环访问集合的枚举数。 + + + + 关闭并清理数据表。 + + + + + 数据表行接口。 + + + + + 获取数据表行的编号。 + + + + + 解析数据表行。 + + 要解析的数据表行字符串。 + 用户自定义数据。 + 是否解析数据表行成功。 + + + + 解析数据表行。 + + 要解析的数据表行二进制流。 + 数据表行二进制流的起始位置。 + 数据表行二进制流的长度。 + 用户自定义数据。 + 是否解析数据表行成功。 + + + + 数据表接口。 + + 数据表行的类型。 + + + + 获取数据表名称。 + + + + + 获取数据表完整名称。 + + + + + 获取数据表行的类型。 + + + + + 获取数据表行数。 + + + + + 获取数据表行。 + + 数据表行的编号。 + 数据表行。 + + + + 获取编号最小的数据表行。 + + + + + 获取编号最大的数据表行。 + + + + + 检查是否存在数据表行。 + + 数据表行的编号。 + 是否存在数据表行。 + + + + 检查是否存在数据表行。 + + 要检查的条件。 + 是否存在数据表行。 + + + + 获取数据表行。 + + 数据表行的编号。 + 数据表行。 + + + + 获取符合条件的数据表行。 + + 要检查的条件。 + 符合条件的数据表行。 + 当存在多个符合条件的数据表行时,仅返回第一个符合条件的数据表行。 + + + + 获取符合条件的数据表行。 + + 要检查的条件。 + 符合条件的数据表行。 + + + + 获取符合条件的数据表行。 + + 要检查的条件。 + 符合条件的数据表行。 + + + + 获取排序后的数据表行。 + + 要排序的条件。 + 排序后的数据表行。 + + + + 获取排序后的数据表行。 + + 要排序的条件。 + 排序后的数据表行。 + + + + 获取排序后的符合条件的数据表行。 + + 要检查的条件。 + 要排序的条件。 + 排序后的符合条件的数据表行。 + + + + 获取排序后的符合条件的数据表行。 + + 要检查的条件。 + 要排序的条件。 + 排序后的符合条件的数据表行。 + + + + 获取所有数据表行。 + + 所有数据表行。 + + + + 获取所有数据表行。 + + 所有数据表行。 + + + + 增加数据表行。 + + 要解析的数据表行字符串。 + 用户自定义数据。 + 是否增加数据表行成功。 + + + + 增加数据表行。 + + 要解析的数据表行二进制流。 + 数据表行二进制流的起始位置。 + 数据表行二进制流的长度。 + 用户自定义数据。 + 是否增加数据表行成功。 + + + + 移除指定数据表行。 + + 要移除数据表行的编号。 + 是否移除数据表行成功。 + + + + 清空所有数据表行。 + + + + + 数据表辅助器接口。 + + + + + 数据表管理器接口。 + + + + + 获取数据表数量。 + + + + + 获取缓冲二进制流的大小。 + + + + + 设置资源管理器。 + + 资源管理器。 + + + + 设置数据表数据提供者辅助器。 + + 数据表数据提供者辅助器。 + + + + 设置数据表辅助器。 + + 数据表辅助器。 + + + + 确保二进制流缓存分配足够大小的内存并缓存。 + + 要确保二进制流缓存分配内存的大小。 + + + + 释放缓存的二进制流。 + + + + + 是否存在数据表。 + + 数据表行的类型。 + 是否存在数据表。 + + + + 是否存在数据表。 + + 数据表行的类型。 + 是否存在数据表。 + + + + 是否存在数据表。 + + 数据表行的类型。 + 数据表名称。 + 是否存在数据表。 + + + + 是否存在数据表。 + + 数据表行的类型。 + 数据表名称。 + 是否存在数据表。 + + + + 获取数据表。 + + 数据表行的类型。 + 要获取的数据表。 + + + + 获取数据表。 + + 数据表行的类型。 + 要获取的数据表。 + + + + 获取数据表。 + + 数据表行的类型。 + 数据表名称。 + 要获取的数据表。 + + + + 获取数据表。 + + 数据表行的类型。 + 数据表名称。 + 要获取的数据表。 + + + + 获取所有数据表。 + + 所有数据表。 + + + + 获取所有数据表。 + + 所有数据表。 + + + + 创建数据表。 + + 数据表行的类型。 + 要创建的数据表。 + + + + 创建数据表。 + + 数据表行的类型。 + 要创建的数据表。 + + + + 创建数据表。 + + 数据表行的类型。 + 数据表名称。 + 要创建的数据表。 + + + + 创建数据表。 + + 数据表行的类型。 + 数据表名称。 + 要创建的数据表。 + + + + 销毁数据表。 + + 数据表行的类型。 + 是否销毁数据表成功。 + + + + 销毁数据表。 + + 数据表行的类型。 + 是否销毁数据表成功。 + + + + 销毁数据表。 + + 数据表行的类型。 + 数据表名称。 + 是否销毁数据表成功。 + + + + 销毁数据表。 + + 数据表行的类型。 + 数据表名称。 + 是否销毁数据表成功。 + + + + 销毁数据表。 + + 数据表行的类型。 + 要销毁的数据表。 + 是否销毁数据表成功。 + + + + 销毁数据表。 + + 要销毁的数据表。 + 是否销毁数据表成功。 + + + + 调试器管理器。 + + + + + 初始化调试器管理器的新实例。 + + + + + 获取游戏框架模块优先级。 + + 优先级较高的模块会优先轮询,并且关闭操作会后进行。 + + + + 获取或设置调试器窗口是否激活。 + + + + + 调试器窗口根结点。 + + + + + 调试器管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理调试器管理器。 + + + + + 注册调试器窗口。 + + 调试器窗口路径。 + 要注册的调试器窗口。 + 初始化调试器窗口参数。 + + + + 解除注册调试器窗口。 + + 调试器窗口路径。 + 是否解除注册调试器窗口成功。 + + + + 获取调试器窗口。 + + 调试器窗口路径。 + 要获取的调试器窗口。 + + + + 选中调试器窗口。 + + 调试器窗口路径。 + 是否成功选中调试器窗口。 + + + + 调试器窗口组。 + + + + + 获取调试器窗口数量。 + + + + + 获取或设置当前选中的调试器窗口索引。 + + + + + 获取当前选中的调试器窗口。 + + + + + 初始化调试组。 + + 初始化调试组参数。 + + + + 关闭调试组。 + + + + + 进入调试器窗口。 + + + + + 离开调试器窗口。 + + + + + 调试组轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 调试器窗口绘制。 + + + + + 获取调试组的调试器窗口名称集合。 + + + + + 获取调试器窗口。 + + 调试器窗口路径。 + 要获取的调试器窗口。 + + + + 选中调试器窗口。 + + 调试器窗口路径。 + 是否成功选中调试器窗口。 + + + + 注册调试器窗口。 + + 调试器窗口路径。 + 要注册的调试器窗口。 + + + + 解除注册调试器窗口。 + + 调试器窗口路径。 + 是否解除注册调试器窗口成功。 + + + + 调试器管理器接口。 + + + + + 获取或设置调试器窗口是否激活。 + + + + + 调试器窗口根结点。 + + + + + 注册调试器窗口。 + + 调试器窗口路径。 + 要注册的调试器窗口。 + 初始化调试器窗口参数。 + + + + 解除注册调试器窗口。 + + 调试器窗口路径。 + 是否解除注册调试器窗口成功。 + + + + 获取调试器窗口。 + + 调试器窗口路径。 + 要获取的调试器窗口。 + + + + 选中调试器窗口。 + + 调试器窗口路径。 + 是否成功选中调试器窗口。 + + + + 调试器窗口接口。 + + + + + 初始化调试器窗口。 + + 初始化调试器窗口参数。 + + + + 关闭调试器窗口。 + + + + + 进入调试器窗口。 + + + + + 离开调试器窗口。 + + + + + 调试器窗口轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 调试器窗口绘制。 + + + + + 调试器窗口组接口。 + + + + + 获取调试器窗口数量。 + + + + + 获取或设置当前选中的调试器窗口索引。 + + + + + 获取当前选中的调试器窗口。 + + + + + 获取调试组的调试器窗口名称集合。 + + + + + 获取调试器窗口。 + + 调试器窗口路径。 + 要获取的调试器窗口。 + + + + 注册调试器窗口。 + + 调试器窗口路径。 + 要注册的调试器窗口。 + + + + 下载相关常量。 + + + + + 默认下载任务优先级。 + + + + + 下载代理辅助器完成事件。 + + + + + 初始化下载代理辅助器完成事件的新实例。 + + + + + 获取下载的数据大小。 + + + + + 创建下载代理辅助器完成事件。 + + 下载的数据大小。 + 创建的下载代理辅助器完成事件。 + + + + 清理下载代理辅助器完成事件。 + + + + + 下载代理辅助器错误事件。 + + + + + 初始化下载代理辅助器错误事件的新实例。 + + + + + 获取是否需要删除正在下载的文件。 + + + + + 获取错误信息。 + + + + + 创建下载代理辅助器错误事件。 + + 是否需要删除正在下载的文件。 + 错误信息。 + 创建的下载代理辅助器错误事件。 + + + + 清理下载代理辅助器错误事件。 + + + + + 下载代理辅助器更新数据流事件。 + + + + + 初始化下载代理辅助器更新数据流事件的新实例。 + + + + + 获取数据流的偏移。 + + + + + 获取数据流的长度。 + + + + + 创建下载代理辅助器更新数据流事件。 + + 下载的数据流。 + 数据流的偏移。 + 数据流的长度。 + 创建的下载代理辅助器更新数据流事件。 + + + + 清理下载代理辅助器更新数据流事件。 + + + + + 获取下载的数据流。 + + + + + 下载代理辅助器更新数据大小事件。 + + + + + 初始化下载代理辅助器更新数据大小事件的新实例。 + + + + + 获取下载的增量数据大小。 + + + + + 创建下载代理辅助器更新数据大小事件。 + + 下载的增量数据大小。 + 创建的下载代理辅助器更新数据大小事件。 + + + + 清理下载代理辅助器更新数据大小事件。 + + + + + 下载失败事件。 + + + + + 初始化下载失败事件的新实例。 + + + + + 获取下载任务的序列编号。 + + + + + 获取下载后存放路径。 + + + + + 获取下载地址。 + + + + + 获取错误信息。 + + + + + 获取用户自定义数据。 + + + + + 创建下载失败事件。 + + 下载任务的序列编号。 + 下载后存放路径。 + 下载地址。 + 错误信息。 + 用户自定义数据。 + 创建的下载失败事件。 + + + + 清理下载失败事件。 + + + + + 下载管理器。 + + + + + 初始化下载管理器的新实例。 + + + + + 获取游戏框架模块优先级。 + + 优先级较高的模块会优先轮询,并且关闭操作会后进行。 + + + + 获取或设置下载是否被暂停。 + + + + + 获取下载代理总数量。 + + + + + 获取可用下载代理数量。 + + + + + 获取工作中下载代理数量。 + + + + + 获取等待下载任务数量。 + + + + + 获取或设置将缓冲区写入磁盘的临界大小。 + + + + + 获取或设置下载超时时长,以秒为单位。 + + + + + 获取当前下载速度。 + + + + + 下载开始事件。 + + + + + 下载更新事件。 + + + + + 下载成功事件。 + + + + + 下载失败事件。 + + + + + 下载管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理下载管理器。 + + + + + 增加下载代理辅助器。 + + 要增加的下载代理辅助器。 + + + + 根据下载任务的序列编号获取下载任务的信息。 + + 要获取信息的下载任务的序列编号。 + 下载任务的信息。 + + + + 根据下载任务的标签获取下载任务的信息。 + + 要获取信息的下载任务的标签。 + 下载任务的信息。 + + + + 根据下载任务的标签获取下载任务的信息。 + + 要获取信息的下载任务的标签。 + 下载任务的信息。 + + + + 获取所有下载任务的信息。 + + 所有下载任务的信息。 + + + + 获取所有下载任务的信息。 + + 所有下载任务的信息。 + + + + 增加下载任务。 + + 下载后存放路径。 + 原始下载地址。 + 新增下载任务的序列编号。 + + + + 增加下载任务。 + + 下载后存放路径。 + 原始下载地址。 + 下载任务的标签。 + 新增下载任务的序列编号。 + + + + 增加下载任务。 + + 下载后存放路径。 + 原始下载地址。 + 下载任务的优先级。 + 新增下载任务的序列编号。 + + + + 增加下载任务。 + + 下载后存放路径。 + 原始下载地址。 + 用户自定义数据。 + 新增下载任务的序列编号。 + + + + 增加下载任务。 + + 下载后存放路径。 + 原始下载地址。 + 下载任务的标签。 + 下载任务的优先级。 + 新增下载任务的序列编号。 + + + + 增加下载任务。 + + 下载后存放路径。 + 原始下载地址。 + 下载任务的标签。 + 用户自定义数据。 + 新增下载任务的序列编号。 + + + + 增加下载任务。 + + 下载后存放路径。 + 原始下载地址。 + 下载任务的优先级。 + 用户自定义数据。 + 新增下载任务的序列编号。 + + + + 增加下载任务。 + + 下载后存放路径。 + 原始下载地址。 + 下载任务的标签。 + 下载任务的优先级。 + 用户自定义数据。 + 新增下载任务的序列编号。 + + + + 根据下载任务的序列编号移除下载任务。 + + 要移除下载任务的序列编号。 + 是否移除下载任务成功。 + + + + 根据下载任务的标签移除下载任务。 + + 要移除下载任务的标签。 + 移除下载任务的数量。 + + + + 移除所有下载任务。 + + 移除下载任务的数量。 + + + + 下载代理。 + + + + + 初始化下载代理的新实例。 + + 下载代理辅助器。 + + + + 获取下载任务。 + + + + + 获取已经等待时间。 + + + + + 获取开始下载时已经存在的大小。 + + + + + 获取本次已经下载的大小。 + + + + + 获取当前的大小。 + + + + + 获取已经存盘的大小。 + + + + + 初始化下载代理。 + + + + + 下载代理轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理下载代理。 + + + + + 开始处理下载任务。 + + 要处理的下载任务。 + 开始处理任务的状态。 + + + + 重置下载代理。 + + + + + 释放资源。 + + + + + 释放资源。 + + 释放资源标记。 + + + + 下载任务。 + + + + + 初始化下载任务的新实例。 + + + + + 获取或设置下载任务的状态。 + + + + + 获取下载后存放路径。 + + + + + 获取原始下载地址。 + + + + + 获取将缓冲区写入磁盘的临界大小。 + + + + + 获取下载超时时长,以秒为单位。 + + + + + 获取下载任务的描述。 + + + + + 创建下载任务。 + + 下载后存放路径。 + 原始下载地址。 + 下载任务的标签。 + 下载任务的优先级。 + 将缓冲区写入磁盘的临界大小。 + 下载超时时长,以秒为单位。 + 用户自定义数据。 + 创建的下载任务。 + + + + 清理下载任务。 + + + + + 下载任务的状态。 + + + + + 准备下载。 + + + + + 下载中。 + + + + + 下载完成。 + + + + + 下载错误。 + + + + + 下载开始事件。 + + + + + 初始化下载开始事件的新实例。 + + + + + 获取下载任务的序列编号。 + + + + + 获取下载后存放路径。 + + + + + 获取下载地址。 + + + + + 获取当前大小。 + + + + + 获取用户自定义数据。 + + + + + 创建下载开始事件。 + + 下载任务的序列编号。 + 下载后存放路径。 + 下载地址。 + 当前大小。 + 用户自定义数据。 + 创建的下载开始事件。 + + + + 清理下载开始事件。 + + + + + 下载成功事件。 + + + + + 初始化下载成功事件的新实例。 + + + + + 获取下载任务的序列编号。 + + + + + 获取下载后存放路径。 + + + + + 获取下载地址。 + + + + + 获取当前大小。 + + + + + 获取用户自定义数据。 + + + + + 创建下载成功事件。 + + 下载任务的序列编号。 + 下载后存放路径。 + 下载地址。 + 当前大小。 + 用户自定义数据。 + 创建的下载成功事件。 + + + + 清理下载成功事件。 + + + + + 下载更新事件。 + + + + + 初始化下载更新事件的新实例。 + + + + + 获取下载任务的序列编号。 + + + + + 获取下载后存放路径。 + + + + + 获取下载地址。 + + + + + 获取当前大小。 + + + + + 获取用户自定义数据。 + + + + + 创建下载更新事件。 + + 下载任务的序列编号。 + 下载后存放路径。 + 下载地址。 + 当前大小。 + 用户自定义数据。 + 创建的下载更新事件。 + + + + 清理下载更新事件。 + + + + + 下载代理辅助器接口。 + + + + + 下载代理辅助器更新数据流事件。 + + + + + 下载代理辅助器更新数据大小事件。 + + + + + 下载代理辅助器完成事件。 + + + + + 下载代理辅助器错误事件。 + + + + + 通过下载代理辅助器下载指定地址的数据。 + + 下载地址。 + 用户自定义数据。 + + + + 通过下载代理辅助器下载指定地址的数据。 + + 下载地址。 + 下载数据起始位置。 + 用户自定义数据。 + + + + 通过下载代理辅助器下载指定地址的数据。 + + 下载地址。 + 下载数据起始位置。 + 下载数据结束位置。 + 用户自定义数据。 + + + + 重置下载代理辅助器。 + + + + + 下载管理器接口。 + + + + + 获取或设置下载是否被暂停。 + + + + + 获取下载代理总数量。 + + + + + 获取可用下载代理数量。 + + + + + 获取工作中下载代理数量。 + + + + + 获取等待下载任务数量。 + + + + + 获取或设置将缓冲区写入磁盘的临界大小。 + + + + + 获取或设置下载超时时长,以秒为单位。 + + + + + 获取当前下载速度。 + + + + + 下载开始事件。 + + + + + 下载更新事件。 + + + + + 下载成功事件。 + + + + + 下载失败事件。 + + + + + 增加下载代理辅助器。 + + 要增加的下载代理辅助器。 + + + + 根据下载任务的序列编号获取下载任务的信息。 + + 要获取信息的下载任务的序列编号。 + 下载任务的信息。 + + + + 根据下载任务的标签获取下载任务的信息。 + + 要获取信息的下载任务的标签。 + 下载任务的信息。 + + + + 根据下载任务的标签获取下载任务的信息。 + + 要获取信息的下载任务的标签。 + 下载任务的信息。 + + + + 获取所有下载任务的信息。 + + 所有下载任务的信息。 + + + + 获取所有下载任务的信息。 + + 所有下载任务的信息。 + + + + 增加下载任务。 + + 下载后存放路径。 + 原始下载地址。 + 新增下载任务的序列编号。 + + + + 增加下载任务。 + + 下载后存放路径。 + 原始下载地址。 + 下载任务的标签。 + 新增下载任务的序列编号。 + + + + 增加下载任务。 + + 下载后存放路径。 + 原始下载地址。 + 下载任务的优先级。 + 新增下载任务的序列编号。 + + + + 增加下载任务。 + + 下载后存放路径。 + 原始下载地址。 + 用户自定义数据。 + 新增下载任务的序列编号。 + + + + 增加下载任务。 + + 下载后存放路径。 + 原始下载地址。 + 下载任务的标签。 + 下载任务的优先级。 + 新增下载任务的序列编号。 + + + + 增加下载任务。 + + 下载后存放路径。 + 原始下载地址。 + 下载任务的标签。 + 用户自定义数据。 + 新增下载任务的序列编号。 + + + + 增加下载任务。 + + 下载后存放路径。 + 原始下载地址。 + 下载任务的优先级。 + 用户自定义数据。 + 新增下载任务的序列编号。 + + + + 增加下载任务。 + + 下载后存放路径。 + 原始下载地址。 + 下载任务的标签。 + 下载任务的优先级。 + 用户自定义数据。 + 新增下载任务的序列编号。 + + + + 根据下载任务的序列编号移除下载任务。 + + 要移除下载任务的序列编号。 + 是否移除下载任务成功。 + + + + 根据下载任务的标签移除下载任务。 + + 要移除下载任务的标签。 + 移除下载任务的数量。 + + + + 移除所有下载任务。 + + 移除下载任务的数量。 + + + + 实体管理器。 + + + + + 初始化实体管理器的新实例。 + + + + + 获取实体数量。 + + + + + 获取实体组数量。 + + + + + 显示实体成功事件。 + + + + + 显示实体失败事件。 + + + + + 显示实体更新事件。 + + + + + 显示实体时加载依赖资源事件。 + + + + + 隐藏实体完成事件。 + + + + + 实体管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理实体管理器。 + + + + + 设置对象池管理器。 + + 对象池管理器。 + + + + 设置资源管理器。 + + 资源管理器。 + + + + 设置实体辅助器。 + + 实体辅助器。 + + + + 是否存在实体组。 + + 实体组名称。 + 是否存在实体组。 + + + + 获取实体组。 + + 实体组名称。 + 要获取的实体组。 + + + + 获取所有实体组。 + + 所有实体组。 + + + + 获取所有实体组。 + + 所有实体组。 + + + + 增加实体组。 + + 实体组名称。 + 实体实例对象池自动释放可释放对象的间隔秒数。 + 实体实例对象池容量。 + 实体实例对象池对象过期秒数。 + 实体实例对象池的优先级。 + 实体组辅助器。 + 是否增加实体组成功。 + + + + 是否存在实体。 + + 实体编号。 + 是否存在实体。 + + + + 是否存在实体。 + + 实体资源名称。 + 是否存在实体。 + + + + 获取实体。 + + 实体编号。 + 要获取的实体。 + + + + 获取实体。 + + 实体资源名称。 + 要获取的实体。 + + + + 获取实体。 + + 实体资源名称。 + 要获取的实体。 + + + + 获取实体。 + + 实体资源名称。 + 要获取的实体。 + + + + 获取所有已加载的实体。 + + 所有已加载的实体。 + + + + 获取所有已加载的实体。 + + 所有已加载的实体。 + + + + 获取所有正在加载实体的编号。 + + 所有正在加载实体的编号。 + + + + 获取所有正在加载实体的编号。 + + 所有正在加载实体的编号。 + + + + 是否正在加载实体。 + + 实体编号。 + 是否正在加载实体。 + + + + 是否是合法的实体。 + + 实体。 + 实体是否合法。 + + + + 显示实体。 + + 实体编号。 + 实体资源名称。 + 实体组名称。 + + + + 显示实体。 + + 实体编号。 + 实体资源名称。 + 实体组名称。 + 加载实体资源的优先级。 + + + + 显示实体。 + + 实体编号。 + 实体资源名称。 + 实体组名称。 + 用户自定义数据。 + + + + 显示实体。 + + 实体编号。 + 实体资源名称。 + 实体组名称。 + 加载实体资源的优先级。 + 用户自定义数据。 + + + + 隐藏实体。 + + 实体编号。 + + + + 隐藏实体。 + + 实体编号。 + 用户自定义数据。 + + + + 隐藏实体。 + + 实体。 + + + + 隐藏实体。 + + 实体。 + 用户自定义数据。 + + + + 隐藏所有已加载的实体。 + + + + + 隐藏所有已加载的实体。 + + 用户自定义数据。 + + + + 隐藏所有正在加载的实体。 + + + + + 获取父实体。 + + 要获取父实体的子实体的实体编号。 + 子实体的父实体。 + + + + 获取父实体。 + + 要获取父实体的子实体。 + 子实体的父实体。 + + + + 获取子实体数量。 + + 要获取子实体数量的父实体的实体编号。 + 子实体数量。 + + + + 获取子实体。 + + 要获取子实体的父实体的实体编号。 + 子实体。 + + + + 获取子实体。 + + 要获取子实体的父实体。 + 子实体。 + + + + 获取所有子实体。 + + 要获取所有子实体的父实体的实体编号。 + 所有子实体。 + + + + 获取所有子实体。 + + 要获取所有子实体的父实体的实体编号。 + 所有子实体。 + + + + 获取所有子实体。 + + 要获取所有子实体的父实体。 + 所有子实体。 + + + + 获取所有子实体。 + + 要获取所有子实体的父实体。 + 所有子实体。 + + + + 附加子实体。 + + 要附加的子实体的实体编号。 + 被附加的父实体的实体编号。 + + + + 附加子实体。 + + 要附加的子实体的实体编号。 + 被附加的父实体的实体编号。 + 用户自定义数据。 + + + + 附加子实体。 + + 要附加的子实体的实体编号。 + 被附加的父实体。 + + + + 附加子实体。 + + 要附加的子实体的实体编号。 + 被附加的父实体。 + 用户自定义数据。 + + + + 附加子实体。 + + 要附加的子实体。 + 被附加的父实体的实体编号。 + + + + 附加子实体。 + + 要附加的子实体。 + 被附加的父实体的实体编号。 + 用户自定义数据。 + + + + 附加子实体。 + + 要附加的子实体。 + 被附加的父实体。 + + + + 附加子实体。 + + 要附加的子实体。 + 被附加的父实体。 + 用户自定义数据。 + + + + 解除子实体。 + + 要解除的子实体的实体编号。 + + + + 解除子实体。 + + 要解除的子实体的实体编号。 + 用户自定义数据。 + + + + 解除子实体。 + + 要解除的子实体。 + + + + 解除子实体。 + + 要解除的子实体。 + 用户自定义数据。 + + + + 解除所有子实体。 + + 被解除的父实体的实体编号。 + + + + 解除所有子实体。 + + 被解除的父实体的实体编号。 + 用户自定义数据。 + + + + 解除所有子实体。 + + 被解除的父实体。 + + + + 解除所有子实体。 + + 被解除的父实体。 + 用户自定义数据。 + + + + 获取实体信息。 + + 实体编号。 + 实体信息。 + + + + 实体组。 + + + + + 初始化实体组的新实例。 + + 实体组名称。 + 实体实例对象池自动释放可释放对象的间隔秒数。 + 实体实例对象池容量。 + 实体实例对象池对象过期秒数。 + 实体实例对象池的优先级。 + 实体组辅助器。 + 对象池管理器。 + + + + 获取实体组名称。 + + + + + 获取实体组中实体数量。 + + + + + 获取或设置实体组实例对象池自动释放可释放对象的间隔秒数。 + + + + + 获取或设置实体组实例对象池的容量。 + + + + + 获取或设置实体组实例对象池对象过期秒数。 + + + + + 获取或设置实体组实例对象池的优先级。 + + + + + 获取实体组辅助器。 + + + + + 实体组轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 实体组中是否存在实体。 + + 实体序列编号。 + 实体组中是否存在实体。 + + + + 实体组中是否存在实体。 + + 实体资源名称。 + 实体组中是否存在实体。 + + + + 从实体组中获取实体。 + + 实体序列编号。 + 要获取的实体。 + + + + 从实体组中获取实体。 + + 实体资源名称。 + 要获取的实体。 + + + + 从实体组中获取实体。 + + 实体资源名称。 + 要获取的实体。 + + + + 从实体组中获取实体。 + + 实体资源名称。 + 要获取的实体。 + + + + 从实体组中获取所有实体。 + + 实体组中的所有实体。 + + + + 从实体组中获取所有实体。 + + 实体组中的所有实体。 + + + + 往实体组增加实体。 + + 要增加的实体。 + + + + 从实体组移除实体。 + + 要移除的实体。 + + + + 实体信息。 + + + + + 实体实例对象。 + + + + + 实体状态。 + + + + + 隐藏实体完成事件。 + + + + + 初始化隐藏实体完成事件的新实例。 + + + + + 获取实体编号。 + + + + + 获取实体资源名称。 + + + + + 获取实体所属的实体组。 + + + + + 获取用户自定义数据。 + + + + + 创建隐藏实体完成事件。 + + 实体编号。 + 实体资源名称。 + 实体所属的实体组。 + 用户自定义数据。 + 创建的隐藏实体完成事件。 + + + + 清理隐藏实体完成事件。 + + + + + 实体接口。 + + + + + 获取实体编号。 + + + + + 获取实体资源名称。 + + + + + 获取实体实例。 + + + + + 获取实体所属的实体组。 + + + + + 实体初始化。 + + 实体编号。 + 实体资源名称。 + 实体所属的实体组。 + 是否是新实例。 + 用户自定义数据。 + + + + 实体回收。 + + + + + 实体显示。 + + 用户自定义数据。 + + + + 实体隐藏。 + + 是否是关闭实体管理器时触发。 + 用户自定义数据。 + + + + 实体附加子实体。 + + 附加的子实体。 + 用户自定义数据。 + + + + 实体解除子实体。 + + 解除的子实体。 + 用户自定义数据。 + + + + 实体附加子实体。 + + 被附加的父实体。 + 用户自定义数据。 + + + + 实体解除子实体。 + + 被解除的父实体。 + 用户自定义数据。 + + + + 实体轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 实体组接口。 + + + + + 获取实体组名称。 + + + + + 获取实体组中实体数量。 + + + + + 获取或设置实体组实例对象池自动释放可释放对象的间隔秒数。 + + + + + 获取或设置实体组实例对象池的容量。 + + + + + 获取或设置实体组实例对象池对象过期秒数。 + + + + + 获取或设置实体组实例对象池的优先级。 + + + + + 获取实体组辅助器。 + + + + + 实体组中是否存在实体。 + + 实体序列编号。 + 实体组中是否存在实体。 + + + + 实体组中是否存在实体。 + + 实体资源名称。 + 实体组中是否存在实体。 + + + + 从实体组中获取实体。 + + 实体序列编号。 + 要获取的实体。 + + + + 从实体组中获取实体。 + + 实体资源名称。 + 要获取的实体。 + + + + 从实体组中获取实体。 + + 实体资源名称。 + 要获取的实体。 + + + + 从实体组中获取实体。 + + 实体资源名称。 + 要获取的实体。 + + + + 从实体组中获取所有实体。 + + 实体组中的所有实体。 + + + + 从实体组中获取所有实体。 + + 实体组中的所有实体。 + + + + 设置实体实例是否被加锁。 + + 实体实例。 + 实体实例是否被加锁。 + + + + 设置实体实例的优先级。 + + 实体实例。 + 实体实例优先级。 + + + + 实体组辅助器接口。 + + + + + 实体辅助器接口。 + + + + + 实例化实体。 + + 要实例化的实体资源。 + 实例化后的实体。 + + + + 创建实体。 + + 实体实例。 + 实体所属的实体组。 + 用户自定义数据。 + 实体。 + + + + 释放实体。 + + 要释放的实体资源。 + 要释放的实体实例。 + + + + 实体管理器接口。 + + + + + 获取实体数量。 + + + + + 获取实体组数量。 + + + + + 显示实体成功事件。 + + + + + 显示实体失败事件。 + + + + + 显示实体更新事件。 + + + + + 显示实体时加载依赖资源事件。 + + + + + 隐藏实体完成事件。 + + + + + 设置对象池管理器。 + + 对象池管理器。 + + + + 设置资源管理器。 + + 资源管理器。 + + + + 设置实体辅助器。 + + 实体辅助器。 + + + + 是否存在实体组。 + + 实体组名称。 + 是否存在实体组。 + + + + 获取实体组。 + + 实体组名称。 + 要获取的实体组。 + + + + 获取所有实体组。 + + 所有实体组。 + + + + 获取所有实体组。 + + 所有实体组。 + + + + 增加实体组。 + + 实体组名称。 + 实体实例对象池自动释放可释放对象的间隔秒数。 + 实体实例对象池容量。 + 实体实例对象池对象过期秒数。 + 实体实例对象池的优先级。 + 实体组辅助器。 + 是否增加实体组成功。 + + + + 是否存在实体。 + + 实体编号。 + 是否存在实体。 + + + + 是否存在实体。 + + 实体资源名称。 + 是否存在实体。 + + + + 获取实体。 + + 实体编号。 + 要获取的实体。 + + + + 获取实体。 + + 实体资源名称。 + 要获取的实体。 + + + + 获取实体。 + + 实体资源名称。 + 要获取的实体。 + + + + 获取实体。 + + 实体资源名称。 + 要获取的实体。 + + + + 获取所有已加载的实体。 + + 所有已加载的实体。 + + + + 获取所有已加载的实体。 + + 所有已加载的实体。 + + + + 获取所有正在加载实体的编号。 + + 所有正在加载实体的编号。 + + + + 获取所有正在加载实体的编号。 + + 所有正在加载实体的编号。 + + + + 是否正在加载实体。 + + 实体编号。 + 是否正在加载实体。 + + + + 是否是合法的实体。 + + 实体。 + 实体是否合法。 + + + + 显示实体。 + + 实体编号。 + 实体资源名称。 + 实体组名称。 + + + + 显示实体。 + + 实体编号。 + 实体资源名称。 + 实体组名称。 + 加载实体资源的优先级。 + + + + 显示实体。 + + 实体编号。 + 实体资源名称。 + 实体组名称。 + 用户自定义数据。 + + + + 显示实体。 + + 实体编号。 + 实体资源名称。 + 实体组名称。 + 加载实体资源的优先级。 + 用户自定义数据。 + + + + 隐藏实体。 + + 实体编号。 + + + + 隐藏实体。 + + 实体编号。 + 用户自定义数据。 + + + + 隐藏实体。 + + 实体。 + + + + 隐藏实体。 + + 实体。 + 用户自定义数据。 + + + + 隐藏所有已加载的实体。 + + + + + 隐藏所有已加载的实体。 + + 用户自定义数据。 + + + + 隐藏所有正在加载的实体。 + + + + + 获取父实体。 + + 要获取父实体的子实体的实体编号。 + 子实体的父实体。 + + + + 获取父实体。 + + 要获取父实体的子实体。 + 子实体的父实体。 + + + + 获取子实体数量。 + + 要获取子实体数量的父实体的实体编号。 + 子实体数量。 + + + + 获取子实体。 + + 要获取子实体的父实体的实体编号。 + 子实体。 + + + + 获取子实体。 + + 要获取子实体的父实体。 + 子实体。 + + + + 获取所有子实体。 + + 要获取所有子实体的父实体的实体编号。 + 所有子实体。 + + + + 获取所有子实体。 + + 要获取所有子实体的父实体的实体编号。 + 所有子实体。 + + + + 获取所有子实体。 + + 要获取所有子实体的父实体。 + 所有子实体。 + + + + 获取所有子实体。 + + 要获取所有子实体的父实体。 + 所有子实体。 + + + + 附加子实体。 + + 要附加的子实体的实体编号。 + 被附加的父实体的实体编号。 + + + + 附加子实体。 + + 要附加的子实体的实体编号。 + 被附加的父实体的实体编号。 + 用户自定义数据。 + + + + 附加子实体。 + + 要附加的子实体的实体编号。 + 被附加的父实体。 + + + + 附加子实体。 + + 要附加的子实体的实体编号。 + 被附加的父实体。 + 用户自定义数据。 + + + + 附加子实体。 + + 要附加的子实体。 + 被附加的父实体的实体编号。 + + + + 附加子实体。 + + 要附加的子实体。 + 被附加的父实体的实体编号。 + 用户自定义数据。 + + + + 附加子实体。 + + 要附加的子实体。 + 被附加的父实体。 + + + + 附加子实体。 + + 要附加的子实体。 + 被附加的父实体。 + 用户自定义数据。 + + + + 解除子实体。 + + 要解除的子实体的实体编号。 + + + + 解除子实体。 + + 要解除的子实体的实体编号。 + 用户自定义数据。 + + + + 解除子实体。 + + 要解除的子实体。 + + + + 解除子实体。 + + 要解除的子实体。 + 用户自定义数据。 + + + + 解除所有子实体。 + + 被解除的父实体的实体编号。 + + + + 解除所有子实体。 + + 被解除的父实体的实体编号。 + 用户自定义数据。 + + + + 解除所有子实体。 + + 被解除的父实体。 + + + + 解除所有子实体。 + + 被解除的父实体。 + 用户自定义数据。 + + + + 显示实体时加载依赖资源事件。 + + + + + 初始化显示实体时加载依赖资源事件的新实例。 + + + + + 获取实体编号。 + + + + + 获取实体资源名称。 + + + + + 获取实体组名称。 + + + + + 获取被加载的依赖资源名称。 + + + + + 获取当前已加载依赖资源数量。 + + + + + 获取总共加载依赖资源数量。 + + + + + 获取用户自定义数据。 + + + + + 创建显示实体时加载依赖资源事件。 + + 实体编号。 + 实体资源名称。 + 实体组名称。 + 被加载的依赖资源名称。 + 当前已加载依赖资源数量。 + 总共加载依赖资源数量。 + 用户自定义数据。 + 创建的显示实体时加载依赖资源事件。 + + + + 清理显示实体时加载依赖资源事件。 + + + + + 显示实体失败事件。 + + + + + 初始化显示实体失败事件的新实例。 + + + + + 获取实体编号。 + + + + + 获取实体资源名称。 + + + + + 获取实体组名称。 + + + + + 获取错误信息。 + + + + + 获取用户自定义数据。 + + + + + 创建显示实体失败事件。 + + 实体编号。 + 实体资源名称。 + 实体组名称。 + 错误信息。 + 用户自定义数据。 + 创建的显示实体失败事件。 + + + + 清理显示实体失败事件。 + + + + + 显示实体成功事件。 + + + + + 初始化显示实体成功事件的新实例。 + + + + + 获取显示成功的实体。 + + + + + 获取加载持续时间。 + + + + + 获取用户自定义数据。 + + + + + 创建显示实体成功事件。 + + 加载成功的实体。 + 加载持续时间。 + 用户自定义数据。 + 创建的显示实体成功事件。 + + + + 清理显示实体成功事件。 + + + + + 显示实体更新事件。 + + + + + 初始化显示实体更新事件的新实例。 + + + + + 获取实体编号。 + + + + + 获取实体资源名称。 + + + + + 获取实体组名称。 + + + + + 获取显示实体进度。 + + + + + 获取用户自定义数据。 + + + + + 创建显示实体更新事件。 + + 实体编号。 + 实体资源名称。 + 实体组名称。 + 显示实体进度。 + 用户自定义数据。 + 创建的显示实体更新事件。 + + + + 清理显示实体更新事件。 + + + + + 事件管理器。 + + + + + 初始化事件管理器的新实例。 + + + + + 获取事件处理函数的数量。 + + + + + 获取事件数量。 + + + + + 获取游戏框架模块优先级。 + + 优先级较高的模块会优先轮询,并且关闭操作会后进行。 + + + + 事件管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理事件管理器。 + + + + + 获取事件处理函数的数量。 + + 事件类型编号。 + 事件处理函数的数量。 + + + + 检查是否存在事件处理函数。 + + 事件类型编号。 + 要检查的事件处理函数。 + 是否存在事件处理函数。 + + + + 订阅事件处理函数。 + + 事件类型编号。 + 要订阅的事件处理函数。 + + + + 取消订阅事件处理函数。 + + 事件类型编号。 + 要取消订阅的事件处理函数。 + + + + 设置默认事件处理函数。 + + 要设置的默认事件处理函数。 + + + + 抛出事件,这个操作是线程安全的,即使不在主线程中抛出,也可保证在主线程中回调事件处理函数,但事件会在抛出后的下一帧分发。 + + 事件源。 + 事件参数。 + + + + 抛出事件立即模式,这个操作不是线程安全的,事件会立刻分发。 + + 事件源。 + 事件参数。 + + + + 游戏逻辑事件基类。 + + + + + 事件管理器接口。 + + + + + 获取事件处理函数的数量。 + + + + + 获取事件数量。 + + + + + 获取事件处理函数的数量。 + + 事件类型编号。 + 事件处理函数的数量。 + + + + 检查是否存在事件处理函数。 + + 事件类型编号。 + 要检查的事件处理函数。 + 是否存在事件处理函数。 + + + + 订阅事件处理函数。 + + 事件类型编号。 + 要订阅的事件处理函数。 + + + + 取消订阅事件处理函数。 + + 事件类型编号。 + 要取消订阅的事件处理函数。 + + + + 设置默认事件处理函数。 + + 要设置的默认事件处理函数。 + + + + 抛出事件,这个操作是线程安全的,即使不在主线程中抛出,也可保证在主线程中回调事件处理函数,但事件会在抛出后的下一帧分发。 + + 事件源。 + 事件参数。 + + + + 抛出事件立即模式,这个操作不是线程安全的,事件会立刻分发。 + + 事件源。 + 事件参数。 + + + + 通用文件系统流。 + + + + + 初始化通用文件系统流的新实例。 + + 要加载的文件系统的完整路径。 + 要加载的文件系统的访问方式。 + 是否创建新的文件系统流。 + + + + 获取或设置文件系统流位置。 + + + + + 获取文件系统流长度。 + + + + + 设置文件系统流长度。 + + 要设置的文件系统流的长度。 + + + + 定位文件系统流位置。 + + 要定位的文件系统流位置的偏移。 + 要定位的文件系统流位置的方式。 + + + + 从文件系统流中读取一个字节。 + + 读取的字节,若已经到达文件结尾,则返回 -1。 + + + + 从文件系统流中读取二进制流。 + + 存储读取文件内容的二进制流。 + 存储读取文件内容的二进制流的起始位置。 + 存储读取文件内容的二进制流的长度。 + 实际读取了多少字节。 + + + + 向文件系统流中写入一个字节。 + + 要写入的字节。 + + + + 向文件系统流中写入二进制流。 + + 存储写入文件内容的二进制流。 + 存储写入文件内容的二进制流的起始位置。 + 存储写入文件内容的二进制流的长度。 + + + + 将文件系统流立刻更新到存储介质中。 + + + + + 关闭文件系统流。 + + + + + 销毁文件系统流。 + + + + + 文件信息。 + + + + + 初始化文件信息的新实例。 + + 文件名称。 + 文件偏移。 + 文件长度。 + + + + 获取文件信息是否有效。 + + + + + 获取文件名称。 + + + + + 获取文件偏移。 + + + + + 获取文件长度。 + + + + + 文件系统。 + + + + + 块数据。 + + + + + 初始化文件系统的新实例。 + + 文件系统完整路径。 + 文件系统访问方式。 + 文件系统流。 + + + + 获取文件系统完整路径。 + + + + + 获取文件系统访问方式。 + + + + + 获取文件数量。 + + + + + 获取最大文件数量。 + + + + + 创建文件系统。 + + 要创建的文件系统的完整路径。 + 要创建的文件系统的访问方式。 + 要创建的文件系统的文件系统流。 + 要创建的文件系统的最大文件数量。 + 要创建的文件系统的最大块数据数量。 + 创建的文件系统。 + + + + 加载文件系统。 + + 要加载的文件系统的完整路径。 + 要加载的文件系统的访问方式。 + 要加载的文件系统的文件系统流。 + 加载的文件系统。 + + + + 关闭并清理文件系统。 + + + + + 获取文件信息。 + + 要获取文件信息的文件名称。 + 获取的文件信息。 + + + + 获取所有文件信息。 + + 获取的所有文件信息。 + + + + 获取所有文件信息。 + + 获取的所有文件信息。 + + + + 检查是否存在指定文件。 + + 要检查的文件名称。 + 是否存在指定文件。 + + + + 读取指定文件。 + + 要读取的文件名称。 + 存储读取文件内容的二进制流。 + + + + 读取指定文件。 + + 要读取的文件名称。 + 存储读取文件内容的二进制流。 + 实际读取了多少字节。 + + + + 读取指定文件。 + + 要读取的文件名称。 + 存储读取文件内容的二进制流。 + 存储读取文件内容的二进制流的起始位置。 + 实际读取了多少字节。 + + + + 读取指定文件。 + + 要读取的文件名称。 + 存储读取文件内容的二进制流。 + 存储读取文件内容的二进制流的起始位置。 + 存储读取文件内容的二进制流的长度。 + 实际读取了多少字节。 + + + + 读取指定文件。 + + 要读取的文件名称。 + 存储读取文件内容的二进制流。 + 实际读取了多少字节。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 要读取片段的长度。 + 存储读取文件片段内容的二进制流。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 要读取片段的偏移。 + 要读取片段的长度。 + 存储读取文件片段内容的二进制流。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 存储读取文件片段内容的二进制流。 + 实际读取了多少字节。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 存储读取文件片段内容的二进制流。 + 要读取片段的长度。 + 实际读取了多少字节。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 存储读取文件片段内容的二进制流。 + 存储读取文件片段内容的二进制流的起始位置。 + 要读取片段的长度。 + 实际读取了多少字节。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 要读取片段的偏移。 + 存储读取文件片段内容的二进制流。 + 实际读取了多少字节。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 要读取片段的偏移。 + 存储读取文件片段内容的二进制流。 + 要读取片段的长度。 + 实际读取了多少字节。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 要读取片段的偏移。 + 存储读取文件片段内容的二进制流。 + 存储读取文件片段内容的二进制流的起始位置。 + 要读取片段的长度。 + 实际读取了多少字节。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 存储读取文件片段内容的二进制流。 + 要读取片段的长度。 + 实际读取了多少字节。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 要读取片段的偏移。 + 存储读取文件片段内容的二进制流。 + 要读取片段的长度。 + 实际读取了多少字节。 + + + + 写入指定文件。 + + 要写入的文件名称。 + 存储写入文件内容的二进制流。 + 是否写入指定文件成功。 + + + + 写入指定文件。 + + 要写入的文件名称。 + 存储写入文件内容的二进制流。 + 存储写入文件内容的二进制流的起始位置。 + 是否写入指定文件成功。 + + + + 写入指定文件。 + + 要写入的文件名称。 + 存储写入文件内容的二进制流。 + 存储写入文件内容的二进制流的起始位置。 + 存储写入文件内容的二进制流的长度。 + 是否写入指定文件成功。 + + + + 写入指定文件。 + + 要写入的文件名称。 + 存储写入文件内容的二进制流。 + 是否写入指定文件成功。 + + + + 写入指定文件。 + + 要写入的文件名称。 + 存储写入文件内容的文件路径。 + 是否写入指定文件成功。 + + + + 将指定文件另存为物理文件。 + + 要另存为的文件名称。 + 存储写入文件内容的文件路径。 + 是否将指定文件另存为物理文件成功。 + + + + 重命名指定文件。 + + 要重命名的文件名称。 + 重命名后的文件名称。 + 是否重命名指定文件成功。 + + + + 删除指定文件。 + + 要删除的文件名称。 + 是否删除指定文件成功。 + + + + 头数据。 + + + + + 字符串数据。 + + + + + 文件系统访问方式。 + + + + + 未指定。 + + + + + 只可读。 + + + + + 只可写。 + + + + + 可读写。 + + + + + 文件系统管理器。 + + + + + 初始化文件系统管理器的新实例。 + + + + + 获取游戏框架模块优先级。 + + 优先级较高的模块会优先轮询,并且关闭操作会后进行。 + + + + 获取文件系统数量。 + + + + + 文件系统管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理文件系统管理器。 + + + + + 设置文件系统辅助器。 + + 文件系统辅助器。 + + + + 检查是否存在文件系统。 + + 要检查的文件系统的完整路径。 + 是否存在文件系统。 + + + + 获取文件系统。 + + 要获取的文件系统的完整路径。 + 获取的文件系统。 + + + + 创建文件系统。 + + 要创建的文件系统的完整路径。 + 要创建的文件系统的访问方式。 + 要创建的文件系统的最大文件数量。 + 要创建的文件系统的最大块数据数量。 + 创建的文件系统。 + + + + 加载文件系统。 + + 要加载的文件系统的完整路径。 + 要加载的文件系统的访问方式。 + 加载的文件系统。 + + + + 销毁文件系统。 + + 要销毁的文件系统。 + 是否删除文件系统对应的物理文件。 + + + + 获取所有文件系统集合。 + + 获取的所有文件系统集合。 + + + + 获取所有文件系统集合。 + + 获取的所有文件系统集合。 + + + + 文件系统流。 + + + + + 缓存二进制流的长度。 + + + + + 缓存二进制流。 + + + + + 获取或设置文件系统流位置。 + + + + + 获取文件系统流长度。 + + + + + 设置文件系统流长度。 + + 要设置的文件系统流的长度。 + + + + 定位文件系统流位置。 + + 要定位的文件系统流位置的偏移。 + 要定位的文件系统流位置的方式。 + + + + 从文件系统流中读取一个字节。 + + 读取的字节,若已经到达文件结尾,则返回 -1。 + + + + 从文件系统流中读取二进制流。 + + 存储读取文件内容的二进制流。 + 存储读取文件内容的二进制流的起始位置。 + 存储读取文件内容的二进制流的长度。 + 实际读取了多少字节。 + + + + 从文件系统流中读取二进制流。 + + 存储读取文件内容的二进制流。 + 存储读取文件内容的二进制流的长度。 + 实际读取了多少字节。 + + + + 向文件系统流中写入一个字节。 + + 要写入的字节。 + + + + 向文件系统流中写入二进制流。 + + 存储写入文件内容的二进制流。 + 存储写入文件内容的二进制流的起始位置。 + 存储写入文件内容的二进制流的长度。 + + + + 向文件系统流中写入二进制流。 + + 存储写入文件内容的二进制流。 + 存储写入文件内容的二进制流的长度。 + + + + 将文件系统流立刻更新到存储介质中。 + + + + + 关闭文件系统流。 + + + + + 文件系统接口。 + + + + + 获取文件系统完整路径。 + + + + + 获取文件系统访问方式。 + + + + + 获取文件数量。 + + + + + 获取最大文件数量。 + + + + + 获取文件信息。 + + 要获取文件信息的文件名称。 + 获取的文件信息。 + + + + 获取所有文件信息。 + + 获取的所有文件信息。 + + + + 获取所有文件信息。 + + 获取的所有文件信息。 + + + + 检查是否存在指定文件。 + + 要检查的文件名称。 + 是否存在指定文件。 + + + + 读取指定文件。 + + 要读取的文件名称。 + 存储读取文件内容的二进制流。 + + + + 读取指定文件。 + + 要读取的文件名称。 + 存储读取文件内容的二进制流。 + 实际读取了多少字节。 + + + + 读取指定文件。 + + 要读取的文件名称。 + 存储读取文件内容的二进制流。 + 存储读取文件内容的二进制流的起始位置。 + 实际读取了多少字节。 + + + + 读取指定文件。 + + 要读取的文件名称。 + 存储读取文件内容的二进制流。 + 存储读取文件内容的二进制流的起始位置。 + 存储读取文件内容的二进制流的长度。 + 实际读取了多少字节。 + + + + 读取指定文件。 + + 要读取的文件名称。 + 存储读取文件内容的二进制流。 + 实际读取了多少字节。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 要读取片段的长度。 + 存储读取文件片段内容的二进制流。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 要读取片段的偏移。 + 要读取片段的长度。 + 存储读取文件片段内容的二进制流。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 存储读取文件片段内容的二进制流。 + 实际读取了多少字节。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 存储读取文件片段内容的二进制流。 + 要读取片段的长度。 + 实际读取了多少字节。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 存储读取文件片段内容的二进制流。 + 存储读取文件片段内容的二进制流的起始位置。 + 要读取片段的长度。 + 实际读取了多少字节。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 要读取片段的偏移。 + 存储读取文件片段内容的二进制流。 + 实际读取了多少字节。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 要读取片段的偏移。 + 存储读取文件片段内容的二进制流。 + 要读取片段的长度。 + 实际读取了多少字节。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 要读取片段的偏移。 + 存储读取文件片段内容的二进制流。 + 存储读取文件片段内容的二进制流的起始位置。 + 要读取片段的长度。 + 实际读取了多少字节。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 存储读取文件片段内容的二进制流。 + 要读取片段的长度。 + 实际读取了多少字节。 + + + + 读取指定文件的指定片段。 + + 要读取片段的文件名称。 + 要读取片段的偏移。 + 存储读取文件片段内容的二进制流。 + 要读取片段的长度。 + 实际读取了多少字节。 + + + + 写入指定文件。 + + 要写入的文件名称。 + 存储写入文件内容的二进制流。 + 是否写入指定文件成功。 + + + + 写入指定文件。 + + 要写入的文件名称。 + 存储写入文件内容的二进制流。 + 存储写入文件内容的二进制流的起始位置。 + 是否写入指定文件成功。 + + + + 写入指定文件。 + + 要写入的文件名称。 + 存储写入文件内容的二进制流。 + 存储写入文件内容的二进制流的起始位置。 + 存储写入文件内容的二进制流的长度。 + 是否写入指定文件成功。 + + + + 写入指定文件。 + + 要写入的文件名称。 + 存储写入文件内容的二进制流。 + 是否写入指定文件成功。 + + + + 写入指定文件。 + + 要写入的文件名称。 + 存储写入文件内容的文件路径。 + 是否写入指定文件成功。 + + + + 将指定文件另存为物理文件。 + + 要另存为的文件名称。 + 存储写入文件内容的文件路径。 + 是否将指定文件另存为物理文件成功。 + + + + 重命名指定文件。 + + 要重命名的文件名称。 + 重命名后的文件名称。 + 是否重命名指定文件成功。 + + + + 删除指定文件。 + + 要删除的文件名称。 + 是否删除指定文件成功。 + + + + 文件系统辅助器接口。 + + + + + 创建文件系统流。 + + 要加载的文件系统的完整路径。 + 要加载的文件系统的访问方式。 + 是否创建新的文件系统流。 + 创建的文件系统流。 + + + + 文件系统管理器。 + + + + + 获取文件系统数量。 + + + + + 设置文件系统辅助器。 + + 文件系统辅助器。 + + + + 检查是否存在文件系统。 + + 要检查的文件系统的完整路径。 + 是否存在文件系统。 + + + + 获取文件系统。 + + 要获取的文件系统的完整路径。 + 获取的文件系统。 + + + + 创建文件系统。 + + 要创建的文件系统的完整路径。 + 要创建的文件系统的访问方式。 + 要创建的文件系统的最大文件数量。 + 要创建的文件系统的最大块数据数量。 + 创建的文件系统。 + + + + 加载文件系统。 + + 要加载的文件系统的完整路径。 + 要加载的文件系统的访问方式。 + 加载的文件系统。 + + + + 销毁文件系统。 + + 要销毁的文件系统。 + 是否删除文件系统对应的物理文件。 + + + + 获取所有文件系统集合。 + + 获取的所有文件系统集合。 + + + + 获取所有文件系统集合。 + + 获取的所有文件系统集合。 + + + + 有限状态机。 + + 有限状态机持有者类型。 + + + + 初始化有限状态机的新实例。 + + + + + 获取有限状态机持有者。 + + + + + 获取有限状态机持有者类型。 + + + + + 获取有限状态机中状态的数量。 + + + + + 获取有限状态机是否正在运行。 + + + + + 获取有限状态机是否被销毁。 + + + + + 获取当前有限状态机状态。 + + + + + 获取当前有限状态机状态名称。 + + + + + 获取当前有限状态机状态持续时间。 + + + + + 创建有限状态机。 + + 有限状态机名称。 + 有限状态机持有者。 + 有限状态机状态集合。 + 创建的有限状态机。 + + + + 创建有限状态机。 + + 有限状态机名称。 + 有限状态机持有者。 + 有限状态机状态集合。 + 创建的有限状态机。 + + + + 清理有限状态机。 + + + + + 开始有限状态机。 + + 要开始的有限状态机状态类型。 + + + + 开始有限状态机。 + + 要开始的有限状态机状态类型。 + + + + 是否存在有限状态机状态。 + + 要检查的有限状态机状态类型。 + 是否存在有限状态机状态。 + + + + 是否存在有限状态机状态。 + + 要检查的有限状态机状态类型。 + 是否存在有限状态机状态。 + + + + 获取有限状态机状态。 + + 要获取的有限状态机状态类型。 + 要获取的有限状态机状态。 + + + + 获取有限状态机状态。 + + 要获取的有限状态机状态类型。 + 要获取的有限状态机状态。 + + + + 获取有限状态机的所有状态。 + + 有限状态机的所有状态。 + + + + 获取有限状态机的所有状态。 + + 有限状态机的所有状态。 + + + + 是否存在有限状态机数据。 + + 有限状态机数据名称。 + 有限状态机数据是否存在。 + + + + 获取有限状态机数据。 + + 要获取的有限状态机数据的类型。 + 有限状态机数据名称。 + 要获取的有限状态机数据。 + + + + 获取有限状态机数据。 + + 有限状态机数据名称。 + 要获取的有限状态机数据。 + + + + 设置有限状态机数据。 + + 要设置的有限状态机数据的类型。 + 有限状态机数据名称。 + 要设置的有限状态机数据。 + + + + 设置有限状态机数据。 + + 有限状态机数据名称。 + 要设置的有限状态机数据。 + + + + 移除有限状态机数据。 + + 有限状态机数据名称。 + 是否移除有限状态机数据成功。 + + + + 有限状态机轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理有限状态机。 + + + + + 切换当前有限状态机状态。 + + 要切换到的有限状态机状态类型。 + + + + 切换当前有限状态机状态。 + + 要切换到的有限状态机状态类型。 + + + + 有限状态机基类。 + + + + + 初始化有限状态机基类的新实例。 + + + + + 获取有限状态机名称。 + + + + + 获取有限状态机完整名称。 + + + + + 获取有限状态机持有者类型。 + + + + + 获取有限状态机中状态的数量。 + + + + + 获取有限状态机是否正在运行。 + + + + + 获取有限状态机是否被销毁。 + + + + + 获取当前有限状态机状态名称。 + + + + + 获取当前有限状态机状态持续时间。 + + + + + 有限状态机轮询。 + + 逻辑流逝时间,以秒为单位。 + 当前已流逝时间,以秒为单位。 + + + + 关闭并清理有限状态机。 + + + + + 有限状态机管理器。 + + + + + 初始化有限状态机管理器的新实例。 + + + + + 获取游戏框架模块优先级。 + + 优先级较高的模块会优先轮询,并且关闭操作会后进行。 + + + + 获取有限状态机数量。 + + + + + 有限状态机管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理有限状态机管理器。 + + + + + 检查是否存在有限状态机。 + + 有限状态机持有者类型。 + 是否存在有限状态机。 + + + + 检查是否存在有限状态机。 + + 有限状态机持有者类型。 + 是否存在有限状态机。 + + + + 检查是否存在有限状态机。 + + 有限状态机持有者类型。 + 有限状态机名称。 + 是否存在有限状态机。 + + + + 检查是否存在有限状态机。 + + 有限状态机持有者类型。 + 有限状态机名称。 + 是否存在有限状态机。 + + + + 获取有限状态机。 + + 有限状态机持有者类型。 + 要获取的有限状态机。 + + + + 获取有限状态机。 + + 有限状态机持有者类型。 + 要获取的有限状态机。 + + + + 获取有限状态机。 + + 有限状态机持有者类型。 + 有限状态机名称。 + 要获取的有限状态机。 + + + + 获取有限状态机。 + + 有限状态机持有者类型。 + 有限状态机名称。 + 要获取的有限状态机。 + + + + 获取所有有限状态机。 + + 所有有限状态机。 + + + + 获取所有有限状态机。 + + 所有有限状态机。 + + + + 创建有限状态机。 + + 有限状态机持有者类型。 + 有限状态机持有者。 + 有限状态机状态集合。 + 要创建的有限状态机。 + + + + 创建有限状态机。 + + 有限状态机持有者类型。 + 有限状态机名称。 + 有限状态机持有者。 + 有限状态机状态集合。 + 要创建的有限状态机。 + + + + 创建有限状态机。 + + 有限状态机持有者类型。 + 有限状态机持有者。 + 有限状态机状态集合。 + 要创建的有限状态机。 + + + + 创建有限状态机。 + + 有限状态机持有者类型。 + 有限状态机名称。 + 有限状态机持有者。 + 有限状态机状态集合。 + 要创建的有限状态机。 + + + + 销毁有限状态机。 + + 有限状态机持有者类型。 + 是否销毁有限状态机成功。 + + + + 销毁有限状态机。 + + 有限状态机持有者类型。 + 是否销毁有限状态机成功。 + + + + 销毁有限状态机。 + + 有限状态机持有者类型。 + 要销毁的有限状态机名称。 + 是否销毁有限状态机成功。 + + + + 销毁有限状态机。 + + 有限状态机持有者类型。 + 要销毁的有限状态机名称。 + 是否销毁有限状态机成功。 + + + + 销毁有限状态机。 + + 有限状态机持有者类型。 + 要销毁的有限状态机。 + 是否销毁有限状态机成功。 + + + + 销毁有限状态机。 + + 要销毁的有限状态机。 + 是否销毁有限状态机成功。 + + + + 有限状态机状态基类。 + + 有限状态机持有者类型。 + + + + 初始化有限状态机状态基类的新实例。 + + + + + 有限状态机状态初始化时调用。 + + 有限状态机引用。 + + + + 有限状态机状态进入时调用。 + + 有限状态机引用。 + + + + 有限状态机状态轮询时调用。 + + 有限状态机引用。 + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 有限状态机状态离开时调用。 + + 有限状态机引用。 + 是否是关闭有限状态机时触发。 + + + + 有限状态机状态销毁时调用。 + + 有限状态机引用。 + + + + 切换当前有限状态机状态。 + + 要切换到的有限状态机状态类型。 + 有限状态机引用。 + + + + 切换当前有限状态机状态。 + + 有限状态机引用。 + 要切换到的有限状态机状态类型。 + + + + 有限状态机接口。 + + 有限状态机持有者类型。 + + + + 获取有限状态机名称。 + + + + + 获取有限状态机完整名称。 + + + + + 获取有限状态机持有者。 + + + + + 获取有限状态机中状态的数量。 + + + + + 获取有限状态机是否正在运行。 + + + + + 获取有限状态机是否被销毁。 + + + + + 获取当前有限状态机状态。 + + + + + 获取当前有限状态机状态持续时间。 + + + + + 开始有限状态机。 + + 要开始的有限状态机状态类型。 + + + + 开始有限状态机。 + + 要开始的有限状态机状态类型。 + + + + 是否存在有限状态机状态。 + + 要检查的有限状态机状态类型。 + 是否存在有限状态机状态。 + + + + 是否存在有限状态机状态。 + + 要检查的有限状态机状态类型。 + 是否存在有限状态机状态。 + + + + 获取有限状态机状态。 + + 要获取的有限状态机状态类型。 + 要获取的有限状态机状态。 + + + + 获取有限状态机状态。 + + 要获取的有限状态机状态类型。 + 要获取的有限状态机状态。 + + + + 获取有限状态机的所有状态。 + + 有限状态机的所有状态。 + + + + 获取有限状态机的所有状态。 + + 有限状态机的所有状态。 + + + + 是否存在有限状态机数据。 + + 有限状态机数据名称。 + 有限状态机数据是否存在。 + + + + 获取有限状态机数据。 + + 要获取的有限状态机数据的类型。 + 有限状态机数据名称。 + 要获取的有限状态机数据。 + + + + 获取有限状态机数据。 + + 有限状态机数据名称。 + 要获取的有限状态机数据。 + + + + 设置有限状态机数据。 + + 要设置的有限状态机数据的类型。 + 有限状态机数据名称。 + 要设置的有限状态机数据。 + + + + 设置有限状态机数据。 + + 有限状态机数据名称。 + 要设置的有限状态机数据。 + + + + 移除有限状态机数据。 + + 有限状态机数据名称。 + 是否移除有限状态机数据成功。 + + + + 有限状态机管理器。 + + + + + 获取有限状态机数量。 + + + + + 检查是否存在有限状态机。 + + 有限状态机持有者类型。 + 是否存在有限状态机。 + + + + 检查是否存在有限状态机。 + + 有限状态机持有者类型。 + 是否存在有限状态机。 + + + + 检查是否存在有限状态机。 + + 有限状态机持有者类型。 + 有限状态机名称。 + 是否存在有限状态机。 + + + + 检查是否存在有限状态机。 + + 有限状态机持有者类型。 + 有限状态机名称。 + 是否存在有限状态机。 + + + + 获取有限状态机。 + + 有限状态机持有者类型。 + 要获取的有限状态机。 + + + + 获取有限状态机。 + + 有限状态机持有者类型。 + 要获取的有限状态机。 + + + + 获取有限状态机。 + + 有限状态机持有者类型。 + 有限状态机名称。 + 要获取的有限状态机。 + + + + 获取有限状态机。 + + 有限状态机持有者类型。 + 有限状态机名称。 + 要获取的有限状态机。 + + + + 获取所有有限状态机。 + + 所有有限状态机。 + + + + 获取所有有限状态机。 + + 所有有限状态机。 + + + + 创建有限状态机。 + + 有限状态机持有者类型。 + 有限状态机持有者。 + 有限状态机状态集合。 + 要创建的有限状态机。 + + + + 创建有限状态机。 + + 有限状态机持有者类型。 + 有限状态机名称。 + 有限状态机持有者。 + 有限状态机状态集合。 + 要创建的有限状态机。 + + + + 创建有限状态机。 + + 有限状态机持有者类型。 + 有限状态机持有者。 + 有限状态机状态集合。 + 要创建的有限状态机。 + + + + 创建有限状态机。 + + 有限状态机持有者类型。 + 有限状态机名称。 + 有限状态机持有者。 + 有限状态机状态集合。 + 要创建的有限状态机。 + + + + 销毁有限状态机。 + + 有限状态机持有者类型。 + 是否销毁有限状态机成功。 + + + + 销毁有限状态机。 + + 有限状态机持有者类型。 + 是否销毁有限状态机成功。 + + + + 销毁有限状态机。 + + 有限状态机持有者类型。 + 要销毁的有限状态机名称。 + 是否销毁有限状态机成功。 + + + + 销毁有限状态机。 + + 有限状态机持有者类型。 + 要销毁的有限状态机名称。 + 是否销毁有限状态机成功。 + + + + 销毁有限状态机。 + + 有限状态机持有者类型。 + 要销毁的有限状态机。 + 是否销毁有限状态机成功。 + + + + 销毁有限状态机。 + + 要销毁的有限状态机。 + 是否销毁有限状态机成功。 + + + + 本地化辅助器接口。 + + + + + 获取系统语言。 + + + + + 本地化管理器接口。 + + + + + 获取或设置本地化语言。 + + + + + 获取系统语言。 + + + + + 获取字典数量。 + + + + + 获取缓冲二进制流的大小。 + + + + + 设置资源管理器。 + + 资源管理器。 + + + + 设置本地化数据提供者辅助器。 + + 本地化数据提供者辅助器。 + + + + 设置本地化辅助器。 + + 本地化辅助器。 + + + + 确保二进制流缓存分配足够大小的内存并缓存。 + + 要确保二进制流缓存分配内存的大小。 + + + + 释放缓存的二进制流。 + + + + + 根据字典主键获取字典内容字符串。 + + 字典主键。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数的类型。 + 字典主键。 + 字典参数。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典参数 8 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 字典参数 8。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典参数 8 的类型。 + 字典参数 9 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 字典参数 8。 + 字典参数 9。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典参数 8 的类型。 + 字典参数 9 的类型。 + 字典参数 10 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 字典参数 8。 + 字典参数 9。 + 字典参数 10。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典参数 8 的类型。 + 字典参数 9 的类型。 + 字典参数 10 的类型。 + 字典参数 11 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 字典参数 8。 + 字典参数 9。 + 字典参数 10。 + 字典参数 11。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典参数 8 的类型。 + 字典参数 9 的类型。 + 字典参数 10 的类型。 + 字典参数 11 的类型。 + 字典参数 12 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 字典参数 8。 + 字典参数 9。 + 字典参数 10。 + 字典参数 11。 + 字典参数 12。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典参数 8 的类型。 + 字典参数 9 的类型。 + 字典参数 10 的类型。 + 字典参数 11 的类型。 + 字典参数 12 的类型。 + 字典参数 13 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 字典参数 8。 + 字典参数 9。 + 字典参数 10。 + 字典参数 11。 + 字典参数 12。 + 字典参数 13。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典参数 8 的类型。 + 字典参数 9 的类型。 + 字典参数 10 的类型。 + 字典参数 11 的类型。 + 字典参数 12 的类型。 + 字典参数 13 的类型。 + 字典参数 14 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 字典参数 8。 + 字典参数 9。 + 字典参数 10。 + 字典参数 11。 + 字典参数 12。 + 字典参数 13。 + 字典参数 14。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典参数 8 的类型。 + 字典参数 9 的类型。 + 字典参数 10 的类型。 + 字典参数 11 的类型。 + 字典参数 12 的类型。 + 字典参数 13 的类型。 + 字典参数 14 的类型。 + 字典参数 15 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 字典参数 8。 + 字典参数 9。 + 字典参数 10。 + 字典参数 11。 + 字典参数 12。 + 字典参数 13。 + 字典参数 14。 + 字典参数 15。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典参数 8 的类型。 + 字典参数 9 的类型。 + 字典参数 10 的类型。 + 字典参数 11 的类型。 + 字典参数 12 的类型。 + 字典参数 13 的类型。 + 字典参数 14 的类型。 + 字典参数 15 的类型。 + 字典参数 16 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 字典参数 8。 + 字典参数 9。 + 字典参数 10。 + 字典参数 11。 + 字典参数 12。 + 字典参数 13。 + 字典参数 14。 + 字典参数 15。 + 字典参数 16。 + 要获取的字典内容字符串。 + + + + 是否存在字典。 + + 字典主键。 + 是否存在字典。 + + + + 根据字典主键获取字典值。 + + 字典主键。 + 字典值。 + + + + 增加字典。 + + 字典主键。 + 字典内容。 + 是否增加字典成功。 + + + + 移除字典。 + + 字典主键。 + 是否移除字典成功。 + + + + 清空所有字典。 + + + + + 本地化语言。 + + + + + 未指定。 + + + + + 南非荷兰语。 + + + + + 阿尔巴尼亚语。 + + + + + 阿拉伯语。 + + + + + 巴斯克语。 + + + + + 白俄罗斯语。 + + + + + 保加利亚语。 + + + + + 加泰罗尼亚语。 + + + + + 简体中文。 + + + + + 繁体中文。 + + + + + 克罗地亚语。 + + + + + 捷克语。 + + + + + 丹麦语。 + + + + + 荷兰语。 + + + + + 英语。 + + + + + 爱沙尼亚语。 + + + + + 法罗语。 + + + + + 芬兰语。 + + + + + 法语。 + + + + + 格鲁吉亚语。 + + + + + 德语。 + + + + + 希腊语。 + + + + + 希伯来语。 + + + + + 匈牙利语。 + + + + + 冰岛语。 + + + + + 印尼语。 + + + + + 意大利语。 + + + + + 日语。 + + + + + 韩语。 + + + + + 拉脱维亚语。 + + + + + 立陶宛语。 + + + + + 马其顿语。 + + + + + 马拉雅拉姆语。 + + + + + 挪威语。 + + + + + 波斯语。 + + + + + 波兰语。 + + + + + 巴西葡萄牙语。 + + + + + 葡萄牙语。 + + + + + 罗马尼亚语。 + + + + + 俄语。 + + + + + 塞尔维亚克罗地亚语。 + + + + + 塞尔维亚西里尔语。 + + + + + 塞尔维亚拉丁语。 + + + + + 斯洛伐克语。 + + + + + 斯洛文尼亚语。 + + + + + 西班牙语。 + + + + + 瑞典语。 + + + + + 泰语。 + + + + + 土耳其语。 + + + + + 乌克兰语。 + + + + + 越南语。 + + + + + 本地化管理器。 + + + + + 初始化本地化管理器的新实例。 + + + + + 获取或设置本地化语言。 + + + + + 获取系统语言。 + + + + + 获取字典数量。 + + + + + 获取缓冲二进制流的大小。 + + + + + 读取字典成功事件。 + + + + + 读取字典失败事件。 + + + + + 读取字典更新事件。 + + + + + 读取字典时加载依赖资源事件。 + + + + + 本地化管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理本地化管理器。 + + + + + 设置资源管理器。 + + 资源管理器。 + + + + 设置本地化数据提供者辅助器。 + + 本地化数据提供者辅助器。 + + + + 设置本地化辅助器。 + + 本地化辅助器。 + + + + 确保二进制流缓存分配足够大小的内存并缓存。 + + 要确保二进制流缓存分配内存的大小。 + + + + 释放缓存的二进制流。 + + + + + 读取字典。 + + 字典资源名称。 + + + + 读取字典。 + + 字典资源名称。 + 加载字典资源的优先级。 + + + + 读取字典。 + + 字典资源名称。 + 用户自定义数据。 + + + + 读取字典。 + + 字典资源名称。 + 加载字典资源的优先级。 + 用户自定义数据。 + + + + 解析字典。 + + 要解析的字典字符串。 + 是否解析字典成功。 + + + + 解析字典。 + + 要解析的字典字符串。 + 用户自定义数据。 + 是否解析字典成功。 + + + + 解析字典。 + + 要解析的字典二进制流。 + 是否解析字典成功。 + + + + 解析字典。 + + 要解析的字典二进制流。 + 用户自定义数据。 + 是否解析字典成功。 + + + + 解析字典。 + + 要解析的字典二进制流。 + 字典二进制流的起始位置。 + 字典二进制流的长度。 + 是否解析字典成功。 + + + + 解析字典。 + + 要解析的字典二进制流。 + 字典二进制流的起始位置。 + 字典二进制流的长度。 + 用户自定义数据。 + 是否解析字典成功。 + + + + 根据字典主键获取字典内容字符串。 + + 字典主键。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数的类型。 + 字典主键。 + 字典参数。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典参数 8 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 字典参数 8。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典参数 8 的类型。 + 字典参数 9 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 字典参数 8。 + 字典参数 9。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典参数 8 的类型。 + 字典参数 9 的类型。 + 字典参数 10 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 字典参数 8。 + 字典参数 9。 + 字典参数 10。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典参数 8 的类型。 + 字典参数 9 的类型。 + 字典参数 10 的类型。 + 字典参数 11 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 字典参数 8。 + 字典参数 9。 + 字典参数 10。 + 字典参数 11。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典参数 8 的类型。 + 字典参数 9 的类型。 + 字典参数 10 的类型。 + 字典参数 11 的类型。 + 字典参数 12 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 字典参数 8。 + 字典参数 9。 + 字典参数 10。 + 字典参数 11。 + 字典参数 12。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典参数 8 的类型。 + 字典参数 9 的类型。 + 字典参数 10 的类型。 + 字典参数 11 的类型。 + 字典参数 12 的类型。 + 字典参数 13 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 字典参数 8。 + 字典参数 9。 + 字典参数 10。 + 字典参数 11。 + 字典参数 12。 + 字典参数 13。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典参数 8 的类型。 + 字典参数 9 的类型。 + 字典参数 10 的类型。 + 字典参数 11 的类型。 + 字典参数 12 的类型。 + 字典参数 13 的类型。 + 字典参数 14 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 字典参数 8。 + 字典参数 9。 + 字典参数 10。 + 字典参数 11。 + 字典参数 12。 + 字典参数 13。 + 字典参数 14。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典参数 8 的类型。 + 字典参数 9 的类型。 + 字典参数 10 的类型。 + 字典参数 11 的类型。 + 字典参数 12 的类型。 + 字典参数 13 的类型。 + 字典参数 14 的类型。 + 字典参数 15 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 字典参数 8。 + 字典参数 9。 + 字典参数 10。 + 字典参数 11。 + 字典参数 12。 + 字典参数 13。 + 字典参数 14。 + 字典参数 15。 + 要获取的字典内容字符串。 + + + + 根据字典主键获取字典内容字符串。 + + 字典参数 1 的类型。 + 字典参数 2 的类型。 + 字典参数 3 的类型。 + 字典参数 4 的类型。 + 字典参数 5 的类型。 + 字典参数 6 的类型。 + 字典参数 7 的类型。 + 字典参数 8 的类型。 + 字典参数 9 的类型。 + 字典参数 10 的类型。 + 字典参数 11 的类型。 + 字典参数 12 的类型。 + 字典参数 13 的类型。 + 字典参数 14 的类型。 + 字典参数 15 的类型。 + 字典参数 16 的类型。 + 字典主键。 + 字典参数 1。 + 字典参数 2。 + 字典参数 3。 + 字典参数 4。 + 字典参数 5。 + 字典参数 6。 + 字典参数 7。 + 字典参数 8。 + 字典参数 9。 + 字典参数 10。 + 字典参数 11。 + 字典参数 12。 + 字典参数 13。 + 字典参数 14。 + 字典参数 15。 + 字典参数 16。 + 要获取的字典内容字符串。 + + + + 是否存在字典。 + + 字典主键。 + 是否存在字典。 + + + + 根据字典主键获取字典值。 + + 字典主键。 + 字典值。 + + + + 增加字典。 + + 字典主键。 + 字典内容。 + 是否增加字典成功。 + + + + 移除字典。 + + 字典主键。 + 是否移除字典成功。 + + + + 清空所有字典。 + + + + + 网络地址类型。 + + + + + 未知。 + + + + + IP 版本 4。 + + + + + IP 版本 6。 + + + + + 网络频道接口。 + + + + + 获取网络频道名称。 + + + + + 获取网络频道所使用的 Socket。 + + + + + 获取是否已连接。 + + + + + 获取网络服务类型。 + + + + + 获取网络地址类型。 + + + + + 获取要发送的消息包数量。 + + + + + 获取累计发送的消息包数量。 + + + + + 获取已接收未处理的消息包数量。 + + + + + 获取累计已接收的消息包数量。 + + + + + 获取或设置当收到消息包时是否重置心跳流逝时间。 + + + + + 获取丢失心跳的次数。 + + + + + 获取或设置心跳间隔时长,以秒为单位。 + + + + + 获取心跳等待时长,以秒为单位。 + + + + + 注册网络消息包处理函数。 + + 要注册的网络消息包处理函数。 + + + + 设置默认事件处理函数。 + + 要设置的默认事件处理函数。 + + + + 连接到远程主机。 + + 远程主机的 IP 地址。 + 远程主机的端口号。 + + + + 连接到远程主机。 + + 远程主机的 IP 地址。 + 远程主机的端口号。 + 用户自定义数据。 + + + + 关闭网络频道。 + + + + + 向远程主机发送消息包。 + + 消息包类型。 + 要发送的消息包。 + + + + 网络频道辅助器接口。 + + + + + 获取消息包头长度。 + + + + + 初始化网络频道辅助器。 + + 网络频道。 + + + + 关闭并清理网络频道辅助器。 + + + + + 准备进行连接。 + + + + + 发送心跳消息包。 + + 是否发送心跳消息包成功。 + + + + 序列化消息包。 + + 消息包类型。 + 要序列化的消息包。 + 要序列化的目标流。 + 是否序列化成功。 + + + + 反序列化消息包头。 + + 要反序列化的来源流。 + 用户自定义错误数据。 + 反序列化后的消息包头。 + + + + 反序列化消息包。 + + 消息包头。 + 要反序列化的来源流。 + 用户自定义错误数据。 + 反序列化后的消息包。 + + + + 网络管理器接口。 + + + + + 获取网络频道数量。 + + + + + 网络连接成功事件。 + + + + + 网络连接关闭事件。 + + + + + 网络心跳包丢失事件。 + + + + + 网络错误事件。 + + + + + 用户自定义网络错误事件。 + + + + + 检查是否存在网络频道。 + + 网络频道名称。 + 是否存在网络频道。 + + + + 获取网络频道。 + + 网络频道名称。 + 要获取的网络频道。 + + + + 获取所有网络频道。 + + 所有网络频道。 + + + + 获取所有网络频道。 + + 所有网络频道。 + + + + 创建网络频道。 + + 网络频道名称。 + 网络服务类型。 + 网络频道辅助器。 + 要创建的网络频道。 + + + + 销毁网络频道。 + + 网络频道名称。 + 是否销毁网络频道成功。 + + + + 网络消息包处理器接口。 + + + + + 获取网络消息包协议编号。 + + + + + 网络消息包处理函数。 + + 网络消息包源。 + 网络消息包内容。 + + + + 网络消息包头接口。 + + + + + 获取网络消息包长度。 + + + + + 网络连接关闭事件。 + + + + + 初始化网络连接关闭事件的新实例。 + + + + + 获取网络频道。 + + + + + 创建网络连接关闭事件。 + + 网络频道。 + 创建的网络连接关闭事件。 + + + + 清理网络连接关闭事件。 + + + + + 网络连接成功事件。 + + + + + 初始化网络连接成功事件的新实例。 + + + + + 获取网络频道。 + + + + + 获取用户自定义数据。 + + + + + 创建网络连接成功事件。 + + 网络频道。 + 用户自定义数据。 + 创建的网络连接成功事件。 + + + + 清理网络连接成功事件。 + + + + + 用户自定义网络错误事件。 + + + + + 初始化用户自定义网络错误事件的新实例。 + + + + + 获取网络频道。 + + + + + 获取用户自定义错误数据。 + + + + + 创建用户自定义网络错误事件。 + + 网络频道。 + 用户自定义错误数据。 + 创建的用户自定义网络错误事件。 + + + + 清理用户自定义网络错误事件。 + + + + + 网络错误码。 + + + + + 未知错误。 + + + + + 地址族错误。 + + + + + Socket 错误。 + + + + + 连接错误。 + + + + + 发送错误。 + + + + + 接收错误。 + + + + + 序列化错误。 + + + + + 反序列化消息包头错误。 + + + + + 反序列化消息包错误。 + + + + + 网络错误事件。 + + + + + 初始化网络错误事件的新实例。 + + + + + 获取网络频道。 + + + + + 获取错误码。 + + + + + 获取 Socket 错误码。 + + + + + 获取错误信息。 + + + + + 创建网络错误事件。 + + 网络频道。 + 错误码。 + Socket 错误码。 + 错误信息。 + 创建的网络错误事件。 + + + + 清理网络错误事件。 + + + + + 网络管理器。 + + + + + 初始化网络管理器的新实例。 + + + + + 获取网络频道数量。 + + + + + 网络连接成功事件。 + + + + + 网络连接关闭事件。 + + + + + 网络心跳包丢失事件。 + + + + + 网络错误事件。 + + + + + 用户自定义网络错误事件。 + + + + + 网络管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理网络管理器。 + + + + + 检查是否存在网络频道。 + + 网络频道名称。 + 是否存在网络频道。 + + + + 获取网络频道。 + + 网络频道名称。 + 要获取的网络频道。 + + + + 获取所有网络频道。 + + 所有网络频道。 + + + + 获取所有网络频道。 + + 所有网络频道。 + + + + 创建网络频道。 + + 网络频道名称。 + 网络服务类型。 + 网络频道辅助器。 + 要创建的网络频道。 + + + + 销毁网络频道。 + + 网络频道名称。 + 是否销毁网络频道成功。 + + + + 网络频道基类。 + + + + + 初始化网络频道基类的新实例。 + + 网络频道名称。 + 网络频道辅助器。 + + + + 获取网络频道名称。 + + + + + 获取网络频道所使用的 Socket。 + + + + + 获取是否已连接。 + + + + + 获取网络服务类型。 + + + + + 获取网络地址类型。 + + + + + 获取要发送的消息包数量。 + + + + + 获取累计发送的消息包数量。 + + + + + 获取已接收未处理的消息包数量。 + + + + + 获取累计已接收的消息包数量。 + + + + + 获取或设置当收到消息包时是否重置心跳流逝时间。 + + + + + 获取丢失心跳的次数。 + + + + + 获取或设置心跳间隔时长,以秒为单位。 + + + + + 获取心跳等待时长,以秒为单位。 + + + + + 网络频道轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭网络频道。 + + + + + 注册网络消息包处理函数。 + + 要注册的网络消息包处理函数。 + + + + 设置默认事件处理函数。 + + 要设置的默认事件处理函数。 + + + + 连接到远程主机。 + + 远程主机的 IP 地址。 + 远程主机的端口号。 + + + + 连接到远程主机。 + + 远程主机的 IP 地址。 + 远程主机的端口号。 + 用户自定义数据。 + + + + 关闭连接并释放所有相关资源。 + + + + + 向远程主机发送消息包。 + + 消息包类型。 + 要发送的消息包。 + + + + 释放资源。 + + + + + 释放资源。 + + 释放资源标记。 + + + + TCP 网络频道。 + + + + + 初始化网络频道的新实例。 + + 网络频道名称。 + 网络频道辅助器。 + + + + 获取网络服务类型。 + + + + + 连接到远程主机。 + + 远程主机的 IP 地址。 + 远程主机的端口号。 + 用户自定义数据。 + + + + 使用同步接收的 TCP 网络频道。 + + + + + 初始化网络频道的新实例。 + + 网络频道名称。 + 网络频道辅助器。 + + + + 获取网络服务类型。 + + + + + 连接到远程主机。 + + 远程主机的 IP 地址。 + 远程主机的端口号。 + 用户自定义数据。 + + + + 网络心跳包丢失事件。 + + + + + 初始化网络心跳包丢失事件的新实例。 + + + + + 获取网络频道。 + + + + + 获取心跳包已丢失次数。 + + + + + 创建网络心跳包丢失事件。 + + 网络频道。 + 心跳包已丢失次数。 + 创建的网络心跳包丢失事件。 + + + + 清理网络心跳包丢失事件。 + + + + + 网络消息包基类。 + + + + + 网络服务类型。 + + + + + TCP 网络服务。 + + + + + 使用同步接收的 TCP 网络服务。 + + + + + 对象池接口。 + + 对象类型。 + + + + 获取对象池名称。 + + + + + 获取对象池完整名称。 + + + + + 获取对象池对象类型。 + + + + + 获取对象池中对象的数量。 + + + + + 获取对象池中能被释放的对象的数量。 + + + + + 获取是否允许对象被多次获取。 + + + + + 获取或设置对象池自动释放可释放对象的间隔秒数。 + + + + + 获取或设置对象池的容量。 + + + + + 获取或设置对象池对象过期秒数。 + + + + + 获取或设置对象池的优先级。 + + + + + 创建对象。 + + 对象。 + 对象是否已被获取。 + + + + 检查对象。 + + 要检查的对象是否存在。 + + + + 检查对象。 + + 对象名称。 + 要检查的对象是否存在。 + + + + 获取对象。 + + 要获取的对象。 + + + + 获取对象。 + + 对象名称。 + 要获取的对象。 + + + + 回收对象。 + + 要回收的对象。 + + + + 回收对象。 + + 要回收的对象。 + + + + 设置对象是否被加锁。 + + 要设置被加锁的对象。 + 是否被加锁。 + + + + 设置对象是否被加锁。 + + 要设置被加锁的对象。 + 是否被加锁。 + + + + 设置对象的优先级。 + + 要设置优先级的对象。 + 优先级。 + + + + 设置对象的优先级。 + + 要设置优先级的对象。 + 优先级。 + + + + 释放对象。 + + 要释放的对象。 + 释放对象是否成功。 + + + + 释放对象。 + + 要释放的对象。 + 释放对象是否成功。 + + + + 释放对象池中的可释放对象。 + + + + + 释放对象池中的可释放对象。 + + 尝试释放对象数量。 + + + + 释放对象池中的可释放对象。 + + 释放对象筛选函数。 + + + + 释放对象池中的可释放对象。 + + 尝试释放对象数量。 + 释放对象筛选函数。 + + + + 释放对象池中的所有未使用对象。 + + + + + 对象池管理器。 + + + + + 获取对象池数量。 + + + + + 检查是否存在对象池。 + + 对象类型。 + 是否存在对象池。 + + + + 检查是否存在对象池。 + + 对象类型。 + 是否存在对象池。 + + + + 检查是否存在对象池。 + + 对象类型。 + 对象池名称。 + 是否存在对象池。 + + + + 检查是否存在对象池。 + + 对象类型。 + 对象池名称。 + 是否存在对象池。 + + + + 检查是否存在对象池。 + + 要检查的条件。 + 是否存在对象池。 + + + + 获取对象池。 + + 对象类型。 + 要获取的对象池。 + + + + 获取对象池。 + + 对象类型。 + 要获取的对象池。 + + + + 获取对象池。 + + 对象类型。 + 对象池名称。 + 要获取的对象池。 + + + + 获取对象池。 + + 对象类型。 + 对象池名称。 + 要获取的对象池。 + + + + 获取对象池。 + + 要检查的条件。 + 要获取的对象池。 + + + + 获取对象池。 + + 要检查的条件。 + 要获取的对象池。 + + + + 获取对象池。 + + 要检查的条件。 + 要获取的对象池。 + + + + 获取所有对象池。 + + 所有对象池。 + + + + 获取所有对象池。 + + 所有对象池。 + + + + 获取所有对象池。 + + 是否根据对象池的优先级排序。 + 所有对象池。 + + + + 获取所有对象池。 + + 是否根据对象池的优先级排序。 + 所有对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池对象过期秒数。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池对象过期秒数。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池对象过期秒数。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池对象过期秒数。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池对象过期秒数。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池对象过期秒数。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池对象过期秒数。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池对象过期秒数。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池自动释放可释放对象的间隔秒数。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池自动释放可释放对象的间隔秒数。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池对象过期秒数。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池对象过期秒数。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池对象过期秒数。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池对象过期秒数。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池对象过期秒数。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池对象过期秒数。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池对象过期秒数。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池对象过期秒数。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池自动释放可释放对象的间隔秒数。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池自动释放可释放对象的间隔秒数。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 销毁对象池。 + + 对象类型。 + 是否销毁对象池成功。 + + + + 销毁对象池。 + + 对象类型。 + 是否销毁对象池成功。 + + + + 销毁对象池。 + + 对象类型。 + 要销毁的对象池名称。 + 是否销毁对象池成功。 + + + + 销毁对象池。 + + 对象类型。 + 要销毁的对象池名称。 + 是否销毁对象池成功。 + + + + 销毁对象池。 + + 对象类型。 + 要销毁的对象池。 + 是否销毁对象池成功。 + + + + 销毁对象池。 + + 要销毁的对象池。 + 是否销毁对象池成功。 + + + + 释放对象池中的可释放对象。 + + + + + 释放对象池中的所有未使用对象。 + + + + + 对象基类。 + + + + + 初始化对象基类的新实例。 + + + + + 获取对象名称。 + + + + + 获取对象。 + + + + + 获取或设置对象是否被加锁。 + + + + + 获取或设置对象的优先级。 + + + + + 获取自定义释放检查标记。 + + + + + 获取对象上次使用时间。 + + + + + 初始化对象基类。 + + 对象。 + + + + 初始化对象基类。 + + 对象名称。 + 对象。 + + + + 初始化对象基类。 + + 对象名称。 + 对象。 + 对象是否被加锁。 + + + + 初始化对象基类。 + + 对象名称。 + 对象。 + 对象的优先级。 + + + + 初始化对象基类。 + + 对象名称。 + 对象。 + 对象是否被加锁。 + 对象的优先级。 + + + + 清理对象基类。 + + + + + 获取对象时的事件。 + + + + + 回收对象时的事件。 + + + + + 释放对象。 + + 是否是关闭对象池时触发。 + + + + 对象信息。 + + + + + 初始化对象信息的新实例。 + + 对象名称。 + 对象是否被加锁。 + 对象自定义释放检查标记。 + 对象的优先级。 + 对象上次使用时间。 + 对象的获取计数。 + + + + 获取对象名称。 + + + + + 获取对象是否被加锁。 + + + + + 获取对象自定义释放检查标记。 + + + + + 获取对象的优先级。 + + + + + 获取对象上次使用时间。 + + + + + 获取对象是否正在使用。 + + + + + 获取对象的获取计数。 + + + + + 对象池基类。 + + + + + 初始化对象池基类的新实例。 + + + + + 初始化对象池基类的新实例。 + + 对象池名称。 + + + + 获取对象池名称。 + + + + + 获取对象池完整名称。 + + + + + 获取对象池对象类型。 + + + + + 获取对象池中对象的数量。 + + + + + 获取对象池中能被释放的对象的数量。 + + + + + 获取是否允许对象被多次获取。 + + + + + 获取或设置对象池自动释放可释放对象的间隔秒数。 + + + + + 获取或设置对象池的容量。 + + + + + 获取或设置对象池对象过期秒数。 + + + + + 获取或设置对象池的优先级。 + + + + + 释放对象池中的可释放对象。 + + + + + 释放对象池中的可释放对象。 + + 尝试释放对象数量。 + + + + 释放对象池中的所有未使用对象。 + + + + + 获取所有对象信息。 + + 所有对象信息。 + + + + 对象池管理器。 + + + + + 初始化对象池管理器的新实例。 + + + + + 获取游戏框架模块优先级。 + + 优先级较高的模块会优先轮询,并且关闭操作会后进行。 + + + + 获取对象池数量。 + + + + + 对象池管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理对象池管理器。 + + + + + 检查是否存在对象池。 + + 对象类型。 + 是否存在对象池。 + + + + 检查是否存在对象池。 + + 对象类型。 + 是否存在对象池。 + + + + 检查是否存在对象池。 + + 对象类型。 + 对象池名称。 + 是否存在对象池。 + + + + 检查是否存在对象池。 + + 对象类型。 + 对象池名称。 + 是否存在对象池。 + + + + 检查是否存在对象池。 + + 要检查的条件。 + 是否存在对象池。 + + + + 获取对象池。 + + 对象类型。 + 要获取的对象池。 + + + + 获取对象池。 + + 对象类型。 + 要获取的对象池。 + + + + 获取对象池。 + + 对象类型。 + 对象池名称。 + 要获取的对象池。 + + + + 获取对象池。 + + 对象类型。 + 对象池名称。 + 要获取的对象池。 + + + + 获取对象池。 + + 要检查的条件。 + 要获取的对象池。 + + + + 获取对象池。 + + 要检查的条件。 + 要获取的对象池。 + + + + 获取对象池。 + + 要检查的条件。 + 要获取的对象池。 + + + + 获取所有对象池。 + + 所有对象池。 + + + + 获取所有对象池。 + + 所有对象池。 + + + + 获取所有对象池。 + + 是否根据对象池的优先级排序。 + 所有对象池。 + + + + 获取所有对象池。 + + 是否根据对象池的优先级排序。 + 所有对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池对象过期秒数。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池对象过期秒数。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池对象过期秒数。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池对象过期秒数。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池对象过期秒数。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池对象过期秒数。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池对象过期秒数。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池对象过期秒数。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池自动释放可释放对象的间隔秒数。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许单次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池自动释放可释放对象的间隔秒数。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许单次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池对象过期秒数。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池对象过期秒数。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池对象过期秒数。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池对象过期秒数。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池对象过期秒数。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池对象过期秒数。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池对象过期秒数。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池对象过期秒数。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池自动释放可释放对象的间隔秒数。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 创建允许多次获取的对象池。 + + 对象类型。 + 对象池名称。 + 对象池自动释放可释放对象的间隔秒数。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + 要创建的允许多次获取的对象池。 + + + + 销毁对象池。 + + 对象类型。 + 是否销毁对象池成功。 + + + + 销毁对象池。 + + 对象类型。 + 是否销毁对象池成功。 + + + + 销毁对象池。 + + 对象类型。 + 要销毁的对象池名称。 + 是否销毁对象池成功。 + + + + 销毁对象池。 + + 对象类型。 + 要销毁的对象池名称。 + 是否销毁对象池成功。 + + + + 销毁对象池。 + + 对象类型。 + 要销毁的对象池。 + 是否销毁对象池成功。 + + + + 销毁对象池。 + + 要销毁的对象池。 + 是否销毁对象池成功。 + + + + 释放对象池中的可释放对象。 + + + + + 释放对象池中的所有未使用对象。 + + + + + 内部对象。 + + 对象类型。 + + + + 初始化内部对象的新实例。 + + + + + 获取对象名称。 + + + + + 获取对象是否被加锁。 + + + + + 获取对象的优先级。 + + + + + 获取自定义释放检查标记。 + + + + + 获取对象上次使用时间。 + + + + + 获取对象是否正在使用。 + + + + + 获取对象的获取计数。 + + + + + 创建内部对象。 + + 对象。 + 对象是否已被获取。 + 创建的内部对象。 + + + + 清理内部对象。 + + + + + 查看对象。 + + 对象。 + + + + 获取对象。 + + 对象。 + + + + 回收对象。 + + + + + 释放对象。 + + 是否是关闭对象池时触发。 + + + + 对象池。 + + 对象类型。 + + + + 初始化对象池的新实例。 + + 对象池名称。 + 是否允许对象被多次获取。 + 对象池自动释放可释放对象的间隔秒数。 + 对象池的容量。 + 对象池对象过期秒数。 + 对象池的优先级。 + + + + 获取对象池对象类型。 + + + + + 获取对象池中对象的数量。 + + + + + 获取对象池中能被释放的对象的数量。 + + + + + 获取是否允许对象被多次获取。 + + + + + 获取或设置对象池自动释放可释放对象的间隔秒数。 + + + + + 获取或设置对象池的容量。 + + + + + 获取或设置对象池对象过期秒数。 + + + + + 获取或设置对象池的优先级。 + + + + + 创建对象。 + + 对象。 + 对象是否已被获取。 + + + + 检查对象。 + + 要检查的对象是否存在。 + + + + 检查对象。 + + 对象名称。 + 要检查的对象是否存在。 + + + + 获取对象。 + + 要获取的对象。 + + + + 获取对象。 + + 对象名称。 + 要获取的对象。 + + + + 回收对象。 + + 要回收的对象。 + + + + 回收对象。 + + 要回收的对象。 + + + + 设置对象是否被加锁。 + + 要设置被加锁的对象。 + 是否被加锁。 + + + + 设置对象是否被加锁。 + + 要设置被加锁的对象。 + 是否被加锁。 + + + + 设置对象的优先级。 + + 要设置优先级的对象。 + 优先级。 + + + + 设置对象的优先级。 + + 要设置优先级的对象。 + 优先级。 + + + + 释放对象。 + + 要释放的对象。 + 释放对象是否成功。 + + + + 释放对象。 + + 要释放的对象。 + 释放对象是否成功。 + + + + 释放对象池中的可释放对象。 + + + + + 释放对象池中的可释放对象。 + + 尝试释放对象数量。 + + + + 释放对象池中的可释放对象。 + + 释放对象筛选函数。 + + + + 释放对象池中的可释放对象。 + + 尝试释放对象数量。 + 释放对象筛选函数。 + + + + 释放对象池中的所有未使用对象。 + + + + + 获取所有对象信息。 + + 所有对象信息。 + + + + 释放对象筛选函数。 + + 对象类型。 + 要筛选的对象集合。 + 需要释放的对象数量。 + 对象过期参考时间。 + 经筛选需要释放的对象集合。 + + + + 流程管理器接口。 + + + + + 获取当前流程。 + + + + + 获取当前流程持续时间。 + + + + + 初始化流程管理器。 + + 有限状态机管理器。 + 流程管理器包含的流程。 + + + + 开始流程。 + + 要开始的流程类型。 + + + + 开始流程。 + + 要开始的流程类型。 + + + + 是否存在流程。 + + 要检查的流程类型。 + 是否存在流程。 + + + + 是否存在流程。 + + 要检查的流程类型。 + 是否存在流程。 + + + + 获取流程。 + + 要获取的流程类型。 + 要获取的流程。 + + + + 获取流程。 + + 要获取的流程类型。 + 要获取的流程。 + + + + 流程基类。 + + + + + 状态初始化时调用。 + + 流程持有者。 + + + + 进入状态时调用。 + + 流程持有者。 + + + + 状态轮询时调用。 + + 流程持有者。 + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 离开状态时调用。 + + 流程持有者。 + 是否是关闭状态机时触发。 + + + + 状态销毁时调用。 + + 流程持有者。 + + + + 流程管理器。 + + + + + 初始化流程管理器的新实例。 + + + + + 获取游戏框架模块优先级。 + + 优先级较高的模块会优先轮询,并且关闭操作会后进行。 + + + + 获取当前流程。 + + + + + 获取当前流程持续时间。 + + + + + 流程管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理流程管理器。 + + + + + 初始化流程管理器。 + + 有限状态机管理器。 + 流程管理器包含的流程。 + + + + 开始流程。 + + 要开始的流程类型。 + + + + 开始流程。 + + 要开始的流程类型。 + + + + 是否存在流程。 + + 要检查的流程类型。 + 是否存在流程。 + + + + 是否存在流程。 + + 要检查的流程类型。 + 是否存在流程。 + + + + 获取流程。 + + 要获取的流程类型。 + 要获取的流程。 + + + + 获取流程。 + + 要获取的流程类型。 + 要获取的流程。 + + + + 使用可更新模式并应用资源包资源完成时的回调函数。 + + 应用的资源包路径。 + 应用资源包资源结果,全部成功为 true,否则为 false。 + + + + 使用可更新模式并检查资源完成时的回调函数。 + + 已移动的资源数量。 + 已移除的资源数量。 + 可更新的资源数量。 + 可更新的资源总大小。 + 可更新的压缩后总大小。 + + + + 检查版本资源列表结果。 + + + + + 已经是最新的。 + + + + + 需要更新。 + + + + + 资源相关常量。 + + + + + 默认资源加载优先级。 + + + + + 解密资源回调函数。 + + 要解密的资源二进制流。 + 解密二进制流的起始位置。 + 解密二进制流的长度。 + 资源名称。 + 变体名称。 + 扩展名称。 + 资源是否在只读区。 + 文件系统名称。 + 资源加载方式。 + 资源大小。 + 资源哈希值。 + + + + 检查资源是否存在的结果。 + + + + + 资源不存在。 + + + + + 资源尚未准备完毕。 + + + + + 存在资源且存储在磁盘上。 + + + + + 存在资源且存储在文件系统里。 + + + + + 存在二进制资源且存储在磁盘上。 + + + + + 存在二进制资源且存储在文件系统里。 + + + + + 加载资源代理辅助器接口。 + + + + + 加载资源代理辅助器异步加载资源更新事件。 + + + + + 加载资源代理辅助器异步读取资源文件完成事件。 + + + + + 加载资源代理辅助器异步读取资源二进制流完成事件。 + + + + + 加载资源代理辅助器异步将资源二进制流转换为加载对象完成事件。 + + + + + 加载资源代理辅助器异步加载资源完成事件。 + + + + + 加载资源代理辅助器错误事件。 + + + + + 通过加载资源代理辅助器开始异步读取资源文件。 + + 要加载资源的完整路径名。 + + + + 通过加载资源代理辅助器开始异步读取资源文件。 + + 要加载资源的文件系统。 + 要加载资源的名称。 + + + + 通过加载资源代理辅助器开始异步读取资源二进制流。 + + 要加载资源的完整路径名。 + + + + 通过加载资源代理辅助器开始异步读取资源二进制流。 + + 要加载资源的文件系统。 + 要加载资源的名称。 + + + + 通过加载资源代理辅助器开始异步将资源二进制流转换为加载对象。 + + 要加载资源的二进制流。 + + + + 通过加载资源代理辅助器开始异步加载资源。 + + 资源。 + 要加载的资源名称。 + 要加载资源的类型。 + 要加载的资源是否是场景。 + + + + 重置加载资源代理辅助器。 + + + + + 使用单机模式并初始化资源完成时的回调函数。 + + + + + 资源组接口。 + + + + + 获取资源组名称。 + + + + + 获取资源组是否准备完毕。 + + + + + 获取资源组包含资源数量。 + + + + + 获取资源组中已准备完成资源数量。 + + + + + 获取资源组包含资源的总大小。 + + + + + 获取资源组包含资源压缩后的总大小。 + + + + + 获取资源组中已准备完成资源的总大小。 + + + + + 获取资源组中已准备完成资源压缩后的总大小。 + + + + + 获取资源组的完成进度。 + + + + + 获取资源组包含的资源名称列表。 + + 资源组包含的资源名称列表。 + + + + 获取资源组包含的资源名称列表。 + + 资源组包含的资源名称列表。 + + + + 资源组集合接口。 + + + + + 获取资源组集合是否准备完毕。 + + + + + 获取资源组集合包含资源数量。 + + + + + 获取资源组集合中已准备完成资源数量。 + + + + + 获取资源组集合包含资源的总大小。 + + + + + 获取资源组集合包含资源压缩后的总大小。 + + + + + 获取资源组集合中已准备完成资源的总大小。 + + + + + 获取资源组集合中已准备完成资源压缩后的总大小。 + + + + + 获取资源组集合的完成进度。 + + + + + 获取资源组集合包含的资源组列表。 + + 资源组包含的资源名称列表。 + + + + 获取资源组集合包含的资源名称列表。 + + 资源组包含的资源名称列表。 + + + + 获取资源组集合包含的资源名称列表。 + + 资源组包含的资源名称列表。 + + + + 资源辅助器接口。 + + + + + 直接从指定文件路径加载数据流。 + + 文件路径。 + 加载数据流回调函数集。 + 用户自定义数据。 + + + + 卸载场景。 + + 场景资源名称。 + 卸载场景回调函数集。 + 用户自定义数据。 + + + + 释放资源。 + + 要释放的资源。 + + + + 资源管理器接口。 + + + + + 获取资源只读区路径。 + + + + + 获取资源读写区路径。 + + + + + 获取资源模式。 + + + + + 获取当前变体。 + + + + + 获取单机模式版本资源列表序列化器。 + + + + + 获取可更新模式版本资源列表序列化器。 + + + + + 获取本地只读区版本资源列表序列化器。 + + + + + 获取本地读写区版本资源列表序列化器。 + + + + + 获取资源包版本资源列表序列化器。 + + + + + 获取当前资源适用的游戏版本号。 + + + + + 获取当前内部资源版本号。 + + + + + 获取资源数量。 + + + + + 获取资源数量。 + + + + + 获取资源组数量。 + + + + + 获取或设置资源更新下载地址。 + + + + + 获取或设置每更新多少字节的资源,重新生成一次版本资源列表。 + + + + + 获取正在应用的资源包路径。 + + + + + 获取等待应用资源数量。 + + + + + 获取或设置资源更新重试次数。 + + + + + 获取正在更新的资源组。 + + + + + 获取等待更新资源数量。 + + + + + 获取使用时下载的等待更新资源数量。 + + + + + 获取候选更新资源数量。 + + + + + 获取加载资源代理总数量。 + + + + + 获取可用加载资源代理数量。 + + + + + 获取工作中加载资源代理数量。 + + + + + 获取等待加载资源任务数量。 + + + + + 获取或设置资源对象池自动释放可释放对象的间隔秒数。 + + + + + 获取或设置资源对象池的容量。 + + + + + 获取或设置资源对象池对象过期秒数。 + + + + + 获取或设置资源对象池的优先级。 + + + + + 获取或设置资源对象池自动释放可释放对象的间隔秒数。 + + + + + 获取或设置资源对象池的容量。 + + + + + 获取或设置资源对象池对象过期秒数。 + + + + + 获取或设置资源对象池的优先级。 + + + + + 资源校验开始事件。 + + + + + 资源校验成功事件。 + + + + + 资源校验失败事件。 + + + + + 资源应用开始事件。 + + + + + 资源应用成功事件。 + + + + + 资源应用失败事件。 + + + + + 资源更新开始事件。 + + + + + 资源更新改变事件。 + + + + + 资源更新成功事件。 + + + + + 资源更新失败事件。 + + + + + 资源更新全部完成事件。 + + + + + 设置资源只读区路径。 + + 资源只读区路径。 + + + + 设置资源读写区路径。 + + 资源读写区路径。 + + + + 设置资源模式。 + + 资源模式。 + + + + 设置当前变体。 + + 当前变体。 + + + + 设置对象池管理器。 + + 对象池管理器。 + + + + 设置文件系统管理器。 + + 文件系统管理器。 + + + + 设置下载管理器。 + + 下载管理器。 + + + + 设置解密资源回调函数。 + + 要设置的解密资源回调函数。 + 如果不设置,将使用默认的解密资源回调函数。 + + + + 设置资源辅助器。 + + 资源辅助器。 + + + + 增加加载资源代理辅助器。 + + 要增加的加载资源代理辅助器。 + + + + 使用单机模式并初始化资源。 + + 使用单机模式并初始化资源完成时的回调函数。 + + + + 使用可更新模式并检查版本资源列表。 + + 最新的内部资源版本号。 + 检查版本资源列表结果。 + + + + 使用可更新模式并更新版本资源列表。 + + 版本资源列表大小。 + 版本资源列表哈希值。 + 版本资源列表压缩后大小。 + 版本资源列表压缩后哈希值。 + 版本资源列表更新回调函数集。 + + + + 使用可更新模式并校验资源。 + + 每帧至少校验资源的大小,以字节为单位。 + 使用可更新模式并校验资源完成时的回调函数。 + + + + 使用可更新模式并检查资源。 + + 是否忽略处理其它变体的资源,若不忽略,将会移除其它变体的资源。 + 使用可更新模式并检查资源完成时的回调函数。 + + + + 使用可更新模式并应用资源包资源。 + + 要应用的资源包路径。 + 使用可更新模式并应用资源包资源完成时的回调函数。 + + + + 使用可更新模式并更新所有资源。 + + 使用可更新模式并更新默认资源组完成时的回调函数。 + + + + 使用可更新模式并更新指定资源组的资源。 + + 要更新的资源组名称。 + 使用可更新模式并更新指定资源组完成时的回调函数。 + + + + 停止更新资源。 + + + + + 校验资源包。 + + 要校验的资源包路径。 + 是否校验资源包成功。 + + + + 获取所有加载资源任务的信息。 + + 所有加载资源任务的信息。 + + + + 获取所有加载资源任务的信息。 + + 所有加载资源任务的信息。 + + + + 检查资源是否存在。 + + 要检查资源的名称。 + 检查资源是否存在的结果。 + + + + 异步加载资源。 + + 要加载资源的名称。 + 加载资源回调函数集。 + + + + 异步加载资源。 + + 要加载资源的名称。 + 要加载资源的类型。 + 加载资源回调函数集。 + + + + 异步加载资源。 + + 要加载资源的名称。 + 加载资源的优先级。 + 加载资源回调函数集。 + + + + 异步加载资源。 + + 要加载资源的名称。 + 加载资源回调函数集。 + 用户自定义数据。 + + + + 异步加载资源。 + + 要加载资源的名称。 + 要加载资源的类型。 + 加载资源的优先级。 + 加载资源回调函数集。 + + + + 异步加载资源。 + + 要加载资源的名称。 + 要加载资源的类型。 + 加载资源回调函数集。 + 用户自定义数据。 + + + + 异步加载资源。 + + 要加载资源的名称。 + 加载资源的优先级。 + 加载资源回调函数集。 + 用户自定义数据。 + + + + 异步加载资源。 + + 要加载资源的名称。 + 要加载资源的类型。 + 加载资源的优先级。 + 加载资源回调函数集。 + 用户自定义数据。 + + + + 卸载资源。 + + 要卸载的资源。 + + + + 异步加载场景。 + + 要加载场景资源的名称。 + 加载场景回调函数集。 + + + + 异步加载场景。 + + 要加载场景资源的名称。 + 加载场景资源的优先级。 + 加载场景回调函数集。 + + + + 异步加载场景。 + + 要加载场景资源的名称。 + 加载场景回调函数集。 + 用户自定义数据。 + + + + 异步加载场景。 + + 要加载场景资源的名称。 + 加载场景资源的优先级。 + 加载场景回调函数集。 + 用户自定义数据。 + + + + 异步卸载场景。 + + 要卸载场景资源的名称。 + 卸载场景回调函数集。 + + + + 异步卸载场景。 + + 要卸载场景资源的名称。 + 卸载场景回调函数集。 + 用户自定义数据。 + + + + 获取二进制资源的实际路径。 + + 要获取实际路径的二进制资源的名称。 + 二进制资源的实际路径。 + 此方法仅适用于二进制资源存储在磁盘(而非文件系统)中的情况。若二进制资源存储在文件系统中时,返回值将始终为空。 + + + + 获取二进制资源的实际路径。 + + 要获取实际路径的二进制资源的名称。 + 二进制资源是否存储在只读区中。 + 二进制资源是否存储在文件系统中。 + 二进制资源或存储二进制资源的文件系统,相对于只读区或者读写区的相对路径。 + 若二进制资源存储在文件系统中,则指示二进制资源在文件系统中的名称,否则此参数返回空。 + 是否获取二进制资源的实际路径成功。 + + + + 获取二进制资源的长度。 + + 要获取长度的二进制资源的名称。 + 二进制资源的长度。 + + + + 异步加载二进制资源。 + + 要加载二进制资源的名称。 + 加载二进制资源回调函数集。 + + + + 异步加载二进制资源。 + + 要加载二进制资源的名称。 + 加载二进制资源回调函数集。 + 用户自定义数据。 + + + + 从文件系统中加载二进制资源。 + + 要加载二进制资源的名称。 + 存储加载二进制资源的二进制流。 + + + + 从文件系统中加载二进制资源。 + + 要加载二进制资源的名称。 + 存储加载二进制资源的二进制流。 + 实际加载了多少字节。 + + + + 从文件系统中加载二进制资源。 + + 要加载二进制资源的名称。 + 存储加载二进制资源的二进制流。 + 存储加载二进制资源的二进制流的起始位置。 + 实际加载了多少字节。 + + + + 从文件系统中加载二进制资源。 + + 要加载二进制资源的名称。 + 存储加载二进制资源的二进制流。 + 存储加载二进制资源的二进制流的起始位置。 + 存储加载二进制资源的二进制流的长度。 + 实际加载了多少字节。 + + + + 从文件系统中加载二进制资源的片段。 + + 要加载片段的二进制资源的名称。 + 要加载片段的长度。 + 存储加载二进制资源片段内容的二进制流。 + + + + 从文件系统中加载二进制资源的片段。 + + 要加载片段的二进制资源的名称。 + 要加载片段的偏移。 + 要加载片段的长度。 + 存储加载二进制资源片段内容的二进制流。 + + + + 从文件系统中加载二进制资源的片段。 + + 要加载片段的二进制资源的名称。 + 存储加载二进制资源片段内容的二进制流。 + 实际加载了多少字节。 + + + + 从文件系统中加载二进制资源的片段。 + + 要加载片段的二进制资源的名称。 + 存储加载二进制资源片段内容的二进制流。 + 要加载片段的长度。 + 实际加载了多少字节。 + + + + 从文件系统中加载二进制资源的片段。 + + 要加载片段的二进制资源的名称。 + 存储加载二进制资源片段内容的二进制流。 + 存储加载二进制资源片段内容的二进制流的起始位置。 + 要加载片段的长度。 + 实际加载了多少字节。 + + + + 从文件系统中加载二进制资源的片段。 + + 要加载片段的二进制资源的名称。 + 要加载片段的偏移。 + 存储加载二进制资源片段内容的二进制流。 + 实际加载了多少字节。 + + + + 从文件系统中加载二进制资源的片段。 + + 要加载片段的二进制资源的名称。 + 要加载片段的偏移。 + 存储加载二进制资源片段内容的二进制流。 + 要加载片段的长度。 + 实际加载了多少字节。 + + + + 从文件系统中加载二进制资源的片段。 + + 要加载片段的二进制资源的名称。 + 要加载片段的偏移。 + 存储加载二进制资源片段内容的二进制流。 + 存储加载二进制资源片段内容的二进制流的起始位置。 + 要加载片段的长度。 + 实际加载了多少字节。 + + + + 检查资源组是否存在。 + + 要检查资源组的名称。 + 资源组是否存在。 + + + + 获取默认资源组。 + + 默认资源组。 + + + + 获取资源组。 + + 要获取的资源组名称。 + 要获取的资源组。 + + + + 获取所有资源组。 + + 所有资源组。 + + + + 获取所有资源组。 + + 所有资源组。 + + + + 获取资源组集合。 + + 要获取的资源组名称的集合。 + 要获取的资源组集合。 + + + + 获取资源组集合。 + + 要获取的资源组名称的集合。 + 要获取的资源组集合。 + + + + 加载资源回调函数集。 + + + + + 初始化加载资源回调函数集的新实例。 + + 加载资源成功回调函数。 + + + + 初始化加载资源回调函数集的新实例。 + + 加载资源成功回调函数。 + 加载资源失败回调函数。 + + + + 初始化加载资源回调函数集的新实例。 + + 加载资源成功回调函数。 + 加载资源更新回调函数。 + + + + 初始化加载资源回调函数集的新实例。 + + 加载资源成功回调函数。 + 加载资源时加载依赖资源回调函数。 + + + + 初始化加载资源回调函数集的新实例。 + + 加载资源成功回调函数。 + 加载资源失败回调函数。 + 加载资源更新回调函数。 + + + + 初始化加载资源回调函数集的新实例。 + + 加载资源成功回调函数。 + 加载资源失败回调函数。 + 加载资源时加载依赖资源回调函数。 + + + + 初始化加载资源回调函数集的新实例。 + + 加载资源成功回调函数。 + 加载资源失败回调函数。 + 加载资源更新回调函数。 + 加载资源时加载依赖资源回调函数。 + + + + 获取加载资源成功回调函数。 + + + + + 获取加载资源失败回调函数。 + + + + + 获取加载资源更新回调函数。 + + + + + 获取加载资源时加载依赖资源回调函数。 + + + + + 加载资源时加载依赖资源回调函数。 + + 要加载的资源名称。 + 被加载的依赖资源名称。 + 当前已加载依赖资源数量。 + 总共加载依赖资源数量。 + 用户自定义数据。 + + + + 加载资源失败回调函数。 + + 要加载的资源名称。 + 加载资源状态。 + 错误信息。 + 用户自定义数据。 + + + + 加载资源成功回调函数。 + + 要加载的资源名称。 + 已加载的资源。 + 加载持续时间。 + 用户自定义数据。 + + + + 加载资源更新回调函数。 + + 要加载的资源名称。 + 加载资源进度。 + 用户自定义数据。 + + + + 加载二进制资源回调函数集。 + + + + + 初始化加载二进制资源回调函数集的新实例。 + + 加载二进制资源成功回调函数。 + + + + 初始化加载二进制资源回调函数集的新实例。 + + 加载二进制资源成功回调函数。 + 加载二进制资源失败回调函数。 + + + + 获取加载二进制资源成功回调函数。 + + + + + 获取加载二进制资源失败回调函数。 + + + + + 加载二进制资源失败回调函数。 + + 要加载的二进制资源名称。 + 加载二进制资源状态。 + 错误信息。 + 用户自定义数据。 + + + + 加载二进制资源成功回调函数。 + + 要加载的二进制资源名称。 + 已加载的二进制资源。 + 加载持续时间。 + 用户自定义数据。 + + + + 加载数据流回调函数集。 + + + + + 初始化加载数据流回调函数集的新实例。 + + 加载数据流成功回调函数。 + + + + 初始化加载数据流回调函数集的新实例。 + + 加载数据流成功回调函数。 + 加载数据流失败回调函数。 + + + + 获取加载数据流成功回调函数。 + + + + + 获取加载数据流失败回调函数。 + + + + + 加载数据流失败回调函数。 + + 文件路径。 + 错误信息。 + 用户自定义数据。 + + + + 加载数据流成功回调函数。 + + 文件路径。 + 数据流。 + 加载持续时间。 + 用户自定义数据。 + + + + 加载资源代理辅助器错误事件。 + + + + + 初始化加载资源代理辅助器错误事件的新实例。 + + + + + 获取加载资源状态。 + + + + + 获取错误信息。 + + + + + 创建加载资源代理辅助器错误事件。 + + 加载资源状态。 + 错误信息。 + 创建的加载资源代理辅助器错误事件。 + + + + 清理加载资源代理辅助器错误事件。 + + + + + 加载资源代理辅助器异步加载资源完成事件。 + + + + + 初始化加载资源代理辅助器异步加载资源完成事件的新实例。 + + + + + 获取加载的资源。 + + + + + 创建加载资源代理辅助器异步加载资源完成事件。 + + 加载的资源。 + 创建的加载资源代理辅助器异步加载资源完成事件。 + + + + 清理加载资源代理辅助器异步加载资源完成事件。 + + + + + 加载资源代理辅助器异步将资源二进制流转换为加载对象完成事件。 + + + + + 初始化加载资源代理辅助器异步将资源二进制流转换为加载对象完成事件的新实例。 + + + + + 获取加载对象。 + + + + + 创建加载资源代理辅助器异步将资源二进制流转换为加载对象完成事件。 + + 资源对象。 + 创建的加载资源代理辅助器异步将资源二进制流转换为加载对象完成事件。 + + + + 清理加载资源代理辅助器异步将资源二进制流转换为加载对象完成事件。 + + + + + 加载资源代理辅助器异步读取资源二进制流完成事件。 + + + + + 初始化加载资源代理辅助器异步读取资源二进制流完成事件的新实例。 + + + + + 创建加载资源代理辅助器异步读取资源二进制流完成事件。 + + 资源的二进制流。 + 创建的加载资源代理辅助器异步读取资源二进制流完成事件。 + + + + 清理加载资源代理辅助器异步读取资源二进制流完成事件。 + + + + + 获取资源的二进制流。 + + 资源的二进制流。 + + + + 加载资源代理辅助器异步将资源文件转换为加载对象完成事件。 + + + + + 初始化加载资源代理辅助器异步将资源文件转换为加载对象完成事件的新实例。 + + + + + 获取加载对象。 + + + + + 创建加载资源代理辅助器异步将资源文件转换为加载对象完成事件。 + + 资源对象。 + 创建的加载资源代理辅助器异步将资源文件转换为加载对象完成事件。 + + + + 清理加载资源代理辅助器异步将资源文件转换为加载对象完成事件。 + + + + + 加载资源代理辅助器更新事件。 + + + + + 初始化加载资源代理辅助器更新事件的新实例。 + + + + + 获取进度类型。 + + + + + 获取进度。 + + + + + 创建加载资源代理辅助器更新事件。 + + 进度类型。 + 进度。 + 创建的加载资源代理辅助器更新事件。 + + + + 清理加载资源代理辅助器更新事件。 + + + + + 加载资源进度类型。 + + + + + 未知类型。 + + + + + 读取资源包。 + + + + + 加载资源包。 + + + + + 加载资源。 + + + + + 加载场景。 + + + + + 加载资源状态。 + + + + + 加载资源完成。 + + + + + 资源不存在。 + + + + + 资源尚未准备完毕。 + + + + + 依赖资源错误。 + + + + + 资源类型错误。 + + + + + 加载资源错误。 + + + + + 加载场景回调函数集。 + + + + + 初始化加载场景回调函数集的新实例。 + + 加载场景成功回调函数。 + + + + 初始化加载场景回调函数集的新实例。 + + 加载场景成功回调函数。 + 加载场景失败回调函数。 + + + + 初始化加载场景回调函数集的新实例。 + + 加载场景成功回调函数。 + 加载场景更新回调函数。 + + + + 初始化加载场景回调函数集的新实例。 + + 加载场景成功回调函数。 + 加载场景时加载依赖资源回调函数。 + + + + 初始化加载场景回调函数集的新实例。 + + 加载场景成功回调函数。 + 加载场景失败回调函数。 + 加载场景更新回调函数。 + + + + 初始化加载场景回调函数集的新实例。 + + 加载场景成功回调函数。 + 加载场景失败回调函数。 + 加载场景时加载依赖资源回调函数。 + + + + 初始化加载场景回调函数集的新实例。 + + 加载场景成功回调函数。 + 加载场景失败回调函数。 + 加载场景更新回调函数。 + 加载场景时加载依赖资源回调函数。 + + + + 获取加载场景成功回调函数。 + + + + + 获取加载场景失败回调函数。 + + + + + 获取加载场景更新回调函数。 + + + + + 获取加载场景时加载依赖资源回调函数。 + + + + + 加载场景时加载依赖资源回调函数。 + + 要加载的场景资源名称。 + 被加载的依赖资源名称。 + 当前已加载依赖资源数量。 + 总共加载依赖资源数量。 + 用户自定义数据。 + + + + 加载场景失败回调函数。 + + 要加载的场景资源名称。 + 加载场景状态。 + 错误信息。 + 用户自定义数据。 + + + + 加载场景成功回调函数。 + + 要加载的场景资源名称。 + 加载持续时间。 + 用户自定义数据。 + + + + 加载场景更新回调函数。 + + 要加载的场景资源名称。 + 加载场景进度。 + 用户自定义数据。 + + + + 本地版本资源列表。 + + + + + 初始化本地版本资源列表的新实例。 + + 包含的资源集合。 + 包含的文件系统集合。 + + + + 获取本地版本资源列表是否有效。 + + + + + 获取包含的资源集合。 + + 包含的资源集合。 + + + + 获取包含的文件系统集合。 + + 包含的文件系统集合。 + + + + 文件系统。 + + + + + 初始化文件系统的新实例。 + + 文件系统名称。 + 文件系统包含的资源索引集合。 + + + + 获取文件系统名称。 + + + + + 获取文件系统包含的资源索引集合。 + + 文件系统包含的资源索引集合。 + + + + 资源。 + + + + + 初始化资源的新实例。 + + 资源名称。 + 资源变体名称。 + 资源扩展名称。 + 资源加载方式。 + 资源长度。 + 资源哈希值。 + + + + 获取资源名称。 + + + + + 获取资源变体名称。 + + + + + 获取资源扩展名称。 + + + + + 获取资源加载方式。 + + + + + 获取资源长度。 + + + + + 获取资源哈希值。 + + + + + 单机模式版本资源列表。 + + + + + 资源。 + + + + + 初始化资源的新实例。 + + 资源名称。 + 资源包含的依赖资源索引集合。 + + + + 获取资源名称。 + + + + + 获取资源包含的依赖资源索引集合。 + + 资源包含的依赖资源索引集合。 + + + + 初始化单机模式版本资源列表的新实例。 + + 适配的游戏版本号。 + 内部资源版本号。 + 包含的资源集合。 + 包含的资源集合。 + 包含的文件系统集合。 + 包含的资源组集合。 + + + + 获取单机模式版本资源列表是否有效。 + + + + + 获取适配的游戏版本号。 + + + + + 获取内部资源版本号。 + + + + + 获取包含的资源集合。 + + 包含的资源集合。 + + + + 获取包含的资源集合。 + + 包含的资源集合。 + + + + 获取包含的文件系统集合。 + + 包含的文件系统集合。 + + + + 获取包含的资源组集合。 + + 包含的资源组集合。 + + + + 文件系统。 + + + + + 初始化文件系统的新实例。 + + 文件系统名称。 + 文件系统包含的资源索引集合。 + + + + 获取文件系统名称。 + + + + + 获取文件系统包含的资源索引集合。 + + 文件系统包含的资源索引集合。 + + + + 资源。 + + + + + 初始化资源的新实例。 + + 资源名称。 + 资源变体名称。 + 资源扩展名称。 + 资源加载方式。 + 资源长度。 + 资源哈希值。 + 资源包含的资源索引集合。 + + + + 获取资源名称。 + + + + + 获取资源变体名称。 + + + + + 获取资源扩展名称。 + + + + + 获取资源加载方式。 + + + + + 获取资源长度。 + + + + + 获取资源哈希值。 + + + + + 获取资源包含的资源索引集合。 + + 资源包含的资源索引集合。 + + + + 资源组。 + + + + + 初始化资源组的新实例。 + + 资源组名称。 + 资源组包含的资源索引集合。 + + + + 获取资源组名称。 + + + + + 获取资源组包含的资源索引集合。 + + 资源组包含的资源索引集合。 + + + + 单机模式版本资源列表序列化器。 + + + + + 初始化单机模式版本资源列表序列化器的新实例。 + + + + + 获取单机模式版本资源列表头标识。 + + 单机模式版本资源列表头标识。 + + + + 本地只读区版本资源列表序列化器。 + + + + + 初始化本地只读区版本资源列表序列化器的新实例。 + + + + + 获取本地只读区版本资源列表头标识。 + + 本地只读区版本资源列表头标识。 + + + + 本地读写区版本资源列表序列化器。 + + + + + 初始化本地读写区版本资源列表序列化器的新实例。 + + + + + 获取本地读写区版本资源列表头标识。 + + 本地读写区版本资源列表头标识。 + + + + 资源应用失败事件。 + + + + + 初始化资源应用失败事件的新实例。 + + + + + 获取资源名称。 + + + + + 获取资源包路径。 + + + + + 获取错误信息。 + + + + + 创建资源应用失败事件。 + + 资源名称。 + 资源包路径。 + 错误信息。 + 创建的资源应用失败事件。 + + + + 清理资源应用失败事件。 + + + + + 资源应用开始事件。 + + + + + 初始化资源应用开始事件的新实例。 + + + + + 获取资源包路径。 + + + + + 获取要应用资源的数量。 + + + + + 获取要应用资源的总大小。 + + + + + 创建资源应用开始事件。 + + 资源包路径。 + 要应用资源的数量。 + 要应用资源的总大小。 + 创建的资源应用开始事件。 + + + + 清理资源应用开始事件。 + + + + + 资源应用成功事件。 + + + + + 初始化资源应用成功事件的新实例。 + + + + + 获取资源名称。 + + + + + 获取资源应用后存放路径。 + + + + + 获取资源包路径。 + + + + + 获取资源大小。 + + + + + 获取压缩后大小。 + + + + + 创建资源应用成功事件。 + + 资源名称。 + 资源应用后存放路径。 + 资源包路径。 + 资源大小。 + 压缩后大小。 + 创建的资源应用成功事件。 + + + + 清理资源应用成功事件。 + + + + + 资源管理器。 + + + + + 资源信息。 + + + + + 初始化资源信息的新实例。 + + 资源名称。 + 所在资源名称。 + 依赖资源名称。 + + + + 获取资源名称。 + + + + + 获取所在资源名称。 + + + + + 获取依赖资源名称。 + + 依赖资源名称。 + + + + 初始化资源管理器的新实例。 + + + + + 获取游戏框架模块优先级。 + + 优先级较高的模块会优先轮询,并且关闭操作会后进行。 + + + + 获取资源只读区路径。 + + + + + 获取资源读写区路径。 + + + + + 获取资源模式。 + + + + + 获取当前变体。 + + + + + 获取单机模式版本资源列表序列化器。 + + + + + 获取可更新模式版本资源列表序列化器。 + + + + + 获取本地只读区版本资源列表序列化器。 + + + + + 获取本地读写区版本资源列表序列化器。 + + + + + 获取资源包版本资源列表序列化器。 + + + + + 获取当前资源适用的游戏版本号。 + + + + + 获取当前内部资源版本号。 + + + + + 获取资源数量。 + + + + + 获取资源数量。 + + + + + 获取资源组数量。 + + + + + 获取或设置资源更新下载地址前缀。 + + + + + 获取或设置每更新多少字节的资源,重新生成一次版本资源列表。 + + + + + 获取正在应用的资源包路径。 + + + + + 获取等待应用资源数量。 + + + + + 获取或设置资源更新重试次数。 + + + + + 获取正在更新的资源组。 + + + + + 获取等待更新资源数量。 + + + + + 获取使用时下载的等待更新资源数量。 + + + + + 获取候选更新资源数量。 + + + + + 获取加载资源代理总数量。 + + + + + 获取可用加载资源代理数量。 + + + + + 获取工作中加载资源代理数量。 + + + + + 获取等待加载资源任务数量。 + + + + + 获取或设置资源对象池自动释放可释放对象的间隔秒数。 + + + + + 获取或设置资源对象池的容量。 + + + + + 获取或设置资源对象池对象过期秒数。 + + + + + 获取或设置资源对象池的优先级。 + + + + + 获取或设置资源对象池自动释放可释放对象的间隔秒数。 + + + + + 获取或设置资源对象池的容量。 + + + + + 获取或设置资源对象池对象过期秒数。 + + + + + 获取或设置资源对象池的优先级。 + + + + + 资源校验开始事件。 + + + + + 资源校验成功事件。 + + + + + 资源校验失败事件。 + + + + + 资源应用开始事件。 + + + + + 资源应用成功事件。 + + + + + 资源应用失败事件。 + + + + + 资源更新开始事件。 + + + + + 资源更新改变事件。 + + + + + 资源更新成功事件。 + + + + + 资源更新失败事件。 + + + + + 资源更新全部完成事件。 + + + + + 资源管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理资源管理器。 + + + + + 设置资源只读区路径。 + + 资源只读区路径。 + + + + 设置资源读写区路径。 + + 资源读写区路径。 + + + + 设置资源模式。 + + 资源模式。 + + + + 设置当前变体。 + + 当前变体。 + + + + 设置对象池管理器。 + + 对象池管理器。 + + + + 设置文件系统管理器。 + + 文件系统管理器。 + + + + 设置下载管理器。 + + 下载管理器。 + + + + 设置解密资源回调函数。 + + 要设置的解密资源回调函数。 + 如果不设置,将使用默认的解密资源回调函数。 + + + + 设置资源辅助器。 + + 资源辅助器。 + + + + 增加加载资源代理辅助器。 + + 要增加的加载资源代理辅助器。 + + + + 使用单机模式并初始化资源。 + + 使用单机模式并初始化资源完成时的回调函数。 + + + + 使用可更新模式并检查版本资源列表。 + + 最新的内部资源版本号。 + 检查版本资源列表结果。 + + + + 使用可更新模式并更新版本资源列表。 + + 版本资源列表大小。 + 版本资源列表哈希值。 + 版本资源列表压缩后大小。 + 版本资源列表压缩后哈希值。 + 版本资源列表更新回调函数集。 + + + + 使用可更新模式并校验资源。 + + 每帧至少校验资源的大小,以字节为单位。 + 使用可更新模式并校验资源完成时的回调函数。 + + + + 使用可更新模式并检查资源。 + + 是否忽略处理其它变体的资源,若不忽略,将会移除其它变体的资源。 + 使用可更新模式并检查资源完成时的回调函数。 + + + + 使用可更新模式并应用资源包资源。 + + 要应用的资源包路径。 + 使用可更新模式并应用资源包资源完成时的回调函数。 + + + + 使用可更新模式并更新所有资源。 + + 使用可更新模式并更新默认资源组完成时的回调函数。 + + + + 使用可更新模式并更新指定资源组的资源。 + + 要更新的资源组名称。 + 使用可更新模式并更新指定资源组完成时的回调函数。 + + + + 停止更新资源。 + + + + + 校验资源包。 + + 要校验的资源包路径。 + 是否校验资源包成功。 + + + + 获取所有加载资源任务的信息。 + + 所有加载资源任务的信息。 + + + + 获取所有加载资源任务的信息。 + + 所有加载资源任务的信息。 + + + + 检查资源是否存在。 + + 要检查资源的名称。 + 检查资源是否存在的结果。 + + + + 异步加载资源。 + + 要加载资源的名称。 + 加载资源回调函数集。 + + + + 异步加载资源。 + + 要加载资源的名称。 + 要加载资源的类型。 + 加载资源回调函数集。 + + + + 异步加载资源。 + + 要加载资源的名称。 + 加载资源的优先级。 + 加载资源回调函数集。 + + + + 异步加载资源。 + + 要加载资源的名称。 + 加载资源回调函数集。 + 用户自定义数据。 + + + + 异步加载资源。 + + 要加载资源的名称。 + 要加载资源的类型。 + 加载资源的优先级。 + 加载资源回调函数集。 + + + + 异步加载资源。 + + 要加载资源的名称。 + 要加载资源的类型。 + 加载资源回调函数集。 + 用户自定义数据。 + + + + 异步加载资源。 + + 要加载资源的名称。 + 加载资源的优先级。 + 加载资源回调函数集。 + 用户自定义数据。 + + + + 异步加载资源。 + + 要加载资源的名称。 + 要加载资源的类型。 + 加载资源的优先级。 + 加载资源回调函数集。 + 用户自定义数据。 + + + + 卸载资源。 + + 要卸载的资源。 + + + + 异步加载场景。 + + 要加载场景资源的名称。 + 加载场景回调函数集。 + + + + 异步加载场景。 + + 要加载场景资源的名称。 + 加载场景资源的优先级。 + 加载场景回调函数集。 + + + + 异步加载场景。 + + 要加载场景资源的名称。 + 加载场景回调函数集。 + 用户自定义数据。 + + + + 异步加载场景。 + + 要加载场景资源的名称。 + 加载场景资源的优先级。 + 加载场景回调函数集。 + 用户自定义数据。 + + + + 异步卸载场景。 + + 要卸载场景资源的名称。 + 卸载场景回调函数集。 + + + + 异步卸载场景。 + + 要卸载场景资源的名称。 + 卸载场景回调函数集。 + 用户自定义数据。 + + + + 获取二进制资源的实际路径。 + + 要获取实际路径的二进制资源的名称。 + 二进制资源的实际路径。 + 此方法仅适用于二进制资源存储在磁盘(而非文件系统)中的情况。若二进制资源存储在文件系统中时,返回值将始终为空。 + + + + 获取二进制资源的实际路径。 + + 要获取实际路径的二进制资源的名称。 + 二进制资源是否存储在只读区中。 + 二进制资源是否存储在文件系统中。 + 二进制资源或存储二进制资源的文件系统,相对于只读区或者读写区的相对路径。 + 若二进制资源存储在文件系统中,则指示二进制资源在文件系统中的名称,否则此参数返回空。 + 是否获取二进制资源的实际路径成功。 + + + + 获取二进制资源的长度。 + + 要获取长度的二进制资源的名称。 + 二进制资源的长度。 + + + + 异步加载二进制资源。 + + 要加载二进制资源的名称。 + 加载二进制资源回调函数集。 + + + + 异步加载二进制资源。 + + 要加载二进制资源的名称。 + 加载二进制资源回调函数集。 + 用户自定义数据。 + + + + 从文件系统中加载二进制资源。 + + 要加载二进制资源的名称。 + 存储加载二进制资源的二进制流。 + + + + 从文件系统中加载二进制资源。 + + 要加载二进制资源的名称。 + 存储加载二进制资源的二进制流。 + 实际加载了多少字节。 + + + + 从文件系统中加载二进制资源。 + + 要加载二进制资源的名称。 + 存储加载二进制资源的二进制流。 + 存储加载二进制资源的二进制流的起始位置。 + 实际加载了多少字节。 + + + + 从文件系统中加载二进制资源。 + + 要加载二进制资源的名称。 + 存储加载二进制资源的二进制流。 + 存储加载二进制资源的二进制流的起始位置。 + 存储加载二进制资源的二进制流的长度。 + 实际加载了多少字节。 + + + + 从文件系统中加载二进制资源的片段。 + + 要加载片段的二进制资源的名称。 + 要加载片段的长度。 + 存储加载二进制资源片段内容的二进制流。 + + + + 从文件系统中加载二进制资源的片段。 + + 要加载片段的二进制资源的名称。 + 要加载片段的偏移。 + 要加载片段的长度。 + 存储加载二进制资源片段内容的二进制流。 + + + + 从文件系统中加载二进制资源的片段。 + + 要加载片段的二进制资源的名称。 + 存储加载二进制资源片段内容的二进制流。 + 实际加载了多少字节。 + + + + 从文件系统中加载二进制资源的片段。 + + 要加载片段的二进制资源的名称。 + 存储加载二进制资源片段内容的二进制流。 + 要加载片段的长度。 + 实际加载了多少字节。 + + + + 从文件系统中加载二进制资源的片段。 + + 要加载片段的二进制资源的名称。 + 存储加载二进制资源片段内容的二进制流。 + 存储加载二进制资源片段内容的二进制流的起始位置。 + 要加载片段的长度。 + 实际加载了多少字节。 + + + + 从文件系统中加载二进制资源的片段。 + + 要加载片段的二进制资源的名称。 + 要加载片段的偏移。 + 存储加载二进制资源片段内容的二进制流。 + 实际加载了多少字节。 + + + + 从文件系统中加载二进制资源的片段。 + + 要加载片段的二进制资源的名称。 + 要加载片段的偏移。 + 存储加载二进制资源片段内容的二进制流。 + 要加载片段的长度。 + 实际加载了多少字节。 + + + + 从文件系统中加载二进制资源的片段。 + + 要加载片段的二进制资源的名称。 + 要加载片段的偏移。 + 存储加载二进制资源片段内容的二进制流。 + 存储加载二进制资源片段内容的二进制流的起始位置。 + 要加载片段的长度。 + 实际加载了多少字节。 + + + + 检查资源组是否存在。 + + 要检查资源组的名称。 + 资源组是否存在。 + + + + 获取默认资源组。 + + 默认资源组。 + + + + 获取资源组。 + + 要获取的资源组名称。 + 要获取的资源组。 + + + + 获取所有资源组。 + + 所有资源组。 + + + + 获取所有资源组。 + + 所有资源组。 + + + + 获取资源组集合。 + + 要获取的资源组名称的集合。 + 要获取的资源组集合。 + + + + 获取资源组集合。 + + 要获取的资源组名称的集合。 + 要获取的资源组集合。 + + + + 资源加载方式类型。 + + + + + 使用文件方式加载。 + + + + + 使用内存方式加载。 + + + + + 使用内存快速解密方式加载。 + + + + + 使用内存解密方式加载。 + + + + + 使用二进制方式加载。 + + + + + 使用二进制快速解密方式加载。 + + + + + 使用二进制解密方式加载。 + + + + + 资源检查器。 + + + + + 资源检查信息。 + + + + + 资源检查状态。 + + + + + 资源状态未知。 + + + + + 资源存在且已存放于只读区中。 + + + + + 资源存在且已存放于读写区中。 + + + + + 资源不适用于当前变体。 + + + + + 资源需要更新。 + + + + + 资源已废弃。 + + + + + 初始化资源检查信息的新实例。 + + 资源名称。 + + + + 获取资源名称。 + + + + + 获取资源检查状态。 + + + + + 获取是否需要移除读写区的资源。 + + + + + 获取是否需要将读写区的资源移动到磁盘。 + + + + + 获取是否需要将读写区的资源移动到文件系统。 + + + + + 获取资源所在的文件系统名称。 + + + + + 获取资源是否使用文件系统。 + + + + + 获取读写资源所在的文件系统名称。 + + + + + 获取资源加载方式。 + + + + + 获取资源大小。 + + + + + 获取资源哈希值。 + + + + + 获取压缩后大小。 + + + + + 获取压缩后哈希值。 + + + + + 临时缓存资源所在的文件系统名称。 + + 资源所在的文件系统名称。 + + + + 设置资源在版本中的信息。 + + 资源加载方式。 + 资源大小。 + 资源哈希值。 + 压缩后大小。 + 压缩后哈希值。 + + + + 设置资源在只读区中的信息。 + + 资源加载方式。 + 资源大小。 + 资源哈希值。 + + + + 设置资源在读写区中的信息。 + + 资源加载方式。 + 资源大小。 + 资源哈希值。 + + + + 刷新资源信息状态。 + + 当前变体。 + 是否忽略处理其它变体的资源,若不忽略则移除。 + + + + 本地资源状态信息。 + + + + + 远程资源状态信息。 + + + + + 初始化资源检查器的新实例。 + + 资源管理器。 + + + + 关闭并清理资源检查器。 + + + + + 检查资源。 + + 当前使用的变体。 + 是否忽略处理其它变体的资源,若不忽略,将会移除其它变体的资源。 + + + + 资源组。 + + + + + 初始化资源组的新实例。 + + 资源组名称。 + 资源信息引用。 + + + + 获取资源组名称。 + + + + + 获取资源组是否准备完毕。 + + + + + 获取资源组包含资源数量。 + + + + + 获取资源组中已准备完成资源数量。 + + + + + 获取资源组包含资源的总大小。 + + + + + 获取资源组包含资源压缩后的总大小。 + + + + + 获取资源组中已准备完成资源的总大小。 + + + + + 获取资源组中已准备完成资源压缩后的总大小。 + + + + + 获取资源组的完成进度。 + + + + + 获取资源组包含的资源名称列表。 + + 资源组包含的资源名称列表。 + + + + 获取资源组包含的资源名称列表。 + + 资源组包含的资源名称列表。 + + + + 获取资源组包含的资源名称列表。 + + 资源组包含的资源名称列表。 + + + + 获取资源组包含的资源名称列表。 + + 资源组包含的资源名称列表。 + + + + 检查指定资源是否属于资源组。 + + 要检查的资源的名称。 + 指定资源是否属于资源组。 + + + + 向资源组中增加资源。 + + 资源名称。 + 资源大小。 + 资源压缩后的大小。 + + + + 资源组集合。 + + + + + 初始化资源组集合的新实例。 + + 资源组集合。 + 资源信息引用。 + + + + 获取资源组集合是否准备完毕。 + + + + + 获取资源组集合包含资源数量。 + + + + + 获取资源组集合中已准备完成资源数量。 + + + + + 获取资源组集合包含资源的总大小。 + + + + + 获取资源组集合包含资源压缩后的总大小。 + + + + + 获取资源组集合中已准备完成资源的总大小。 + + + + + 获取资源组集合中已准备完成资源压缩后的总大小。 + + + + + 获取资源组集合的完成进度。 + + + + + 获取资源组集合包含的资源组列表。 + + 资源组包含的资源名称列表。 + + + + 获取资源组集合包含的资源名称列表。 + + 资源组包含的资源名称列表。 + + + + 获取资源组集合包含的资源名称列表。 + + 资源组包含的资源名称列表。 + + + + 资源信息。 + + + + + 初始化资源信息的新实例。 + + 资源名称。 + 文件系统名称。 + 资源加载方式。 + 资源大小。 + 资源哈希值。 + 压缩后资源大小。 + 资源是否在只读区。 + 资源是否准备完毕。 + + + + 获取资源名称。 + + + + + 获取资源是否使用文件系统。 + + + + + 获取文件系统名称。 + + + + + 获取资源是否通过二进制方式加载。 + + + + + 获取资源加载方式。 + + + + + 获取资源大小。 + + + + + 获取资源哈希值。 + + + + + 获取压缩后资源大小。 + + + + + 获取资源是否在只读区。 + + + + + 获取资源是否准备完毕。 + + + + + 标记资源准备完毕。 + + + + + 资源初始化器。 + + + + + 初始化资源初始化器的新实例。 + + 资源管理器。 + + + + 关闭并清理资源初始化器。 + + + + + 初始化资源。 + + + + + 加载资源器。 + + + + + 资源对象。 + + + + + 初始化加载资源器的新实例。 + + 资源管理器。 + + + + 获取加载资源代理总数量。 + + + + + 获取可用加载资源代理数量。 + + + + + 获取工作中加载资源代理数量。 + + + + + 获取等待加载资源任务数量。 + + + + + 获取或设置资源对象池自动释放可释放对象的间隔秒数。 + + + + + 获取或设置资源对象池的容量。 + + + + + 获取或设置资源对象池对象过期秒数。 + + + + + 获取或设置资源对象池的优先级。 + + + + + 获取或设置资源对象池自动释放可释放对象的间隔秒数。 + + + + + 获取或设置资源对象池的容量。 + + + + + 获取或设置资源对象池对象过期秒数。 + + + + + 获取或设置资源对象池的优先级。 + + + + + 加载资源器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理加载资源器。 + + + + + 设置对象池管理器。 + + 对象池管理器。 + + + + 增加加载资源代理辅助器。 + + 要增加的加载资源代理辅助器。 + 资源辅助器。 + 资源只读区路径。 + 资源读写区路径。 + 要设置的解密资源回调函数。 + + + + 检查资源是否存在。 + + 要检查资源的名称。 + 检查资源是否存在的结果。 + + + + 异步加载资源。 + + 要加载资源的名称。 + 要加载资源的类型。 + 加载资源的优先级。 + 加载资源回调函数集。 + 用户自定义数据。 + + + + 卸载资源。 + + 要卸载的资源。 + + + + 异步加载场景。 + + 要加载场景资源的名称。 + 加载场景资源的优先级。 + 加载场景回调函数集。 + 用户自定义数据。 + + + + 异步卸载场景。 + + 要卸载场景资源的名称。 + 卸载场景回调函数集。 + 用户自定义数据。 + + + + 获取二进制资源的实际路径。 + + 要获取实际路径的二进制资源的名称。 + 二进制资源的实际路径。 + 此方法仅适用于二进制资源存储在磁盘(而非文件系统)中的情况。若二进制资源存储在文件系统中时,返回值将始终为空。 + + + + 获取二进制资源的实际路径。 + + 要获取实际路径的二进制资源的名称。 + 二进制资源是否存储在只读区中。 + 二进制资源是否存储在文件系统中。 + 二进制资源或存储二进制资源的文件系统,相对于只读区或者读写区的相对路径。 + 若二进制资源存储在文件系统中,则指示二进制资源在文件系统中的名称,否则此参数返回空。 + 是否获取二进制资源的实际路径成功。 + + + + 获取二进制资源的长度。 + + 要获取长度的二进制资源的名称。 + 二进制资源的长度。 + + + + 异步加载二进制资源。 + + 要加载二进制资源的名称。 + 加载二进制资源回调函数集。 + 用户自定义数据。 + + + + 从文件系统中加载二进制资源。 + + 要加载二进制资源的名称。 + 存储加载二进制资源的二进制流。 + + + + 从文件系统中加载二进制资源。 + + 要加载二进制资源的名称。 + 存储加载二进制资源的二进制流。 + 存储加载二进制资源的二进制流的起始位置。 + 存储加载二进制资源的二进制流的长度。 + 实际加载了多少字节。 + + + + 从文件系统中加载二进制资源的片段。 + + 要加载片段的二进制资源的名称。 + 要加载片段的偏移。 + 要加载片段的长度。 + 存储加载二进制资源片段内容的二进制流。 + + + + 从文件系统中加载二进制资源的片段。 + + 要加载片段的二进制资源的名称。 + 要加载片段的偏移。 + 存储加载二进制资源片段内容的二进制流。 + 存储加载二进制资源片段内容的二进制流的起始位置。 + 要加载片段的长度。 + 实际加载了多少字节。 + + + + 获取所有加载资源任务的信息。 + + 所有加载资源任务的信息。 + + + + 获取所有加载资源任务的信息。 + + 所有加载资源任务的信息。 + + + + 加载资源代理。 + + + + + 初始化加载资源代理的新实例。 + + 加载资源代理辅助器。 + 资源辅助器。 + 加载资源器。 + 资源只读区路径。 + 资源读写区路径。 + 解密资源回调函数。 + + + + 获取加载资源任务。 + + + + + 初始化加载资源代理。 + + + + + 加载资源代理轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理加载资源代理。 + + + + + 开始处理加载资源任务。 + + 要处理的加载资源任务。 + 开始处理任务的状态。 + + + + 重置加载资源代理。 + + + + + 资源对象。 + + + + + 资源名称。 + + + + + 初始化资源名称的新实例。 + + 资源名称。 + 变体名称。 + 扩展名称。 + + + + 获取资源名称。 + + + + + 获取变体名称。 + + + + + 获取扩展名称。 + + + + + 资源名称比较器。 + + + + + 资源更新器。 + + + + + 资源应用信息。 + + + + + 初始化资源应用信息的新实例。 + + 资源名称。 + 资源所在的文件系统名称。 + 资源加载方式。 + 资源偏移。 + 资源大小。 + 资源哈希值。 + 压缩后大小。 + 压缩后哈希值。 + 资源路径。 + + + + 获取资源名称。 + + + + + 获取资源是否使用文件系统。 + + + + + 获取资源所在的文件系统名称。 + + + + + 获取资源加载方式。 + + + + + 获取资源偏移。 + + + + + 获取资源大小。 + + + + + 获取资源哈希值。 + + + + + 获取压缩后大小。 + + + + + 获取压缩后哈希值。 + + + + + 获取资源路径。 + + + + + 初始化资源更新器的新实例。 + + 资源管理器。 + + + + 获取或设置每更新多少字节的资源,重新生成一次版本资源列表。 + + + + + 获取正在应用的资源包路径。 + + + + + 获取等待应用资源数量。 + + + + + 获取或设置资源更新重试次数。 + + + + + 获取正在更新的资源组。 + + + + + 获取等待更新资源数量。 + + + + + 获取使用时下载的等待更新资源数量。 + + + + + 获取候选更新资源数量。 + + + + + 资源更新器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理资源更新器。 + + + + + 设置下载管理器。 + + 下载管理器。 + + + + 增加资源更新。 + + 资源名称。 + 资源所在的文件系统名称。 + 资源加载方式。 + 资源大小。 + 资源哈希值。 + 压缩后大小。 + 压缩后哈希值。 + 资源路径。 + + + + 检查资源完成。 + + 是否需要生成读写区版本资源列表。 + + + + 应用指定资源包的资源。 + + 要应用的资源包路径。 + + + + 更新指定资源组的资源。 + + 要更新的资源组。 + + + + 停止更新资源。 + + + + + 更新指定资源。 + + 要更新的资源名称。 + + + + 资源更新信息。 + + + + + 初始化资源更新信息的新实例。 + + 资源名称。 + 资源所在的文件系统名称。 + 资源加载方式。 + 资源大小。 + 资源哈希值。 + 压缩后大小。 + 压缩后哈希值。 + 资源路径。 + + + + 获取资源名称。 + + + + + 获取资源是否使用文件系统。 + + + + + 获取资源所在的文件系统名称。 + + + + + 获取资源加载方式。 + + + + + 获取资源大小。 + + + + + 获取资源哈希值。 + + + + + 获取压缩后大小。 + + + + + 获取压缩后哈希值。 + + + + + 获取资源路径。 + + + + + 获取或设置下载状态。 + + + + + 获取或设置已重试次数。 + + + + + 资源校验器。 + + + + + 初始化资源校验器的新实例。 + + 资源管理器。 + + + + 资源校验器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理资源校验器。 + + + + + 校验资源。 + + 每帧至少校验资源的大小,以字节为单位。 + + + + 资源校验信息。 + + + + + 初始化资源校验信息的新实例。 + + 资源名称。 + 资源所在的文件系统名称。 + 资源加载方式。 + 资源大小。 + 资源哈希值。 + + + + 获取资源名称。 + + + + + 获取资源是否使用文件系统。 + + + + + 获取资源所在的文件系统名称。 + + + + + 获取资源加载方式。 + + + + + 获取资源大小。 + + + + + 获取资源哈希值。 + + + + + 版本资源列表处理器。 + + + + + 初始化版本资源列表处理器的新实例。 + + 资源管理器。 + + + + 关闭并清理版本资源列表处理器。 + + + + + 设置下载管理器。 + + 下载管理器。 + + + + 检查版本资源列表。 + + 最新的内部资源版本号。 + 检查版本资源列表结果。 + + + + 更新版本资源列表。 + + 版本资源列表大小。 + 版本资源列表哈希值。 + 版本资源列表压缩后大小。 + 版本资源列表压缩后哈希值。 + + + + 资源模式。 + + + + + 未指定。 + + + + + 单机模式。 + + + + + 预下载的可更新模式。 + + + + + 使用时下载的可更新模式。 + + + + + 资源包版本资源列表。 + + + + + 初始化资源包版本资源列表的新实例。 + + 资源数据偏移。 + 资源数据长度。 + 资源数据哈希值。 + 包含的资源集合。 + + + + 获取资源包版本资源列表是否有效。 + + + + + 获取资源数据偏移。 + + + + + 获取资源数据长度。 + + + + + 获取资源数据哈希值。 + + + + + 获取包含的资源集合。 + + 包含的资源集合。 + + + + 资源。 + + + + + 初始化资源的新实例。 + + 资源名称。 + 资源变体名称。 + 资源扩展名称。 + 资源加载方式。 + 资源偏移。 + 资源长度。 + 资源哈希值。 + 资源压缩后长度。 + 资源压缩后哈希值。 + + + + 获取资源名称。 + + + + + 获取资源变体名称。 + + + + + 获取资源扩展名称。 + + + + + 获取资源加载方式。 + + + + + 获取资源偏移。 + + + + + 获取资源长度。 + + + + + 获取资源哈希值。 + + + + + 获取资源压缩后长度。 + + + + + 获取资源压缩后哈希值。 + + + + + 资源包版本资源列表序列化器。 + + + + + 初始化资源包版本资源列表序列化器的新实例。 + + + + + 获取资源包版本资源列表头标识。 + + 资源包版本资源列表头标识。 + + + + 资源更新全部完成事件。 + + + + + 初始化资源更新全部完成事件的新实例。 + + + + + 创建资源更新全部完成事件。 + + 创建的资源更新全部完成事件。 + + + + 清理资源更新全部完成事件。 + + + + + 资源更新改变事件。 + + + + + 初始化资源更新改变事件的新实例。 + + + + + 获取资源名称。 + + + + + 获取资源下载后存放路径。 + + + + + 获取下载地址。 + + + + + 获取当前下载大小。 + + + + + 获取压缩后大小。 + + + + + 创建资源更新改变事件。 + + 资源名称。 + 资源下载后存放路径。 + 资源下载地址。 + 当前下载大小。 + 压缩后大小。 + 创建的资源更新改变事件。 + + + + 清理资源更新改变事件。 + + + + + 资源更新失败事件。 + + + + + 初始化资源更新失败事件的新实例。 + + + + + 获取资源名称。 + + + + + 获取下载地址。 + + + + + 获取已重试次数。 + + + + + 获取设定的重试次数。 + + + + + 获取错误信息。 + + + + + 创建资源更新失败事件。 + + 资源名称。 + 下载地址。 + 已重试次数。 + 设定的重试次数。 + 错误信息。 + 创建的资源更新失败事件。 + 当已重试次数达到设定的重试次数时,将不再重试。 + + + + 清理资源更新失败事件。 + + + + + 资源更新开始事件。 + + + + + 初始化资源更新开始事件的新实例。 + + + + + 获取资源名称。 + + + + + 获取资源下载后存放路径。 + + + + + 获取下载地址。 + + + + + 获取当前下载大小。 + + + + + 获取压缩后大小。 + + + + + 获取已重试下载次数。 + + + + + 创建资源更新开始事件。 + + 资源名称。 + 资源下载后存放路径。 + 资源下载地址。 + 当前下载大小。 + 压缩后大小。 + 已重试下载次数。 + 创建的资源更新开始事件。 + + + + 清理资源更新开始事件。 + + + + + 资源更新成功事件。 + + + + + 初始化资源更新成功事件的新实例。 + + + + + 获取资源名称。 + + + + + 获取资源下载后存放路径。 + + + + + 获取下载地址。 + + + + + 获取资源大小。 + + + + + 获取压缩后大小。 + + + + + 创建资源更新成功事件。 + + 资源名称。 + 资源下载后存放路径。 + 资源下载地址。 + 资源大小。 + 压缩后大小。 + 创建的资源更新成功事件。 + + + + 清理资源更新成功事件。 + + + + + 资源校验失败事件。 + + + + + 初始化资源校验失败事件的新实例。 + + + + + 获取资源名称。 + + + + + 创建资源校验失败事件。 + + 资源名称。 + 创建的资源校验失败事件。 + + + + 清理资源校验失败事件。 + + + + + 资源校验开始事件。 + + + + + 初始化资源校验开始事件的新实例。 + + + + + 获取要校验资源的数量。 + + + + + 获取要校验资源的总大小。 + + + + + 创建资源校验开始事件。 + + 要校验资源的数量。 + 要校验资源的总大小。 + 创建的资源校验开始事件。 + + + + 清理资源校验开始事件。 + + + + + 资源校验成功事件。 + + + + + 初始化资源校验成功事件的新实例。 + + + + + 获取资源名称。 + + + + + 获取资源大小。 + + + + + 创建资源校验成功事件。 + + 资源名称。 + 资源大小。 + 创建的资源校验成功事件。 + + + + 清理资源校验成功事件。 + + + + + 卸载场景回调函数集。 + + + + + 初始化卸载场景回调函数集的新实例。 + + 卸载场景成功回调函数。 + + + + 初始化卸载场景回调函数集的新实例。 + + 卸载场景成功回调函数。 + 卸载场景失败回调函数。 + + + + 获取卸载场景成功回调函数。 + + + + + 获取卸载场景失败回调函数。 + + + + + 卸载场景失败回调函数。 + + 要卸载的场景资源名称。 + 用户自定义数据。 + + + + 卸载场景成功回调函数。 + + 要卸载的场景资源名称。 + 用户自定义数据。 + + + + 可更新模式版本资源列表。 + + + + + 资源。 + + + + + 初始化资源的新实例。 + + 资源名称。 + 资源包含的依赖资源索引集合。 + + + + 获取资源名称。 + + + + + 获取资源包含的依赖资源索引集合。 + + 资源包含的依赖资源索引集合。 + + + + 初始化可更新模式版本资源列表的新实例。 + + 适配的游戏版本号。 + 内部资源版本号。 + 包含的资源集合。 + 包含的资源集合。 + 包含的文件系统集合。 + 包含的资源组集合。 + + + + 获取可更新模式版本资源列表是否有效。 + + + + + 获取适配的游戏版本号。 + + + + + 获取内部资源版本号。 + + + + + 获取包含的资源集合。 + + 包含的资源集合。 + + + + 获取包含的资源集合。 + + 包含的资源集合。 + + + + 获取包含的文件系统集合。 + + 包含的文件系统集合。 + + + + 获取包含的资源组集合。 + + 包含的资源组集合。 + + + + 文件系统。 + + + + + 初始化文件系统的新实例。 + + 文件系统名称。 + 文件系统包含的资源索引集合。 + + + + 获取文件系统名称。 + + + + + 获取文件系统包含的资源索引集合。 + + 文件系统包含的资源索引集合。 + + + + 资源。 + + + + + 初始化资源的新实例。 + + 资源名称。 + 资源变体名称。 + 资源扩展名称。 + 资源加载方式。 + 资源长度。 + 资源哈希值。 + 资源压缩后长度。 + 资源压缩后哈希值。 + 资源包含的资源索引集合。 + + + + 获取资源名称。 + + + + + 获取资源变体名称。 + + + + + 获取资源扩展名称。 + + + + + 获取资源加载方式。 + + + + + 获取资源长度。 + + + + + 获取资源哈希值。 + + + + + 获取资源压缩后长度。 + + + + + 获取资源压缩后哈希值。 + + + + + 获取资源包含的资源索引集合。 + + 资源包含的资源索引集合。 + + + + 资源组。 + + + + + 初始化资源组的新实例。 + + 资源组名称。 + 资源组包含的资源索引集合。 + + + + 获取资源组名称。 + + + + + 获取资源组包含的资源索引集合。 + + 资源组包含的资源索引集合。 + + + + 可更新模式版本资源列表序列化器。 + + + + + 初始化可更新模式版本资源列表序列化器的新实例。 + + + + + 获取可更新模式版本资源列表头标识。 + + 可更新模式版本资源列表头标识。 + + + + 使用可更新模式并更新指定资源组完成时的回调函数。 + + 更新的资源组。 + 更新资源结果,全部成功为 true,否则为 false。 + + + + 版本资源列表更新回调函数集。 + + + + + 初始化版本资源列表更新回调函数集的新实例。 + + 版本资源列表更新成功回调函数。 + + + + 初始化版本资源列表更新回调函数集的新实例。 + + 版本资源列表更新成功回调函数。 + 版本资源列表更新失败回调函数。 + + + + 获取版本资源列表更新成功回调函数。 + + + + + 获取版本资源列表更新失败回调函数。 + + + + + 版本资源列表更新失败回调函数。 + + 版本资源列表更新地址。 + 错误信息。 + + + + 版本资源列表更新成功回调函数。 + + 版本资源列表更新后存放路径。 + 版本资源列表更新地址。 + + + + 使用可更新模式并校验资源完成时的回调函数。 + + 校验资源结果,全部成功为 true,否则为 false。 + + + + 场景管理器接口。 + + + + + 加载场景成功事件。 + + + + + 加载场景失败事件。 + + + + + 加载场景更新事件。 + + + + + 加载场景时加载依赖资源事件。 + + + + + 卸载场景成功事件。 + + + + + 卸载场景失败事件。 + + + + + 设置资源管理器。 + + 资源管理器。 + + + + 获取场景是否已加载。 + + 场景资源名称。 + 场景是否已加载。 + + + + 获取已加载场景的资源名称。 + + 已加载场景的资源名称。 + + + + 获取已加载场景的资源名称。 + + 已加载场景的资源名称。 + + + + 获取场景是否正在加载。 + + 场景资源名称。 + 场景是否正在加载。 + + + + 获取正在加载场景的资源名称。 + + 正在加载场景的资源名称。 + + + + 获取正在加载场景的资源名称。 + + 正在加载场景的资源名称。 + + + + 获取场景是否正在卸载。 + + 场景资源名称。 + 场景是否正在卸载。 + + + + 获取正在卸载场景的资源名称。 + + 正在卸载场景的资源名称。 + + + + 获取正在卸载场景的资源名称。 + + 正在卸载场景的资源名称。 + + + + 检查场景资源是否存在。 + + 要检查场景资源的名称。 + 场景资源是否存在。 + + + + 加载场景。 + + 场景资源名称。 + + + + 加载场景。 + + 场景资源名称。 + 加载场景资源的优先级。 + + + + 加载场景。 + + 场景资源名称。 + 用户自定义数据。 + + + + 加载场景。 + + 场景资源名称。 + 加载场景资源的优先级。 + 用户自定义数据。 + + + + 卸载场景。 + + 场景资源名称。 + + + + 卸载场景。 + + 场景资源名称。 + 用户自定义数据。 + + + + 加载场景时加载依赖资源事件。 + + + + + 初始化加载场景时加载依赖资源事件的新实例。 + + + + + 获取场景资源名称。 + + + + + 获取被加载的依赖资源名称。 + + + + + 获取当前已加载依赖资源数量。 + + + + + 获取总共加载依赖资源数量。 + + + + + 获取用户自定义数据。 + + + + + 创建场景时加载依赖资源事件。 + + 场景资源名称。 + 被加载的依赖资源名称。 + 当前已加载依赖资源数量。 + 总共加载依赖资源数量。 + 用户自定义数据。 + 创建的场景时加载依赖资源事件。 + + + + 清理场景时加载依赖资源事件。 + + + + + 加载场景失败事件。 + + + + + 初始化加载场景失败事件的新实例。 + + + + + 获取场景资源名称。 + + + + + 获取错误信息。 + + + + + 获取用户自定义数据。 + + + + + 创建加载场景失败事件。 + + 场景资源名称。 + 错误信息。 + 用户自定义数据。 + 创建的加载场景失败事件。 + + + + 清理加载场景失败事件。 + + + + + 加载场景成功事件。 + + + + + 初始化加载场景成功事件的新实例。 + + + + + 获取场景资源名称。 + + + + + 获取加载持续时间。 + + + + + 获取用户自定义数据。 + + + + + 创建加载场景成功事件。 + + 场景资源名称。 + 加载持续时间。 + 用户自定义数据。 + 创建的加载场景成功事件。 + + + + 清理加载场景成功事件。 + + + + + 加载场景更新事件。 + + + + + 初始化加载场景更新事件的新实例。 + + + + + 获取场景资源名称。 + + + + + 获取加载场景进度。 + + + + + 获取用户自定义数据。 + + + + + 创建加载场景更新事件。 + + 场景资源名称。 + 加载场景进度。 + 用户自定义数据。 + 创建的加载场景更新事件。 + + + + 清理加载场景更新事件。 + + + + + 场景管理器。 + + + + + 初始化场景管理器的新实例。 + + + + + 获取游戏框架模块优先级。 + + 优先级较高的模块会优先轮询,并且关闭操作会后进行。 + + + + 加载场景成功事件。 + + + + + 加载场景失败事件。 + + + + + 加载场景更新事件。 + + + + + 加载场景时加载依赖资源事件。 + + + + + 卸载场景成功事件。 + + + + + 卸载场景失败事件。 + + + + + 场景管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理场景管理器。 + + + + + 设置资源管理器。 + + 资源管理器。 + + + + 获取场景是否已加载。 + + 场景资源名称。 + 场景是否已加载。 + + + + 获取已加载场景的资源名称。 + + 已加载场景的资源名称。 + + + + 获取已加载场景的资源名称。 + + 已加载场景的资源名称。 + + + + 获取场景是否正在加载。 + + 场景资源名称。 + 场景是否正在加载。 + + + + 获取正在加载场景的资源名称。 + + 正在加载场景的资源名称。 + + + + 获取正在加载场景的资源名称。 + + 正在加载场景的资源名称。 + + + + 获取场景是否正在卸载。 + + 场景资源名称。 + 场景是否正在卸载。 + + + + 获取正在卸载场景的资源名称。 + + 正在卸载场景的资源名称。 + + + + 获取正在卸载场景的资源名称。 + + 正在卸载场景的资源名称。 + + + + 检查场景资源是否存在。 + + 要检查场景资源的名称。 + 场景资源是否存在。 + + + + 加载场景。 + + 场景资源名称。 + + + + 加载场景。 + + 场景资源名称。 + 加载场景资源的优先级。 + + + + 加载场景。 + + 场景资源名称。 + 用户自定义数据。 + + + + 加载场景。 + + 场景资源名称。 + 加载场景资源的优先级。 + 用户自定义数据。 + + + + 卸载场景。 + + 场景资源名称。 + + + + 卸载场景。 + + 场景资源名称。 + 用户自定义数据。 + + + + 卸载场景失败事件。 + + + + + 初始化卸载场景失败事件的新实例。 + + + + + 获取场景资源名称。 + + + + + 获取用户自定义数据。 + + + + + 创建卸载场景失败事件。 + + 场景资源名称。 + 用户自定义数据。 + 创建的卸载场景失败事件。 + + + + 清理卸载场景失败事件。 + + + + + 卸载场景成功事件。 + + + + + 初始化卸载场景成功事件的新实例。 + + + + + 获取场景资源名称。 + + + + + 获取用户自定义数据。 + + + + + 创建卸载场景成功事件。 + + 场景资源名称。 + 用户自定义数据。 + 创建的卸载场景成功事件。 + + + + 清理卸载场景成功事件。 + + + + + 游戏配置辅助器接口。 + + + + + 获取游戏配置项数量。 + + + + + 加载游戏配置。 + + 是否加载游戏配置成功。 + + + + 保存游戏配置。 + + 是否保存游戏配置成功。 + + + + 获取所有游戏配置项的名称。 + + 所有游戏配置项的名称。 + + + + 获取所有游戏配置项的名称。 + + 所有游戏配置项的名称。 + + + + 检查是否存在指定游戏配置项。 + + 要检查游戏配置项的名称。 + 指定的游戏配置项是否存在。 + + + + 移除指定游戏配置项。 + + 要移除游戏配置项的名称。 + 是否移除指定游戏配置项成功。 + + + + 清空所有游戏配置项。 + + + + + 从指定游戏配置项中读取布尔值。 + + 要获取游戏配置项的名称。 + 读取的布尔值。 + + + + 从指定游戏配置项中读取布尔值。 + + 要获取游戏配置项的名称。 + 当指定的游戏配置项不存在时,返回此默认值。 + 读取的布尔值。 + + + + 向指定游戏配置项写入布尔值。 + + 要写入游戏配置项的名称。 + 要写入的布尔值。 + + + + 从指定游戏配置项中读取整数值。 + + 要获取游戏配置项的名称。 + 读取的整数值。 + + + + 从指定游戏配置项中读取整数值。 + + 要获取游戏配置项的名称。 + 当指定的游戏配置项不存在时,返回此默认值。 + 读取的整数值。 + + + + 向指定游戏配置项写入整数值。 + + 要写入游戏配置项的名称。 + 要写入的整数值。 + + + + 从指定游戏配置项中读取浮点数值。 + + 要获取游戏配置项的名称。 + 读取的浮点数值。 + + + + 从指定游戏配置项中读取浮点数值。 + + 要获取游戏配置项的名称。 + 当指定的游戏配置项不存在时,返回此默认值。 + 读取的浮点数值。 + + + + 向指定游戏配置项写入浮点数值。 + + 要写入游戏配置项的名称。 + 要写入的浮点数值。 + + + + 从指定游戏配置项中读取字符串值。 + + 要获取游戏配置项的名称。 + 读取的字符串值。 + + + + 从指定游戏配置项中读取字符串值。 + + 要获取游戏配置项的名称。 + 当指定的游戏配置项不存在时,返回此默认值。 + 读取的字符串值。 + + + + 向指定游戏配置项写入字符串值。 + + 要写入游戏配置项的名称。 + 要写入的字符串值。 + + + + 从指定游戏配置项中读取对象。 + + 要读取对象的类型。 + 要获取游戏配置项的名称。 + 读取的对象。 + + + + 从指定游戏配置项中读取对象。 + + 要读取对象的类型。 + 要获取游戏配置项的名称。 + 读取的对象。 + + + + 从指定游戏配置项中读取对象。 + + 要读取对象的类型。 + 要获取游戏配置项的名称。 + 当指定的游戏配置项不存在时,返回此默认对象。 + 读取的对象。 + + + + 从指定游戏配置项中读取对象。 + + 要读取对象的类型。 + 要获取游戏配置项的名称。 + 当指定的游戏配置项不存在时,返回此默认对象。 + 读取的对象。 + + + + 向指定游戏配置项写入对象。 + + 要写入对象的类型。 + 要写入游戏配置项的名称。 + 要写入的对象。 + + + + 向指定游戏配置项写入对象。 + + 要写入游戏配置项的名称。 + 要写入的对象。 + + + + 游戏配置管理器接口。 + + + + + 获取游戏配置项数量。 + + + + + 设置游戏配置辅助器。 + + 游戏配置辅助器。 + + + + 加载游戏配置。 + + 是否加载游戏配置成功。 + + + + 保存游戏配置。 + + 是否保存游戏配置成功。 + + + + 获取所有游戏配置项的名称。 + + 所有游戏配置项的名称。 + + + + 获取所有游戏配置项的名称。 + + 所有游戏配置项的名称。 + + + + 检查是否存在指定游戏配置项。 + + 要检查游戏配置项的名称。 + 指定的游戏配置项是否存在。 + + + + 移除指定游戏配置项。 + + 要移除游戏配置项的名称。 + 是否移除指定游戏配置项成功。 + + + + 清空所有游戏配置项。 + + + + + 从指定游戏配置项中读取布尔值。 + + 要获取游戏配置项的名称。 + 读取的布尔值。 + + + + 从指定游戏配置项中读取布尔值。 + + 要获取游戏配置项的名称。 + 当指定的游戏配置项不存在时,返回此默认值。 + 读取的布尔值。 + + + + 向指定游戏配置项写入布尔值。 + + 要写入游戏配置项的名称。 + 要写入的布尔值。 + + + + 从指定游戏配置项中读取整数值。 + + 要获取游戏配置项的名称。 + 读取的整数值。 + + + + 从指定游戏配置项中读取整数值。 + + 要获取游戏配置项的名称。 + 当指定的游戏配置项不存在时,返回此默认值。 + 读取的整数值。 + + + + 向指定游戏配置项写入整数值。 + + 要写入游戏配置项的名称。 + 要写入的整数值。 + + + + 从指定游戏配置项中读取浮点数值。 + + 要获取游戏配置项的名称。 + 读取的浮点数值。 + + + + 从指定游戏配置项中读取浮点数值。 + + 要获取游戏配置项的名称。 + 当指定的游戏配置项不存在时,返回此默认值。 + 读取的浮点数值。 + + + + 向指定游戏配置项写入浮点数值。 + + 要写入游戏配置项的名称。 + 要写入的浮点数值。 + + + + 从指定游戏配置项中读取字符串值。 + + 要获取游戏配置项的名称。 + 读取的字符串值。 + + + + 从指定游戏配置项中读取字符串值。 + + 要获取游戏配置项的名称。 + 当指定的游戏配置项不存在时,返回此默认值。 + 读取的字符串值。 + + + + 向指定游戏配置项写入字符串值。 + + 要写入游戏配置项的名称。 + 要写入的字符串值。 + + + + 从指定游戏配置项中读取对象。 + + 要读取对象的类型。 + 要获取游戏配置项的名称。 + 读取的对象。 + + + + 从指定游戏配置项中读取对象。 + + 要读取对象的类型。 + 要获取游戏配置项的名称。 + 读取的对象。 + + + + 从指定游戏配置项中读取对象。 + + 要读取对象的类型。 + 要获取游戏配置项的名称。 + 当指定的游戏配置项不存在时,返回此默认对象。 + 读取的对象。 + + + + 从指定游戏配置项中读取对象。 + + 要读取对象的类型。 + 要获取游戏配置项的名称。 + 当指定的游戏配置项不存在时,返回此默认对象。 + 读取的对象。 + + + + 向指定游戏配置项写入对象。 + + 要写入对象的类型。 + 要写入游戏配置项的名称。 + 要写入的对象。 + + + + 向指定游戏配置项写入对象。 + + 要写入游戏配置项的名称。 + 要写入的对象。 + + + + 游戏配置管理器。 + + + + + 初始化游戏配置管理器的新实例。 + + + + + 获取游戏配置项数量。 + + + + + 游戏配置管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理游戏配置管理器。 + + + + + 设置游戏配置辅助器。 + + 游戏配置辅助器。 + + + + 加载游戏配置。 + + 是否加载游戏配置成功。 + + + + 保存游戏配置。 + + 是否保存游戏配置成功。 + + + + 获取所有游戏配置项的名称。 + + 所有游戏配置项的名称。 + + + + 获取所有游戏配置项的名称。 + + 所有游戏配置项的名称。 + + + + 检查是否存在指定游戏配置项。 + + 要检查游戏配置项的名称。 + 指定的游戏配置项是否存在。 + + + + 移除指定游戏配置项。 + + 要移除游戏配置项的名称。 + 是否移除指定游戏配置项成功。 + + + + 清空所有游戏配置项。 + + + + + 从指定游戏配置项中读取布尔值。 + + 要获取游戏配置项的名称。 + 读取的布尔值。 + + + + 从指定游戏配置项中读取布尔值。 + + 要获取游戏配置项的名称。 + 当指定的游戏配置项不存在时,返回此默认值。 + 读取的布尔值。 + + + + 向指定游戏配置项写入布尔值。 + + 要写入游戏配置项的名称。 + 要写入的布尔值。 + + + + 从指定游戏配置项中读取整数值。 + + 要获取游戏配置项的名称。 + 读取的整数值。 + + + + 从指定游戏配置项中读取整数值。 + + 要获取游戏配置项的名称。 + 当指定的游戏配置项不存在时,返回此默认值。 + 读取的整数值。 + + + + 向指定游戏配置项写入整数值。 + + 要写入游戏配置项的名称。 + 要写入的整数值。 + + + + 从指定游戏配置项中读取浮点数值。 + + 要获取游戏配置项的名称。 + 读取的浮点数值。 + + + + 从指定游戏配置项中读取浮点数值。 + + 要获取游戏配置项的名称。 + 当指定的游戏配置项不存在时,返回此默认值。 + 读取的浮点数值。 + + + + 向指定游戏配置项写入浮点数值。 + + 要写入游戏配置项的名称。 + 要写入的浮点数值。 + + + + 从指定游戏配置项中读取字符串值。 + + 要获取游戏配置项的名称。 + 读取的字符串值。 + + + + 从指定游戏配置项中读取字符串值。 + + 要获取游戏配置项的名称。 + 当指定的游戏配置项不存在时,返回此默认值。 + 读取的字符串值。 + + + + 向指定游戏配置项写入字符串值。 + + 要写入游戏配置项的名称。 + 要写入的字符串值。 + + + + 从指定游戏配置项中读取对象。 + + 要读取对象的类型。 + 要获取游戏配置项的名称。 + 读取的对象。 + + + + 从指定游戏配置项中读取对象。 + + 要读取对象的类型。 + 要获取游戏配置项的名称。 + 读取的对象。 + + + + 从指定游戏配置项中读取对象。 + + 要读取对象的类型。 + 要获取游戏配置项的名称。 + 当指定的游戏配置项不存在时,返回此默认对象。 + 读取的对象。 + + + + 从指定游戏配置项中读取对象。 + + 要读取对象的类型。 + 要获取游戏配置项的名称。 + 当指定的游戏配置项不存在时,返回此默认对象。 + 读取的对象。 + + + + 向指定游戏配置项写入对象。 + + 要写入对象的类型。 + 要写入游戏配置项的名称。 + 要写入的对象。 + + + + 向指定游戏配置项写入对象。 + + 要写入游戏配置项的名称。 + 要写入的对象。 + + + + 声音相关常量。 + + + + + 声音代理接口。 + + + + + 获取所在的声音组。 + + + + + 获取声音的序列编号。 + + + + + 获取当前是否正在播放。 + + + + + 获取声音长度。 + + + + + 获取或设置播放位置。 + + + + + 获取或设置是否静音。 + + + + + 获取或设置在声音组内是否静音。 + + + + + 获取或设置是否循环播放。 + + + + + 获取或设置声音优先级。 + + + + + 获取音量大小。 + + + + + 获取或设置在声音组内音量大小。 + + + + + 获取或设置声音音调。 + + + + + 获取或设置声音立体声声相。 + + + + + 获取或设置声音空间混合量。 + + + + + 获取或设置声音最大距离。 + + + + + 获取或设置声音多普勒等级。 + + + + + 获取声音代理辅助器。 + + + + + 播放声音。 + + + + + 播放声音。 + + 声音淡入时间,以秒为单位。 + + + + 停止播放声音。 + + + + + 停止播放声音。 + + 声音淡出时间,以秒为单位。 + + + + 暂停播放声音。 + + + + + 暂停播放声音。 + + 声音淡出时间,以秒为单位。 + + + + 恢复播放声音。 + + + + + 恢复播放声音。 + + 声音淡入时间,以秒为单位。 + + + + 重置声音代理。 + + + + + 声音代理辅助器接口。 + + + + + 获取当前是否正在播放。 + + + + + 获取声音长度。 + + + + + 获取或设置播放位置。 + + + + + 获取或设置是否静音。 + + + + + 获取或设置是否循环播放。 + + + + + 获取或设置声音优先级。 + + + + + 获取或设置音量大小。 + + + + + 获取或设置声音音调。 + + + + + 获取或设置声音立体声声相。 + + + + + 获取或设置声音空间混合量。 + + + + + 获取或设置声音最大距离。 + + + + + 获取或设置声音多普勒等级。 + + + + + 重置声音代理事件。 + + + + + 播放声音。 + + 声音淡入时间,以秒为单位。 + + + + 停止播放声音。 + + 声音淡出时间,以秒为单位。 + + + + 暂停播放声音。 + + 声音淡出时间,以秒为单位。 + + + + 恢复播放声音。 + + 声音淡入时间,以秒为单位。 + + + + 重置声音代理辅助器。 + + + + + 设置声音资源。 + + 声音资源。 + 是否设置声音资源成功。 + + + + 声音组接口。 + + + + + 获取声音组名称。 + + + + + 获取声音代理数。 + + + + + 获取或设置声音组中的声音是否避免被同优先级声音替换。 + + + + + 获取或设置声音组静音。 + + + + + 获取或设置声音组音量。 + + + + + 获取声音组辅助器。 + + + + + 停止所有已加载的声音。 + + + + + 停止所有已加载的声音。 + + 声音淡出时间,以秒为单位。 + + + + 声音组辅助器接口。 + + + + + 声音辅助器接口。 + + + + + 释放声音资源。 + + 要释放的声音资源。 + + + + 声音管理器接口。 + + + + + 获取声音组数量。 + + + + + 播放声音成功事件。 + + + + + 播放声音失败事件。 + + + + + 播放声音更新事件。 + + + + + 播放声音时加载依赖资源事件。 + + + + + 设置资源管理器。 + + 资源管理器。 + + + + 设置声音辅助器。 + + 声音辅助器。 + + + + 是否存在指定声音组。 + + 声音组名称。 + 指定声音组是否存在。 + + + + 获取指定声音组。 + + 声音组名称。 + 要获取的声音组。 + + + + 获取所有声音组。 + + 所有声音组。 + + + + 获取所有声音组。 + + 所有声音组。 + + + + 增加声音组。 + + 声音组名称。 + 声音组辅助器。 + 是否增加声音组成功。 + + + + 增加声音组。 + + 声音组名称。 + 声音组中的声音是否避免被同优先级声音替换。 + 声音组是否静音。 + 声音组音量。 + 声音组辅助器。 + 是否增加声音组成功。 + + + + 增加声音代理辅助器。 + + 声音组名称。 + 要增加的声音代理辅助器。 + + + + 获取所有正在加载声音的序列编号。 + + 所有正在加载声音的序列编号。 + + + + 获取所有正在加载声音的序列编号。 + + 所有正在加载声音的序列编号。 + + + + 是否正在加载声音。 + + 声音序列编号。 + 是否正在加载声音。 + + + + 播放声音。 + + 声音资源名称。 + 声音组名称。 + 声音的序列编号。 + + + + 播放声音。 + + 声音资源名称。 + 声音组名称。 + 加载声音资源的优先级。 + 声音的序列编号。 + + + + 播放声音。 + + 声音资源名称。 + 声音组名称。 + 播放声音参数。 + 声音的序列编号。 + + + + 播放声音。 + + 声音资源名称。 + 声音组名称。 + 用户自定义数据。 + 声音的序列编号。 + + + + 播放声音。 + + 声音资源名称。 + 声音组名称。 + 加载声音资源的优先级。 + 播放声音参数。 + 声音的序列编号。 + + + + 播放声音。 + + 声音资源名称。 + 声音组名称。 + 加载声音资源的优先级。 + 用户自定义数据。 + 声音的序列编号。 + + + + 播放声音。 + + 声音资源名称。 + 声音组名称。 + 播放声音参数。 + 用户自定义数据。 + 声音的序列编号。 + + + + 播放声音。 + + 声音资源名称。 + 声音组名称。 + 加载声音资源的优先级。 + 播放声音参数。 + 用户自定义数据。 + 声音的序列编号。 + + + + 停止播放声音。 + + 要停止播放声音的序列编号。 + 是否停止播放声音成功。 + + + + 停止播放声音。 + + 要停止播放声音的序列编号。 + 声音淡出时间,以秒为单位。 + 是否停止播放声音成功。 + + + + 停止所有已加载的声音。 + + + + + 停止所有已加载的声音。 + + 声音淡出时间,以秒为单位。 + + + + 停止所有正在加载的声音。 + + + + + 暂停播放声音。 + + 要暂停播放声音的序列编号。 + + + + 暂停播放声音。 + + 要暂停播放声音的序列编号。 + 声音淡出时间,以秒为单位。 + + + + 恢复播放声音。 + + 要恢复播放声音的序列编号。 + + + + 恢复播放声音。 + + 要恢复播放声音的序列编号。 + 声音淡入时间,以秒为单位。 + + + + 播放声音时加载依赖资源事件。 + + + + + 初始化播放声音时加载依赖资源事件的新实例。 + + + + + 获取声音的序列编号。 + + + + + 获取声音资源名称。 + + + + + 获取声音组名称。 + + + + + 获取播放声音参数。 + + + + + 获取被加载的依赖资源名称。 + + + + + 获取当前已加载依赖资源数量。 + + + + + 获取总共加载依赖资源数量。 + + + + + 获取用户自定义数据。 + + + + + 创建播放声音时加载依赖资源事件。 + + 声音的序列编号。 + 声音资源名称。 + 声音组名称。 + 播放声音参数。 + 被加载的依赖资源名称。 + 当前已加载依赖资源数量。 + 总共加载依赖资源数量。 + 用户自定义数据。 + 创建的播放声音时加载依赖资源事件。 + + + + 清理播放声音时加载依赖资源事件。 + + + + + 播放声音错误码。 + + + + + 未知错误。 + + + + + 声音组不存在。 + + + + + 声音组没有声音代理。 + + + + + 加载资源失败。 + + + + + 播放声音因优先级低被忽略。 + + + + + 设置声音资源失败。 + + + + + 播放声音失败事件。 + + + + + 初始化播放声音失败事件的新实例。 + + + + + 获取声音的序列编号。 + + + + + 获取声音资源名称。 + + + + + 获取声音组名称。 + + + + + 获取播放声音参数。 + + + + + 获取错误码。 + + + + + 获取错误信息。 + + + + + 获取用户自定义数据。 + + + + + 创建播放声音失败事件。 + + 声音的序列编号。 + 声音资源名称。 + 声音组名称。 + 播放声音参数。 + 错误码。 + 错误信息。 + 用户自定义数据。 + 创建的播放声音失败事件。 + + + + 清理播放声音失败事件。 + + + + + 播放声音参数。 + + + + + 初始化播放声音参数的新实例。 + + + + + 获取或设置播放位置。 + + + + + 获取或设置在声音组内是否静音。 + + + + + 获取或设置是否循环播放。 + + + + + 获取或设置声音优先级。 + + + + + 获取或设置在声音组内音量大小。 + + + + + 获取或设置声音淡入时间,以秒为单位。 + + + + + 获取或设置声音音调。 + + + + + 获取或设置声音立体声声相。 + + + + + 获取或设置声音空间混合量。 + + + + + 获取或设置声音最大距离。 + + + + + 获取或设置声音多普勒等级。 + + + + + 创建播放声音参数。 + + 创建的播放声音参数。 + + + + 清理播放声音参数。 + + + + + 播放声音成功事件。 + + + + + 初始化播放声音成功事件的新实例。 + + + + + 获取声音的序列编号。 + + + + + 获取声音资源名称。 + + + + + 获取用于播放的声音代理。 + + + + + 获取加载持续时间。 + + + + + 获取用户自定义数据。 + + + + + 创建播放声音成功事件。 + + 声音的序列编号。 + 声音资源名称。 + 用于播放的声音代理。 + 加载持续时间。 + 用户自定义数据。 + 创建的播放声音成功事件。 + + + + 清理播放声音成功事件。 + + + + + 播放声音更新事件。 + + + + + 初始化播放声音更新事件的新实例。 + + + + + 获取声音的序列编号。 + + + + + 获取声音资源名称。 + + + + + 获取声音组名称。 + + + + + 获取播放声音参数。 + + + + + 获取加载声音进度。 + + + + + 获取用户自定义数据。 + + + + + 创建播放声音更新事件。 + + 声音的序列编号。 + 声音资源名称。 + 声音组名称。 + 播放声音参数。 + 加载声音进度。 + 用户自定义数据。 + 创建的播放声音更新事件。 + + + + 清理播放声音更新事件。 + + + + + 重置声音代理事件。 + + + + + 初始化重置声音代理事件的新实例。 + + + + + 创建重置声音代理事件。 + + 创建的重置声音代理事件。 + + + + 清理重置声音代理事件。 + + + + + 声音管理器。 + + + + + 初始化声音管理器的新实例。 + + + + + 获取声音组数量。 + + + + + 播放声音成功事件。 + + + + + 播放声音失败事件。 + + + + + 播放声音更新事件。 + + + + + 播放声音时加载依赖资源事件。 + + + + + 声音管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理声音管理器。 + + + + + 设置资源管理器。 + + 资源管理器。 + + + + 设置声音辅助器。 + + 声音辅助器。 + + + + 是否存在指定声音组。 + + 声音组名称。 + 指定声音组是否存在。 + + + + 获取指定声音组。 + + 声音组名称。 + 要获取的声音组。 + + + + 获取所有声音组。 + + 所有声音组。 + + + + 获取所有声音组。 + + 所有声音组。 + + + + 增加声音组。 + + 声音组名称。 + 声音组辅助器。 + 是否增加声音组成功。 + + + + 增加声音组。 + + 声音组名称。 + 声音组中的声音是否避免被同优先级声音替换。 + 声音组是否静音。 + 声音组音量。 + 声音组辅助器。 + 是否增加声音组成功。 + + + + 增加声音代理辅助器。 + + 声音组名称。 + 要增加的声音代理辅助器。 + + + + 获取所有正在加载声音的序列编号。 + + 所有正在加载声音的序列编号。 + + + + 获取所有正在加载声音的序列编号。 + + 所有正在加载声音的序列编号。 + + + + 是否正在加载声音。 + + 声音序列编号。 + 是否正在加载声音。 + + + + 播放声音。 + + 声音资源名称。 + 声音组名称。 + 声音的序列编号。 + + + + 播放声音。 + + 声音资源名称。 + 声音组名称。 + 加载声音资源的优先级。 + 声音的序列编号。 + + + + 播放声音。 + + 声音资源名称。 + 声音组名称。 + 播放声音参数。 + 声音的序列编号。 + + + + 播放声音。 + + 声音资源名称。 + 声音组名称。 + 用户自定义数据。 + 声音的序列编号。 + + + + 播放声音。 + + 声音资源名称。 + 声音组名称。 + 加载声音资源的优先级。 + 播放声音参数。 + 声音的序列编号。 + + + + 播放声音。 + + 声音资源名称。 + 声音组名称。 + 加载声音资源的优先级。 + 用户自定义数据。 + 声音的序列编号。 + + + + 播放声音。 + + 声音资源名称。 + 声音组名称。 + 播放声音参数。 + 用户自定义数据。 + 声音的序列编号。 + + + + 播放声音。 + + 声音资源名称。 + 声音组名称。 + 加载声音资源的优先级。 + 播放声音参数。 + 用户自定义数据。 + 声音的序列编号。 + + + + 停止播放声音。 + + 要停止播放声音的序列编号。 + 是否停止播放声音成功。 + + + + 停止播放声音。 + + 要停止播放声音的序列编号。 + 声音淡出时间,以秒为单位。 + 是否停止播放声音成功。 + + + + 停止所有已加载的声音。 + + + + + 停止所有已加载的声音。 + + 声音淡出时间,以秒为单位。 + + + + 停止所有正在加载的声音。 + + + + + 暂停播放声音。 + + 要暂停播放声音的序列编号。 + + + + 暂停播放声音。 + + 要暂停播放声音的序列编号。 + 声音淡出时间,以秒为单位。 + + + + 恢复播放声音。 + + 要恢复播放声音的序列编号。 + + + + 恢复播放声音。 + + 要恢复播放声音的序列编号。 + 声音淡入时间,以秒为单位。 + + + + 声音代理。 + + + + + 初始化声音代理的新实例。 + + 所在的声音组。 + 声音辅助器接口。 + 声音代理辅助器接口。 + + + + 获取所在的声音组。 + + + + + 获取或设置声音的序列编号。 + + + + + 获取当前是否正在播放。 + + + + + 获取声音长度。 + + + + + 获取或设置播放位置。 + + + + + 获取是否静音。 + + + + + 获取或设置在声音组内是否静音。 + + + + + 获取或设置是否循环播放。 + + + + + 获取或设置声音优先级。 + + + + + 获取音量大小。 + + + + + 获取或设置在声音组内音量大小。 + + + + + 获取或设置声音音调。 + + + + + 获取或设置声音立体声声相。 + + + + + 获取或设置声音空间混合量。 + + + + + 获取或设置声音最大距离。 + + + + + 获取或设置声音多普勒等级。 + + + + + 获取声音代理辅助器。 + + + + + 获取声音创建时间。 + + + + + 播放声音。 + + + + + 播放声音。 + + 声音淡入时间,以秒为单位。 + + + + 停止播放声音。 + + + + + 停止播放声音。 + + 声音淡出时间,以秒为单位。 + + + + 暂停播放声音。 + + + + + 暂停播放声音。 + + 声音淡出时间,以秒为单位。 + + + + 恢复播放声音。 + + + + + 恢复播放声音。 + + 声音淡入时间,以秒为单位。 + + + + 重置声音代理。 + + + + + 声音组。 + + + + + 初始化声音组的新实例。 + + 声音组名称。 + 声音组辅助器。 + + + + 获取声音组名称。 + + + + + 获取声音代理数。 + + + + + 获取或设置声音组中的声音是否避免被同优先级声音替换。 + + + + + 获取或设置声音组静音。 + + + + + 获取或设置声音组音量。 + + + + + 获取声音组辅助器。 + + + + + 增加声音代理辅助器。 + + 声音辅助器接口。 + 要增加的声音代理辅助器。 + + + + 播放声音。 + + 声音的序列编号。 + 声音资源。 + 播放声音参数。 + 错误码。 + 用于播放的声音代理。 + + + + 停止播放声音。 + + 要停止播放声音的序列编号。 + 声音淡出时间,以秒为单位。 + 是否停止播放声音成功。 + + + + 暂停播放声音。 + + 要暂停播放声音的序列编号。 + 声音淡出时间,以秒为单位。 + 是否暂停播放声音成功。 + + + + 恢复播放声音。 + + 要恢复播放声音的序列编号。 + 声音淡入时间,以秒为单位。 + 是否恢复播放声音成功。 + + + + 停止所有已加载的声音。 + + + + + 停止所有已加载的声音。 + + 声音淡出时间,以秒为单位。 + + + + 关闭界面完成事件。 + + + + + 初始化关闭界面完成事件的新实例。 + + + + + 获取界面序列编号。 + + + + + 获取界面资源名称。 + + + + + 获取界面所属的界面组。 + + + + + 获取用户自定义数据。 + + + + + 创建关闭界面完成事件。 + + 界面序列编号。 + 界面资源名称。 + 界面所属的界面组。 + 用户自定义数据。 + 创建的关闭界面完成事件。 + + + + 清理关闭界面完成事件。 + + + + + 界面接口。 + + + + + 获取界面序列编号。 + + + + + 获取界面资源名称。 + + + + + 获取界面实例。 + + + + + 获取界面所属的界面组。 + + + + + 获取界面在界面组中的深度。 + + + + + 获取是否暂停被覆盖的界面。 + + + + + 初始化界面。 + + 界面序列编号。 + 界面资源名称。 + 界面所属的界面组。 + 是否暂停被覆盖的界面。 + 是否是新实例。 + 用户自定义数据。 + + + + 界面回收。 + + + + + 界面打开。 + + 用户自定义数据。 + + + + 界面关闭。 + + 是否是关闭界面管理器时触发。 + 用户自定义数据。 + + + + 界面暂停。 + + + + + 界面暂停恢复。 + + + + + 界面遮挡。 + + + + + 界面遮挡恢复。 + + + + + 界面激活。 + + 用户自定义数据。 + + + + 界面轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 界面深度改变。 + + 界面组深度。 + 界面在界面组中的深度。 + + + + 界面辅助器接口。 + + + + + 实例化界面。 + + 要实例化的界面资源。 + 实例化后的界面。 + + + + 创建界面。 + + 界面实例。 + 界面所属的界面组。 + 用户自定义数据。 + 界面。 + + + + 释放界面。 + + 要释放的界面资源。 + 要释放的界面实例。 + + + + 界面组接口。 + + + + + 获取界面组名称。 + + + + + 获取或设置界面组深度。 + + + + + 获取或设置界面组是否暂停。 + + + + + 获取界面组中界面数量。 + + + + + 获取当前界面。 + + + + + 获取界面组辅助器。 + + + + + 界面组中是否存在界面。 + + 界面序列编号。 + 界面组中是否存在界面。 + + + + 界面组中是否存在界面。 + + 界面资源名称。 + 界面组中是否存在界面。 + + + + 从界面组中获取界面。 + + 界面序列编号。 + 要获取的界面。 + + + + 从界面组中获取界面。 + + 界面资源名称。 + 要获取的界面。 + + + + 从界面组中获取界面。 + + 界面资源名称。 + 要获取的界面。 + + + + 从界面组中获取界面。 + + 界面资源名称。 + 要获取的界面。 + + + + 从界面组中获取所有界面。 + + 界面组中的所有界面。 + + + + 从界面组中获取所有界面。 + + 界面组中的所有界面。 + + + + 界面组辅助器接口。 + + + + + 设置界面组深度。 + + 界面组深度。 + + + + 界面管理器接口。 + + + + + 获取界面组数量。 + + + + + 获取或设置界面实例对象池自动释放可释放对象的间隔秒数。 + + + + + 获取或设置界面实例对象池的容量。 + + + + + 获取或设置界面实例对象池对象过期秒数。 + + + + + 获取或设置界面实例对象池的优先级。 + + + + + 打开界面成功事件。 + + + + + 打开界面失败事件。 + + + + + 打开界面更新事件。 + + + + + 打开界面时加载依赖资源事件。 + + + + + 关闭界面完成事件。 + + + + + 设置对象池管理器。 + + 对象池管理器。 + + + + 设置资源管理器。 + + 资源管理器。 + + + + 设置界面辅助器。 + + 界面辅助器。 + + + + 是否存在界面组。 + + 界面组名称。 + 是否存在界面组。 + + + + 获取界面组。 + + 界面组名称。 + 要获取的界面组。 + + + + 获取所有界面组。 + + 所有界面组。 + + + + 获取所有界面组。 + + 所有界面组。 + + + + 增加界面组。 + + 界面组名称。 + 界面组辅助器。 + 是否增加界面组成功。 + + + + 增加界面组。 + + 界面组名称。 + 界面组深度。 + 界面组辅助器。 + 是否增加界面组成功。 + + + + 是否存在界面。 + + 界面序列编号。 + 是否存在界面。 + + + + 是否存在界面。 + + 界面资源名称。 + 是否存在界面。 + + + + 获取界面。 + + 界面序列编号。 + 要获取的界面。 + + + + 获取界面。 + + 界面资源名称。 + 要获取的界面。 + + + + 获取界面。 + + 界面资源名称。 + 要获取的界面。 + + + + 获取界面。 + + 界面资源名称。 + 要获取的界面。 + + + + 获取所有已加载的界面。 + + 所有已加载的界面。 + + + + 获取所有已加载的界面。 + + 所有已加载的界面。 + + + + 获取所有正在加载界面的序列编号。 + + 所有正在加载界面的序列编号。 + + + + 获取所有正在加载界面的序列编号。 + + 所有正在加载界面的序列编号。 + + + + 是否正在加载界面。 + + 界面序列编号。 + 是否正在加载界面。 + + + + 是否正在加载界面。 + + 界面资源名称。 + 是否正在加载界面。 + + + + 是否是合法的界面。 + + 界面。 + 界面是否合法。 + + + + 打开界面。 + + 界面资源名称。 + 界面组名称。 + 界面的序列编号。 + + + + 打开界面。 + + 界面资源名称。 + 界面组名称。 + 加载界面资源的优先级。 + 界面的序列编号。 + + + + 打开界面。 + + 界面资源名称。 + 界面组名称。 + 是否暂停被覆盖的界面。 + 界面的序列编号。 + + + + 打开界面。 + + 界面资源名称。 + 界面组名称。 + 用户自定义数据。 + 界面的序列编号。 + + + + 打开界面。 + + 界面资源名称。 + 界面组名称。 + 加载界面资源的优先级。 + 是否暂停被覆盖的界面。 + 界面的序列编号。 + + + + 打开界面。 + + 界面资源名称。 + 界面组名称。 + 加载界面资源的优先级。 + 用户自定义数据。 + 界面的序列编号。 + + + + 打开界面。 + + 界面资源名称。 + 界面组名称。 + 是否暂停被覆盖的界面。 + 用户自定义数据。 + 界面的序列编号。 + + + + 打开界面。 + + 界面资源名称。 + 界面组名称。 + 加载界面资源的优先级。 + 是否暂停被覆盖的界面。 + 用户自定义数据。 + 界面的序列编号。 + + + + 关闭界面。 + + 要关闭界面的序列编号。 + + + + 关闭界面。 + + 要关闭界面的序列编号。 + 用户自定义数据。 + + + + 关闭界面。 + + 要关闭的界面。 + + + + 关闭界面。 + + 要关闭的界面。 + 用户自定义数据。 + + + + 关闭所有已加载的界面。 + + + + + 关闭所有已加载的界面。 + + 用户自定义数据。 + + + + 关闭所有正在加载的界面。 + + + + + 激活界面。 + + 要激活的界面。 + + + + 激活界面。 + + 要激活的界面。 + 用户自定义数据。 + + + + 设置界面实例是否被加锁。 + + 要设置是否被加锁的界面实例。 + 界面实例是否被加锁。 + + + + 设置界面实例的优先级。 + + 要设置优先级的界面实例。 + 界面实例优先级。 + + + + 打开界面时加载依赖资源事件。 + + + + + 初始化打开界面时加载依赖资源事件的新实例。 + + + + + 获取界面序列编号。 + + + + + 获取界面资源名称。 + + + + + 获取界面组名称。 + + + + + 获取是否暂停被覆盖的界面。 + + + + + 获取被加载的依赖资源名称。 + + + + + 获取当前已加载依赖资源数量。 + + + + + 获取总共加载依赖资源数量。 + + + + + 获取用户自定义数据。 + + + + + 创建打开界面时加载依赖资源事件。 + + 界面序列编号。 + 界面资源名称。 + 界面组名称。 + 是否暂停被覆盖的界面。 + 被加载的依赖资源名称。 + 当前已加载依赖资源数量。 + 总共加载依赖资源数量。 + 用户自定义数据。 + 创建的打开界面时加载依赖资源事件。 + + + + 清理打开界面时加载依赖资源事件。 + + + + + 打开界面失败事件。 + + + + + 初始化打开界面失败事件的新实例。 + + + + + 获取界面序列编号。 + + + + + 获取界面资源名称。 + + + + + 获取界面组名称。 + + + + + 获取是否暂停被覆盖的界面。 + + + + + 获取错误信息。 + + + + + 获取用户自定义数据。 + + + + + 创建打开界面失败事件。 + + 界面序列编号。 + 界面资源名称。 + 界面组名称。 + 是否暂停被覆盖的界面。 + 错误信息。 + 用户自定义数据。 + 创建的打开界面失败事件。 + + + + 清理打开界面失败事件。 + + + + + 打开界面成功事件。 + + + + + 初始化打开界面成功事件的新实例。 + + + + + 获取打开成功的界面。 + + + + + 获取加载持续时间。 + + + + + 获取用户自定义数据。 + + + + + 创建打开界面成功事件。 + + 加载成功的界面。 + 加载持续时间。 + 用户自定义数据。 + 创建的打开界面成功事件。 + + + + 清理打开界面成功事件。 + + + + + 打开界面更新事件。 + + + + + 初始化打开界面更新事件的新实例。 + + + + + 获取界面序列编号。 + + + + + 获取界面资源名称。 + + + + + 获取界面组名称。 + + + + + 获取是否暂停被覆盖的界面。 + + + + + 获取打开界面进度。 + + + + + 获取用户自定义数据。 + + + + + 创建打开界面更新事件。 + + 界面序列编号。 + 界面资源名称。 + 界面组名称。 + 是否暂停被覆盖的界面。 + 打开界面进度。 + 用户自定义数据。 + 创建的打开界面更新事件。 + + + + 清理打开界面更新事件。 + + + + + 界面管理器。 + + + + + 初始化界面管理器的新实例。 + + + + + 获取界面组数量。 + + + + + 获取或设置界面实例对象池自动释放可释放对象的间隔秒数。 + + + + + 获取或设置界面实例对象池的容量。 + + + + + 获取或设置界面实例对象池对象过期秒数。 + + + + + 获取或设置界面实例对象池的优先级。 + + + + + 打开界面成功事件。 + + + + + 打开界面失败事件。 + + + + + 打开界面更新事件。 + + + + + 打开界面时加载依赖资源事件。 + + + + + 关闭界面完成事件。 + + + + + 界面管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理界面管理器。 + + + + + 设置对象池管理器。 + + 对象池管理器。 + + + + 设置资源管理器。 + + 资源管理器。 + + + + 设置界面辅助器。 + + 界面辅助器。 + + + + 是否存在界面组。 + + 界面组名称。 + 是否存在界面组。 + + + + 获取界面组。 + + 界面组名称。 + 要获取的界面组。 + + + + 获取所有界面组。 + + 所有界面组。 + + + + 获取所有界面组。 + + 所有界面组。 + + + + 增加界面组。 + + 界面组名称。 + 界面组辅助器。 + 是否增加界面组成功。 + + + + 增加界面组。 + + 界面组名称。 + 界面组深度。 + 界面组辅助器。 + 是否增加界面组成功。 + + + + 是否存在界面。 + + 界面序列编号。 + 是否存在界面。 + + + + 是否存在界面。 + + 界面资源名称。 + 是否存在界面。 + + + + 获取界面。 + + 界面序列编号。 + 要获取的界面。 + + + + 获取界面。 + + 界面资源名称。 + 要获取的界面。 + + + + 获取界面。 + + 界面资源名称。 + 要获取的界面。 + + + + 获取界面。 + + 界面资源名称。 + 要获取的界面。 + + + + 获取所有已加载的界面。 + + 所有已加载的界面。 + + + + 获取所有已加载的界面。 + + 所有已加载的界面。 + + + + 获取所有正在加载界面的序列编号。 + + 所有正在加载界面的序列编号。 + + + + 获取所有正在加载界面的序列编号。 + + 所有正在加载界面的序列编号。 + + + + 是否正在加载界面。 + + 界面序列编号。 + 是否正在加载界面。 + + + + 是否正在加载界面。 + + 界面资源名称。 + 是否正在加载界面。 + + + + 是否是合法的界面。 + + 界面。 + 界面是否合法。 + + + + 打开界面。 + + 界面资源名称。 + 界面组名称。 + 界面的序列编号。 + + + + 打开界面。 + + 界面资源名称。 + 界面组名称。 + 加载界面资源的优先级。 + 界面的序列编号。 + + + + 打开界面。 + + 界面资源名称。 + 界面组名称。 + 是否暂停被覆盖的界面。 + 界面的序列编号。 + + + + 打开界面。 + + 界面资源名称。 + 界面组名称。 + 用户自定义数据。 + 界面的序列编号。 + + + + 打开界面。 + + 界面资源名称。 + 界面组名称。 + 加载界面资源的优先级。 + 是否暂停被覆盖的界面。 + 界面的序列编号。 + + + + 打开界面。 + + 界面资源名称。 + 界面组名称。 + 加载界面资源的优先级。 + 用户自定义数据。 + 界面的序列编号。 + + + + 打开界面。 + + 界面资源名称。 + 界面组名称。 + 是否暂停被覆盖的界面。 + 用户自定义数据。 + 界面的序列编号。 + + + + 打开界面。 + + 界面资源名称。 + 界面组名称。 + 加载界面资源的优先级。 + 是否暂停被覆盖的界面。 + 用户自定义数据。 + 界面的序列编号。 + + + + 关闭界面。 + + 要关闭界面的序列编号。 + + + + 关闭界面。 + + 要关闭界面的序列编号。 + 用户自定义数据。 + + + + 关闭界面。 + + 要关闭的界面。 + + + + 关闭界面。 + + 要关闭的界面。 + 用户自定义数据。 + + + + 关闭所有已加载的界面。 + + + + + 关闭所有已加载的界面。 + + 用户自定义数据。 + + + + 关闭所有正在加载的界面。 + + + + + 激活界面。 + + 要激活的界面。 + + + + 激活界面。 + + 要激活的界面。 + 用户自定义数据。 + + + + 设置界面实例是否被加锁。 + + 要设置是否被加锁的界面实例。 + 界面实例是否被加锁。 + + + + 设置界面实例的优先级。 + + 要设置优先级的界面实例。 + 界面实例优先级。 + + + + 界面实例对象。 + + + + + 界面组。 + + + + + 初始化界面组的新实例。 + + 界面组名称。 + 界面组深度。 + 界面组辅助器。 + + + + 获取界面组名称。 + + + + + 获取或设置界面组深度。 + + + + + 获取或设置界面组是否暂停。 + + + + + 获取界面组中界面数量。 + + + + + 获取当前界面。 + + + + + 获取界面组辅助器。 + + + + + 界面组轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 界面组中是否存在界面。 + + 界面序列编号。 + 界面组中是否存在界面。 + + + + 界面组中是否存在界面。 + + 界面资源名称。 + 界面组中是否存在界面。 + + + + 从界面组中获取界面。 + + 界面序列编号。 + 要获取的界面。 + + + + 从界面组中获取界面。 + + 界面资源名称。 + 要获取的界面。 + + + + 从界面组中获取界面。 + + 界面资源名称。 + 要获取的界面。 + + + + 从界面组中获取界面。 + + 界面资源名称。 + 要获取的界面。 + + + + 从界面组中获取所有界面。 + + 界面组中的所有界面。 + + + + 从界面组中获取所有界面。 + + 界面组中的所有界面。 + + + + 往界面组增加界面。 + + 要增加的界面。 + + + + 从界面组移除界面。 + + 要移除的界面。 + + + + 激活界面。 + + 要激活的界面。 + 用户自定义数据。 + + + + 刷新界面组。 + + + + + 界面组界面信息。 + + + + + 实用函数集。 + + + + + 程序集相关的实用函数。 + + + + + 获取已加载的程序集。 + + 已加载的程序集。 + + + + 获取已加载的程序集中的所有类型。 + + 已加载的程序集中的所有类型。 + + + + 获取已加载的程序集中的所有类型。 + + 已加载的程序集中的所有类型。 + + + + 获取已加载的程序集中的指定类型。 + + 要获取的类型名。 + 已加载的程序集中的指定类型。 + + + + 压缩解压缩相关的实用函数。 + + + + + 设置压缩解压缩辅助器。 + + 要设置的压缩解压缩辅助器。 + + + + 压缩数据。 + + 要压缩的数据的二进制流。 + 压缩后的数据的二进制流。 + + + + 压缩数据。 + + 要压缩的数据的二进制流。 + 压缩后的数据的二进制流。 + 是否压缩数据成功。 + + + + 压缩数据。 + + 要压缩的数据的二进制流。 + 要压缩的数据的二进制流的偏移。 + 要压缩的数据的二进制流的长度。 + 压缩后的数据的二进制流。 + + + + 压缩数据。 + + 要压缩的数据的二进制流。 + 要压缩的数据的二进制流的偏移。 + 要压缩的数据的二进制流的长度。 + 压缩后的数据的二进制流。 + 是否压缩数据成功。 + + + + 压缩数据。 + + 要压缩的数据的二进制流。 + 压缩后的数据的二进制流。 + + + + 压缩数据。 + + 要压缩的数据的二进制流。 + 压缩后的数据的二进制流。 + 是否压缩数据成功。 + + + + 解压缩数据。 + + 要解压缩的数据的二进制流。 + 解压缩后的数据的二进制流。 + + + + 解压缩数据。 + + 要解压缩的数据的二进制流。 + 解压缩后的数据的二进制流。 + 是否解压缩数据成功。 + + + + 解压缩数据。 + + 要解压缩的数据的二进制流。 + 要解压缩的数据的二进制流的偏移。 + 要解压缩的数据的二进制流的长度。 + 解压缩后的数据的二进制流。 + + + + 解压缩数据。 + + 要解压缩的数据的二进制流。 + 要解压缩的数据的二进制流的偏移。 + 要解压缩的数据的二进制流的长度。 + 解压缩后的数据的二进制流。 + 是否解压缩数据成功。 + + + + 解压缩数据。 + + 要解压缩的数据的二进制流。 + 是否解压缩数据成功。 + + + + 解压缩数据。 + + 要解压缩的数据的二进制流。 + 解压缩后的数据的二进制流。 + 是否解压缩数据成功。 + + + + 压缩解压缩辅助器接口。 + + + + + 压缩数据。 + + 要压缩的数据的二进制流。 + 要压缩的数据的二进制流的偏移。 + 要压缩的数据的二进制流的长度。 + 压缩后的数据的二进制流。 + 是否压缩数据成功。 + + + + 压缩数据。 + + 要压缩的数据的二进制流。 + 压缩后的数据的二进制流。 + 是否压缩数据成功。 + + + + 解压缩数据。 + + 要解压缩的数据的二进制流。 + 要解压缩的数据的二进制流的偏移。 + 要解压缩的数据的二进制流的长度。 + 解压缩后的数据的二进制流。 + 是否解压缩数据成功。 + + + + 解压缩数据。 + + 要解压缩的数据的二进制流。 + 解压缩后的数据的二进制流。 + 是否解压缩数据成功。 + + + + 类型转换相关的实用函数。 + + + + + 获取数据在此计算机结构中存储时的字节顺序。 + + + + + 获取或设置屏幕每英寸点数。 + + + + + 将像素转换为厘米。 + + 像素。 + 厘米。 + + + + 将厘米转换为像素。 + + 厘米。 + 像素。 + + + + 将像素转换为英寸。 + + 像素。 + 英寸。 + + + + 将英寸转换为像素。 + + 英寸。 + 像素。 + + + + 以字节数组的形式获取指定的布尔值。 + + 要转换的布尔值。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的布尔值。 + + 要转换的布尔值。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的布尔值。 + + 要转换的布尔值。 + 用于存放结果的字节数组。 + buffer 内的起始位置。 + + + + 返回由字节数组中首字节转换来的布尔值。 + + 字节数组。 + 如果 value 中的首字节非零,则为 true,否则为 false。 + + + + 返回由字节数组中指定位置的一个字节转换来的布尔值。 + + 字节数组。 + value 内的起始位置。 + 如果 value 中指定位置的字节非零,则为 true,否则为 false。 + + + + 以字节数组的形式获取指定的 Unicode 字符值。 + + 要转换的字符。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的 Unicode 字符值。 + + 要转换的字符。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的 Unicode 字符值。 + + 要转换的字符。 + 用于存放结果的字节数组。 + buffer 内的起始位置。 + + + + 返回由字节数组中前两个字节转换来的 Unicode 字符。 + + 字节数组。 + 由两个字节构成的字符。 + + + + 返回由字节数组中指定位置的两个字节转换来的 Unicode 字符。 + + 字节数组。 + value 内的起始位置。 + 由两个字节构成的字符。 + + + + 以字节数组的形式获取指定的 16 位有符号整数值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的 16 位有符号整数值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的 16 位有符号整数值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + buffer 内的起始位置。 + + + + 返回由字节数组中前两个字节转换来的 16 位有符号整数。 + + 字节数组。 + 由两个字节构成的 16 位有符号整数。 + + + + 返回由字节数组中指定位置的两个字节转换来的 16 位有符号整数。 + + 字节数组。 + value 内的起始位置。 + 由两个字节构成的 16 位有符号整数。 + + + + 以字节数组的形式获取指定的 16 位无符号整数值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的 16 位无符号整数值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的 16 位无符号整数值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + buffer 内的起始位置。 + + + + 返回由字节数组中前两个字节转换来的 16 位无符号整数。 + + 字节数组。 + 由两个字节构成的 16 位无符号整数。 + + + + 返回由字节数组中指定位置的两个字节转换来的 16 位无符号整数。 + + 字节数组。 + value 内的起始位置。 + 由两个字节构成的 16 位无符号整数。 + + + + 以字节数组的形式获取指定的 32 位有符号整数值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的 32 位有符号整数值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的 32 位有符号整数值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + buffer 内的起始位置。 + + + + 返回由字节数组中前四个字节转换来的 32 位有符号整数。 + + 字节数组。 + 由四个字节构成的 32 位有符号整数。 + + + + 返回由字节数组中指定位置的四个字节转换来的 32 位有符号整数。 + + 字节数组。 + value 内的起始位置。 + 由四个字节构成的 32 位有符号整数。 + + + + 以字节数组的形式获取指定的 32 位无符号整数值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的 32 位无符号整数值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的 32 位无符号整数值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + buffer 内的起始位置。 + + + + 返回由字节数组中前四个字节转换来的 32 位无符号整数。 + + 字节数组。 + 由四个字节构成的 32 位无符号整数。 + + + + 返回由字节数组中指定位置的四个字节转换来的 32 位无符号整数。 + + 字节数组。 + value 内的起始位置。 + 由四个字节构成的 32 位无符号整数。 + + + + 以字节数组的形式获取指定的 64 位有符号整数值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的 64 位有符号整数值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的 64 位有符号整数值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + buffer 内的起始位置。 + + + + 返回由字节数组中前八个字节转换来的 64 位有符号整数。 + + 字节数组。 + 由八个字节构成的 64 位有符号整数。 + + + + 返回由字节数组中指定位置的八个字节转换来的 64 位有符号整数。 + + 字节数组。 + value 内的起始位置。 + 由八个字节构成的 64 位有符号整数。 + + + + 以字节数组的形式获取指定的 64 位无符号整数值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的 64 位无符号整数值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的 64 位无符号整数值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + buffer 内的起始位置。 + + + + 返回由字节数组中前八个字节转换来的 64 位无符号整数。 + + 字节数组。 + 由八个字节构成的 64 位无符号整数。 + + + + 返回由字节数组中指定位置的八个字节转换来的 64 位无符号整数。 + + 字节数组。 + value 内的起始位置。 + 由八个字节构成的 64 位无符号整数。 + + + + 以字节数组的形式获取指定的单精度浮点值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的单精度浮点值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的单精度浮点值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + buffer 内的起始位置。 + + + + 返回由字节数组中前四个字节转换来的单精度浮点数。 + + 字节数组。 + 由四个字节构成的单精度浮点数。 + + + + 返回由字节数组中指定位置的四个字节转换来的单精度浮点数。 + + 字节数组。 + value 内的起始位置。 + 由四个字节构成的单精度浮点数。 + + + + 以字节数组的形式获取指定的双精度浮点值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的双精度浮点值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定的双精度浮点值。 + + 要转换的数字。 + 用于存放结果的字节数组。 + buffer 内的起始位置。 + + + + 返回由字节数组中前八个字节转换来的双精度浮点数。 + + 字节数组。 + 由八个字节构成的双精度浮点数。 + + + + 返回由字节数组中指定位置的八个字节转换来的双精度浮点数。 + + 字节数组。 + value 内的起始位置。 + 由八个字节构成的双精度浮点数。 + + + + 以字节数组的形式获取 UTF-8 编码的字符串。 + + 要转换的字符串。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取 UTF-8 编码的字符串。 + + 要转换的字符串。 + 用于存放结果的字节数组。 + buffer 内实际填充了多少字节。 + + + + 以字节数组的形式获取 UTF-8 编码的字符串。 + + 要转换的字符串。 + 用于存放结果的字节数组。 + buffer 内的起始位置。 + buffer 内实际填充了多少字节。 + + + + 以字节数组的形式获取指定编码的字符串。 + + 要转换的字符串。 + 要使用的编码。 + 用于存放结果的字节数组。 + + + + 以字节数组的形式获取指定编码的字符串。 + + 要转换的字符串。 + 要使用的编码。 + 用于存放结果的字节数组。 + buffer 内实际填充了多少字节。 + + + + 以字节数组的形式获取指定编码的字符串。 + + 要转换的字符串。 + 要使用的编码。 + 用于存放结果的字节数组。 + buffer 内的起始位置。 + buffer 内实际填充了多少字节。 + + + + 返回由字节数组使用 UTF-8 编码转换成的字符串。 + + 字节数组。 + 转换后的字符串。 + + + + 返回由字节数组使用指定编码转换成的字符串。 + + 字节数组。 + 要使用的编码。 + 转换后的字符串。 + + + + 返回由字节数组使用 UTF-8 编码转换成的字符串。 + + 字节数组。 + value 内的起始位置。 + 长度。 + 转换后的字符串。 + + + + 返回由字节数组使用指定编码转换成的字符串。 + + 字节数组。 + value 内的起始位置。 + 长度。 + 要使用的编码。 + 转换后的字符串。 + + + + 加密解密相关的实用函数。 + + + + + 将 bytes 使用 code 做异或运算的快速版本。 + + 原始二进制流。 + 异或二进制流。 + 异或后的二进制流。 + + + + 将 bytes 使用 code 做异或运算的快速版本。此方法将复用并改写传入的 bytes 作为返回值,而不额外分配内存空间。 + + 原始及异或后的二进制流。 + 异或二进制流。 + + + + 将 bytes 使用 code 做异或运算。 + + 原始二进制流。 + 异或二进制流。 + 异或后的二进制流。 + + + + 将 bytes 使用 code 做异或运算。此方法将复用并改写传入的 bytes 作为返回值,而不额外分配内存空间。 + + 原始及异或后的二进制流。 + 异或二进制流。 + + + + 将 bytes 使用 code 做异或运算。 + + 原始二进制流。 + 异或计算的开始位置。 + 异或计算长度,若小于 0,则计算整个二进制流。 + 异或二进制流。 + 异或后的二进制流。 + + + + 将 bytes 使用 code 做异或运算。此方法将复用并改写传入的 bytes 作为返回值,而不额外分配内存空间。 + + 原始及异或后的二进制流。 + 异或计算的开始位置。 + 异或计算长度。 + 异或二进制流。 + + + + JSON 相关的实用函数。 + + + + + 设置 JSON 辅助器。 + + 要设置的 JSON 辅助器。 + + + + 将对象序列化为 JSON 字符串。 + + 要序列化的对象。 + 序列化后的 JSON 字符串。 + + + + 将 JSON 字符串反序列化为对象。 + + 对象类型。 + 要反序列化的 JSON 字符串。 + 反序列化后的对象。 + + + + 将 JSON 字符串反序列化为对象。 + + 对象类型。 + 要反序列化的 JSON 字符串。 + 反序列化后的对象。 + + + + JSON 辅助器接口。 + + + + + 将对象序列化为 JSON 字符串。 + + 要序列化的对象。 + 序列化后的 JSON 字符串。 + + + + 将 JSON 字符串反序列化为对象。 + + 对象类型。 + 要反序列化的 JSON 字符串。 + 反序列化后的对象。 + + + + 将 JSON 字符串反序列化为对象。 + + 对象类型。 + 要反序列化的 JSON 字符串。 + 反序列化后的对象。 + + + + Marshal 相关的实用函数。 + + + + + 获取缓存的从进程的非托管内存中分配的内存的大小。 + + + + + 确保从进程的非托管内存中分配足够大小的内存并缓存。 + + 要确保从进程的非托管内存中分配内存的大小。 + + + + 释放缓存的从进程的非托管内存中分配的内存。 + + + + + 将数据从对象转换为二进制流。 + + 要转换的对象的类型。 + 要转换的对象。 + 存储转换结果的二进制流。 + + + + 将数据从对象转换为二进制流。 + + 要转换的对象的类型。 + 要转换的对象。 + 要转换的对象的大小。 + 存储转换结果的二进制流。 + + + + 将数据从对象转换为二进制流。 + + 要转换的对象的类型。 + 要转换的对象。 + 存储转换结果的二进制流。 + + + + 将数据从对象转换为二进制流。 + + 要转换的对象的类型。 + 要转换的对象。 + 要转换的对象的大小。 + 存储转换结果的二进制流。 + + + + 将数据从对象转换为二进制流。 + + 要转换的对象的类型。 + 要转换的对象。 + 存储转换结果的二进制流。 + 写入存储转换结果的二进制流的起始位置。 + + + + 将数据从对象转换为二进制流。 + + 要转换的对象的类型。 + 要转换的对象。 + 要转换的对象的大小。 + 存储转换结果的二进制流。 + 写入存储转换结果的二进制流的起始位置。 + + + + 将数据从二进制流转换为对象。 + + 要转换的对象的类型。 + 要转换的二进制流。 + 存储转换结果的对象。 + + + + 将数据从二进制流转换为对象。 + + 要转换的对象的类型。 + 要转换的二进制流。 + 读取要转换的二进制流的起始位置。 + 存储转换结果的对象。 + + + + 将数据从二进制流转换为对象。 + + 要转换的对象的类型。 + 要转换的对象的大小。 + 要转换的二进制流。 + 存储转换结果的对象。 + + + + 将数据从二进制流转换为对象。 + + 要转换的对象的类型。 + 要转换的对象的大小。 + 要转换的二进制流。 + 读取要转换的二进制流的起始位置。 + 存储转换结果的对象。 + + + + 路径相关的实用函数。 + + + + + 获取规范的路径。 + + 要规范的路径。 + 规范的路径。 + + + + 获取远程格式的路径(带有file:// 或 http:// 前缀)。 + + 原始路径。 + 远程格式路径。 + + + + 移除空文件夹。 + + 要处理的文件夹名称。 + 是否移除空文件夹成功。 + + + + 随机相关的实用函数。 + + + + + 设置随机数种子。 + + 随机数种子。 + + + + 返回非负随机数。 + + 大于等于零且小于 System.Int32.MaxValue 的 32 位带符号整数。 + + + + 返回一个小于所指定最大值的非负随机数。 + + 要生成的随机数的上界(随机数不能取该上界值)。maxValue 必须大于等于零。 + 大于等于零且小于 maxValue 的 32 位带符号整数,即:返回值的范围通常包括零但不包括 maxValue。不过,如果 maxValue 等于零,则返回 maxValue。 + + + + 返回一个指定范围内的随机数。 + + 返回的随机数的下界(随机数可取该下界值)。 + 返回的随机数的上界(随机数不能取该上界值)。maxValue 必须大于等于 minValue。 + 一个大于等于 minValue 且小于 maxValue 的 32 位带符号整数,即:返回的值范围包括 minValue 但不包括 maxValue。如果 minValue 等于 maxValue,则返回 minValue。 + + + + 返回一个介于 0.0 和 1.0 之间的随机数。 + + 大于等于 0.0 并且小于 1.0 的双精度浮点数。 + + + + 用随机数填充指定字节数组的元素。 + + 包含随机数的字节数组。 + + + + 字符相关的实用函数。 + + + + + 设置字符辅助器。 + + 要设置的字符辅助器。 + + + + 获取格式化字符串。 + + 字符串参数的类型。 + 字符串格式。 + 字符串参数。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串参数 8 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 字符串参数 8。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串参数 8 的类型。 + 字符串参数 9 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 字符串参数 8。 + 字符串参数 9。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串参数 8 的类型。 + 字符串参数 9 的类型。 + 字符串参数 10 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 字符串参数 8。 + 字符串参数 9。 + 字符串参数 10。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串参数 8 的类型。 + 字符串参数 9 的类型。 + 字符串参数 10 的类型。 + 字符串参数 11 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 字符串参数 8。 + 字符串参数 9。 + 字符串参数 10。 + 字符串参数 11。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串参数 8 的类型。 + 字符串参数 9 的类型。 + 字符串参数 10 的类型。 + 字符串参数 11 的类型。 + 字符串参数 12 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 字符串参数 8。 + 字符串参数 9。 + 字符串参数 10。 + 字符串参数 11。 + 字符串参数 12。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串参数 8 的类型。 + 字符串参数 9 的类型。 + 字符串参数 10 的类型。 + 字符串参数 11 的类型。 + 字符串参数 12 的类型。 + 字符串参数 13 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 字符串参数 8。 + 字符串参数 9。 + 字符串参数 10。 + 字符串参数 11。 + 字符串参数 12。 + 字符串参数 13。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串参数 8 的类型。 + 字符串参数 9 的类型。 + 字符串参数 10 的类型。 + 字符串参数 11 的类型。 + 字符串参数 12 的类型。 + 字符串参数 13 的类型。 + 字符串参数 14 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 字符串参数 8。 + 字符串参数 9。 + 字符串参数 10。 + 字符串参数 11。 + 字符串参数 12。 + 字符串参数 13。 + 字符串参数 14。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串参数 8 的类型。 + 字符串参数 9 的类型。 + 字符串参数 10 的类型。 + 字符串参数 11 的类型。 + 字符串参数 12 的类型。 + 字符串参数 13 的类型。 + 字符串参数 14 的类型。 + 字符串参数 15 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 字符串参数 8。 + 字符串参数 9。 + 字符串参数 10。 + 字符串参数 11。 + 字符串参数 12。 + 字符串参数 13。 + 字符串参数 14。 + 字符串参数 15。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串参数 8 的类型。 + 字符串参数 9 的类型。 + 字符串参数 10 的类型。 + 字符串参数 11 的类型。 + 字符串参数 12 的类型。 + 字符串参数 13 的类型。 + 字符串参数 14 的类型。 + 字符串参数 15 的类型。 + 字符串参数 16 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 字符串参数 8。 + 字符串参数 9。 + 字符串参数 10。 + 字符串参数 11。 + 字符串参数 12。 + 字符串参数 13。 + 字符串参数 14。 + 字符串参数 15。 + 字符串参数 16。 + 格式化后的字符串。 + + + + 字符辅助器接口。 + + + + + 获取格式化字符串。 + + 字符串参数的类型。 + 字符串格式。 + 字符串参数。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串参数 8 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 字符串参数 8。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串参数 8 的类型。 + 字符串参数 9 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 字符串参数 8。 + 字符串参数 9。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串参数 8 的类型。 + 字符串参数 9 的类型。 + 字符串参数 10 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 字符串参数 8。 + 字符串参数 9。 + 字符串参数 10。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串参数 8 的类型。 + 字符串参数 9 的类型。 + 字符串参数 10 的类型。 + 字符串参数 11 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 字符串参数 8。 + 字符串参数 9。 + 字符串参数 10。 + 字符串参数 11。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串参数 8 的类型。 + 字符串参数 9 的类型。 + 字符串参数 10 的类型。 + 字符串参数 11 的类型。 + 字符串参数 12 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 字符串参数 8。 + 字符串参数 9。 + 字符串参数 10。 + 字符串参数 11。 + 字符串参数 12。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串参数 8 的类型。 + 字符串参数 9 的类型。 + 字符串参数 10 的类型。 + 字符串参数 11 的类型。 + 字符串参数 12 的类型。 + 字符串参数 13 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 字符串参数 8。 + 字符串参数 9。 + 字符串参数 10。 + 字符串参数 11。 + 字符串参数 12。 + 字符串参数 13。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串参数 8 的类型。 + 字符串参数 9 的类型。 + 字符串参数 10 的类型。 + 字符串参数 11 的类型。 + 字符串参数 12 的类型。 + 字符串参数 13 的类型。 + 字符串参数 14 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 字符串参数 8。 + 字符串参数 9。 + 字符串参数 10。 + 字符串参数 11。 + 字符串参数 12。 + 字符串参数 13。 + 字符串参数 14。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串参数 8 的类型。 + 字符串参数 9 的类型。 + 字符串参数 10 的类型。 + 字符串参数 11 的类型。 + 字符串参数 12 的类型。 + 字符串参数 13 的类型。 + 字符串参数 14 的类型。 + 字符串参数 15 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 字符串参数 8。 + 字符串参数 9。 + 字符串参数 10。 + 字符串参数 11。 + 字符串参数 12。 + 字符串参数 13。 + 字符串参数 14。 + 字符串参数 15。 + 格式化后的字符串。 + + + + 获取格式化字符串。 + + 字符串参数 1 的类型。 + 字符串参数 2 的类型。 + 字符串参数 3 的类型。 + 字符串参数 4 的类型。 + 字符串参数 5 的类型。 + 字符串参数 6 的类型。 + 字符串参数 7 的类型。 + 字符串参数 8 的类型。 + 字符串参数 9 的类型。 + 字符串参数 10 的类型。 + 字符串参数 11 的类型。 + 字符串参数 12 的类型。 + 字符串参数 13 的类型。 + 字符串参数 14 的类型。 + 字符串参数 15 的类型。 + 字符串参数 16 的类型。 + 字符串格式。 + 字符串参数 1。 + 字符串参数 2。 + 字符串参数 3。 + 字符串参数 4。 + 字符串参数 5。 + 字符串参数 6。 + 字符串参数 7。 + 字符串参数 8。 + 字符串参数 9。 + 字符串参数 10。 + 字符串参数 11。 + 字符串参数 12。 + 字符串参数 13。 + 字符串参数 14。 + 字符串参数 15。 + 字符串参数 16。 + 格式化后的字符串。 + + + + 校验相关的实用函数。 + + + + + CRC32 算法。 + + + + + 计算二进制流的 CRC32。 + + 指定的二进制流。 + 计算后的 CRC32。 + + + + 计算二进制流的 CRC32。 + + 指定的二进制流。 + 二进制流的偏移。 + 二进制流的长度。 + 计算后的 CRC32。 + + + + 计算二进制流的 CRC32。 + + 指定的二进制流。 + 计算后的 CRC32。 + + + + 获取 CRC32 数值的二进制数组。 + + CRC32 数值。 + CRC32 数值的二进制数组。 + + + + 获取 CRC32 数值的二进制数组。 + + CRC32 数值。 + 要存放结果的数组。 + + + + 获取 CRC32 数值的二进制数组。 + + CRC32 数值。 + 要存放结果的数组。 + CRC32 数值的二进制数组在结果数组内的起始位置。 + + + + Web 请求相关常量。 + + + + + 默认 Web 请求任务优先级。 + + + + + Web 请求代理辅助器接口。 + + + + + Web 请求代理辅助器完成事件。 + + + + + Web 请求代理辅助器错误事件。 + + + + + 通过 Web 请求代理辅助器发送 Web 请求。 + + Web 请求地址。 + 用户自定义数据。 + + + + 通过 Web 请求代理辅助器发送 Web 请求。 + + Web 请求地址。 + 要发送的数据流。 + 用户自定义数据。 + + + + 重置 Web 请求代理辅助器。 + + + + + Web 请求管理器接口。 + + + + + 获取 Web 请求代理总数量。 + + + + + 获取可用 Web 请求代理数量。 + + + + + 获取工作中 Web 请求代理数量。 + + + + + 获取等待 Web 请求数量。 + + + + + 获取或设置 Web 请求超时时长,以秒为单位。 + + + + + Web 请求开始事件。 + + + + + Web 请求成功事件。 + + + + + Web 请求失败事件。 + + + + + 增加 Web 请求代理辅助器。 + + 要增加的 Web 请求代理辅助器。 + + + + 根据 Web 请求任务的序列编号获取 Web 请求任务的信息。 + + 要获取信息的 Web 请求任务的序列编号。 + Web 请求任务的信息。 + + + + 根据 Web 请求任务的标签获取 Web 请求任务的信息。 + + 要获取信息的 Web 请求任务的标签。 + Web 请求任务的信息。 + + + + 根据 Web 请求任务的标签获取 Web 请求任务的信息。 + + 要获取信息的 Web 请求任务的标签。 + Web 请求任务的信息。 + + + + 获取所有 Web 请求任务的信息。 + + 所有 Web 请求任务的信息。 + + + + 获取所有 Web 请求任务的信息。 + + 所有 Web 请求任务的信息。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 要发送的数据流。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + Web 请求任务的标签。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + Web 请求任务的优先级。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 用户自定义数据。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 要发送的数据流。 + Web 请求任务的标签。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 要发送的数据流。 + Web 请求任务的优先级。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 要发送的数据流。 + 用户自定义数据。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + Web 请求任务的标签。 + Web 请求任务的优先级。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + Web 请求任务的标签。 + 用户自定义数据。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + Web 请求任务的优先级。 + 用户自定义数据。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 要发送的数据流。 + Web 请求任务的标签。 + Web 请求任务的优先级。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 要发送的数据流。 + Web 请求任务的标签。 + 用户自定义数据。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 要发送的数据流。 + Web 请求任务的优先级。 + 用户自定义数据。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + Web 请求任务的标签。 + Web 请求任务的优先级。 + 用户自定义数据。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 要发送的数据流。 + Web 请求任务的标签。 + Web 请求任务的优先级。 + 用户自定义数据。 + 新增 Web 请求任务的序列编号。 + + + + 根据 Web 请求任务的序列编号移除 Web 请求任务。 + + 要移除 Web 请求任务的序列编号。 + 是否移除 Web 请求任务成功。 + + + + 根据 Web 请求任务的标签移除 Web 请求任务。 + + 要移除 Web 请求任务的标签。 + 移除 Web 请求任务的数量。 + + + + 移除所有 Web 请求任务。 + + 移除 Web 请求任务的数量。 + + + + Web 请求代理辅助器完成事件。 + + + + + 初始化 Web 请求代理辅助器完成事件的新实例。 + + + + + 创建 Web 请求代理辅助器完成事件。 + + Web 响应的数据流。 + 创建的 Web 请求代理辅助器完成事件。 + + + + 清理 Web 请求代理辅助器完成事件。 + + + + + 获取 Web 响应的数据流。 + + Web 响应的数据流。 + + + + Web 请求代理辅助器错误事件。 + + + + + 初始化 Web 请求代理辅助器错误事件的新实例。 + + + + + 获取错误信息。 + + + + + 创建 Web 请求代理辅助器错误事件。 + + 错误信息。 + 创建的 Web 请求代理辅助器错误事件。 + + + + 清理 Web 请求代理辅助器错误事件。 + + + + + Web 请求失败事件。 + + + + + 初始化 Web 请求失败事件的新实例。 + + + + + 获取 Web 请求任务的序列编号。 + + + + + 获取 Web 请求地址。 + + + + + 获取错误信息。 + + + + + 获取用户自定义数据。 + + + + + 创建 Web 请求失败事件。 + + Web 请求任务的序列编号。 + Web 请求地址。 + 错误信息。 + 用户自定义数据。 + 创建的 Web 请求失败事件。 + + + + 清理 Web 请求失败事件。 + + + + + Web 请求管理器。 + + + + + 初始化 Web 请求管理器的新实例。 + + + + + 获取 Web 请求代理总数量。 + + + + + 获取可用 Web 请求代理数量。 + + + + + 获取工作中 Web 请求代理数量。 + + + + + 获取等待 Web 请求数量。 + + + + + 获取或设置 Web 请求超时时长,以秒为单位。 + + + + + Web 请求开始事件。 + + + + + Web 请求成功事件。 + + + + + Web 请求失败事件。 + + + + + Web 请求管理器轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理 Web 请求管理器。 + + + + + 增加 Web 请求代理辅助器。 + + 要增加的 Web 请求代理辅助器。 + + + + 根据 Web 请求任务的序列编号获取 Web 请求任务的信息。 + + 要获取信息的 Web 请求任务的序列编号。 + Web 请求任务的信息。 + + + + 根据 Web 请求任务的标签获取 Web 请求任务的信息。 + + 要获取信息的 Web 请求任务的标签。 + Web 请求任务的信息。 + + + + 根据 Web 请求任务的标签获取 Web 请求任务的信息。 + + 要获取信息的 Web 请求任务的标签。 + Web 请求任务的信息。 + + + + 获取所有 Web 请求任务的信息。 + + 所有 Web 请求任务的信息。 + + + + 获取所有 Web 请求任务的信息。 + + 所有 Web 请求任务的信息。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 要发送的数据流。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + Web 请求任务的标签。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + Web 请求任务的优先级。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 用户自定义数据。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 要发送的数据流。 + Web 请求任务的标签。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 要发送的数据流。 + Web 请求任务的优先级。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 要发送的数据流。 + 用户自定义数据。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + Web 请求任务的标签。 + Web 请求任务的优先级。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + Web 请求任务的标签。 + 用户自定义数据。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + Web 请求任务的优先级。 + 用户自定义数据。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 要发送的数据流。 + Web 请求任务的标签。 + Web 请求任务的优先级。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 要发送的数据流。 + Web 请求任务的标签。 + 用户自定义数据。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 要发送的数据流。 + Web 请求任务的优先级。 + 用户自定义数据。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + Web 请求任务的标签。 + Web 请求任务的优先级。 + 用户自定义数据。 + 新增 Web 请求任务的序列编号。 + + + + 增加 Web 请求任务。 + + Web 请求地址。 + 要发送的数据流。 + Web 请求任务的标签。 + Web 请求任务的优先级。 + 用户自定义数据。 + 新增 Web 请求任务的序列编号。 + + + + 根据 Web 请求任务的序列编号移除 Web 请求任务。 + + 要移除 Web 请求任务的序列编号。 + 是否移除 Web 请求任务成功。 + + + + 根据 Web 请求任务的标签移除 Web 请求任务。 + + 要移除 Web 请求任务的标签。 + 移除 Web 请求任务的数量。 + + + + 移除所有 Web 请求任务。 + + 移除 Web 请求任务的数量。 + + + + Web 请求代理。 + + + + + 初始化 Web 请求代理的新实例。 + + Web 请求代理辅助器。 + + + + 获取 Web 请求任务。 + + + + + 获取已经等待时间。 + + + + + 初始化 Web 请求代理。 + + + + + Web 请求代理轮询。 + + 逻辑流逝时间,以秒为单位。 + 真实流逝时间,以秒为单位。 + + + + 关闭并清理 Web 请求代理。 + + + + + 开始处理 Web 请求任务。 + + 要处理的 Web 请求任务。 + 开始处理任务的状态。 + + + + 重置 Web 请求代理。 + + + + + Web 请求任务。 + + + + + 获取或设置 Web 请求任务的状态。 + + + + + 获取要发送的远程地址。 + + + + + 获取 Web 请求超时时长,以秒为单位。 + + + + + 获取 Web 请求任务的描述。 + + + + + 创建 Web 请求任务。 + + 要发送的远程地址。 + 要发送的数据流。 + Web 请求任务的标签。 + Web 请求任务的优先级。 + 下载超时时长,以秒为单位。 + 用户自定义数据。 + 创建的 Web 请求任务。 + + + + 清理 Web 请求任务。 + + + + + 获取要发送的数据流。 + + + + + Web 请求任务的状态。 + + + + + 准备请求。 + + + + + 请求中。 + + + + + 请求完成。 + + + + + 请求错误。 + + + + + Web 请求开始事件。 + + + + + 初始化 Web 请求开始事件的新实例。 + + + + + 获取 Web 请求任务的序列编号。 + + + + + 获取 Web 请求地址。 + + + + + 获取用户自定义数据。 + + + + + 创建 Web 请求开始事件。 + + Web 请求任务的序列编号。 + Web 请求地址。 + 用户自定义数据。 + 创建的 Web 请求开始事件。 + + + + 清理 Web 请求开始事件。 + + + + + Web 请求成功事件。 + + + + + 初始化 Web 请求成功事件的新实例。 + + + + + 获取 Web 请求任务的序列编号。 + + + + + 获取 Web 请求地址。 + + + + + 获取用户自定义数据。 + + + + + 创建 Web 请求成功事件。 + + Web 请求任务的序列编号。 + Web 请求地址。 + Web 响应的数据流。 + 用户自定义数据。 + 创建的 Web 请求成功事件。 + + + + 清理 Web 请求成功事件。 + + + + + 获取 Web 响应的数据流。 + + Web 响应的数据流。 + + + diff --git a/src/Libraries/GameFramework.xml.meta b/src/Libraries/GameFramework.xml.meta new file mode 100644 index 0000000..2f84387 --- /dev/null +++ b/src/Libraries/GameFramework.xml.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 46cbe7b3d65fad24ba53d57c6bca3740 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Libraries/ICSharpCode.SharpZipLib.dll b/src/Libraries/ICSharpCode.SharpZipLib.dll new file mode 100644 index 0000000000000000000000000000000000000000..fe643ebc638dfc94f1149528d0e9a91ce52dc353 GIT binary patch literal 200704 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

M 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 + + + + + + diff --git a/src/Libraries/link.xml.meta b/src/Libraries/link.xml.meta new file mode 100644 index 0000000..7dee6d6 --- /dev/null +++ b/src/Libraries/link.xml.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 843cc923151568443b86e022f705a263 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: