From ebbbb7332e546afdad064739fc17cb07f7dc04c9 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 12 May 2026 15:34:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=9A=E8=BD=AE=E5=AE=A1=E6=9F=A5=E5=92=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Events/Boss/EVT_BossFightEnded.asset | 15 + .../Events/Boss/EVT_BossFightEnded.asset.meta | 8 + .../Events/Boss/EVT_BossFightStarted.asset | 15 + .../Boss/EVT_BossFightStarted.asset.meta | 8 + .../Combat/EVT_DeathScreenConfirmed.asset | 15 + .../EVT_DeathScreenConfirmed.asset.meta | 8 + .../Events/Combat/EVT_PlayerRespawned.asset | 15 + .../Combat/EVT_PlayerRespawned.asset.meta | 8 + .../Events/Combat/EVT_RespawnCompleted.asset | 15 + .../Combat/EVT_RespawnCompleted.asset.meta | 8 + .../Events/Combat/EVT_RespawnStarted.asset | 15 + .../Combat/EVT_RespawnStarted.asset.meta | 8 + .../Data/Events/Core/EVT_FadeInRequest.asset | 15 + .../Events/Core/EVT_FadeInRequest.asset.meta | 8 + .../Data/Events/Core/EVT_FadeOutRequest.asset | 15 + .../Events/Core/EVT_FadeOutRequest.asset.meta | 8 + .../Events/Core/EVT_GameStateChanged.asset | 15 + .../Core/EVT_GameStateChanged.asset.meta | 8 + Assets/Data/Events/Core/EVT_SceneLoaded.asset | 15 + .../Events/Core/EVT_SceneLoaded.asset.meta | 8 + .../Data/Events/UI/EVT_FastTravelOpen.asset | 15 + .../Events/UI/EVT_FastTravelOpen.asset.meta | 8 + Assets/Data/Events/UI/EVT_MapOpen.asset | 15 + Assets/Data/Events/UI/EVT_MapOpen.asset.meta | 8 + Assets/Data/Events/UI/EVT_ShopOpen.asset | 15 + Assets/Data/Events/UI/EVT_ShopOpen.asset.meta | 8 + .../Events/World/EVT_SavePointActivated.asset | 15 + .../World/EVT_SavePointActivated.asset.meta | 8 + Assets/Data/Input.meta | 8 + Assets/Data/Input/InputReader.asset | 16 + Assets/Data/Input/InputReader.asset.meta | 8 + .../Player/PLY_PlayerAnimationConfig.asset | 29 + .../PLY_PlayerAnimationConfig.asset.meta | 8 + .../Player/PLY_PlayerMovementConfig.asset | 36 + .../PLY_PlayerMovementConfig.asset.meta | 8 + Assets/Data/Player/PLY_PlayerStats.asset | 23 + Assets/Data/Player/PLY_PlayerStats.asset.meta | 8 + Assets/Resources/Art.meta | 8 + Assets/Resources/Art/Builds.meta | 8 + Assets/Resources/Art/Effects.meta | 8 + Assets/Resources/Art/Effects/Textures.meta | 8 + .../Art/Effects/Textures/Fx_Ring03.png | Bin 0 -> 36037 bytes .../Art/Effects/Textures/Fx_Ring03.png.meta | 123 + Assets/Scenes/Persistent.unity | 2553 +- Assets/Scenes/Persistent.unity.meta | 2 +- Assets/Scenes/TestRoom.unity | 2606 + Assets/Scenes/TestRoom.unity.meta | 7 + Assets/Scenes/Testings.meta | 8 + Assets/Scenes/Testings/Build01.prefab | 789419 +++++++++++++++ Assets/Scenes/Testings/Build01.prefab.meta | 7 + Assets/Scenes/Testings/Test.prefab | 286 + Assets/Scenes/Testings/Test.prefab.meta | 7 + Assets/Scripts/Animation/.gitkeep | 0 .../Scripts/Animation/AnimationEventBinder.cs | 31 + .../AnimationEventBinder.cs.meta} | 2 +- .../Animation/AnimationEventConfigSO.cs | 64 + .../AnimationEventConfigSO.cs.meta} | 2 +- .../Scripts/Animation/AnimationEventType.cs | 49 + ...der.cs.meta => AnimationEventType.cs.meta} | 2 +- .../Animation/BaseGames.Animation.asmdef | 7 +- .../Scripts/Animation/EnemyAnimationEvents.cs | 111 + .../EnemyAnimationEvents.cs.meta} | 2 +- .../Animation/IAnimationEventHandler.cs | 17 + .../Animation/IAnimationEventHandler.cs.meta | 11 + .../Animation/PlayerAnimationEvents.cs | 149 + .../Animation/PlayerAnimationEvents.cs.meta | 11 + Assets/Scripts/Animation/_Placeholder.cs | 3 - Assets/Scripts/Audio/.gitkeep | 0 Assets/Scripts/Audio/AudioEventSO.cs | 52 + Assets/Scripts/Audio/AudioEventSO.cs.meta | 11 + Assets/Scripts/Audio/AudioManager.cs | 97 +- Assets/Scripts/Audio/AudioManager.cs.meta | 2 +- Assets/Scripts/Audio/BGMController.cs | 27 +- Assets/Scripts/Audio/CombatSFXController.cs | 52 +- Assets/Scripts/Audio/FootstepAudioConfigSO.cs | 30 + .../Audio/FootstepAudioConfigSO.cs.meta | 11 + Assets/Scripts/Audio/FootstepMaterial.cs | 16 + Assets/Scripts/Audio/FootstepMaterial.cs.meta | 11 + .../Scripts/Audio/FootstepMaterialMarker.cs | 16 + .../Audio/FootstepMaterialMarker.cs.meta | 11 + Assets/Scripts/Audio/GlobalSFXPlayer.cs | 44 + Assets/Scripts/Audio/GlobalSFXPlayer.cs.meta | 11 + .../Audio/UnderwaterAudioController.cs | 30 + .../Audio/UnderwaterAudioController.cs.meta | 11 + Assets/Scripts/Audio/_Placeholder.cs | 3 - .../Scripts/Camera/CameraStateController.cs | 15 +- Assets/Scripts/Camera/CameraTriggerZone.cs | 3 +- Assets/Scripts/Camera/ICameraService.cs | 18 + Assets/Scripts/Camera/ICameraService.cs.meta | 11 + Assets/Scripts/Combat/ArcProjectile.cs | 27 + Assets/Scripts/Combat/ArcProjectile.cs.meta | 11 + Assets/Scripts/Combat/BaseGames.Combat.asmdef | 1 + Assets/Scripts/Combat/ClashConfigSO.cs | 24 + Assets/Scripts/Combat/ClashConfigSO.cs.meta | 11 + Assets/Scripts/Combat/ClashResolver.cs | 70 + Assets/Scripts/Combat/ClashResolver.cs.meta | 11 + Assets/Scripts/Combat/DamageInfo.cs | 107 +- Assets/Scripts/Combat/HitBox.cs | 113 +- Assets/Scripts/Combat/HitStopManager.cs | 102 + Assets/Scripts/Combat/HitStopManager.cs.meta | 11 + Assets/Scripts/Combat/HomingProjectile.cs | 40 + .../Scripts/Combat/HomingProjectile.cs.meta | 11 + Assets/Scripts/Combat/HurtBox.cs | 45 +- Assets/Scripts/Combat/IClashService.cs | 11 + Assets/Scripts/Combat/IClashService.cs.meta | 11 + Assets/Scripts/Combat/IProjectileService.cs | 17 + .../Scripts/Combat/IProjectileService.cs.meta | 11 + Assets/Scripts/Combat/LinearProjectile.cs | 13 + .../Scripts/Combat/LinearProjectile.cs.meta | 11 + Assets/Scripts/Combat/ParryableProjectile.cs | 58 + .../Combat/ParryableProjectile.cs.meta | 11 + Assets/Scripts/Combat/PoiseWindowConfig.cs | 31 + .../Scripts/Combat/PoiseWindowConfig.cs.meta | 11 + Assets/Scripts/Combat/Projectile.cs | 67 + Assets/Scripts/Combat/Projectile.cs.meta | 11 + Assets/Scripts/Combat/ProjectileConfigSO.cs | 28 + .../Scripts/Combat/ProjectileConfigSO.cs.meta | 11 + Assets/Scripts/Combat/ProjectileManager.cs | 55 + .../Scripts/Combat/ProjectileManager.cs.meta | 11 + Assets/Scripts/Combat/ShieldComponent.cs | 131 +- Assets/Scripts/Combat/ShieldConfigSO.cs | 29 + Assets/Scripts/Combat/ShieldConfigSO.cs.meta | 11 + Assets/Scripts/Combat/SkillHitBoxInstance.cs | 48 + .../Combat/SkillHitBoxInstance.cs.meta | 11 + Assets/Scripts/Combat/StatusEffects/.gitkeep | 0 .../BaseGames.Combat.StatusEffects.asmdef | 3 +- .../Combat/StatusEffects/FireEffect.cs | 45 + .../Combat/StatusEffects/FireEffect.cs.meta | 11 + .../Combat/StatusEffects/PoisonEffect.cs | 54 + .../Combat/StatusEffects/PoisonEffect.cs.meta | 11 + .../Combat/StatusEffects/StaggerEffect.cs | 33 + .../StatusEffects/StaggerEffect.cs.meta | 11 + .../Combat/StatusEffects/StatusEffect.cs | 91 + .../Combat/StatusEffects/StatusEffect.cs.meta | 11 + .../StatusEffectEventChannelSO.cs | 21 + .../StatusEffectEventChannelSO.cs.meta | 11 + .../StatusEffects/StatusEffectManager.cs | 176 +- .../Combat/StatusEffects/StatusEffectType.cs | 15 + .../StatusEffects/StatusEffectType.cs.meta | 11 + .../Combat/StatusEffects/_Placeholder.cs | 3 - Assets/Scripts/Combat/_Placeholder.cs | 3 - .../Scripts/Core/Assets/AddressKeyRegistry.cs | 2 +- Assets/Scripts/Core/Assets/AddressKeys.cs | 16 +- .../Core/Assets/AssetReleaseTracker.cs | 22 +- Assets/Scripts/Core/DeathRespawnService.cs | 58 +- Assets/Scripts/Core/Difficulty.meta | 8 + .../Core/Difficulty/DifficultyManager.cs | 79 + .../Core/Difficulty/DifficultyManager.cs.meta | 11 + .../Core/Difficulty/DifficultyScalerSO.cs | 41 + .../Difficulty/DifficultyScalerSO.cs.meta | 11 + .../Core/Difficulty/IDifficultyService.cs | 22 + .../Difficulty/IDifficultyService.cs.meta | 11 + .../Scripts/Core/Events/BaseEventChannelSO.cs | 56 +- ...EventChannels.cs => BoolEventChannelSO.cs} | 0 .../Core/Events/BoolEventChannelSO.cs.meta | 11 + Assets/Scripts/Core/Events/DamageInfo.cs | 11 +- .../Events/DifficultyChangedEventChannel.cs | 2 +- Assets/Scripts/Core/Events/EventBusMonitor.cs | 42 +- .../Core/Events/EventChannelRegistry.cs | 7 +- .../Core/Events/EventChannelRegistry.cs.meta | 2 +- Assets/Scripts/Core/Events/HitInfo.cs | 11 +- .../Events/PrimitiveEventChannels.cs.meta | 11 - .../Core/{ => Events}/ServiceLocator.cs | 17 + .../Core/{ => Events}/ServiceLocator.cs.meta | 0 Assets/Scripts/Core/GameIds.cs | 97 + Assets/Scripts/Core/GameIds.cs.meta | 11 + Assets/Scripts/Core/GameManager.cs | 77 +- Assets/Scripts/Core/GameManager.cs.meta | 2 +- Assets/Scripts/Core/GameServiceRegistrar.cs | 163 +- .../Scripts/Core/GameServiceRegistrar.cs.meta | 2 +- Assets/Scripts/Core/GlobalSettingsSO.cs | 3 + Assets/Scripts/Core/IAudioService.cs | 5 + Assets/Scripts/Core/IObjectPoolService.cs | 23 + .../Scripts/Core/IObjectPoolService.cs.meta | 11 + Assets/Scripts/Core/ISaveService.cs | 56 +- Assets/Scripts/Core/ISettingsService.cs | 22 + Assets/Scripts/Core/ISettingsService.cs.meta | 11 + Assets/Scripts/Core/NullAudioService.cs | 5 +- Assets/Scripts/Core/Pool/GlobalObjectPool.cs | 88 +- Assets/Scripts/Core/Pool/PooledObject.cs | 7 + Assets/Scripts/Core/Save/CrashReporter.cs | 65 + .../Scripts/Core/Save/CrashReporter.cs.meta | 11 + .../Scripts/Core/Save/EmergencySaveService.cs | 18 +- Assets/Scripts/Core/Save/ISaveableRegistry.cs | 16 + .../Core/Save/ISaveableRegistry.cs.meta | 11 + Assets/Scripts/Core/Save/LocalFileStorage.cs | 5 +- Assets/Scripts/Core/Save/SaveData.cs | 47 +- Assets/Scripts/Core/Save/SaveManager.cs | 174 +- Assets/Scripts/Core/Save/SaveMigrator.cs | 82 +- .../Core/Save/SaveableMonoBehaviour.cs | 25 + .../Core/Save/SaveableMonoBehaviour.cs.meta | 11 + Assets/Scripts/Core/SceneLoader.cs | 41 +- Assets/Scripts/Core/SceneService.cs | 12 +- Assets/Scripts/Core/SceneService.cs.meta | 2 +- Assets/Scripts/Core/SettingsManager.cs | 14 +- Assets/Scripts/Core/Validation.meta | 8 + .../Scripts/Core/Validation/IValidatable.cs | 34 + .../Core/Validation/IValidatable.cs.meta | 11 + .../Cutscene/BaseGames.Cutscene.asmdef | 6 +- Assets/Scripts/Cutscene/CutsceneManager.cs | 107 + .../Scripts/Cutscene/CutsceneManager.cs.meta | 11 + Assets/Scripts/Cutscene/CutsceneSO.cs | 52 + Assets/Scripts/Cutscene/CutsceneSO.cs.meta | 11 + Assets/Scripts/Cutscene/CutsceneTrigger.cs | 81 + .../Scripts/Cutscene/CutsceneTrigger.cs.meta | 11 + Assets/Scripts/Cutscene/SignalEmitterClip.cs | 63 + .../Cutscene/SignalEmitterClip.cs.meta | 11 + .../Dialogue/BaseGames.Dialogue.asmdef | 7 +- Assets/Scripts/Dialogue/DialogueDataSO.cs | 6 +- Assets/Scripts/Dialogue/DialogueManager.cs | 141 + .../Scripts/Dialogue/DialogueManager.cs.meta | 11 + Assets/Scripts/Dialogue/DialogueSequenceSO.cs | 41 + .../Dialogue/DialogueSequenceSO.cs.meta | 11 + Assets/Scripts/Dialogue/DialogueUI.cs | 102 + Assets/Scripts/Dialogue/DialogueUI.cs.meta | 11 + Assets/Scripts/Dialogue/IDialogueService.cs | 18 + .../Scripts/Dialogue/IDialogueService.cs.meta | 11 + Assets/Scripts/Dialogue/InteractableNPC.cs | 55 + .../Scripts/Dialogue/InteractableNPC.cs.meta | 11 + .../Dialogue/InteractionPromptController.cs | 45 + .../InteractionPromptController.cs.meta | 11 + Assets/Scripts/Dialogue/NarrativeNPC.cs | 67 + Assets/Scripts/Dialogue/NarrativeNPC.cs.meta | 11 + Assets/Scripts/Editor/.gitkeep | 0 Assets/Scripts/Editor/Achievements.meta | 8 + .../Achievements/AchievementSOEditor.cs | 112 + .../Achievements/AchievementSOEditor.cs.meta | 11 + Assets/Scripts/Editor/AddressKeyValidator.cs | 29 +- .../Editor/AddressReferenceGraphWindow.cs | 288 + .../AddressReferenceGraphWindow.cs.meta | 11 + Assets/Scripts/Editor/BaseGames.Editor.asmdef | 72 +- .../Scripts/Editor/BossSkillSequenceWindow.cs | 305 + .../Editor/BossSkillSequenceWindow.cs.meta | 11 + Assets/Scripts/Editor/Combat.meta | 8 + Assets/Scripts/Editor/Combat/HurtBoxEditor.cs | 64 + .../Editor/Combat/HurtBoxEditor.cs.meta | 11 + .../Editor/CreateEventChannelAssets.cs | 18 +- Assets/Scripts/Editor/Equipment.meta | 8 + .../Editor/Equipment/CharmEffectDrawer.cs | 118 + .../Equipment/CharmEffectDrawer.cs.meta | 11 + .../Scripts/Editor/EventBusMonitorWindow.cs | 117 + .../Editor/EventBusMonitorWindow.cs.meta | 11 + .../Scripts/Editor/EventChainEditorWindow.cs | 312 + .../Editor/EventChainEditorWindow.cs.meta | 11 + Assets/Scripts/Editor/EventChannelEditor.cs | 92 + .../Scripts/Editor/EventChannelEditor.cs.meta | 11 + Assets/Scripts/Editor/EventConfigEditor.cs | 173 + .../Scripts/Editor/EventConfigEditor.cs.meta | 11 + Assets/Scripts/Editor/Map.meta | 8 + .../Scripts/Editor/Map/MapRoomDataEditor.cs | 140 + .../Editor/Map/MapRoomDataEditor.cs.meta | 11 + .../Scripts/Editor/NavSurfaceBakeShortcut.cs | 67 + .../Editor/NavSurfaceBakeShortcut.cs.meta | 11 + Assets/Scripts/Editor/SceneScaffoldTools.cs | 904 + .../Scripts/Editor/SceneScaffoldTools.cs.meta | 11 + .../Editor/ScriptExecutionOrderTools.cs | 142 + .../Editor/ScriptExecutionOrderTools.cs.meta | 11 + Assets/Scripts/Editor/Validation.meta | 8 + .../Editor/Validation/SOValidationRunner.cs | 78 + .../Validation/SOValidationRunner.cs.meta | 11 + Assets/Scripts/Editor/World.meta | 8 + .../Editor/World/DestructibleTileEditor.cs | 52 + .../World/DestructibleTileEditor.cs.meta | 11 + Assets/Scripts/Enemies/AI/BD_CanAttack.cs | 27 + .../Scripts/Enemies/AI/BD_CanAttack.cs.meta | 11 + Assets/Scripts/Enemies/AI/BD_EnterPhase.cs | 31 + .../Scripts/Enemies/AI/BD_EnterPhase.cs.meta | 11 + Assets/Scripts/Enemies/AI/BD_FaceTarget.cs | 32 + .../Scripts/Enemies/AI/BD_FaceTarget.cs.meta | 11 + Assets/Scripts/Enemies/AI/BD_IsGrounded.cs | 27 + .../Scripts/Enemies/AI/BD_IsGrounded.cs.meta | 11 + Assets/Scripts/Enemies/AI/BD_IsHPBelow.cs | 32 + .../Scripts/Enemies/AI/BD_IsHPBelow.cs.meta | 11 + Assets/Scripts/Enemies/AI/BD_IsNearEdge.cs | 28 + .../Scripts/Enemies/AI/BD_IsNearEdge.cs.meta | 11 + .../Scripts/Enemies/AI/BD_IsPlayerVisible.cs | 28 + .../Enemies/AI/BD_IsPlayerVisible.cs.meta | 11 + Assets/Scripts/Enemies/AI/BD_IsStateMatch.cs | 43 + .../Enemies/AI/BD_IsStateMatch.cs.meta | 11 + Assets/Scripts/Enemies/AI/BD_JumpTo.cs | 45 + Assets/Scripts/Enemies/AI/BD_JumpTo.cs.meta | 11 + Assets/Scripts/Enemies/AI/BD_MoveTo.cs | 42 + Assets/Scripts/Enemies/AI/BD_MoveTo.cs.meta | 11 + Assets/Scripts/Enemies/AI/BD_MoveToPlayer.cs | 15 +- Assets/Scripts/Enemies/AI/BD_Patrol.cs | 37 +- Assets/Scripts/Enemies/AI/BD_PlayAnimation.cs | 37 + .../Enemies/AI/BD_PlayAnimation.cs.meta | 11 + Assets/Scripts/Enemies/AI/BD_SetAlert.cs | 29 + Assets/Scripts/Enemies/AI/BD_SetAlert.cs.meta | 11 + .../Scripts/Enemies/AI/BD_SpawnProjectile.cs | 39 + .../Enemies/AI/BD_SpawnProjectile.cs.meta | 11 + Assets/Scripts/Enemies/AI/BD_StopMovement.cs | 28 + .../Enemies/AI/BD_StopMovement.cs.meta | 11 + Assets/Scripts/Enemies/AI/BD_SummonMinions.cs | 43 + .../Enemies/AI/BD_SummonMinions.cs.meta | 11 + .../Scripts/Enemies/AI/BD_TelegraphAttack.cs | 40 + .../Enemies/AI/BD_TelegraphAttack.cs.meta | 11 + Assets/Scripts/Enemies/AI/BD_TeleportTo.cs | 23 + .../Scripts/Enemies/AI/BD_TeleportTo.cs.meta | 11 + Assets/Scripts/Enemies/AI/BD_Wait.cs | 30 + Assets/Scripts/Enemies/AI/BD_Wait.cs.meta | 11 + .../Scripts/Enemies/AI/BD_WaitForAnimation.cs | 34 + .../Enemies/AI/BD_WaitForAnimation.cs.meta | 11 + Assets/Scripts/Enemies/AI/BD_WaitRandom.cs | 33 + .../Scripts/Enemies/AI/BD_WaitRandom.cs.meta | 11 + .../Enemies/AI/BaseGames.Enemies.AI.asmdef | 1 + Assets/Scripts/Enemies/AI/BatchLOSSystem.cs | 97 + .../Scripts/Enemies/AI/BatchLOSSystem.cs.meta | 11 + Assets/Scripts/Enemies/AI/ILOSRequester.cs | 27 + .../Scripts/Enemies/AI/ILOSRequester.cs.meta | 11 + .../Scripts/Enemies/BaseGames.Enemies.asmdef | 6 +- .../Scripts/Enemies/Boss/AttackPatternSO.cs | 33 + .../Enemies/Boss/AttackPatternSO.cs.meta | 11 + Assets/Scripts/Enemies/Boss/BossBase.cs | 48 + Assets/Scripts/Enemies/Boss/BossBase.cs.meta | 11 + .../Enemies/Boss/BossResourceConfigSO.cs | 26 + .../Enemies/Boss/BossResourceConfigSO.cs.meta | 11 + .../Scripts/Enemies/Boss/BossSkillExecutor.cs | 184 + .../Enemies/Boss/BossSkillExecutor.cs.meta | 11 + Assets/Scripts/Enemies/Boss/BossSkillSO.cs | 61 + .../Scripts/Enemies/Boss/BossSkillSO.cs.meta | 11 + Assets/Scripts/Enemies/Boss/BossSkillTypes.cs | 184 + .../Enemies/Boss/BossSkillTypes.cs.meta | 11 + .../Enemies/Boss/Patterns/TelegraphSystem.cs | 50 + .../Boss/Patterns/TelegraphSystem.cs.meta | 11 + .../Scripts/Enemies/Boss/SkillSequenceSO.cs | 26 + .../Enemies/Boss/SkillSequenceSO.cs.meta | 11 + .../Scripts/Enemies/Boss/WeakPointSystem.cs | 51 + .../Enemies/Boss/WeakPointSystem.cs.meta | 11 + .../Scripts/Enemies/EnemyAnimationConfigSO.cs | 20 + Assets/Scripts/Enemies/EnemyBase.cs | 176 +- Assets/Scripts/Enemies/EnemyCombat.cs | 5 +- Assets/Scripts/Enemies/EnemyFeedback.cs | 3 +- Assets/Scripts/Enemies/EnemyMovement.cs | 39 +- Assets/Scripts/Enemies/EnemyPoiseComponent.cs | 40 + .../Enemies/EnemyPoiseComponent.cs.meta | 11 + Assets/Scripts/Enemies/EnemyQuotaManager.cs | 104 + .../Scripts/Enemies/EnemyQuotaManager.cs.meta | 11 + Assets/Scripts/Enemies/EnemyStats.cs | 52 +- Assets/Scripts/Enemies/EnemyStatsSO.cs | 6 + Assets/Scripts/Enemies/FlyingEnemy.cs | 70 + Assets/Scripts/Enemies/FlyingEnemy.cs.meta | 11 + Assets/Scripts/Enemies/ILOSRequester.cs | 27 + Assets/Scripts/Enemies/ILOSRequester.cs.meta | 11 + Assets/Scripts/Enemies/IPathAgent.cs | 4 + Assets/Scripts/Enemies/LootResolver.cs | 72 + Assets/Scripts/Enemies/LootResolver.cs.meta | 11 + Assets/Scripts/Enemies/LootTableSO.cs | 29 + Assets/Scripts/Enemies/LootTableSO.cs.meta | 11 + .../Enemies/Navigation/EnemyNavAgent.cs | 11 + Assets/Scripts/Enemies/RangedEnemy.cs | 54 + Assets/Scripts/Enemies/RangedEnemy.cs.meta | 11 + Assets/Scripts/Enemies/States.meta | 8 + .../Enemies/States/EnemyControlledState.cs | 14 + .../States/EnemyControlledState.cs.meta | 11 + .../Scripts/Enemies/States/EnemyDeadState.cs | 18 + .../Enemies/States/EnemyDeadState.cs.meta | 11 + .../Scripts/Enemies/States/EnemyHurtState.cs | 26 + .../Enemies/States/EnemyHurtState.cs.meta | 11 + .../Enemies/States/EnemyStaggerState.cs | 25 + .../Enemies/States/EnemyStaggerState.cs.meta | 11 + Assets/Scripts/Enemies/States/IEnemyState.cs | 19 + .../Enemies/States/IEnemyState.cs.meta | 11 + .../Equipment/BaseGames.Equipment.asmdef | 7 +- Assets/Scripts/Equipment/CharmCatalogSO.cs | 25 + .../Scripts/Equipment/CharmCatalogSO.cs.meta | 11 + Assets/Scripts/Equipment/CharmSO.cs | 42 + Assets/Scripts/Equipment/CharmSO.cs.meta | 11 + Assets/Scripts/Equipment/Effects.meta | 8 + .../Equipment/Effects/AttackSpeedEffect.cs | 28 + .../Effects/AttackSpeedEffect.cs.meta | 11 + .../Scripts/Equipment/Effects/OnHitEffect.cs | 69 + .../Equipment/Effects/OnHitEffect.cs.meta | 11 + .../Effects/SkillNumericModifierEffect.cs | 27 + .../SkillNumericModifierEffect.cs.meta | 11 + .../Effects/SkillSlotOverrideEffect.cs | 30 + .../Effects/SkillSlotOverrideEffect.cs.meta | 11 + .../Equipment/Effects/SoulSpellEffect.cs | 36 + .../Equipment/Effects/SoulSpellEffect.cs.meta | 11 + .../Equipment/Effects/StatModifierEffect.cs | 28 + .../Effects/StatModifierEffect.cs.meta | 11 + .../Equipment/Effects/WeaponOverrideEffect.cs | 29 + .../Effects/WeaponOverrideEffect.cs.meta | 11 + Assets/Scripts/Equipment/EquipmentConfigSO.cs | 20 + .../Equipment/EquipmentConfigSO.cs.meta | 11 + Assets/Scripts/Equipment/EquipmentManager.cs | 143 + .../Equipment/EquipmentManager.cs.meta | 11 + Assets/Scripts/Equipment/ICharmEffect.cs | 32 + Assets/Scripts/Equipment/ICharmEffect.cs.meta | 11 + Assets/Scripts/Equipment/ToolCatalogSO.cs | 25 + .../Scripts/Equipment/ToolCatalogSO.cs.meta | 11 + Assets/Scripts/Equipment/ToolSO.cs | 51 + Assets/Scripts/Equipment/ToolSO.cs.meta | 11 + Assets/Scripts/Equipment/ToolSlotManager.cs | 91 + .../Scripts/Equipment/ToolSlotManager.cs.meta | 11 + Assets/Scripts/EventChain.meta | 8 + .../BaseGames.EventChain.asmdef} | 8 +- .../BaseGames.EventChain.asmdef.meta | 7 + .../Scripts/EventChain/EventChainManager.cs | 139 + .../EventChain/EventChainManager.cs.meta | 11 + Assets/Scripts/EventChain/EventChainSO.cs | 288 + .../Scripts/EventChain/EventChainSO.cs.meta | 11 + Assets/Scripts/Feedback/IFeedbackPlayer.cs | 4 + Assets/Scripts/Feedback/PlayerFeedback.cs | 2 + Assets/Scripts/Input/.gitkeep | 0 Assets/Scripts/Input/ConflictDetector.cs | 46 + Assets/Scripts/Input/ConflictDetector.cs.meta | 11 + Assets/Scripts/Input/InputReaderBootstrap.cs | 56 + .../Input/InputReaderBootstrap.cs.meta | 11 + Assets/Scripts/Input/InputReaderSO.cs | 188 +- Assets/Scripts/Input/_Placeholder.cs | 3 - Assets/Scripts/Input/_Placeholder.cs.meta | 11 - .../Localization/ILocalizationService.cs | 24 + .../Localization/ILocalizationService.cs.meta | 11 + Assets/Scripts/Localization/Language.cs | 14 + Assets/Scripts/Localization/Language.cs.meta | 11 + .../Localization/LanguageEventChannelSO.cs | 12 + .../LanguageEventChannelSO.cs.meta | 11 + .../Localization/LocalizationManager.cs | 177 + .../Localization/LocalizationManager.cs.meta | 11 + Assets/Scripts/Parry/BaseGames.Parry.asmdef | 5 +- Assets/Scripts/Parry/ParryConfigSO.cs | 38 + Assets/Scripts/Parry/ParryConfigSO.cs.meta | 11 + Assets/Scripts/Parry/ParryInfo.cs | 15 + Assets/Scripts/Parry/ParryInfo.cs.meta | 11 + .../Scripts/Parry/ParryInfoEventChannelSO.cs | 13 + .../Parry/ParryInfoEventChannelSO.cs.meta | 11 + Assets/Scripts/Parry/ParrySystem.cs | 201 +- Assets/Scripts/Platform/_Placeholder.cs | 3 - Assets/Scripts/Platform/_Placeholder.cs.meta | 11 - Assets/Scripts/Player/FormConfigSO.cs | 24 +- Assets/Scripts/Player/FormController.cs | 68 +- Assets/Scripts/Player/FormSO.cs | 24 + Assets/Scripts/Player/FormSO.cs.meta | 11 + Assets/Scripts/Player/FormType.cs | 13 + Assets/Scripts/Player/FormType.cs.meta | 11 + .../Scripts/Player/PlayerAnimationConfigSO.cs | 20 + Assets/Scripts/Player/PlayerCombat.cs | 59 +- Assets/Scripts/Player/PlayerMovement.cs | 69 +- .../Scripts/Player/PlayerMovementConfigSO.cs | 3 + Assets/Scripts/Player/PlayerStats.cs | 100 +- Assets/Scripts/Player/PlayerWallDetector.cs | 70 + .../Scripts/Player/PlayerWallDetector.cs.meta | 11 + Assets/Scripts/Player/SkillManager.cs | 9 +- Assets/Scripts/Player/SpringSystem.cs | 9 +- Assets/Scripts/Player/StatType.cs | 16 + Assets/Scripts/Player/StatType.cs.meta | 11 + .../Scripts/Player/States/AerialDashState.cs | 72 + .../Player/States/AerialDashState.cs.meta | 11 + .../Scripts/Player/States/AirAttackState.cs | 46 + .../Player/States/AirAttackState.cs.meta | 11 + Assets/Scripts/Player/States/AttackState.cs | 20 +- .../States/BaseGames.Player.States.asmdef | 4 +- Assets/Scripts/Player/States/DashState.cs | 77 + .../Scripts/Player/States/DashState.cs.meta | 11 + Assets/Scripts/Player/States/DeadState.cs | 39 + .../Scripts/Player/States/DeadState.cs.meta | 11 + .../Scripts/Player/States/DownAttackState.cs | 70 + .../Player/States/DownAttackState.cs.meta | 11 + Assets/Scripts/Player/States/FallState.cs | 10 +- Assets/Scripts/Player/States/HurtState.cs | 63 + .../Scripts/Player/States/HurtState.cs.meta | 11 + Assets/Scripts/Player/States/IdleState.cs | 8 +- Assets/Scripts/Player/States/JumpState.cs | 4 +- Assets/Scripts/Player/States/ParryState.cs | 46 + .../Scripts/Player/States/ParryState.cs.meta | 11 + .../Scripts/Player/States/PlayerController.cs | 293 +- .../Player/States/PlayerController.cs.meta | 2 +- .../Scripts/Player/States/PlayerStateBase.cs | 16 + Assets/Scripts/Player/States/RunState.cs | 10 +- Assets/Scripts/Player/States/SpringState.cs | 55 + .../Scripts/Player/States/SpringState.cs.meta | 11 + Assets/Scripts/Player/States/SwimState.cs | 85 + .../Scripts/Player/States/SwimState.cs.meta | 11 + Assets/Scripts/Player/States/UpAttackState.cs | 47 + .../Player/States/UpAttackState.cs.meta | 11 + Assets/Scripts/Player/States/WallJumpState.cs | 58 + .../Player/States/WallJumpState.cs.meta | 11 + .../Scripts/Player/States/WallSlideState.cs | 55 + .../Player/States/WallSlideState.cs.meta | 11 + Assets/Scripts/Player/WeaponManager.cs | 93 +- Assets/Scripts/Player/WeaponSO.cs | 85 +- Assets/Scripts/Progression/Achievement.meta | 8 + .../CollectedAllCharmsCondition.cs | 22 + .../CollectedAllCharmsCondition.cs.meta | 11 + .../Achievement/CollectedItemCondition.cs | 16 + .../CollectedItemCondition.cs.meta | 11 + .../Achievement/DefeatedAllBossesCondition.cs | 30 + .../DefeatedAllBossesCondition.cs.meta | 11 + .../Achievement/DefeatedBossCondition.cs | 16 + .../Achievement/DefeatedBossCondition.cs.meta | 11 + .../Achievement/EnteredRegionCondition.cs | 16 + .../EnteredRegionCondition.cs.meta | 11 + .../Achievement/EventTriggeredCondition.cs | 22 + .../EventTriggeredCondition.cs.meta | 11 + .../Achievement/MapExplorationCondition.cs | 22 + .../MapExplorationCondition.cs.meta | 11 + .../Achievement/NailClashCountCondition.cs | 32 + .../NailClashCountCondition.cs.meta | 11 + .../Achievement/NoHealRunCondition.cs | 29 + .../Achievement/NoHealRunCondition.cs.meta | 11 + .../Achievement/ParryCountCondition.cs | 22 + .../Achievement/ParryCountCondition.cs.meta | 11 + .../Achievement/TimedBossKillCondition.cs | 23 + .../TimedBossKillCondition.cs.meta | 11 + .../UnlockedAllAbilitiesCondition.cs | 38 + .../UnlockedAllAbilitiesCondition.cs.meta | 11 + .../Progression/AchievementCondition.cs | 18 + .../Progression/AchievementCondition.cs.meta | 11 + .../Scripts/Progression/AchievementManager.cs | 170 + .../Progression/AchievementManager.cs.meta | 11 + Assets/Scripts/Progression/AchievementSO.cs | 36 +- .../Progression/BaseGames.Progression.asmdef | 5 +- .../Progression/BossProgressTracker.cs | 33 + .../Progression/BossProgressTracker.cs.meta | 11 + .../Scripts/Progression/HPContainerPickup.cs | 67 + .../Progression/HPContainerPickup.cs.meta | 11 + .../Progression/IAchievementService.cs | 22 + .../Progression/IAchievementService.cs.meta | 11 + Assets/Scripts/Progression/ProgressLock.cs | 70 + .../Scripts/Progression/ProgressLock.cs.meta | 11 + .../Scripts/Progression/RegionDefinitionSO.cs | 33 + .../Progression/RegionDefinitionSO.cs.meta | 11 + Assets/Scripts/Quest.meta | 8 + Assets/Scripts/Quest/BaseGames.Quest.asmdef | 22 + .../Scripts/Quest/BaseGames.Quest.asmdef.meta | 7 + Assets/Scripts/Quest/BossRushSequenceSO.cs | 24 + .../Scripts/Quest/BossRushSequenceSO.cs.meta | 11 + Assets/Scripts/Quest/ChallengeEncounterSO.cs | 25 + .../Quest/ChallengeEncounterSO.cs.meta | 11 + Assets/Scripts/Quest/ChallengeRoomManager.cs | 138 + .../Quest/ChallengeRoomManager.cs.meta | 11 + Assets/Scripts/Quest/ChallengeRoomSO.cs | 38 + Assets/Scripts/Quest/ChallengeRoomSO.cs.meta | 11 + Assets/Scripts/Quest/ChallengeRoomTrigger.cs | 53 + .../Quest/ChallengeRoomTrigger.cs.meta | 11 + Assets/Scripts/Quest/IQuestManager.cs | 24 + Assets/Scripts/Quest/IQuestManager.cs.meta | 11 + Assets/Scripts/Quest/QuestGiver.cs | 82 + Assets/Scripts/Quest/QuestGiver.cs.meta | 11 + Assets/Scripts/Quest/QuestManager.cs | 270 + Assets/Scripts/Quest/QuestManager.cs.meta | 11 + Assets/Scripts/Quest/QuestObjectiveSO.cs | 81 + Assets/Scripts/Quest/QuestObjectiveSO.cs.meta | 11 + Assets/Scripts/Quest/QuestSO.cs | 46 + Assets/Scripts/Quest/QuestSO.cs.meta | 11 + Assets/Scripts/Quest/RewardSO.cs | 46 + Assets/Scripts/Quest/RewardSO.cs.meta | 11 + Assets/Scripts/Skills.meta | 8 + Assets/Scripts/Skills/BaseGames.Skills.asmdef | 20 + .../Skills/BaseGames.Skills.asmdef.meta | 7 + Assets/Scripts/Skills/FormSkillSO.cs | 85 + Assets/Scripts/Skills/FormSkillSO.cs.meta | 11 + Assets/Scripts/Skills/SkillManager.cs | 135 + Assets/Scripts/Skills/SkillManager.cs.meta | 11 + .../Scripts/Skills/SkillModifierRegistry.cs | 167 + .../Skills/SkillModifierRegistry.cs.meta | 11 + Assets/Scripts/Skills/SkillSlotNames.cs | 14 + Assets/Scripts/Skills/SkillSlotNames.cs.meta | 11 + Assets/Scripts/Spells/BaseGames.Spells.asmdef | 4 +- Assets/Scripts/Spells/SpellManager.cs | 109 + Assets/Scripts/Spells/SpellManager.cs.meta | 11 + Assets/Scripts/Spells/SpellSO.cs | 77 + Assets/Scripts/Spells/SpellSO.cs.meta | 11 + Assets/Scripts/Support.meta | 8 + Assets/Scripts/Support/Accessibility.meta | 8 + .../Accessibility/AccessibilityManager.cs | 89 + .../AccessibilityManager.cs.meta | 11 + .../Accessibility/AccessibilitySettingsSO.cs | 46 + .../AccessibilitySettingsSO.cs.meta | 11 + .../Support/Accessibility/ColorBlindFilter.cs | 137 + .../Accessibility/ColorBlindFilter.cs.meta | 11 + Assets/Scripts/Support/Analytics.meta | 8 + .../Support/Analytics/AnalyticsManager.cs | 134 + .../Analytics/AnalyticsManager.cs.meta | 11 + .../Support/Analytics/IAnalyticsService.cs | 21 + .../Analytics/IAnalyticsService.cs.meta | 11 + Assets/Scripts/Support/AntiSoftlock.meta | 8 + .../AntiSoftlock/AntiSoftlockSystem.cs | 140 + .../AntiSoftlock/AntiSoftlockSystem.cs.meta | 11 + .../Support/AntiSoftlock/HardAbilityGate.cs | 36 + .../AntiSoftlock/HardAbilityGate.cs.meta | 11 + .../Support/AntiSoftlock/RoomEscapeInfoSO.cs | 25 + .../AntiSoftlock/RoomEscapeInfoSO.cs.meta | 11 + .../Scripts/Support/BaseGames.Support.asmdef | 34 + .../Support/BaseGames.Support.asmdef.meta | 7 + Assets/Scripts/Support/Debug.meta | 8 + .../Scripts/Support/Debug/DebugCheatSystem.cs | 155 + .../Support/Debug/DebugCheatSystem.cs.meta | 11 + Assets/Scripts/Support/Localization.meta | 8 + .../Support/Localization/LanguageManagerSO.cs | 51 + .../Localization/LanguageManagerSO.cs.meta | 11 + .../Localization/LocalizationManager.cs | 59 + .../Localization/LocalizationManager.cs.meta | 11 + Assets/Scripts/Support/Platform.meta | 8 + .../Platform/BaseGames.Platform.asmdef | 17 + .../Platform/BaseGames.Platform.asmdef.meta | 2 +- .../Support/Platform/IPlatformService.cs | 54 + .../Support/Platform/IPlatformService.cs.meta | 11 + .../Support/Platform/NullPlatformService.cs | 46 + .../Platform/NullPlatformService.cs.meta | 11 + .../Support/Platform/PlatformBootstrap.cs | 42 + .../Platform/PlatformBootstrap.cs.meta | 11 + .../Support/Platform/SteamPlatformService.cs | 149 + .../Platform/SteamPlatformService.cs.meta | 11 + Assets/Scripts/Support/Speedrun.meta | 8 + .../Scripts/Support/Speedrun/SpeedrunTimer.cs | 104 + .../Support/Speedrun/SpeedrunTimer.cs.meta | 11 + .../Tutorial/BaseGames.Tutorial.asmdef | 6 +- .../Scripts/Tutorial/ContextualHintTrigger.cs | 53 + .../Tutorial/ContextualHintTrigger.cs.meta | 11 + Assets/Scripts/Tutorial/ITutorialService.cs | 17 + .../Scripts/Tutorial/ITutorialService.cs.meta | 11 + Assets/Scripts/Tutorial/TutorialHintUI.cs | 52 + .../Scripts/Tutorial/TutorialHintUI.cs.meta | 11 + Assets/Scripts/Tutorial/TutorialManager.cs | 76 + .../Scripts/Tutorial/TutorialManager.cs.meta | 11 + Assets/Scripts/UI/BaseGames.UI.asmdef | 7 +- Assets/Scripts/UI/FloatingDamageText.cs | 140 + Assets/Scripts/UI/FloatingDamageText.cs.meta | 11 + Assets/Scripts/UI/HUD/BossHPBar.cs | 110 + Assets/Scripts/UI/HUD/BossHPBar.cs.meta | 11 + Assets/Scripts/UI/HUD/HUDController.cs | 32 +- Assets/Scripts/UI/InputDeviceIconSetSO.cs | 31 + .../Scripts/UI/InputDeviceIconSetSO.cs.meta | 11 + Assets/Scripts/UI/InputDeviceIconSwitcher.cs | 68 + .../UI/InputDeviceIconSwitcher.cs.meta | 11 + Assets/Scripts/UI/LoadingOverlay.cs | 60 + Assets/Scripts/UI/LoadingOverlay.cs.meta | 11 + Assets/Scripts/UI/LoadingScreenManager.cs | 80 + .../Scripts/UI/LoadingScreenManager.cs.meta | 11 + .../Scripts/UI/Menus/DeathScreenController.cs | 11 +- .../Scripts/UI/Menus/PauseMenuController.cs | 59 + .../UI/Menus/PauseMenuController.cs.meta | 11 + Assets/Scripts/UI/Menus/SaveSlotController.cs | 141 + .../UI/Menus/SaveSlotController.cs.meta | 11 + .../UI/Menus/SettingsPanelController.cs | 66 + .../UI/Menus/SettingsPanelController.cs.meta | 11 + Assets/Scripts/UI/SaveIndicator.cs | 59 + Assets/Scripts/UI/SaveIndicator.cs.meta | 11 + Assets/Scripts/UI/Settings.meta | 8 + Assets/Scripts/UI/Settings/RebindActionRow.cs | 113 + .../UI/Settings/RebindActionRow.cs.meta | 11 + Assets/Scripts/UI/Settings/RebindPanel.cs | 67 + .../Scripts/UI/Settings/RebindPanel.cs.meta | 11 + Assets/Scripts/UI/ToastManager.cs | 126 + Assets/Scripts/UI/ToastManager.cs.meta | 11 + Assets/Scripts/UI/ToolHUD.cs | 74 + Assets/Scripts/UI/ToolHUD.cs.meta | 11 + Assets/Scripts/UI/UIManager.cs | 29 +- Assets/Scripts/VFX/BaseGames.VFX.asmdef | 5 +- Assets/Scripts/VFX/HitFXSpawner.cs | 19 +- Assets/Scripts/VFX/HurtFlashController.cs | 6 +- Assets/Scripts/VFX/IVFXPoolService.cs | 19 + Assets/Scripts/VFX/IVFXPoolService.cs.meta | 11 + Assets/Scripts/VFX/PaletteSwapSystem.cs | 85 + Assets/Scripts/VFX/PaletteSwapSystem.cs.meta | 11 + Assets/Scripts/VFX/PostProcessManager.cs | 135 + Assets/Scripts/VFX/PostProcessManager.cs.meta | 11 + Assets/Scripts/VFX/RegionLightController.cs | 114 + .../Scripts/VFX/RegionLightController.cs.meta | 11 + Assets/Scripts/VFX/VFXCatalogSO.cs | 17 +- Assets/Scripts/VFX/VFXPool.cs | 99 +- Assets/Scripts/World/AbilityGate.cs | 60 + Assets/Scripts/World/AbilityGate.cs.meta | 11 + Assets/Scripts/World/AbilityUnlock.cs | 57 + Assets/Scripts/World/AbilityUnlock.cs.meta | 11 + Assets/Scripts/World/BaseGames.World.asmdef | 10 +- Assets/Scripts/World/BreadcrumbTracker.cs | 61 + .../Scripts/World/BreadcrumbTracker.cs.meta | 11 + Assets/Scripts/World/Collectible.cs | 95 + Assets/Scripts/World/Collectible.cs.meta | 11 + Assets/Scripts/World/CollectibleSpawner.cs | 58 + .../Scripts/World/CollectibleSpawner.cs.meta | 11 + .../Scripts/World/CollectibleSpawnerConfig.cs | 18 + .../World/CollectibleSpawnerConfig.cs.meta | 11 + Assets/Scripts/World/CrumblePlatform.cs | 69 + Assets/Scripts/World/CrumblePlatform.cs.meta | 11 + Assets/Scripts/World/DeathShade.cs | 50 + Assets/Scripts/World/DeathShade.cs.meta | 11 + Assets/Scripts/World/DestructibleTile.cs | 63 + Assets/Scripts/World/DestructibleTile.cs.meta | 11 + .../Scripts/World/DirectionalDestructible.cs | 55 + .../World/DirectionalDestructible.cs.meta | 11 + .../Scripts/World/DirectionalInteractable.cs | 119 + .../World/DirectionalInteractable.cs.meta | 11 + Assets/Scripts/World/FalseWall.cs | 95 + Assets/Scripts/World/FalseWall.cs.meta | 11 + Assets/Scripts/World/HazardZone.cs | 42 + Assets/Scripts/World/HazardZone.cs.meta | 11 + Assets/Scripts/World/InteractableDetector.cs | 81 + .../World/InteractableDetector.cs.meta | 11 + Assets/Scripts/World/Liquid.meta | 8 + .../World/Liquid/LiquidPhysicsConfigSO.cs | 37 + .../Liquid/LiquidPhysicsConfigSO.cs.meta | 11 + Assets/Scripts/World/Liquid/LiquidType.cs | 12 + .../Scripts/World/Liquid/LiquidType.cs.meta | 11 + Assets/Scripts/World/Liquid/LiquidZone.cs | 54 + .../Scripts/World/Liquid/LiquidZone.cs.meta | 11 + .../UnderwaterPostProcessingController.cs | 56 + ...UnderwaterPostProcessingController.cs.meta | 11 + .../Scripts/World/Liquid/WaterDangerState.cs | 69 + .../World/Liquid/WaterDangerState.cs.meta | 11 + Assets/Scripts/World/MagicWall.cs | 26 + Assets/Scripts/World/MagicWall.cs.meta | 11 + .../World/Map/BaseGames.World.Map.asmdef | 4 +- Assets/Scripts/World/Map/IMapService.cs | 14 + Assets/Scripts/World/Map/IMapService.cs.meta | 11 + Assets/Scripts/World/Map/MapManager.cs | 96 + Assets/Scripts/World/Map/MapManager.cs.meta | 11 + Assets/Scripts/World/Map/MapPanel.cs | 123 + Assets/Scripts/World/Map/MapPanel.cs.meta | 11 + Assets/Scripts/World/Map/MapPin.cs | 52 + Assets/Scripts/World/Map/MapPin.cs.meta | 11 + Assets/Scripts/World/Map/MapPlayerTracker.cs | 51 + .../World/Map/MapPlayerTracker.cs.meta | 11 + Assets/Scripts/World/Map/MapRoomDataSO.cs | 72 + .../Scripts/World/Map/MapRoomDataSO.cs.meta | 11 + Assets/Scripts/World/MovingPlatform.cs | 144 + Assets/Scripts/World/MovingPlatform.cs.meta | 11 + Assets/Scripts/World/PhantomInteractable.cs | 19 + .../Scripts/World/PhantomInteractable.cs.meta | 11 + Assets/Scripts/World/PhantomPlate.cs | 74 + Assets/Scripts/World/PhantomPlate.cs.meta | 11 + Assets/Scripts/World/PlayerSpawnPoint.cs | 27 + Assets/Scripts/World/PlayerSpawnPoint.cs.meta | 11 + Assets/Scripts/World/Puzzle.meta | 8 + Assets/Scripts/World/Puzzle/PuzzleDoor.cs | 18 + .../Scripts/World/Puzzle/PuzzleDoor.cs.meta | 11 + .../Scripts/World/Puzzle/PuzzleInterfaces.cs | 33 + .../World/Puzzle/PuzzleInterfaces.cs.meta | 11 + Assets/Scripts/World/Puzzle/PuzzleReceiver.cs | 57 + .../World/Puzzle/PuzzleReceiver.cs.meta | 11 + Assets/Scripts/World/Puzzle/PuzzleSwitch.cs | 98 + .../Scripts/World/Puzzle/PuzzleSwitch.cs.meta | 11 + Assets/Scripts/World/Puzzle/PuzzleWire.cs | 51 + .../Scripts/World/Puzzle/PuzzleWire.cs.meta | 11 + Assets/Scripts/World/RoomController.cs | 38 + Assets/Scripts/World/RoomController.cs.meta | 11 + Assets/Scripts/World/RoomTransition.cs | 83 + Assets/Scripts/World/RoomTransition.cs.meta | 11 + .../World/Shop/BaseGames.World.Shop.asmdef | 6 +- Assets/Scripts/World/Shop/ShopController.cs | 179 + .../Scripts/World/Shop/ShopController.cs.meta | 11 + Assets/Scripts/World/Shop/ShopInventorySO.cs | 36 + .../World/Shop/ShopInventorySO.cs.meta | 11 + Assets/Scripts/World/Shop/ShopItemSO.cs | 42 + Assets/Scripts/World/Shop/ShopItemSO.cs.meta | 11 + Assets/Scripts/World/Shop/ShopNPC.cs | 62 + Assets/Scripts/World/Shop/ShopNPC.cs.meta | 11 + Assets/Scripts/World/SoftTerrain.cs | 13 + Assets/Scripts/World/SoftTerrain.cs.meta | 11 + Assets/Scripts/World/WorldMarker.cs | 68 + Assets/Scripts/World/WorldMarker.cs.meta | 11 + .../World/WorldMarkerEventChannelSO.cs | 12 + .../World/WorldMarkerEventChannelSO.cs.meta | 11 + Assets/Scripts/World/WorldStateRegistry.cs | 116 + .../Scripts/World/WorldStateRegistry.cs.meta | 11 + Assets/Settings/PlayerInputActions.cs | 1481 + Assets/Settings/PlayerInputActions.cs.meta | 11 + .../Settings/PlayerInputActions.inputactions | 561 +- .../PlayerInputActions.inputactions.meta | 2 +- Docs/Architecture/00_CoverageIndex.md | 20 +- Docs/Architecture/08_WorldModule.md | 49 +- Docs/Architecture/09_ProgressionModule.md | 107 +- Docs/Architecture/18_VFXFeedbackModule.md | 426 +- Docs/Architecture/20_ShieldModule.md | 37 +- Docs/Architecture/22_QuestChallengeModule.md | 13 +- Docs/Architecture/README.md | 8 +- Docs/Plan/00_DevelopmentPlan.md | 343 +- Docs/Plan/03_Phase2_CoreGameplay.md | 188 +- Docs/Plan/04_Phase3_WorldProgression.md | 136 +- Docs/Plan/05_Phase4_ContentPolish.md | 51 +- Docs/Review/AdvancedCodeReview.md | 583 + Docs/Review/ArchitectureDeepDive_2025.md | 722 + Docs/Review/CodeQualityReview.md | 470 + Docs/Review/CodeReview_2025_Full.md | 546 + Docs/Review/CommercialGradeReview_2026.md | 765 + Docs/Review/ComprehensiveCodeReview.md | 634 + Docs/Review/DeepDive_2026.md | 606 + Docs/Review/DeepDive_2026_Q2.md | 160 + Docs/Review/DeepDive_2026_Q3.md | 355 + Docs/Review/DeepDive_2026_Q4.md | 260 + Docs/Review/DeepDive_2026_Q5.md | 425 + Docs/Review/DeepDive_2026_Q6.md | 929 + Docs/Review/FinalReview_PostFix_2026.md | 728 + Docs/Review/FrameworkReview_2026_May.md | 528 + Docs/Review/FrameworkReview_2026_May_v2.md | 587 + Docs/Review/FrameworkReview_2026_May_v3.md | 174 + Docs/Review/FrameworkReview_2026_May_v4.md | 717 + Docs/Review/FrameworkReview_2026_May_v5.md | 559 + Docs/Review/FrameworkReview_2026_May_v6.md | 425 + Docs/Review/FrameworkReview_2026_May_v7.md | 503 + Docs/Review/FrameworkReview_Full.md | 925 + Docs/Review/FullCodeReview.md | 769 + Docs/Review/FullSystemReview_2026_Final.md | 938 + Docs/Review/MasterCodeReview.md | 549 + Docs/Review/MasterCodeReview_2026_Full.md | 853 + Docs/Review/MasterReview_2025_PostFix.md | 560 + ..._Code_Consistency_Assessment_2026-05-11.md | 743 + .../Verification/Phase1_Verification_Guide.md | 176 +- ProjectSettings/EditorBuildSettings.asset | 1 + ProjectSettings/Physics2DSettings.asset | 14 +- ProjectSettings/TagManager.asset | 16 +- zeling_v2.sln | 280 +- 805 files changed, 838724 insertions(+), 1905 deletions(-) create mode 100644 Assets/Data/Events/Boss/EVT_BossFightEnded.asset create mode 100644 Assets/Data/Events/Boss/EVT_BossFightEnded.asset.meta create mode 100644 Assets/Data/Events/Boss/EVT_BossFightStarted.asset create mode 100644 Assets/Data/Events/Boss/EVT_BossFightStarted.asset.meta create mode 100644 Assets/Data/Events/Combat/EVT_DeathScreenConfirmed.asset create mode 100644 Assets/Data/Events/Combat/EVT_DeathScreenConfirmed.asset.meta create mode 100644 Assets/Data/Events/Combat/EVT_PlayerRespawned.asset create mode 100644 Assets/Data/Events/Combat/EVT_PlayerRespawned.asset.meta create mode 100644 Assets/Data/Events/Combat/EVT_RespawnCompleted.asset create mode 100644 Assets/Data/Events/Combat/EVT_RespawnCompleted.asset.meta create mode 100644 Assets/Data/Events/Combat/EVT_RespawnStarted.asset create mode 100644 Assets/Data/Events/Combat/EVT_RespawnStarted.asset.meta create mode 100644 Assets/Data/Events/Core/EVT_FadeInRequest.asset create mode 100644 Assets/Data/Events/Core/EVT_FadeInRequest.asset.meta create mode 100644 Assets/Data/Events/Core/EVT_FadeOutRequest.asset create mode 100644 Assets/Data/Events/Core/EVT_FadeOutRequest.asset.meta create mode 100644 Assets/Data/Events/Core/EVT_GameStateChanged.asset create mode 100644 Assets/Data/Events/Core/EVT_GameStateChanged.asset.meta create mode 100644 Assets/Data/Events/Core/EVT_SceneLoaded.asset create mode 100644 Assets/Data/Events/Core/EVT_SceneLoaded.asset.meta create mode 100644 Assets/Data/Events/UI/EVT_FastTravelOpen.asset create mode 100644 Assets/Data/Events/UI/EVT_FastTravelOpen.asset.meta create mode 100644 Assets/Data/Events/UI/EVT_MapOpen.asset create mode 100644 Assets/Data/Events/UI/EVT_MapOpen.asset.meta create mode 100644 Assets/Data/Events/UI/EVT_ShopOpen.asset create mode 100644 Assets/Data/Events/UI/EVT_ShopOpen.asset.meta create mode 100644 Assets/Data/Events/World/EVT_SavePointActivated.asset create mode 100644 Assets/Data/Events/World/EVT_SavePointActivated.asset.meta create mode 100644 Assets/Data/Input.meta create mode 100644 Assets/Data/Input/InputReader.asset create mode 100644 Assets/Data/Input/InputReader.asset.meta create mode 100644 Assets/Data/Player/PLY_PlayerAnimationConfig.asset create mode 100644 Assets/Data/Player/PLY_PlayerAnimationConfig.asset.meta create mode 100644 Assets/Data/Player/PLY_PlayerMovementConfig.asset create mode 100644 Assets/Data/Player/PLY_PlayerMovementConfig.asset.meta create mode 100644 Assets/Data/Player/PLY_PlayerStats.asset create mode 100644 Assets/Data/Player/PLY_PlayerStats.asset.meta create mode 100644 Assets/Resources/Art.meta create mode 100644 Assets/Resources/Art/Builds.meta create mode 100644 Assets/Resources/Art/Effects.meta create mode 100644 Assets/Resources/Art/Effects/Textures.meta create mode 100644 Assets/Resources/Art/Effects/Textures/Fx_Ring03.png create mode 100644 Assets/Resources/Art/Effects/Textures/Fx_Ring03.png.meta create mode 100644 Assets/Scenes/TestRoom.unity create mode 100644 Assets/Scenes/TestRoom.unity.meta create mode 100644 Assets/Scenes/Testings.meta create mode 100644 Assets/Scenes/Testings/Build01.prefab create mode 100644 Assets/Scenes/Testings/Build01.prefab.meta create mode 100644 Assets/Scenes/Testings/Test.prefab create mode 100644 Assets/Scenes/Testings/Test.prefab.meta delete mode 100644 Assets/Scripts/Animation/.gitkeep create mode 100644 Assets/Scripts/Animation/AnimationEventBinder.cs rename Assets/Scripts/{Audio/_Placeholder.cs.meta => Animation/AnimationEventBinder.cs.meta} (83%) create mode 100644 Assets/Scripts/Animation/AnimationEventConfigSO.cs rename Assets/Scripts/{Combat/StatusEffects/_Placeholder.cs.meta => Animation/AnimationEventConfigSO.cs.meta} (83%) create mode 100644 Assets/Scripts/Animation/AnimationEventType.cs rename Assets/Scripts/Animation/{_Placeholder.cs.meta => AnimationEventType.cs.meta} (83%) create mode 100644 Assets/Scripts/Animation/EnemyAnimationEvents.cs rename Assets/Scripts/{Combat/_Placeholder.cs.meta => Animation/EnemyAnimationEvents.cs.meta} (83%) create mode 100644 Assets/Scripts/Animation/IAnimationEventHandler.cs create mode 100644 Assets/Scripts/Animation/IAnimationEventHandler.cs.meta create mode 100644 Assets/Scripts/Animation/PlayerAnimationEvents.cs create mode 100644 Assets/Scripts/Animation/PlayerAnimationEvents.cs.meta delete mode 100644 Assets/Scripts/Animation/_Placeholder.cs delete mode 100644 Assets/Scripts/Audio/.gitkeep create mode 100644 Assets/Scripts/Audio/AudioEventSO.cs create mode 100644 Assets/Scripts/Audio/AudioEventSO.cs.meta create mode 100644 Assets/Scripts/Audio/FootstepAudioConfigSO.cs create mode 100644 Assets/Scripts/Audio/FootstepAudioConfigSO.cs.meta create mode 100644 Assets/Scripts/Audio/FootstepMaterial.cs create mode 100644 Assets/Scripts/Audio/FootstepMaterial.cs.meta create mode 100644 Assets/Scripts/Audio/FootstepMaterialMarker.cs create mode 100644 Assets/Scripts/Audio/FootstepMaterialMarker.cs.meta create mode 100644 Assets/Scripts/Audio/GlobalSFXPlayer.cs create mode 100644 Assets/Scripts/Audio/GlobalSFXPlayer.cs.meta create mode 100644 Assets/Scripts/Audio/UnderwaterAudioController.cs create mode 100644 Assets/Scripts/Audio/UnderwaterAudioController.cs.meta delete mode 100644 Assets/Scripts/Audio/_Placeholder.cs create mode 100644 Assets/Scripts/Camera/ICameraService.cs create mode 100644 Assets/Scripts/Camera/ICameraService.cs.meta create mode 100644 Assets/Scripts/Combat/ArcProjectile.cs create mode 100644 Assets/Scripts/Combat/ArcProjectile.cs.meta create mode 100644 Assets/Scripts/Combat/ClashConfigSO.cs create mode 100644 Assets/Scripts/Combat/ClashConfigSO.cs.meta create mode 100644 Assets/Scripts/Combat/ClashResolver.cs create mode 100644 Assets/Scripts/Combat/ClashResolver.cs.meta create mode 100644 Assets/Scripts/Combat/HitStopManager.cs create mode 100644 Assets/Scripts/Combat/HitStopManager.cs.meta create mode 100644 Assets/Scripts/Combat/HomingProjectile.cs create mode 100644 Assets/Scripts/Combat/HomingProjectile.cs.meta create mode 100644 Assets/Scripts/Combat/IClashService.cs create mode 100644 Assets/Scripts/Combat/IClashService.cs.meta create mode 100644 Assets/Scripts/Combat/IProjectileService.cs create mode 100644 Assets/Scripts/Combat/IProjectileService.cs.meta create mode 100644 Assets/Scripts/Combat/LinearProjectile.cs create mode 100644 Assets/Scripts/Combat/LinearProjectile.cs.meta create mode 100644 Assets/Scripts/Combat/ParryableProjectile.cs create mode 100644 Assets/Scripts/Combat/ParryableProjectile.cs.meta create mode 100644 Assets/Scripts/Combat/PoiseWindowConfig.cs create mode 100644 Assets/Scripts/Combat/PoiseWindowConfig.cs.meta create mode 100644 Assets/Scripts/Combat/Projectile.cs create mode 100644 Assets/Scripts/Combat/Projectile.cs.meta create mode 100644 Assets/Scripts/Combat/ProjectileConfigSO.cs create mode 100644 Assets/Scripts/Combat/ProjectileConfigSO.cs.meta create mode 100644 Assets/Scripts/Combat/ProjectileManager.cs create mode 100644 Assets/Scripts/Combat/ProjectileManager.cs.meta create mode 100644 Assets/Scripts/Combat/ShieldConfigSO.cs create mode 100644 Assets/Scripts/Combat/ShieldConfigSO.cs.meta create mode 100644 Assets/Scripts/Combat/SkillHitBoxInstance.cs create mode 100644 Assets/Scripts/Combat/SkillHitBoxInstance.cs.meta delete mode 100644 Assets/Scripts/Combat/StatusEffects/.gitkeep create mode 100644 Assets/Scripts/Combat/StatusEffects/FireEffect.cs create mode 100644 Assets/Scripts/Combat/StatusEffects/FireEffect.cs.meta create mode 100644 Assets/Scripts/Combat/StatusEffects/PoisonEffect.cs create mode 100644 Assets/Scripts/Combat/StatusEffects/PoisonEffect.cs.meta create mode 100644 Assets/Scripts/Combat/StatusEffects/StaggerEffect.cs create mode 100644 Assets/Scripts/Combat/StatusEffects/StaggerEffect.cs.meta create mode 100644 Assets/Scripts/Combat/StatusEffects/StatusEffect.cs create mode 100644 Assets/Scripts/Combat/StatusEffects/StatusEffect.cs.meta create mode 100644 Assets/Scripts/Combat/StatusEffects/StatusEffectEventChannelSO.cs create mode 100644 Assets/Scripts/Combat/StatusEffects/StatusEffectEventChannelSO.cs.meta create mode 100644 Assets/Scripts/Combat/StatusEffects/StatusEffectType.cs create mode 100644 Assets/Scripts/Combat/StatusEffects/StatusEffectType.cs.meta delete mode 100644 Assets/Scripts/Combat/StatusEffects/_Placeholder.cs delete mode 100644 Assets/Scripts/Combat/_Placeholder.cs create mode 100644 Assets/Scripts/Core/Difficulty.meta create mode 100644 Assets/Scripts/Core/Difficulty/DifficultyManager.cs create mode 100644 Assets/Scripts/Core/Difficulty/DifficultyManager.cs.meta create mode 100644 Assets/Scripts/Core/Difficulty/DifficultyScalerSO.cs create mode 100644 Assets/Scripts/Core/Difficulty/DifficultyScalerSO.cs.meta create mode 100644 Assets/Scripts/Core/Difficulty/IDifficultyService.cs create mode 100644 Assets/Scripts/Core/Difficulty/IDifficultyService.cs.meta rename Assets/Scripts/Core/Events/{PrimitiveEventChannels.cs => BoolEventChannelSO.cs} (100%) create mode 100644 Assets/Scripts/Core/Events/BoolEventChannelSO.cs.meta delete mode 100644 Assets/Scripts/Core/Events/PrimitiveEventChannels.cs.meta rename Assets/Scripts/Core/{ => Events}/ServiceLocator.cs (70%) rename Assets/Scripts/Core/{ => Events}/ServiceLocator.cs.meta (100%) create mode 100644 Assets/Scripts/Core/GameIds.cs create mode 100644 Assets/Scripts/Core/GameIds.cs.meta create mode 100644 Assets/Scripts/Core/IObjectPoolService.cs create mode 100644 Assets/Scripts/Core/IObjectPoolService.cs.meta create mode 100644 Assets/Scripts/Core/ISettingsService.cs create mode 100644 Assets/Scripts/Core/ISettingsService.cs.meta create mode 100644 Assets/Scripts/Core/Save/CrashReporter.cs create mode 100644 Assets/Scripts/Core/Save/CrashReporter.cs.meta create mode 100644 Assets/Scripts/Core/Save/ISaveableRegistry.cs create mode 100644 Assets/Scripts/Core/Save/ISaveableRegistry.cs.meta create mode 100644 Assets/Scripts/Core/Save/SaveableMonoBehaviour.cs create mode 100644 Assets/Scripts/Core/Save/SaveableMonoBehaviour.cs.meta create mode 100644 Assets/Scripts/Core/Validation.meta create mode 100644 Assets/Scripts/Core/Validation/IValidatable.cs create mode 100644 Assets/Scripts/Core/Validation/IValidatable.cs.meta create mode 100644 Assets/Scripts/Cutscene/CutsceneManager.cs create mode 100644 Assets/Scripts/Cutscene/CutsceneManager.cs.meta create mode 100644 Assets/Scripts/Cutscene/CutsceneSO.cs create mode 100644 Assets/Scripts/Cutscene/CutsceneSO.cs.meta create mode 100644 Assets/Scripts/Cutscene/CutsceneTrigger.cs create mode 100644 Assets/Scripts/Cutscene/CutsceneTrigger.cs.meta create mode 100644 Assets/Scripts/Cutscene/SignalEmitterClip.cs create mode 100644 Assets/Scripts/Cutscene/SignalEmitterClip.cs.meta create mode 100644 Assets/Scripts/Dialogue/DialogueManager.cs create mode 100644 Assets/Scripts/Dialogue/DialogueManager.cs.meta create mode 100644 Assets/Scripts/Dialogue/DialogueSequenceSO.cs create mode 100644 Assets/Scripts/Dialogue/DialogueSequenceSO.cs.meta create mode 100644 Assets/Scripts/Dialogue/DialogueUI.cs create mode 100644 Assets/Scripts/Dialogue/DialogueUI.cs.meta create mode 100644 Assets/Scripts/Dialogue/IDialogueService.cs create mode 100644 Assets/Scripts/Dialogue/IDialogueService.cs.meta create mode 100644 Assets/Scripts/Dialogue/InteractableNPC.cs create mode 100644 Assets/Scripts/Dialogue/InteractableNPC.cs.meta create mode 100644 Assets/Scripts/Dialogue/InteractionPromptController.cs create mode 100644 Assets/Scripts/Dialogue/InteractionPromptController.cs.meta create mode 100644 Assets/Scripts/Dialogue/NarrativeNPC.cs create mode 100644 Assets/Scripts/Dialogue/NarrativeNPC.cs.meta delete mode 100644 Assets/Scripts/Editor/.gitkeep create mode 100644 Assets/Scripts/Editor/Achievements.meta create mode 100644 Assets/Scripts/Editor/Achievements/AchievementSOEditor.cs create mode 100644 Assets/Scripts/Editor/Achievements/AchievementSOEditor.cs.meta create mode 100644 Assets/Scripts/Editor/AddressReferenceGraphWindow.cs create mode 100644 Assets/Scripts/Editor/AddressReferenceGraphWindow.cs.meta create mode 100644 Assets/Scripts/Editor/BossSkillSequenceWindow.cs create mode 100644 Assets/Scripts/Editor/BossSkillSequenceWindow.cs.meta create mode 100644 Assets/Scripts/Editor/Combat.meta create mode 100644 Assets/Scripts/Editor/Combat/HurtBoxEditor.cs create mode 100644 Assets/Scripts/Editor/Combat/HurtBoxEditor.cs.meta create mode 100644 Assets/Scripts/Editor/Equipment.meta create mode 100644 Assets/Scripts/Editor/Equipment/CharmEffectDrawer.cs create mode 100644 Assets/Scripts/Editor/Equipment/CharmEffectDrawer.cs.meta create mode 100644 Assets/Scripts/Editor/EventBusMonitorWindow.cs create mode 100644 Assets/Scripts/Editor/EventBusMonitorWindow.cs.meta create mode 100644 Assets/Scripts/Editor/EventChainEditorWindow.cs create mode 100644 Assets/Scripts/Editor/EventChainEditorWindow.cs.meta create mode 100644 Assets/Scripts/Editor/EventChannelEditor.cs create mode 100644 Assets/Scripts/Editor/EventChannelEditor.cs.meta create mode 100644 Assets/Scripts/Editor/EventConfigEditor.cs create mode 100644 Assets/Scripts/Editor/EventConfigEditor.cs.meta create mode 100644 Assets/Scripts/Editor/Map.meta create mode 100644 Assets/Scripts/Editor/Map/MapRoomDataEditor.cs create mode 100644 Assets/Scripts/Editor/Map/MapRoomDataEditor.cs.meta create mode 100644 Assets/Scripts/Editor/NavSurfaceBakeShortcut.cs create mode 100644 Assets/Scripts/Editor/NavSurfaceBakeShortcut.cs.meta create mode 100644 Assets/Scripts/Editor/SceneScaffoldTools.cs create mode 100644 Assets/Scripts/Editor/SceneScaffoldTools.cs.meta create mode 100644 Assets/Scripts/Editor/ScriptExecutionOrderTools.cs create mode 100644 Assets/Scripts/Editor/ScriptExecutionOrderTools.cs.meta create mode 100644 Assets/Scripts/Editor/Validation.meta create mode 100644 Assets/Scripts/Editor/Validation/SOValidationRunner.cs create mode 100644 Assets/Scripts/Editor/Validation/SOValidationRunner.cs.meta create mode 100644 Assets/Scripts/Editor/World.meta create mode 100644 Assets/Scripts/Editor/World/DestructibleTileEditor.cs create mode 100644 Assets/Scripts/Editor/World/DestructibleTileEditor.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_CanAttack.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_CanAttack.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_EnterPhase.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_EnterPhase.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_FaceTarget.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_FaceTarget.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_IsGrounded.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_IsGrounded.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_IsHPBelow.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_IsHPBelow.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_IsNearEdge.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_IsNearEdge.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_IsPlayerVisible.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_IsPlayerVisible.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_IsStateMatch.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_IsStateMatch.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_JumpTo.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_JumpTo.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_MoveTo.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_MoveTo.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_PlayAnimation.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_PlayAnimation.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_SetAlert.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_SetAlert.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_SpawnProjectile.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_SpawnProjectile.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_StopMovement.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_StopMovement.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_SummonMinions.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_SummonMinions.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_TelegraphAttack.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_TelegraphAttack.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_TeleportTo.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_TeleportTo.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_Wait.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_Wait.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_WaitForAnimation.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_WaitForAnimation.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BD_WaitRandom.cs create mode 100644 Assets/Scripts/Enemies/AI/BD_WaitRandom.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/BatchLOSSystem.cs create mode 100644 Assets/Scripts/Enemies/AI/BatchLOSSystem.cs.meta create mode 100644 Assets/Scripts/Enemies/AI/ILOSRequester.cs create mode 100644 Assets/Scripts/Enemies/AI/ILOSRequester.cs.meta create mode 100644 Assets/Scripts/Enemies/Boss/AttackPatternSO.cs create mode 100644 Assets/Scripts/Enemies/Boss/AttackPatternSO.cs.meta create mode 100644 Assets/Scripts/Enemies/Boss/BossBase.cs create mode 100644 Assets/Scripts/Enemies/Boss/BossBase.cs.meta create mode 100644 Assets/Scripts/Enemies/Boss/BossResourceConfigSO.cs create mode 100644 Assets/Scripts/Enemies/Boss/BossResourceConfigSO.cs.meta create mode 100644 Assets/Scripts/Enemies/Boss/BossSkillExecutor.cs create mode 100644 Assets/Scripts/Enemies/Boss/BossSkillExecutor.cs.meta create mode 100644 Assets/Scripts/Enemies/Boss/BossSkillSO.cs create mode 100644 Assets/Scripts/Enemies/Boss/BossSkillSO.cs.meta create mode 100644 Assets/Scripts/Enemies/Boss/BossSkillTypes.cs create mode 100644 Assets/Scripts/Enemies/Boss/BossSkillTypes.cs.meta create mode 100644 Assets/Scripts/Enemies/Boss/Patterns/TelegraphSystem.cs create mode 100644 Assets/Scripts/Enemies/Boss/Patterns/TelegraphSystem.cs.meta create mode 100644 Assets/Scripts/Enemies/Boss/SkillSequenceSO.cs create mode 100644 Assets/Scripts/Enemies/Boss/SkillSequenceSO.cs.meta create mode 100644 Assets/Scripts/Enemies/Boss/WeakPointSystem.cs create mode 100644 Assets/Scripts/Enemies/Boss/WeakPointSystem.cs.meta create mode 100644 Assets/Scripts/Enemies/EnemyPoiseComponent.cs create mode 100644 Assets/Scripts/Enemies/EnemyPoiseComponent.cs.meta create mode 100644 Assets/Scripts/Enemies/EnemyQuotaManager.cs create mode 100644 Assets/Scripts/Enemies/EnemyQuotaManager.cs.meta create mode 100644 Assets/Scripts/Enemies/FlyingEnemy.cs create mode 100644 Assets/Scripts/Enemies/FlyingEnemy.cs.meta create mode 100644 Assets/Scripts/Enemies/ILOSRequester.cs create mode 100644 Assets/Scripts/Enemies/ILOSRequester.cs.meta create mode 100644 Assets/Scripts/Enemies/LootResolver.cs create mode 100644 Assets/Scripts/Enemies/LootResolver.cs.meta create mode 100644 Assets/Scripts/Enemies/LootTableSO.cs create mode 100644 Assets/Scripts/Enemies/LootTableSO.cs.meta create mode 100644 Assets/Scripts/Enemies/RangedEnemy.cs create mode 100644 Assets/Scripts/Enemies/RangedEnemy.cs.meta create mode 100644 Assets/Scripts/Enemies/States.meta create mode 100644 Assets/Scripts/Enemies/States/EnemyControlledState.cs create mode 100644 Assets/Scripts/Enemies/States/EnemyControlledState.cs.meta create mode 100644 Assets/Scripts/Enemies/States/EnemyDeadState.cs create mode 100644 Assets/Scripts/Enemies/States/EnemyDeadState.cs.meta create mode 100644 Assets/Scripts/Enemies/States/EnemyHurtState.cs create mode 100644 Assets/Scripts/Enemies/States/EnemyHurtState.cs.meta create mode 100644 Assets/Scripts/Enemies/States/EnemyStaggerState.cs create mode 100644 Assets/Scripts/Enemies/States/EnemyStaggerState.cs.meta create mode 100644 Assets/Scripts/Enemies/States/IEnemyState.cs create mode 100644 Assets/Scripts/Enemies/States/IEnemyState.cs.meta create mode 100644 Assets/Scripts/Equipment/CharmCatalogSO.cs create mode 100644 Assets/Scripts/Equipment/CharmCatalogSO.cs.meta create mode 100644 Assets/Scripts/Equipment/CharmSO.cs create mode 100644 Assets/Scripts/Equipment/CharmSO.cs.meta create mode 100644 Assets/Scripts/Equipment/Effects.meta create mode 100644 Assets/Scripts/Equipment/Effects/AttackSpeedEffect.cs create mode 100644 Assets/Scripts/Equipment/Effects/AttackSpeedEffect.cs.meta create mode 100644 Assets/Scripts/Equipment/Effects/OnHitEffect.cs create mode 100644 Assets/Scripts/Equipment/Effects/OnHitEffect.cs.meta create mode 100644 Assets/Scripts/Equipment/Effects/SkillNumericModifierEffect.cs create mode 100644 Assets/Scripts/Equipment/Effects/SkillNumericModifierEffect.cs.meta create mode 100644 Assets/Scripts/Equipment/Effects/SkillSlotOverrideEffect.cs create mode 100644 Assets/Scripts/Equipment/Effects/SkillSlotOverrideEffect.cs.meta create mode 100644 Assets/Scripts/Equipment/Effects/SoulSpellEffect.cs create mode 100644 Assets/Scripts/Equipment/Effects/SoulSpellEffect.cs.meta create mode 100644 Assets/Scripts/Equipment/Effects/StatModifierEffect.cs create mode 100644 Assets/Scripts/Equipment/Effects/StatModifierEffect.cs.meta create mode 100644 Assets/Scripts/Equipment/Effects/WeaponOverrideEffect.cs create mode 100644 Assets/Scripts/Equipment/Effects/WeaponOverrideEffect.cs.meta create mode 100644 Assets/Scripts/Equipment/EquipmentConfigSO.cs create mode 100644 Assets/Scripts/Equipment/EquipmentConfigSO.cs.meta create mode 100644 Assets/Scripts/Equipment/EquipmentManager.cs create mode 100644 Assets/Scripts/Equipment/EquipmentManager.cs.meta create mode 100644 Assets/Scripts/Equipment/ICharmEffect.cs create mode 100644 Assets/Scripts/Equipment/ICharmEffect.cs.meta create mode 100644 Assets/Scripts/Equipment/ToolCatalogSO.cs create mode 100644 Assets/Scripts/Equipment/ToolCatalogSO.cs.meta create mode 100644 Assets/Scripts/Equipment/ToolSO.cs create mode 100644 Assets/Scripts/Equipment/ToolSO.cs.meta create mode 100644 Assets/Scripts/Equipment/ToolSlotManager.cs create mode 100644 Assets/Scripts/Equipment/ToolSlotManager.cs.meta create mode 100644 Assets/Scripts/EventChain.meta rename Assets/Scripts/{Platform/BaseGames.Platform.asmdef => EventChain/BaseGames.EventChain.asmdef} (64%) create mode 100644 Assets/Scripts/EventChain/BaseGames.EventChain.asmdef.meta create mode 100644 Assets/Scripts/EventChain/EventChainManager.cs create mode 100644 Assets/Scripts/EventChain/EventChainManager.cs.meta create mode 100644 Assets/Scripts/EventChain/EventChainSO.cs create mode 100644 Assets/Scripts/EventChain/EventChainSO.cs.meta delete mode 100644 Assets/Scripts/Input/.gitkeep create mode 100644 Assets/Scripts/Input/ConflictDetector.cs create mode 100644 Assets/Scripts/Input/ConflictDetector.cs.meta create mode 100644 Assets/Scripts/Input/InputReaderBootstrap.cs create mode 100644 Assets/Scripts/Input/InputReaderBootstrap.cs.meta delete mode 100644 Assets/Scripts/Input/_Placeholder.cs delete mode 100644 Assets/Scripts/Input/_Placeholder.cs.meta create mode 100644 Assets/Scripts/Localization/ILocalizationService.cs create mode 100644 Assets/Scripts/Localization/ILocalizationService.cs.meta create mode 100644 Assets/Scripts/Localization/Language.cs create mode 100644 Assets/Scripts/Localization/Language.cs.meta create mode 100644 Assets/Scripts/Localization/LanguageEventChannelSO.cs create mode 100644 Assets/Scripts/Localization/LanguageEventChannelSO.cs.meta create mode 100644 Assets/Scripts/Localization/LocalizationManager.cs create mode 100644 Assets/Scripts/Localization/LocalizationManager.cs.meta create mode 100644 Assets/Scripts/Parry/ParryConfigSO.cs create mode 100644 Assets/Scripts/Parry/ParryConfigSO.cs.meta create mode 100644 Assets/Scripts/Parry/ParryInfo.cs create mode 100644 Assets/Scripts/Parry/ParryInfo.cs.meta create mode 100644 Assets/Scripts/Parry/ParryInfoEventChannelSO.cs create mode 100644 Assets/Scripts/Parry/ParryInfoEventChannelSO.cs.meta delete mode 100644 Assets/Scripts/Platform/_Placeholder.cs delete mode 100644 Assets/Scripts/Platform/_Placeholder.cs.meta create mode 100644 Assets/Scripts/Player/FormSO.cs create mode 100644 Assets/Scripts/Player/FormSO.cs.meta create mode 100644 Assets/Scripts/Player/FormType.cs create mode 100644 Assets/Scripts/Player/FormType.cs.meta create mode 100644 Assets/Scripts/Player/PlayerWallDetector.cs create mode 100644 Assets/Scripts/Player/PlayerWallDetector.cs.meta create mode 100644 Assets/Scripts/Player/StatType.cs create mode 100644 Assets/Scripts/Player/StatType.cs.meta create mode 100644 Assets/Scripts/Player/States/AerialDashState.cs create mode 100644 Assets/Scripts/Player/States/AerialDashState.cs.meta create mode 100644 Assets/Scripts/Player/States/AirAttackState.cs create mode 100644 Assets/Scripts/Player/States/AirAttackState.cs.meta create mode 100644 Assets/Scripts/Player/States/DashState.cs create mode 100644 Assets/Scripts/Player/States/DashState.cs.meta create mode 100644 Assets/Scripts/Player/States/DeadState.cs create mode 100644 Assets/Scripts/Player/States/DeadState.cs.meta create mode 100644 Assets/Scripts/Player/States/DownAttackState.cs create mode 100644 Assets/Scripts/Player/States/DownAttackState.cs.meta create mode 100644 Assets/Scripts/Player/States/HurtState.cs create mode 100644 Assets/Scripts/Player/States/HurtState.cs.meta create mode 100644 Assets/Scripts/Player/States/ParryState.cs create mode 100644 Assets/Scripts/Player/States/ParryState.cs.meta create mode 100644 Assets/Scripts/Player/States/SpringState.cs create mode 100644 Assets/Scripts/Player/States/SpringState.cs.meta create mode 100644 Assets/Scripts/Player/States/SwimState.cs create mode 100644 Assets/Scripts/Player/States/SwimState.cs.meta create mode 100644 Assets/Scripts/Player/States/UpAttackState.cs create mode 100644 Assets/Scripts/Player/States/UpAttackState.cs.meta create mode 100644 Assets/Scripts/Player/States/WallJumpState.cs create mode 100644 Assets/Scripts/Player/States/WallJumpState.cs.meta create mode 100644 Assets/Scripts/Player/States/WallSlideState.cs create mode 100644 Assets/Scripts/Player/States/WallSlideState.cs.meta create mode 100644 Assets/Scripts/Progression/Achievement.meta create mode 100644 Assets/Scripts/Progression/Achievement/CollectedAllCharmsCondition.cs create mode 100644 Assets/Scripts/Progression/Achievement/CollectedAllCharmsCondition.cs.meta create mode 100644 Assets/Scripts/Progression/Achievement/CollectedItemCondition.cs create mode 100644 Assets/Scripts/Progression/Achievement/CollectedItemCondition.cs.meta create mode 100644 Assets/Scripts/Progression/Achievement/DefeatedAllBossesCondition.cs create mode 100644 Assets/Scripts/Progression/Achievement/DefeatedAllBossesCondition.cs.meta create mode 100644 Assets/Scripts/Progression/Achievement/DefeatedBossCondition.cs create mode 100644 Assets/Scripts/Progression/Achievement/DefeatedBossCondition.cs.meta create mode 100644 Assets/Scripts/Progression/Achievement/EnteredRegionCondition.cs create mode 100644 Assets/Scripts/Progression/Achievement/EnteredRegionCondition.cs.meta create mode 100644 Assets/Scripts/Progression/Achievement/EventTriggeredCondition.cs create mode 100644 Assets/Scripts/Progression/Achievement/EventTriggeredCondition.cs.meta create mode 100644 Assets/Scripts/Progression/Achievement/MapExplorationCondition.cs create mode 100644 Assets/Scripts/Progression/Achievement/MapExplorationCondition.cs.meta create mode 100644 Assets/Scripts/Progression/Achievement/NailClashCountCondition.cs create mode 100644 Assets/Scripts/Progression/Achievement/NailClashCountCondition.cs.meta create mode 100644 Assets/Scripts/Progression/Achievement/NoHealRunCondition.cs create mode 100644 Assets/Scripts/Progression/Achievement/NoHealRunCondition.cs.meta create mode 100644 Assets/Scripts/Progression/Achievement/ParryCountCondition.cs create mode 100644 Assets/Scripts/Progression/Achievement/ParryCountCondition.cs.meta create mode 100644 Assets/Scripts/Progression/Achievement/TimedBossKillCondition.cs create mode 100644 Assets/Scripts/Progression/Achievement/TimedBossKillCondition.cs.meta create mode 100644 Assets/Scripts/Progression/Achievement/UnlockedAllAbilitiesCondition.cs create mode 100644 Assets/Scripts/Progression/Achievement/UnlockedAllAbilitiesCondition.cs.meta create mode 100644 Assets/Scripts/Progression/AchievementCondition.cs create mode 100644 Assets/Scripts/Progression/AchievementCondition.cs.meta create mode 100644 Assets/Scripts/Progression/AchievementManager.cs create mode 100644 Assets/Scripts/Progression/AchievementManager.cs.meta create mode 100644 Assets/Scripts/Progression/BossProgressTracker.cs create mode 100644 Assets/Scripts/Progression/BossProgressTracker.cs.meta create mode 100644 Assets/Scripts/Progression/HPContainerPickup.cs create mode 100644 Assets/Scripts/Progression/HPContainerPickup.cs.meta create mode 100644 Assets/Scripts/Progression/IAchievementService.cs create mode 100644 Assets/Scripts/Progression/IAchievementService.cs.meta create mode 100644 Assets/Scripts/Progression/ProgressLock.cs create mode 100644 Assets/Scripts/Progression/ProgressLock.cs.meta create mode 100644 Assets/Scripts/Progression/RegionDefinitionSO.cs create mode 100644 Assets/Scripts/Progression/RegionDefinitionSO.cs.meta create mode 100644 Assets/Scripts/Quest.meta create mode 100644 Assets/Scripts/Quest/BaseGames.Quest.asmdef create mode 100644 Assets/Scripts/Quest/BaseGames.Quest.asmdef.meta create mode 100644 Assets/Scripts/Quest/BossRushSequenceSO.cs create mode 100644 Assets/Scripts/Quest/BossRushSequenceSO.cs.meta create mode 100644 Assets/Scripts/Quest/ChallengeEncounterSO.cs create mode 100644 Assets/Scripts/Quest/ChallengeEncounterSO.cs.meta create mode 100644 Assets/Scripts/Quest/ChallengeRoomManager.cs create mode 100644 Assets/Scripts/Quest/ChallengeRoomManager.cs.meta create mode 100644 Assets/Scripts/Quest/ChallengeRoomSO.cs create mode 100644 Assets/Scripts/Quest/ChallengeRoomSO.cs.meta create mode 100644 Assets/Scripts/Quest/ChallengeRoomTrigger.cs create mode 100644 Assets/Scripts/Quest/ChallengeRoomTrigger.cs.meta create mode 100644 Assets/Scripts/Quest/IQuestManager.cs create mode 100644 Assets/Scripts/Quest/IQuestManager.cs.meta create mode 100644 Assets/Scripts/Quest/QuestGiver.cs create mode 100644 Assets/Scripts/Quest/QuestGiver.cs.meta create mode 100644 Assets/Scripts/Quest/QuestManager.cs create mode 100644 Assets/Scripts/Quest/QuestManager.cs.meta create mode 100644 Assets/Scripts/Quest/QuestObjectiveSO.cs create mode 100644 Assets/Scripts/Quest/QuestObjectiveSO.cs.meta create mode 100644 Assets/Scripts/Quest/QuestSO.cs create mode 100644 Assets/Scripts/Quest/QuestSO.cs.meta create mode 100644 Assets/Scripts/Quest/RewardSO.cs create mode 100644 Assets/Scripts/Quest/RewardSO.cs.meta create mode 100644 Assets/Scripts/Skills.meta create mode 100644 Assets/Scripts/Skills/BaseGames.Skills.asmdef create mode 100644 Assets/Scripts/Skills/BaseGames.Skills.asmdef.meta create mode 100644 Assets/Scripts/Skills/FormSkillSO.cs create mode 100644 Assets/Scripts/Skills/FormSkillSO.cs.meta create mode 100644 Assets/Scripts/Skills/SkillManager.cs create mode 100644 Assets/Scripts/Skills/SkillManager.cs.meta create mode 100644 Assets/Scripts/Skills/SkillModifierRegistry.cs create mode 100644 Assets/Scripts/Skills/SkillModifierRegistry.cs.meta create mode 100644 Assets/Scripts/Skills/SkillSlotNames.cs create mode 100644 Assets/Scripts/Skills/SkillSlotNames.cs.meta create mode 100644 Assets/Scripts/Spells/SpellManager.cs create mode 100644 Assets/Scripts/Spells/SpellManager.cs.meta create mode 100644 Assets/Scripts/Spells/SpellSO.cs create mode 100644 Assets/Scripts/Spells/SpellSO.cs.meta create mode 100644 Assets/Scripts/Support.meta create mode 100644 Assets/Scripts/Support/Accessibility.meta create mode 100644 Assets/Scripts/Support/Accessibility/AccessibilityManager.cs create mode 100644 Assets/Scripts/Support/Accessibility/AccessibilityManager.cs.meta create mode 100644 Assets/Scripts/Support/Accessibility/AccessibilitySettingsSO.cs create mode 100644 Assets/Scripts/Support/Accessibility/AccessibilitySettingsSO.cs.meta create mode 100644 Assets/Scripts/Support/Accessibility/ColorBlindFilter.cs create mode 100644 Assets/Scripts/Support/Accessibility/ColorBlindFilter.cs.meta create mode 100644 Assets/Scripts/Support/Analytics.meta create mode 100644 Assets/Scripts/Support/Analytics/AnalyticsManager.cs create mode 100644 Assets/Scripts/Support/Analytics/AnalyticsManager.cs.meta create mode 100644 Assets/Scripts/Support/Analytics/IAnalyticsService.cs create mode 100644 Assets/Scripts/Support/Analytics/IAnalyticsService.cs.meta create mode 100644 Assets/Scripts/Support/AntiSoftlock.meta create mode 100644 Assets/Scripts/Support/AntiSoftlock/AntiSoftlockSystem.cs create mode 100644 Assets/Scripts/Support/AntiSoftlock/AntiSoftlockSystem.cs.meta create mode 100644 Assets/Scripts/Support/AntiSoftlock/HardAbilityGate.cs create mode 100644 Assets/Scripts/Support/AntiSoftlock/HardAbilityGate.cs.meta create mode 100644 Assets/Scripts/Support/AntiSoftlock/RoomEscapeInfoSO.cs create mode 100644 Assets/Scripts/Support/AntiSoftlock/RoomEscapeInfoSO.cs.meta create mode 100644 Assets/Scripts/Support/BaseGames.Support.asmdef create mode 100644 Assets/Scripts/Support/BaseGames.Support.asmdef.meta create mode 100644 Assets/Scripts/Support/Debug.meta create mode 100644 Assets/Scripts/Support/Debug/DebugCheatSystem.cs create mode 100644 Assets/Scripts/Support/Debug/DebugCheatSystem.cs.meta create mode 100644 Assets/Scripts/Support/Localization.meta create mode 100644 Assets/Scripts/Support/Localization/LanguageManagerSO.cs create mode 100644 Assets/Scripts/Support/Localization/LanguageManagerSO.cs.meta create mode 100644 Assets/Scripts/Support/Localization/LocalizationManager.cs create mode 100644 Assets/Scripts/Support/Localization/LocalizationManager.cs.meta create mode 100644 Assets/Scripts/Support/Platform.meta create mode 100644 Assets/Scripts/Support/Platform/BaseGames.Platform.asmdef rename Assets/Scripts/{ => Support}/Platform/BaseGames.Platform.asmdef.meta (76%) create mode 100644 Assets/Scripts/Support/Platform/IPlatformService.cs create mode 100644 Assets/Scripts/Support/Platform/IPlatformService.cs.meta create mode 100644 Assets/Scripts/Support/Platform/NullPlatformService.cs create mode 100644 Assets/Scripts/Support/Platform/NullPlatformService.cs.meta create mode 100644 Assets/Scripts/Support/Platform/PlatformBootstrap.cs create mode 100644 Assets/Scripts/Support/Platform/PlatformBootstrap.cs.meta create mode 100644 Assets/Scripts/Support/Platform/SteamPlatformService.cs create mode 100644 Assets/Scripts/Support/Platform/SteamPlatformService.cs.meta create mode 100644 Assets/Scripts/Support/Speedrun.meta create mode 100644 Assets/Scripts/Support/Speedrun/SpeedrunTimer.cs create mode 100644 Assets/Scripts/Support/Speedrun/SpeedrunTimer.cs.meta create mode 100644 Assets/Scripts/Tutorial/ContextualHintTrigger.cs create mode 100644 Assets/Scripts/Tutorial/ContextualHintTrigger.cs.meta create mode 100644 Assets/Scripts/Tutorial/ITutorialService.cs create mode 100644 Assets/Scripts/Tutorial/ITutorialService.cs.meta create mode 100644 Assets/Scripts/Tutorial/TutorialHintUI.cs create mode 100644 Assets/Scripts/Tutorial/TutorialHintUI.cs.meta create mode 100644 Assets/Scripts/Tutorial/TutorialManager.cs create mode 100644 Assets/Scripts/Tutorial/TutorialManager.cs.meta create mode 100644 Assets/Scripts/UI/FloatingDamageText.cs create mode 100644 Assets/Scripts/UI/FloatingDamageText.cs.meta create mode 100644 Assets/Scripts/UI/HUD/BossHPBar.cs create mode 100644 Assets/Scripts/UI/HUD/BossHPBar.cs.meta create mode 100644 Assets/Scripts/UI/InputDeviceIconSetSO.cs create mode 100644 Assets/Scripts/UI/InputDeviceIconSetSO.cs.meta create mode 100644 Assets/Scripts/UI/InputDeviceIconSwitcher.cs create mode 100644 Assets/Scripts/UI/InputDeviceIconSwitcher.cs.meta create mode 100644 Assets/Scripts/UI/LoadingOverlay.cs create mode 100644 Assets/Scripts/UI/LoadingOverlay.cs.meta create mode 100644 Assets/Scripts/UI/LoadingScreenManager.cs create mode 100644 Assets/Scripts/UI/LoadingScreenManager.cs.meta create mode 100644 Assets/Scripts/UI/Menus/PauseMenuController.cs create mode 100644 Assets/Scripts/UI/Menus/PauseMenuController.cs.meta create mode 100644 Assets/Scripts/UI/Menus/SaveSlotController.cs create mode 100644 Assets/Scripts/UI/Menus/SaveSlotController.cs.meta create mode 100644 Assets/Scripts/UI/Menus/SettingsPanelController.cs create mode 100644 Assets/Scripts/UI/Menus/SettingsPanelController.cs.meta create mode 100644 Assets/Scripts/UI/SaveIndicator.cs create mode 100644 Assets/Scripts/UI/SaveIndicator.cs.meta create mode 100644 Assets/Scripts/UI/Settings.meta create mode 100644 Assets/Scripts/UI/Settings/RebindActionRow.cs create mode 100644 Assets/Scripts/UI/Settings/RebindActionRow.cs.meta create mode 100644 Assets/Scripts/UI/Settings/RebindPanel.cs create mode 100644 Assets/Scripts/UI/Settings/RebindPanel.cs.meta create mode 100644 Assets/Scripts/UI/ToastManager.cs create mode 100644 Assets/Scripts/UI/ToastManager.cs.meta create mode 100644 Assets/Scripts/UI/ToolHUD.cs create mode 100644 Assets/Scripts/UI/ToolHUD.cs.meta create mode 100644 Assets/Scripts/VFX/IVFXPoolService.cs create mode 100644 Assets/Scripts/VFX/IVFXPoolService.cs.meta create mode 100644 Assets/Scripts/VFX/PaletteSwapSystem.cs create mode 100644 Assets/Scripts/VFX/PaletteSwapSystem.cs.meta create mode 100644 Assets/Scripts/VFX/PostProcessManager.cs create mode 100644 Assets/Scripts/VFX/PostProcessManager.cs.meta create mode 100644 Assets/Scripts/VFX/RegionLightController.cs create mode 100644 Assets/Scripts/VFX/RegionLightController.cs.meta create mode 100644 Assets/Scripts/World/AbilityGate.cs create mode 100644 Assets/Scripts/World/AbilityGate.cs.meta create mode 100644 Assets/Scripts/World/AbilityUnlock.cs create mode 100644 Assets/Scripts/World/AbilityUnlock.cs.meta create mode 100644 Assets/Scripts/World/BreadcrumbTracker.cs create mode 100644 Assets/Scripts/World/BreadcrumbTracker.cs.meta create mode 100644 Assets/Scripts/World/Collectible.cs create mode 100644 Assets/Scripts/World/Collectible.cs.meta create mode 100644 Assets/Scripts/World/CollectibleSpawner.cs create mode 100644 Assets/Scripts/World/CollectibleSpawner.cs.meta create mode 100644 Assets/Scripts/World/CollectibleSpawnerConfig.cs create mode 100644 Assets/Scripts/World/CollectibleSpawnerConfig.cs.meta create mode 100644 Assets/Scripts/World/CrumblePlatform.cs create mode 100644 Assets/Scripts/World/CrumblePlatform.cs.meta create mode 100644 Assets/Scripts/World/DeathShade.cs create mode 100644 Assets/Scripts/World/DeathShade.cs.meta create mode 100644 Assets/Scripts/World/DestructibleTile.cs create mode 100644 Assets/Scripts/World/DestructibleTile.cs.meta create mode 100644 Assets/Scripts/World/DirectionalDestructible.cs create mode 100644 Assets/Scripts/World/DirectionalDestructible.cs.meta create mode 100644 Assets/Scripts/World/DirectionalInteractable.cs create mode 100644 Assets/Scripts/World/DirectionalInteractable.cs.meta create mode 100644 Assets/Scripts/World/FalseWall.cs create mode 100644 Assets/Scripts/World/FalseWall.cs.meta create mode 100644 Assets/Scripts/World/HazardZone.cs create mode 100644 Assets/Scripts/World/HazardZone.cs.meta create mode 100644 Assets/Scripts/World/InteractableDetector.cs create mode 100644 Assets/Scripts/World/InteractableDetector.cs.meta create mode 100644 Assets/Scripts/World/Liquid.meta create mode 100644 Assets/Scripts/World/Liquid/LiquidPhysicsConfigSO.cs create mode 100644 Assets/Scripts/World/Liquid/LiquidPhysicsConfigSO.cs.meta create mode 100644 Assets/Scripts/World/Liquid/LiquidType.cs create mode 100644 Assets/Scripts/World/Liquid/LiquidType.cs.meta create mode 100644 Assets/Scripts/World/Liquid/LiquidZone.cs create mode 100644 Assets/Scripts/World/Liquid/LiquidZone.cs.meta create mode 100644 Assets/Scripts/World/Liquid/UnderwaterPostProcessingController.cs create mode 100644 Assets/Scripts/World/Liquid/UnderwaterPostProcessingController.cs.meta create mode 100644 Assets/Scripts/World/Liquid/WaterDangerState.cs create mode 100644 Assets/Scripts/World/Liquid/WaterDangerState.cs.meta create mode 100644 Assets/Scripts/World/MagicWall.cs create mode 100644 Assets/Scripts/World/MagicWall.cs.meta create mode 100644 Assets/Scripts/World/Map/IMapService.cs create mode 100644 Assets/Scripts/World/Map/IMapService.cs.meta create mode 100644 Assets/Scripts/World/Map/MapManager.cs create mode 100644 Assets/Scripts/World/Map/MapManager.cs.meta create mode 100644 Assets/Scripts/World/Map/MapPanel.cs create mode 100644 Assets/Scripts/World/Map/MapPanel.cs.meta create mode 100644 Assets/Scripts/World/Map/MapPin.cs create mode 100644 Assets/Scripts/World/Map/MapPin.cs.meta create mode 100644 Assets/Scripts/World/Map/MapPlayerTracker.cs create mode 100644 Assets/Scripts/World/Map/MapPlayerTracker.cs.meta create mode 100644 Assets/Scripts/World/Map/MapRoomDataSO.cs create mode 100644 Assets/Scripts/World/Map/MapRoomDataSO.cs.meta create mode 100644 Assets/Scripts/World/MovingPlatform.cs create mode 100644 Assets/Scripts/World/MovingPlatform.cs.meta create mode 100644 Assets/Scripts/World/PhantomInteractable.cs create mode 100644 Assets/Scripts/World/PhantomInteractable.cs.meta create mode 100644 Assets/Scripts/World/PhantomPlate.cs create mode 100644 Assets/Scripts/World/PhantomPlate.cs.meta create mode 100644 Assets/Scripts/World/PlayerSpawnPoint.cs create mode 100644 Assets/Scripts/World/PlayerSpawnPoint.cs.meta create mode 100644 Assets/Scripts/World/Puzzle.meta create mode 100644 Assets/Scripts/World/Puzzle/PuzzleDoor.cs create mode 100644 Assets/Scripts/World/Puzzle/PuzzleDoor.cs.meta create mode 100644 Assets/Scripts/World/Puzzle/PuzzleInterfaces.cs create mode 100644 Assets/Scripts/World/Puzzle/PuzzleInterfaces.cs.meta create mode 100644 Assets/Scripts/World/Puzzle/PuzzleReceiver.cs create mode 100644 Assets/Scripts/World/Puzzle/PuzzleReceiver.cs.meta create mode 100644 Assets/Scripts/World/Puzzle/PuzzleSwitch.cs create mode 100644 Assets/Scripts/World/Puzzle/PuzzleSwitch.cs.meta create mode 100644 Assets/Scripts/World/Puzzle/PuzzleWire.cs create mode 100644 Assets/Scripts/World/Puzzle/PuzzleWire.cs.meta create mode 100644 Assets/Scripts/World/RoomController.cs create mode 100644 Assets/Scripts/World/RoomController.cs.meta create mode 100644 Assets/Scripts/World/RoomTransition.cs create mode 100644 Assets/Scripts/World/RoomTransition.cs.meta create mode 100644 Assets/Scripts/World/Shop/ShopController.cs create mode 100644 Assets/Scripts/World/Shop/ShopController.cs.meta create mode 100644 Assets/Scripts/World/Shop/ShopInventorySO.cs create mode 100644 Assets/Scripts/World/Shop/ShopInventorySO.cs.meta create mode 100644 Assets/Scripts/World/Shop/ShopItemSO.cs create mode 100644 Assets/Scripts/World/Shop/ShopItemSO.cs.meta create mode 100644 Assets/Scripts/World/Shop/ShopNPC.cs create mode 100644 Assets/Scripts/World/Shop/ShopNPC.cs.meta create mode 100644 Assets/Scripts/World/SoftTerrain.cs create mode 100644 Assets/Scripts/World/SoftTerrain.cs.meta create mode 100644 Assets/Scripts/World/WorldMarker.cs create mode 100644 Assets/Scripts/World/WorldMarker.cs.meta create mode 100644 Assets/Scripts/World/WorldMarkerEventChannelSO.cs create mode 100644 Assets/Scripts/World/WorldMarkerEventChannelSO.cs.meta create mode 100644 Assets/Scripts/World/WorldStateRegistry.cs create mode 100644 Assets/Scripts/World/WorldStateRegistry.cs.meta create mode 100644 Assets/Settings/PlayerInputActions.cs create mode 100644 Assets/Settings/PlayerInputActions.cs.meta create mode 100644 Docs/Review/AdvancedCodeReview.md create mode 100644 Docs/Review/ArchitectureDeepDive_2025.md create mode 100644 Docs/Review/CodeQualityReview.md create mode 100644 Docs/Review/CodeReview_2025_Full.md create mode 100644 Docs/Review/CommercialGradeReview_2026.md create mode 100644 Docs/Review/ComprehensiveCodeReview.md create mode 100644 Docs/Review/DeepDive_2026.md create mode 100644 Docs/Review/DeepDive_2026_Q2.md create mode 100644 Docs/Review/DeepDive_2026_Q3.md create mode 100644 Docs/Review/DeepDive_2026_Q4.md create mode 100644 Docs/Review/DeepDive_2026_Q5.md create mode 100644 Docs/Review/DeepDive_2026_Q6.md create mode 100644 Docs/Review/FinalReview_PostFix_2026.md create mode 100644 Docs/Review/FrameworkReview_2026_May.md create mode 100644 Docs/Review/FrameworkReview_2026_May_v2.md create mode 100644 Docs/Review/FrameworkReview_2026_May_v3.md create mode 100644 Docs/Review/FrameworkReview_2026_May_v4.md create mode 100644 Docs/Review/FrameworkReview_2026_May_v5.md create mode 100644 Docs/Review/FrameworkReview_2026_May_v6.md create mode 100644 Docs/Review/FrameworkReview_2026_May_v7.md create mode 100644 Docs/Review/FrameworkReview_Full.md create mode 100644 Docs/Review/FullCodeReview.md create mode 100644 Docs/Review/FullSystemReview_2026_Final.md create mode 100644 Docs/Review/MasterCodeReview.md create mode 100644 Docs/Review/MasterCodeReview_2026_Full.md create mode 100644 Docs/Review/MasterReview_2025_PostFix.md create mode 100644 Docs/Verification/Architecture_Code_Consistency_Assessment_2026-05-11.md diff --git a/Assets/Data/Events/Boss/EVT_BossFightEnded.asset b/Assets/Data/Events/Boss/EVT_BossFightEnded.asset new file mode 100644 index 0000000..1bb8bbe --- /dev/null +++ b/Assets/Data/Events/Boss/EVT_BossFightEnded.asset @@ -0,0 +1,15 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d3e424c1787e5be4fa918201b1830192, type: 3} + m_Name: EVT_BossFightEnded + m_EditorClassIdentifier: + description: diff --git a/Assets/Data/Events/Boss/EVT_BossFightEnded.asset.meta b/Assets/Data/Events/Boss/EVT_BossFightEnded.asset.meta new file mode 100644 index 0000000..2de68c6 --- /dev/null +++ b/Assets/Data/Events/Boss/EVT_BossFightEnded.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6f942c86e6b23b24ca575bc0626ab022 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Events/Boss/EVT_BossFightStarted.asset b/Assets/Data/Events/Boss/EVT_BossFightStarted.asset new file mode 100644 index 0000000..dc7f024 --- /dev/null +++ b/Assets/Data/Events/Boss/EVT_BossFightStarted.asset @@ -0,0 +1,15 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 23dad55c2f7bcc54a92ed61cc6f27c5b, type: 3} + m_Name: EVT_BossFightStarted + m_EditorClassIdentifier: + description: diff --git a/Assets/Data/Events/Boss/EVT_BossFightStarted.asset.meta b/Assets/Data/Events/Boss/EVT_BossFightStarted.asset.meta new file mode 100644 index 0000000..efd6b33 --- /dev/null +++ b/Assets/Data/Events/Boss/EVT_BossFightStarted.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 155af2621b0a923468c62edf4fadbc21 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Events/Combat/EVT_DeathScreenConfirmed.asset b/Assets/Data/Events/Combat/EVT_DeathScreenConfirmed.asset new file mode 100644 index 0000000..70003df --- /dev/null +++ b/Assets/Data/Events/Combat/EVT_DeathScreenConfirmed.asset @@ -0,0 +1,15 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 07c5881d0d5ca3c42949a79f40939c3e, type: 3} + m_Name: EVT_DeathScreenConfirmed + m_EditorClassIdentifier: + description: diff --git a/Assets/Data/Events/Combat/EVT_DeathScreenConfirmed.asset.meta b/Assets/Data/Events/Combat/EVT_DeathScreenConfirmed.asset.meta new file mode 100644 index 0000000..8ca970f --- /dev/null +++ b/Assets/Data/Events/Combat/EVT_DeathScreenConfirmed.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0f5ae456aa9eccd49975a9ff609705f6 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Events/Combat/EVT_PlayerRespawned.asset b/Assets/Data/Events/Combat/EVT_PlayerRespawned.asset new file mode 100644 index 0000000..ddc7f3a --- /dev/null +++ b/Assets/Data/Events/Combat/EVT_PlayerRespawned.asset @@ -0,0 +1,15 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 07c5881d0d5ca3c42949a79f40939c3e, type: 3} + m_Name: EVT_PlayerRespawned + m_EditorClassIdentifier: + description: diff --git a/Assets/Data/Events/Combat/EVT_PlayerRespawned.asset.meta b/Assets/Data/Events/Combat/EVT_PlayerRespawned.asset.meta new file mode 100644 index 0000000..9b8da62 --- /dev/null +++ b/Assets/Data/Events/Combat/EVT_PlayerRespawned.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f5e5756bf4809cf41abc48cf331dc354 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Events/Combat/EVT_RespawnCompleted.asset b/Assets/Data/Events/Combat/EVT_RespawnCompleted.asset new file mode 100644 index 0000000..d1cd3e0 --- /dev/null +++ b/Assets/Data/Events/Combat/EVT_RespawnCompleted.asset @@ -0,0 +1,15 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 07c5881d0d5ca3c42949a79f40939c3e, type: 3} + m_Name: EVT_RespawnCompleted + m_EditorClassIdentifier: + description: diff --git a/Assets/Data/Events/Combat/EVT_RespawnCompleted.asset.meta b/Assets/Data/Events/Combat/EVT_RespawnCompleted.asset.meta new file mode 100644 index 0000000..44f7959 --- /dev/null +++ b/Assets/Data/Events/Combat/EVT_RespawnCompleted.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ca2f6b7e523ce5b4793f42d48fa600ba +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Events/Combat/EVT_RespawnStarted.asset b/Assets/Data/Events/Combat/EVT_RespawnStarted.asset new file mode 100644 index 0000000..1e7e72a --- /dev/null +++ b/Assets/Data/Events/Combat/EVT_RespawnStarted.asset @@ -0,0 +1,15 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 07c5881d0d5ca3c42949a79f40939c3e, type: 3} + m_Name: EVT_RespawnStarted + m_EditorClassIdentifier: + description: diff --git a/Assets/Data/Events/Combat/EVT_RespawnStarted.asset.meta b/Assets/Data/Events/Combat/EVT_RespawnStarted.asset.meta new file mode 100644 index 0000000..e0dd8bd --- /dev/null +++ b/Assets/Data/Events/Combat/EVT_RespawnStarted.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5945b07508ca07646ba9cbf9a39b7683 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Events/Core/EVT_FadeInRequest.asset b/Assets/Data/Events/Core/EVT_FadeInRequest.asset new file mode 100644 index 0000000..4e82194 --- /dev/null +++ b/Assets/Data/Events/Core/EVT_FadeInRequest.asset @@ -0,0 +1,15 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 07c5881d0d5ca3c42949a79f40939c3e, type: 3} + m_Name: EVT_FadeInRequest + m_EditorClassIdentifier: + description: diff --git a/Assets/Data/Events/Core/EVT_FadeInRequest.asset.meta b/Assets/Data/Events/Core/EVT_FadeInRequest.asset.meta new file mode 100644 index 0000000..049cc10 --- /dev/null +++ b/Assets/Data/Events/Core/EVT_FadeInRequest.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d00a4fdb64a17324bb699756f5f0006c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Events/Core/EVT_FadeOutRequest.asset b/Assets/Data/Events/Core/EVT_FadeOutRequest.asset new file mode 100644 index 0000000..6ab8783 --- /dev/null +++ b/Assets/Data/Events/Core/EVT_FadeOutRequest.asset @@ -0,0 +1,15 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 07c5881d0d5ca3c42949a79f40939c3e, type: 3} + m_Name: EVT_FadeOutRequest + m_EditorClassIdentifier: + description: diff --git a/Assets/Data/Events/Core/EVT_FadeOutRequest.asset.meta b/Assets/Data/Events/Core/EVT_FadeOutRequest.asset.meta new file mode 100644 index 0000000..13ce679 --- /dev/null +++ b/Assets/Data/Events/Core/EVT_FadeOutRequest.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e0ce042bacf58e24ba78ba34fc16394b +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Events/Core/EVT_GameStateChanged.asset b/Assets/Data/Events/Core/EVT_GameStateChanged.asset new file mode 100644 index 0000000..689dea5 --- /dev/null +++ b/Assets/Data/Events/Core/EVT_GameStateChanged.asset @@ -0,0 +1,15 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: c060c20fe37837c408cc5a628f6d8863, type: 3} + m_Name: EVT_GameStateChanged + m_EditorClassIdentifier: + description: diff --git a/Assets/Data/Events/Core/EVT_GameStateChanged.asset.meta b/Assets/Data/Events/Core/EVT_GameStateChanged.asset.meta new file mode 100644 index 0000000..cf01359 --- /dev/null +++ b/Assets/Data/Events/Core/EVT_GameStateChanged.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6c0a62225f8da694c94cd934fc29b72e +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Events/Core/EVT_SceneLoaded.asset b/Assets/Data/Events/Core/EVT_SceneLoaded.asset new file mode 100644 index 0000000..374b48d --- /dev/null +++ b/Assets/Data/Events/Core/EVT_SceneLoaded.asset @@ -0,0 +1,15 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 23dad55c2f7bcc54a92ed61cc6f27c5b, type: 3} + m_Name: EVT_SceneLoaded + m_EditorClassIdentifier: + description: diff --git a/Assets/Data/Events/Core/EVT_SceneLoaded.asset.meta b/Assets/Data/Events/Core/EVT_SceneLoaded.asset.meta new file mode 100644 index 0000000..e1d8817 --- /dev/null +++ b/Assets/Data/Events/Core/EVT_SceneLoaded.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9cd668f69c1c8be4e9fdda565b86df0c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Events/UI/EVT_FastTravelOpen.asset b/Assets/Data/Events/UI/EVT_FastTravelOpen.asset new file mode 100644 index 0000000..6ff4b54 --- /dev/null +++ b/Assets/Data/Events/UI/EVT_FastTravelOpen.asset @@ -0,0 +1,15 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 07c5881d0d5ca3c42949a79f40939c3e, type: 3} + m_Name: EVT_FastTravelOpen + m_EditorClassIdentifier: + description: diff --git a/Assets/Data/Events/UI/EVT_FastTravelOpen.asset.meta b/Assets/Data/Events/UI/EVT_FastTravelOpen.asset.meta new file mode 100644 index 0000000..170ed49 --- /dev/null +++ b/Assets/Data/Events/UI/EVT_FastTravelOpen.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ec4958dcfb565ae4481c47e64838a05d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Events/UI/EVT_MapOpen.asset b/Assets/Data/Events/UI/EVT_MapOpen.asset new file mode 100644 index 0000000..f92c746 --- /dev/null +++ b/Assets/Data/Events/UI/EVT_MapOpen.asset @@ -0,0 +1,15 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 07c5881d0d5ca3c42949a79f40939c3e, type: 3} + m_Name: EVT_MapOpen + m_EditorClassIdentifier: + description: diff --git a/Assets/Data/Events/UI/EVT_MapOpen.asset.meta b/Assets/Data/Events/UI/EVT_MapOpen.asset.meta new file mode 100644 index 0000000..ea956a2 --- /dev/null +++ b/Assets/Data/Events/UI/EVT_MapOpen.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c2e3392d42dd3e046819a949ab861b17 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Events/UI/EVT_ShopOpen.asset b/Assets/Data/Events/UI/EVT_ShopOpen.asset new file mode 100644 index 0000000..9cd5e51 --- /dev/null +++ b/Assets/Data/Events/UI/EVT_ShopOpen.asset @@ -0,0 +1,15 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 23dad55c2f7bcc54a92ed61cc6f27c5b, type: 3} + m_Name: EVT_ShopOpen + m_EditorClassIdentifier: + description: diff --git a/Assets/Data/Events/UI/EVT_ShopOpen.asset.meta b/Assets/Data/Events/UI/EVT_ShopOpen.asset.meta new file mode 100644 index 0000000..a4243a7 --- /dev/null +++ b/Assets/Data/Events/UI/EVT_ShopOpen.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8ef6f571506099c49af09e36ba74f97e +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Events/World/EVT_SavePointActivated.asset b/Assets/Data/Events/World/EVT_SavePointActivated.asset new file mode 100644 index 0000000..6cc9c2a --- /dev/null +++ b/Assets/Data/Events/World/EVT_SavePointActivated.asset @@ -0,0 +1,15 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 23dad55c2f7bcc54a92ed61cc6f27c5b, type: 3} + m_Name: EVT_SavePointActivated + m_EditorClassIdentifier: + description: diff --git a/Assets/Data/Events/World/EVT_SavePointActivated.asset.meta b/Assets/Data/Events/World/EVT_SavePointActivated.asset.meta new file mode 100644 index 0000000..d08fc7c --- /dev/null +++ b/Assets/Data/Events/World/EVT_SavePointActivated.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2f6be6329d5d8cd4e956d00cd29cab96 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Input.meta b/Assets/Data/Input.meta new file mode 100644 index 0000000..b260b58 --- /dev/null +++ b/Assets/Data/Input.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 194d26dc1491bb84c8304ad81f146308 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Input/InputReader.asset b/Assets/Data/Input/InputReader.asset new file mode 100644 index 0000000..bd533c1 --- /dev/null +++ b/Assets/Data/Input/InputReader.asset @@ -0,0 +1,16 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3945955c08d2670458e14d41e1236946, type: 3} + m_Name: InputReader + m_EditorClassIdentifier: + _inputActions: {fileID: -944628639613478452, guid: 6d0341a640ba64043a8cd70f771b962d, type: 3} + _onPauseRequested: {fileID: 11400000, guid: 032b9e347985f7a46ac37371acd2f619, type: 2} diff --git a/Assets/Data/Input/InputReader.asset.meta b/Assets/Data/Input/InputReader.asset.meta new file mode 100644 index 0000000..27ea0a2 --- /dev/null +++ b/Assets/Data/Input/InputReader.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d3d7b464ae0409a47a5b4b627c6b3ca6 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Player/PLY_PlayerAnimationConfig.asset b/Assets/Data/Player/PLY_PlayerAnimationConfig.asset new file mode 100644 index 0000000..9beadee --- /dev/null +++ b/Assets/Data/Player/PLY_PlayerAnimationConfig.asset @@ -0,0 +1,29 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5ec15df6b0d345c4f92ba459e89dc02f, type: 3} + m_Name: PLY_PlayerAnimationConfig + m_EditorClassIdentifier: + Idle: {fileID: 0} + Run: {fileID: 0} + Jump: {fileID: 0} + Fall: {fileID: 0} + Dash: {fileID: 0} + WallSlide: {fileID: 0} + Hurt: {fileID: 0} + Dead: {fileID: 0} + UseSpring: {fileID: 0} + GroundAttacks: [] + AirAttack: {fileID: 0} + UpAttack: {fileID: 0} + DownAttack: {fileID: 0} + ParryStart: {fileID: 0} + ParrySuccess: {fileID: 0} diff --git a/Assets/Data/Player/PLY_PlayerAnimationConfig.asset.meta b/Assets/Data/Player/PLY_PlayerAnimationConfig.asset.meta new file mode 100644 index 0000000..c664639 --- /dev/null +++ b/Assets/Data/Player/PLY_PlayerAnimationConfig.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b8fe5871cf6cc774aac57d984e29e2d9 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Player/PLY_PlayerMovementConfig.asset b/Assets/Data/Player/PLY_PlayerMovementConfig.asset new file mode 100644 index 0000000..99bef3d --- /dev/null +++ b/Assets/Data/Player/PLY_PlayerMovementConfig.asset @@ -0,0 +1,36 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 81da55e0fcf99d34693cbc5a348225c3, type: 3} + m_Name: PLY_PlayerMovementConfig + m_EditorClassIdentifier: + RunSpeed: 7 + Acceleration: 50 + Deceleration: 80 + JumpForce: 18 + CoyoteTime: 0.12 + FallGravityMult: 2.5 + MaxFallSpeed: 20 + DashSpeed: 20 + DashDuration: 0.18 + DashCooldown: 0.4 + MaxAerialDashes: 1 + WallSlideSpeed: 2 + WallJumpForceX: 12 + WallJumpForceY: 16 + WallRayLength: 0.55 + WallRayOffsetY: 0.2 + WallGrabMaxHeightGain: 0.5 + WallGrabReleaseDelay: 0.08 + WallJumpBackForceX: 14 + WallJumpAwayForceX: 10 + WallJumpAwayForceY: 18 + WallJumpInputLockDuration: 0.15 diff --git a/Assets/Data/Player/PLY_PlayerMovementConfig.asset.meta b/Assets/Data/Player/PLY_PlayerMovementConfig.asset.meta new file mode 100644 index 0000000..44988e9 --- /dev/null +++ b/Assets/Data/Player/PLY_PlayerMovementConfig.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ed5e9f8d24bf223479a3bcbbb458294d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Player/PLY_PlayerStats.asset b/Assets/Data/Player/PLY_PlayerStats.asset new file mode 100644 index 0000000..33fc038 --- /dev/null +++ b/Assets/Data/Player/PLY_PlayerStats.asset @@ -0,0 +1,23 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 31a9f22bef1315643bf5a49f2a6edd2b, type: 3} + m_Name: PLY_PlayerStats + m_EditorClassIdentifier: + MaxHP: 5 + MaxSoulPower: 100 + MaxSpiritPower: 100 + SpiritRegenRate: 5 + MaxSpringCharges: 3 + SpringHealAmount: 2 + SpringKillThreshold: 4 + InvincibilityDuration: 0.6 + InitialGeo: 0 diff --git a/Assets/Data/Player/PLY_PlayerStats.asset.meta b/Assets/Data/Player/PLY_PlayerStats.asset.meta new file mode 100644 index 0000000..7d3ee7b --- /dev/null +++ b/Assets/Data/Player/PLY_PlayerStats.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e1705c75c3139e94a8b1c9e90002d364 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/Art.meta b/Assets/Resources/Art.meta new file mode 100644 index 0000000..96403c2 --- /dev/null +++ b/Assets/Resources/Art.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 55368645c788ddc4994002d987e16ed8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/Art/Builds.meta b/Assets/Resources/Art/Builds.meta new file mode 100644 index 0000000..ddc2bdc --- /dev/null +++ b/Assets/Resources/Art/Builds.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8dc030a56b7172d40934e0f5c7f186a6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/Art/Effects.meta b/Assets/Resources/Art/Effects.meta new file mode 100644 index 0000000..d8ac56a --- /dev/null +++ b/Assets/Resources/Art/Effects.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ed95febcdefa7d74caf641da93e597dd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/Art/Effects/Textures.meta b/Assets/Resources/Art/Effects/Textures.meta new file mode 100644 index 0000000..12c1667 --- /dev/null +++ b/Assets/Resources/Art/Effects/Textures.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1de403e36134fbe4cb817afac9eb9f4d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/Art/Effects/Textures/Fx_Ring03.png b/Assets/Resources/Art/Effects/Textures/Fx_Ring03.png new file mode 100644 index 0000000000000000000000000000000000000000..a0a818d0ac47cf7157f38013135fdda09f4a4780 GIT binary patch literal 36037 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 + /// 静态工具类:将 AnimationEventConfigSO 中声明的事件注入 Animancer ClipTransition。 + /// 使用 Animancer Pro API:ClipTransition.Events.SetCallback(normalizedTime, Action)。 + /// 调用时机:MonoBehaviour.Awake(在 Animancer 播放前完成绑定)。 + /// + public static class AnimationEventBinder + { + /// + /// 将 config 中的所有事件回调注入到 clip 的 Animancer 事件系统。 + /// + /// 目标 Animancer ClipTransition。 + /// 事件配置资产(null 时静默跳过)。 + /// 事件接收者(通常是同一 MonoBehaviour)。 + public static void Bind(ClipTransition clip, AnimationEventConfigSO config, IAnimationEventHandler receiver) + { + if (clip == null || config == null || receiver == null) return; + + foreach (var entry in config.SortedEvents) + { + // 捕获循环变量,避免闭包陷阱 + var captured = entry; + clip.Events.Add(captured.normalizedTime, () => + receiver.HandleEvent(captured.eventType, captured.data)); + } + } + } +} diff --git a/Assets/Scripts/Audio/_Placeholder.cs.meta b/Assets/Scripts/Animation/AnimationEventBinder.cs.meta similarity index 83% rename from Assets/Scripts/Audio/_Placeholder.cs.meta rename to Assets/Scripts/Animation/AnimationEventBinder.cs.meta index 3f96efe..238924c 100644 --- a/Assets/Scripts/Audio/_Placeholder.cs.meta +++ b/Assets/Scripts/Animation/AnimationEventBinder.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: c7d6fe0521388084e83314560a394951 +guid: e9c16d7d7a6978a4c98a745261fbbf4f MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Scripts/Animation/AnimationEventConfigSO.cs b/Assets/Scripts/Animation/AnimationEventConfigSO.cs new file mode 100644 index 0000000..2da3ae1 --- /dev/null +++ b/Assets/Scripts/Animation/AnimationEventConfigSO.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace BaseGames.Animation +{ + /// + /// 动画事件配置资产(架构 §AnimationModule)。 + /// 为指定 AnimationClip 声明一组事件时机,由 AnimationEventBinder 在运行时注入 Animancer 回调。 + /// + /// 使用方式: + /// 1. 在 Inspector 中创建此资产(菜单:Animation/EventConfig)。 + /// 2. 配置 TargetClip 和 Events 列表。 + /// 3. 在 MonoBehaviour.Awake 中:AnimationEventBinder.Bind(clip, config, this)。 + /// + [CreateAssetMenu(menuName = "Animation/EventConfig")] + public class AnimationEventConfigSO : ScriptableObject + { + [Serializable] + public struct EventEntry + { + public AnimationEventType eventType; + + [Range(0f, 1f)] + [Tooltip("事件在动画中的归一化时间(0 = 开始,1 = 结束)。")] + public float normalizedTime; + + [Tooltip("附加字符串数据(可空)。用于 payload 参数。")] + public string data; + } + + [Header("绑定的动画片段")] + public AnimationClip targetClip; + + [Header("事件时机列表(归一化时间)")] + public EventEntry[] events; + + /// + /// 用于编辑器工具验证:记录创建 Config 时 Clip 的实际帧数, + /// 若 Clip 被替换(长度漂移 >5 帧)则 EventConfigEditor 显示警告。 + /// + [HideInInspector] public float ExpectedClipLength = -1f; + + // ── 运行时查询 ─────────────────────────────────────────────────── + + /// 按归一化时间升序返回所有事件(保证 SetCallback 顺序稳定)。 + public IEnumerable SortedEvents => + events != null + ? events.OrderBy(e => e.normalizedTime) + : Enumerable.Empty(); + + /// + /// 返回第一个匹配类型的归一化时间;未找到时返回 -1。 + /// + public float GetNormalizedTime(AnimationEventType eventType) + { + if (events == null) return -1f; + foreach (var e in events) + if (e.eventType == eventType) return e.normalizedTime; + return -1f; + } + } +} diff --git a/Assets/Scripts/Combat/StatusEffects/_Placeholder.cs.meta b/Assets/Scripts/Animation/AnimationEventConfigSO.cs.meta similarity index 83% rename from Assets/Scripts/Combat/StatusEffects/_Placeholder.cs.meta rename to Assets/Scripts/Animation/AnimationEventConfigSO.cs.meta index 84f8f33..def50fb 100644 --- a/Assets/Scripts/Combat/StatusEffects/_Placeholder.cs.meta +++ b/Assets/Scripts/Animation/AnimationEventConfigSO.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 1dfc988231a6ac14a9aa035ba1719ab0 +guid: 3dc3625236c5f8747ae7352e78c01137 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Scripts/Animation/AnimationEventType.cs b/Assets/Scripts/Animation/AnimationEventType.cs new file mode 100644 index 0000000..698b7c9 --- /dev/null +++ b/Assets/Scripts/Animation/AnimationEventType.cs @@ -0,0 +1,49 @@ +namespace BaseGames.Animation +{ + /// + /// 动画事件类型枚举(架构 §AnimationModule)。 + /// 用作 AnimationEventConfigSO 中事件时机的分类标签, + /// 与 AnimationEventBinder 配合将 Animancer ClipTransition 回调路由到 IAnimationEventHandler。 + /// + public enum AnimationEventType + { + // ── 命中判定 ────────────────────────────────────────────────────── + EnableHitBox, + DisableHitBox, + AttackImpact, // 攻击命中瞬间反馈(不依赖特定 HitBox) + + // ── 招架窗口 ────────────────────────────────────────────────────── + EnableParryWindow, + DisableParryWindow, + + // ── 无敌帧 ──────────────────────────────────────────────────────── + EnableIFrame, + DisableIFrame, + + // ── 移动反馈 ────────────────────────────────────────────────────── + Footstep, + LandImpact, + JumpLaunch, + + // ── 交互 ────────────────────────────────────────────────────────── + EnableInteract, + + // ── 通用反馈 ────────────────────────────────────────────────────── + TriggerFeedback, // payload:FeedbackPreset 名称 + PlaySFX, // payload:SFX Id + + // ── 敌方专用 ────────────────────────────────────────────────────── + SpawnProjectile, // payload:弹幕配置 Id + RoarStart, + RoarEnd, + PhaseTwoStart, + + // ── 玩家取消窗口 ────────────────────────────────────────────────── + CancelWindowOpen, + CancelWindowClose, + + // ── 状态机钩子 ──────────────────────────────────────────────────── + StateTransition, // payload:目标状态 Id + AnimationComplete, // payload:上下文字符串(可空) + } +} diff --git a/Assets/Scripts/Animation/_Placeholder.cs.meta b/Assets/Scripts/Animation/AnimationEventType.cs.meta similarity index 83% rename from Assets/Scripts/Animation/_Placeholder.cs.meta rename to Assets/Scripts/Animation/AnimationEventType.cs.meta index 4852d81..1df675b 100644 --- a/Assets/Scripts/Animation/_Placeholder.cs.meta +++ b/Assets/Scripts/Animation/AnimationEventType.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 14d8a3e3d7371e54eb25a5d4dded4645 +guid: 97412b5f50276484fa226d97b8c3769b MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Scripts/Animation/BaseGames.Animation.asmdef b/Assets/Scripts/Animation/BaseGames.Animation.asmdef index a03c84e..b85d6ba 100644 --- a/Assets/Scripts/Animation/BaseGames.Animation.asmdef +++ b/Assets/Scripts/Animation/BaseGames.Animation.asmdef @@ -9,7 +9,12 @@ "rootNamespace": "BaseGames.Animation", "references": [ "BaseGames.Core.Events", - "Kybernetik.Animancer" + "Kybernetik.Animancer", + "BaseGames.Combat", + "BaseGames.Parry", + "BaseGames.Feedback", + "BaseGames.Player", + "BaseGames.Enemies" ], "autoReferenced": true, "overrideReferences": false, diff --git a/Assets/Scripts/Animation/EnemyAnimationEvents.cs b/Assets/Scripts/Animation/EnemyAnimationEvents.cs new file mode 100644 index 0000000..ea0e9d9 --- /dev/null +++ b/Assets/Scripts/Animation/EnemyAnimationEvents.cs @@ -0,0 +1,111 @@ +using System; +using Animancer; +using BaseGames.Combat; +using BaseGames.Enemies; +using UnityEngine; + +namespace BaseGames.Animation +{ + /// + /// 敌人动画事件接收器(架构 §AnimationModule)。 + /// 挂载于敌人 Prefab 根节点。负责将 Animancer 动画时间点回调路由到 + /// HitBox 激活、弹幕生成、嘶吼状态、二阶段切换等系统。 + /// + /// 使用方式: + /// 1. 在 Inspector 中填充 _hitBoxes、_enemy。 + /// 2. 将每条 ClipTransition + AnimationEventConfigSO 配对添加到 _bindings。 + /// 3. Awake 中 AnimationEventBinder.Bind 自动完成注入。 + /// + public class EnemyAnimationEvents : MonoBehaviour, IAnimationEventHandler + { + [Serializable] + public struct EventBinding + { + [Tooltip("Animancer ClipTransition(需与动画组件中同一引用绑定)。")] + public ClipTransition clip; + + [Tooltip("对应的 AnimationEventConfigSO 资产。")] + public AnimationEventConfigSO config; + } + + [Header("子系统引用")] + [SerializeField] private HitBox[] _hitBoxes; + [SerializeField] private EnemyBase _enemy; + + [Header("事件绑定(每个 Clip 对应一个配置资产)")] + [SerializeField] private EventBinding[] _bindings; + + private void Awake() + { + if (_enemy == null) + _enemy = GetComponentInParent(); + + foreach (var b in _bindings) + AnimationEventBinder.Bind(b.clip, b.config, this); + } + + // ── IAnimationEventHandler ───────────────────────────────────────── + + public void HandleEvent(AnimationEventType type, string payload) + { + switch (type) + { + // ── 命中判定 ────────────────────────────────────────────── + case AnimationEventType.EnableHitBox: + SetHitBoxActive(payload, true); + break; + + case AnimationEventType.DisableHitBox: + SetHitBoxActive(payload, false); + break; + + // ── 弹幕 / 技能 ─────────────────────────────────────────── + case AnimationEventType.SpawnProjectile: + _enemy?.SpawnProjectile(payload); + break; + + // ── 嘶吼 ────────────────────────────────────────────────── + case AnimationEventType.RoarStart: + _enemy?.SetRoaring(true); + break; + + case AnimationEventType.RoarEnd: + _enemy?.SetRoaring(false); + break; + + // ── 二阶段切换 ──────────────────────────────────────────── + case AnimationEventType.PhaseTwoStart: + _enemy?.TriggerPhaseTwo(); + break; + + // ── 状态机钩子 ──────────────────────────────────────────── + case AnimationEventType.AnimationComplete: + _enemy?.OnAnimationComplete(payload); + break; + } + } + + // ── 私有辅助 ─────────────────────────────────────────────────────── + + /// + /// 按 Id 激活或停用 HitBox。 + /// payload 为空时操作所有 HitBox;非空时仅操作 Id 匹配的 HitBox。 + /// + private void SetHitBoxActive(string id, bool active) + { + if (_hitBoxes == null) return; + + bool matchAll = string.IsNullOrEmpty(id); + + foreach (var hb in _hitBoxes) + { + if (hb == null) continue; + if (matchAll || hb.Id == id) + { + if (active) hb.Activate(); + else hb.Deactivate(); + } + } + } + } +} diff --git a/Assets/Scripts/Combat/_Placeholder.cs.meta b/Assets/Scripts/Animation/EnemyAnimationEvents.cs.meta similarity index 83% rename from Assets/Scripts/Combat/_Placeholder.cs.meta rename to Assets/Scripts/Animation/EnemyAnimationEvents.cs.meta index 341e9ff..50222e9 100644 --- a/Assets/Scripts/Combat/_Placeholder.cs.meta +++ b/Assets/Scripts/Animation/EnemyAnimationEvents.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: b9c4356cd693b604bb0889f9538eb13e +guid: 13003b40ed7bd164d8d90eb58f0548ec MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Scripts/Animation/IAnimationEventHandler.cs b/Assets/Scripts/Animation/IAnimationEventHandler.cs new file mode 100644 index 0000000..af44d0a --- /dev/null +++ b/Assets/Scripts/Animation/IAnimationEventHandler.cs @@ -0,0 +1,17 @@ +namespace BaseGames.Animation +{ + /// + /// 动画事件接收接口(架构 §AnimationModule)。 + /// 由 PlayerAnimationEvents / EnemyAnimationEvents 实现。 + /// AnimationEventBinder 将 Animancer 回调路由至此接口,避免硬耦合。 + /// + public interface IAnimationEventHandler + { + /// + /// 处理由 Animancer ClipTransition 触发的动画事件。 + /// + /// 事件类型。 + /// 附加字符串数据(可为 null 或空)。 + void HandleEvent(AnimationEventType type, string payload); + } +} diff --git a/Assets/Scripts/Animation/IAnimationEventHandler.cs.meta b/Assets/Scripts/Animation/IAnimationEventHandler.cs.meta new file mode 100644 index 0000000..479a6ef --- /dev/null +++ b/Assets/Scripts/Animation/IAnimationEventHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f2c57ff98093620448d1f7c8ce6d0511 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Animation/PlayerAnimationEvents.cs b/Assets/Scripts/Animation/PlayerAnimationEvents.cs new file mode 100644 index 0000000..c117f84 --- /dev/null +++ b/Assets/Scripts/Animation/PlayerAnimationEvents.cs @@ -0,0 +1,149 @@ +using System; +using Animancer; +using BaseGames.Combat; +using BaseGames.Feedback; +using BaseGames.Parry; +using BaseGames.Player; +using UnityEngine; + +namespace BaseGames.Animation +{ + /// + /// 玩家动画事件接收器(架构 §AnimationModule)。 + /// 挂载于玩家 Prefab 根节点。负责将 Animancer 动画时间点回调路由到 + /// HitBox 激活、招架窗口、无敌帧、移动取消窗口、反馈等系统。 + /// + /// 使用方式: + /// 1. 在 Inspector 中填充 _hitBoxes、_hurtBox、_mover、_parrySystem。 + /// 2. 将每条 ClipTransition + AnimationEventConfigSO 配对添加到 _bindings。 + /// 3. Awake 中 AnimationEventBinder.Bind 自动完成注入。 + /// + public class PlayerAnimationEvents : MonoBehaviour, IAnimationEventHandler + { + [Serializable] + public struct EventBinding + { + [Tooltip("Animancer ClipTransition(需与动画组件中同一引用绑定)。")] + public ClipTransition clip; + + [Tooltip("对应的 AnimationEventConfigSO 资产。")] + public AnimationEventConfigSO config; + } + + [Header("子系统引用")] + [SerializeField] private HitBox[] _hitBoxes; + [SerializeField] private HurtBox _hurtBox; + [SerializeField] private PlayerMovement _mover; + [SerializeField] private ParrySystem _parrySystem; + + [Header("事件绑定(每个 Clip 对应一个配置资产)")] + [SerializeField] private EventBinding[] _bindings; + + private IFeedbackPlayer _feedback; + + private void Awake() + { + // IFeedbackPlayer 由父层或同层实现(PlayerFeedback / NullFeedbackPlayer) + _feedback = GetComponentInParent() + ?? NullFeedbackPlayer.Instance; + + foreach (var b in _bindings) + AnimationEventBinder.Bind(b.clip, b.config, this); + } + + // ── IAnimationEventHandler ───────────────────────────────────────── + + public void HandleEvent(AnimationEventType type, string payload) + { + switch (type) + { + // ── 命中判定 ────────────────────────────────────────────── + case AnimationEventType.EnableHitBox: + SetHitBoxActive(payload, true); + break; + + case AnimationEventType.DisableHitBox: + SetHitBoxActive(payload, false); + break; + + case AnimationEventType.AttackImpact: + _feedback.PlayAttackWhoosh(); + break; + + // ── 招架窗口 ────────────────────────────────────────────── + case AnimationEventType.EnableParryWindow: + _parrySystem?.OpenParryWindow(); + break; + + case AnimationEventType.DisableParryWindow: + _parrySystem?.CloseParryWindow(); + break; + + // ── 无敌帧 ──────────────────────────────────────────────── + case AnimationEventType.EnableIFrame: + _hurtBox?.SetInvincible(true); + break; + + case AnimationEventType.DisableIFrame: + _hurtBox?.SetInvincible(false); + break; + + // ── 移动反馈 ────────────────────────────────────────────── + case AnimationEventType.LandImpact: + _feedback.PlayLandImpact(); + break; + + case AnimationEventType.JumpLaunch: + _feedback.PlayJumpLaunch(); + break; + + case AnimationEventType.Footstep: + _feedback.PlayFootstep(); + break; + + // ── 取消窗口 ────────────────────────────────────────────── + case AnimationEventType.CancelWindowOpen: + _mover?.SetCancelWindowOpen(true); + break; + + case AnimationEventType.CancelWindowClose: + _mover?.SetCancelWindowOpen(false); + break; + + // ── 通用反馈 ────────────────────────────────────────────── + case AnimationEventType.TriggerFeedback: + if (!string.IsNullOrEmpty(payload)) + _feedback.TriggerPreset(payload); + break; + + case AnimationEventType.PlaySFX: + if (!string.IsNullOrEmpty(payload)) + _feedback.PlaySFXById(payload); + break; + } + } + + // ── 私有辅助 ─────────────────────────────────────────────────────── + + /// + /// 按 Id 激活或停用 HitBox。 + /// payload 为空时操作所有 HitBox;非空时仅操作 Id 匹配的 HitBox。 + /// + private void SetHitBoxActive(string id, bool active) + { + if (_hitBoxes == null) return; + + bool matchAll = string.IsNullOrEmpty(id); + + foreach (var hb in _hitBoxes) + { + if (hb == null) continue; + if (matchAll || hb.Id == id) + { + if (active) hb.Activate(); + else hb.Deactivate(); + } + } + } + } +} diff --git a/Assets/Scripts/Animation/PlayerAnimationEvents.cs.meta b/Assets/Scripts/Animation/PlayerAnimationEvents.cs.meta new file mode 100644 index 0000000..d7b8022 --- /dev/null +++ b/Assets/Scripts/Animation/PlayerAnimationEvents.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4e3a90eca47d12a49a07be7f38a376e8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Animation/_Placeholder.cs b/Assets/Scripts/Animation/_Placeholder.cs deleted file mode 100644 index 3df68a3..0000000 --- a/Assets/Scripts/Animation/_Placeholder.cs +++ /dev/null @@ -1,3 +0,0 @@ -// Placeholder to prevent asmdef-no-scripts warning. -namespace BaseGames.Animation { } - diff --git a/Assets/Scripts/Audio/.gitkeep b/Assets/Scripts/Audio/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Assets/Scripts/Audio/AudioEventSO.cs b/Assets/Scripts/Audio/AudioEventSO.cs new file mode 100644 index 0000000..1d82b67 --- /dev/null +++ b/Assets/Scripts/Audio/AudioEventSO.cs @@ -0,0 +1,52 @@ +using UnityEngine; +using UnityEngine.Audio; + +namespace BaseGames.Audio +{ + /// + /// 音效事件 ScriptableObject。随机从 Clips 中选取一条播放, + /// 并在 Volume / Pitch 区间内随机变化,增强音效多样性。 + /// + [CreateAssetMenu(menuName = "Audio/AudioEvent")] + public class AudioEventSO : ScriptableObject + { + [SerializeField] private AudioClip[] _clips; + [SerializeField] private float _volumeMin = 0.9f; + [SerializeField] private float _volumeMax = 1.0f; + [SerializeField] private float _pitchMin = 0.95f; + [SerializeField] private float _pitchMax = 1.05f; + [SerializeField] private AudioMixerGroup _mixerGroup; + + public AudioClip PickClip() + { + if (_clips == null || _clips.Length == 0) return null; + return _clips[Random.Range(0, _clips.Length)]; + } + + public float PickVolume() => Random.Range(_volumeMin, _volumeMax); + + /// 通过已有的 AudioSource 播放(适用于场景内固定位置音效)。 + public void Play(AudioSource source) + { + var clip = PickClip(); + if (clip == null || source == null) return; + + source.clip = clip; + source.volume = PickVolume(); + source.pitch = Random.Range(_pitchMin, _pitchMax); + source.outputAudioMixerGroup = _mixerGroup; + source.Play(); + } + + /// 通过 AudioSource.PlayOneShot 播放(不打断当前音效)。 + public void PlayOneShot(AudioSource source) + { + var clip = PickClip(); + if (clip == null || source == null) return; + + source.outputAudioMixerGroup = _mixerGroup; + source.pitch = Random.Range(_pitchMin, _pitchMax); + source.PlayOneShot(clip, PickVolume()); + } + } +} diff --git a/Assets/Scripts/Audio/AudioEventSO.cs.meta b/Assets/Scripts/Audio/AudioEventSO.cs.meta new file mode 100644 index 0000000..664d470 --- /dev/null +++ b/Assets/Scripts/Audio/AudioEventSO.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 356d306cd9d1eaa46b9d283761bcba54 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Audio/AudioManager.cs b/Assets/Scripts/Audio/AudioManager.cs index 9310b42..b6aa1c4 100644 --- a/Assets/Scripts/Audio/AudioManager.cs +++ b/Assets/Scripts/Audio/AudioManager.cs @@ -22,8 +22,22 @@ namespace BaseGames.Audio [SerializeField] private AudioSource _bgmSourceB; [Header("SFX Pool(建议 6 个,均路由到 SFX MixerGroup)")] + [Tooltip("轮转池大小:同帧触发数超过此值时最旧的音效会被打断。建议 6~8 个。")] [SerializeField] private AudioSource[] _sfxSources; + [Header("Audio Config(BGM 映射)")] + [SerializeField] private AudioConfigSO _audioConfig; + + [Header("SFX 注册表(key → AudioEventSO)")] + [SerializeField] private AudioEventEntry[] _sfxRegistry; + + [System.Serializable] + public struct AudioEventEntry + { + public string Key; + public AudioEventSO Event; + } + [Header("Event Channels - Subscribe")] [SerializeField] private VoidEventChannelSO _onPlayerDied; @@ -31,49 +45,61 @@ namespace BaseGames.Audio private AudioSource _inactiveBGMSource; private Coroutine _crossfadeCoroutine; private int _sfxRoundRobin; - - // ── 遗留单例(已废弃;新代码请使用 ServiceLocator.Get())──────────── - [System.Obsolete("Use ServiceLocator.Get() instead.")] - public static AudioManager Instance { get; private set; } + private Dictionary _sfxLookup; + private readonly CompositeDisposable _subs = new(); private void Awake() { -#pragma warning disable CS0618 - if (Instance != null) { Destroy(gameObject); return; } - Instance = this; -#pragma warning restore CS0618 + if (ServiceLocator.GetOrDefault() != null) { Destroy(gameObject); return; } + + Debug.Assert(_audioConfig != null, "[AudioManager] _audioConfig 未赋值,请在 Inspector 中指定 AudioConfigSO。", this); _activeBGMSource = _bgmSourceA; _inactiveBGMSource = _bgmSourceB; - // ServiceLocator 注册(覆盖 GameServiceRegistrar 的 NullAudioService 兜底) ServiceLocator.Register(this); + BuildSFXLookup(); + Initialize(); } private void OnEnable() { - if (_onPlayerDied != null) - _onPlayerDied.OnEventRaised += HandlePlayerDied; + _onPlayerDied?.Subscribe(HandlePlayerDied).AddTo(_subs); } private void OnDisable() { - if (_onPlayerDied != null) - _onPlayerDied.OnEventRaised -= HandlePlayerDied; + _subs.Clear(); } - // ── IAudioService string-key API(Phase 2 接入 AudioEventSO 后完整实现)───────────── - /// - /// 按 Addressable key 播放 BGM。Phase 2 接入 AudioEventSO 前为占位警告。 - /// - public void PlayBGM(string key) - => Debug.LogWarning($"[AudioManager] PlayBGM(key) 尚未接入 AudioEventSO(Phase 2)。key={key}"); + private void OnDestroy() + { + ServiceLocator.Unregister(this); + } - /// - /// 按 Addressable key 播放 SFX。Phase 2 接入 AudioEventSO 前为占位警告。 - /// + // ── IAudioService string-key API ──────────────────────────────────────────── + /// 按 Zone/Boss key 查 AudioConfigSO 播放 BGM。 + public void PlayBGM(string key) + { + var clip = _audioConfig.GetZoneBGM(key) ?? _audioConfig.GetBossBGM(key); + if (clip == null) + { + Debug.LogWarning($"[AudioManager] BGM key '{key}' 在 AudioConfigSO 中未找到。"); + return; + } + PlayBGM(clip); + } + + /// 按 key 查 SFX 注册表播放 AudioEventSO。 public void PlaySFX(string key) - => Debug.LogWarning($"[AudioManager] PlaySFX(key) 尚未接入 AudioEventSO(Phase 2)。key={key}"); + { + if (_sfxLookup != null && _sfxLookup.TryGetValue(key, out var evt)) + { + evt?.PlayOneShot(NextSFXSource()); + return; + } + Debug.LogWarning($"[AudioManager] SFX key '{key}' 未在注册表中找到。"); + } // ── 音量控制 ───────────────────────────────────────────────────────────── /// @@ -83,10 +109,15 @@ namespace BaseGames.Audio public void SetVolume(string exposedParam, float linear) => _mixer.SetFloat(exposedParam, LinearToDecibel(linear)); - /// 读取 GlobalSettings 并应用所有音量初始值。 + /// 读取 SettingsManager 已加载的设置数据并应用四路音量到 AudioMixer。 public void Initialize() { - // TODO: 从 SettingsManager / PlayerPrefs 读取保存的音量值并应用 + var settings = ServiceLocator.GetOrDefault(); + GlobalSettingsData data = settings?.Current ?? new GlobalSettingsData(); + SetVolume(AudioMixerKeys.Master, data.MasterVolume); + SetVolume(AudioMixerKeys.BGM, data.BGMVolume); + SetVolume(AudioMixerKeys.SFX, data.SFXVolume); + SetVolume(AudioMixerKeys.Ambient, data.AmbientVolume); } // ── BGM ────────────────────────────────────────────────────────────────── @@ -113,6 +144,7 @@ namespace BaseGames.Audio { if (clip == null) return; var src = NextSFXSource(); + if (src == null) return; src.volume = volumeScale; src.PlayOneShot(clip); } @@ -138,9 +170,22 @@ namespace BaseGames.Audio TransitionToSnapshot("Dead", 1.5f); } + private void BuildSFXLookup() + { + _sfxLookup = new Dictionary(_sfxRegistry?.Length ?? 0); + if (_sfxRegistry == null) return; + foreach (var entry in _sfxRegistry) + if (!string.IsNullOrEmpty(entry.Key) && entry.Event != null) + _sfxLookup[entry.Key] = entry.Event; + } + private AudioSource NextSFXSource() { - if (_sfxSources == null || _sfxSources.Length == 0) return _bgmSourceA; + if (_sfxSources == null || _sfxSources.Length == 0) + { + Debug.LogError("[AudioManager] SFX Source 池为空,请在 Inspector 中为 _sfxSources 赋值。"); + return null; + } return _sfxSources[_sfxRoundRobin++ % _sfxSources.Length]; } diff --git a/Assets/Scripts/Audio/AudioManager.cs.meta b/Assets/Scripts/Audio/AudioManager.cs.meta index 123ba96..695cabd 100644 --- a/Assets/Scripts/Audio/AudioManager.cs.meta +++ b/Assets/Scripts/Audio/AudioManager.cs.meta @@ -4,7 +4,7 @@ MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] - executionOrder: 0 + executionOrder: -500 icon: {instanceID: 0} userData: assetBundleName: diff --git a/Assets/Scripts/Audio/BGMController.cs b/Assets/Scripts/Audio/BGMController.cs index 21aa0ad..434f198 100644 --- a/Assets/Scripts/Audio/BGMController.cs +++ b/Assets/Scripts/Audio/BGMController.cs @@ -30,19 +30,23 @@ namespace BaseGames.Audio private MusicState _musicState = MusicState.Exploration; private string _currentRegion = string.Empty; + private readonly CompositeDisposable _subscriptions = new(); + + private void Awake() + { + Debug.Assert(_config != null, "[BGMController] _config 未赋值,请在 Inspector 中指定 AudioConfigSO。", this); + } private void OnEnable() { - if (_onBossFightToggled != null) _onBossFightToggled.OnEventRaised += OnBossFightToggled; - if (_onRegionEntered != null) _onRegionEntered.OnEventRaised += OnRegionEntered; - if (_onGameStateChanged != null) _onGameStateChanged.OnEventRaised += HandleStateChanged; + _onBossFightToggled?.Subscribe(OnBossFightToggled).AddTo(_subscriptions); + _onRegionEntered?.Subscribe(OnRegionEntered).AddTo(_subscriptions); + _onGameStateChanged?.Subscribe(HandleStateChanged).AddTo(_subscriptions); } private void OnDisable() { - if (_onBossFightToggled != null) _onBossFightToggled.OnEventRaised -= OnBossFightToggled; - if (_onRegionEntered != null) _onRegionEntered.OnEventRaised -= OnRegionEntered; - if (_onGameStateChanged != null) _onGameStateChanged.OnEventRaised -= HandleStateChanged; + _subscriptions.Clear(); } private void OnBossFightToggled(bool started) @@ -50,7 +54,7 @@ namespace BaseGames.Audio if (started) { _musicState = MusicState.Boss; - var clip = _config != null ? _config.GetBossBGM(_currentRegion) : null; + var clip = _config.GetBossBGM(_currentRegion); _audioManager.PlayBGM(clip, fadeOutDur: 1f, fadeInDur: 0.5f); _audioManager.TransitionToSnapshot("BossFight", 0.5f); } @@ -63,9 +67,9 @@ namespace BaseGames.Audio private IEnumerator PlayVictoryThenRestore() { _musicState = MusicState.Victory; - _audioManager.PlayBGM(_config != null ? _config.VictoryStingBGM : null, + _audioManager.PlayBGM(_config.VictoryStingBGM, fadeOutDur: 0.3f, fadeInDur: 0.1f); - float dur = _config != null ? _config.VictoryStingDuration : 4f; + float dur = _config.VictoryStingDuration; yield return new WaitForSecondsRealtime(dur); _musicState = MusicState.Exploration; OnRegionEntered(_currentRegion); @@ -78,16 +82,15 @@ namespace BaseGames.Audio _currentRegion = regionId; if (_musicState == MusicState.Exploration) { - var clip = _config != null ? _config.GetZoneBGM(regionId) : null; + var clip = _config.GetZoneBGM(regionId); _audioManager.PlayBGM(clip, fadeOutDur: 1f, fadeInDur: 1f); } } private void HandleStateChanged(GameStateId state) { - // ⚠️ GameStateId 是 struct,不能用 switch;使用 if/else + GameStates 常量 if (state == GameStates.MainMenu) - _audioManager.PlayBGM(_config != null ? _config.MainMenuBGM : null, + _audioManager.PlayBGM(_config.MainMenuBGM, fadeOutDur: 0.5f, fadeInDur: 1.0f); else if (state == GameStates.Paused) _audioManager.TransitionToSnapshot("Paused", 0.2f); diff --git a/Assets/Scripts/Audio/CombatSFXController.cs b/Assets/Scripts/Audio/CombatSFXController.cs index bdf8124..987611f 100644 --- a/Assets/Scripts/Audio/CombatSFXController.cs +++ b/Assets/Scripts/Audio/CombatSFXController.cs @@ -5,8 +5,9 @@ using BaseGames.Combat; namespace BaseGames.Audio { /// - /// 订阅战斗/死亡事件,通过 AudioManager 播放对应 SFX。 + /// 订阅战斗/死亡事件,通过 GlobalSFXPlayer 播放对应 AudioEventSO 音效。 /// 挂载在 Persistent 场景的 [Systems] GameObject 上。 + /// 使用 AudioEventSO 替代裸 AudioClip,支持随机音量 / 音调 / 多片段。 /// public class CombatSFXController : MonoBehaviour { @@ -15,57 +16,52 @@ namespace BaseGames.Audio [SerializeField] private VoidEventChannelSO _onPlayerDied; [Header("Default Hit SFX")] - [SerializeField] private AudioClip _defaultHitSFX; + [SerializeField] private AudioEventSO _defaultHitSFX; [Header("Per-Type Hit SFX (optional, overrides default)")] - [SerializeField] private AudioClip _sparkHitSFX; - [SerializeField] private AudioClip _slashHitSFX; - [SerializeField] private AudioClip _bloodHitSFX; - [SerializeField] private AudioClip _magicHitSFX; - [SerializeField] private AudioClip _heavyHitSFX; - [SerializeField] private AudioClip _critHitSFX; - [SerializeField] private AudioClip _parryHitSFX; - [SerializeField] private AudioClip _fireHitSFX; - [SerializeField] private AudioClip _iceHitSFX; + [SerializeField] private AudioEventSO _sparkHitSFX; + [SerializeField] private AudioEventSO _slashHitSFX; + [SerializeField] private AudioEventSO _bloodHitSFX; + [SerializeField] private AudioEventSO _magicHitSFX; + [SerializeField] private AudioEventSO _heavyHitSFX; + [SerializeField] private AudioEventSO _critHitSFX; + [SerializeField] private AudioEventSO _parryHitSFX; + [SerializeField] private AudioEventSO _fireHitSFX; + [SerializeField] private AudioEventSO _iceHitSFX; [Header("Death SFX")] - [SerializeField] private AudioClip _playerDeathSFX; + [SerializeField] private AudioEventSO _playerDeathSFX; + + private readonly CompositeDisposable _subs = new(); private void OnEnable() { - if (_onHitConfirmed != null) - _onHitConfirmed.OnEventRaised += HandleHit; - - if (_onPlayerDied != null) - _onPlayerDied.OnEventRaised += HandlePlayerDied; + _onHitConfirmed?.Subscribe(HandleHit).AddTo(_subs); + _onPlayerDied?.Subscribe(HandlePlayerDied).AddTo(_subs); } private void OnDisable() { - if (_onHitConfirmed != null) - _onHitConfirmed.OnEventRaised -= HandleHit; - - if (_onPlayerDied != null) - _onPlayerDied.OnEventRaised -= HandlePlayerDied; + _subs.Clear(); } private void HandleHit(HitInfo info) { - AudioClip clip = ResolveHitClip(info.DamageInfo.FxType); - if (clip == null) return; + AudioEventSO sfx = ResolveHitSFX(info.DamageInfo.FxType); + if (sfx == null) return; - AudioManager.Instance.PlaySFXAtPosition(clip, info.HitPoint); + GlobalSFXPlayer.Play(sfx, info.HitPoint); } private void HandlePlayerDied() { if (_playerDeathSFX == null) return; - AudioManager.Instance.PlaySFX(_playerDeathSFX); + GlobalSFXPlayer.Play(_playerDeathSFX); } - private AudioClip ResolveHitClip(HitFxType fxType) + private AudioEventSO ResolveHitSFX(HitFxType fxType) { - AudioClip perType = fxType switch + AudioEventSO perType = fxType switch { HitFxType.Spark => _sparkHitSFX, HitFxType.Slash => _slashHitSFX, diff --git a/Assets/Scripts/Audio/FootstepAudioConfigSO.cs b/Assets/Scripts/Audio/FootstepAudioConfigSO.cs new file mode 100644 index 0000000..f87adea --- /dev/null +++ b/Assets/Scripts/Audio/FootstepAudioConfigSO.cs @@ -0,0 +1,30 @@ +// Assets/Scripts/Audio/FootstepAudioConfigSO.cs +// 脚步声音效配置(Architecture 21_LiquidPuzzleModule §3.3) +using System; +using UnityEngine; + +namespace BaseGames.Audio +{ + [CreateAssetMenu(menuName = "BaseGames/Audio/FootstepAudioConfig")] + public class FootstepAudioConfigSO : ScriptableObject + { + [Serializable] + public struct MaterialEntry + { + public FootstepMaterial material; + public AudioClip[] clips; // 随机选一个,防止重复感 + [Range(0f, 1f)] public float volume; + [Range(0.8f, 1.2f)] public float pitchVariance; // 每次随机 pitch 偏移范围 + } + + public MaterialEntry[] entries; + + public MaterialEntry? GetEntry(FootstepMaterial mat) + { + if (entries == null) return null; + foreach (var e in entries) + if (e.material == mat) return e; + return null; + } + } +} diff --git a/Assets/Scripts/Audio/FootstepAudioConfigSO.cs.meta b/Assets/Scripts/Audio/FootstepAudioConfigSO.cs.meta new file mode 100644 index 0000000..f161532 --- /dev/null +++ b/Assets/Scripts/Audio/FootstepAudioConfigSO.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 902dc34e76d63a041a7e9a564b89a5e0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Audio/FootstepMaterial.cs b/Assets/Scripts/Audio/FootstepMaterial.cs new file mode 100644 index 0000000..ba76bde --- /dev/null +++ b/Assets/Scripts/Audio/FootstepMaterial.cs @@ -0,0 +1,16 @@ +// Assets/Scripts/Audio/FootstepMaterial.cs +// 脚步声材质枚举(Architecture 21_LiquidPuzzleModule §3.3) +namespace BaseGames.Audio +{ + public enum FootstepMaterial + { + Stone, // 石板地(默认) + Dirt, // 泥土/草地 + Wood, // 木板 + Metal, // 金属格栅 + Water, // 浅水区(溅水声) + Sand, // 沙地 + Grass, // 草丛 + Cave, // 洞穴(回响加强) + } +} diff --git a/Assets/Scripts/Audio/FootstepMaterial.cs.meta b/Assets/Scripts/Audio/FootstepMaterial.cs.meta new file mode 100644 index 0000000..f4c8786 --- /dev/null +++ b/Assets/Scripts/Audio/FootstepMaterial.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3dc331b476a760a49b238b6645aae052 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Audio/FootstepMaterialMarker.cs b/Assets/Scripts/Audio/FootstepMaterialMarker.cs new file mode 100644 index 0000000..9934f50 --- /dev/null +++ b/Assets/Scripts/Audio/FootstepMaterialMarker.cs @@ -0,0 +1,16 @@ +// Assets/Scripts/Audio/FootstepMaterialMarker.cs +// 挂载到地面碰撞体所在 GameObject,标记该地面的脚步声材质 +// (Architecture 21_LiquidPuzzleModule §3.3) +using UnityEngine; + +namespace BaseGames.Audio +{ + /// + /// 挂载到地面碰撞体所在 GameObject(Tilemap 图层 or 单体地形 Prefab)。 + /// 若玩家脚下 GameObject 无此组件,默认使用 FootstepMaterial.Stone。 + /// + public class FootstepMaterialMarker : MonoBehaviour + { + public FootstepMaterial material = FootstepMaterial.Stone; + } +} diff --git a/Assets/Scripts/Audio/FootstepMaterialMarker.cs.meta b/Assets/Scripts/Audio/FootstepMaterialMarker.cs.meta new file mode 100644 index 0000000..d46037e --- /dev/null +++ b/Assets/Scripts/Audio/FootstepMaterialMarker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9bed4b6cdf4d7064793fe22fefccce43 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Audio/GlobalSFXPlayer.cs b/Assets/Scripts/Audio/GlobalSFXPlayer.cs new file mode 100644 index 0000000..b7f1242 --- /dev/null +++ b/Assets/Scripts/Audio/GlobalSFXPlayer.cs @@ -0,0 +1,44 @@ +using UnityEngine; +using BaseGames.Core; + +namespace BaseGames.Audio +{ + /// + /// 全局 SFX 播放入口(单例 MonoBehaviour)。 + /// 通过 静态方法播放 , + /// 可选传入世界坐标以在指定位置 3D 播放。 + /// + public class GlobalSFXPlayer : MonoBehaviour + { + private static GlobalSFXPlayer _instance; + + [SerializeField] private AudioSource _globalSFXSource; + + private void Awake() + { + if (_instance != null) { Destroy(gameObject); return; } + _instance = this; + } + + /// + /// 播放一个音效事件。 + /// 若传入 ,则在该位置 3D 播放;否则使用全局 AudioSource 2D 播放。 + /// + public static void Play(AudioEventSO audioEvent, Vector2? worldPos = null) + { + if (audioEvent == null || _instance == null) return; + + if (worldPos.HasValue) + { + var clip = audioEvent.PickClip(); + if (clip != null) + ServiceLocator.GetOrDefault() + ?.PlaySFXAtPosition(clip, worldPos.Value, audioEvent.PickVolume()); + } + else + { + audioEvent.Play(_instance._globalSFXSource); + } + } + } +} diff --git a/Assets/Scripts/Audio/GlobalSFXPlayer.cs.meta b/Assets/Scripts/Audio/GlobalSFXPlayer.cs.meta new file mode 100644 index 0000000..e90932f --- /dev/null +++ b/Assets/Scripts/Audio/GlobalSFXPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8275d76dd985c4c419f4c477b9275de3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Audio/UnderwaterAudioController.cs b/Assets/Scripts/Audio/UnderwaterAudioController.cs new file mode 100644 index 0000000..0fd1455 --- /dev/null +++ b/Assets/Scripts/Audio/UnderwaterAudioController.cs @@ -0,0 +1,30 @@ +// Assets/Scripts/Audio/UnderwaterAudioController.cs +// 进入 LiquidZone 时切换水下 DSP 处理(Architecture 21_LiquidPuzzleModule §3.4) +using UnityEngine; +using UnityEngine.Audio; + +namespace BaseGames.Audio +{ + /// + /// 挂载于 PlayerController 所在 GameObject。 + /// 由 LiquidZone.OnTriggerEnter2D / OnTriggerExit2D 直接调用。 + /// 切换 AudioMixer Snapshot 以应用/解除水下 DSP 处理。 + /// + public class UnderwaterAudioController : MonoBehaviour + { + [SerializeField] private AudioMixer _mixer; + [SerializeField] private float _transitionDuration = 0.3f; + + /// 玩家进入 Water 类型液体时调用。 + public void EnterWater() + { + _mixer?.FindSnapshot("Underwater")?.TransitionTo(_transitionDuration); + } + + /// 玩家离开液体时调用。 + public void ExitWater() + { + _mixer?.FindSnapshot("Default")?.TransitionTo(_transitionDuration); + } + } +} diff --git a/Assets/Scripts/Audio/UnderwaterAudioController.cs.meta b/Assets/Scripts/Audio/UnderwaterAudioController.cs.meta new file mode 100644 index 0000000..c93b09b --- /dev/null +++ b/Assets/Scripts/Audio/UnderwaterAudioController.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9d32189c7a2ecbe4caf2bf3d8aa174f2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Audio/_Placeholder.cs b/Assets/Scripts/Audio/_Placeholder.cs deleted file mode 100644 index f7c4065..0000000 --- a/Assets/Scripts/Audio/_Placeholder.cs +++ /dev/null @@ -1,3 +0,0 @@ -// Placeholder to prevent asmdef-no-scripts warning. -namespace BaseGames.Audio { } - diff --git a/Assets/Scripts/Camera/CameraStateController.cs b/Assets/Scripts/Camera/CameraStateController.cs index f4ffa8f..979b412 100644 --- a/Assets/Scripts/Camera/CameraStateController.cs +++ b/Assets/Scripts/Camera/CameraStateController.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using UnityEngine; using Unity.Cinemachine; +using BaseGames.Core; namespace BaseGames.Camera { @@ -9,10 +10,8 @@ namespace BaseGames.Camera /// 须放置在持久化场景中。 /// [DefaultExecutionOrder(-100)] - public class CameraStateController : MonoBehaviour + public class CameraStateController : MonoBehaviour, ICameraService { - public static CameraStateController Instance { get; private set; } - [Header("引用")] [SerializeField] private CinemachineBrain _brain; [SerializeField] private CinemachineImpulseSource _impulseSource; @@ -27,17 +26,13 @@ namespace BaseGames.Camera // ── Unity Lifecycle ─────────────────────────────────────────────────── private void Awake() { - if (Instance != null && Instance != this) - { - Destroy(gameObject); - return; - } - Instance = this; + if (ServiceLocator.GetOrDefault() != null) { Destroy(gameObject); return; } + ServiceLocator.Register(this); } private void OnDestroy() { - if (Instance == this) Instance = null; + ServiceLocator.Unregister(this); } // ── 公开 API ────────────────────────────────────────────────────────── diff --git a/Assets/Scripts/Camera/CameraTriggerZone.cs b/Assets/Scripts/Camera/CameraTriggerZone.cs index a188d5e..de4c234 100644 --- a/Assets/Scripts/Camera/CameraTriggerZone.cs +++ b/Assets/Scripts/Camera/CameraTriggerZone.cs @@ -1,4 +1,5 @@ using UnityEngine; +using BaseGames.Core; namespace BaseGames.Camera { @@ -26,7 +27,7 @@ namespace BaseGames.Camera if (!Application.isPlaying) return; if (!other.CompareTag(_playerTag)) return; if (_targetCamera != null) - CameraStateController.Instance?.SwitchRoom(_targetCamera); + ServiceLocator.GetOrDefault()?.SwitchRoom(_targetCamera); } private void OnDrawGizmos() diff --git a/Assets/Scripts/Camera/ICameraService.cs b/Assets/Scripts/Camera/ICameraService.cs new file mode 100644 index 0000000..4945c3e --- /dev/null +++ b/Assets/Scripts/Camera/ICameraService.cs @@ -0,0 +1,18 @@ +namespace BaseGames.Camera +{ + /// + /// 相机服务接口。供 RoomController / CameraTriggerZone 等调用, + /// 通过 ServiceLocator.Get<ICameraService>() 访问,无需直接依赖 CameraStateController。 + /// + public interface ICameraService + { + /// 切换到目标房间相机。 + void SwitchRoom(RoomCamera targetCamera); + + /// 注册一个房间相机到控制器注册表。 + void RegisterRoomCamera(RoomCamera camera); + + /// 注销一个房间相机。 + void UnregisterRoomCamera(RoomCamera camera); + } +} diff --git a/Assets/Scripts/Camera/ICameraService.cs.meta b/Assets/Scripts/Camera/ICameraService.cs.meta new file mode 100644 index 0000000..43c6a06 --- /dev/null +++ b/Assets/Scripts/Camera/ICameraService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 94f7141340a22a54aa504d7c6a09eeb3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/ArcProjectile.cs b/Assets/Scripts/Combat/ArcProjectile.cs new file mode 100644 index 0000000..57aec7f --- /dev/null +++ b/Assets/Scripts/Combat/ArcProjectile.cs @@ -0,0 +1,27 @@ +using UnityEngine; + +namespace BaseGames.Combat +{ + /// + /// 抛物线抛射物。以 指定抛射角, + /// 并启用重力缩放,使子弹呈弧线飞行。 + /// + public class ArcProjectile : Projectile + { + protected override void OnInitialized() + { + float rad = _config.LaunchAngleDeg * Mathf.Deg2Rad; + float vX = Mathf.Cos(rad) * _config.Speed * Mathf.Sign(Direction.x == 0f ? 1f : Direction.x); + float vY = Mathf.Sin(rad) * _config.Speed; + + _rb.velocity = new Vector2(vX, vY); + _rb.gravityScale = _config.GravityScale; + } + + protected override void OnDisable() + { + base.OnDisable(); + _rb.gravityScale = 0f; + } + } +} diff --git a/Assets/Scripts/Combat/ArcProjectile.cs.meta b/Assets/Scripts/Combat/ArcProjectile.cs.meta new file mode 100644 index 0000000..73962a7 --- /dev/null +++ b/Assets/Scripts/Combat/ArcProjectile.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f43e5039135c2f84682862a9249e2688 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/BaseGames.Combat.asmdef b/Assets/Scripts/Combat/BaseGames.Combat.asmdef index 07dafca..a3cc749 100644 --- a/Assets/Scripts/Combat/BaseGames.Combat.asmdef +++ b/Assets/Scripts/Combat/BaseGames.Combat.asmdef @@ -8,6 +8,7 @@ "versionDefines": [], "rootNamespace": "BaseGames.Combat", "references": [ + "BaseGames.Core", "BaseGames.Core.Events", "BaseGames.Parry" ], diff --git a/Assets/Scripts/Combat/ClashConfigSO.cs b/Assets/Scripts/Combat/ClashConfigSO.cs new file mode 100644 index 0000000..8e10bc1 --- /dev/null +++ b/Assets/Scripts/Combat/ClashConfigSO.cs @@ -0,0 +1,24 @@ +using UnityEngine; + +namespace BaseGames.Combat +{ + /// + /// 拼刀系统参数配置(架构 06_CombatModule §15)。 + /// 资产路径: Assets/ScriptableObjects/Config/Combat/ClashConfig.asset + /// + [CreateAssetMenu(menuName = "Combat/ClashConfig")] + public class ClashConfigSO : ScriptableObject + { + [Header("HitStop")] + [Tooltip("拼刀冻帧数(比普通命中的 2 帧更短)")] + public int ClashFreezeFrames = 1; + + [Header("弹开")] + [Tooltip("拼刀弹开力度(Impulse 模式)")] + public float ClashKnockbackForce = 6.0f; + + [Header("Camera Impulse")] + [Tooltip("Cinemachine Impulse 强度(轻微震感)")] + public float ClashImpulseStrength = 0.3f; + } +} diff --git a/Assets/Scripts/Combat/ClashConfigSO.cs.meta b/Assets/Scripts/Combat/ClashConfigSO.cs.meta new file mode 100644 index 0000000..db3c3f2 --- /dev/null +++ b/Assets/Scripts/Combat/ClashConfigSO.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d2dce4002cf4f99429388b2038fa2f60 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/ClashResolver.cs b/Assets/Scripts/Combat/ClashResolver.cs new file mode 100644 index 0000000..c179c30 --- /dev/null +++ b/Assets/Scripts/Combat/ClashResolver.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using UnityEngine; +using BaseGames.Core; +using BaseGames.Core.Events; + +namespace BaseGames.Combat +{ + /// + /// 拼刀系统单例服务(架构 06_CombatModule §15)。 + /// 常驻 Persistent 场景,由 GameManager 持有。 + /// 当玩家与敌人的近战 HitBox 同时激活并相互重叠时触发拼刀:双方均不扣血,各自弹开。 + /// + [DefaultExecutionOrder(-500)] + public class ClashResolver : MonoBehaviour, IClashService + { + [SerializeField] private VoidEventChannelSO _onNailClash; // EVT_NailClash + [SerializeField] private ClashConfigSO _config; + + // 防止同一碰撞在同帧被双方 HitBox 各触发一次(去重) + // Key = (min(idA,idB), max(idA,idB)),顺序无关且无 XOR 哈希碰撞风险 + private readonly HashSet<(int, int)> _processedThisFrame = new(); + + private void Awake() + { + if (ServiceLocator.GetOrDefault() != null) + { + Destroy(gameObject); + return; + } + ServiceLocator.Register(this); + Debug.Assert(_config != null, "[ClashResolver] _config 未赋值,请在 Inspector 中指定 ClashConfigSO。", this); + } + + private void LateUpdate() => _processedThisFrame.Clear(); + + /// + /// 由 HitBox.OnTriggerEnter2D 调用。 + /// 对同一对 HitBox 每帧只处理一次(HashSet 去重)。 + /// + public void ResolveClash(HitBox hitBoxA, HitBox hitBoxB) + { + int idA = hitBoxA.GetInstanceID(); + int idB = hitBoxB.GetInstanceID(); + (int, int) key = (System.Math.Min(idA, idB), System.Math.Max(idA, idB)); + if (!_processedThisFrame.Add(key)) return; + + // 1. 拼刀冻帧(比普通命中的 2 帧更短,强化拼刀手感) + ServiceLocator.GetOrDefault()?.FreezeFrames(_config.ClashFreezeFrames); + + // 2. 双方弹开 + ApplyClashKnockback(hitBoxA.OwnerRigidbody, hitBoxB.transform.position); + ApplyClashKnockback(hitBoxB.OwnerRigidbody, hitBoxA.transform.position); + + // 3. 广播事件(VFX / Audio / CameraImpulse 订阅) + _onNailClash?.Raise(); + } + + private void ApplyClashKnockback(Rigidbody2D rb, Vector2 oppositePos) + { + if (rb == null) return; + Vector2 dir = ((Vector2)rb.transform.position - oppositePos).normalized; + rb.AddForce(dir * _config.ClashKnockbackForce, ForceMode2D.Impulse); + } + + private void OnDestroy() + { + ServiceLocator.Unregister(this); + } + } +} diff --git a/Assets/Scripts/Combat/ClashResolver.cs.meta b/Assets/Scripts/Combat/ClashResolver.cs.meta new file mode 100644 index 0000000..b2f1942 --- /dev/null +++ b/Assets/Scripts/Combat/ClashResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: df7a93b79e446e24dbb27d2971c85f39 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/DamageInfo.cs b/Assets/Scripts/Combat/DamageInfo.cs index d0d5bad..a33f87b 100644 --- a/Assets/Scripts/Combat/DamageInfo.cs +++ b/Assets/Scripts/Combat/DamageInfo.cs @@ -5,12 +5,13 @@ namespace BaseGames.Combat { /// /// 单次伤害信息。流水线:RawDamage → Amount(护盾修改)→ FinalDamage(防御减免后)。 - /// ⚠️ 非 readonly struct — Builder 就地写入字段。 + /// ⚠️ 保留为可变 struct:HurtBox 流水线需要在方法内修改本地副本的 Amount / FinalDamage。 + /// Builder 通过独立字段构造,不直接修改 DamageInfo 实例。 /// [System.Serializable] public struct DamageInfo { - public int RawDamage; // HitBox 设定的原始值(Builder.SetRaw 写入一次) + public int RawDamage; // HitBox 设定的原始值(工厂/Builder 写入一次) public int Amount; // 流水线中被护盾/防御修改 public int FinalDamage; // HurtBox 写入,最终 HP 扣除量 public Vector2 KnockbackDirection; @@ -28,50 +29,92 @@ namespace BaseGames.Combat public string SkillId; // ── Builder ────────────────────────────────────────────────────────── + /// + /// 通过独立字段构造 DamageInfo,避免直接持有可变 DamageInfo 实例。 + /// public class Builder { - private DamageInfo _d; + private int _raw; + private DamageType _type; + private DamageCategory _category; + private DamageFlags _flags; + private DamageTags _tags; + private string _skillId; + private string _sourceId; + private Vector2 _knockbackDirection; + private float _knockbackForce; + private float _hitStunDuration; + private HitFxType _fxType; + private BreakLevel _break; + private Vector2 _sourcePosition; + private int _sourceLayer; public Builder() { } // SetRaw 同步初始化 Amount(Amount 始终以 RawDamage 为起点) - public Builder SetRaw(int v) { _d.RawDamage = v; _d.Amount = v; return this; } - public Builder SetType(DamageType v) { _d.Type = v; return this; } - public Builder SetCategory(DamageCategory v){ _d.Category = v; return this; } - public Builder SetFlags(DamageFlags v) { _d.Flags = v; return this; } - public Builder SetTags(DamageTags v) { _d.Tags = v; return this; } - public Builder SetSkillId(string v) { _d.SkillId = v; return this; } - public Builder SetSourceId(string v) { _d.SourceId = v; return this; } - public Builder SetKnockback(Vector2 dir, float force) - { _d.KnockbackDirection = dir; _d.KnockbackForce = force; return this; } - public Builder SetStun(float dur) { _d.HitStunDuration = dur; return this; } - public Builder SetFx(HitFxType v) { _d.FxType = v; return this; } - public Builder SetBreak(BreakLevel v) { _d.Break = v; return this; } - public Builder SetSourcePos(Vector2 v) { _d.SourcePosition = v; return this; } - public Builder SetLayer(int v) { _d.SourceLayer = v; return this; } - public DamageInfo Build() => _d; + public Builder SetRaw(int v) { _raw = v; return this; } + public Builder SetType(DamageType v) { _type = v; return this; } + public Builder SetCategory(DamageCategory v) { _category = v; return this; } + public Builder SetFlags(DamageFlags v) { _flags = v; return this; } + public Builder SetTags(DamageTags v) { _tags = v; return this; } + public Builder SetSkillId(string v) { _skillId = v; return this; } + public Builder SetSourceId(string v) { _sourceId = v; return this; } + public Builder SetKnockback(Vector2 dir, float force) { _knockbackDirection = dir; _knockbackForce = force; return this; } + public Builder SetStun(float dur) { _hitStunDuration = dur; return this; } + public Builder SetFx(HitFxType v) { _fxType = v; return this; } + public Builder SetBreak(BreakLevel v) { _break = v; return this; } + public Builder SetSourcePos(Vector2 v) { _sourcePosition = v; return this; } + public Builder SetLayer(int v) { _sourceLayer = v; return this; } + + public DamageInfo Build() => new DamageInfo + { + RawDamage = _raw, + Amount = _raw, + Type = _type, + Category = _category, + Flags = _flags, + Tags = _tags, + SkillId = _skillId, + SourceId = _sourceId, + KnockbackDirection = _knockbackDirection, + KnockbackForce = _knockbackForce, + HitStunDuration = _hitStunDuration, + FxType = _fxType, + Break = _break, + SourcePosition = _sourcePosition, + SourceLayer = _sourceLayer, + }; } /// - /// ⚡ 零堆分配工厂(热路径首选)。直接从 DamageSourceSO 填入基础字段。 - /// KnockbackDirection / SourcePosition / SourceLayer 等运行时字段由调用方就地赋值。 + /// ⚡ 零堆分配工厂(热路径首选)。从 DamageSourceSO 填入所有静态字段; + /// 可选传入运行时字段(knockbackDir、sourcePos、sourceLayer), + /// 无需调用方事后就地赋值。 /// - public static DamageInfo From(DamageSourceSO so) + public static DamageInfo From( + DamageSourceSO so, + Vector2 knockbackDir = default, + Vector2 sourcePos = default, + int sourceLayer = 0) { int baseAmt = Mathf.RoundToInt(so.BaseDamage * so.DamageMultiplier); return new DamageInfo { - RawDamage = baseAmt, - Amount = baseAmt, - Type = so.Type, - Category = so.Category, - Flags = so.Flags, - Tags = so.Tags, - HitStunDuration = so.HitStunDuration, - FxType = so.FxType, - Break = so.BreakLevel, - SourceId = so.sourceId, - SkillId = so.skillId, + RawDamage = baseAmt, + Amount = baseAmt, + Type = so.Type, + Category = so.Category, + Flags = so.Flags, + Tags = so.Tags, + HitStunDuration = so.HitStunDuration, + FxType = so.FxType, + Break = so.BreakLevel, + SourceId = so.sourceId, + SkillId = so.skillId, + KnockbackDirection = knockbackDir, + KnockbackForce = so.KnockbackForce, + SourcePosition = sourcePos, + SourceLayer = sourceLayer, }; } } diff --git a/Assets/Scripts/Combat/HitBox.cs b/Assets/Scripts/Combat/HitBox.cs index b5a4f56..61f3596 100644 --- a/Assets/Scripts/Combat/HitBox.cs +++ b/Assets/Scripts/Combat/HitBox.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; using UnityEngine; +using BaseGames.Core; namespace BaseGames.Combat { /// /// 攻击判定盒。挂载在武器 Prefab 或技能 HitBox Prefab 的子节点上。 - /// Phase 1 简化:直接挂在 Player Prefab 子节点 [HitBoxGround/Up/Down/Air]。 + /// 直接挂在 Player Prefab 子节点 [HitBoxGround/Up/Down/Air]。 /// Collider2D 需设 IsTrigger = true,Layer = PlayerHitBox 或 EnemyHitBox。 /// [RequireComponent(typeof(Collider2D))] @@ -14,12 +15,38 @@ namespace BaseGames.Combat [SerializeField] private DamageSourceSO _defaultSource; [SerializeField] private float _hitCooldown = 0.1f; + /// + /// HitBox 标识符,供 PlayerAnimationEvents / EnemyAnimationEvents 按名称精确激活特定判定盒。 + /// 留空表示"无 Id";事件 payload 为空时将操作所有 HitBox。 + /// + [SerializeField] private string _id = ""; + public string Id => _id; + + /// + /// 对立阵营 HitBox 所在的 Layer 掩码(用于拼刀检测)。 + /// Inspector 中将 PlayerHitBox 与 EnemyHitBox 两个 Layer 均勾选。 + /// + [SerializeField] private LayerMask _rivalHitBoxMask; + private DamageSourceSO _currentSource; private Transform _attackerTransform; + private Rigidbody2D _ownerRigidbody; private bool _isActive; + private IClashService _clashService; + + /// HitBox 当前是否激活(供 ClashResolver 查询)。 + public bool IsActive => _isActive; + + /// 当前 Source 是否携带 CanClash 标记(供 ClashResolver 查询)。 + public bool CanClash => _currentSource != null && _currentSource.Flags.HasFlag(DamageFlags.CanClash); + + /// 宿主角色的 Rigidbody2D(用于拼刀弹开力计算)。 + public Rigidbody2D OwnerRigidbody => _ownerRigidbody; + + // 拼刀检测所需的对立层掩码(Inspector 配置) /// 命中确认委托(PlayerCombat / EnemyCombat 订阅)。 - public System.Action OnHitConfirmed; + public event System.Action OnHitConfirmed; /// /// 激活 HitBox。source/attacker 均可选,未传则使用 Inspector 默认值。 @@ -30,9 +57,25 @@ namespace BaseGames.Combat _currentSource = source ?? _defaultSource; _attackerTransform = attacker ?? transform; _isActive = true; + // 缓存宿主 Rigidbody2D(沿父层级向上查找) + _ownerRigidbody = _attackerTransform.GetComponentInParent(); + // 每次激活清空当前激活期已命中目标集合(防止连击连段导致同一阶段多次命中目标) + _hitThisActivation.Clear(); + _hitCooldownTimers.Clear(); } - public void Deactivate() => _isActive = false; + public void Deactivate() + { + _isActive = false; + _hitThisActivation.Clear(); + _hitCooldownTimers.Clear(); + } + + /// 仅替换当前 DamageSource(不改变激活状态,供 PlayerCombat 连击段切换)。 + public void SetDamageSource(DamageSourceSO source) + { + if (source != null) _currentSource = source; + } private void Awake() { @@ -40,35 +83,60 @@ namespace BaseGames.Combat var col = GetComponent(); if (!col.isTrigger) Debug.LogWarning($"[HitBox] {name}: Collider2D.isTrigger 应为 true。", this); + // 缓存 IClashService:OnTriggerEnter2D 为物理热路径,避免每次调用 Dictionary 查找 + _clashService = ServiceLocator.GetOrDefault(); } private void OnDisable() { _isActive = false; + _hitThisActivation.Clear(); _hitCooldownTimers.Clear(); } - private void OnTriggerEnter2D(Collider2D other) + private void OnTriggerExit2D(Collider2D other) { + // 目标离开判定区域时清除其冷却记录,防止持续激活的 HitBox(环境危险等) + // 因有效目标持续流动而无限积累已离场对象。 + // 注意:_hitThisActivation 刻意保留,确保同一激活期内不重复命中。 + _hitCooldownTimers.Remove(other); + } + + private void OnTriggerEnter2D(Collider2D other) { if (!_isActive) return; if (_currentSource == null) { Debug.LogWarning($"[HitBox] {name}: 无 DamageSourceSO,跳过命中。", this); return; } + // 同一激活期防止对同一 Collider 重复命中(一次攻击每个目标至多命中一次) + if (!_hitThisActivation.Add(other)) return; if (!CheckCooldown(other)) return; Vector2 knockDir = ((Vector2)other.bounds.center - (Vector2)_attackerTransform.position).normalized; - // ⚡ 零 GC:struct 工厂,就地赋值运行时字段 - var info = DamageInfo.From(_currentSource); - info.KnockbackDirection = knockDir; - info.KnockbackForce = _currentSource.KnockbackForce; - info.SourcePosition = _attackerTransform.position; - info.SourceLayer = _attackerTransform.gameObject.layer; + // ⚡ 零 GC:struct 工厂,运行时字段内联传入 + var info = DamageInfo.From( + _currentSource, + knockDir, + _attackerTransform.position, + _attackerTransform.gameObject.layer); - // ① 命中 HurtBox + // ① 拼刀检测:当前 HitBox 携带 CanClash 标记,且碰到对立阵营的 HitBox 层 + int otherLayer = other.gameObject.layer; + bool isRivalHitBoxLayer = (_rivalHitBoxMask.value & (1 << otherLayer)) != 0; + if (isRivalHitBoxLayer && CanClash) + { + var rivalHitBox = other.GetComponent(); + if (rivalHitBox != null && rivalHitBox.IsActive && rivalHitBox.CanClash) + { + _clashService?.ResolveClash(this, rivalHitBox); + return; // 拼刀,中止伤害流水线 + } + } + + // ② 命中 HurtBox var hurtBox = other.GetComponent(); if (hurtBox != null) { @@ -77,12 +145,14 @@ namespace BaseGames.Combat return; } - // ② 命中 IBreakable(机关/障碍物) + // ③ 命中 IBreakable(机关/障碍物) other.GetComponent()?.TryInteract(info); } - // ── 同目标多帧命中冷却 ──────────────────────────────────────────────── - private readonly Dictionary _hitCooldownTimers = new(); + // ── 当前激活期已命中目标集合(防止复合子 Collider 导致同帧多次命中)──────────── + private readonly HashSet _hitThisActivation = new(8); + // ── 同目标多帧命中冷却(防止 Trigger 处于重叠状态时重入等殊情导致的连击)── + private readonly Dictionary _hitCooldownTimers = new(8); private bool CheckCooldown(Collider2D other) { @@ -92,5 +162,20 @@ namespace BaseGames.Combat _hitCooldownTimers[other] = now; return true; } + +#if UNITY_EDITOR + private void OnDrawGizmos() + { + var col = GetComponent(); + if (col == null) return; + // 激活时显示橙色判定框,非激活时显示极淡轮廓 + Gizmos.color = _isActive + ? new UnityEngine.Color(1f, 0.5f, 0f, 0.55f) + : new UnityEngine.Color(1f, 0.5f, 0f, 0.1f); + Gizmos.DrawCube(col.bounds.center, col.bounds.size); + Gizmos.color = new UnityEngine.Color(1f, 0.5f, 0f, 0.9f); + Gizmos.DrawWireCube(col.bounds.center, col.bounds.size); + } +#endif } } diff --git a/Assets/Scripts/Combat/HitStopManager.cs b/Assets/Scripts/Combat/HitStopManager.cs new file mode 100644 index 0000000..03e3c78 --- /dev/null +++ b/Assets/Scripts/Combat/HitStopManager.cs @@ -0,0 +1,102 @@ +using System.Collections; +using UnityEngine; +using BaseGames.Core; + +namespace BaseGames.Combat +{ + /// + /// 命中冻帧服务接口。 + /// + public interface IHitStopService + { + /// 冻帧 帧(以 fixedDeltaTime 换算为实际时长)。 + void FreezeFrames(int frames); + /// 冻帧指定时长(Unscaled 秒)。 + void FreezeDuration(float unscaledSeconds); + /// 游戏正常时间缩放(默认 1);子弹时间等功能修改此属性。 + float BaseTimeScale { get; set; } + } + + /// + /// 命中冻帧服务(HitStop)(架构 06_CombatModule §16)。 + /// 通过短暂将 Time.timeScale 设为 0 实现"冻帧"效果,强化打击感。 + /// 常驻 Persistent 场景,由 GameManager 持有;通过 ServiceLocator 注册访问。 + /// + /// 设计说明: + /// - 多次并发请求取最长持续时间(StopCoroutine + 重启) + /// - 使用 WaitForSecondsRealtime 确保 timeScale=0 时协程仍能恢复 + /// - OnDestroy 强制还原 timeScale,防止异常退出导致游戏卡死 + /// + [DefaultExecutionOrder(-400)] + public class HitStopManager : MonoBehaviour, IHitStopService + { + /// 游戏正常时间缩放(默认 1);通过属性读取以便外部修改子弹时间时保留基准值。 + public float BaseTimeScale + { + get => _baseTimeScale; + set => _baseTimeScale = Mathf.Clamp(value, 0.01f, 10f); + } + private float _baseTimeScale = 1f; + + private Coroutine _activeRoutine; + private float _freezeEndTime; + + // ── 生命周期 ────────────────────────────────────────────────────── + + private void Awake() + { + if (ServiceLocator.GetOrDefault() != null) { Destroy(gameObject); return; } + ServiceLocator.Register(this); + } + + private void OnDestroy() + { + // 安全恢复:防止场景卸载/异常退出时 timeScale 永久为 0 + Time.timeScale = _baseTimeScale; + ServiceLocator.Unregister(this); + } + + // ── 公共 API ────────────────────────────────────────────────────── + + /// + /// 冻帧 帧(以 fixedDeltaTime 换算为实际时长)。 + /// 若已有冻帧进行中,取两者中持续时间较长的(避免短请求截断较长的冻帧)。 + /// + /// 冻帧帧数(fixedDeltaTime 单位)。0 或负数无效。 + public void FreezeFrames(int frames) + { + if (frames <= 0) return; + FreezeDuration(frames * Time.fixedDeltaTime); + } + + /// + /// 冻帧指定时长(Unscaled 实际秒数)。 + /// 若已有冻帧进行中,取两者中较长的。 + /// + /// 实际时长(秒),不受 timeScale 影响。 + public void FreezeDuration(float unscaledSeconds) + { + if (unscaledSeconds <= 0f) return; + + float newEndTime = Time.unscaledTime + unscaledSeconds; + // 已有更长的冻帧进行中,放弃短请求,避免截断 + if (_activeRoutine != null && newEndTime <= _freezeEndTime) return; + + _freezeEndTime = newEndTime; + if (_activeRoutine != null) + StopCoroutine(_activeRoutine); + + _activeRoutine = StartCoroutine(FreezeRoutine(unscaledSeconds)); + } + + // ── 内部实现 ────────────────────────────────────────────────────── + + private IEnumerator FreezeRoutine(float unscaledSeconds) + { + Time.timeScale = 0f; + yield return new WaitForSecondsRealtime(unscaledSeconds); + Time.timeScale = _baseTimeScale; + _activeRoutine = null; + } + } +} diff --git a/Assets/Scripts/Combat/HitStopManager.cs.meta b/Assets/Scripts/Combat/HitStopManager.cs.meta new file mode 100644 index 0000000..a864236 --- /dev/null +++ b/Assets/Scripts/Combat/HitStopManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9aace2ae37bf9d0459a4cdeb34595885 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/HomingProjectile.cs b/Assets/Scripts/Combat/HomingProjectile.cs new file mode 100644 index 0000000..d5cb68a --- /dev/null +++ b/Assets/Scripts/Combat/HomingProjectile.cs @@ -0,0 +1,40 @@ +using UnityEngine; + +namespace BaseGames.Combat +{ + /// + /// 追踪抛射物。初始以 Direction 发射后,每帧向目标转向, + /// 转向力由 控制。 + /// + public class HomingProjectile : Projectile + { + private Transform _target; + + /// 注入追踪目标(由 ProjectileManager 在生成时调用)。 + public void SetTarget(Transform t) => _target = t; + + protected override void OnInitialized() + { + _rb.velocity = Direction * _config.Speed; + } + + protected override void Update() + { + base.Update(); + if (_target == null || _config == null) return; + + Vector2 toTarget = ((Vector2)_target.position - _rb.position).normalized; + _rb.velocity += toTarget * (_config.HomingStrength * Time.deltaTime); + + float maxSpeed = _config.Speed * 1.5f; + if (_rb.velocity.sqrMagnitude > maxSpeed * maxSpeed) + _rb.velocity = _rb.velocity.normalized * maxSpeed; + } + + protected override void OnDisable() + { + base.OnDisable(); + _target = null; + } + } +} diff --git a/Assets/Scripts/Combat/HomingProjectile.cs.meta b/Assets/Scripts/Combat/HomingProjectile.cs.meta new file mode 100644 index 0000000..c3cd333 --- /dev/null +++ b/Assets/Scripts/Combat/HomingProjectile.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cb4a2de7e8deb224db43f89dbe62748d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/HurtBox.cs b/Assets/Scripts/Combat/HurtBox.cs index f5508e5..e0262b6 100644 --- a/Assets/Scripts/Combat/HurtBox.cs +++ b/Assets/Scripts/Combat/HurtBox.cs @@ -12,10 +12,11 @@ namespace BaseGames.Combat public class HurtBox : MonoBehaviour { // ── 伤害接受方(Awake 注入)────────────────────────────────────────── - private IDamageable _owner; - private IShieldable _shieldable; // 由 PlayerController.Awake() 注入 - private ParrySystem _parrySystem; // Phase 2 由 PlayerController.Awake() 注入 - private IPoiseSource _poiseSource; // Phase 2 由 EnemyBase.Awake() 注入 + private IDamageable _owner; + private IShieldable _shieldable; // 由 PlayerController.Awake() 注入 + private ParrySystem _parrySystem; // 由 PlayerController.Awake() 注入 + private IPoiseSource _poiseSource; // 由 EnemyBase.Awake() 注入 + private IStatusEffectable _statusEffectable; // Awake 缓存,避免每次受击调用 GetComponent private bool _isHurtBoxInvincible; private bool _isActive = true; @@ -30,10 +31,18 @@ namespace BaseGames.Combat public void SetPoiseSource(IPoiseSource src) => _poiseSource = src; public void SetInvincible(bool value) => _isHurtBoxInvincible = value; public void SetActive(bool value) => _isActive = value; - +#if UNITY_EDITOR + // 付给编辑器的只读属性——避免反射并限制编辑器与运行时字段名耐合性。 + public object EditorOwner => _owner; + public object EditorShieldable => _shieldable; + public object EditorParrySystem => _parrySystem; + public object EditorPoiseSource => _poiseSource; + public object EditorStatusEffectable => _statusEffectable; +#endif private void Awake() { _owner = GetComponentInParent(); + _statusEffectable = GetComponentInParent(); if (_owner == null) Debug.LogWarning($"[HurtBox] {name}: 父节点中未找到 IDamageable 实现。", this); } @@ -50,12 +59,12 @@ namespace BaseGames.Combat if ((_owner.IsInvincible || _isHurtBoxInvincible) && !info.Flags.HasFlag(DamageFlags.IgnoreIFrame)) return; - // 2. 弹反检查(Phase 1 _parrySystem == null 跳过) + // 2. 弹反检查(_parrySystem == null 时跳过) // ParrySystem 只暴露窗口状态,伤害数据留在 Combat 层,无跨程序集数据依赖。 if (_parrySystem != null && info.Flags.HasFlag(DamageFlags.CanBeParried)) if (_parrySystem.ConsumeParry()) return; - // 3. 霸体检查(Phase 1 _poiseSource == null 跳过) + // 3. 霸体检查(_poiseSource == null 时跳过) if (!info.Flags.HasFlag(DamageFlags.ForceBreak) && _poiseSource != null) { PoiseLevel curPoise = _poiseSource.GetCurrentPoiseLevel(); @@ -96,11 +105,23 @@ namespace BaseGames.Combat }); // 8. 状态效果触发(DoT — Fire / Poison) - // 使用接口避免对 StatusEffects 程序集的直接依赖 - if (_owner is UnityEngine.MonoBehaviour mb) - { - mb.GetComponent()?.ApplyStatusEffect(info.Type); - } + // _statusEffectable 已在 Awake 中缓存,无需每次受击调用 GetComponent + _statusEffectable?.ApplyStatusEffect(info.Type); } + +#if UNITY_EDITOR + private void OnDrawGizmos() + { + var col = GetComponent(); + if (col == null) return; + // 激活时红色不透明,无敌/非激活时半透明 + Gizmos.color = (_isActive && !_isHurtBoxInvincible) + ? new UnityEngine.Color(1f, 0f, 0f, 0.45f) + : new UnityEngine.Color(1f, 0f, 0f, 0.1f); + Gizmos.DrawCube(col.bounds.center, col.bounds.size); + Gizmos.color = new UnityEngine.Color(1f, 0f, 0f, 0.9f); + Gizmos.DrawWireCube(col.bounds.center, col.bounds.size); + } +#endif } } diff --git a/Assets/Scripts/Combat/IClashService.cs b/Assets/Scripts/Combat/IClashService.cs new file mode 100644 index 0000000..7326e07 --- /dev/null +++ b/Assets/Scripts/Combat/IClashService.cs @@ -0,0 +1,11 @@ +// Assets/Scripts/Combat/IClashService.cs +// 拼刀服务接口,通过 ServiceLocator 注册与查询。 +// ClashResolver 实现此接口;HitBox 等调用方通过接口解耦。 + +namespace BaseGames.Combat +{ + public interface IClashService + { + void ResolveClash(HitBox hitBoxA, HitBox hitBoxB); + } +} diff --git a/Assets/Scripts/Combat/IClashService.cs.meta b/Assets/Scripts/Combat/IClashService.cs.meta new file mode 100644 index 0000000..810bdf7 --- /dev/null +++ b/Assets/Scripts/Combat/IClashService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7df722a95e7dc7f4b808256877e44af7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/IProjectileService.cs b/Assets/Scripts/Combat/IProjectileService.cs new file mode 100644 index 0000000..a36d320 --- /dev/null +++ b/Assets/Scripts/Combat/IProjectileService.cs @@ -0,0 +1,17 @@ +using UnityEngine; + +namespace BaseGames.Combat +{ + /// + /// 抛射物服务接口。通过 ServiceLocator 注册,供敌人 AI 生成追踪弹使用。 + /// + public interface IProjectileService + { + /// 当前缓存的玩家 Transform,生成追踪弹时注入目标。 + Transform PlayerTransform { get; } + + /// 完整初始化一枚 HomingProjectile 并注入追踪目标。 + void LaunchHoming(HomingProjectile proj, Vector2 direction, + ProjectileConfigSO config, DamageInfo damageInfo); + } +} diff --git a/Assets/Scripts/Combat/IProjectileService.cs.meta b/Assets/Scripts/Combat/IProjectileService.cs.meta new file mode 100644 index 0000000..775e221 --- /dev/null +++ b/Assets/Scripts/Combat/IProjectileService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b7d8b713a91da1d408f02c68e666f8fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/LinearProjectile.cs b/Assets/Scripts/Combat/LinearProjectile.cs new file mode 100644 index 0000000..28fe4a9 --- /dev/null +++ b/Assets/Scripts/Combat/LinearProjectile.cs @@ -0,0 +1,13 @@ +namespace BaseGames.Combat +{ + /// + /// 直线抛射物。以固定速度沿 Direction 方向飞行,无重力。 + /// + public class LinearProjectile : Projectile + { + protected override void OnInitialized() + { + _rb.velocity = Direction * _config.Speed; + } + } +} diff --git a/Assets/Scripts/Combat/LinearProjectile.cs.meta b/Assets/Scripts/Combat/LinearProjectile.cs.meta new file mode 100644 index 0000000..0e15d94 --- /dev/null +++ b/Assets/Scripts/Combat/LinearProjectile.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8e7b0c1c571010c4c9f65f953274086d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/ParryableProjectile.cs b/Assets/Scripts/Combat/ParryableProjectile.cs new file mode 100644 index 0000000..ae9e8c7 --- /dev/null +++ b/Assets/Scripts/Combat/ParryableProjectile.cs @@ -0,0 +1,58 @@ +using UnityEngine; +using BaseGames.Parry; + +namespace BaseGames.Combat +{ + /// + /// 可被玩家弹反的抛射物。 + /// 触发时优先检测弹反窗口;若成功弹反则反向飞行并可切换伤害源; + /// 否则走正常伤害流水线。 + /// + public class ParryableProjectile : LinearProjectile + { + [SerializeField] private DamageSourceSO _reflectSource; + + private bool _reflected; + + protected override void OnInitialized() + { + // 禁用子 HitBox 的自动检测,改由本组件的 OnTriggerEnter2D 手动处理, + // 以便在命中前插入弹反判断。 + _hitBox.Deactivate(); + _rb.velocity = Direction * _config.Speed; + } + + private void OnTriggerEnter2D(Collider2D other) + { + // ── 弹反判断 ──────────────────────────────────────────────── + if (!_reflected) + { + var parrySystem = other.GetComponentInParent(); + if (parrySystem != null && parrySystem.IsParrying && parrySystem.ConsumeParry()) + { + _reflected = true; + Direction = -Direction; + _rb.velocity = Direction * _config.Speed * _config.ParrySpeedMultiplier; + + if (_reflectSource != null) + DamageInfo = DamageInfo.From(_reflectSource); + return; + } + } + + // ── 正常命中 ───────────────────────────────────────────────── + var hurtBox = other.GetComponent(); + if (hurtBox != null) + { + hurtBox.ReceiveDamage(DamageInfo); + ReturnToPool(); + } + } + + protected override void OnDisable() + { + base.OnDisable(); + _reflected = false; + } + } +} diff --git a/Assets/Scripts/Combat/ParryableProjectile.cs.meta b/Assets/Scripts/Combat/ParryableProjectile.cs.meta new file mode 100644 index 0000000..d296d85 --- /dev/null +++ b/Assets/Scripts/Combat/ParryableProjectile.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 075db832266507d40bc71389d6d3f333 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/PoiseWindowConfig.cs b/Assets/Scripts/Combat/PoiseWindowConfig.cs new file mode 100644 index 0000000..5c2e551 --- /dev/null +++ b/Assets/Scripts/Combat/PoiseWindowConfig.cs @@ -0,0 +1,31 @@ +using System; + +namespace BaseGames.Combat +{ + /// + /// 描述某个状态/技能在特定动画时间段内拥有的霸体等级(架构 06_CombatModule §13)。 + /// 在状态机的 Update() 或 AnimancerEvent 中与动画归一化时间对比,决定当前霸体等级。 + /// 使用示例: + /// + /// [SerializeField] private PoiseWindowConfig[] _poiseWindows; + /// public PoiseLevel GetCurrentPoiseLevel() + /// { + /// float t = _animancer.States.Current?.NormalizedTime ?? 0f; + /// foreach (var w in _poiseWindows) + /// if (t >= w.NormalizedStart && t <= w.NormalizedEnd) + /// return w.Level; + /// return PoiseLevel.None; + /// } + /// + /// + [Serializable] + public struct PoiseWindowConfig + { + /// 此时间窗口期间的霸体等级。 + public PoiseLevel Level; + /// 动画归一化时间起点(0~1)。 + public float NormalizedStart; + /// 动画归一化时间终点(0~1)。 + public float NormalizedEnd; + } +} diff --git a/Assets/Scripts/Combat/PoiseWindowConfig.cs.meta b/Assets/Scripts/Combat/PoiseWindowConfig.cs.meta new file mode 100644 index 0000000..00b7c51 --- /dev/null +++ b/Assets/Scripts/Combat/PoiseWindowConfig.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aaa68ed270c340743958655059e74cfd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/Projectile.cs b/Assets/Scripts/Combat/Projectile.cs new file mode 100644 index 0000000..2daff08 --- /dev/null +++ b/Assets/Scripts/Combat/Projectile.cs @@ -0,0 +1,67 @@ +using UnityEngine; +using BaseGames.Core; +using BaseGames.Core.Pool; + +namespace BaseGames.Combat +{ + /// + /// 抛射物基类。子类通过重写 设定初速度。 + /// 依赖 子组件进行碰撞伤害检测。 + /// + [RequireComponent(typeof(Rigidbody2D), typeof(HitBox))] + public abstract class Projectile : MonoBehaviour + { + [HideInInspector] public DamageInfo DamageInfo; + [HideInInspector] public Vector2 Direction; + + protected ProjectileConfigSO _config; + protected Rigidbody2D _rb; + protected HitBox _hitBox; + protected float _aliveTimer; + + private PooledObject _pooledObject; + + protected virtual void Awake() + { + _rb = GetComponent(); + _hitBox = GetComponent(); + _pooledObject = GetComponent(); + } + + /// 从对象池取出后的初始化入口。 + public virtual void Initialize(ProjectileConfigSO config, DamageInfo damageInfo, Vector2 direction) + { + _config = config; + DamageInfo = damageInfo; + Direction = direction.normalized; + _aliveTimer = 0f; + + _hitBox.Activate(config.DamageSource); + OnInitialized(); + } + + /// 子类在此设定初速度或附加初始化逻辑。 + protected virtual void OnInitialized() { } + + protected virtual void Update() + { + _aliveTimer += Time.deltaTime; + if (_config != null && _aliveTimer >= _config.Lifetime) + ReturnToPool(); + } + + /// 停用并归还对象池。 + protected void ReturnToPool() + { + _hitBox.Deactivate(); + gameObject.SetActive(false); + if (_pooledObject != null && _config != null) + ServiceLocator.GetOrDefault()?.Despawn(_config.PoolKey, _pooledObject); + } + + protected virtual void OnDisable() + { + _aliveTimer = 0f; + } + } +} diff --git a/Assets/Scripts/Combat/Projectile.cs.meta b/Assets/Scripts/Combat/Projectile.cs.meta new file mode 100644 index 0000000..499142f --- /dev/null +++ b/Assets/Scripts/Combat/Projectile.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b3be0438f9175ab4f989693a8b01fdc7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/ProjectileConfigSO.cs b/Assets/Scripts/Combat/ProjectileConfigSO.cs new file mode 100644 index 0000000..66f1cc8 --- /dev/null +++ b/Assets/Scripts/Combat/ProjectileConfigSO.cs @@ -0,0 +1,28 @@ +using UnityEngine; + +namespace BaseGames.Combat +{ + /// + /// 抛射物配置 ScriptableObject。描述一类抛射物的运动、伤害与对象池参数。 + /// + [CreateAssetMenu(menuName = "Combat/ProjectileConfig")] + public class ProjectileConfigSO : ScriptableObject + { + [Header("伤害")] + public DamageSourceSO DamageSource; + + [Header("运动")] + public float Speed = 12f; + public float Lifetime = 5f; + public float LaunchAngleDeg = 45f; + public float GravityScale = 1f; + public float HomingStrength = 4f; + + [Header("对象池")] + public string PoolKey; + + [Header("弹反")] + public float ParrySpeedMultiplier = 1.2f; + public float ParryDamageMultiplier = 2.0f; + } +} diff --git a/Assets/Scripts/Combat/ProjectileConfigSO.cs.meta b/Assets/Scripts/Combat/ProjectileConfigSO.cs.meta new file mode 100644 index 0000000..7d338cb --- /dev/null +++ b/Assets/Scripts/Combat/ProjectileConfigSO.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 34d03fe23f5830b4e8abbe28bfbb5e52 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/ProjectileManager.cs b/Assets/Scripts/Combat/ProjectileManager.cs new file mode 100644 index 0000000..0c19ef4 --- /dev/null +++ b/Assets/Scripts/Combat/ProjectileManager.cs @@ -0,0 +1,55 @@ +using UnityEngine; +using BaseGames.Core; +using BaseGames.Core.Events; + +namespace BaseGames.Combat +{ + /// + /// 抛射物管理器(单例 MonoBehaviour)。 + /// 缓存玩家 Transform,供追踪类抛射物注入目标引用。 + /// + public class ProjectileManager : MonoBehaviour, IProjectileService + { + [SerializeField] private TransformEventChannelSO _onPlayerSpawned; + + private Transform _playerTransform; + private readonly CompositeDisposable _subs = new(); + + /// 当前缓存的玩家 Transform,生成追踪弹时使用。 + public Transform PlayerTransform => _playerTransform; + + private void Awake() + { + if (ServiceLocator.GetOrDefault() != null) { Destroy(gameObject); return; } + ServiceLocator.Register(this); + } + + private void OnDestroy() + { + ServiceLocator.Unregister(this); + } + + private void OnEnable() + { + _onPlayerSpawned?.Subscribe(OnPlayerSpawned).AddTo(_subs); + } + + private void OnDisable() + { + _subs.Clear(); + } + + private void OnPlayerSpawned(Transform player) => _playerTransform = player; + + /// + /// 完整初始化一枚 并注入追踪目标。 + /// + public void LaunchHoming(HomingProjectile proj, Vector2 direction, + ProjectileConfigSO config, DamageInfo damageInfo) + { + if (proj == null || config == null) return; + proj.Initialize(config, damageInfo, direction); + proj.SetTarget(_playerTransform); + } + } +} diff --git a/Assets/Scripts/Combat/ProjectileManager.cs.meta b/Assets/Scripts/Combat/ProjectileManager.cs.meta new file mode 100644 index 0000000..703fa5b --- /dev/null +++ b/Assets/Scripts/Combat/ProjectileManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ac14f526cd2afcd4d8cdd44780805472 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/ShieldComponent.cs b/Assets/Scripts/Combat/ShieldComponent.cs index e6f874a..4f5de2d 100644 --- a/Assets/Scripts/Combat/ShieldComponent.cs +++ b/Assets/Scripts/Combat/ShieldComponent.cs @@ -1,20 +1,137 @@ using UnityEngine; +using System; +using BaseGames.Core.Events; namespace BaseGames.Combat { /// - /// 护盾组件(Phase 1 存根)。实现 IShieldable 接口供 HurtBox 注入。 - /// Phase 2 实现完整护盾逻辑(护盾值、再生、破盾事件)。 + /// 护盾组件。实现 IShieldable 接口供 HurtBox 注入。 + /// 护盾参数通过 ShieldConfigSO 集中配置。 /// public class ShieldComponent : MonoBehaviour, IShieldable { - public bool HasShield { get; private set; } + [Header("配置资产")] + [SerializeField] private ShieldConfigSO _config; + + [Header("VFX 事件频道")] + [SerializeField] private VoidEventChannelSO _onShieldBrokenChannel; // 护盾破碎 → 播放破碎 VFX + [SerializeField] private VoidEventChannelSO _onShieldRestoredChannel; // 护盾恢复 → 播放恢复 VFX + + // ── 运行时属性 ──────────────────────────────────────────────────────── + public int MaxShieldHP => _config.MaxShieldHP; + public int CurrentShieldHP { get; private set; } + /// 当前是否能吸收伤害(护盾 HP > 0 且不在破碎惩罚期)。 + public bool HasShield => CurrentShieldHP > 0 && _brokenPenaltyTimer <= 0f; + + private float AbsorptionRatio => _config.DamageAbsorptionRatio; + private float RechargeDelay => _config.RechargeDelay; + private float RechargeRate => _config.RechargeRate; + private float BrokenPenaltyDur => _config.BrokenPenaltyDuration; + private float ParryRestoreRatio => _config.ParryRestoreRatio; + + public event Action OnShieldChanged; // (current, max) + public event Action OnShieldBroken; + + private float _regenDelayTimer; + private float _brokenPenaltyTimer; + + private void Awake() + { + Debug.Assert(_config != null, "[ShieldComponent] _config 未赋值,请在 Inspector 中指定 ShieldConfigSO。", this); + CurrentShieldHP = MaxShieldHP; + } + + private void Update() + { + int maxHP = MaxShieldHP; + if (maxHP <= 0) return; + + // 破碎惩罚计时 + if (_brokenPenaltyTimer > 0f) + { + _brokenPenaltyTimer -= Time.deltaTime; + // 破碎惩罚结束 → 广播护盾恢复事件(VFX 钩子) + if (_brokenPenaltyTimer <= 0f) + _onShieldRestoredChannel?.Raise(); + return; + } + + if (CurrentShieldHP >= maxHP) return; + if (RechargeRate <= 0f) return; + + if (_regenDelayTimer > 0f) + { + _regenDelayTimer -= Time.deltaTime; + return; + } + + int prev = CurrentShieldHP; + CurrentShieldHP = Mathf.Min(CurrentShieldHP + Mathf.CeilToInt(RechargeRate * Time.deltaTime), maxHP); + if (CurrentShieldHP != prev) + OnShieldChanged?.Invoke(CurrentShieldHP, maxHP); + } /// - /// 尝试以护盾吸收伤害。 - /// 返回穿透量(0 = 全部吸收,>0 = 穿透量继续走 TakeDamage 流程)。 - /// Phase 1:护盾不存在,全量穿透。 + /// 尝试以护盾吸收伤害。返回穿透量(0=全部吸收,>0=穿透量继续走 TakeDamage 流程)。 /// - public int AbsorbDamage(int amount) => amount; + public int AbsorbDamage(int amount) + { + if (!HasShield) return amount; + + _regenDelayTimer = RechargeDelay; + + int maxHP = MaxShieldHP; + // 按吸收比例计算实际由护盾承担的伤害 + int toAbsorb = Mathf.FloorToInt(amount * AbsorptionRatio); + toAbsorb = Mathf.Min(toAbsorb, CurrentShieldHP); + int passthrough = amount - toAbsorb; + + CurrentShieldHP -= toAbsorb; + OnShieldChanged?.Invoke(CurrentShieldHP, maxHP); + + if (CurrentShieldHP <= 0) + { + CurrentShieldHP = 0; + _brokenPenaltyTimer = BrokenPenaltyDur; + OnShieldBroken?.Invoke(); + _onShieldBrokenChannel?.Raise(); // VFX 钩子:播放护盾破碎特效 + } + + return passthrough; + } + + /// 存档点 / 复活时调用:完全恢复护盾并清除惩罚状态。 + public void FullRecharge() + { + bool wasBroken = _brokenPenaltyTimer > 0f || CurrentShieldHP <= 0; + _brokenPenaltyTimer = 0f; + _regenDelayTimer = 0f; + CurrentShieldHP = MaxShieldHP; + OnShieldChanged?.Invoke(CurrentShieldHP, MaxShieldHP); + if (wasBroken) + _onShieldRestoredChannel?.Raise(); // VFX 钩子:护盾已满血恢复 + } + + /// 弹反成功时调用:按 ParryRestoreRatio 恢复护盾(会清除惩罚状态)。 + public void OnParrySuccess() + { + int maxHP = MaxShieldHP; + if (maxHP <= 0) return; + + _brokenPenaltyTimer = 0f; + _regenDelayTimer = 0f; + int restore = Mathf.CeilToInt(maxHP * ParryRestoreRatio); + CurrentShieldHP = Mathf.Min(CurrentShieldHP + restore, maxHP); + OnShieldChanged?.Invoke(CurrentShieldHP, maxHP); + } + + /// Inspector / 道具系统调用:设置最大护盾并重置当前值。 + public void SetMaxShieldHP(int max) + { + _maxShieldHP = Mathf.Max(0, max); + _brokenPenaltyTimer = 0f; + CurrentShieldHP = MaxShieldHP; + OnShieldChanged?.Invoke(CurrentShieldHP, MaxShieldHP); + } } } diff --git a/Assets/Scripts/Combat/ShieldConfigSO.cs b/Assets/Scripts/Combat/ShieldConfigSO.cs new file mode 100644 index 0000000..5edf840 --- /dev/null +++ b/Assets/Scripts/Combat/ShieldConfigSO.cs @@ -0,0 +1,29 @@ +using UnityEngine; + +namespace BaseGames.Combat +{ + /// + /// 护盾配置资产(架构 06_CombatModule §6 ShieldSystem)。 + /// 可通过 Assets/Create/Combat/ShieldConfig 创建。 + /// + [CreateAssetMenu(menuName = "Combat/ShieldConfig", fileName = "ShieldConfig")] + public class ShieldConfigSO : ScriptableObject + { + [Header("护盾数值")] + public int MaxShieldHP = 0; // 0 = 无护盾(默认禁用) + [Range(0f, 1f)] + public float DamageAbsorptionRatio = 1.0f; // 1.0 = 全吸收;<1.0 = 部分穿透 + public float RechargeDelay = 2.0f; // 受击后延迟多久才开始再生(秒) + public float RechargeRate = 20f; // 每秒再生 HP + + [Header("破碎惩罚")] + public float BrokenPenaltyDuration = 3.0f; // 护盾破碎后不可再生的惩罚时长(秒) + + [Header("存档点")] + public bool FullRechargeOnSavePoint = true; // 抵达存档点时是否完全恢复护盾 + + [Header("弹反加成")] + [Range(0f, 1f)] + public float ParryRestoreRatio = 0.3f; // 弹反成功后恢复护盾比例(占 MaxShieldHP) + } +} diff --git a/Assets/Scripts/Combat/ShieldConfigSO.cs.meta b/Assets/Scripts/Combat/ShieldConfigSO.cs.meta new file mode 100644 index 0000000..135b072 --- /dev/null +++ b/Assets/Scripts/Combat/ShieldConfigSO.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b2e617a47cdc4b64383a6a907023b3e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/SkillHitBoxInstance.cs b/Assets/Scripts/Combat/SkillHitBoxInstance.cs new file mode 100644 index 0000000..82ed528 --- /dev/null +++ b/Assets/Scripts/Combat/SkillHitBoxInstance.cs @@ -0,0 +1,48 @@ +using UnityEngine; +using BaseGames.Combat; + +namespace BaseGames.Combat +{ + /// + /// 技能 HitBox 实例(架构 09_ProgressionModule §9 SkillHitBoxInstance)。 + /// 挂载于技能 HitBox Prefab 根节点。 + /// 命名规范: Assets/Prefabs/Skills/SKL_{skillId}_HitBox.prefab + /// + /// Prefab 内部层级示例(近战 AoE 技能): + /// [SKL_SkySlash_HitBox] + /// └── [HitBox] ← 扇形/圆形 PolygonCollider2D + /// └── HitBox.cs + /// + public class SkillHitBoxInstance : MonoBehaviour + { + [SerializeField] private HitBox[] _hitBoxes; // 技能可有多个 HitBox(多段伤害) + + public event System.Action OnHitConfirmed; + + private void Awake() + { + foreach (var hb in _hitBoxes) + { + if (hb == null) continue; + hb.OnHitConfirmed += info => OnHitConfirmed?.Invoke(info); + } + } + + /// 激活所有 HitBox,传入伤害数据源和攻击者 Transform。 + public void Activate(DamageSourceSO source, Transform attacker) + { + foreach (var hb in _hitBoxes) + hb?.Activate(source, attacker); + } + + /// duration 秒后自动销毁此 GameObject。 + public void AutoDestroyAfter(float duration) + => Destroy(gameObject, Mathf.Max(0f, duration)); + + private void OnDestroy() + { + foreach (var hb in _hitBoxes) + hb?.Deactivate(); + } + } +} diff --git a/Assets/Scripts/Combat/SkillHitBoxInstance.cs.meta b/Assets/Scripts/Combat/SkillHitBoxInstance.cs.meta new file mode 100644 index 0000000..a2cf086 --- /dev/null +++ b/Assets/Scripts/Combat/SkillHitBoxInstance.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 776f1908eabcca546937010169b91efc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/StatusEffects/.gitkeep b/Assets/Scripts/Combat/StatusEffects/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Assets/Scripts/Combat/StatusEffects/BaseGames.Combat.StatusEffects.asmdef b/Assets/Scripts/Combat/StatusEffects/BaseGames.Combat.StatusEffects.asmdef index a239957..bf9cbdd 100644 --- a/Assets/Scripts/Combat/StatusEffects/BaseGames.Combat.StatusEffects.asmdef +++ b/Assets/Scripts/Combat/StatusEffects/BaseGames.Combat.StatusEffects.asmdef @@ -8,7 +8,8 @@ "versionDefines": [], "rootNamespace": "BaseGames.Combat.StatusEffects", "references": [ - "BaseGames.Combat" + "BaseGames.Combat", + "BaseGames.Core.Events" ], "autoReferenced": true, "overrideReferences": false, diff --git a/Assets/Scripts/Combat/StatusEffects/FireEffect.cs b/Assets/Scripts/Combat/StatusEffects/FireEffect.cs new file mode 100644 index 0000000..cd7ba76 --- /dev/null +++ b/Assets/Scripts/Combat/StatusEffects/FireEffect.cs @@ -0,0 +1,45 @@ +namespace BaseGames.Combat.StatusEffects +{ + /// + /// 燃烧效果(架构 06_CombatModule §11)。 + /// 规则:不可叠加(MaxStacks = 1);重复施加刷新持续时间;每 0.5 秒造成 1 点 True 伤害。 + /// + public class FireEffect : StatusEffect + { + private const float BaseDuration = 3.0f; // 持续 3 秒 + private const float DotInterval = 0.5f; // 每 0.5 秒一次 + + public override StatusEffectType EffectType => StatusEffectType.Fire; + public override int MaxStacks => 1; + + public FireEffect() + { + TickInterval = DotInterval; + } + + public override void OnApply(StatusEffectManager owner) + { + base.OnApply(owner); + owner.SetShaderParam("_FireGlow", 1f); + } + + /// 每次 DoT Tick 造成 1 点 True 伤害(绕过护盾,无敌帧不免疫)。 + public override void OnTick() + { + var info = new DamageInfo.Builder() + .SetRaw(1) + .SetType(DamageType.True) + .SetFlags(DamageFlags.IgnoreIFrame) + .Build(); + Owner?.ApplyDirectDamage(info); + } + + public override void OnExpire() + { + Owner?.SetShaderParam("_FireGlow", 0f); + } + + protected override float GetBaseDuration() => BaseDuration; + public override string GetDisplayName() => "燃烧"; + } +} diff --git a/Assets/Scripts/Combat/StatusEffects/FireEffect.cs.meta b/Assets/Scripts/Combat/StatusEffects/FireEffect.cs.meta new file mode 100644 index 0000000..547716a --- /dev/null +++ b/Assets/Scripts/Combat/StatusEffects/FireEffect.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2d692ae17737ac54bb0940c3487a59be +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/StatusEffects/PoisonEffect.cs b/Assets/Scripts/Combat/StatusEffects/PoisonEffect.cs new file mode 100644 index 0000000..186e972 --- /dev/null +++ b/Assets/Scripts/Combat/StatusEffects/PoisonEffect.cs @@ -0,0 +1,54 @@ +namespace BaseGames.Combat.StatusEffects +{ + /// + /// 中毒效果(架构 06_CombatModule §11)。 + /// 规则:最多叠加 3 层;每层叠加伤害 +1;每 1 秒造成 StackCount 点 True 伤害。 + /// + public class PoisonEffect : StatusEffect + { + private const float BaseDuration = 5.0f; // 持续 5 秒 + private const float DotInterval = 1.0f; // 每 1 秒一次 + + public override StatusEffectType EffectType => StatusEffectType.Poison; + public override int MaxStacks => 3; + + public PoisonEffect() + { + TickInterval = DotInterval; + } + + public override void OnApply(StatusEffectManager owner) + { + base.OnApply(owner); + UpdateShader(); + } + + public override void OnStack() + { + base.OnStack(); + UpdateShader(); + } + + public override void OnTick() + { + var info = new DamageInfo.Builder() + .SetRaw(StackCount) // 叠层越多伤害越高 + .SetType(DamageType.True) + .SetFlags(DamageFlags.IgnoreIFrame) + .Build(); + Owner?.ApplyDirectDamage(info); + } + + public override void OnExpire() + { + StackCount = 0; + Owner?.SetShaderParam("_PoisonGlow", 0f); + } + + private void UpdateShader() + => Owner?.SetShaderParam("_PoisonGlow", StackCount / (float)MaxStacks); + + protected override float GetBaseDuration() => BaseDuration; + public override string GetDisplayName() => $"中毒 ×{StackCount}"; + } +} diff --git a/Assets/Scripts/Combat/StatusEffects/PoisonEffect.cs.meta b/Assets/Scripts/Combat/StatusEffects/PoisonEffect.cs.meta new file mode 100644 index 0000000..8941339 --- /dev/null +++ b/Assets/Scripts/Combat/StatusEffects/PoisonEffect.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 97257dbfc13b78441a89c652f51310cf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/StatusEffects/StaggerEffect.cs b/Assets/Scripts/Combat/StatusEffects/StaggerEffect.cs new file mode 100644 index 0000000..a77b4ba --- /dev/null +++ b/Assets/Scripts/Combat/StatusEffects/StaggerEffect.cs @@ -0,0 +1,33 @@ +namespace BaseGames.Combat.StatusEffects +{ + /// + /// 硬直效果(架构 06_CombatModule §11)。 + /// 规则:不可叠加(MaxStacks = 1);施加期间宿主无法执行动作。 + /// 外部查询:entity.GetComponent<StatusEffectManager>()?.HasEffect(StatusEffectType.Stagger) + /// + public class StaggerEffect : StatusEffect + { + private const float BaseDuration = 0.5f; // 默认硬直持续时间(秒) + + private readonly float _overrideDuration; + + /// 自定义持续时间(<= 0 使用默认值)。 + public StaggerEffect(float duration = 0f) + { + _overrideDuration = duration > 0f ? duration : BaseDuration; + TickInterval = 0f; // 无 DoT Tick + } + + public override StatusEffectType EffectType => StatusEffectType.Stagger; + public override int MaxStacks => 1; + + public override void OnApply(StatusEffectManager owner) + { + Owner = owner; + Duration = _overrideDuration; + } + + protected override float GetBaseDuration() => _overrideDuration; + public override string GetDisplayName() => "硬直"; + } +} diff --git a/Assets/Scripts/Combat/StatusEffects/StaggerEffect.cs.meta b/Assets/Scripts/Combat/StatusEffects/StaggerEffect.cs.meta new file mode 100644 index 0000000..6a42e31 --- /dev/null +++ b/Assets/Scripts/Combat/StatusEffects/StaggerEffect.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 23c67d6324b88a1468bd20baa4f109c3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/StatusEffects/StatusEffect.cs b/Assets/Scripts/Combat/StatusEffects/StatusEffect.cs new file mode 100644 index 0000000..4c99a15 --- /dev/null +++ b/Assets/Scripts/Combat/StatusEffects/StatusEffect.cs @@ -0,0 +1,91 @@ +using UnityEngine; + +namespace BaseGames.Combat.StatusEffects +{ + /// + /// 状态效果抽象基类(架构 06_CombatModule §11)。 + /// ⚠️ 类名为 StatusEffect(非 StatusEffectBase)。 + /// + /// 生命周期: + /// OnApply(owner) → Update(delta) × N [内部调用 OnTick()] → OnExpire() + /// + /// 叠加规则:同类型再次施加时调用 OnStack();Manager 保证每种类型只有一个实例。 + /// + public abstract class StatusEffect + { + /// 效果类型标识(用作 Dictionary key)。 + public abstract StatusEffectType EffectType { get; } + + /// 最大叠加层数(1 = 不可叠加,重复施加只刷新持续时间)。 + public abstract int MaxStacks { get; } + + /// 当前叠加层数。 + public int StackCount { get; protected set; } = 1; + + /// 当前剩余持续时间(秒)。 + public float Duration { get; protected set; } + + /// 每次 Tick 的间隔(秒)。 + public float TickInterval { get; protected set; } + + /// 是否已过期(由 Manager 每帧检查)。 + public virtual bool IsExpired => Duration <= 0f; + + private float _tickTimer; + + /// 宿主 Manager(OnApply 时注入,OnTick/OnExpire 中可访问)。 + public StatusEffectManager Owner { get; protected set; } + + // ── 生命周期回调(可重写)───────────────────────────────────────── + + /// + /// 效果施加时调用(Owner 在此注入)。 + /// ⚠️ 参数为 StatusEffectManager(非 IDamageable),架构 06 §11。 + /// + public virtual void OnApply(StatusEffectManager owner) + { + Owner = owner; + Duration = GetBaseDuration(); + } + + /// + /// 同类型效果再次施加时调用(叠层 / 刷新持续时间)。 + /// 默认行为:刷新持续时间并叠加层数(若未达上限)。 + /// + public virtual void OnStack() + { + Duration = GetBaseDuration(); + StackCount = Mathf.Min(StackCount + 1, MaxStacks); + } + + /// 每个 TickInterval 秒调用一次(DoT 等周期效果)。 + public virtual void OnTick() { } + + /// 效果到期 / 被净化时调用。⚠️ 名称 OnExpire(非 OnRemove)。 + public virtual void OnExpire() { } + + // ── 框架驱动(由 Manager.Update 调用,每帧执行)────────────────── + + /// 递减持续时间并在到达 Tick 间隔时触发 OnTick。 + public void Update(float delta) + { + Duration -= delta; + if (TickInterval <= 0f) return; + + _tickTimer += delta; + if (_tickTimer >= TickInterval) + { + _tickTimer -= TickInterval; + OnTick(); + } + } + + // ── 子类必须实现 ────────────────────────────────────────────────── + + /// 返回本类型效果的基础持续时间(秒)。 + protected abstract float GetBaseDuration(); + + /// 返回本效果的本地化显示名称。 + public abstract string GetDisplayName(); + } +} diff --git a/Assets/Scripts/Combat/StatusEffects/StatusEffect.cs.meta b/Assets/Scripts/Combat/StatusEffects/StatusEffect.cs.meta new file mode 100644 index 0000000..4ca5540 --- /dev/null +++ b/Assets/Scripts/Combat/StatusEffects/StatusEffect.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4490046c0c848054e9a6846dc272f9f6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/StatusEffects/StatusEffectEventChannelSO.cs b/Assets/Scripts/Combat/StatusEffects/StatusEffectEventChannelSO.cs new file mode 100644 index 0000000..a1e44f4 --- /dev/null +++ b/Assets/Scripts/Combat/StatusEffects/StatusEffectEventChannelSO.cs @@ -0,0 +1,21 @@ +using UnityEngine; +using BaseGames.Core.Events; + +namespace BaseGames.Combat.StatusEffects +{ + /// 状态效果事件(应用 / 到期时广播,可用于 UI 更新)。 + public struct StatusEffectEvent + { + /// 效果类型。 + public StatusEffectType EffectType; + + /// 当前叠加层数(到期时为 0)。 + public int StackCount; + + /// 剩余持续时间(到期时为 0)。 + public float RemainingDuration; + } + + [CreateAssetMenu(menuName = "Events/StatusEffect")] + public class StatusEffectEventChannelSO : BaseEventChannelSO { } +} diff --git a/Assets/Scripts/Combat/StatusEffects/StatusEffectEventChannelSO.cs.meta b/Assets/Scripts/Combat/StatusEffects/StatusEffectEventChannelSO.cs.meta new file mode 100644 index 0000000..2fc88bc --- /dev/null +++ b/Assets/Scripts/Combat/StatusEffects/StatusEffectEventChannelSO.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e2aa7161466b28442acab31ae092166b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/StatusEffects/StatusEffectManager.cs b/Assets/Scripts/Combat/StatusEffects/StatusEffectManager.cs index b9a614a..6e5a9d7 100644 --- a/Assets/Scripts/Combat/StatusEffects/StatusEffectManager.cs +++ b/Assets/Scripts/Combat/StatusEffects/StatusEffectManager.cs @@ -1,20 +1,176 @@ +using System; +using System.Collections.Generic; using UnityEngine; -using BaseGames.Combat; namespace BaseGames.Combat.StatusEffects { /// - /// 状态效果管理器(Phase 1 桩)。 - /// 实现 IStatusEffectable 接口,由 HurtBox 通过接口调用,避免程序集循环依赖。 - /// Phase 2 实现完整的效果叠加、持续时间、DoT 伤害计算。 + /// 状态效果管理器。 + /// + /// 架构 06_CombatModule §11: + /// - 双结构:List(Update 遍历)+ Dictionary(O(1) 类型查找) + /// - 实现 IStatusEffectable,接受来自 HurtBox 的 DamageType 并映射到具体效果 + /// - DealDotDamage:StatusEffect 子类通过 Owner 调用,绕过无敌帧造成 DoT + /// - CleanseEffect:净化指定类型效果(道具/技能使用) /// + [RequireComponent(typeof(SpriteRenderer))] public class StatusEffectManager : MonoBehaviour, IStatusEffectable { - // Phase 1:空实现 - public void ApplyStatusEffect(DamageType type) { } - } + [Header("事件频道(可选)")] + [SerializeField] private StatusEffectEventChannelSO _onStatusEffectApplied; + [SerializeField] private StatusEffectEventChannelSO _onStatusEffectExpired; - // ── Phase 1 占位效果类型 ────────────────────────────────────────────────── - public class FireEffect { } - public class PoisonEffect { } + // ── 双结构 ───────────────────────────────────────────────────────── + private readonly List _activeList = new(); + private readonly Dictionary _activeIndex = new(); + + // ── Shader 渲染(MaterialPropertyBlock,不修改共享材质)───────── + private SpriteRenderer _renderer; + private MaterialPropertyBlock _propBlock; + + // ── DoT 伤害代理(由 StatusEffect.OnTick 通过 Owner 调用)────────── + private IDamageable _damageable; + + // ── 效果工厂字典(可在 Awake 后动态注册)───────────────────── + private readonly Dictionary> _effectFactories = new(); + + private void Awake() + { + _renderer = GetComponent(); + _propBlock = new MaterialPropertyBlock(); + _damageable = GetComponentInParent(); + + // 默认标准效果注册(子类或外部模块可调用 RegisterEffectFactory 覆盖或扩展) + RegisterEffectFactory(DamageType.Fire, () => new FireEffect()); + RegisterEffectFactory(DamageType.Poison, () => new PoisonEffect()); + } + + private void Update() + { + float delta = Time.deltaTime; + + // 逆序遍历,避免移除时索引错位 + for (int i = _activeList.Count - 1; i >= 0; i--) + { + StatusEffect effect = _activeList[i]; + effect.Update(delta); + + if (effect.IsExpired) + RemoveAt(i, effect); + } + } + + // ── IStatusEffectable 实现 ───────────────────────────────────────── + + /// + /// HurtBox 调用入口:将 DamageType 映射为具体 StatusEffect 实例并施加。 + /// + public void ApplyStatusEffect(DamageType type) + { + StatusEffect effect = CreateEffect(type); + if (effect != null) + ApplyEffect(effect); + } + /// + /// 注册或覆盖一个 DamageType 对应的效果工厂。 + /// Boss 或特殊游玩法式可在运行时注册自定义效果。 + /// + public void RegisterEffectFactory(DamageType type, Func factory) + => _effectFactories[type] = factory; + // ── 公开 API ─────────────────────────────────────────────────────── + + /// 直接施加一个具体效果(供技能/Boss 使用)。 + public void ApplyEffect(StatusEffect effect) + { + if (_activeIndex.TryGetValue(effect.EffectType, out StatusEffect existing)) + { + existing.OnStack(); + BroadcastApplied(existing); + } + else + { + effect.OnApply(this); + _activeList.Add(effect); + _activeIndex[effect.EffectType] = effect; + BroadcastApplied(effect); + } + } + + /// 净化指定类型效果(净化道具/技能调用)。 + public void CleanseEffect(StatusEffectType type) + { + if (!_activeIndex.TryGetValue(type, out StatusEffect effect)) return; + + effect.OnExpire(); + _activeIndex.Remove(type); + _activeList.Remove(effect); + BroadcastExpired(effect); + } + + /// 查询是否存在指定类型效果(供状态机/UI 轮询)。 + public bool HasEffect(StatusEffectType type) => _activeIndex.ContainsKey(type); + + /// 获取指定类型效果(可为 null)。 + public StatusEffect GetEffect(StatusEffectType type) + => _activeIndex.TryGetValue(type, out var e) ? e : null; + + /// + /// DoT 伤害代理(架构 06 §10)。StatusEffect.OnTick() 调用此方法,传入已构建好的 DamageInfo。 + /// + public void ApplyDirectDamage(DamageInfo info) + { + _damageable?.TakeDamage(info); + } + + /// 设置 Sprite Shader 参数(MaterialPropertyBlock,不修改共享材质)。 + public void SetShaderParam(string param, float value) + { + if (_renderer == null) return; + _renderer.GetPropertyBlock(_propBlock); + _propBlock.SetFloat(param, value); + _renderer.SetPropertyBlock(_propBlock); + } + + /// 净化所有状态效果(存档点激活 / 返回城镇等调用)。 + public void CleanseAll() + { + foreach (var e in _activeList) e.OnExpire(); + _activeList.Clear(); + _activeIndex.Clear(); + } + + // ── 私有辅助 ─────────────────────────────────────────────────────── + + private StatusEffect CreateEffect(DamageType type) + => _effectFactories.TryGetValue(type, out var factory) ? factory() : null; + + private void RemoveAt(int index, StatusEffect effect) + { + effect.OnExpire(); + _activeList.RemoveAt(index); + _activeIndex.Remove(effect.EffectType); + BroadcastExpired(effect); + } + + private void BroadcastApplied(StatusEffect effect) + { + _onStatusEffectApplied?.Raise(new StatusEffectEvent + { + EffectType = effect.EffectType, + StackCount = effect.StackCount, + RemainingDuration = effect.Duration, + }); + } + + private void BroadcastExpired(StatusEffect effect) + { + _onStatusEffectExpired?.Raise(new StatusEffectEvent + { + EffectType = effect.EffectType, + StackCount = 0, + RemainingDuration = 0f, + }); + } + } } + diff --git a/Assets/Scripts/Combat/StatusEffects/StatusEffectType.cs b/Assets/Scripts/Combat/StatusEffects/StatusEffectType.cs new file mode 100644 index 0000000..d6423ca --- /dev/null +++ b/Assets/Scripts/Combat/StatusEffects/StatusEffectType.cs @@ -0,0 +1,15 @@ +namespace BaseGames.Combat.StatusEffects +{ + /// + /// 状态效果类型枚举(架构 06_CombatModule §11)。 + /// 用于状态机索引(Dictionary key)和事件载荷,与 DamageType 相互独立。 + /// + public enum StatusEffectType + { + Fire, // 燃烧(DoT,不可叠加,重复施加刷新持续时间) + Poison, // 中毒(DoT,最多 3 层叠加) + Stagger, // 硬直(无法行动 N 秒) + Freeze, // 冻结(减速 / 固化) + Stun, // 眩晕(无法行动) + } +} diff --git a/Assets/Scripts/Combat/StatusEffects/StatusEffectType.cs.meta b/Assets/Scripts/Combat/StatusEffects/StatusEffectType.cs.meta new file mode 100644 index 0000000..cff0887 --- /dev/null +++ b/Assets/Scripts/Combat/StatusEffects/StatusEffectType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9a5502c061dc69b4b82113ed5b282740 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Combat/StatusEffects/_Placeholder.cs b/Assets/Scripts/Combat/StatusEffects/_Placeholder.cs deleted file mode 100644 index 20e1c1c..0000000 --- a/Assets/Scripts/Combat/StatusEffects/_Placeholder.cs +++ /dev/null @@ -1,3 +0,0 @@ -// Placeholder to prevent asmdef-no-scripts warning. -namespace BaseGames.Combat.StatusEffects { } - diff --git a/Assets/Scripts/Combat/_Placeholder.cs b/Assets/Scripts/Combat/_Placeholder.cs deleted file mode 100644 index 9ca8578..0000000 --- a/Assets/Scripts/Combat/_Placeholder.cs +++ /dev/null @@ -1,3 +0,0 @@ -// Placeholder to prevent asmdef-no-scripts warning. -namespace BaseGames.Combat { } - diff --git a/Assets/Scripts/Core/Assets/AddressKeyRegistry.cs b/Assets/Scripts/Core/Assets/AddressKeyRegistry.cs index 64414f9..0be052d 100644 --- a/Assets/Scripts/Core/Assets/AddressKeyRegistry.cs +++ b/Assets/Scripts/Core/Assets/AddressKeyRegistry.cs @@ -50,7 +50,7 @@ namespace BaseGames.Core.Assets /// /// 解析 key,返回对应的 Addressable 地址字符串。 - /// 若 key 未注册则返回原 key(兼容直接使用静态常量的调用方)。 + /// 若 key 未注册,直接将 key 作为 Addressable 地址使用。 /// public static string Resolve(string key) { diff --git a/Assets/Scripts/Core/Assets/AddressKeys.cs b/Assets/Scripts/Core/Assets/AddressKeys.cs index 2a197a0..d15ebe6 100644 --- a/Assets/Scripts/Core/Assets/AddressKeys.cs +++ b/Assets/Scripts/Core/Assets/AddressKeys.cs @@ -43,10 +43,16 @@ namespace BaseGames.Core.Assets // ── Config ScriptableObjects ───────────────────────────────────── public const string DataFootstepCatalog = "Config/FootstepCatalog"; - // ── Labels(批量加载用)────────────────────────────────────────── - public const string LabelEnemy = "Enemy"; - public const string LabelPoolable = "Poolable"; - public const string LabelBGM = "BGM"; - public const string LabelCharms = "Charms"; + /// + /// Addressable 标签常量(用于批量加载)。 + /// 注意:这里是标签名称而非资产地址,不会被 AddressKeyValidator 校验。 + /// + public static class Labels + { + public const string Enemy = "Enemy"; + public const string Poolable = "Poolable"; + public const string BGM = "BGM"; + public const string Charms = "Charms"; + } } } diff --git a/Assets/Scripts/Core/Assets/AssetReleaseTracker.cs b/Assets/Scripts/Core/Assets/AssetReleaseTracker.cs index bc2d4b0..d50da46 100644 --- a/Assets/Scripts/Core/Assets/AssetReleaseTracker.cs +++ b/Assets/Scripts/Core/Assets/AssetReleaseTracker.cs @@ -8,7 +8,6 @@ namespace BaseGames.Core /// /// 资产释放跟踪器。 /// 事件驱动:监听 EVT_SceneLoadRequest,在新场景加载前清理旧场景的对象池缓存。 - /// ⚠️ 不使用显式注册 API;GlobalObjectPool.ClearPool 在场景切换时批量清理。 /// public class AssetReleaseTracker : MonoBehaviour { @@ -16,28 +15,21 @@ namespace BaseGames.Core [SerializeField] private SceneLoadRequestEventChannelSO _onSceneLoadRequest; private string _lastLoadedScene; + private readonly CompositeDisposable _subs = new(); - private void OnEnable() - { - if (_onSceneLoadRequest != null) - _onSceneLoadRequest.OnEventRaised += OnSceneLoadRequested; - } - - private void OnDisable() - { - if (_onSceneLoadRequest != null) - _onSceneLoadRequest.OnEventRaised -= OnSceneLoadRequested; - } + private void OnEnable() => _onSceneLoadRequest?.Subscribe(OnSceneLoadRequested).AddTo(_subs); + private void OnDisable() => _subs.Clear(); private void OnSceneLoadRequested(SceneLoadRequest req) { if (string.IsNullOrEmpty(_lastLoadedScene)) { _lastLoadedScene = req.SceneName; return; } // 清除旧场景的敌人对象池缓存(按需扩展) - if (GlobalObjectPool.Instance != null) + var pool = ServiceLocator.GetOrDefault(); + if (pool != null) { - GlobalObjectPool.Instance.ClearPool(AddressKeys.PrefabEnemyGrunt); - GlobalObjectPool.Instance.ClearPool(AddressKeys.PrefabEnemySkullArch); + pool.ClearPool(AddressKeys.PrefabEnemyGrunt); + pool.ClearPool(AddressKeys.PrefabEnemySkullArch); } _lastLoadedScene = req.SceneName; diff --git a/Assets/Scripts/Core/DeathRespawnService.cs b/Assets/Scripts/Core/DeathRespawnService.cs index 9237737..c9b85bc 100644 --- a/Assets/Scripts/Core/DeathRespawnService.cs +++ b/Assets/Scripts/Core/DeathRespawnService.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Threading.Tasks; using UnityEngine; using BaseGames.Core.Events; @@ -20,7 +21,7 @@ namespace BaseGames.Core } /// - /// 死亡/复活流程独立服务(Phase 0 骨架,Phase 1 完整实现)。 + /// 死亡/复活流程独立服务。 /// public class DeathRespawnService : MonoBehaviour, IDeathRespawnService { @@ -30,49 +31,58 @@ namespace BaseGames.Core [SerializeField] private float _respawnFadeDuration = 0.4f; [Header("Event Channels - Raise")] - [SerializeField] private VoidEventChannelSO _onRespawnStarted; - [SerializeField] private VoidEventChannelSO _onRespawnCompleted; + [SerializeField] private VoidEventChannelSO _onRespawnStarted; + [SerializeField] private VoidEventChannelSO _onRespawnCompleted; + [SerializeField] private SceneLoadRequestEventChannelSO _onSceneLoadRequest; [Header("Event Channels - Listen")] [SerializeField] private VoidEventChannelSO _onDeathScreenConfirmed; - private bool _deathConfirmed; - - private void OnEnable() - { - if (_onDeathScreenConfirmed != null) - _onDeathScreenConfirmed.OnEventRaised += HandleDeathScreenConfirmed; - } - - private void OnDisable() - { - if (_onDeathScreenConfirmed != null) - _onDeathScreenConfirmed.OnEventRaised -= HandleDeathScreenConfirmed; - } - - private void HandleDeathScreenConfirmed() => _deathConfirmed = true; - public IEnumerator StartDeathSequenceCoroutine() { yield return new WaitForSeconds(_deathAnimDuration); yield return new WaitForSeconds(_deathScreenDelay); - _deathConfirmed = false; - yield return new WaitUntil(() => _deathConfirmed); + + // 局部订阅确认事件,不依赖类级 bool 字段 + bool confirmed = false; + var sub = _onDeathScreenConfirmed?.Subscribe(() => confirmed = true); + yield return new WaitUntil(() => confirmed); + sub?.Dispose(); } public IEnumerator StartRespawnCoroutine() { _onRespawnStarted?.Raise(); yield return new WaitForSeconds(_respawnFadeDuration); - // Phase 1:加载存档场景(TODO) + + // 通过 SceneLoadRequest 频道触发场景重载,复用 SceneService / RoomTransition 路径 + var sm = ServiceLocator.GetOrDefault(); + _onSceneLoadRequest?.Raise(new SceneLoadRequest + { + SceneName = sm?.LastCheckpointScene, + EntryTransitionId = sm?.LastCheckpointSpawnId, + ShowLoadingScreen = true, + IsRespawn = true, + }); + yield return new WaitForSeconds(_respawnFadeDuration); _onRespawnCompleted?.Raise(); } public IEnumerator StartGameOverCoroutine() { - // Phase 1:SteelSoul 清档并返回主菜单(TODO) - yield return null; + // 1. 删除当前存档槽 + var saveManager = ServiceLocator.GetOrDefault(); + if (saveManager != null) + { + var task = saveManager.DeleteSlotAsync(saveManager.ActiveSlot); + yield return new WaitUntil(() => task.IsCompleted); + } + + // 2. 返回主菜单 + var sceneService = ServiceLocator.GetOrDefault(); + if (sceneService != null) + yield return sceneService.LoadMainMenuCoroutine(); } } } diff --git a/Assets/Scripts/Core/Difficulty.meta b/Assets/Scripts/Core/Difficulty.meta new file mode 100644 index 0000000..b5967e2 --- /dev/null +++ b/Assets/Scripts/Core/Difficulty.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7982848c7ca0270419ab1c0a32fde9a0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Core/Difficulty/DifficultyManager.cs b/Assets/Scripts/Core/Difficulty/DifficultyManager.cs new file mode 100644 index 0000000..56ef939 --- /dev/null +++ b/Assets/Scripts/Core/Difficulty/DifficultyManager.cs @@ -0,0 +1,79 @@ +using UnityEngine; +using BaseGames.Core.Events; +using BaseGames.Core.Save; + +namespace BaseGames.Core +{ + /// + /// 难度管理器(单例 MonoBehaviour)。 + /// 统一管理当前难度档位,并通过事件频道广播变化。 + /// 订阅者通过 获取具体缩放数值。 + /// + [DefaultExecutionOrder(-900)] + public class DifficultyManager : MonoBehaviour, ISaveable, IDifficultyService + { + [SerializeField] private DifficultyScalerSO[] _allScalers; + [SerializeField] private DifficultyChangedEventChannel _onDifficultyChanged; + + public DifficultyLevel CurrentLevel { get; private set; } = DifficultyLevel.Normal; + public DifficultyScalerSO CurrentScaler { get; private set; } + + private void Awake() + { + if (ServiceLocator.GetOrDefault() != null) { Destroy(gameObject); return; } + ServiceLocator.Register(this); + Apply(DifficultyLevel.Normal); + ServiceLocator.GetOrDefault()?.Register(this); + } + + private void OnDestroy() + { + ServiceLocator.Unregister(this); + ServiceLocator.GetOrDefault()?.Unregister(this); + } + + /// + /// 在游戏流程中切换难度。 + /// SteelSoul 模式一旦选定,中途无法降级。 + /// + public void ChangeDifficulty(DifficultyLevel level) + { + if (CurrentLevel == DifficultyLevel.SteelSoul && level != DifficultyLevel.SteelSoul) + { + Debug.LogWarning("[DifficultyManager] SteelSoul 模式无法在游戏中途降级。"); + return; + } + Apply(level); + } + + /// 按档位查找对应的缩放器,未配置时返回 null。 + public DifficultyScalerSO GetScaler(DifficultyLevel level) + { + if (_allScalers == null) return null; + foreach (var s in _allScalers) + if (s != null && s.Level == level) return s; + return null; + } + + private void Apply(DifficultyLevel level) + { + CurrentLevel = level; + CurrentScaler = GetScaler(level); + _onDifficultyChanged?.Raise(CurrentLevel); + } + + // ── ISaveable ──────────────────────────────────────────────────────── + + public void OnSave(SaveData saveData) + { + if (saveData?.Meta != null) + saveData.Meta.IsSteelSoul = CurrentLevel == DifficultyLevel.SteelSoul; + } + + public void OnLoad(SaveData saveData) + { + if (saveData?.Meta != null && saveData.Meta.IsSteelSoul) + Apply(DifficultyLevel.SteelSoul); + } + } +} diff --git a/Assets/Scripts/Core/Difficulty/DifficultyManager.cs.meta b/Assets/Scripts/Core/Difficulty/DifficultyManager.cs.meta new file mode 100644 index 0000000..86f051e --- /dev/null +++ b/Assets/Scripts/Core/Difficulty/DifficultyManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7a810da0a9739024d90a4f7415aeb2a6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Core/Difficulty/DifficultyScalerSO.cs b/Assets/Scripts/Core/Difficulty/DifficultyScalerSO.cs new file mode 100644 index 0000000..a84b460 --- /dev/null +++ b/Assets/Scripts/Core/Difficulty/DifficultyScalerSO.cs @@ -0,0 +1,41 @@ +using UnityEngine; +using BaseGames.Core.Events; + +namespace BaseGames.Core +{ + /// + /// 难度缩放配置 ScriptableObject。每个难度档位对应一个实例。 + /// + [CreateAssetMenu(menuName = "Core/DifficultyScaler")] + public class DifficultyScalerSO : ScriptableObject + { + [Header("标识")] + public DifficultyLevel Level; + + [Header("玩家")] + public float PlayerMaxHPMultiplier = 1.0f; + public float PlayerDamageMultiplier = 1.0f; + public float InvincibilityFrameScale = 1.0f; + + [Header("敌人")] + public float EnemyDamageMultiplier = 1.0f; + public float EnemyHPMultiplier = 1.0f; + public float BossDamageMultiplier = 1.0f; + public float BossHPMultiplier = 1.0f; + + [Header("经济")] + public float ShopPriceMultiplier = 1.0f; + public float GeoDropMultiplier = 1.0f; + + [Header("机制")] + public bool CanReviveWithGeoLoss = true; + public bool InstantDeathOnZeroHP = false; + public bool GeoPenaltyOnDeath = true; + + [Header("AI")] + public float EnemyAttackIntervalScale = 1.0f; + public float EnemyAggroRangeScale = 1.0f; + public float EnemyReactionTimeScale = 1.0f; + public int EnemyAggressionLevel = 2; + } +} diff --git a/Assets/Scripts/Core/Difficulty/DifficultyScalerSO.cs.meta b/Assets/Scripts/Core/Difficulty/DifficultyScalerSO.cs.meta new file mode 100644 index 0000000..7f1477c --- /dev/null +++ b/Assets/Scripts/Core/Difficulty/DifficultyScalerSO.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d7e8ad48b35397348a800079849ee535 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Core/Difficulty/IDifficultyService.cs b/Assets/Scripts/Core/Difficulty/IDifficultyService.cs new file mode 100644 index 0000000..fb008f1 --- /dev/null +++ b/Assets/Scripts/Core/Difficulty/IDifficultyService.cs @@ -0,0 +1,22 @@ +namespace BaseGames.Core +{ + /// + /// 难度服务接口(架构 §DifficultyModule)。 + /// 提供当前难度档位及缩放参数,供跨程序集的游戏系统(战斗、商店、掉落等)查询, + /// 无需直接依赖 具体类型。 + /// + public interface IDifficultyService + { + /// 当前难度档位。 + DifficultyLevel CurrentLevel { get; } + + /// 当前档位对应的缩放配置。未配置时返回 null。 + DifficultyScalerSO CurrentScaler { get; } + + /// 切换到指定难度。SteelSoul 模式一旦选定不可降级。 + void ChangeDifficulty(DifficultyLevel level); + + /// 按档位查找缩放器,未配置时返回 null。 + DifficultyScalerSO GetScaler(DifficultyLevel level); + } +} diff --git a/Assets/Scripts/Core/Difficulty/IDifficultyService.cs.meta b/Assets/Scripts/Core/Difficulty/IDifficultyService.cs.meta new file mode 100644 index 0000000..e48a55a --- /dev/null +++ b/Assets/Scripts/Core/Difficulty/IDifficultyService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 20984324b3111c5489d9c4c3e55ec4f6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Core/Events/BaseEventChannelSO.cs b/Assets/Scripts/Core/Events/BaseEventChannelSO.cs index 44f2330..62cbbad 100644 --- a/Assets/Scripts/Core/Events/BaseEventChannelSO.cs +++ b/Assets/Scripts/Core/Events/BaseEventChannelSO.cs @@ -10,15 +10,37 @@ namespace BaseGames.Core.Events { [Multiline] public string description; - public event Action OnEventRaised; + private event Action _onEventRaisedBacking; +#if UNITY_EDITOR + private int _subscriberCount; +#endif + + public event Action OnEventRaised + { + add + { + _onEventRaisedBacking += value; +#if UNITY_EDITOR + _subscriberCount++; +#endif + } + remove + { + _onEventRaisedBacking -= value; +#if UNITY_EDITOR + _subscriberCount--; +#endif + } + } public void Raise(T value) { #if UNITY_EDITOR EventBusMonitor.Record(name, value?.ToString() ?? "null", - OnEventRaised?.GetInvocationList().Length ?? 0); + _subscriberCount, + Time.frameCount); #endif - OnEventRaised?.Invoke(value); + _onEventRaisedBacking?.Invoke(value); } /// @@ -38,15 +60,37 @@ namespace BaseGames.Core.Events { [Multiline] public string description; - public event Action OnEventRaised; + private event Action _onEventRaisedBacking; +#if UNITY_EDITOR + private int _subscriberCount; +#endif + + public event Action OnEventRaised + { + add + { + _onEventRaisedBacking += value; +#if UNITY_EDITOR + _subscriberCount++; +#endif + } + remove + { + _onEventRaisedBacking -= value; +#if UNITY_EDITOR + _subscriberCount--; +#endif + } + } public void Raise() { #if UNITY_EDITOR EventBusMonitor.Record(name, "", - OnEventRaised?.GetInvocationList().Length ?? 0); + _subscriberCount, + Time.frameCount); #endif - OnEventRaised?.Invoke(); + _onEventRaisedBacking?.Invoke(); } /// diff --git a/Assets/Scripts/Core/Events/PrimitiveEventChannels.cs b/Assets/Scripts/Core/Events/BoolEventChannelSO.cs similarity index 100% rename from Assets/Scripts/Core/Events/PrimitiveEventChannels.cs rename to Assets/Scripts/Core/Events/BoolEventChannelSO.cs diff --git a/Assets/Scripts/Core/Events/BoolEventChannelSO.cs.meta b/Assets/Scripts/Core/Events/BoolEventChannelSO.cs.meta new file mode 100644 index 0000000..7d837ce --- /dev/null +++ b/Assets/Scripts/Core/Events/BoolEventChannelSO.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d5c798758acf2c64097cf4ff3b088530 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Core/Events/DamageInfo.cs b/Assets/Scripts/Core/Events/DamageInfo.cs index 2699bc8..e340fe4 100644 --- a/Assets/Scripts/Core/Events/DamageInfo.cs +++ b/Assets/Scripts/Core/Events/DamageInfo.cs @@ -1,9 +1,2 @@ -// 此文件已废弃。DamageInfo / DamageInfoEventChannelSO 已迁移至 -// Assets/Scripts/Combat/DamageInfo.cs (namespace BaseGames.Combat) -// 程序集 BaseGames.Combat.asmdef - -namespace BaseGames.Core.Events -{ - // 保留空命名空间,避免 .meta 文件冲突。 - // ReSharper disable once EmptyNamespace -} +// DamageInfo 及 DamageInfoEventChannelSO 定义位于 Assets/Scripts/Combat/DamageInfo.cs。 +namespace BaseGames.Core.Events { } diff --git a/Assets/Scripts/Core/Events/DifficultyChangedEventChannel.cs b/Assets/Scripts/Core/Events/DifficultyChangedEventChannel.cs index 8a51773..97c7e37 100644 --- a/Assets/Scripts/Core/Events/DifficultyChangedEventChannel.cs +++ b/Assets/Scripts/Core/Events/DifficultyChangedEventChannel.cs @@ -3,7 +3,7 @@ using UnityEngine; namespace BaseGames.Core.Events { /// - /// 难度变更事件频道。Phase 2 难度系统使用。 + /// 难度变更事件频道。 /// 发布:DifficultyScalerSO / SettingsManager /// 订阅:所有需要感知当前难度的系统 /// diff --git a/Assets/Scripts/Core/Events/EventBusMonitor.cs b/Assets/Scripts/Core/Events/EventBusMonitor.cs index 49e8f52..55f80dd 100644 --- a/Assets/Scripts/Core/Events/EventBusMonitor.cs +++ b/Assets/Scripts/Core/Events/EventBusMonitor.cs @@ -11,29 +11,53 @@ namespace BaseGames.Core.Events public string ChannelName; public string Payload; public int ListenerCount; + public int FrameCount; public System.DateTime Timestamp; } - private static readonly System.Collections.Generic.Queue _records - = new System.Collections.Generic.Queue(256); + private const int Capacity = 256; - public static System.Collections.Generic.IEnumerable Records => _records; + // 固定大小环形缓冲区,避免 Queue.Enqueue/Dequeue 产生的 GC 分配 + private static readonly EventRecord[] _buffer = new EventRecord[Capacity]; + private static int _head = 0; // 下一条写入的位置 + private static int _count = 0; // 当前有效记录数(≤ Capacity) - public static void Record(string channelName, string payload, int listenerCount) + /// 按时间顺序(最旧→最新)枚举所有已记录事件。 + public static System.Collections.Generic.IEnumerable Records { - if (_records.Count >= 256) _records.Dequeue(); - _records.Enqueue(new EventRecord + get + { + int start = _count < Capacity ? 0 : _head; + int total = System.Math.Min(_count, Capacity); + for (int i = 0; i < total; i++) + yield return _buffer[(start + i) % Capacity]; + } + } + + // frameCount 使用 int(与 Time.frameCount 类型一致)。 + // 在 @1000fps 持续运行约 24.8 天后发生有符号溢出(C# 默认 unchecked,环绕为负数)。 + // 对于 Editor 调试工具此溢出无实际影响,此处不做处理。 + public static void Record(string channelName, string payload, int listenerCount, int frameCount) + { + _buffer[_head] = new EventRecord { ChannelName = channelName, Payload = payload, ListenerCount = listenerCount, + FrameCount = frameCount, Timestamp = System.DateTime.Now - }); + }; + _head = (_head + 1) % Capacity; + if (_count < Capacity) _count++; } - public static void Clear() => _records.Clear(); + public static void Clear() + { + _head = 0; + _count = 0; + } #else - public static void Record(string channelName, string payload, int listenerCount) { } + public static void Record(string channelName, string payload, int listenerCount, int frameCount) { } #endif } } diff --git a/Assets/Scripts/Core/Events/EventChannelRegistry.cs b/Assets/Scripts/Core/Events/EventChannelRegistry.cs index c9035e1..41a97f2 100644 --- a/Assets/Scripts/Core/Events/EventChannelRegistry.cs +++ b/Assets/Scripts/Core/Events/EventChannelRegistry.cs @@ -10,19 +10,16 @@ namespace BaseGames.Core.Events /// public class EventChannelRegistry : MonoBehaviour, IEventChannelRegistry { - public static EventChannelRegistry Instance { get; private set; } - private readonly Dictionary _channels = new(); private void Awake() { - if (Instance != null && Instance != this) + if (BaseGames.Core.ServiceLocator.GetOrDefault() != null) { Destroy(gameObject); return; } - Instance = this; - DontDestroyOnLoad(gameObject); + BaseGames.Core.ServiceLocator.Register(this); } /// 由 EventChannelRegistrar 在场景初始化时批量注册频道 SO。 diff --git a/Assets/Scripts/Core/Events/EventChannelRegistry.cs.meta b/Assets/Scripts/Core/Events/EventChannelRegistry.cs.meta index 41e5388..ac0dea8 100644 --- a/Assets/Scripts/Core/Events/EventChannelRegistry.cs.meta +++ b/Assets/Scripts/Core/Events/EventChannelRegistry.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 661043851605d4849bef40ea15c556b4 +guid: 147bb5b987a0a244ba3a39c71852ca51 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Scripts/Core/Events/HitInfo.cs b/Assets/Scripts/Core/Events/HitInfo.cs index 9c85edb..6ed4b4d 100644 --- a/Assets/Scripts/Core/Events/HitInfo.cs +++ b/Assets/Scripts/Core/Events/HitInfo.cs @@ -1,9 +1,2 @@ -// 此文件已废弃。HitInfo / HitConfirmedEventChannelSO 已迁移至 -// Assets/Scripts/Combat/HitInfo.cs (namespace BaseGames.Combat) -// 程序集 BaseGames.Combat.asmdef - -namespace BaseGames.Core.Events -{ - // 保留空命名空间,避免 .meta 文件冲突。 - // ReSharper disable once EmptyNamespace -} +// HitInfo 及 HitConfirmedEventChannelSO 定义位于 Assets/Scripts/Combat/HitInfo.cs。 +namespace BaseGames.Core.Events { } diff --git a/Assets/Scripts/Core/Events/PrimitiveEventChannels.cs.meta b/Assets/Scripts/Core/Events/PrimitiveEventChannels.cs.meta deleted file mode 100644 index c1a03ca..0000000 --- a/Assets/Scripts/Core/Events/PrimitiveEventChannels.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: d3e424c1787e5be4fa918201b1830192 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/Core/ServiceLocator.cs b/Assets/Scripts/Core/Events/ServiceLocator.cs similarity index 70% rename from Assets/Scripts/Core/ServiceLocator.cs rename to Assets/Scripts/Core/Events/ServiceLocator.cs index 39a4a56..9d17d3c 100644 --- a/Assets/Scripts/Core/ServiceLocator.cs +++ b/Assets/Scripts/Core/Events/ServiceLocator.cs @@ -6,6 +6,7 @@ namespace BaseGames.Core { /// /// 轻量服务定位器。通过类型键注册/查找服务,支持接口类型注册(依赖倒置)。 + /// 线程安全:仅在 Unity 主线程调用。异步上下文(Task/Thread)不得访问此类。 /// public static class ServiceLocator { @@ -37,6 +38,22 @@ namespace BaseGames.Core => _services.TryGetValue(typeof(TInterface), out var svc) && svc is TInterface typed ? typed : fallback; + /// + /// 注销服务。场景卸载或 Manager OnDestroy 时调用,防止持有已销毁对象的引用。 + /// + public static void Unregister() + => _services.Remove(typeof(TInterface)); + + /// + /// 安全版注销:仅当注册的实例与 相同时才移除, + /// 避免后注册的新实例被前一个实例的 OnDestroy 错误清除。 + /// + public static void Unregister(TInterface impl) + { + if (_services.TryGetValue(typeof(TInterface), out var svc) && ReferenceEquals(svc, impl)) + _services.Remove(typeof(TInterface)); + } + #if UNITY_EDITOR /// 单元测试中替换服务实现。 public static void OverrideForTest(TInterface mock) diff --git a/Assets/Scripts/Core/ServiceLocator.cs.meta b/Assets/Scripts/Core/Events/ServiceLocator.cs.meta similarity index 100% rename from Assets/Scripts/Core/ServiceLocator.cs.meta rename to Assets/Scripts/Core/Events/ServiceLocator.cs.meta diff --git a/Assets/Scripts/Core/GameIds.cs b/Assets/Scripts/Core/GameIds.cs new file mode 100644 index 0000000..cfa18b8 --- /dev/null +++ b/Assets/Scripts/Core/GameIds.cs @@ -0,0 +1,97 @@ +namespace BaseGames.Core +{ + // ===================================================================== + // GameIds —— 全局字符串 ID 常量(架构 02_CoreModule §IDs) + // ===================================================================== + // 目的:消除散落在配置 SO 和代码中的 magic string, + // 提供编译期校验 + IDE 自动补全 + 全局唯一检索点。 + // + // 使用方式: + // ① Inspector 侧:在 ScriptableObject 的 string 字段输入对应常量值(仅此处可见文本)。 + // ② 代码侧:直接引用常量,如 GameIds.Boss.ForestBoss,避免硬编码字符串。 + // + // 新增规则: + // - 按域划分嵌套静态类(Boss / Chain / Quest / Ability / Scene / Collectible / Npc) + // - 命名风格:PascalCase,与 Unity 资产命名对齐(如 "Chain_BossForest_Defeated") + // - 不要将常量值改名,只新增;如需废弃请标注 [System.Obsolete] + // ===================================================================== + + public static class GameIds + { + // ── Boss IDs ─────────────────────────────────────────────────────── + /// Boss 唯一标识符,对应 BossDefeatedCondition.bossId 及 BossDataSO.bossId。 + public static class Boss + { + public const string ForestBoss = "Boss_Forest"; + public const string CaveBoss = "Boss_Cave"; + public const string CastleBoss = "Boss_Castle"; + public const string FinalBoss = "Boss_Final"; + } + + // ── Chain IDs ────────────────────────────────────────────────────── + /// 事件链唯一标识符,对应 EventChainSO.chainId 及 ChainCompletedCondition.chainId。 + public static class Chain + { + public const string BossForestDefeated = "Chain_BossForest_Defeated"; + public const string CaveBossDefeated = "Chain_CaveBoss_Defeated"; + public const string CastleBossDefeated = "Chain_CastleBoss_Defeated"; + public const string FinalBossDefeated = "Chain_FinalBoss_Defeated"; + public const string TutorialComplete = "Chain_Tutorial_Complete"; + } + + // ── Quest IDs ────────────────────────────────────────────────────── + /// 任务唯一标识符,对应 QuestSO.questId。 + public static class Quest + { + public const string MainQuest_Chapter1 = "Quest_Main_Ch1"; + public const string MainQuest_Chapter2 = "Quest_Main_Ch2"; + public const string SideQuest_Merchant = "Quest_Side_Merchant"; + } + + // ── Ability IDs ──────────────────────────────────────────────────── + /// 能力/技能唯一标识符,对应 AbilityUnlockedCondition.abilityId。 + public static class Ability + { + public const string DoubleJump = "Ability_DoubleJump"; + public const string Dash = "Ability_Dash"; + public const string WallJump = "Ability_WallJump"; + public const string Parry = "Ability_Parry"; + } + + // ── Scene / Room Names ───────────────────────────────────────────── + /// 场景/房间标识符,对应 RoomEnteredCondition.sceneName。与 Unity Build Settings 场景名对齐。 + public static class Scene + { + public const string Forest = "Scene_Forest"; + public const string Cave = "Scene_Cave"; + public const string Castle = "Scene_Castle"; + public const string Hub = "Scene_Hub"; + public const string Tutorial = "Scene_Tutorial"; + } + + // ── Collectible IDs ──────────────────────────────────────────────── + /// 可收集物唯一标识符,对应 CollectibleCollectedCondition.itemId。 + public static class Collectible + { + public const string AncientKey = "Item_AncientKey"; + public const string MapFragment = "Item_MapFragment"; + } + + // ── NPC IDs ──────────────────────────────────────────────────────── + /// NPC 唯一标识符,对应 DialogueCompletedCondition.npcId。 + public static class Npc + { + public const string OldMerchant = "Npc_OldMerchant"; + public const string SageGuard = "Npc_SageGuard"; + } + + // ── Flag IDs ─────────────────────────────────────────────────────── + /// 全局布尔存档标记,对应 FlagSetCondition.flagId 及 SaveManager.SetFlag/GetFlag。 + public static class Flag + { + public const string TutorialShown = "Flag_TutorialShown"; + public const string FirstDeath = "Flag_FirstDeath"; + public const string MerchantIntroduced = "Flag_MerchantIntroduced"; + } + } +} diff --git a/Assets/Scripts/Core/GameIds.cs.meta b/Assets/Scripts/Core/GameIds.cs.meta new file mode 100644 index 0000000..7449ad0 --- /dev/null +++ b/Assets/Scripts/Core/GameIds.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9133700debab31540bae36f9130c9791 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Core/GameManager.cs b/Assets/Scripts/Core/GameManager.cs index d65cf76..483916c 100644 --- a/Assets/Scripts/Core/GameManager.cs +++ b/Assets/Scripts/Core/GameManager.cs @@ -13,14 +13,9 @@ namespace BaseGames.Core [DefaultExecutionOrder(-1000)] public class GameManager : MonoBehaviour { - // ── 单例 ────────────────────────────────────────────────────────── - public static GameManager Instance { get; private set; } - // ── Inspector 引用 ──────────────────────────────────────────────── [Header("Managers")] [SerializeField] private SettingsManager _settingsManager; - [SerializeField] private DeathRespawnService _deathRespawnService; - [SerializeField] private SceneService _sceneService; [Header("Event Channels - Listen")] [SerializeField] private VoidEventChannelSO _onPlayerDied; @@ -35,54 +30,51 @@ namespace BaseGames.Core [SerializeField] private VoidEventChannelSO _onPlayerRespawned; // ── 状态机 ──────────────────────────────────────────────────────── - private readonly GameStateMachine _fsm = new GameStateMachine(); + private readonly GameStateMachine _fsm = new GameStateMachine(); + private readonly CompositeDisposable _subs = new(); public GameStateId CurrentState => _fsm.CurrentStateId; + private static GameManager _instance; + // ────────────────────────────────────────────────────────────────── private void Awake() { - if (Instance != null && Instance != this) { Destroy(gameObject); return; } - Instance = this; - DontDestroyOnLoad(gameObject); + if (_instance != null) { Destroy(gameObject); return; } + _instance = this; + DontDestroyOnLoad(transform.root.gameObject); - RegisterServices(); RegisterStates(); _settingsManager?.Initialize(); _fsm.TransitionTo(GameStates.Initializing, out _); } - private void OnEnable() + private void Start() { - if (_onPlayerDied) _onPlayerDied.OnEventRaised += HandlePlayerDied; - if (_onPauseRequested) _onPauseRequested.OnEventRaised += HandlePauseRequested; - if (_onResumeRequested) _onResumeRequested.OnEventRaised += HandleResumeRequested; - if (_onBossFightStarted) _onBossFightStarted.OnEventRaised += HandleBossFightStarted; - if (_onBossFightEnded) _onBossFightEnded.OnEventRaised += HandleBossFightEnded; - if (_onDeathScreenConfirmed) _onDeathScreenConfirmed.OnEventRaised += HandleDeathScreenConfirmed; + // 在 Start 广播初始状态,确保其他组件已在 OnEnable 中完成订阅。 + _onGameStateChanged?.Raise(new Events.GameStateId(GameStates.Initializing.Id)); } - private void OnDisable() + private void OnEnable() { - if (_onPlayerDied) _onPlayerDied.OnEventRaised -= HandlePlayerDied; - if (_onPauseRequested) _onPauseRequested.OnEventRaised -= HandlePauseRequested; - if (_onResumeRequested) _onResumeRequested.OnEventRaised -= HandleResumeRequested; - if (_onBossFightStarted) _onBossFightStarted.OnEventRaised -= HandleBossFightStarted; - if (_onBossFightEnded) _onBossFightEnded.OnEventRaised -= HandleBossFightEnded; - if (_onDeathScreenConfirmed) _onDeathScreenConfirmed.OnEventRaised -= HandleDeathScreenConfirmed; + _onPlayerDied? .Subscribe(HandlePlayerDied).AddTo(_subs); + _onPauseRequested? .Subscribe(HandlePauseRequested).AddTo(_subs); + _onResumeRequested? .Subscribe(HandleResumeRequested).AddTo(_subs); + _onBossFightStarted? .Subscribe(HandleBossFightStarted).AddTo(_subs); + _onBossFightEnded? .Subscribe(HandleBossFightEnded).AddTo(_subs); + _onDeathScreenConfirmed?.Subscribe(HandleDeathScreenConfirmed).AddTo(_subs); + } + + private void OnDisable() => _subs.Clear(); + + private void OnDestroy() + { + if (_instance == this) _instance = null; } private void Update() => _fsm.Tick(Time.deltaTime); // ── 初始化 ──────────────────────────────────────────────────────── - private void RegisterServices() - { - if (_deathRespawnService) - ServiceLocator.Register(_deathRespawnService); - if (_sceneService) - ServiceLocator.Register(_sceneService); - } - private void RegisterStates() { _fsm.Register(new InitializingState()); @@ -111,8 +103,13 @@ namespace BaseGames.Core // ── 事件处理 ────────────────────────────────────────────────────── private void HandlePlayerDied() => StartCoroutine(DeathFlow()); - private void HandlePauseRequested() => RequestTransition(GameStates.Paused); - private void HandleResumeRequested() => RequestTransition(GameStates.Gameplay); + private void HandlePauseRequested() + { + _prePauseState = _fsm.CurrentStateId; + RequestTransition(GameStates.Paused); + } + + private void HandleResumeRequested() => RequestTransition(_prePauseState); private void HandleBossFightStarted(string bossId) => RequestTransition(GameStates.BossFight); @@ -123,13 +120,23 @@ namespace BaseGames.Core else RequestTransition(GameStates.GameOver); } - private bool _deathScreenConfirmed; + private bool _deathScreenConfirmed; + private GameStateId _prePauseState = GameStates.Gameplay; private void HandleDeathScreenConfirmed() => _deathScreenConfirmed = true; private IEnumerator DeathFlow() { RequestTransition(GameStates.Dead); var deathService = ServiceLocator.Get(); + + // SteelSoul 模式:清档后返回主菜单(架构 19 §6) + var scaler = ServiceLocator.GetOrDefault()?.CurrentScaler; + if (scaler != null && scaler.InstantDeathOnZeroHP) + { + yield return deathService.StartGameOverCoroutine(); + yield break; + } + yield return deathService.StartDeathSequenceCoroutine(); // 等待玩家在死亡画面点击重试 diff --git a/Assets/Scripts/Core/GameManager.cs.meta b/Assets/Scripts/Core/GameManager.cs.meta index 3baf4fd..e7ba91c 100644 --- a/Assets/Scripts/Core/GameManager.cs.meta +++ b/Assets/Scripts/Core/GameManager.cs.meta @@ -4,7 +4,7 @@ MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] - executionOrder: 0 + executionOrder: -1000 icon: {instanceID: 0} userData: assetBundleName: diff --git a/Assets/Scripts/Core/GameServiceRegistrar.cs b/Assets/Scripts/Core/GameServiceRegistrar.cs index 64d19fb..f6aa97c 100644 --- a/Assets/Scripts/Core/GameServiceRegistrar.cs +++ b/Assets/Scripts/Core/GameServiceRegistrar.cs @@ -1,5 +1,7 @@ using UnityEngine; +using UnityEngine.SceneManagement; using BaseGames.Core.Events; +using BaseGames.Core.Save; namespace BaseGames.Core { @@ -13,10 +15,25 @@ namespace BaseGames.Core [SerializeField] private DeathRespawnService _deathRespawnService; [SerializeField] private SceneService _sceneService; [SerializeField] private EventChannelRegistry _eventChannelRegistry; + [SerializeField] private SaveManager _saveManager; + /// + /// Persistent 场景中唯一保留的主 AudioListener(通常挂在主相机上)。 + /// 在 Inspector 中绑定后可完全跳过 Awake 时的 FindObjectsOfType 全场景扫描。 + /// 未绑定时自动执行运行时扫描。 + /// + [SerializeField] private AudioListener _primaryListener; + private bool _audioListenerFixLogged; private void Awake() { - // 注册 NullAudioService 作为兜底;Phase 2 Audio 模块 Awake 后会用真实实现覆盖 + // 若 Inspector 已绑定主 AudioListener,直接使用,跳过全场景扫描 + if (_primaryListener != null) + DisableDuplicateListenersInCurrentScenes(); + else + EnsureSingleAudioListener(); // 未绑定时回退:全量扫描并自动缓存 + SceneManager.sceneLoaded += OnSceneLoaded; + + // 注册 NullAudioService 作为兜底;AudioManager.Awake 后会用真实实现覆盖 ServiceLocator.RegisterIfAbsent(new NullAudioService()); if (_deathRespawnService) @@ -25,6 +42,150 @@ namespace BaseGames.Core ServiceLocator.Register(_sceneService); if (_eventChannelRegistry) ServiceLocator.Register(_eventChannelRegistry); + if (_saveManager) + ServiceLocator.Register(new SaveServiceAdapter(_saveManager)); + } + + private void OnDestroy() + { + SceneManager.sceneLoaded -= OnSceneLoaded; + } + + private void OnSceneLoaded(Scene scene, LoadSceneMode mode) + { + if (_primaryListener != null) + { + // 仅扫描新加载场景的根节点,避免 FindObjectsOfType 全场景遍历 + int disabled = 0; + foreach (var root in scene.GetRootGameObjects()) + foreach (var al in root.GetComponentsInChildren(true)) + { + if (al == _primaryListener || !al.enabled) continue; + al.enabled = false; + disabled++; + } + if (disabled > 0) + Debug.LogWarning($"[GameServiceRegistrar] Scene '{scene.name}' 中禁用了 {disabled} 个多余的 AudioListener。"); + } + else + { + // 首次加载时 _primaryListener 可能尚未缓存,回退到全量扫描 + EnsureSingleAudioListener(); + } + } + + /// + /// 当 Inspector 已预绑定 _primaryListener 时,无需全场景扫描。 + /// 仅遍历当前已加载场景的根节点,禁用多余的 AudioListener。 + /// + private void DisableDuplicateListenersInCurrentScenes() + { + int sceneCount = SceneManager.sceneCount; + int disabled = 0; + for (int s = 0; s < sceneCount; s++) + { + Scene scene = SceneManager.GetSceneAt(s); + if (!scene.isLoaded) continue; + foreach (var root in scene.GetRootGameObjects()) + foreach (var al in root.GetComponentsInChildren(true)) + { + if (al == _primaryListener || !al.enabled) continue; + al.enabled = false; + disabled++; + } + } + if (disabled > 0 && !_audioListenerFixLogged) + { + Debug.LogWarning($"[GameServiceRegistrar] Disabled {disabled} duplicate AudioListener(s). Primary: '{_primaryListener.gameObject.name}'."); + _audioListenerFixLogged = true; + } + } + + private void EnsureSingleAudioListener() + { + AudioListener[] listeners = FindObjectsOfType(true); AudioListener primary = null; + int enabledCount = 0; + + for (int i = 0; i < listeners.Length; i++) + { + AudioListener listener = listeners[i]; + if (listener == null || !listener.enabled || !listener.gameObject.activeInHierarchy) + continue; + + enabledCount++; + if (primary == null) + primary = listener; + + if (listener.gameObject.scene.name == "Persistent") + primary = listener; + } + + if (enabledCount <= 1 || primary == null) + { + _primaryListener = primary; // 缓存(含 enabledCount==1 时的单个监听器) + return; + } + + int disabled = 0; + for (int i = 0; i < listeners.Length; i++) + { + AudioListener listener = listeners[i]; + if (listener == null || listener == primary) + continue; + if (!listener.enabled || !listener.gameObject.activeInHierarchy) + continue; + + listener.enabled = false; + disabled++; + } + + _primaryListener = primary; // 缓存主监听器 + + if (disabled > 0 && !_audioListenerFixLogged) + { + Debug.LogWarning($"[GameServiceRegistrar] Found {enabledCount} active AudioListeners. Kept '{primary.gameObject.name}' and disabled {disabled} duplicate listener(s)."); + _audioListenerFixLogged = true; + } } } + + /// + /// 将 SaveManager 适配为 ISaveService。 + /// 由于 BaseGames.Core.Save 与 BaseGames.Core 存在循环依赖风险, + /// SaveManager 不直接声明实现 ISaveService;由此 Adapter 桥接, + /// 保持 Core → Core.Save 的单向依赖方向不变。 + /// + internal sealed class SaveServiceAdapter : ISaveService + { + private readonly BaseGames.Core.Save.SaveManager _save; + + internal SaveServiceAdapter(BaseGames.Core.Save.SaveManager save) => _save = save; + + // ── I/O 操作 ────────────────────────────────────────────────────── + public System.Threading.Tasks.Task SaveAsync(int slot) => _save.SaveAsync(slot); + public System.Threading.Tasks.Task LoadAsync(int slot) => _save.LoadAsync(slot); + public void QuickSave() => _save.QuickSave(); + public void QuickLoad() => _save.QuickLoad(); + public System.Threading.Tasks.Task QuickLoadAsync() => _save.LoadAsync(BaseGames.Core.Save.SaveManager.QuickSaveSlot); + public bool HasSave(int slot) => _save.SlotExists(slot); + public int ActiveSlot => _save.CurrentSlot; + public System.Threading.Tasks.Task DeleteSlotAsync(int slot) => _save.DeleteSlotAsync(slot); + + // ── 存档点 ──────────────────────────────────────────────────────── + public string LastCheckpointScene => _save.LastCheckpointScene; + public string LastCheckpointSpawnId => _save.LastCheckpointSpawnId; + + // ── 世界状态查询 ────────────────────────────────────────────────── + public bool IsWorldCollected(string id) => _save.IsWorldCollected(id); + public bool IsDoorOpened(string id) => _save.IsDoorOpened(id); + public bool IsBossDefeated(string bossId) => _save.IsBossDefeated(bossId); + public int GetPlayerMaxHP() => _save.GetPlayerMaxHP(); + public bool IsFirstClear(string challengeId) => _save.IsFirstClear(challengeId); + + // ── 世界标志 & 事件链 ───────────────────────────────────────────── + public bool GetFlag(string flagId) => _save.GetFlag(flagId); + public void SetFlag(string flagId, bool value) => _save.SetFlag(flagId, value); + public System.Collections.Generic.IEnumerable GetCompletedChains() => _save.GetCompletedChains(); + public void SetChainCompleted(string chainId) => _save.SetChainCompleted(chainId); + } } diff --git a/Assets/Scripts/Core/GameServiceRegistrar.cs.meta b/Assets/Scripts/Core/GameServiceRegistrar.cs.meta index f2fa92d..d12d3ef 100644 --- a/Assets/Scripts/Core/GameServiceRegistrar.cs.meta +++ b/Assets/Scripts/Core/GameServiceRegistrar.cs.meta @@ -4,7 +4,7 @@ MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] - executionOrder: 0 + executionOrder: -2000 icon: {instanceID: 0} userData: assetBundleName: diff --git a/Assets/Scripts/Core/GlobalSettingsSO.cs b/Assets/Scripts/Core/GlobalSettingsSO.cs index 0d25abe..163d3fd 100644 --- a/Assets/Scripts/Core/GlobalSettingsSO.cs +++ b/Assets/Scripts/Core/GlobalSettingsSO.cs @@ -41,6 +41,9 @@ namespace BaseGames.Core [Header("Language")] public string DefaultLanguage = "zh-CN"; + [Header("Speedrun")] + public bool ShowSpeedrunTimer = false; + /// 将 SO 默认值填入 GlobalSettingsData。 public GlobalSettingsData CreateDefault() => new GlobalSettingsData { diff --git a/Assets/Scripts/Core/IAudioService.cs b/Assets/Scripts/Core/IAudioService.cs index 6c5e270..f16c042 100644 --- a/Assets/Scripts/Core/IAudioService.cs +++ b/Assets/Scripts/Core/IAudioService.cs @@ -1,3 +1,5 @@ +using UnityEngine; + namespace BaseGames.Core { /// @@ -15,6 +17,9 @@ namespace BaseGames.Core /// 单次播放音效。 void PlaySFX(string key); + /// 在世界坐标播放音效片段(用于 3D 音效)。 + void PlaySFXAtPosition(AudioClip clip, Vector2 position, float volumeScale = 1f); + /// 设置混音器音量(0–1)。group 取 AudioMixerKeys 常量。 void SetVolume(string group, float normalizedVolume); } diff --git a/Assets/Scripts/Core/IObjectPoolService.cs b/Assets/Scripts/Core/IObjectPoolService.cs new file mode 100644 index 0000000..2b1e789 --- /dev/null +++ b/Assets/Scripts/Core/IObjectPoolService.cs @@ -0,0 +1,23 @@ +using UnityEngine; + +namespace BaseGames.Core +{ + /// + /// 全局对象池服务接口。 + /// 通过 ServiceLocator 访问,解耦调用方与 GlobalObjectPool 具体实现。 + /// + public interface IObjectPoolService + { + /// 从池中取出指定 Component 类型的对象,并激活到指定位置/朝向。 + T Spawn(string key, Vector3 position, Quaternion rotation) where T : Component; + + /// 从池中取出 GameObject,并激活到指定位置/朝向。 + GameObject Spawn(string key, Vector3 position, Quaternion rotation); + + /// 将对象归还到池中(停用并入队)。 + void Despawn(string key, Pool.PooledObject po); + + /// 清空指定 key 的对象池(场景卸载时调用)。 + void ClearPool(string key); + } +} diff --git a/Assets/Scripts/Core/IObjectPoolService.cs.meta b/Assets/Scripts/Core/IObjectPoolService.cs.meta new file mode 100644 index 0000000..e116446 --- /dev/null +++ b/Assets/Scripts/Core/IObjectPoolService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: de3e4fbb738acc34a8b1e4c18eae9e3a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Core/ISaveService.cs b/Assets/Scripts/Core/ISaveService.cs index 83bc6b7..e1b6c34 100644 --- a/Assets/Scripts/Core/ISaveService.cs +++ b/Assets/Scripts/Core/ISaveService.cs @@ -1,23 +1,29 @@ +using System.Collections.Generic; using System.Threading.Tasks; namespace BaseGames.Core { /// - /// 存档服务接口。对外暴露存档系统的高层操作,供其他模块通过 ServiceLocator 访问。 - /// 实现由 BaseGames.Core.Save 程序集的 SaveManager 提供。 + /// 存档服务接口。对外暴露存档系统的全部公开 API,供其他模块通过 ServiceLocator 访问。 + /// 实现由 BaseGames.Core.Save 程序集的 SaveManager 经 SaveServiceAdapter 桥接提供。 /// public interface ISaveService { + // ── I/O 操作 ────────────────────────────────────────────────────── + /// 将当前游戏状态写入指定存档槽。 Task SaveAsync(int slot); /// 从指定存档槽加载游戏状态。成功返回 true,存档损坏/不存在返回 false。 Task LoadAsync(int slot); - /// 快速存档(覆盖当前活跃槽)。 + /// 快速存档(覆盖快速存档槽,fire-and-forget)。 void QuickSave(); - /// 快速读档(从当前活跃槽加载)。 + /// 快速读档(从快速存档槽加载,fire-and-forget)。 + void QuickLoad(); + + /// 快速读档(awaitable 版本)。 Task QuickLoadAsync(); /// 指定槽是否存在有效存档。 @@ -25,5 +31,47 @@ namespace BaseGames.Core /// 当前活跃存档槽(0–2)。 int ActiveSlot { get; } + + /// 删除指定存档槽数据。 + Task DeleteSlotAsync(int slot); + + // ── 存档点 ──────────────────────────────────────────────────────── + + /// 上次存档时的场景名(用于死亡复活跳转)。 + string LastCheckpointScene { get; } + + /// 上次存档时的出生点 ID。 + string LastCheckpointSpawnId { get; } + + // ── 世界状态查询 ────────────────────────────────────────────────── + + /// 指定收藏物 ID 是否已拾取。 + bool IsWorldCollected(string id); + + /// 指定门 / 进程锁 ID 是否已开启。 + bool IsDoorOpened(string id); + + /// 指定 Boss ID 是否已被击败。 + bool IsBossDefeated(string bossId); + + /// 当前存档中玩家最大 HP(存档未加载时返回 0)。 + int GetPlayerMaxHP(); + + /// 是否为指定挑战房间的首次通关(首次调用返回 true 并标记)。 + bool IsFirstClear(string challengeId); + + // ── 世界标志 & 事件链 ───────────────────────────────────────────── + + /// 读取世界标志位。 + bool GetFlag(string flagId); + + /// 写入世界标志位。 + void SetFlag(string flagId, bool value); + + /// 获取所有已完成的事件链 ID。 + IEnumerable GetCompletedChains(); + + /// 标记某条事件链已完成。 + void SetChainCompleted(string chainId); } } diff --git a/Assets/Scripts/Core/ISettingsService.cs b/Assets/Scripts/Core/ISettingsService.cs new file mode 100644 index 0000000..9ebd7b5 --- /dev/null +++ b/Assets/Scripts/Core/ISettingsService.cs @@ -0,0 +1,22 @@ +// Assets/Scripts/Core/ISettingsService.cs +// 全局设置服务接口,通过 ServiceLocator 注册与查询。 +// SettingsManager 实现此接口;调用方通过 ISettingsService 解耦。 + +using UnityEngine; + +namespace BaseGames.Core +{ + public interface ISettingsService + { + GlobalSettingsData Current { get; } + void SetMasterVolume(float v); + void SetBGMVolume(float v); + void SetSFXVolume(float v); + void SetAmbientVolume(float v); + void SetResolution(int w, int h, FullScreenMode mode); + void SetVSync(bool enabled); + void SetTargetFrameRate(int fps); + void SetLanguage(string localeCode); + void Save(); + } +} diff --git a/Assets/Scripts/Core/ISettingsService.cs.meta b/Assets/Scripts/Core/ISettingsService.cs.meta new file mode 100644 index 0000000..0e7d2e2 --- /dev/null +++ b/Assets/Scripts/Core/ISettingsService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 691c1a15bfcb61444b5a4a342fa6bd88 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Core/NullAudioService.cs b/Assets/Scripts/Core/NullAudioService.cs index 7bae663..5856091 100644 --- a/Assets/Scripts/Core/NullAudioService.cs +++ b/Assets/Scripts/Core/NullAudioService.cs @@ -5,7 +5,7 @@ namespace BaseGames.Core /// /// IAudioService 的空实现,作为兜底防止 NullReferenceException。 /// 在 GameServiceRegistrar 中作为默认音频服务注册; - /// Phase 2 Audio 模块实现完整后替换。 + /// AudioManager 初始化后会覆盖此实现。 /// public sealed class NullAudioService : IAudioService { @@ -18,6 +18,9 @@ namespace BaseGames.Core public void PlaySFX(string key) => Debug.LogWarning($"[NullAudioService] PlaySFX({key}) — 音频系统未初始化。"); + public void PlaySFXAtPosition(AudioClip clip, Vector2 position, float volumeScale = 1f) + => Debug.LogWarning($"[NullAudioService] PlaySFXAtPosition({clip?.name}) — 音频系统未初始化。"); + public void SetVolume(string group, float normalizedVolume) => Debug.LogWarning($"[NullAudioService] SetVolume({group}, {normalizedVolume}) — 音频系统未初始化。"); } diff --git a/Assets/Scripts/Core/Pool/GlobalObjectPool.cs b/Assets/Scripts/Core/Pool/GlobalObjectPool.cs index 6255eac..67239b7 100644 --- a/Assets/Scripts/Core/Pool/GlobalObjectPool.cs +++ b/Assets/Scripts/Core/Pool/GlobalObjectPool.cs @@ -11,10 +11,8 @@ namespace BaseGames.Core.Pool /// 先调用 预热后,再通过 取出对象。 /// [DefaultExecutionOrder(-800)] - public class GlobalObjectPool : MonoBehaviour + public class GlobalObjectPool : MonoBehaviour, IObjectPoolService { - public static GlobalObjectPool Instance { get; private set; } - [System.Serializable] public struct PoolConfig { @@ -26,29 +24,28 @@ namespace BaseGames.Core.Pool [SerializeField] private PoolConfig[] _warmupConfigs; - private readonly Dictionary> _pools = new(); - private readonly Dictionary> _alive = new(); - private readonly Dictionary _prefabCache = new(); - private readonly Dictionary _maxCounts = new(); + private readonly Dictionary> _pools = new(); + private readonly Dictionary> _alive = new(); + private readonly Dictionary _prefabCache = new(); + private readonly Dictionary _maxCounts = new(); private void Awake() { - if (Instance != null) { Destroy(gameObject); return; } - Instance = this; + if (ServiceLocator.GetOrDefault() != null) { Destroy(gameObject); return; } + ServiceLocator.Register(this); + } + + private void OnDestroy() + { + ServiceLocator.Unregister(this); } // ── 预热 ────────────────────────────────────────────────────────── - /// 在场景加载完成后(StartCoroutine)调用预热。 - public IEnumerator WarmupCoroutine() - { - foreach (var cfg in _warmupConfigs) - { - _maxCounts[cfg.AddressKey] = cfg.MaxCount; - yield return WarmupSingleCoroutine(cfg.AddressKey, cfg.InitialCount); - } - } - - /// async Task 版本(可 await,供非 MonoBehaviour 调用)。 + /// + /// 异步预热所有配置中的对象。 + /// MonoBehaviour 中可用 StartCoroutine(pool.WarmupAsync().AsIEnumerator()) 桥接, + /// 或直接 await(UniTask / Awaitable)。 + /// public async Task WarmupAsync() { foreach (var cfg in _warmupConfigs) @@ -58,17 +55,6 @@ namespace BaseGames.Core.Pool } } - private IEnumerator WarmupSingleCoroutine(string key, int count) - { - var loadOp = Addressables.LoadAssetAsync(key); - yield return loadOp; - var prefab = loadOp.Result; - _prefabCache[key] = prefab; - EnsureCollections(key, count); - for (int i = 0; i < count; i++) - EnqueueNew(key, prefab); - } - private async Task WarmupSingleAsync(string key, int count) { var prefab = await Addressables.LoadAssetAsync(key).Task; @@ -81,7 +67,9 @@ namespace BaseGames.Core.Pool private void EnsureCollections(string key, int capacity) { if (!_pools.ContainsKey(key)) _pools[key] = new Queue(capacity); - if (!_alive.ContainsKey(key)) _alive[key] = new List(); + // MaxCount==0 表示无上限,无需追踪活跃对象,跳过 _alive 分配 + if (_maxCounts.GetValueOrDefault(key, 0) > 0 && !_alive.ContainsKey(key)) + _alive[key] = new LinkedList(); } private void EnqueueNew(string key, GameObject prefab) @@ -105,23 +93,25 @@ namespace BaseGames.Core.Pool { if (!_pools.TryGetValue(key, out var queue)) { - Debug.LogError($"[GlobalObjectPool] '{key}' 未预热,请先调用 WarmupAsync/WarmupCoroutine。"); + Debug.LogError($"[GlobalObjectPool] '{key}' 未预热,请先调用 WarmupAsync。"); return null; } PooledObject po; - var aliveList = GetAliveList(key); - int maxCount = _maxCounts.GetValueOrDefault(key, 0); + int maxCount = _maxCounts.GetValueOrDefault(key, 0); + // maxCount==0 时不追踪活跃对象,aliveList 保持 null + LinkedList aliveList = maxCount > 0 ? GetAliveList(key) : null; if (queue.Count > 0) { po = queue.Dequeue(); } - else if (maxCount > 0 && aliveList.Count >= maxCount) + else if (aliveList != null && aliveList.Count >= maxCount) { - // 已达上限:LRU 回收最早 Spawn 的活跃对象 - po = aliveList[0]; - aliveList.RemoveAt(0); + // 已达上限:LRU 回收最早 Spawn 的活跃对象(LinkedList 头节点即最老) + po = aliveList.First.Value; + aliveList.RemoveFirst(); // O(1) + po.AliveNode = null; po.ForceReturnToPool(); Debug.LogWarning($"[GlobalObjectPool] '{key}' 已达 MaxCount={maxCount},LRU 回收中。"); } @@ -143,22 +133,30 @@ namespace BaseGames.Core.Pool po.transform.SetPositionAndRotation(pos, rot); po.gameObject.SetActive(true); po.OnSpawn(); - aliveList.Add(po); + // 存储节点引用,供 Despawn 时 O(1) 移除 + if (aliveList != null) + po.AliveNode = aliveList.AddLast(po); // 尾部 = 最新,头部 = 最老(LRU) return po; } // ── Despawn ─────────────────────────────────────────────────────── public void Despawn(string key, PooledObject po) { - var aliveList = GetAliveList(key); - aliveList.Remove(po); + // 通过存储的节点 O(1) 移除,避免 LinkedList.Remove(value) 的 O(n) 遍历 + if (po.AliveNode != null) + { + if (_alive.TryGetValue(key, out var aliveRef)) + aliveRef.Remove(po.AliveNode); + po.AliveNode = null; + } po.gameObject.SetActive(false); po.OnDespawn(); int maxCount = _maxCounts.GetValueOrDefault(key, 0); int queueSize = _pools.TryGetValue(key, out var queue) ? queue.Count : 0; + int aliveCount = (maxCount > 0 && _alive.TryGetValue(key, out var al)) ? al.Count : 0; - if (maxCount > 0 && queueSize + aliveList.Count >= maxCount) + if (maxCount > 0 && queueSize + aliveCount >= maxCount) { Destroy(po.gameObject); return; @@ -204,10 +202,10 @@ namespace BaseGames.Core.Pool } } - private List GetAliveList(string key) + private LinkedList GetAliveList(string key) { if (!_alive.TryGetValue(key, out var list)) - _alive[key] = list = new List(); + _alive[key] = list = new LinkedList(); return list; } } diff --git a/Assets/Scripts/Core/Pool/PooledObject.cs b/Assets/Scripts/Core/Pool/PooledObject.cs index 7c0213c..5d8792a 100644 --- a/Assets/Scripts/Core/Pool/PooledObject.cs +++ b/Assets/Scripts/Core/Pool/PooledObject.cs @@ -14,6 +14,13 @@ namespace BaseGames.Core.Pool public string AddressKey { get; private set; } private GlobalObjectPool _pool; + /// + /// 对象在 GlobalObjectPool._alive 链表中的节点引用。 + /// 存储节点可将 Despawn 时的链表移除从 O(n) 降为 O(1)。 + /// 仅 GlobalObjectPool 内部读写,业务代码不应访问。 + /// + internal LinkedListNode AliveNode; + // 组件缓存(避免反复 GetComponent) private readonly Dictionary _componentCache = new(); diff --git a/Assets/Scripts/Core/Save/CrashReporter.cs b/Assets/Scripts/Core/Save/CrashReporter.cs new file mode 100644 index 0000000..1da1bd0 --- /dev/null +++ b/Assets/Scripts/Core/Save/CrashReporter.cs @@ -0,0 +1,65 @@ +using System; +using System.IO; +using UnityEngine; + +namespace BaseGames.Core.Save +{ + /// + /// 崩溃检测与诊断日志写入。 + /// 监听 Unity 异常日志并在 OnApplicationPause 非正常退出时触发紧急存档(槽 99)。 + /// + public class CrashReporter : MonoBehaviour + { + [SerializeField] private SaveManager _saveManager; + [SerializeField] private EmergencySaveService _emergencyService; + + private bool _cleanExit; + + private void OnEnable() + { + Application.logMessageReceived += OnLogMessage; + Application.quitting += OnCleanQuit; + } + + private void OnDisable() + { + Application.logMessageReceived -= OnLogMessage; + Application.quitting -= OnCleanQuit; + } + + private void OnCleanQuit() => _cleanExit = true; + + private void OnApplicationPause(bool pauseStatus) + { + // 移动端:App 被切出且未完成正常退出流程时触发紧急存档 + if (pauseStatus && !_cleanExit && _saveManager != null) + _ = _saveManager.SaveAsync(EmergencySaveService.EmergencySlot); + } + + private void OnLogMessage(string condition, string stackTrace, LogType type) + { + if (type == LogType.Exception || type == LogType.Error) + WriteDiagnosticLog(condition, stackTrace); + } + + /// 检查是否存在上次崩溃或意外退出留下的紧急存档。 + public bool HasEmergencySave() + => _emergencyService != null && _emergencyService.HasEmergencySave(); + + /// 同步写入崩溃诊断日志——崩溃场景下 async 不可靠,改用同步 IO。 + private void WriteDiagnosticLog(string condition, string stackTrace) + { + try + { + string timestamp = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss"); + string logPath = Path.Combine(Application.persistentDataPath, $"crash_{timestamp}.log"); + string content = $"[{DateTime.UtcNow:o}]\n{condition}\n\n{stackTrace}"; + File.WriteAllText(logPath, content); + } + catch + { + // 日志写入失败不能再抛异常,否则会造成无限递归 + } + } + } +} diff --git a/Assets/Scripts/Core/Save/CrashReporter.cs.meta b/Assets/Scripts/Core/Save/CrashReporter.cs.meta new file mode 100644 index 0000000..ff77d3e --- /dev/null +++ b/Assets/Scripts/Core/Save/CrashReporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 18aff6d8d08f33f43a5980c8c4f94c2b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Core/Save/EmergencySaveService.cs b/Assets/Scripts/Core/Save/EmergencySaveService.cs index 3ca3c11..b936b62 100644 --- a/Assets/Scripts/Core/Save/EmergencySaveService.cs +++ b/Assets/Scripts/Core/Save/EmergencySaveService.cs @@ -6,7 +6,7 @@ namespace BaseGames.Core.Save { public class EmergencySaveService : MonoBehaviour { - private const int EmergencySlot = 99; + public const int EmergencySlot = 99; [SerializeField] private float _intervalSeconds = 120f; [SerializeField] private SaveManager _saveManager; @@ -14,16 +14,14 @@ namespace BaseGames.Core.Save private bool _gameplayActive; private float _timer; + private readonly CompositeDisposable _subscriptions = new(); private void OnEnable() { - if (_onGameplayActive != null) _onGameplayActive.OnEventRaised += OnGameplayActiveChanged; + _onGameplayActive?.Subscribe(OnGameplayActiveChanged).AddTo(_subscriptions); } - private void OnDisable() - { - if (_onGameplayActive != null) _onGameplayActive.OnEventRaised -= OnGameplayActiveChanged; - } + private void OnDisable() => _subscriptions.Clear(); private void OnGameplayActiveChanged(bool value) => _gameplayActive = value; @@ -44,8 +42,12 @@ namespace BaseGames.Core.Save public async Task PromoteToSlot(int targetSlot) { if (_saveManager == null) return; - // Phase 1 stub:完整实现在 Phase 2 LocalFileStorage API - await Task.CompletedTask; + var storage = new LocalFileStorage(); + if (!storage.Exists(EmergencySlot)) return; + string json = await storage.ReadAsync(EmergencySlot); + if (json == null) return; + await storage.WriteAsync(targetSlot, json); + await storage.DeleteAsync(EmergencySlot); } } } diff --git a/Assets/Scripts/Core/Save/ISaveableRegistry.cs b/Assets/Scripts/Core/Save/ISaveableRegistry.cs new file mode 100644 index 0000000..b73be79 --- /dev/null +++ b/Assets/Scripts/Core/Save/ISaveableRegistry.cs @@ -0,0 +1,16 @@ +namespace BaseGames.Core.Save +{ + /// + /// ISaveable 注册表接口。 + /// 将 ISaveable 对象的注册/注销责任与 SaveManager 的具体实现类解耦, + /// 使 SaveableMonoBehaviour 等组件无需直接依赖 。 + /// + public interface ISaveableRegistry + { + /// 加入存档系统管理。 + void Register(ISaveable saveable); + + /// 从存档系统移除。 + void Unregister(ISaveable saveable); + } +} diff --git a/Assets/Scripts/Core/Save/ISaveableRegistry.cs.meta b/Assets/Scripts/Core/Save/ISaveableRegistry.cs.meta new file mode 100644 index 0000000..2745a6d --- /dev/null +++ b/Assets/Scripts/Core/Save/ISaveableRegistry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2b59605354f1e174593553d2d09fb624 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Core/Save/LocalFileStorage.cs b/Assets/Scripts/Core/Save/LocalFileStorage.cs index 31a5a7b..b23bd44 100644 --- a/Assets/Scripts/Core/Save/LocalFileStorage.cs +++ b/Assets/Scripts/Core/Save/LocalFileStorage.cs @@ -11,6 +11,9 @@ namespace BaseGames.Core.Save /// public class LocalFileStorage : ISaveStorage { + /// 存档槽位总数。调整此值时需同步更新 UI 存档槽列表。 + public const int MaxSlots = 3; + private readonly string _saveDir; public LocalFileStorage() @@ -54,7 +57,7 @@ namespace BaseGames.Core.Save public IEnumerable GetExistingSlots() { - for (int i = 0; i < 3; i++) + for (int i = 0; i < MaxSlots; i++) if (Exists(i)) yield return i; } diff --git a/Assets/Scripts/Core/Save/SaveData.cs b/Assets/Scripts/Core/Save/SaveData.cs index 92a4b96..3ffe58a 100644 --- a/Assets/Scripts/Core/Save/SaveData.cs +++ b/Assets/Scripts/Core/Save/SaveData.cs @@ -25,6 +25,8 @@ namespace BaseGames.Core.Save public ShopsSaveData Shops = new(); public StatsSaveData Stats = new(); public NGPlusSaveData NGPlus = null; // null = 非 NG+ 模式 + public TutorialSaveData Tutorial = new(); + public SettingsSaveData Settings = new(); public Dictionary DLC = new(); } @@ -40,6 +42,8 @@ namespace BaseGames.Core.Save public int NGPlusCount; public int SaveCount; public string Checksum; // HMAC-SHA256 + /// 此存档是否以钢铁之魂(一命)模式开始;加载后 DifficultyManager 会锁定到 SteelSoul。 + public bool IsSteelSoul; } // ─── Player ─────────────────────────────────────────────────────────────── @@ -92,6 +96,7 @@ namespace BaseGames.Core.Save public List OpenedDoors = new(); public List DefeatedBossIds = new(); public List CollectedIds = new(); + public List DestroyedObjectIds = new(); public Dictionary Switches = new(); public Dictionary NpcRelations = new(); public HashSet ChallengeFirstClears = new(); @@ -101,8 +106,29 @@ namespace BaseGames.Core.Save [Serializable] public class MapSaveData { - public Dictionary> DiscoveredRooms = new(); - public Dictionary MapPurchased = new(); + public List ExploredRooms = new(); // 踏入过的房间 ID + public List MappedRooms = new(); // 完整地图信息(购买/存档点揭示) + public List Pins = new(); // 玩家自定义地图标记 + } + + /// 玩家在地图上放置的自定义标记(架构 15_MapShopModule §1.5)。 + [Serializable] + public class MapPin + { + public string RoomId; + public float NormalizedPosX; + public float NormalizedPosY; + public int PinTypeInt; // PinType 枚举整数值,避免 SaveData 依赖 Map 程序集 + public string Note; // 玩家备注(最多 64 字符) + } + + public enum PinType + { + Marker = 0, + Chest = 1, + Enemy = 2, + Path = 3, + Note = 4, } // ─── Quests ─────────────────────────────────────────────────────────────── @@ -144,6 +170,7 @@ namespace BaseGames.Core.Save public int EnemyKills, Deaths, ParrySuccess, ParryFail; public int GeoEarned, GeoLost; public float DistanceTraveled; + public float SpeedrunTime; public int SaveCount; public Dictionary SkillUseCounts = new(); public Dictionary DeathsByBoss = new(); @@ -215,4 +242,20 @@ namespace BaseGames.Core.Save public string SceneName; public string ActiveFormId; } + + // ─── Tutorial ───────────────────────────────────────────────────────────── + [Serializable] + public class TutorialSaveData + { + /// 已完成(不再弹出)的提示 ID 列表。 + public List CompletedHintIds = new(); + } + + // ─── Settings ───────────────────────────────────────────────────────────── + [Serializable] + public class SettingsSaveData + { + /// 玩家选择的语言。空字符串 = 使用系统默认。 + public string Language = string.Empty; + } } diff --git a/Assets/Scripts/Core/Save/SaveManager.cs b/Assets/Scripts/Core/Save/SaveManager.cs index e133d34..68626aa 100644 --- a/Assets/Scripts/Core/Save/SaveManager.cs +++ b/Assets/Scripts/Core/Save/SaveManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; using UnityEngine; @@ -8,33 +9,35 @@ using UnityEngine; namespace BaseGames.Core.Save { /// - /// 存档管理器(Phase 0 骨架)。 + /// 存档管理器。 /// 完整序列化/反序列化由 Newtonsoft.Json 驱动。 /// [DefaultExecutionOrder(-900)] - public class SaveManager : MonoBehaviour + public class SaveManager : MonoBehaviour, ISaveableRegistry { - public static SaveManager Instance { get; private set; } - - public const string MinCompatibleVersion = "1.0"; - private const int QuickSaveSlot = 98; + public const int QuickSaveSlot = 98; [Header("Event Channels - Raise")] [SerializeField] private BaseGames.Core.Events.BoolEventChannelSO _onSaveIndicatorVisible; private ISaveStorage _storage; - private readonly List _saveables = new(); + private readonly HashSet _saveables = new(); + private readonly SemaphoreSlim _saveLock = new SemaphoreSlim(1, 1); private SaveData _current; private int _currentSlot = 0; + public int CurrentSlot => _currentSlot; - public static string LastCheckpointScene { get; private set; } - public static string LastCheckpointSpawnId { get; private set; } + public string LastCheckpointScene { get; private set; } + public string LastCheckpointSpawnId { get; private set; } + + private static SaveManager _instance; private void Awake() { - if (Instance != null) { Destroy(gameObject); return; } - Instance = this; + if (_instance != null) { Destroy(gameObject); return; } + _instance = this; + ServiceLocator.Register(this); _storage = new LocalFileStorage(); } @@ -46,69 +49,113 @@ namespace BaseGames.Core.Save // ── 存档 ────────────────────────────────────────────────────────────── public async Task SaveAsync(int slot = -1) { - int targetSlot = slot < 0 ? _currentSlot : slot; - _current ??= new SaveData(); + await _saveLock.WaitAsync(); + try + { + int targetSlot = slot < 0 ? _currentSlot : slot; + _current ??= new SaveData(); - _onSaveIndicatorVisible?.Raise(true); - foreach (var s in _saveables) s.OnSave(_current); + _onSaveIndicatorVisible?.Raise(true); + var snapshot = _saveables.ToList(); + foreach (var s in snapshot) s.OnSave(_current); - _current.Meta.LastSaved = DateTime.UtcNow.ToString("o"); - _current.Meta.SlotIndex = targetSlot; - _current.Meta.SaveCount++; + _current.Meta.LastSaved = DateTime.UtcNow.ToString("o"); + _current.Meta.SlotIndex = targetSlot; + _current.Meta.SaveCount++; - // 先清空 checksum,序列化并计算,再序列化含 checksum 的最终版本 - _current.Meta.Checksum = null; - string jsonForChecksum = JsonConvert.SerializeObject(_current, Formatting.Indented); - _current.Meta.Checksum = ComputeChecksum(jsonForChecksum); - string finalJson = JsonConvert.SerializeObject(_current, Formatting.Indented); + // 先清空 checksum,序列化并计算,再序列化含 checksum 的最终版本 + // 使用 Formatting.None 减少序列化字符串体积和 GC 分配 + _current.Meta.Checksum = null; + string jsonForChecksum = JsonConvert.SerializeObject(_current, Formatting.None); + _current.Meta.Checksum = ComputeChecksum(jsonForChecksum); + string finalJson = JsonConvert.SerializeObject(_current, Formatting.None); - await _storage.WriteAsync(targetSlot, finalJson); + await _storage.WriteAsync(targetSlot, finalJson); - LastCheckpointScene = _current.Player?.Scene; - LastCheckpointSpawnId = _current.Meta?.SavePointId; + LastCheckpointScene = _current.Player?.Scene; + LastCheckpointSpawnId = _current.Meta?.SavePointId; - _onSaveIndicatorVisible?.Raise(false); + _onSaveIndicatorVisible?.Raise(false); + } + finally + { + _saveLock.Release(); + } } // ── 读档 ────────────────────────────────────────────────────────────── public async Task LoadAsync(int slot) { - if (!_storage.Exists(slot)) return false; - - string json = await _storage.ReadAsync(slot); - if (json == null) return false; - - SaveData loaded; - try { loaded = JsonConvert.DeserializeObject(json); } - catch (Exception e) + await _saveLock.WaitAsync(); + try { - Debug.LogError($"[SaveManager] 存档解析失败 slot={slot}: {e.Message}"); - return false; - } + if (!_storage.Exists(slot)) return false; - if (!ValidateChecksum(loaded, json)) + string json = await _storage.ReadAsync(slot); + if (json == null) return false; + + SaveData loaded; + try { loaded = JsonConvert.DeserializeObject(json); } + catch (Exception e) + { + Debug.LogError($"[SaveManager] 存档解析失败 slot={slot}: {e.Message}"); + return false; + } + + if (!ValidateChecksum(loaded, json)) + { + Debug.LogWarning("[SaveManager] 存档 checksum 校验失败。"); + // 非 SteelSoul 模式仍允许加载(只是警告) + } + + loaded = SaveMigrator.Migrate(loaded); + _current = loaded; + _currentSlot = slot; + + var snapshot = _saveables.ToList(); + foreach (var s in snapshot) s.OnLoad(_current); + + LastCheckpointScene = _current.Player?.Scene; + LastCheckpointSpawnId = _current.Meta?.SavePointId; + return true; + } + finally { - Debug.LogWarning("[SaveManager] 存档 checksum 校验失败。"); - // 非 SteelSoul 模式仍允许加载(只是警告) + _saveLock.Release(); } - - loaded = SaveMigrator.Migrate(loaded); - _current = loaded; - _currentSlot = slot; - - foreach (var s in _saveables) s.OnLoad(_current); - - LastCheckpointScene = _current.Player?.Scene; - LastCheckpointSpawnId = _current.Meta?.SavePointId; - return true; } public bool SlotExists(int slot) => _storage.Exists(slot); public IEnumerable GetExistingSlots() => _storage.GetExistingSlots(); + // ── 具名数据访问器(替代直接访问 Data 属性) ────────────────────────────────── + /// 判断指定收藏物 ID 是否已拾取。 + public bool IsWorldCollected(string id) + => _current?.World.CollectedIds.Contains(id) == true; + + /// 判断指定门 / 进程锁 ID 是否已开启。 + public bool IsDoorOpened(string id) + => _current?.World.OpenedDoors.Contains(id) == true; + + /// 返回当前存档中玩家最大 HP(存档未加载时返回 0)。 + public int GetPlayerMaxHP() => _current?.Player.MaxHP ?? 0; + // ── 快速存档(挑战房间用)──────────────────────────────────────────── - public void QuickSave() => _ = SaveAsync(QuickSaveSlot); - public void QuickLoad() => _ = LoadAsync(QuickSaveSlot); + public void QuickSave() => RunFireAndForget(SaveAsync(QuickSaveSlot), "QuickSave"); + public void QuickLoad() => RunFireAndForget(LoadAsync(QuickSaveSlot), "QuickLoad"); + + /// + /// 将 async Task 包裹为 async void,捕获所有异常并输出到 Log。 + /// 用于无法 await 的调用点(按钮回调、MonoBehaviour 方法等)。 + /// + private static async void RunFireAndForget(Task task, string context) + { + try { await task; } + catch (Exception e) + { + Debug.LogError($"[SaveManager] {context} 失败: {e.Message}\n{e.StackTrace}"); + } + } // ── 世界标志 / EventChain ───────────────────────────────────────────── public bool GetFlag(string flagId) @@ -159,6 +206,12 @@ namespace BaseGames.Core.Save _current.Meta.SlotIndex = slotIndex; } + public async Task DeleteSlotAsync(int slotIndex) + { + await _storage.DeleteAsync(slotIndex); + if (_currentSlot == slotIndex) _current = null; + } + // ── 私有工具 ────────────────────────────────────────────────────────── private string ComputeChecksum(string json) { @@ -170,9 +223,10 @@ namespace BaseGames.Core.Save private string GetHmacKey() { - string deviceId = SystemInfo.deviceUniqueIdentifier; - const string salt = "ZelingV2SaveIntegrity_v1"; - return $"{deviceId}:{salt}"; + // ⚠️ 不使用 deviceUniqueIdentifier——设备唯一标识在玩家换设备时会改变, + // 导致所有存档 checksum 永久失效。改用游戏固定密钥。 + const string gameSecret = "ZelingV2SaveIntegrity_v2_9a3f7c1b"; + return gameSecret; } private bool ValidateChecksum(SaveData data, string rawJson) @@ -180,9 +234,15 @@ namespace BaseGames.Core.Save if (string.IsNullOrEmpty(data?.Meta?.Checksum)) return true; var saved = data.Meta.Checksum; data.Meta.Checksum = null; - string jsonNoChecksum = JsonConvert.SerializeObject(data, Formatting.Indented); + string jsonNoChecksum = JsonConvert.SerializeObject(data, Formatting.None); data.Meta.Checksum = saved; return ComputeChecksum(jsonNoChecksum) == saved; } + + private void OnDestroy() + { + if (_instance == this) _instance = null; + ServiceLocator.Unregister(this); + } } } diff --git a/Assets/Scripts/Core/Save/SaveMigrator.cs b/Assets/Scripts/Core/Save/SaveMigrator.cs index df45252..adcae5f 100644 --- a/Assets/Scripts/Core/Save/SaveMigrator.cs +++ b/Assets/Scripts/Core/Save/SaveMigrator.cs @@ -1,60 +1,68 @@ -using System.Collections.Generic; using UnityEngine; namespace BaseGames.Core.Save { /// - /// 处理存档版本升级(旧版 → 新版数据结构)。 + /// 处理存档版本迁移。 + /// 迁移链:旧版本 → "2.0" → "2.1"(CurrentVersion),每个分支按顺序落下执行(fall-through)。 /// public static class SaveMigrator { - private const string CurrentVersion = "2.1"; + public const string CurrentVersion = "2.1"; public static SaveData Migrate(SaveData data) { if (data?.Meta == null) return data; - switch (data.Meta.Version) + + string v = data.Meta.Version ?? ""; + + // ── 旧版本(< 2.0)→ 2.0 ─────────────────────────────────────────── + if (string.IsNullOrEmpty(v) || IsOlderThan(v, "2.0")) { - case "1.0": data = MigrateFrom1_0(data); goto case "1.1"; - case "1.1": data = MigrateFrom1_1(data); goto case "2.0"; - case "2.0": data = MigrateFrom2_0(data); goto case "2.1"; - case "2.1": break; - default: - Debug.LogWarning($"[SaveMigrator] 未知存档版本 '{data.Meta.Version}',跳过迁移。"); - break; + // 2.0 引入的新存档节:若为 null 则补充空白对象 + data.Tutorial ??= new TutorialSaveData(); + data.Settings ??= new SettingsSaveData(); + data.EventChains ??= new EventChainsSaveData(); + data.ChallengeRooms ??= new ChallengeRoomsSaveData(); + data.NGPlus = null; // 非 NG+ 模式 + + // Player 子字段兼容(旧版本可能未记录 DeathShade) + if (data.Player != null) + data.Player.DeathShade ??= new DeathShadeSaveData(); + + Debug.Log($"[SaveMigrator] 从 '{v}' 迁移至 '2.0'。"); + v = "2.0"; } + + // ── 2.0 → 2.1 ─────────────────────────────────────────────────────── + if (v == "2.0") + { + // 2.1 在 SaveMeta 中增加 IsSteelSoul;JSON 反序列化已默认 false,无需额外处理。 + // Stats 新增 SpeedrunTime;默认 0f,无需额外处理。 + // Map.Pins 列表:旧版可能为 null + if (data.Map != null) + data.Map.Pins ??= new System.Collections.Generic.List(); + + Debug.Log("[SaveMigrator] 从 '2.0' 迁移至 '2.1'。"); + v = "2.1"; + } + + // ── 未识别的未来版本 ───────────────────────────────────────────────── + if (v != CurrentVersion) + Debug.LogWarning($"[SaveMigrator] 未知版本 '{v}',将直接使用(可能存在兼容性问题)。"); + data.Meta.Version = CurrentVersion; return data; } - private static SaveData MigrateFrom1_0(SaveData d) - { - d.Equipment ??= new EquipmentSaveData(); - d.Player ??= new PlayerSaveData(); - d.World ??= new WorldSaveData(); - d.Equipment.UpgradedCharmIds ??= new List(); - return d; - } + // ── 工具 ───────────────────────────────────────────────────────────────── - private static SaveData MigrateFrom1_1(SaveData d) + /// 简单语义版本比较:返回 是否严格小于 + private static bool IsOlderThan(string v, string target) { - d.Stats ??= new StatsSaveData(); - d.Stats.SkillUseCounts ??= new Dictionary(); - if (d.Player.ShieldHP == 0 && !d.Player.ShieldIsBroken) - d.Player.ShieldHP = -1; - return d; - } - - private static SaveData MigrateFrom2_0(SaveData d) - { - // 2.0 → 2.1:Player.AbilityFlags (uint bitmask) 替换旧版 Dictionary Abilities - // 旧版若 AbilityFlags 为 0 且 ExtensionData 含 "Abilities" 字段,则转换 - if (d.Player.AbilityFlags == 0 && d.ExtensionData.ContainsKey("Abilities")) - { - // 此处仅清除旧字段,具体位掩码由 AbilitySystem 在 OnLoad 时根据 ExtrensionData 转换 - d.ExtensionData.Remove("Abilities"); - } - return d; + return System.Version.TryParse(v, out var sv) && + System.Version.TryParse(target, out var tv) && + sv < tv; } } } diff --git a/Assets/Scripts/Core/Save/SaveableMonoBehaviour.cs b/Assets/Scripts/Core/Save/SaveableMonoBehaviour.cs new file mode 100644 index 0000000..88337ec --- /dev/null +++ b/Assets/Scripts/Core/Save/SaveableMonoBehaviour.cs @@ -0,0 +1,25 @@ +using UnityEngine; +using BaseGames.Core; + +namespace BaseGames.Core.Save +{ + /// + /// 带自动注册/注销的 ISaveable MonoBehaviour 基类。 + /// 继承此类可消除每个存档对象手动调用 ServiceLocator.GetOrDefault<SaveManager>()?.Register/Unregister 的样板代码。 + /// + /// 生命周期: + /// OnEnable → ServiceLocator.GetOrDefault<SaveManager>()?.Register(this) + /// OnDisable → ServiceLocator.GetOrDefault<SaveManager>()?.Unregister(this) + /// + /// 子类只需实现 。 + /// 若子类需要自定义 OnEnable/OnDisable,请先调用 base.OnEnable() / base.OnDisable()。 + /// + public abstract class SaveableMonoBehaviour : MonoBehaviour, ISaveable + { + protected virtual void OnEnable() => ServiceLocator.GetOrDefault()?.Register(this); + protected virtual void OnDisable() => ServiceLocator.GetOrDefault()?.Unregister(this); + + public abstract void OnSave(SaveData saveData); + public abstract void OnLoad(SaveData saveData); + } +} diff --git a/Assets/Scripts/Core/Save/SaveableMonoBehaviour.cs.meta b/Assets/Scripts/Core/Save/SaveableMonoBehaviour.cs.meta new file mode 100644 index 0000000..ec80f3d --- /dev/null +++ b/Assets/Scripts/Core/Save/SaveableMonoBehaviour.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d8716b07a7bca5c43bb372f78884cc13 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Core/SceneLoader.cs b/Assets/Scripts/Core/SceneLoader.cs index b8626d4..95946a2 100644 --- a/Assets/Scripts/Core/SceneLoader.cs +++ b/Assets/Scripts/Core/SceneLoader.cs @@ -9,9 +9,9 @@ using BaseGames.Core.Events; namespace BaseGames.Core { /// - /// Addressables 场景加载器(Phase 0 骨架)。 + /// Addressables 场景加载器。 /// 监听 EVT_SceneLoadRequest,Additive 加载指定场景,完成后发布 EVT_SceneLoaded。 - /// Phase 1 完整实现由 SceneService 包装调用。 + /// 完整实现由 SceneService 包装调用。 /// [DefaultExecutionOrder(-950)] public class SceneLoader : MonoBehaviour @@ -24,17 +24,16 @@ namespace BaseGames.Core private string _currentRoomScene; private AsyncOperationHandle _currentHandle; + private readonly CompositeDisposable _subs = new(); private void OnEnable() { - if (_onSceneLoadRequest != null) - _onSceneLoadRequest.OnEventRaised += HandleRequest; + _onSceneLoadRequest?.Subscribe(HandleRequest).AddTo(_subs); } private void OnDisable() { - if (_onSceneLoadRequest != null) - _onSceneLoadRequest.OnEventRaised -= HandleRequest; + _subs.Clear(); } private void HandleRequest(SceneLoadRequest request) @@ -42,27 +41,27 @@ namespace BaseGames.Core private IEnumerator LoadSceneCoroutine(SceneLoadRequest request) { - // 卸载旧场景 + // 先加载新场景(Additive),成功后再卸载旧场景 + // 顺序保证:若加载失败,旧场景仍保持可用,不会出现无场景的空状态 + var loadOp = Addressables.LoadSceneAsync(request.SceneName, LoadSceneMode.Additive); + yield return loadOp; + + if (loadOp.Status != AsyncOperationStatus.Succeeded) + { + Debug.LogError($"[SceneLoader] 加载场景失败:{request.SceneName}(旧场景保持不变)"); + yield break; + } + + // 新场景加载成功,再卸载旧场景 if (!string.IsNullOrEmpty(_currentRoomScene) && _currentHandle.IsValid()) { var unloadOp = Addressables.UnloadSceneAsync(_currentHandle); yield return unloadOp; } - // 加载新场景(Additive) - var loadOp = Addressables.LoadSceneAsync(request.SceneName, LoadSceneMode.Additive); - yield return loadOp; - - if (loadOp.Status == AsyncOperationStatus.Succeeded) - { - _currentHandle = loadOp; - _currentRoomScene = request.SceneName; - _onSceneLoaded?.Raise(request.SceneName); - } - else - { - Debug.LogError($"[SceneLoader] 加载场景失败:{request.SceneName}"); - } + _currentHandle = loadOp; + _currentRoomScene = request.SceneName; + _onSceneLoaded?.Raise(request.SceneName); } /// 手动卸载当前房间场景(供 SceneService 调用)。 diff --git a/Assets/Scripts/Core/SceneService.cs b/Assets/Scripts/Core/SceneService.cs index c826ea5..eaa9918 100644 --- a/Assets/Scripts/Core/SceneService.cs +++ b/Assets/Scripts/Core/SceneService.cs @@ -19,7 +19,7 @@ namespace BaseGames.Core } /// - /// 场景管理服务(Phase 0 骨架,Phase 1 完整实现)。 + /// 场景管理服务。 /// [DefaultExecutionOrder(-900)] public class SceneService : MonoBehaviour, ISceneService @@ -35,18 +35,14 @@ namespace BaseGames.Core [SerializeField] private float _fadeDuration = 0.3f; private string _currentRoomScene; + private readonly CompositeDisposable _subscriptions = new(); private void OnEnable() { - if (_onSceneLoadRequest != null) - _onSceneLoadRequest.OnEventRaised += HandleSceneLoadRequest; + _onSceneLoadRequest?.Subscribe(HandleSceneLoadRequest).AddTo(_subscriptions); } - private void OnDisable() - { - if (_onSceneLoadRequest != null) - _onSceneLoadRequest.OnEventRaised -= HandleSceneLoadRequest; - } + private void OnDisable() => _subscriptions.Clear(); private void HandleSceneLoadRequest(SceneLoadRequest request) => StartCoroutine(LoadSceneCoroutine(request)); diff --git a/Assets/Scripts/Core/SceneService.cs.meta b/Assets/Scripts/Core/SceneService.cs.meta index 215dde3..cb55385 100644 --- a/Assets/Scripts/Core/SceneService.cs.meta +++ b/Assets/Scripts/Core/SceneService.cs.meta @@ -4,7 +4,7 @@ MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] - executionOrder: 0 + executionOrder: -900 icon: {instanceID: 0} userData: assetBundleName: diff --git a/Assets/Scripts/Core/SettingsManager.cs b/Assets/Scripts/Core/SettingsManager.cs index dcdd051..9d01a86 100644 --- a/Assets/Scripts/Core/SettingsManager.cs +++ b/Assets/Scripts/Core/SettingsManager.cs @@ -7,7 +7,7 @@ namespace BaseGames.Core /// 全局设置管理器。从 GlobalSettingsSO 读取默认值,从文件加载用户覆盖。 /// [DefaultExecutionOrder(-800)] - public class SettingsManager : MonoBehaviour + public class SettingsManager : MonoBehaviour, ISettingsService { private const string SettingsFileName = "settings.json"; @@ -18,6 +18,11 @@ namespace BaseGames.Core public GlobalSettingsData Current => _current; + private void Awake() + { + ServiceLocator.Register(this); + } + /// 由 GameManager.Awake 调用。读取设置文件,应用音量/分辨率。 public void Initialize() { @@ -61,7 +66,7 @@ namespace BaseGames.Core Screen.fullScreenMode = FullScreenMode.FullScreenWindow; } - // ── 音量设置(调用 AudioManager,Phase 1 接通)──────────────────── + // ── 音量设置(调用 AudioManager)──────────────────── public void SetMasterVolume(float v) { _current.MasterVolume = v; Save(); } public void SetBGMVolume(float v) { _current.BGMVolume = v; Save(); } public void SetSFXVolume(float v) { _current.SFXVolume = v; Save(); } @@ -92,5 +97,10 @@ namespace BaseGames.Core _current.Language = localeCode; Save(); } + + private void OnDestroy() + { + ServiceLocator.Unregister(this); + } } } diff --git a/Assets/Scripts/Core/Validation.meta b/Assets/Scripts/Core/Validation.meta new file mode 100644 index 0000000..ff60be7 --- /dev/null +++ b/Assets/Scripts/Core/Validation.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 02c77a4457ac1ce45822fdcd54a16a51 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Core/Validation/IValidatable.cs b/Assets/Scripts/Core/Validation/IValidatable.cs new file mode 100644 index 0000000..c2532eb --- /dev/null +++ b/Assets/Scripts/Core/Validation/IValidatable.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace BaseGames.Core +{ + public enum ValidationSeverity { Error, Warning } + + /// + /// 单条验证结果:严重级别 + 消息文本。 + /// 使用静态工厂方法 Error() / Warning() 创建。 + /// + public readonly struct ValidationResult + { + public ValidationSeverity Severity { get; } + public string Message { get; } + + public ValidationResult(ValidationSeverity severity, string message) + { + Severity = severity; + Message = message; + } + + public static ValidationResult Error(string message) => new(ValidationSeverity.Error, message); + public static ValidationResult Warning(string message) => new(ValidationSeverity.Warning, message); + } + + /// + /// 可验证 ScriptableObject 接口。实现此接口的 SO 会被 SOValidationRunner 自动扫描。 + /// Validate() 返回零条结果 = 数据合法;否则每条按 分类报告。 + /// + public interface IValidatable + { + IEnumerable Validate(); + } +} diff --git a/Assets/Scripts/Core/Validation/IValidatable.cs.meta b/Assets/Scripts/Core/Validation/IValidatable.cs.meta new file mode 100644 index 0000000..1287c3e --- /dev/null +++ b/Assets/Scripts/Core/Validation/IValidatable.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7bb0908d1e6d6684e870b43ab3da2d01 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Cutscene/BaseGames.Cutscene.asmdef b/Assets/Scripts/Cutscene/BaseGames.Cutscene.asmdef index 5c97df2..711a202 100644 --- a/Assets/Scripts/Cutscene/BaseGames.Cutscene.asmdef +++ b/Assets/Scripts/Cutscene/BaseGames.Cutscene.asmdef @@ -9,7 +9,11 @@ "rootNamespace": "BaseGames.Cutscene", "references": [ "BaseGames.Core.Events", - "BaseGames.Dialogue" + "BaseGames.Input", + "BaseGames.Dialogue", + "BaseGames.World", + "BaseGames.Camera", + "Unity.Timeline" ], "autoReferenced": true, "overrideReferences": false, diff --git a/Assets/Scripts/Cutscene/CutsceneManager.cs b/Assets/Scripts/Cutscene/CutsceneManager.cs new file mode 100644 index 0000000..eaae7fb --- /dev/null +++ b/Assets/Scripts/Cutscene/CutsceneManager.cs @@ -0,0 +1,107 @@ +using BaseGames.Core.Events; +using BaseGames.Input; +using UnityEngine; +using UnityEngine.Playables; +using UnityEngine.Timeline; + +namespace BaseGames.Cutscene +{ + /// + /// Unity Timeline 过场动画封装(架构 14_NarrativeModule §11)。 + /// 负责播放/停止 CutsceneSO;切换 Action Map;广播过场开始/结束事件。 + /// + [RequireComponent(typeof(PlayableDirector))] + public class CutsceneManager : MonoBehaviour + { + [SerializeField] private InputReaderSO _inputReader; + + [Header("事件频道")] + [SerializeField] private VoidEventChannelSO _onCutsceneStarted; + [SerializeField] private VoidEventChannelSO _onCutsceneEnded; + [SerializeField] private StringEventChannelSO _onPlayCutsceneById; // 由 PlayCutsceneAction 触发 + + [Header("过场素材库(PlayById 查找用)")] + [SerializeField] private CutsceneSO[] _registeredCutscenes; + + private PlayableDirector _director; + private readonly CompositeDisposable _subs = new(); + + /// 是否正在播放过场。 + public bool IsPlaying => _director != null && _director.state == PlayState.Playing; + + // ── 生命周期 ────────────────────────────────────────────────────── + + private void Awake() + { + _director = GetComponent(); + } + + private void OnEnable() + { + _onPlayCutsceneById?.Subscribe(PlayById).AddTo(_subs); + } + + private void OnDisable() + { + _subs.Clear(); + } + + // ── 公开 API ────────────────────────────────────────────────────── + + /// 通过 cutsceneId 查找并播放(供 PlayCutsceneAction 通过事件触发)。 + public void PlayById(string cutsceneId) + { + if (_registeredCutscenes == null) return; + foreach (var cs in _registeredCutscenes) + if (cs != null && cs.cutsceneId == cutsceneId) { PlayCutscene(cs); return; } + + Debug.LogWarning($"[CutsceneManager] 找不到 cutsceneId='{cutsceneId}'"); + } + + /// 播放指定过场 SO。 + public void PlayCutscene(CutsceneSO cutscene) + { + if (cutscene == null || IsPlaying) return; + _director.playableAsset = cutscene.Timeline; + + // 应用 Track → GameObject 绑定 + if (cutscene.Bindings != null && cutscene.Timeline != null) + { + var outputs = cutscene.Timeline.outputs; + int idx = 0; + foreach (var output in outputs) + { + if (idx >= cutscene.Bindings.Length) break; + var binding = cutscene.Bindings[idx]; + if (!string.IsNullOrEmpty(binding.trackName) && output.streamName == binding.trackName + && binding.target != null) + { + _director.SetGenericBinding(output.sourceObject, binding.target); + } + idx++; + } + } + + _director.stopped += OnCutsceneStopped; + _director.Play(); + + _inputReader?.EnableUIInput(); + _onCutsceneStarted?.Raise(); + } + + /// 立即停止当前过场。 + public void StopCutscene() + { + if (_director != null) _director.Stop(); + } + + // ── 内部 ────────────────────────────────────────────────────────── + + private void OnCutsceneStopped(PlayableDirector director) + { + _director.stopped -= OnCutsceneStopped; + _inputReader?.EnableGameplayInput(); + _onCutsceneEnded?.Raise(); + } + } +} diff --git a/Assets/Scripts/Cutscene/CutsceneManager.cs.meta b/Assets/Scripts/Cutscene/CutsceneManager.cs.meta new file mode 100644 index 0000000..ccdc7e4 --- /dev/null +++ b/Assets/Scripts/Cutscene/CutsceneManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7b85bd04fd48a5e41b7e675944f6c355 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Cutscene/CutsceneSO.cs b/Assets/Scripts/Cutscene/CutsceneSO.cs new file mode 100644 index 0000000..769ee97 --- /dev/null +++ b/Assets/Scripts/Cutscene/CutsceneSO.cs @@ -0,0 +1,52 @@ +using System; +using BaseGames.Camera; +using BaseGames.Dialogue; +using UnityEngine; +using UnityEngine.Timeline; + +namespace BaseGames.Cutscene +{ + /// + /// 过场动画数据资产(架构 14_NarrativeModule §11.5)。 + /// 定义一段完整的过场内容:Timeline 资产、Track 绑定、摄像机混合、可叠加对话层。 + /// 资产路径:Assets/ScriptableObjects/Cutscene/CS_{SceneId}_{ContextId}.asset + /// + [CreateAssetMenu(menuName = "Cutscene/Cutscene")] + public class CutsceneSO : ScriptableObject + { + [Header("Identity")] + public string cutsceneId; // 全局唯一,用于存档去重和 CutsceneManager.PlayById 查找 + public string displayName; + public bool playOnlyOnce; // true → 仅首次播放(后续触发跳过) + public bool isSkippable = true; // 是否允许玩家跳过 + public Sprite thumbnail; // 过场预览图(剧情重放 UI 用) + + [Header("Timeline")] + public TimelineAsset Timeline; // Unity Timeline 资产 + + [Header("Timeline Bindings")] + // Track 与场景 GameObject 的绑定关系(避免 PlayableDirector 硬引用场景对象) + public CutsceneBinding[] Bindings; + + [Header("Camera Blend")] + [Tooltip("进入过场时的摄像机混合配置(可空 = 默认瞬切)")] + public CameraBlendProfileSO BlendIn; + [Tooltip("退出过场时的摄像机混合配置(可空 = 默认瞬切)")] + public CameraBlendProfileSO BlendOut; + + [Header("Optional Dialogue Overlay")] + [Tooltip("过场中可叠加播放的对话序列(由 Timeline Marker 或 SignalEmitterClip 触发)")] + public DialogueSequenceSO[] DialogueLayers; + } + + /// 将一条 Timeline Track(通过名称索引)绑定到运行时场景对象。 + [Serializable] + public struct CutsceneBinding + { + [Tooltip("Timeline Track 的名称(需与 PlayableDirector 内轨道名一致)")] + public string trackName; + + [Tooltip("绑定的目标对象;若为 null,CutsceneManager 从场景中按 tag/name 查找")] + public UnityEngine.Object target; + } +} diff --git a/Assets/Scripts/Cutscene/CutsceneSO.cs.meta b/Assets/Scripts/Cutscene/CutsceneSO.cs.meta new file mode 100644 index 0000000..549e0f6 --- /dev/null +++ b/Assets/Scripts/Cutscene/CutsceneSO.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fc1fcf01f619dac408023483275c18da +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Cutscene/CutsceneTrigger.cs b/Assets/Scripts/Cutscene/CutsceneTrigger.cs new file mode 100644 index 0000000..06a92cf --- /dev/null +++ b/Assets/Scripts/Cutscene/CutsceneTrigger.cs @@ -0,0 +1,81 @@ +using BaseGames.Core.Events; +using BaseGames.World; +using UnityEngine; + +namespace BaseGames.Cutscene +{ + /// + /// 过场动画触发器(架构 14_NarrativeModule §11.5)。 + /// 支持四种触发模式:进入区域、玩家交互、场景加载、事件频道。 + /// 实现 IInteractable 以支持 OnInteract 模式。 + /// + public class CutsceneTrigger : MonoBehaviour, IInteractable + { + public enum TriggerMode + { + OnEnter, // 进入 Trigger 碰撞区域 + OnInteract, // 玩家主动交互(IInteractable) + OnSceneLoad, // 场景加载完毕(Start) + OnEvent, // 订阅事件频道触发 + } + + [SerializeField] private CutsceneSO _cutscene; + [SerializeField] private TriggerMode _mode = TriggerMode.OnEnter; + [SerializeField] private CutsceneManager _cutsceneManager; + [SerializeField] private VoidEventChannelSO _triggerEventChannel; // OnEvent 模式使用 + [SerializeField] private WorldStateRegistry _worldState; // SO 注入,记录/查询播放状态 + + private readonly CompositeDisposable _subs = new(); + + // ── IInteractable(OnInteract 模式)────────────────────────────── + public bool CanInteract => _mode == TriggerMode.OnInteract; + public string InteractPrompt => "查看"; + + public void Interact(Transform player) => TriggerCutscene(); + public void OnPlayerEnterRange(Transform player) { } + public void OnPlayerExitRange() { } + + // ── 生命周期 ────────────────────────────────────────────────────── + + private void OnEnable() + { + if (_mode == TriggerMode.OnEvent) + _triggerEventChannel?.Subscribe(TriggerCutscene).AddTo(_subs); + } + + private void OnDisable() + { + _subs.Clear(); + } + + private void Start() + { + if (_mode == TriggerMode.OnSceneLoad) TriggerCutscene(); + } + + private void OnTriggerEnter2D(Collider2D other) + { + if (_mode != TriggerMode.OnEnter) return; + if (!other.CompareTag("Player")) return; + TriggerCutscene(); + } + + // ── 触发逻辑 ────────────────────────────────────────────────────── + + private void TriggerCutscene() + { + if (_cutscene == null || _cutsceneManager == null) return; + + // 已播放且仅播一次 → 跳过 + if (_cutscene.playOnlyOnce && _worldState != null + && _worldState.HasFlag($"cutscene_played_{_cutscene.cutsceneId}")) + return; + + _cutsceneManager.PlayCutscene(_cutscene); + _worldState?.SetFlag($"cutscene_played_{_cutscene.cutsceneId}"); + + // 区域触发后禁用自身,防止重入 + if (_mode == TriggerMode.OnEnter) enabled = false; + } + } +} diff --git a/Assets/Scripts/Cutscene/CutsceneTrigger.cs.meta b/Assets/Scripts/Cutscene/CutsceneTrigger.cs.meta new file mode 100644 index 0000000..141ca60 --- /dev/null +++ b/Assets/Scripts/Cutscene/CutsceneTrigger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7d7aa12751a7a604db8018b083d8db3b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Cutscene/SignalEmitterClip.cs b/Assets/Scripts/Cutscene/SignalEmitterClip.cs new file mode 100644 index 0000000..f6d85e6 --- /dev/null +++ b/Assets/Scripts/Cutscene/SignalEmitterClip.cs @@ -0,0 +1,63 @@ +using BaseGames.Core.Events; +using UnityEngine; +using UnityEngine.Playables; +using UnityEngine.Timeline; + +namespace BaseGames.Cutscene +{ + // ===================================================================== + // SignalEmitterClip —— Timeline 零耦合事件桥接(架构 14 §11.6) + // ===================================================================== + + /// + /// 在 Timeline 轨道上放置此 Clip,Clip 播放时向目标 Void 事件频道 Raise 一次事件。 + /// 用途:Timeline 动画与游戏逻辑保持零耦合(不直接引用场景对象)。 + /// 使用场景示例:过场第 3 秒 → 触发 EVT_BossPhase2 → BossOrchestrator 切换阶段。 + /// + [CreateAssetMenu(menuName = "BaseGames/Cutscene/SignalEmitterClip")] + public class SignalEmitterClip : PlayableAsset, ITimelineClipAsset + { + [Tooltip("Clip 播放时发射的目标 Void 事件频道 SO")] + [SerializeField] private VoidEventChannelSO _targetChannel; + + /// Timeline 系统查询 Clip 能力(无额外能力)。 + public ClipCaps clipCaps => ClipCaps.None; + + public override Playable CreatePlayable(PlayableGraph graph, GameObject owner) + { + var playable = ScriptPlayable.Create(graph); + playable.GetBehaviour().Clip = this; + return playable; + } + + /// 供 SignalEmitterBehaviour 内部调用,发射事件。 + internal void Fire() => _targetChannel?.Raise(); + } + + // ───────────────────────────────────────────────────────────────────── + + /// + /// SignalEmitterClip 对应的 PlayableBehaviour,处理 ProcessFrame 时机。 + /// + public class SignalEmitterBehaviour : PlayableBehaviour + { + /// 由 CreatePlayable 注入,内部访问 Fire() 发射事件。 + public SignalEmitterClip Clip; + + private bool _fired; + + public override void OnBehaviourPlay(Playable playable, FrameData info) + { + _fired = false; // 支持 Timeline 循环/重播时重置 + } + + public override void ProcessFrame(Playable playable, FrameData info, object playerData) + { + if (!_fired && Clip != null) + { + Clip.Fire(); + _fired = true; + } + } + } +} diff --git a/Assets/Scripts/Cutscene/SignalEmitterClip.cs.meta b/Assets/Scripts/Cutscene/SignalEmitterClip.cs.meta new file mode 100644 index 0000000..168cb5b --- /dev/null +++ b/Assets/Scripts/Cutscene/SignalEmitterClip.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 70594aff0301cac46bb95c619167d06b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Dialogue/BaseGames.Dialogue.asmdef b/Assets/Scripts/Dialogue/BaseGames.Dialogue.asmdef index 8c804ee..72ba80c 100644 --- a/Assets/Scripts/Dialogue/BaseGames.Dialogue.asmdef +++ b/Assets/Scripts/Dialogue/BaseGames.Dialogue.asmdef @@ -8,7 +8,12 @@ "versionDefines": [], "rootNamespace": "BaseGames.Dialogue", "references": [ - "BaseGames.Core.Events" + "BaseGames.Core", + "BaseGames.Core.Events", + "BaseGames.Input", + "BaseGames.World", + "Unity.TextMeshPro", + "Unity.InputSystem" ], "autoReferenced": true, "overrideReferences": false, diff --git a/Assets/Scripts/Dialogue/DialogueDataSO.cs b/Assets/Scripts/Dialogue/DialogueDataSO.cs index 5c77e18..3b908ab 100644 --- a/Assets/Scripts/Dialogue/DialogueDataSO.cs +++ b/Assets/Scripts/Dialogue/DialogueDataSO.cs @@ -3,9 +3,7 @@ using UnityEngine; namespace BaseGames.Dialogue { /// - /// 对话数据 ScriptableObject(存根)。 - /// Phase 3 Dialogue 模块实现时填充完整字段(对话行、Speaker、选项分支等)。 - /// 此处仅声明类型,供 DialogueEventChannelSO 引用。 + /// 对话数据 ScriptableObject。 /// [CreateAssetMenu(menuName = "Dialogue/DialogueData")] public class DialogueDataSO : ScriptableObject @@ -17,7 +15,7 @@ namespace BaseGames.Dialogue public string speakerName; [TextArea(3, 8)] - [Tooltip("Phase 3 前的占位文本;正式内容在 DialogueLineSO[] 中定义。")] + [Tooltip("对话内容占位文本,正式内容在 DialogueLineSO[] 中定义。")] public string placeholderText; } } diff --git a/Assets/Scripts/Dialogue/DialogueManager.cs b/Assets/Scripts/Dialogue/DialogueManager.cs new file mode 100644 index 0000000..37f450f --- /dev/null +++ b/Assets/Scripts/Dialogue/DialogueManager.cs @@ -0,0 +1,141 @@ +using System.Collections; +using BaseGames.Core; +using BaseGames.Core.Events; +using BaseGames.Input; +using BaseGames.World; +using UnityEngine; + +namespace BaseGames.Dialogue +{ + /// + /// 对话管理器(架构 14_NarrativeModule §4)。 + /// 驱动 DialogueUI 打字机效果;管理 Action Map 切换;向 QuestManager 广播对话完成事件。 + /// 在 Awake 中注册到 ServiceLocator。 + /// + public class DialogueManager : MonoBehaviour, IDialogueService + { + [SerializeField] private DialogueUI _dialogueBox; + [SerializeField] private InputReaderSO _inputReader; + [SerializeField] private WorldStateRegistry _worldState; + + [Header("事件频道")] + [SerializeField] private VoidEventChannelSO _onDialogueStarted; + [SerializeField] private VoidEventChannelSO _onDialogueEnded; + [SerializeField] private StringEventChannelSO _onNpcDialogueCompleted; // → EVT_NpcDialogueCompleted (npcId) + + private bool _skipRequested; + + /// 当前是否有对话正在播放。 + public bool IsDialogueActive { get; private set; } + + // ── 生命周期 ────────────────────────────────────────────────────── + + private void Awake() + { + if (ServiceLocator.GetOrDefault() != null) { Destroy(gameObject); return; } + ServiceLocator.Register(this); + } + + private void OnDestroy() + { + ServiceLocator.Unregister(this); + } + + private void OnEnable() + { + if (_inputReader != null) _inputReader.SubmitEvent += OnSubmit; + } + + private void OnDisable() + { + if (_inputReader != null) _inputReader.SubmitEvent -= OnSubmit; + } + + // ── 公开 API ────────────────────────────────────────────────────── + + /// + /// 启动对话序列。若已有对话在播放则忽略新请求。 + /// 由 InteractableNPC.Interact() 调用。 + /// + /// 要播放的对话序列 SO。 + /// NPC 标识符,对话结束时随 EVT_NpcDialogueCompleted 广播。 + public void StartDialogue(DialogueSequenceSO sequence, string npcId = "") + { + if (IsDialogueActive || sequence == null) return; + IsDialogueActive = true; + _skipRequested = false; + + // 切换到 UI Action Map(禁用玩家移动输入) + _inputReader.EnableUIInput(); + + _onDialogueStarted?.Raise(); + StartCoroutine(PlaySequence(sequence, npcId)); + } + + // ── 输入回调 ────────────────────────────────────────────────────── + + private void OnSubmit() => _skipRequested = true; + + // ── 内部协程 ────────────────────────────────────────────────────── + + private IEnumerator PlaySequence(DialogueSequenceSO sequence, string npcId) + { + // 选择条件变体(查询 ConditionalVariant.conditionFlag — 未实现 WorldStateRegistry 查询时直接使用默认序列) + var resolved = ResolveVariant(sequence); + + foreach (var line in resolved.lines) + { + _skipRequested = false; + _dialogueBox.ShowLine(line); + + // 等待打字完成,期间允许跳过 + yield return new WaitUntil(() => !_dialogueBox.IsTyping || _skipRequested); + if (_dialogueBox.IsTyping) _dialogueBox.SkipTyping(); + + // 等待玩家按 Submit 推进下一行 + _skipRequested = false; + yield return new WaitUntil(() => _skipRequested); + } + + EndDialogue(npcId); + } + + private void EndDialogue(string npcId) + { + _dialogueBox.Hide(); + IsDialogueActive = false; + + // 恢复 Gameplay Action Map + _inputReader.EnableGameplayInput(); + + _onDialogueEnded?.Raise(); + + if (!string.IsNullOrEmpty(npcId)) + _onNpcDialogueCompleted?.Raise(npcId); + } + + /// + /// 根据 ConditionalVariant 选择正确的序列版本。 + /// 按顺序检查 variants:第一个满足 WorldStateRegistry 标志的变体胜出; + /// 未注入 WorldStateRegistry 或无满足条件的变体时,返回原序列(默认)。 + /// + private DialogueSequenceSO ResolveVariant(DialogueSequenceSO sequence) + { + if (sequence.variants == null || sequence.variants.Length == 0) + return sequence; + + if (_worldState != null) + { + foreach (var variant in sequence.variants) + { + if (!string.IsNullOrEmpty(variant.conditionFlag) + && variant.sequence != null + && _worldState.HasFlag(variant.conditionFlag)) + return variant.sequence; + } + } + + return sequence; + } + } +} diff --git a/Assets/Scripts/Dialogue/DialogueManager.cs.meta b/Assets/Scripts/Dialogue/DialogueManager.cs.meta new file mode 100644 index 0000000..35f8607 --- /dev/null +++ b/Assets/Scripts/Dialogue/DialogueManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5eade47a9934aaf428bf94aa09b30e23 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Dialogue/DialogueSequenceSO.cs b/Assets/Scripts/Dialogue/DialogueSequenceSO.cs new file mode 100644 index 0000000..d6a307e --- /dev/null +++ b/Assets/Scripts/Dialogue/DialogueSequenceSO.cs @@ -0,0 +1,41 @@ +using UnityEngine; + +namespace BaseGames.Dialogue +{ + /// + /// 对话行结构(架构 14_NarrativeModule §3)。 + /// 每行包含说话人、文本(本地化 Key)和可选的语音片段。 + /// + [System.Serializable] + public struct DialogueLine + { + public string speakerNameKey; // 本地化 key(如 "NPC_Elder_Name") + [TextArea(2, 5)] + public string textKey; // 本地化文本 key(如 "DLG_Elder_001") + public Sprite portraitSprite; // 可选说话人头像 + public AudioClip voiceClip; // 可选语音 + [Min(0.01f)] + public float typewriterDelay; // 每字符延迟(秒,0 = 使用默认 0.03f) + } + + /// + /// 对话序列 SO(架构 14_NarrativeModule §3)。 + /// 一个 NPC 对话场合对应一个序列,由若干 DialogueLine 组成。 + /// 资产路径: Assets/ScriptableObjects/Dialogue/DLG_{NpcId}_{Context}.asset + /// + [CreateAssetMenu(menuName = "Dialogue/DialogueSequence")] + public class DialogueSequenceSO : ScriptableObject + { + public string sequenceId; // 全局唯一,如 "DLG_Elder_Quest_Available" + public DialogueLine[] lines; + + /// 条件变体:满足特定世界标志时替换整个序列。 + [System.Serializable] + public struct ConditionalVariant + { + public string conditionFlag; // WorldState flag key + public DialogueSequenceSO sequence; + } + public ConditionalVariant[] variants; + } +} diff --git a/Assets/Scripts/Dialogue/DialogueSequenceSO.cs.meta b/Assets/Scripts/Dialogue/DialogueSequenceSO.cs.meta new file mode 100644 index 0000000..2ccbc97 --- /dev/null +++ b/Assets/Scripts/Dialogue/DialogueSequenceSO.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 037a9d55368dde649ac6c1c6a1e80dad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Dialogue/DialogueUI.cs b/Assets/Scripts/Dialogue/DialogueUI.cs new file mode 100644 index 0000000..5c2667e --- /dev/null +++ b/Assets/Scripts/Dialogue/DialogueUI.cs @@ -0,0 +1,102 @@ +using System.Collections; +using System.Text; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace BaseGames.Dialogue +{ + /// + /// 对话框 UI 组件(架构 14_NarrativeModule §5)。 + /// 挂载在 Canvas_Overlay 下的 DialogueBox 子对象。 + /// 负责打字机效果、头像显示、继续提示。 + /// + public class DialogueUI : MonoBehaviour + { + [SerializeField] private GameObject _rootPanel; + [SerializeField] private TMP_Text _speakerNameText; + [SerializeField] private TMP_Text _dialogueText; + [SerializeField] private GameObject _speakerNamePanel; // 无名称时隐藏整个名称框 + [SerializeField] private GameObject _continuePrompt; // "▼" 图标,打字完成后显示 + [SerializeField] private Image _speakerPortrait; // 角色头像框 + + private Coroutine _typingCoroutine; + private DialogueLine _currentLine; + private const float DefaultTypewriterDelay = 0.03f; + + /// 当前是否仍在执行打字机效果。 + public bool IsTyping { get; private set; } + + // ── 公开 API ────────────────────────────────────────────────────── + + /// 显示一行对话并开始打字机效果。 + public void ShowLine(DialogueLine line) + { + _currentLine = line; + _rootPanel.SetActive(true); + _continuePrompt.SetActive(false); + + // 说话人名称 + bool hasSpeaker = !string.IsNullOrEmpty(line.speakerNameKey); + if (_speakerNamePanel != null) _speakerNamePanel.SetActive(hasSpeaker); + if (hasSpeaker && _speakerNameText != null) + _speakerNameText.text = line.speakerNameKey; + + // 头像 + if (_speakerPortrait != null) + { + _speakerPortrait.gameObject.SetActive(line.portraitSprite != null); + if (line.portraitSprite != null) _speakerPortrait.sprite = line.portraitSprite; + } + + // 开始打字机协程 + if (_typingCoroutine != null) StopCoroutine(_typingCoroutine); + _typingCoroutine = StartCoroutine(TypeLine(line)); + } + + /// 立即显示全部文字(跳过打字机效果)。 + public void SkipTyping() + { + if (!IsTyping) return; + if (_typingCoroutine != null) + { + StopCoroutine(_typingCoroutine); + _typingCoroutine = null; + } + if (_dialogueText != null) + _dialogueText.text = _currentLine.textKey ?? ""; + IsTyping = false; + if (_continuePrompt != null) _continuePrompt.SetActive(true); + } + + /// 隐藏对话框面板。 + public void Hide() + { + if (_rootPanel != null) _rootPanel.SetActive(false); + } + + // ── 内部协程 ────────────────────────────────────────────────────── + + private IEnumerator TypeLine(DialogueLine line) + { + IsTyping = true; + float delay = line.typewriterDelay > 0f ? line.typewriterDelay : DefaultTypewriterDelay; + string text = line.textKey ?? ""; + + // 性能:StringBuilder 避免每帧字符串分配(O(n²) → O(n)) + var sb = new StringBuilder(text.Length); + if (_dialogueText != null) _dialogueText.text = ""; + + foreach (char c in text) + { + sb.Append(c); + if (_dialogueText != null) _dialogueText.SetText(sb); // TMP SetText(StringBuilder) 零分配 + yield return new WaitForSecondsRealtime(delay); + } + + IsTyping = false; + if (_continuePrompt != null) _continuePrompt.SetActive(true); + _typingCoroutine = null; + } + } +} diff --git a/Assets/Scripts/Dialogue/DialogueUI.cs.meta b/Assets/Scripts/Dialogue/DialogueUI.cs.meta new file mode 100644 index 0000000..45ae12e --- /dev/null +++ b/Assets/Scripts/Dialogue/DialogueUI.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 699768da82efa244f815d5dbce5b23dc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Dialogue/IDialogueService.cs b/Assets/Scripts/Dialogue/IDialogueService.cs new file mode 100644 index 0000000..122cc64 --- /dev/null +++ b/Assets/Scripts/Dialogue/IDialogueService.cs @@ -0,0 +1,18 @@ +namespace BaseGames.Dialogue +{ + /// + /// 对话服务接口。通过 ServiceLocator 注册,供 NPC 和测试使用。 + /// + public interface IDialogueService + { + /// 当前是否有对话正在播放。 + bool IsDialogueActive { get; } + + /// + /// 启动对话序列。若已有对话在播放则忽略新请求。 + /// + /// 要播放的对话序列 SO。 + /// NPC 标识符,对话结束时随 EVT_NpcDialogueCompleted 广播。 + void StartDialogue(DialogueSequenceSO sequence, string npcId = ""); + } +} diff --git a/Assets/Scripts/Dialogue/IDialogueService.cs.meta b/Assets/Scripts/Dialogue/IDialogueService.cs.meta new file mode 100644 index 0000000..9de23f0 --- /dev/null +++ b/Assets/Scripts/Dialogue/IDialogueService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cd651be055b226649b2594e0774e764d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Dialogue/InteractableNPC.cs b/Assets/Scripts/Dialogue/InteractableNPC.cs new file mode 100644 index 0000000..af24d55 --- /dev/null +++ b/Assets/Scripts/Dialogue/InteractableNPC.cs @@ -0,0 +1,55 @@ +using UnityEngine; +using BaseGames.World; + +namespace BaseGames.Dialogue +{ + /// + /// 可交互 NPC 基类(架构 14_NarrativeModule §5)。 + /// 实现 IInteractable,触发对话序列;子类(如 QuestGiver)可覆盖对话选择逻辑。 + /// 程序集: BaseGames.Dialogue + /// + public class InteractableNPC : MonoBehaviour, IInteractable + { + [SerializeField] protected string _npcId; + [SerializeField] protected DialogueSequenceSO _defaultDialogue; + [SerializeField] protected float _interactRadius = 1.5f; + + // ── IInteractable ────────────────────────────────────────────────── + public virtual bool CanInteract => true; + public virtual string InteractPrompt => "对话"; + + public void Interact(Transform player) + { + if (!CanInteract) return; + Interact_Internal(player); + var dialogue = GetCurrentDialogue(); + if (dialogue != null) + PlayDialogue(dialogue, player); + } + + public virtual void OnPlayerEnterRange(Transform player) { } + public virtual void OnPlayerExitRange() { } + + // ── 子类覆盖点 ────────────────────────────────────────────────────── + + /// 交互前置逻辑(如任务接收/完成判断)。子类覆盖此方法。 + protected virtual void Interact_Internal(Transform player) { } + + /// 返回当前应播放的对话序列。子类根据任务状态返回不同版本。 + protected virtual DialogueSequenceSO GetCurrentDialogue() => _defaultDialogue; + + // ── 对话播放 ─────────────────────────────────────────────────────── + + protected virtual void PlayDialogue(DialogueSequenceSO sequence, Transform player) + { + if (sequence == null) return; + var manager = BaseGames.Core.ServiceLocator.GetOrDefault(); + if (manager == null) + { + Debug.LogWarning($"[InteractableNPC:{_npcId}] DialogueManager 未注册到 ServiceLocator,无法播放对话。"); + return; + } + manager.StartDialogue(sequence, _npcId); + } + } +} diff --git a/Assets/Scripts/Dialogue/InteractableNPC.cs.meta b/Assets/Scripts/Dialogue/InteractableNPC.cs.meta new file mode 100644 index 0000000..73e6e61 --- /dev/null +++ b/Assets/Scripts/Dialogue/InteractableNPC.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2fd9677f057c21d4cbec1f99f76a13d1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Dialogue/InteractionPromptController.cs b/Assets/Scripts/Dialogue/InteractionPromptController.cs new file mode 100644 index 0000000..127fb3d --- /dev/null +++ b/Assets/Scripts/Dialogue/InteractionPromptController.cs @@ -0,0 +1,45 @@ +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.UI; + +namespace BaseGames.Dialogue +{ + /// + /// 交互提示 UI 控制器(架构 14_NarrativeModule §2)。 + /// 挂载在每个 IInteractable GameObject 的子节点(Prefab 实例),默认隐藏。 + /// 根据当前活跃输入设备自动切换图标(键盘/手柄)。 + /// + public class InteractionPromptController : MonoBehaviour + { + [SerializeField] private GameObject _promptRoot; + [SerializeField] private Image _icon; + [SerializeField] private Sprite _keyboardIcon; + [SerializeField] private Sprite _gamepadIcon; + + private void Awake() + { + if (_promptRoot != null) _promptRoot.SetActive(false); + } + + /// 显示交互提示,根据输入设备选择图标。 + public void Show() + { + if (_promptRoot == null) return; + _promptRoot.SetActive(true); + UpdateIcon(); + } + + /// 隐藏交互提示。 + public void Hide() + { + if (_promptRoot != null) _promptRoot.SetActive(false); + } + + private void UpdateIcon() + { + if (_icon == null) return; + bool isGamepad = Gamepad.current != null && Gamepad.current.enabled; + _icon.sprite = isGamepad ? _gamepadIcon : _keyboardIcon; + } + } +} diff --git a/Assets/Scripts/Dialogue/InteractionPromptController.cs.meta b/Assets/Scripts/Dialogue/InteractionPromptController.cs.meta new file mode 100644 index 0000000..eb4d35f --- /dev/null +++ b/Assets/Scripts/Dialogue/InteractionPromptController.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 55e266aa215ebcf47b8c84b710bf03f8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Dialogue/NarrativeNPC.cs b/Assets/Scripts/Dialogue/NarrativeNPC.cs new file mode 100644 index 0000000..3238c19 --- /dev/null +++ b/Assets/Scripts/Dialogue/NarrativeNPC.cs @@ -0,0 +1,67 @@ +using System; +using BaseGames.World; +using UnityEngine; + +namespace BaseGames.Dialogue +{ + /// + /// 条件对话 NPC(架构 14_NarrativeModule §7)。 + /// 扩展 InteractableNPC,根据 WorldStateRegistry 标志动态选择对话版本。 + /// 版本列表从高到低优先级排列;第一个满足条件的版本生效。 + /// + public class NarrativeNPC : InteractableNPC + { + [Header("台词版本集(从高到低优先级排列)")] + [SerializeField] private DialogueVersion[] _dialogueVersions; + [SerializeField] private DialogueSequenceSO _fallbackDialogue; // 无条件满足时的兜底台词 + [SerializeField] private WorldStateRegistry _worldState; // SO 注入 + + protected override DialogueSequenceSO GetCurrentDialogue() + { + if (_dialogueVersions == null) return _fallbackDialogue; + + foreach (var version in _dialogueVersions) + { + if (version != null && version.CheckConditions(_worldState)) + return version.dialogue; + } + + return _fallbackDialogue; + } + } + + // ───────────────────────────────────────────────────────────────────────── + + /// + /// 一个对话版本及其激活条件(架构 14_NarrativeModule §7)。 + /// + [Serializable] + public class DialogueVersion + { + [Tooltip("编辑器显示名,如'森林 Boss 击败后'")] + public string versionLabel; + public DialogueSequenceSO dialogue; + + [Tooltip("全部满足才激活此版本(AND 关系)")] + public string[] requiredFlags; + + [Tooltip("有任意一个 = 此版本不激活(NOT 关系)")] + public string[] blockedByFlags; + + /// 检查此版本的激活条件(AND requiredFlags / NOT blockedByFlags)。 + public bool CheckConditions(WorldStateRegistry registry) + { + if (registry == null) return false; + + if (requiredFlags != null) + foreach (var f in requiredFlags) + if (!registry.HasFlag(f)) return false; + + if (blockedByFlags != null) + foreach (var f in blockedByFlags) + if (registry.HasFlag(f)) return false; + + return true; + } + } +} diff --git a/Assets/Scripts/Dialogue/NarrativeNPC.cs.meta b/Assets/Scripts/Dialogue/NarrativeNPC.cs.meta new file mode 100644 index 0000000..df65123 --- /dev/null +++ b/Assets/Scripts/Dialogue/NarrativeNPC.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 59f5feba1a7232f4a8cd823402a5e512 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/.gitkeep b/Assets/Scripts/Editor/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Assets/Scripts/Editor/Achievements.meta b/Assets/Scripts/Editor/Achievements.meta new file mode 100644 index 0000000..7f96797 --- /dev/null +++ b/Assets/Scripts/Editor/Achievements.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 07ed02361aa3739468cbd36457aecda6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/Achievements/AchievementSOEditor.cs b/Assets/Scripts/Editor/Achievements/AchievementSOEditor.cs new file mode 100644 index 0000000..fb762e0 --- /dev/null +++ b/Assets/Scripts/Editor/Achievements/AchievementSOEditor.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using BaseGames.Progression; + +namespace BaseGames.Editor.Achievements +{ + /// + /// AchievementSO 自定义 Inspector(架构 16_SupportingModules §2.4)。 + /// 在 conditions 数组中内联展示各 AchievementCondition SO 的关键字段, + /// 并在头部显示条件类型的中文名,提供 Ping 和删除按钮。 + /// + [CustomEditor(typeof(AchievementSO))] + public class AchievementSOEditor : UnityEditor.Editor + { + private static readonly Dictionary _conditionLabels = new() + { + { "DefeatedBossCondition", "击败 Boss" }, + { "DefeatedAllBossesCondition", "击败全部 Boss" }, + { "EnteredRegionCondition", "到达区域" }, + { "MapExplorationCondition", "地图探索 %" }, + { "CollectedItemCondition", "收集物品" }, + { "CollectedAllCharmsCondition", "集满全部 Charm" }, + { "UnlockedAllAbilitiesCondition", "解锁全部能力" }, + { "NoHealRunCondition", "无治疗通关" }, + { "TimedBossKillCondition", "限时击败 Boss" }, + { "ParryCountCondition", "弹反 N 次" }, + { "NailClashCountCondition", "拼刀 N 次" }, + { "EventTriggeredCondition", "监听事件" }, + }; + + private SerializedProperty _conditionsProp; + + private void OnEnable() + { + _conditionsProp = serializedObject.FindProperty("conditions"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + // 绘制除 conditions 之外的所有默认字段 + DrawPropertiesExcluding(serializedObject, "conditions"); + + EditorGUILayout.Space(8); + EditorGUILayout.LabelField("解锁条件(AND 全部满足)", EditorStyles.boldLabel); + + for (int i = 0; i < _conditionsProp.arraySize; i++) + { + var elemProp = _conditionsProp.GetArrayElementAtIndex(i); + var condSO = elemProp.objectReferenceValue as AchievementCondition; + + string typeName = condSO?.GetType().Name ?? ""; + string label = condSO != null && _conditionLabels.TryGetValue(typeName, out var n) + ? $"{n} [{condSO.name}]" + : (condSO != null ? $"{typeName} [{condSO.name}]" : "(未指定条件 SO)"); + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + // 标题行 + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(label, EditorStyles.boldLabel); + + if (condSO != null && GUILayout.Button("↗", GUILayout.Width(24))) + EditorGUIUtility.PingObject(condSO); + + var prevColor = GUI.color; + GUI.color = Color.red * 0.9f; + if (GUILayout.Button("✕", GUILayout.Width(24))) + { + GUI.color = prevColor; + // 先将引用置空再删除,避免删除保留引用的 Unity 行为 + _conditionsProp.GetArrayElementAtIndex(i).objectReferenceValue = null; + _conditionsProp.DeleteArrayElementAtIndex(i); + serializedObject.ApplyModifiedProperties(); + break; + } + GUI.color = prevColor; + EditorGUILayout.EndHorizontal(); + + // 内联展开 SO 字段(可编辑) + if (condSO != null) + { + var innerSO = new SerializedObject(condSO); + innerSO.Update(); + var prop = innerSO.GetIterator(); + prop.NextVisible(true); // 跳过 m_Script + while (prop.NextVisible(false)) + EditorGUILayout.PropertyField(prop, true); + if (innerSO.ApplyModifiedProperties()) + EditorUtility.SetDirty(condSO); + } + else + { + EditorGUILayout.PropertyField(elemProp, GUIContent.none); + } + + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(2); + } + + // 添加按钮 + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("+ 添加条件 SO 引用")) + _conditionsProp.arraySize++; + EditorGUILayout.EndHorizontal(); + + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Assets/Scripts/Editor/Achievements/AchievementSOEditor.cs.meta b/Assets/Scripts/Editor/Achievements/AchievementSOEditor.cs.meta new file mode 100644 index 0000000..09a19ef --- /dev/null +++ b/Assets/Scripts/Editor/Achievements/AchievementSOEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 95b58f8c5a3285c4abbf929f7bf36946 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/AddressKeyValidator.cs b/Assets/Scripts/Editor/AddressKeyValidator.cs index 5f99b8d..f5e989f 100644 --- a/Assets/Scripts/Editor/AddressKeyValidator.cs +++ b/Assets/Scripts/Editor/AddressKeyValidator.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; using UnityEngine; #if UNITY_EDITOR using UnityEditor.AddressableAssets; @@ -16,6 +18,30 @@ namespace BaseGames.Editor /// 是否与 Addressable 分组中实际存在的地址同步(架构 13_AssetPoolModule §10)。 /// /// 菜单:BaseGames → Tools → Validate Address Keys + /// Build 回调顺序 = 0(在 SOValidationRunner callbackOrder = 1 之前执行) + /// + public class AddressKeyValidatorBuildHook : IPreprocessBuildWithReport + { + public int callbackOrder => 0; + + public void OnPreprocessBuild(BuildReport report) + { + var results = AddressKeyValidator.RunValidation(); + int missing = results.Count(r => !r.ExistsInAddressables); + if (missing > 0) + { + var orphans = results + .Where(r => !r.ExistsInAddressables) + .Select(r => $"AddressKeys.{r.FieldName} = \"{r.Value}\""); + throw new BuildFailedException( + $"[AddressKeyValidator] {missing} 个孤儿 AddressKey,构建中止:\n" + + string.Join("\n", orphans)); + } + } + } + + /// + /// Editor 静态工具类:验证逻辑和 MenuItem 入口。 /// public static class AddressKeyValidator { @@ -91,7 +117,8 @@ namespace BaseGames.Editor if (missing == 0) Debug.Log($"[AddressKeyValidator] ✓ 所有 {results.Count} 个 AddressKeys 常量均在 Addressable 分组中存在。"); else - Debug.LogWarning($"[AddressKeyValidator] 共 {results.Count} 个常量,发现 {missing} 个孤儿 Key,请检查 Addressable 分组配置。"); + Debug.LogWarning($"[AddressKeyValidator] 共 {results.Count} 个常量,发现 {missing} 个孤儿 Key。" + + $"尚未创建的 Prefab/Scene 资产请在创建后添加至 Addressables 分组。"); } // ── 结果结构 ────────────────────────────────────────────────────────── diff --git a/Assets/Scripts/Editor/AddressReferenceGraphWindow.cs b/Assets/Scripts/Editor/AddressReferenceGraphWindow.cs new file mode 100644 index 0000000..d1203e7 --- /dev/null +++ b/Assets/Scripts/Editor/AddressReferenceGraphWindow.cs @@ -0,0 +1,288 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using UnityEditor; +using UnityEngine; + +namespace BaseGames.Editor +{ + /// + /// Addressable Key 引用关系图窗口(架构 13_AssetPoolModule §11)。 + /// 菜单:BaseGames/Tools/Asset Reference Graph + /// + /// 功能: + /// - 扫描所有 .cs 文件中对 AddressKeys.X 的引用 + /// - 列出每个 Key:声明位置、引用文件列表、是否存在于 Addressables + /// - 孤儿 Key(有声明无引用)标红显示 + /// - 无效 Key(有引用但不存在于 Addressables)标橙显示 + /// - 一键导出 CSV + /// + public class AddressReferenceGraphWindow : EditorWindow + { + // ── State ────────────────────────────────────────────────────────── + private List _entries; + private Vector2 _scrollPos; + private string _searchFilter = ""; + private bool _showOrphansOnly; + private bool _showMissingOnly; + + // ── Colors ───────────────────────────────────────────────────────── + private static readonly Color ColOrphan = new Color(0.90f, 0.15f, 0.15f, 0.80f); // 孤儿 Key(无引用) + private static readonly Color ColMissing = new Color(0.95f, 0.55f, 0.10f, 0.80f); // 无效 Key(不在 Addressables) + private static readonly Color ColOk = new Color(0.20f, 0.75f, 0.30f, 0.80f); // 正常 + + [MenuItem("BaseGames/Tools/Asset Reference Graph")] + public static void OpenWindow() + { + var win = GetWindow("Asset Reference Graph"); + win.minSize = new Vector2(900, 500); + win.Show(); + } + + // ── GUI ──────────────────────────────────────────────────────────── + + private void OnGUI() + { + DrawToolbar(); + + if (_entries == null) + { + EditorGUILayout.HelpBox("点击上方「扫描」按钮分析 AddressKeys 引用关系。", MessageType.Info); + return; + } + + DrawFilterRow(); + DrawResults(); + } + + // ── Toolbar ─────────────────────────────────────────────────────── + + private void DrawToolbar() + { + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + + if (GUILayout.Button("扫描", EditorStyles.toolbarButton, GUILayout.Width(60))) + RunScan(); + + if (_entries != null && GUILayout.Button("导出 CSV", EditorStyles.toolbarButton, GUILayout.Width(70))) + ExportCsv(); + + GUILayout.FlexibleSpace(); + + if (_entries != null) + { + int orphans = _entries.Count(e => e.ReferenceCount == 0); + int missing = _entries.Count(e => !e.ExistsInAddressables); + EditorGUILayout.LabelField( + $"共 {_entries.Count} 个 Key | 孤儿:{orphans} | 未在 Addressables:{missing}", + EditorStyles.toolbarButton); + } + + EditorGUILayout.EndHorizontal(); + } + + // ── 过滤行 ──────────────────────────────────────────────────────── + + private void DrawFilterRow() + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("搜索:", GUILayout.Width(40)); + _searchFilter = EditorGUILayout.TextField(_searchFilter, GUILayout.ExpandWidth(true)); + _showOrphansOnly = EditorGUILayout.ToggleLeft("仅显示孤儿", _showOrphansOnly, GUILayout.Width(90)); + _showMissingOnly = EditorGUILayout.ToggleLeft("仅显示缺失", _showMissingOnly, GUILayout.Width(90)); + EditorGUILayout.EndHorizontal(); + } + + // ── 结果列表 ────────────────────────────────────────────────────── + + private void DrawResults() + { + var filtered = _entries.AsEnumerable(); + + if (_showOrphansOnly) + filtered = filtered.Where(e => e.ReferenceCount == 0); + if (_showMissingOnly) + filtered = filtered.Where(e => !e.ExistsInAddressables); + if (!string.IsNullOrEmpty(_searchFilter)) + filtered = filtered.Where(e => + e.FieldName.IndexOf(_searchFilter, System.StringComparison.OrdinalIgnoreCase) >= 0); + + var list = filtered.ToList(); + + // 表头 + EditorGUILayout.BeginHorizontal(EditorStyles.helpBox); + EditorGUILayout.LabelField("状态", GUILayout.Width(50)); + EditorGUILayout.LabelField("Key 名称", GUILayout.Width(280)); + EditorGUILayout.LabelField("地址值", GUILayout.Width(300)); + EditorGUILayout.LabelField("引用数", GUILayout.Width(60)); + EditorGUILayout.EndHorizontal(); + + _scrollPos = EditorGUILayout.BeginScrollView(_scrollPos); + + foreach (var entry in list) + { + bool isOrphan = entry.ReferenceCount == 0; + bool isMissing = !entry.ExistsInAddressables; + + Color statusColor = isOrphan ? ColOrphan : (isMissing ? ColMissing : ColOk); + string statusIcon = isOrphan ? "⊘" : (isMissing ? "⚠" : "✓"); + + var prevBg = GUI.backgroundColor; + GUI.backgroundColor = statusColor * 0.6f; + EditorGUILayout.BeginHorizontal("box"); + GUI.backgroundColor = prevBg; + + EditorGUILayout.LabelField(statusIcon, GUILayout.Width(50)); + EditorGUILayout.LabelField(entry.FieldName, GUILayout.Width(280)); + + // 地址值可点击 → Ping Addressable asset + if (GUILayout.Button(entry.Value, + isOrphan ? EditorStyles.label : EditorStyles.miniButtonMid, + GUILayout.Width(300))) + { + PingAddressableAsset(entry.Value); + } + + EditorGUILayout.LabelField( + $"{entry.ReferenceCount}", + GUILayout.Width(60)); + + EditorGUILayout.EndHorizontal(); + + // 展开:显示引用文件列表 + if (entry.ReferenceCount > 0 && entry.ReferencedInFiles != null) + { + foreach (var file in entry.ReferencedInFiles) + { + EditorGUILayout.BeginHorizontal(); + GUILayout.Space(60); + EditorGUILayout.LabelField($" ↳ {file}", EditorStyles.miniLabel); + EditorGUILayout.EndHorizontal(); + } + } + } + + EditorGUILayout.EndScrollView(); + } + + // ── 扫描逻辑 ────────────────────────────────────────────────────── + + private void RunScan() + { + _entries = new List(); + + // 1. 收集所有 AddressKeys 常量 + var keyFields = typeof(BaseGames.Core.Assets.AddressKeys) + .GetFields(System.Reflection.BindingFlags.Public + | System.Reflection.BindingFlags.Static + | System.Reflection.BindingFlags.FlattenHierarchy) + .Where(f => f.IsLiteral && !f.IsInitOnly && f.FieldType == typeof(string)); + + var keyDict = new Dictionary(); + foreach (var f in keyFields) + { + var value = (string)f.GetRawConstantValue(); + keyDict[f.Name] = new KeyEntry + { + FieldName = f.Name, + Value = value, + ExistsInAddressables = false, + ReferencedInFiles = new List() + }; + } + + // 2. 检查 Addressables + var registeredAddresses = AddressKeyValidator.RunValidation() + .Where(r => r.ExistsInAddressables) + .Select(r => r.FieldName) + .ToHashSet(); + + foreach (var kv in keyDict) + kv.Value.ExistsInAddressables = registeredAddresses.Contains(kv.Key); + + // 3. 扫描 .cs 文件引用 + var csFiles = Directory.GetFiles( + Path.Combine(Application.dataPath, "Scripts"), + "*.cs", + SearchOption.AllDirectories); + + foreach (var file in csFiles) + { + string content; + try { content = File.ReadAllText(file); } + catch { continue; } + + foreach (var kv in keyDict) + { + // 匹配 AddressKeys.FieldName(单词边界,避免前缀误匹配) + if (Regex.IsMatch(content, $@"\bAddressKeys\.{Regex.Escape(kv.Key)}\b")) + { + string relativePath = "Assets" + file.Substring(Application.dataPath.Length).Replace('\\', '/'); + kv.Value.ReferencedInFiles.Add(relativePath); + } + } + } + + foreach (var kv in keyDict) + _entries.Add(kv.Value); + + _entries.Sort((a, b) => + { + // 孤儿排最前,其次缺失,最后正常 + int aScore = a.ReferenceCount == 0 ? 0 : (!a.ExistsInAddressables ? 1 : 2); + int bScore = b.ReferenceCount == 0 ? 0 : (!b.ExistsInAddressables ? 1 : 2); + return aScore != bScore ? aScore.CompareTo(bScore) : string.Compare(a.FieldName, b.FieldName); + }); + + Debug.Log($"[AddressReferenceGraph] 扫描完成:{_entries.Count} 个 Key," + + $"{_entries.Count(e => e.ReferenceCount == 0)} 孤儿," + + $"{_entries.Count(e => !e.ExistsInAddressables)} 未在 Addressables。"); + } + + // ── CSV 导出 ────────────────────────────────────────────────────── + + private void ExportCsv() + { + string path = EditorUtility.SaveFilePanel("导出 CSV", "", "AddressKeyReport", "csv"); + if (string.IsNullOrEmpty(path)) return; + + using var writer = new StreamWriter(path, false, System.Text.Encoding.UTF8); + writer.WriteLine("FieldName,Value,ExistsInAddressables,ReferenceCount,ReferencedFiles"); + foreach (var e in _entries) + { + string files = e.ReferencedInFiles != null + ? string.Join(" | ", e.ReferencedInFiles) + : ""; + writer.WriteLine($"{e.FieldName},{e.Value},{e.ExistsInAddressables},{e.ReferenceCount},{files}"); + } + + Debug.Log($"[AddressReferenceGraph] CSV 已导出:{path}"); + } + + // ── Ping Addressable ────────────────────────────────────────────── + + private static void PingAddressableAsset(string address) + { +#if UNITY_EDITOR + var guids = AssetDatabase.FindAssets($"\"{address}\""); + if (guids.Length > 0) + { + var obj = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guids[0])); + if (obj != null) EditorGUIUtility.PingObject(obj); + } +#endif + } + + // ── Data ────────────────────────────────────────────────────────── + + private class KeyEntry + { + public string FieldName; + public string Value; + public bool ExistsInAddressables; + public List ReferencedInFiles; + public int ReferenceCount => ReferencedInFiles?.Count ?? 0; + } + } +} diff --git a/Assets/Scripts/Editor/AddressReferenceGraphWindow.cs.meta b/Assets/Scripts/Editor/AddressReferenceGraphWindow.cs.meta new file mode 100644 index 0000000..9daca7f --- /dev/null +++ b/Assets/Scripts/Editor/AddressReferenceGraphWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 884c3c28d25877643afa90b72ba2a650 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/BaseGames.Editor.asmdef b/Assets/Scripts/Editor/BaseGames.Editor.asmdef index 046e957..06f28a6 100644 --- a/Assets/Scripts/Editor/BaseGames.Editor.asmdef +++ b/Assets/Scripts/Editor/BaseGames.Editor.asmdef @@ -1,32 +1,42 @@ { - "excludePlatforms": [], - "allowUnsafeCode": false, - "precompiledReferences": [], - "name": "BaseGames.Editor", - "defineConstraints": [], - "noEngineReferences": false, - "versionDefines": [], - "rootNamespace": "BaseGames.Editor", - "references": [ - "BaseGames.Core", - "BaseGames.Core.Events", - "Unity.Addressables", - "Unity.Addressables.Editor", - "BaseGames.Core.Save", - "BaseGames.Input", - "BaseGames.Combat", - "BaseGames.Player", - "BaseGames.Enemies", - "BaseGames.World", - "BaseGames.UI", - "BaseGames.Audio", - "BaseGames.Feedback", - "BaseGames.Dialogue", - "BaseGames.Progression" - ], - "autoReferenced": false, - "overrideReferences": false, - "includePlatforms": [ - "Editor" - ] -} + "name": "BaseGames.Editor", + "rootNamespace": "BaseGames.Editor", + "references": [ + "BaseGames.Core", + "BaseGames.Core.Events", + "Unity.Addressables", + "Unity.Addressables.Editor", + "BaseGames.Core.Save", + "BaseGames.Input", + "BaseGames.Combat", + "BaseGames.Player", + "BaseGames.Player.States", + "BaseGames.Enemies", + "BaseGames.Camera", + "BaseGames.World", + "BaseGames.UI", + "BaseGames.Audio", + "BaseGames.Feedback", + "BaseGames.Dialogue", + "BaseGames.Progression", + "PathBerserker2d", + "Unity.Cinemachine", + "Kybernetik.Animancer", + "BaseGames.Animation", + "BaseGames.Equipment", + "BaseGames.Skills", + "BaseGames.World.Map", + "BaseGames.EventChain" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Scripts/Editor/BossSkillSequenceWindow.cs b/Assets/Scripts/Editor/BossSkillSequenceWindow.cs new file mode 100644 index 0000000..0020df3 --- /dev/null +++ b/Assets/Scripts/Editor/BossSkillSequenceWindow.cs @@ -0,0 +1,305 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using BaseGames.Boss; + +namespace BaseGames.Editor +{ + /// + /// Boss 技能序列甘特图可视化窗口(架构 23_BossSkillModule §12)。 + /// 菜单:BaseGames/Tools/Boss Skill Sequence Viewer + /// + /// 功能: + /// - 拖放 BossSkillSO 或 SkillSequenceSO 资产加载 + /// - 甘特图:Windup(黄色)→ Active(红色)→ Recovery(灰色)各阶段时序条 + /// - VulnerabilityWindow 绿色覆盖层(TriggerDelay 偏移 + Duration 宽度) + /// - DurationNormalized < 0.1 时阶段条变红警告 + /// - 点击阶段条高亮对应 AttackPatternSO(EditorGUIUtility.PingObject) + /// + public class BossSkillSequenceWindow : EditorWindow + { + // ── State ────────────────────────────────────────────────────────── + private BossSkillSO _loadedSkill; + private SkillSequenceSO _loadedSequence; + private Vector2 _scrollPos; + + // ── Layout ───────────────────────────────────────────────────────── + private const float HeaderH = 24f; + private const float RowH = 28f; + private const float LabelW = 180f; + private const float TimelineW = 600f; + private const float MinBarWidth = 6f; + + // ── Colors ───────────────────────────────────────────────────────── + private static readonly Color ColWindup = new Color(0.95f, 0.80f, 0.10f, 0.85f); + private static readonly Color ColActive = new Color(0.90f, 0.20f, 0.15f, 0.85f); + private static readonly Color ColRecovery = new Color(0.50f, 0.50f, 0.55f, 0.70f); + private static readonly Color ColVuln = new Color(0.10f, 0.90f, 0.30f, 0.45f); + private static readonly Color ColDelay = new Color(0.25f, 0.25f, 0.30f, 0.50f); + private static readonly Color ColWarn = new Color(0.95f, 0.10f, 0.10f, 0.85f); + + [MenuItem("BaseGames/Tools/Boss Skill Sequence Viewer")] + public static void OpenWindow() + { + var win = GetWindow("Boss Skill Sequence"); + win.minSize = new Vector2(900, 400); + win.Show(); + } + + // ── GUI ──────────────────────────────────────────────────────────── + + private void OnGUI() + { + DrawToolbar(); + + if (_loadedSkill == null && _loadedSequence == null) + { + EditorGUILayout.HelpBox( + "将 BossSkillSO 或 SkillSequenceSO 资产拖放到此处,或使用上方字段加载。", + MessageType.Info); + HandleDragDrop(); + return; + } + + _scrollPos = EditorGUILayout.BeginScrollView(_scrollPos); + + if (_loadedSkill != null) + DrawSkillTimeline(_loadedSkill); + else if (_loadedSequence != null) + DrawSequenceTimeline(_loadedSequence); + + EditorGUILayout.EndScrollView(); + } + + // ── Toolbar ─────────────────────────────────────────────────────── + + private void DrawToolbar() + { + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + + EditorGUILayout.LabelField("技能:", GUILayout.Width(36)); + var newSkill = (BossSkillSO)EditorGUILayout.ObjectField( + _loadedSkill, typeof(BossSkillSO), false, GUILayout.Width(200)); + if (newSkill != _loadedSkill) + { + _loadedSkill = newSkill; + _loadedSequence = null; + } + + GUILayout.Space(12); + EditorGUILayout.LabelField("序列:", GUILayout.Width(36)); + var newSeq = (SkillSequenceSO)EditorGUILayout.ObjectField( + _loadedSequence, typeof(SkillSequenceSO), false, GUILayout.Width(200)); + if (newSeq != _loadedSequence) + { + _loadedSequence = newSeq; + _loadedSkill = null; + } + + GUILayout.FlexibleSpace(); + + if (GUILayout.Button("清除", EditorStyles.toolbarButton, GUILayout.Width(50))) + { + _loadedSkill = null; + _loadedSequence = null; + } + + EditorGUILayout.EndHorizontal(); + } + + // ── BossSkillSO 时间轴 ──────────────────────────────────────────── + + private void DrawSkillTimeline(BossSkillSO skill) + { + EditorGUILayout.LabelField($"技能:{skill.displayName} [{skill.skillId}]", + EditorStyles.boldLabel); + EditorGUILayout.Space(4); + + if (skill.attackPatterns == null || skill.attackPatterns.Length == 0) + { + EditorGUILayout.HelpBox("此技能没有 AttackPattern。", MessageType.Warning); + return; + } + + // 计算总时长 + float totalDuration = 0f; + foreach (var p in skill.attackPatterns) + if (p != null) totalDuration += p.WindupDuration + p.ActiveDuration + p.RecoveryDuration; + + if (totalDuration <= 0f) totalDuration = 1f; + + DrawTimelineHeader(totalDuration); + + float cursor = 0f; + for (int i = 0; i < skill.attackPatterns.Length; i++) + { + var pattern = skill.attackPatterns[i]; + if (pattern == null) continue; + DrawPatternRow($"[{i}] {pattern.name}", pattern, ref cursor, totalDuration); + } + + // 绘制 VulnerabilityWindows + if (skill.vulnerabilityWindows != null && skill.vulnerabilityWindows.Length > 0) + { + EditorGUILayout.Space(4); + EditorGUILayout.LabelField("弱点窗口(Vulnerability Windows)", EditorStyles.miniBoldLabel); + foreach (var vw in skill.vulnerabilityWindows) + DrawVulnWindowRow(vw, totalDuration); + } + } + + // ── SkillSequenceSO 时间轴 ──────────────────────────────────────── + + private void DrawSequenceTimeline(SkillSequenceSO sequence) + { + EditorGUILayout.LabelField($"序列:{sequence.name}", EditorStyles.boldLabel); + EditorGUILayout.Space(4); + + if (sequence.steps == null || sequence.steps.Length == 0) + { + EditorGUILayout.HelpBox("此序列没有步骤。", MessageType.Warning); + return; + } + + // 计算总时长 + float totalDuration = 0f; + foreach (var step in sequence.steps) + { + totalDuration += step.delayBeforeStep; + if (step.pattern != null) + totalDuration += step.pattern.WindupDuration + step.pattern.ActiveDuration + step.pattern.RecoveryDuration; + } + if (totalDuration <= 0f) totalDuration = 1f; + + DrawTimelineHeader(totalDuration); + + float cursor = 0f; + for (int i = 0; i < sequence.steps.Length; i++) + { + var step = sequence.steps[i]; + + // 延迟条 + if (step.delayBeforeStep > 0f) + { + DrawBar($"延迟 {step.delayBeforeStep:F2}s", cursor, step.delayBeforeStep, + totalDuration, ColDelay, null); + cursor += step.delayBeforeStep; + } + + if (step.pattern != null) + DrawPatternRow($"[{i}] {step.pattern.name}", step.pattern, ref cursor, totalDuration); + } + } + + // ── 共用绘制方法 ────────────────────────────────────────────────── + + private void DrawTimelineHeader(float totalDuration) + { + Rect headerRect = EditorGUILayout.GetControlRect(false, HeaderH); + headerRect.x += LabelW; + headerRect.width -= LabelW; + + EditorGUI.DrawRect(headerRect, new Color(0.18f, 0.18f, 0.20f)); + + // 刻度线(每 0.5s 一条) + float step = 0.5f; + for (float t = 0; t <= totalDuration + 0.001f; t += step) + { + float x = headerRect.x + (t / totalDuration) * headerRect.width; + EditorGUI.DrawRect(new Rect(x, headerRect.y, 1f, HeaderH * 0.6f), Color.gray); + EditorGUI.LabelField(new Rect(x + 2f, headerRect.y, 40f, HeaderH), + $"{t:F1}s", new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = Color.gray } }); + } + } + + private void DrawPatternRow(string label, AttackPatternSO pattern, ref float cursor, float totalDuration) + { + float windupDur = pattern.WindupDuration; + float activeDur = pattern.ActiveDuration; + float recoveryDur = pattern.RecoveryDuration; + + float rowStart = cursor; + EditorGUILayout.BeginHorizontal(GUILayout.Height(RowH)); + + // 标签 + Ping + if (GUILayout.Button(label, EditorStyles.miniLabel, GUILayout.Width(LabelW), GUILayout.Height(RowH))) + EditorGUIUtility.PingObject(pattern); + + Rect timelineRect = EditorGUILayout.GetControlRect(false, RowH, + GUILayout.Width(TimelineW)); + + // Windup + if (windupDur > 0f) + DrawBarInRect(timelineRect, cursor, windupDur, totalDuration, + windupDur / (windupDur + activeDur + recoveryDur) < 0.1f ? ColWarn : ColWindup); + cursor += windupDur; + + // Active + if (activeDur > 0f) + DrawBarInRect(timelineRect, cursor, activeDur, totalDuration, + activeDur / (windupDur + activeDur + recoveryDur) < 0.1f ? ColWarn : ColActive); + cursor += activeDur; + + // Recovery + if (recoveryDur > 0f) + DrawBarInRect(timelineRect, cursor, recoveryDur, totalDuration, + recoveryDur / (windupDur + activeDur + recoveryDur) < 0.1f ? ColWarn : ColRecovery); + cursor += recoveryDur; + + _ = rowStart; // suppress unused warning + EditorGUILayout.EndHorizontal(); + } + + private void DrawVulnWindowRow(VulnerabilityWindow vw, float totalDuration) + { + string label = $"弱点:{vw.TriggerType} +{vw.TriggerDelay:F2}s / {vw.Duration:F2}s"; + DrawBar(label, vw.TriggerDelay, vw.Duration, totalDuration, ColVuln, null); + } + + private void DrawBar(string label, float start, float duration, float totalDuration, + Color color, AttackPatternSO pingTarget) + { + EditorGUILayout.BeginHorizontal(GUILayout.Height(RowH)); + + if (GUILayout.Button(label, EditorStyles.miniLabel, GUILayout.Width(LabelW), GUILayout.Height(RowH))) + { + if (pingTarget != null) EditorGUIUtility.PingObject(pingTarget); + } + + Rect timelineRect = EditorGUILayout.GetControlRect(false, RowH, GUILayout.Width(TimelineW)); + DrawBarInRect(timelineRect, start, duration, totalDuration, color); + + EditorGUILayout.EndHorizontal(); + } + + private static void DrawBarInRect(Rect timeline, float start, float duration, + float totalDuration, Color color) + { + float x = timeline.x + (start / totalDuration) * timeline.width; + float w = Mathf.Max(MinBarWidth, (duration / totalDuration) * timeline.width); + EditorGUI.DrawRect(new Rect(x, timeline.y + 2f, w, timeline.height - 4f), color); + } + + // ── Drag & Drop ─────────────────────────────────────────────────── + + private void HandleDragDrop() + { + var evt = Event.current; + if (evt.type != EventType.DragUpdated && evt.type != EventType.DragPerform) return; + + DragAndDrop.visualMode = DragAndDropVisualMode.Copy; + + if (evt.type == EventType.DragPerform) + { + DragAndDrop.AcceptDrag(); + foreach (var obj in DragAndDrop.objectReferences) + { + if (obj is BossSkillSO skill) { _loadedSkill = skill; _loadedSequence = null; break; } + if (obj is SkillSequenceSO seq) { _loadedSequence = seq; _loadedSkill = null; break; } + } + Repaint(); + } + evt.Use(); + } + } +} diff --git a/Assets/Scripts/Editor/BossSkillSequenceWindow.cs.meta b/Assets/Scripts/Editor/BossSkillSequenceWindow.cs.meta new file mode 100644 index 0000000..397db5f --- /dev/null +++ b/Assets/Scripts/Editor/BossSkillSequenceWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d47145d394333184eb3ff822e3c4aa4d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/Combat.meta b/Assets/Scripts/Editor/Combat.meta new file mode 100644 index 0000000..253e654 --- /dev/null +++ b/Assets/Scripts/Editor/Combat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a884190f06d571d47b05f7693fab90e2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/Combat/HurtBoxEditor.cs b/Assets/Scripts/Editor/Combat/HurtBoxEditor.cs new file mode 100644 index 0000000..d3fa618 --- /dev/null +++ b/Assets/Scripts/Editor/Combat/HurtBoxEditor.cs @@ -0,0 +1,64 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; +using BaseGames.Combat; + +namespace BaseGames.Editor +{ + /// + /// HurtBox 运行时注入状态可视化面板。 + /// 通过 HurtBox 上的 Editor* 属性读取注入状态,以颜色区分是否注入成功。 + /// 绿色 = 注入完成;橙色 = 未注入(该能力静默不生效);灰色 = 非 PlayMode。 + /// + [CustomEditor(typeof(HurtBox))] + public class HurtBoxEditor : UnityEditor.Editor + { + // (属性访问器, 标签, 缺席说明) + private static readonly (System.Func getter, string label, string absentNote)[] _fields = + { + (hb => hb.EditorOwner, "Owner (IDamageable)", "— 注入失败,ReceiveDamage 将无效"), + (hb => hb.EditorShieldable, "Shieldable", "— 未注入(玩家专属,敌人无需)"), + (hb => hb.EditorParrySystem, "ParrySystem", "— 未注入(弹反静默不生效)"), + (hb => hb.EditorPoiseSource, "PoiseSource", "— 未注入(霸体静默不生效)"), + (hb => hb.EditorStatusEffectable, "StatusEffectable", "— 未注入(状态效果静默不生效)"), + }; + + public override void OnInspectorGUI() + { + DrawDefaultInspector(); + + EditorGUILayout.Space(4); + EditorGUILayout.LabelField("── 运行时注入状态 ──", EditorStyles.boldLabel); + + if (!Application.isPlaying) + { + EditorGUILayout.HelpBox("进入 PlayMode 后查看注入状态。", MessageType.Info); + return; + } + + var hurtBox = (HurtBox)target; + + foreach (var (getter, label, absentNote) in _fields) + { + var value = getter(hurtBox); + bool present = value != null; + + var savedColor = GUI.contentColor; + GUI.contentColor = present + ? new Color(0.3f, 0.9f, 0.4f) // 绿 + : new Color(1.0f, 0.6f, 0.1f); // 橙 + + string displayValue = present + ? $"✓ {value.GetType().Name}" + : $"✗ null {absentNote}"; + + EditorGUILayout.LabelField(label, displayValue); + GUI.contentColor = savedColor; + } + + // 持续刷新(避免只显示初始状态) + if (Application.isPlaying) Repaint(); + } + } +} +#endif diff --git a/Assets/Scripts/Editor/Combat/HurtBoxEditor.cs.meta b/Assets/Scripts/Editor/Combat/HurtBoxEditor.cs.meta new file mode 100644 index 0000000..eaa12f4 --- /dev/null +++ b/Assets/Scripts/Editor/Combat/HurtBoxEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8650ccc7960fe304a95be1c629ef7b1e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/CreateEventChannelAssets.cs b/Assets/Scripts/Editor/CreateEventChannelAssets.cs index f2e5a47..f4697f5 100644 --- a/Assets/Scripts/Editor/CreateEventChannelAssets.cs +++ b/Assets/Scripts/Editor/CreateEventChannelAssets.cs @@ -30,7 +30,11 @@ namespace BaseGames.Editor CreateAsset ("Core", "EVT_Vector2"); CreateAsset ("Core", "EVT_Transform"); CreateAsset ("Core", "EVT_GameState"); + CreateAsset ("Core", "EVT_GameStateChanged"); CreateAsset("Core", "EVT_SceneLoadRequest"); + CreateAsset ("Core", "EVT_SceneLoaded"); + CreateAsset ("Core", "EVT_FadeInRequest"); + CreateAsset ("Core", "EVT_FadeOutRequest"); // ── 难度 ────────────────────────────────────────────────────────── CreateAsset("Difficulty", "EVT_DifficultyChanged"); @@ -38,14 +42,20 @@ namespace BaseGames.Editor // ── 战斗 ────────────────────────────────────────────────────────── CreateAsset ("Combat", "EVT_HitConfirmed"); CreateAsset ("Combat", "EVT_PlayerDied"); + CreateAsset ("Combat", "EVT_DeathScreenConfirmed"); CreateAsset ("Combat", "EVT_EnemyDied"); CreateAsset ("Combat", "EVT_ParrySuccess"); CreateAsset ("Combat", "EVT_PlayerRespawn"); + CreateAsset ("Combat", "EVT_PlayerRespawned"); + CreateAsset ("Combat", "EVT_RespawnStarted"); + CreateAsset ("Combat", "EVT_RespawnCompleted"); // ── Boss ────────────────────────────────────────────────────────── CreateAsset ("Boss", "EVT_BossSkill"); CreateAsset ("Boss", "EVT_BossPhase"); CreateAsset ("Boss", "EVT_StatusEffect"); + CreateAsset ("Boss", "EVT_BossFightStarted"); + CreateAsset ("Boss", "EVT_BossFightEnded"); // ── 任务 ────────────────────────────────────────────────────────── CreateAsset("Quest", "EVT_QuestStateChanged"); @@ -54,8 +64,14 @@ namespace BaseGames.Editor // ── UI ──────────────────────────────────────────────────────────── CreateAsset ("UI", "EVT_PauseRequested"); CreateAsset ("UI", "EVT_PauseResumed"); + CreateAsset ("UI", "EVT_FastTravelOpen"); + CreateAsset ("UI", "EVT_ShopOpen"); + CreateAsset ("UI", "EVT_MapOpen"); CreateAsset ("UI", "EVT_ColorblindMode"); + // ── World ───────────────────────────────────────────────────────── + CreateAsset ("World", "EVT_SavePointActivated"); + // ── 对话/商店 ───────────────────────────────────────────────────── CreateAsset ("Dialogue", "EVT_ShopPurchase"); CreateAsset ("Dialogue", "EVT_DialogueStartRequest"); @@ -121,7 +137,7 @@ namespace BaseGames.Editor Debug.Log($"[CreateEventChannelAssets] 已创建: {fullPath}"); } - /// 递归创建所有缺失的中间文件夹(兼容 AssetDatabase)。 + /// 递归创建所有缺失的中间文件夹(使用 AssetDatabase API)。 private static void EnsureDirectory(string path) { if (AssetDatabase.IsValidFolder(path)) diff --git a/Assets/Scripts/Editor/Equipment.meta b/Assets/Scripts/Editor/Equipment.meta new file mode 100644 index 0000000..a71ac0f --- /dev/null +++ b/Assets/Scripts/Editor/Equipment.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 53f701b15574fcc49bc11d1e8798ba52 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/Equipment/CharmEffectDrawer.cs b/Assets/Scripts/Editor/Equipment/CharmEffectDrawer.cs new file mode 100644 index 0000000..1961964 --- /dev/null +++ b/Assets/Scripts/Editor/Equipment/CharmEffectDrawer.cs @@ -0,0 +1,118 @@ +#if UNITY_EDITOR +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using BaseGames.Equipment; + +namespace BaseGames.Editor.Equipment +{ + /// + /// 为 CharmSO.effects(List<ICharmEffect>)提供友好的 Inspector 体验(架构 09_ProgressionModule §4.1)。 + /// - 下拉菜单选类型(显示中文名而非 C# 全称) + /// - 每条效果展开显示字段 + GetEffectDescription() 预览文字 + /// - 支持单条删除 + /// + [CustomEditor(typeof(CharmSO))] + public class CharmSOEditor : UnityEditor.Editor + { + // 已注册的所有 ICharmEffect 实现类型(反射收集) + private static readonly Type[] _effectTypes = CollectEffectTypes(); + + // 策划友好名称映射 + private static readonly Dictionary _typeLabels = new() + { + { typeof(StatModifierEffect), "属性加成" }, + { typeof(AttackSpeedEffect), "攻击速度" }, + { typeof(OnHitEffect), "命中触发" }, + { typeof(SoulSpellEffect), "灵魂法术" }, + { typeof(SkillNumericModifierEffect), "技能数值修改" }, + { typeof(SkillSlotOverrideEffect), "技能插槽替换" }, + { typeof(WeaponOverrideEffect), "武器替换" }, + }; + + private SerializedProperty _effectsProp; + + private void OnEnable() + => _effectsProp = serializedObject.FindProperty("effects"); + + public override void OnInspectorGUI() + { + serializedObject.Update(); + DrawPropertiesExcluding(serializedObject, "effects"); + + EditorGUILayout.Space(8); + EditorGUILayout.LabelField("Effects", EditorStyles.boldLabel); + + if (_effectsProp != null) + { + for (int i = 0; i < _effectsProp.arraySize; i++) + { + var elemProp = _effectsProp.GetArrayElementAtIndex(i); + var effect = elemProp.managedReferenceValue as ICharmEffect; + string label = effect != null && _typeLabels.TryGetValue(effect.GetType(), out var n) + ? n : (effect?.GetType().Name ?? "null"); + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(label, EditorStyles.boldLabel); + + if (GUILayout.Button("✕", GUILayout.Width(24))) + { + _effectsProp.DeleteArrayElementAtIndex(i); + serializedObject.ApplyModifiedProperties(); + break; + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.PropertyField(elemProp, GUIContent.none, true); + + if (effect != null) + EditorGUILayout.LabelField(effect.GetEffectDescription(), + EditorStyles.miniLabel); + + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(2); + } + } + + // 添加效果按钮(下拉菜单) + if (GUILayout.Button("+ 添加效果")) + { + var menu = new GenericMenu(); + foreach (var t in _effectTypes) + { + var captured = t; + string menuLabel = _typeLabels.GetValueOrDefault(t, t.Name); + menu.AddItem(new GUIContent(menuLabel), false, () => + { + if (_effectsProp == null) return; + _effectsProp.arraySize++; + _effectsProp + .GetArrayElementAtIndex(_effectsProp.arraySize - 1) + .managedReferenceValue = Activator.CreateInstance(captured); + serializedObject.ApplyModifiedProperties(); + }); + } + menu.ShowAsContext(); + } + + serializedObject.ApplyModifiedProperties(); + } + + private static Type[] CollectEffectTypes() + { + var baseType = typeof(ICharmEffect); + return AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(a => + { + try { return a.GetTypes(); } + catch { return Array.Empty(); } + }) + .Where(t => t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom(t)) + .ToArray(); + } + } +} +#endif diff --git a/Assets/Scripts/Editor/Equipment/CharmEffectDrawer.cs.meta b/Assets/Scripts/Editor/Equipment/CharmEffectDrawer.cs.meta new file mode 100644 index 0000000..4a62186 --- /dev/null +++ b/Assets/Scripts/Editor/Equipment/CharmEffectDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 38fb3e35ebefbc8418ba2ea0b5781f92 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/EventBusMonitorWindow.cs b/Assets/Scripts/Editor/EventBusMonitorWindow.cs new file mode 100644 index 0000000..3d61edf --- /dev/null +++ b/Assets/Scripts/Editor/EventBusMonitorWindow.cs @@ -0,0 +1,117 @@ +using System; +using System.Linq; +using BaseGames.Core.Events; +using UnityEditor; +using UnityEngine; + +namespace BaseGames.Editor +{ + public sealed class EventBusMonitorWindow : EditorWindow + { + private string _filter = string.Empty; + private bool _pauseCapture; + private bool _autoScroll = true; + private Vector2 _scroll; + + [MenuItem("BaseGames/Tools/Event Bus Monitor %#e")] + public static void OpenWindow() + { + EventBusMonitorWindow window = GetWindow("Event Bus Monitor"); + window.minSize = new Vector2(760f, 320f); + } + + private void OnEnable() + { + EditorApplication.update += RepaintWhilePlaying; + } + + private void OnDisable() + { + EditorApplication.update -= RepaintWhilePlaying; + } + + private void OnGUI() + { + DrawToolbar(); + DrawHeader(); + DrawRows(); + } + + private void DrawToolbar() + { + using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) + { + GUILayout.Label("Filter", GUILayout.Width(34f)); + _filter = GUILayout.TextField(_filter, EditorStyles.toolbarTextField, GUILayout.MinWidth(180f)); + + GUILayout.Space(8f); + _pauseCapture = GUILayout.Toggle(_pauseCapture, "Pause", EditorStyles.toolbarButton, GUILayout.Width(56f)); + _autoScroll = GUILayout.Toggle(_autoScroll, "Auto Scroll", EditorStyles.toolbarButton, GUILayout.Width(82f)); + + if (GUILayout.Button("Clear", EditorStyles.toolbarButton, GUILayout.Width(48f))) + EventBusMonitor.Clear(); + + GUILayout.FlexibleSpace(); + GUILayout.Label(EditorApplication.isPlaying ? "Play Mode" : "Edit Mode", EditorStyles.miniLabel); + } + } + + private void DrawHeader() + { + using (new EditorGUILayout.HorizontalScope()) + { + GUILayout.Label("Time", EditorStyles.boldLabel, GUILayout.Width(80f)); + GUILayout.Label("Frame", EditorStyles.boldLabel, GUILayout.Width(60f)); + GUILayout.Label("Channel", EditorStyles.boldLabel, GUILayout.Width(220f)); + GUILayout.Label("Payload", EditorStyles.boldLabel, GUILayout.ExpandWidth(true)); + GUILayout.Label("Subs", EditorStyles.boldLabel, GUILayout.Width(48f)); + } + EditorGUILayout.LabelField(GUIContent.none, GUI.skin.horizontalSlider); + } + + private void DrawRows() + { + var records = EventBusMonitor.Records; + if (!string.IsNullOrWhiteSpace(_filter)) + { + records = records.Where(record => + record.ChannelName.IndexOf(_filter, StringComparison.OrdinalIgnoreCase) >= 0 || + record.Payload.IndexOf(_filter, StringComparison.OrdinalIgnoreCase) >= 0); + } + + var displayRecords = records.ToArray(); + + _scroll = EditorGUILayout.BeginScrollView(_scroll); + foreach (var record in displayRecords) + DrawRow(record); + EditorGUILayout.EndScrollView(); + + if (_autoScroll && Event.current.type == EventType.Repaint) + _scroll.y = float.MaxValue; + } + + private void DrawRow(EventBusMonitor.EventRecord record) + { + Color oldColor = GUI.color; + if (record.ListenerCount == 0) + GUI.color = new Color(1f, 0.65f, 0.65f); + + using (new EditorGUILayout.HorizontalScope()) + { + GUILayout.Label(record.Timestamp.ToString("HH:mm:ss.fff"), GUILayout.Width(80f)); + GUILayout.Label($"#{record.FrameCount}", GUILayout.Width(60f)); + GUILayout.Label(record.ChannelName, GUILayout.Width(220f)); + GUILayout.Label(record.Payload, GUILayout.ExpandWidth(true)); + GUILayout.Label(record.ListenerCount.ToString(), GUILayout.Width(48f)); + } + + GUI.color = oldColor; + } + + private void RepaintWhilePlaying() + { + if (!_pauseCapture) + Repaint(); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Editor/EventBusMonitorWindow.cs.meta b/Assets/Scripts/Editor/EventBusMonitorWindow.cs.meta new file mode 100644 index 0000000..29badf1 --- /dev/null +++ b/Assets/Scripts/Editor/EventBusMonitorWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 861ce74d8a5c0ce4f957719423a0be7b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/EventChainEditorWindow.cs b/Assets/Scripts/Editor/EventChainEditorWindow.cs new file mode 100644 index 0000000..5d9c4c0 --- /dev/null +++ b/Assets/Scripts/Editor/EventChainEditorWindow.cs @@ -0,0 +1,312 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using BaseGames.EventChain; + +namespace BaseGames.Editor +{ + /// + /// 事件链可视化编辑器窗口(架构 14_NarrativeModule §13)。 + /// 菜单:BaseGames/Tools/Event Chain Viewer + /// + /// 功能: + /// - 左侧:chainId 分组总览(按完成状态着色) + /// - 右侧:选中链的 Conditions 和 Actions 表格 + /// - Play Mode:运行时状态着色(已完成=绿 / 条件满足=橙 / 未满足=白) + /// - ChainCompletedCondition 依赖链箭头指示 + /// - 执行日志(最近 20 条) + /// - 双击 → EditorGUIUtility.PingObject + /// + public class EventChainEditorWindow : EditorWindow + { + // ── State ────────────────────────────────────────────────────────── + private EventChainSO[] _allChains; + private EventChainSO _selectedChain; + private Vector2 _leftScroll; + private Vector2 _rightScroll; + private Vector2 _logScroll; + + private static readonly List _log = new(); + private const int MaxLogEntries = 20; + + // ── Colors ───────────────────────────────────────────────────────── + private static readonly Color ColCompleted = new Color(0.15f, 0.75f, 0.25f, 0.80f); + private static readonly Color ColActive = new Color(0.95f, 0.60f, 0.10f, 0.80f); + private static readonly Color ColPending = new Color(0.70f, 0.70f, 0.75f, 0.80f); + + [MenuItem("BaseGames/Tools/Event Chain Viewer")] + public static void OpenWindow() + { + var win = GetWindow("Event Chain Viewer"); + win.minSize = new Vector2(800, 500); + win.Show(); + } + + /// 外部调用:向执行日志追加一条记录(可在运行时由 EventChainManager 调用)。 + public static void LogExecution(string chainId, string message) + { + _log.Add($"[{System.DateTime.Now:HH:mm:ss}] [{chainId}] {message}"); + if (_log.Count > MaxLogEntries) + _log.RemoveAt(0); + } + + // ── Lifecycle ───────────────────────────────────────────────────── + + private void OnEnable() + { + RefreshChainList(); + EditorApplication.playModeStateChanged += OnPlayModeChanged; + EventChainManager.OnChainExecutedInEditor += LogExecution; + } + + private void OnDisable() + { + EditorApplication.playModeStateChanged -= OnPlayModeChanged; + EventChainManager.OnChainExecutedInEditor -= LogExecution; + } + + private void OnPlayModeChanged(PlayModeStateChange state) + { + if (state == PlayModeStateChange.EnteredPlayMode + || state == PlayModeStateChange.ExitingPlayMode) + { + RefreshChainList(); + Repaint(); + } + } + + private void RefreshChainList() + { + var guids = AssetDatabase.FindAssets("t:EventChainSO"); + var chains = new List(guids.Length); + foreach (var g in guids) + { + var path = AssetDatabase.GUIDToAssetPath(g); + var chain = AssetDatabase.LoadAssetAtPath(path); + if (chain != null) chains.Add(chain); + } + _allChains = chains.OrderBy(c => c.chainId).ToArray(); + } + + // ── GUI ──────────────────────────────────────────────────────────── + + private void OnGUI() + { + DrawToolbar(); + + EditorGUILayout.BeginHorizontal(); + + // 左:链列表 + EditorGUILayout.BeginVertical(GUILayout.Width(240)); + DrawChainList(); + EditorGUILayout.EndVertical(); + + // 分割线 + EditorGUILayout.BeginVertical(GUILayout.Width(2)); + EditorGUI.DrawRect(GUILayoutUtility.GetRect(2, position.height), new Color(0.1f, 0.1f, 0.1f)); + EditorGUILayout.EndVertical(); + + // 右:选中链详情 + EditorGUILayout.BeginVertical(); + if (_selectedChain != null) + DrawChainDetail(_selectedChain); + else + EditorGUILayout.HelpBox("从左侧选择一条事件链查看详情。", MessageType.None); + EditorGUILayout.EndVertical(); + + EditorGUILayout.EndHorizontal(); + } + + // ── Toolbar ─────────────────────────────────────────────────────── + + private void DrawToolbar() + { + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + if (GUILayout.Button("刷新", EditorStyles.toolbarButton, GUILayout.Width(50))) + RefreshChainList(); + GUILayout.FlexibleSpace(); + EditorGUILayout.LabelField( + $"共 {_allChains?.Length ?? 0} 条事件链", + EditorStyles.toolbarButton); + EditorGUILayout.EndHorizontal(); + } + + // ── 左侧链列表 ──────────────────────────────────────────────────── + + private void DrawChainList() + { + EditorGUILayout.LabelField("事件链列表", EditorStyles.boldLabel); + _leftScroll = EditorGUILayout.BeginScrollView(_leftScroll); + + if (_allChains == null || _allChains.Length == 0) + { + EditorGUILayout.HelpBox("未找到 EventChainSO 资产。", MessageType.Info); + EditorGUILayout.EndScrollView(); + return; + } + + foreach (var chain in _allChains) + { + if (chain == null) continue; + + bool isSelected = _selectedChain == chain; + bool isCompleted = IsChainCompleted(chain); + bool isActive = Application.isPlaying && IsChainActive(chain); + + Color bgColor = isCompleted ? ColCompleted : isActive ? ColActive : ColPending; + var prevBg = GUI.backgroundColor; + GUI.backgroundColor = isSelected ? bgColor * 1.4f : bgColor * 0.7f; + + EditorGUILayout.BeginHorizontal("box"); + + // 状态图标 + string icon = isCompleted ? "✓" : isActive ? "▶" : "○"; + EditorGUILayout.LabelField(icon, GUILayout.Width(16)); + + if (GUILayout.Button(chain.chainId, isSelected ? EditorStyles.boldLabel : EditorStyles.label)) + _selectedChain = chain; + + // 双击 Ping + if (Event.current.type == EventType.MouseDown && Event.current.clickCount == 2 + && GUILayoutUtility.GetLastRect().Contains(Event.current.mousePosition)) + { + EditorGUIUtility.PingObject(chain); + Event.current.Use(); + } + + EditorGUILayout.EndHorizontal(); + GUI.backgroundColor = prevBg; + } + + EditorGUILayout.EndScrollView(); + } + + // ── 右侧详情 ────────────────────────────────────────────────────── + + private void DrawChainDetail(EventChainSO chain) + { + _rightScroll = EditorGUILayout.BeginScrollView(_rightScroll); + + // 标题行 + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField( + $"事件链:{chain.chainId}", + EditorStyles.boldLabel); + if (GUILayout.Button("↗ Ping", GUILayout.Width(60))) + EditorGUIUtility.PingObject(chain); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.LabelField( + $"可重复:{(chain.repeatable ? "是" : "否")} | " + + $"动作间隔:{chain.actionDelay:F2}s", + EditorStyles.miniLabel); + + EditorGUILayout.Space(6); + + // Conditions 表格 + EditorGUILayout.LabelField("触发条件(全部满足才触发)", EditorStyles.boldLabel); + if (chain.conditions != null && chain.conditions.Length > 0) + { + foreach (var cond in chain.conditions) + { + if (cond == null) continue; + + bool met = Application.isPlaying && cond.IsMet(); + var prevBg = GUI.backgroundColor; + GUI.backgroundColor = met ? ColCompleted * 0.8f : new Color(0.9f, 0.9f, 0.9f, 0.3f); + + EditorGUILayout.BeginHorizontal("box"); + string status = Application.isPlaying ? (met ? "✓" : "✗") : "—"; + EditorGUILayout.LabelField(status, GUILayout.Width(20)); + EditorGUILayout.LabelField(cond.GetType().Name, GUILayout.Width(220)); + + // 依赖箭头:ChainCompletedCondition + if (cond is ChainCompletedCondition depCond) + { + EditorGUILayout.LabelField($"→ 依赖链:{depCond.chainId}", + EditorStyles.miniLabel); + } + + if (GUILayout.Button("↗", GUILayout.Width(24))) + EditorGUIUtility.PingObject(cond); + EditorGUILayout.EndHorizontal(); + GUI.backgroundColor = prevBg; + } + } + else + { + EditorGUILayout.LabelField("(无条件,立即触发)", EditorStyles.miniLabel); + } + + EditorGUILayout.Space(6); + + // Actions 表格 + EditorGUILayout.LabelField("执行动作(顺序执行)", EditorStyles.boldLabel); + if (chain.actions != null && chain.actions.Length > 0) + { + for (int i = 0; i < chain.actions.Length; i++) + { + var action = chain.actions[i]; + if (action == null) continue; + + EditorGUILayout.BeginHorizontal("box"); + EditorGUILayout.LabelField($"[{i}]", GUILayout.Width(30)); + EditorGUILayout.LabelField(action.GetType().Name, GUILayout.Width(200)); + EditorGUILayout.LabelField(action.name, EditorStyles.miniLabel); + if (GUILayout.Button("↗", GUILayout.Width(24))) + EditorGUIUtility.PingObject(action); + EditorGUILayout.EndHorizontal(); + } + } + else + { + EditorGUILayout.LabelField("(无动作)", EditorStyles.miniLabel); + } + + // 执行日志 + EditorGUILayout.Space(6); + EditorGUILayout.LabelField($"执行日志(最近 {MaxLogEntries} 条)", EditorStyles.boldLabel); + _logScroll = EditorGUILayout.BeginScrollView(_logScroll, GUILayout.Height(120)); + var relevantLogs = _log.Where(l => l.Contains(chain.chainId)).ToList(); + if (relevantLogs.Count == 0) + EditorGUILayout.LabelField("—(无日志)", EditorStyles.miniLabel); + else + foreach (var entry in relevantLogs) + EditorGUILayout.LabelField(entry, EditorStyles.miniLabel); + EditorGUILayout.EndScrollView(); + + EditorGUILayout.EndScrollView(); + } + + // ── 运行时状态查询 ──────────────────────────────────────────────── + + private static bool IsChainCompleted(EventChainSO chain) + { + if (!Application.isPlaying) return false; + var manager = FindFirstObjectByType(); + if (manager == null) return false; + // 通过反射读取 _completedChains + var field = typeof(EventChainManager).GetField( + "_completedChains", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + if (field?.GetValue(manager) is HashSet completed) + return completed.Contains(chain.chainId); + return false; + } + + private static bool IsChainActive(EventChainSO chain) + { + // 链"激活中"= 有任意条件已满足但链未完成 + if (chain.conditions == null) return false; + return chain.conditions.Any(c => c != null && c.IsMet()); + } + + private void Update() + { + // Play Mode 下每秒刷新一次以更新状态颜色 + if (Application.isPlaying) + Repaint(); + } + } +} diff --git a/Assets/Scripts/Editor/EventChainEditorWindow.cs.meta b/Assets/Scripts/Editor/EventChainEditorWindow.cs.meta new file mode 100644 index 0000000..cae4163 --- /dev/null +++ b/Assets/Scripts/Editor/EventChainEditorWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e8cb0e5db63d15d418e73c29e6ff6f1f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/EventChannelEditor.cs b/Assets/Scripts/Editor/EventChannelEditor.cs new file mode 100644 index 0000000..dfb43a4 --- /dev/null +++ b/Assets/Scripts/Editor/EventChannelEditor.cs @@ -0,0 +1,92 @@ +using System; +using UnityEditor; +using UnityEngine; +using BaseGames.Core.Events; + +namespace BaseGames.Editor +{ + /// + /// 为 VoidEventChannelSO 提供 Inspector 内的"Raise(测试触发)"按钮。 + /// 仅在 Play Mode 下可用,防止在编辑状态误触发副作用。 + /// + [CustomEditor(typeof(VoidBaseEventChannelSO), true)] + public class VoidEventChannelSOEditor : UnityEditor.Editor + { + public override void OnInspectorGUI() + { + DrawDefaultInspector(); + + EditorGUILayout.Space(6); + EditorGUI.BeginDisabledGroup(!Application.isPlaying); + if (GUILayout.Button("▶ Raise(测试触发)", GUILayout.Height(28))) + { + var channel = (VoidBaseEventChannelSO)target; + channel.Raise(); + Debug.Log($"[EventChannelEditor] Raised: {target.name}"); + } + EditorGUI.EndDisabledGroup(); + + if (!Application.isPlaying) + { + EditorGUILayout.HelpBox("进入 Play Mode 后可点击 Raise 触发此事件。", MessageType.Info); + } + } + } + + /// + /// 为所有 BaseEventChannelSO<T> 子类提供 Inspector 内的订阅者数量显示和说明标签。 + /// 因泛型限制,Raise 按钮由具体类型的派生 Editor 提供(见下方注册器)。 + /// + [CustomEditor(typeof(ScriptableObject), true)] + public class GenericEventChannelSOEditor : UnityEditor.Editor + { + // 仅对 BaseEventChannelSO 子类生效 + private bool _isEventChannel; + + private void OnEnable() + { + var t = target.GetType(); + while (t != null && t != typeof(object)) + { + if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(BaseEventChannelSO<>)) + { + _isEventChannel = true; + break; + } + t = t.BaseType; + } + } + + public override void OnInspectorGUI() + { + if (!_isEventChannel) + { + DrawDefaultInspector(); + return; + } + + DrawDefaultInspector(); + + EditorGUILayout.Space(6); + EditorGUI.BeginDisabledGroup(true); + if (Application.isPlaying) + { + // 反射获取订阅者数量 + var field = target.GetType().GetField("OnEventRaised", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + if (field != null) + { + var del = field.GetValue(target) as Delegate; + int count = del?.GetInvocationList().Length ?? 0; + EditorGUILayout.LabelField("当前订阅者数量", count.ToString()); + } + } + EditorGUI.EndDisabledGroup(); + + if (!Application.isPlaying) + { + EditorGUILayout.HelpBox("进入 Play Mode 可查看实时订阅者数量。", MessageType.Info); + } + } + } +} diff --git a/Assets/Scripts/Editor/EventChannelEditor.cs.meta b/Assets/Scripts/Editor/EventChannelEditor.cs.meta new file mode 100644 index 0000000..bd77096 --- /dev/null +++ b/Assets/Scripts/Editor/EventChannelEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 39fd6fe0ebb5ceb4db85f82919217956 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/EventConfigEditor.cs b/Assets/Scripts/Editor/EventConfigEditor.cs new file mode 100644 index 0000000..cda98b3 --- /dev/null +++ b/Assets/Scripts/Editor/EventConfigEditor.cs @@ -0,0 +1,173 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using BaseGames.Animation; + +namespace BaseGames.Editor +{ + /// + /// AnimationEventConfigSO 自定义 Inspector(架构 §AnimationModule)。 + /// 功能: + /// - 以时间线色块可视化事件分布 + /// - 自动检测 Clip 长度漂移(超过 5 帧则显示警告) + /// - 验证归一化时间范围 [0, 1] + /// - 一键对事件按归一化时间排序 + /// + [CustomEditor(typeof(AnimationEventConfigSO))] + public class EventConfigEditor : UnityEditor.Editor + { + // ── 事件类型 → 色块颜色映射 ──────────────────────────────────────── + private static readonly Dictionary _colorMap = new() + { + { AnimationEventType.EnableHitBox, new Color(0.9f, 0.2f, 0.2f, 0.8f) }, // 红 + { AnimationEventType.DisableHitBox, new Color(0.9f, 0.2f, 0.2f, 0.8f) }, + { AnimationEventType.AttackImpact, new Color(0.9f, 0.2f, 0.2f, 0.8f) }, + + { AnimationEventType.EnableIFrame, new Color(0.2f, 0.8f, 0.2f, 0.8f) }, // 绿 + { AnimationEventType.DisableIFrame, new Color(0.2f, 0.8f, 0.2f, 0.8f) }, + + { AnimationEventType.Footstep, new Color(0.2f, 0.4f, 0.9f, 0.8f) }, // 蓝 + { AnimationEventType.PlaySFX, new Color(0.2f, 0.4f, 0.9f, 0.8f) }, + { AnimationEventType.LandImpact, new Color(0.2f, 0.4f, 0.9f, 0.8f) }, + { AnimationEventType.JumpLaunch, new Color(0.2f, 0.4f, 0.9f, 0.8f) }, + + { AnimationEventType.EnableParryWindow, new Color(0.9f, 0.8f, 0.1f, 0.8f) }, // 黄 + { AnimationEventType.DisableParryWindow,new Color(0.9f, 0.8f, 0.1f, 0.8f) }, + + { AnimationEventType.TriggerFeedback, new Color(0.6f, 0.2f, 0.9f, 0.8f) }, // 紫 + + { AnimationEventType.CancelWindowOpen, new Color(0.9f, 0.5f, 0.1f, 0.8f) }, // 橙 + { AnimationEventType.CancelWindowClose, new Color(0.9f, 0.5f, 0.1f, 0.8f) }, + + { AnimationEventType.SpawnProjectile, new Color(0.9f, 0.9f, 0.9f, 0.8f) }, // 白 + { AnimationEventType.RoarStart, new Color(0.9f, 0.9f, 0.9f, 0.8f) }, + { AnimationEventType.RoarEnd, new Color(0.9f, 0.9f, 0.9f, 0.8f) }, + { AnimationEventType.PhaseTwoStart, new Color(0.9f, 0.9f, 0.9f, 0.8f) }, + }; + + private const float TimelineHeight = 24f; + private const float MarkerWidth = 3f; + private const float DriftThresholdFrames = 5f; + + public override void OnInspectorGUI() + { + var config = (AnimationEventConfigSO)target; + serializedObject.Update(); + + // ── 时间线预览 ─────────────────────────────────────────────── + EditorGUILayout.LabelField("事件时间线预览", EditorStyles.boldLabel); + DrawTimeline(config); + EditorGUILayout.Space(4f); + + // ── 标准字段 ───────────────────────────────────────────────── + DrawDefaultInspector(); + EditorGUILayout.Space(4f); + + // ── 验证警告 ───────────────────────────────────────────────── + ValidateEntries(config); + + // ── Clip 长度漂移检测 ───────────────────────────────────────── + if (config.targetClip != null && config.ExpectedClipLength > 0f) + { + float fps = config.targetClip.frameRate; + float actualLen = config.targetClip.length; + float drift = Mathf.Abs(actualLen - config.ExpectedClipLength) * fps; + + if (drift > DriftThresholdFrames) + { + EditorGUILayout.HelpBox( + $"⚠ Clip 长度已变化 {drift:F1} 帧(期望 {config.ExpectedClipLength:F3}s," + + $"实际 {actualLen:F3}s)。\n请检查事件时机是否需要更新。", + MessageType.Warning); + } + } + + EditorGUILayout.Space(4f); + + // ── 操作按钮 ───────────────────────────────────────────────── + EditorGUILayout.BeginHorizontal(); + + if (GUILayout.Button("按时间排序")) + { + SortEvents(config); + } + + if (config.targetClip != null && GUILayout.Button("记录当前 Clip 长度")) + { + Undo.RecordObject(config, "记录 Clip 长度"); + config.ExpectedClipLength = config.targetClip.length; + EditorUtility.SetDirty(config); + } + + EditorGUILayout.EndHorizontal(); + + serializedObject.ApplyModifiedProperties(); + } + + // ── 时间线绘制 ──────────────────────────────────────────────────── + + private static void DrawTimeline(AnimationEventConfigSO config) + { + Rect rect = GUILayoutUtility.GetRect(GUIContent.none, GUIStyle.none, + GUILayout.Height(TimelineHeight), GUILayout.ExpandWidth(true)); + + // 背景轨道 + EditorGUI.DrawRect(rect, new Color(0.15f, 0.15f, 0.15f, 1f)); + + // 标尺刻度(每 10% 一条) + for (int i = 0; i <= 10; i++) + { + float x = rect.x + rect.width * i / 10f; + float h = (i % 5 == 0) ? rect.height * 0.6f : rect.height * 0.3f; + var tick = new Rect(x, rect.y + rect.height - h, 1f, h); + EditorGUI.DrawRect(tick, new Color(0.5f, 0.5f, 0.5f, 0.8f)); + } + + if (config.events == null) return; + + foreach (var entry in config.events) + { + float nx = Mathf.Clamp01(entry.normalizedTime); + float xPos = rect.x + rect.width * nx - MarkerWidth * 0.5f; + var markerRect = new Rect(xPos, rect.y + 2f, MarkerWidth, rect.height - 4f); + + Color color = _colorMap.TryGetValue(entry.eventType, out var c) + ? c + : new Color(0.8f, 0.8f, 0.8f, 0.8f); + + EditorGUI.DrawRect(markerRect, color); + } + } + + // ── 验证 ────────────────────────────────────────────────────────── + + private static void ValidateEntries(AnimationEventConfigSO config) + { + if (config.events == null) return; + + for (int i = 0; i < config.events.Length; i++) + { + float t = config.events[i].normalizedTime; + if (t < 0f || t > 1f) + { + EditorGUILayout.HelpBox( + $"事件 [{i}] {config.events[i].eventType}:" + + $"normalizedTime = {t:F3} 超出 [0, 1] 范围。", + MessageType.Error); + } + } + } + + // ── 排序 ────────────────────────────────────────────────────────── + + private static void SortEvents(AnimationEventConfigSO config) + { + if (config.events == null || config.events.Length < 2) return; + + Undo.RecordObject(config, "排序动画事件"); + System.Array.Sort(config.events, (a, b) => + a.normalizedTime.CompareTo(b.normalizedTime)); + EditorUtility.SetDirty(config); + } + } +} diff --git a/Assets/Scripts/Editor/EventConfigEditor.cs.meta b/Assets/Scripts/Editor/EventConfigEditor.cs.meta new file mode 100644 index 0000000..95d0bbe --- /dev/null +++ b/Assets/Scripts/Editor/EventConfigEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c992100309cc05a40bb06a3e23076c5b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/Map.meta b/Assets/Scripts/Editor/Map.meta new file mode 100644 index 0000000..e66df0b --- /dev/null +++ b/Assets/Scripts/Editor/Map.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 08a52815a08c8c3428ccb6a530171ddd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/Map/MapRoomDataEditor.cs b/Assets/Scripts/Editor/Map/MapRoomDataEditor.cs new file mode 100644 index 0000000..a6c2c9b --- /dev/null +++ b/Assets/Scripts/Editor/Map/MapRoomDataEditor.cs @@ -0,0 +1,140 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; +using BaseGames.World.Map; + +namespace BaseGames.Editor.Map +{ + /// + /// MapRoomDataSO 自定义编辑器(架构 15_MapShopModule §5)。 + /// 在 Scene View 中直接拖拽调整房间格子位置/大小;提供一键居中 SceneView 快捷按钮。 + /// 拖动自动吸附到整格精度;左下/右上角可独立拖动(含反转保护);支持 Undo。 + /// + [CustomEditor(typeof(MapRoomDataSO))] + public class MapRoomDataEditor : UnityEditor.Editor + { + private const float CELL_SIZE = 1f; // 每格在 Scene 中的世界单位尺寸 + + private static readonly Color FillColor = new Color(0.2f, 0.6f, 1f, 0.15f); + private static readonly Color OutlineColor = new Color(0.2f, 0.6f, 1f, 0.9f); + private static readonly Color HandleColor = new Color(1f, 0.85f, 0.2f, 1f); + + private static readonly GUIStyle LabelStyle = new GUIStyle + { + alignment = TextAnchor.MiddleCenter, + fontStyle = FontStyle.Bold, + normal = { textColor = Color.white }, + }; + + private MapRoomDataSO _target; + + private void OnEnable() => _target = (MapRoomDataSO)target; + + // ── Inspector ───────────────────────────────────────────────────────── + + public override void OnInspectorGUI() + { + DrawDefaultInspector(); + EditorGUILayout.Space(8); + + EditorGUILayout.HelpBox( + "在 Scene View 中可直接拖拽房间角点调整 GridPosition / GridSize。\n" + + "拖动自动吸附到 1 格精度,支持 Undo。", + MessageType.Info); + + if (GUILayout.Button("居中 Scene View 到此房间", GUILayout.Height(28))) + CenterSceneViewOnRoom(_target); + } + + // ── Scene GUI ───────────────────────────────────────────────────────── + + private void OnSceneGUI() + { + if (_target == null) return; + + Vector3 origin = new Vector3( + _target.GridPosition.x * CELL_SIZE, + _target.GridPosition.y * CELL_SIZE, 0f); + Vector3 size = new Vector3( + _target.GridSize.x * CELL_SIZE, + _target.GridSize.y * CELL_SIZE, 0f); + + // 绘制半透明矩形(Vector3[] 重载正确) + Handles.DrawSolidRectangleWithOutline(GetRectCorners(origin, size), FillColor, OutlineColor); + + // 房间 ID 标签(居中、加粗、白色) + Handles.Label(origin + size * 0.5f, + string.IsNullOrEmpty(_target.RoomId) ? "(No RoomId)" : _target.RoomId, + LabelStyle); + + // ── 双角控制点(左下 = BL,右上 = TR)──────────────────────────── + EditorGUI.BeginChangeCheck(); + Vector3 newBL = DragHandle(origin, "BL"); + Vector3 newTR = DragHandle(origin + size, "TR"); + + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(_target, "Resize MapRoom"); + + // 防反转:确保 BL ≤ TR + float minX = Mathf.Min(newBL.x, newTR.x); + float minY = Mathf.Min(newBL.y, newTR.y); + float maxX = Mathf.Max(newBL.x, newTR.x); + float maxY = Mathf.Max(newBL.y, newTR.y); + + _target.GridPosition = ToGrid(new Vector2(minX, minY)); + var newSize = ToGrid(new Vector2(maxX, maxY)) - _target.GridPosition; + _target.GridSize = new Vector2Int(Mathf.Max(1, newSize.x), Mathf.Max(1, newSize.y)); + + EditorUtility.SetDirty(_target); + } + } + + // ── 帮助方法 ────────────────────────────────────────────────────────── + + private static Vector3 DragHandle(Vector3 pos, string label) + { + float sz = HandleUtility.GetHandleSize(pos) * 0.12f; + Color prev = Handles.color; + Handles.color = HandleColor; + var result = Handles.FreeMoveHandle(pos, sz, Vector3.zero, Handles.DotHandleCap); + Handles.color = prev; + return SnapToGrid(result); + } + + /// 将世界坐标吸附到最近格点。 + private static Vector3 SnapToGrid(Vector3 world) + => new(Mathf.Round(world.x / CELL_SIZE) * CELL_SIZE, + Mathf.Round(world.y / CELL_SIZE) * CELL_SIZE, + 0f); + + private static Vector2Int ToGrid(Vector2 world) + => new(Mathf.RoundToInt(world.x / CELL_SIZE), + Mathf.RoundToInt(world.y / CELL_SIZE)); + + private static void CenterSceneViewOnRoom(MapRoomDataSO room) + { + if (room == null) return; + var sv = SceneView.lastActiveSceneView; + if (sv == null) return; + + Vector3 center = new Vector3( + (room.GridPosition.x + room.GridSize.x * 0.5f) * CELL_SIZE, + (room.GridPosition.y + room.GridSize.y * 0.5f) * CELL_SIZE, 0f); + + sv.Frame(new Bounds(center, new Vector3( + room.GridSize.x * CELL_SIZE * 2f, + room.GridSize.y * CELL_SIZE * 2f, 1f)), false); + } + + private static Vector3[] GetRectCorners(Vector3 origin, Vector3 size) + => new[] + { + origin, + origin + new Vector3(size.x, 0f, 0f), + origin + size, + origin + new Vector3(0f, size.y, 0f), + }; + } +} +#endif diff --git a/Assets/Scripts/Editor/Map/MapRoomDataEditor.cs.meta b/Assets/Scripts/Editor/Map/MapRoomDataEditor.cs.meta new file mode 100644 index 0000000..1477d8b --- /dev/null +++ b/Assets/Scripts/Editor/Map/MapRoomDataEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 168d8a104fffcaf4db9849cd8b2140f9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/NavSurfaceBakeShortcut.cs b/Assets/Scripts/Editor/NavSurfaceBakeShortcut.cs new file mode 100644 index 0000000..66daa39 --- /dev/null +++ b/Assets/Scripts/Editor/NavSurfaceBakeShortcut.cs @@ -0,0 +1,67 @@ +using UnityEditor; +using UnityEngine; +using PathBerserker2d; + +namespace BaseGames.Editor +{ + /// + /// 快捷键:BaseGames → Tools → Bake All NavSurfaces(Ctrl+Shift+B) + /// 烘焙当前场景中所有 PathBerserker2d NavSurface 的导航网格。 + /// 等效于在每个 NavSurface Inspector 中逐一点击 "Bake"。 + /// + public static class NavSurfaceBakeShortcut + { + [MenuItem("BaseGames/Tools/Bake All NavSurfaces %#b", priority = 100)] + public static void BakeAll() + { + var surfaces = Object.FindObjectsByType(FindObjectsSortMode.None); + if (surfaces.Length == 0) + { + Debug.Log("[NavSurfaceBake] 当前场景没有找到 NavSurface 组件。"); + return; + } + + int count = 0; + foreach (var surface in surfaces) + { + if (surface == null) continue; + + surface.StartBakeJob(); + EditorApplication.update -= MakeWatcher(surface); + EditorApplication.update += MakeWatcher(surface); + count++; + } + + Debug.Log($"[NavSurfaceBake] 开始烘焙 {count} 个 NavSurface……"); + } + + [MenuItem("BaseGames/Tools/Bake All NavSurfaces %#b", validate = true)] + private static bool BakeAllValidate() + { + // 仅在非 Play Mode 时可用(NavSurface.Bake 仅支持编辑器模式) + return !Application.isPlaying; + } + + // ── 每个 NavSurface 独立监听烘焙完成 ────────────────────────────── + + private static EditorApplication.CallbackFunction MakeWatcher(NavSurface surface) + { + EditorApplication.CallbackFunction watcher = null; + watcher = () => + { + if (surface == null || surface.BakeJob == null) + { + EditorApplication.update -= watcher; + return; + } + if (surface.BakeJob.IsFinished) + { + EditorApplication.update -= watcher; + EditorUtility.SetDirty(surface); + Debug.Log($"[NavSurfaceBake] ✓ {surface.name} 烘焙完成({surface.BakeJob.TotalBakeTime} ms)"); + } + }; + return watcher; + } + } +} diff --git a/Assets/Scripts/Editor/NavSurfaceBakeShortcut.cs.meta b/Assets/Scripts/Editor/NavSurfaceBakeShortcut.cs.meta new file mode 100644 index 0000000..723f231 --- /dev/null +++ b/Assets/Scripts/Editor/NavSurfaceBakeShortcut.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 291f3b33fb176b8469ebaaa8afa317ba +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/SceneScaffoldTools.cs b/Assets/Scripts/Editor/SceneScaffoldTools.cs new file mode 100644 index 0000000..cd9cc62 --- /dev/null +++ b/Assets/Scripts/Editor/SceneScaffoldTools.cs @@ -0,0 +1,904 @@ +using System.Collections.Generic; +using System.Reflection; +using BaseGames.Audio; +using BaseGames.Camera; +using BaseGames.Combat; +using BaseGames.Core; +using BaseGames.Core.Events; +using BaseGames.Core.Pool; +using BaseGames.Enemies; +using BaseGames.Input; +using BaseGames.Player; +using BaseGames.Player.States; +using BaseGames.UI; +using BaseGames.UI.HUD; +using BaseGames.UI.Menus; +using BaseGames.World; +using PathBerserker2d; +using Unity.Cinemachine; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEngine.Tilemaps; +using UnityEngine.UI; + +namespace BaseGames.Editor +{ + public static class SceneScaffoldTools + { + + [MenuItem("BaseGames/Tools/Scaffold Persistent Scene")] + public static void ScaffoldPersistentScene() + { + var report = new List(); + + EnsureEventChannelAssets(report); + + GameObject root = GetOrCreateRoot("[Persistent]"); + Transform services = GetOrCreateChild(root.transform, "[Services]"); + Transform input = GetOrCreateChild(root.transform, "[Input]"); + Transform camera = GetOrCreateChild(root.transform, "[Camera]"); + Transform ui = GetOrCreateChild(root.transform, "[UI]"); + + GameObject registrarGo = GetOrCreateChild(services, "GameServiceRegistrar").gameObject; + GameObject deathRespawnGo = GetOrCreateChild(services, "DeathRespawnService").gameObject; + GameObject sceneServiceGo = GetOrCreateChild(services, "SceneService").gameObject; + GameObject sceneLoaderGo = GetOrCreateChild(services, "SceneLoader").gameObject; + GameObject registryGo = GetOrCreateChild(services, "EventChannelRegistry").gameObject; + GameObject settingsGo = GetOrCreateChild(services, "SettingsManager").gameObject; + GameObject poolGo = GetOrCreateChild(services, "GlobalObjectPool").gameObject; + GameObject gameManagerGo = GetOrCreateChild(services, "GameManager").gameObject; + GameObject audioManagerGo = GetOrCreateChild(services, "AudioManager").gameObject; + + GameServiceRegistrar registrar = GetOrAddComponent(registrarGo); + DeathRespawnService deathRespawnService = GetOrAddComponent(deathRespawnGo); + SceneService sceneService = GetOrAddComponent(sceneServiceGo); + SceneLoader sceneLoader = GetOrAddComponent(sceneLoaderGo); + EventChannelRegistry registry = GetOrAddComponent(registryGo); + SettingsManager settingsManager = GetOrAddComponent(settingsGo); + GetOrAddComponent(poolGo); + GameManager gameManager = GetOrAddComponent(gameManagerGo); + AudioManager audioManager = GetOrAddComponent(audioManagerGo); + + GameObject inputHolderGo = GetOrCreateChild(input, "InputReaderHolder").gameObject; + Object inputReaderAsset = FindFirstAssetByType("InputReader", "InputReaderSO"); + if (inputReaderAsset == null) + inputReaderAsset = EnsureInputReaderAsset(report); + + InputReaderBootstrap inputBootstrap = GetOrAddComponent(inputHolderGo); + AssignReference(inputBootstrap, "_inputReader", inputReaderAsset, report); + if (inputReaderAsset != null) + { + AssignReference(inputReaderAsset, "_onPauseRequested", FindFirstAssetByType("EVT_PauseRequested"), report); + AssignReference(inputReaderAsset, "_inputActions", FindFirstAssetWithExtension(".inputactions", "PlayerInputActions", "InputActions"), report); + } + if (inputReaderAsset == null) + report.Add("未找到 InputReaderSO 资产,InputReaderBootstrap 将保持空引用。请补齐 Assets/Data/Input/InputReader.asset。"); + + GameObject mainCameraGo = GetOrCreateChild(camera, "Main Camera").gameObject; + UnityEngine.Camera mainCamera = GetOrAddComponent(mainCameraGo); + mainCamera.orthographic = false; + mainCamera.fieldOfView = 60f; + mainCameraGo.tag = "MainCamera"; + GetOrAddComponent(mainCameraGo); + CinemachineBrain brain = GetOrAddComponent(mainCameraGo); + + GameObject cameraStateGo = GetOrCreateChild(camera, "CameraStateController").gameObject; + CameraStateController cameraStateController = GetOrAddComponent(cameraStateGo); + CinemachineImpulseSource impulseSource = GetOrAddComponent(cameraStateGo); + + GameObject uiRootGo = GetOrCreateChild(ui, "UIRoot").gameObject; + UIManager uiManager = GetOrAddComponent(uiRootGo); + + GameObject hudCanvasGo = GetOrCreateCanvas(uiRootGo.transform, "HUD Canvas", 0); + GameObject hudRootGo = GetOrCreateChild(hudCanvasGo.transform, "HUDRoot").gameObject; + HUDController hudController = GetOrAddComponent(hudRootGo); + + GameObject pauseRootGo = GetOrCreateChild(uiRootGo.transform, "PauseMenuRoot").gameObject; + GameObject settingsRootGo = GetOrCreateChild(uiRootGo.transform, "SettingsRoot").gameObject; + GameObject mapRootGo = GetOrCreateChild(uiRootGo.transform, "MapRoot").gameObject; + GameObject shopRootGo = GetOrCreateChild(uiRootGo.transform, "ShopRoot").gameObject; + pauseRootGo.SetActive(false); + settingsRootGo.SetActive(false); + mapRootGo.SetActive(false); + shopRootGo.SetActive(false); + + GameObject deathCanvasGo = GetOrCreateCanvas(uiRootGo.transform, "DeathScreen Canvas", 10); + GameObject deathRootGo = GetOrCreateChild(deathCanvasGo.transform, "DeathScreenRoot").gameObject; + DeathScreenController deathScreenController = GetOrAddComponent(deathRootGo); + deathRootGo.SetActive(false); + GameObject respawnButtonGo = GetOrCreateChild(deathRootGo.transform, "RespawnButton").gameObject; + GetOrAddComponent(respawnButtonGo); + Button respawnButton = GetOrAddComponent