diff --git a/Assets/Model/建筑/地图.meta b/Assets/Model/建筑/地图.meta deleted file mode 100644 index 0d94932..0000000 --- a/Assets/Model/建筑/地图.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: XS9MvS7/U3m6dqDMGv9c/gMis+P/vXKeKPOgPN5GaQgqPD66VCPaI9g= -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Model/建筑/地图/地图.mat b/Assets/Model/建筑/地图/地图.mat deleted file mode 100644 index 4e0c75f..0000000 --- a/Assets/Model/建筑/地图/地图.mat +++ /dev/null @@ -1,83 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:yousandi.cn,2023: ---- !u!21 &2100000 -Material: - serializedVersion: 8 - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: "\u5730\u56FE" - m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} - m_Parent: {fileID: 0} - m_ModifiedSerializedProperties: 0 - m_ValidKeywords: [] - m_InvalidKeywords: [] - m_LightmapFlags: 4 - m_EnableInstancingVariants: 0 - m_DoubleSidedGI: 0 - m_CustomRenderQueue: -1 - stringTagMap: {} - disabledShaderPasses: [] - m_LockedProperties: - m_SavedProperties: - serializedVersion: 3 - m_TexEnvs: - - _BumpMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _DetailAlbedoMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _DetailMask: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _DetailNormalMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _EmissionMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _MainTex: - m_Texture: {fileID: 2800000, guid: c8a4cdbb7cdc01545aaa9d2d0b0a600f, type: 3} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _MetallicGlossMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _OcclusionMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _ParallaxMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - m_Ints: [] - m_Floats: - - _BumpScale: 1 - - _Cutoff: 0.5 - - _DetailNormalMapScale: 1 - - _DstBlend: 0 - - _GlossMapScale: 1 - - _Glossiness: 0.1 - - _GlossyReflections: 1 - - _Metallic: 0 - - _Mode: 0 - - _OcclusionStrength: 1 - - _Parallax: 0.02 - - _SmoothnessTextureChannel: 0 - - _SpecularHighlights: 1 - - _SrcBlend: 1 - - _UVSec: 0 - - _ZWrite: 1 - m_Colors: - - _Color: {r: 1, g: 1, b: 1, a: 1} - - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} - m_BuildTextureStacks: [] diff --git a/Assets/Model/建筑/地图/地图.mat.meta b/Assets/Model/建筑/地图/地图.mat.meta deleted file mode 100644 index 4f98fe7..0000000 --- a/Assets/Model/建筑/地图/地图.mat.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: DC8X4yivUH0YWIxSi3xxgZIYGN3DwMGCn0PJg22u/mzP6FQrGaqOtlY= -NativeFormatImporter: - externalObjects: {} - mainObjectFileID: 2100000 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Model/建筑/地图/地形.fbx b/Assets/Model/建筑/地图/地形.fbx deleted file mode 100644 index 817733a..0000000 Binary files a/Assets/Model/建筑/地图/地形.fbx and /dev/null differ diff --git a/Assets/Model/建筑/地图/地形.fbx.meta b/Assets/Model/建筑/地图/地形.fbx.meta deleted file mode 100644 index 54d27bb..0000000 --- a/Assets/Model/建筑/地图/地形.fbx.meta +++ /dev/null @@ -1,210 +0,0 @@ -fileFormatVersion: 2 -guid: Bn0c53+vAnn1MuHWRIWqU/OKht8DK/67DdDi52Zxz6Vg6v5KX+owqds= -ModelImporter: - serializedVersion: 22361 - internalIDToNameTable: [] - externalObjects: - - first: - type: UnityEngine:Material - assembly: UnityEngine.CoreModule - name: No Name - second: {fileID: 2100000, guid: 3e8f22364fa4c5744ade699b9db58a42, type: 2} - materials: - materialImportMode: 1 - materialName: 0 - materialSearch: 1 - materialLocation: 1 - animations: - legacyGenerateAnimations: 4 - bakeSimulation: 0 - resampleCurves: 1 - optimizeGameObjects: 0 - removeConstantScaleCurves: 0 - motionNodeName: - rigImportErrors: - rigImportWarnings: - animationImportErrors: - animationImportWarnings: - animationRetargetingWarnings: - animationDoRetargetingWarnings: 0 - importAnimatedCustomProperties: 0 - importConstraints: 0 - animationCompression: 1 - aCLCompressionLevel: 3 - aCLCurvePrecision: 0.01 - aCLFastSampleMode: 1 - animationRotationError: 0.5 - animationPositionError: 0.5 - animationScaleError: 0.5 - animationWrapMode: 0 - extraExposedTransformPaths: [] - extraUserProperties: [] - clipAnimations: [] - isReadable: 0 - tuanjieGIEnable: 0 - tuanjieGIGenSDFFactor: 1 - tuanjieGIGenSDFBias: 0.25 - tuanjieGIDoubleSidedMode: 0 - tuanjieGIGenFacetNums: 12 - meshes: - lODScreenPercentages: [] - globalScale: 100 - meshCompression: 0 - totalVerticesCount: 10615 - totalTrianglesCount: 6000 - addColliders: 0 - useSRGBMaterialColor: 1 - sortHierarchyByName: 1 - importPhysicalCameras: 1 - importVisibility: 1 - importBlendShapes: 1 - importCameras: 1 - importLights: 1 - nodeNameCollisionStrategy: 1 - fileIdsGeneration: 2 - swapUVChannels: 0 - generateSecondaryUV: 0 - useFileUnits: 1 - keepQuads: 0 - weldVertices: 1 - bakeAxisConversion: 0 - preserveHierarchy: 0 - skinWeightsMode: 0 - maxBonesPerVertex: 4 - minBoneWeight: 0.001 - optimizeBones: 1 - meshOptimizationFlags: -1 - indexFormat: 0 - secondaryUVAngleDistortion: 8 - secondaryUVAreaDistortion: 15.000001 - secondaryUVHardAngle: 88 - secondaryUVMarginMethod: 1 - secondaryUVMinLightmapResolution: 40 - secondaryUVMinObjectScale: 1 - secondaryUVPackMargin: 4 - useFileScale: 1 - strictVertexDataChecks: 0 - virtualGeometry: - virtualGeometry: 0 - useTransparent: 0 - setVGForeachMesh: 0 - isFirstUse: 1 - virtualGeometrySettings: - generateGeometryBuffer: 0 - surfaceCompensation: 0 - allMeshUse: 1 - ignoreSimpleMesh: 0 - transformScaleState: 0 - isPossibleOverflow: 0 - hasBoneData: 0 - isBoneCountOverflow: 0 - virtualGeometryPlatformSettings: - - serializedVersion: 0 - overridden: 0 - ignorePlatformSupport: 0 - buildTarget: DefaultVirtualGeometryPlatform - positionPrecision: -2147483648 - normalPrecision: -2147483648 - tangentPrecision: -2147483648 - boneWeightPrecision: -2147483648 - detailedTriangleCount: 0 - detailedVertexCount: 0 - rootPageSize: 0 - streamablePageSize: 0 - reserveTrianglePercent: 100 - reserveDetailLevel: 0 - - serializedVersion: 0 - overridden: 0 - ignorePlatformSupport: 0 - buildTarget: Standalone - positionPrecision: -2147483648 - normalPrecision: -2147483648 - tangentPrecision: -2147483648 - boneWeightPrecision: -2147483648 - detailedTriangleCount: 0 - detailedVertexCount: 0 - rootPageSize: 0 - streamablePageSize: 0 - reserveTrianglePercent: 100 - reserveDetailLevel: 0 - - serializedVersion: 0 - overridden: 0 - ignorePlatformSupport: 0 - buildTarget: iPhone - positionPrecision: -2147483648 - normalPrecision: -2147483648 - tangentPrecision: -2147483648 - boneWeightPrecision: -2147483648 - detailedTriangleCount: 0 - detailedVertexCount: 0 - rootPageSize: 0 - streamablePageSize: 0 - reserveTrianglePercent: 100 - reserveDetailLevel: 0 - - serializedVersion: 0 - overridden: 0 - ignorePlatformSupport: 0 - buildTarget: Android - positionPrecision: -2147483648 - normalPrecision: -2147483648 - tangentPrecision: -2147483648 - boneWeightPrecision: -2147483648 - detailedTriangleCount: 0 - detailedVertexCount: 0 - rootPageSize: 0 - streamablePageSize: 0 - reserveTrianglePercent: 100 - reserveDetailLevel: 0 - selectedVGMeshes: - - index: 0 - meshName: model - lodLevel: 0 - meshGeometryBuffer: 0 - supportVGMeshes: - - index: 0 - meshName: model - lodLevel: 0 - meshGeometryBuffer: 0 - tangentSpace: - normalSmoothAngle: 60 - normalImportMode: 0 - tangentImportMode: 3 - normalCalculationMode: 4 - legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0 - blendShapeNormalImportMode: 1 - normalSmoothingSource: 0 - referencedClips: [] - importAnimation: 1 - overrideDefaultPose: 0 - humanDescription: - serializedVersion: 3 - human: [] - skeleton: [] - namedPoseList: - - name: Default - skeleton: [] - armTwist: 0.5 - foreArmTwist: 0.5 - upperLegTwist: 0.5 - legTwist: 0.5 - armStretch: 0.05 - legStretch: 0.05 - feetSpacing: 0 - globalScale: 1 - rootMotionBoneName: - defaultPoseIndex: -1 - hasTranslationDoF: 0 - hasExtraRoot: 0 - skeletonHasParents: 1 - lastHumanDescriptionAvatarSource: {instanceID: 0} - autoGenerateAvatarMappingIfUnspecified: 1 - animationType: 2 - humanoidOversampling: 1 - avatarSetup: 0 - addHumanoidExtraRootOnlyWhenUsingAvatar: 1 - importBlendShapeDeformPercent: 1 - remapMaterialsIfMaterialImportModeIsNone: 0 - additionalBone: 0 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Model/建筑/地图/地形.png b/Assets/Model/建筑/地图/地形.png deleted file mode 100644 index 00d24af..0000000 Binary files a/Assets/Model/建筑/地图/地形.png and /dev/null differ diff --git a/Assets/Model/建筑/城堡/红色城堡.fbx.meta b/Assets/Model/建筑/城堡/红色城堡.fbx.meta index d8a753a..caa4295 100644 --- a/Assets/Model/建筑/城堡/红色城堡.fbx.meta +++ b/Assets/Model/建筑/城堡/红色城堡.fbx.meta @@ -48,7 +48,7 @@ ModelImporter: tuanjieGIGenFacetNums: 12 meshes: lODScreenPercentages: [] - globalScale: 10 + globalScale: 6 meshCompression: 0 totalVerticesCount: 16991 totalTrianglesCount: 9992 @@ -190,7 +190,7 @@ ModelImporter: armStretch: 0.05 legStretch: 0.05 feetSpacing: 0 - globalScale: 0.099999994 + globalScale: 0.06 rootMotionBoneName: defaultPoseIndex: -1 hasTranslationDoF: 0 diff --git a/Assets/Model/建筑/城堡/蓝色城堡.fbx.meta b/Assets/Model/建筑/城堡/蓝色城堡.fbx.meta index 5366ca7..22830f4 100644 --- a/Assets/Model/建筑/城堡/蓝色城堡.fbx.meta +++ b/Assets/Model/建筑/城堡/蓝色城堡.fbx.meta @@ -48,7 +48,7 @@ ModelImporter: tuanjieGIGenFacetNums: 12 meshes: lODScreenPercentages: [] - globalScale: 10 + globalScale: 6 meshCompression: 0 totalVerticesCount: 9600 totalTrianglesCount: 4998 @@ -190,7 +190,7 @@ ModelImporter: armStretch: 0.05 legStretch: 0.05 feetSpacing: 0 - globalScale: 0.099999994 + globalScale: 0.06 rootMotionBoneName: defaultPoseIndex: -1 hasTranslationDoF: 0 diff --git a/Assets/Model/建筑/水晶矿/水晶矿.fbx.meta b/Assets/Model/建筑/水晶矿/水晶矿.fbx.meta index cf998cc..164e55b 100644 --- a/Assets/Model/建筑/水晶矿/水晶矿.fbx.meta +++ b/Assets/Model/建筑/水晶矿/水晶矿.fbx.meta @@ -48,7 +48,7 @@ ModelImporter: tuanjieGIGenFacetNums: 12 meshes: lODScreenPercentages: [] - globalScale: 7 + globalScale: 5 meshCompression: 0 totalVerticesCount: 9513 totalTrianglesCount: 5000 @@ -190,7 +190,7 @@ ModelImporter: armStretch: 0.05 legStretch: 0.05 feetSpacing: 0 - globalScale: 0.07 + globalScale: 0.049999997 rootMotionBoneName: defaultPoseIndex: -1 hasTranslationDoF: 0 diff --git a/Assets/Model/建筑/随机出兵点/随机出兵点.fbx.meta b/Assets/Model/建筑/随机出兵点/随机出兵点.fbx.meta index c2a81bc..fa8fcb8 100644 --- a/Assets/Model/建筑/随机出兵点/随机出兵点.fbx.meta +++ b/Assets/Model/建筑/随机出兵点/随机出兵点.fbx.meta @@ -48,7 +48,7 @@ ModelImporter: tuanjieGIGenFacetNums: 12 meshes: lODScreenPercentages: [] - globalScale: 8 + globalScale: 5 meshCompression: 0 totalVerticesCount: 7065 totalTrianglesCount: 4998 @@ -190,7 +190,7 @@ ModelImporter: armStretch: 0.05 legStretch: 0.05 feetSpacing: 0 - globalScale: 0.08 + globalScale: 0.049999997 rootMotionBoneName: defaultPoseIndex: -1 hasTranslationDoF: 0 diff --git a/Assets/Picture/火球.png b/Assets/Picture/火球.png new file mode 100644 index 0000000..e272f7c Binary files /dev/null and b/Assets/Picture/火球.png differ diff --git a/Assets/Model/建筑/地图/地形.png.meta b/Assets/Picture/火球.png.meta similarity index 94% rename from Assets/Model/建筑/地图/地形.png.meta rename to Assets/Picture/火球.png.meta index 06953c9..b6c163f 100644 --- a/Assets/Model/建筑/地图/地形.png.meta +++ b/Assets/Picture/火球.png.meta @@ -1,12 +1,12 @@ fileFormatVersion: 2 -guid: XHJOsXn5ASkhOJr0MEYSQYy6himVjaf03uXeT/ZsugbZpNL2iCO7gZQ= +guid: Dyge4C35Vnnq9iTG5ZmWmqf2Oxk9jbr1Rdztf1BQsLGOYai/r+CQlV8= TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 14 mipmaps: mipMapMode: 0 - enableMipMap: 1 + enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 @@ -40,13 +40,13 @@ TextureImporter: filterMode: 1 aniso: 1 mipBias: 0 - wrapU: 0 - wrapV: 0 + wrapU: 1 + wrapV: 1 wrapW: 0 - nPOTScale: 1 + nPOTScale: 0 lightmap: 0 compressionQuality: 50 - spriteMode: 0 + spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 @@ -55,9 +55,9 @@ TextureImporter: spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 1 alphaUsage: 1 - alphaIsTransparency: 0 + alphaIsTransparency: 1 spriteTessellationDetail: -1 - textureType: 0 + textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 @@ -72,7 +72,7 @@ TextureImporter: platformSettings: - serializedVersion: 3 buildTarget: DefaultTexturePlatform - maxTextureSize: 512 + maxTextureSize: 64 maxPlaceholderSize: 32 resizeAlgorithm: 0 textureFormat: -1 @@ -160,7 +160,7 @@ TextureImporter: outline: [] physicsShape: [] bones: [] - spriteID: + spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: diff --git a/Assets/Scenes/Chapter1/CoreBattleTest.scene b/Assets/Scenes/Chapter1/CoreBattleTest.scene index 08884f7..aa0993f 100644 Binary files a/Assets/Scenes/Chapter1/CoreBattleTest.scene and b/Assets/Scenes/Chapter1/CoreBattleTest.scene differ diff --git a/Assets/Script/ChaosGateSpawner.cs b/Assets/Script/ChaosGateSpawner.cs new file mode 100644 index 0000000..1a7a569 --- /dev/null +++ b/Assets/Script/ChaosGateSpawner.cs @@ -0,0 +1,130 @@ +using UnityEngine; + +/// +/// 混沌之门:被占领后按固定间隔从所属阵营卡组随机出兵。 +/// +[RequireComponent(typeof(CrystalMineCapture))] +public class ChaosGateSpawner : MonoBehaviour +{ + [Header("出兵配置")] + [Min(1f)] public float spawnInterval = 12f; + [Min(0f)] public float spawnRadius = 1.5f; + public bool spawnImmediatelyOnCapture = false; + + [Header("蓝方卡组")] + [SerializeField] private string[] blueDeckDataNames = + { + "人类士兵Data", "弓箭手Data", "盾卫Data", "精锐士兵Data", + "法师Data", "狂暴狼人Data", "石巨人Data", "弓箭猎人Data" + }; + + private CrystalMineCapture capture; + private UnitTeam unitTeam; + private float spawnTimer; + private int previousTeamId = 2; + + private void Awake() + { + capture = GetComponent(); + unitTeam = GetComponent(); + spawnTimer = spawnInterval; + } + + private void Update() + { + if (unitTeam == null) + { + unitTeam = GetComponent(); + if (unitTeam == null) return; + } + + int currentTeamId = unitTeam.teamId; + if (currentTeamId != previousTeamId) + { + previousTeamId = currentTeamId; + spawnTimer = spawnImmediatelyOnCapture ? 0f : spawnInterval; + } + + if (currentTeamId != 0 && currentTeamId != 1) + { + spawnTimer = spawnInterval; + return; + } + + spawnTimer -= Time.deltaTime; + if (spawnTimer > 0f) return; + + if (TrySpawn(currentTeamId)) + spawnTimer = spawnInterval; + else + spawnTimer = 1f; + } + + private bool TrySpawn(int teamId) + { + Vector3 position = transform.position; + position.x += Random.Range(-spawnRadius, spawnRadius); + position.z += Random.Range(-spawnRadius, spawnRadius); + + if (teamId == 1) + { + EnemyCastleAI enemyAI = FindObjectOfType(); + return enemyAI != null && enemyAI.TrySpawnFromPortal(position, transform.rotation); + } + + CharacterData card = ChooseBlueCard(); + if (card == null || CastleSpawnManager.Instance == null) + return false; + + return SpawnBlueUnit(card, position, transform.rotation); + } + + private CharacterData ChooseBlueCard() + { + CharacterData[] cards = new CharacterData[blueDeckDataNames.Length]; + int validCount = 0; + foreach (string dataName in blueDeckDataNames) + { + CharacterData data = Resources.Load("Data/" + dataName); + if (data != null && data.unitPrefab != null) + cards[validCount++] = data; + } + + if (validCount == 0) return null; + + float totalWeight = 0f; + for (int i = 0; i < validCount; i++) + totalWeight += 1f / Mathf.Max(1, cards[i].manaCost); + + float roll = Random.value * totalWeight; + for (int i = 0; i < validCount; i++) + { + roll -= 1f / Mathf.Max(1, cards[i].manaCost); + if (roll <= 0f) return cards[i]; + } + return cards[validCount - 1]; + } + + private bool SpawnBlueUnit(CharacterData data, Vector3 position, Quaternion rotation) + { + if (ManaSystem.Instance == null || !ManaSystem.Instance.TryConsumeMana(data.manaCost)) + return false; + + GameObject unit = Instantiate(data.unitPrefab, position, rotation); + UnitTeam team = unit.GetComponent(); + if (team != null) + { + team.teamId = 0; + unit.tag = "Friend"; + } + + UnitBase unitBase = unit.GetComponent(); + if (unitBase != null) + { + unitBase.InitializeFromPlayerData(data.GetMaxHealth(1), data.GetAttackPower(1), data.GetMoveSpeed(1)); + } + + return true; + } + +} diff --git a/Assets/Script/ChaosGateSpawner.cs.meta b/Assets/Script/ChaosGateSpawner.cs.meta new file mode 100644 index 0000000..150ed36 --- /dev/null +++ b/Assets/Script/ChaosGateSpawner.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: WnsW5HmqByrprA9NWfcR8/RRH5kmtPm0TGQevEsLKuNlTEbLzjkqpVA= +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Script/CrystalMineCapture.cs b/Assets/Script/CrystalMineCapture.cs index 3befd7c..5d52511 100644 --- a/Assets/Script/CrystalMineCapture.cs +++ b/Assets/Script/CrystalMineCapture.cs @@ -10,7 +10,12 @@ public class CrystalMineCapture : MonoBehaviour [Tooltip("是否自动添加中立阵营(teamId=2)。拆家版关闭以避免单位自动锁敌水晶矿")] public bool autoSetNeutralTeam = true; - [Header("水晶矿光球")] + [Header("资源产出")] + [Tooltip("占领后是否按固定间隔给所属阵营增加能量")] + public bool producesMana = true; + [Min(1f)] public float manaInterval = 8f; + [Min(1)] public int manaAmount = 1; + [SerializeField] private AdjustableGlowOrb glowOrb; [Tooltip("混沌裂缝传送门(随机出兵点),随争夺进度同步变色,可留空")] [SerializeField] private RiftPortalVisual riftVisual; @@ -31,6 +36,7 @@ public class CrystalMineCapture : MonoBehaviour private Image redFill; private RectTransform marker; private UnitTeam unitTeam; + private float manaTimer; private static Sprite roundedSprite; private static readonly int BaseColorId = Shader.PropertyToID("_Color"); @@ -76,6 +82,8 @@ public class CrystalMineCapture : MonoBehaviour private void LateUpdate() { + UpdateManaProduction(); + if (barRect == null) return; barRect.position = transform.position + Vector3.up * GetBarHeight(); @@ -109,6 +117,30 @@ public class CrystalMineCapture : MonoBehaviour return barHeightCache.Value; } + private void UpdateManaProduction() + { + if (!producesMana || unitTeam == null || (unitTeam.teamId != 0 && unitTeam.teamId != 1)) + { + manaTimer = 0f; + return; + } + + manaTimer += Time.deltaTime; + while (manaTimer >= manaInterval) + { + manaTimer -= manaInterval; + if (unitTeam.teamId == 0) + { + ManaSystem.Instance?.AddMana(manaAmount); + } + else + { + EnemyCastleAI enemyAI = FindObjectOfType(); + enemyAI?.AddMana(manaAmount); + } + } + } + public bool ReceiveCaptureDamage(float damage, GameObject attacker) { if (damage <= 0f || attacker == null) return false; diff --git a/Assets/Script/EnemyCastleAI.cs b/Assets/Script/EnemyCastleAI.cs index 7e7e7b4..ecf125a 100644 --- a/Assets/Script/EnemyCastleAI.cs +++ b/Assets/Script/EnemyCastleAI.cs @@ -22,9 +22,17 @@ public class EnemyCastleAI : MonoBehaviour [SerializeField] private float enemyUnitWeight = 1.5f; [SerializeField] private float laneRotationWeight = 0.7f; - [Header("卡组")] + [Header("卡组轮转")] + [SerializeField] private int visibleSlotCount = 4; + [SerializeField] private string[] deckDataNames = + { + "人类士兵Data", "弓箭手Data", "盾卫Data", "精锐士兵Data", + "法师Data", "狂暴狼人Data", "石巨人Data", "弓箭猎人Data" + }; [SerializeField] private CharacterData[] deck; + private readonly List hand = new List(); + private readonly Queue drawQueue = new Queue(); private Transform redSpawnPoint; private AttackTargetController targetController; private float currentMana; @@ -40,9 +48,9 @@ public class EnemyCastleAI : MonoBehaviour redSpawnPoint = FindRedSpawnPoint(); targetController = AttackTargetController.Instance; currentMana = maxMana; - if (deck == null || deck.Length == 0) - deck = Resources.LoadAll("Data"); - initialized = redSpawnPoint != null && deck != null && deck.Length > 0; + BuildDeck(); + BuildHand(); + initialized = redSpawnPoint != null && hand.Count > 0; if (!initialized) Debug.LogWarning("EnemyCastleAI 初始化失败:缺少红方出兵点或角色卡组。"); } @@ -65,10 +73,11 @@ public class EnemyCastleAI : MonoBehaviour int lane = ChooseLane(); CharacterData card = ChooseCard(lane); - if (card == null || card.manaCost > currentMana) return; + if (card == null) return; if (Spawn(card, lane)) { currentMana -= card.manaCost; + RotatePlayedCard(card); spawnTimer = minimumSpawnInterval; if (lane == lastLane) consecutiveLaneSpawns++; else consecutiveLaneSpawns = 1; @@ -76,6 +85,50 @@ public class EnemyCastleAI : MonoBehaviour } } + public void AddMana(int amount) + { + if (amount <= 0) return; + currentMana = Mathf.Min(maxMana, currentMana + amount); + } + + public int CurrentMana => Mathf.FloorToInt(currentMana); + + public bool TrySpawnFromPortal(Vector3 spawnPosition, Quaternion spawnRotation) + { + if (!initialized) return false; + + List affordable = new List(); + foreach (CharacterData card in hand) + { + if (card != null && card.unitPrefab != null && card.manaCost <= currentMana) + affordable.Add(card); + } + + if (affordable.Count == 0) return false; + + CharacterData selected = SelectWeightedCard(affordable); + if (!SpawnAt(selected, spawnPosition, spawnRotation, teamId)) return false; + + currentMana -= selected.manaCost; + RotatePlayedCard(selected); + return true; + } + + private CharacterData SelectWeightedCard(List cards) + { + float totalWeight = 0f; + foreach (CharacterData card in cards) + totalWeight += 1f / Mathf.Max(1, card.manaCost); + + float roll = Random.value * totalWeight; + foreach (CharacterData card in cards) + { + roll -= 1f / Mathf.Max(1, card.manaCost); + if (roll <= 0f) return card; + } + return cards[cards.Count - 1]; + } + private int ChooseLane() { float left = ScoreLane(leftLaneX, 0); @@ -126,24 +179,71 @@ public class EnemyCastleAI : MonoBehaviour { CharacterData best = null; float bestScore = float.NegativeInfinity; - foreach (var card in deck) + foreach (var card in hand) { if (card == null || card.unitPrefab == null || card.manaCost > currentMana) continue; float score = Random.Range(0f, 1f); if (card.manaCost <= 2) score += 2f; if (currentMana >= 7 && card.manaCost >= 5) score += 2f; if (lane != 1 && card.manaCost <= 4) score += 0.5f; - if (score > bestScore) { bestScore = score; best = card; } + if (score > bestScore) + { + bestScore = score; + best = card; + } } return best; } - private bool Spawn(CharacterData data, int lane) + private void BuildDeck() { - GameObject unit = Instantiate(data.unitPrefab, redSpawnPoint.position, redSpawnPoint.rotation); + if (deck != null && deck.Length > 0) return; + + var loadedDeck = new List(); + foreach (string dataName in deckDataNames) + { + CharacterData data = Resources.Load("Data/" + dataName); + if (data == null) + { + Debug.LogWarning($"敌方卡组缺少角色数据: Resources/Data/{dataName}.asset"); + continue; + } + loadedDeck.Add(data); + } + deck = loadedDeck.ToArray(); + } + + private void BuildHand() + { + hand.Clear(); + drawQueue.Clear(); + if (deck == null) return; + + int handCount = Mathf.Min(visibleSlotCount, deck.Length); + for (int i = 0; i < handCount; i++) + hand.Add(deck[i]); + for (int i = handCount; i < deck.Length; i++) + drawQueue.Enqueue(deck[i]); + } + + private void RotatePlayedCard(CharacterData playedCard) + { + int handIndex = hand.IndexOf(playedCard); + if (handIndex < 0 || drawQueue.Count == 0) return; + + hand[handIndex] = drawQueue.Dequeue(); + drawQueue.Enqueue(playedCard); + } + + private bool SpawnAt(CharacterData data, Vector3 position, Quaternion rotation, int spawnTeamId) + { + GameObject unit = Instantiate(data.unitPrefab, position, rotation); UnitTeam team = unit.GetComponent(); - if (team != null) team.teamId = teamId; - unit.tag = "Enemy"; + if (team != null) + { + team.teamId = spawnTeamId; + unit.tag = spawnTeamId == 0 ? "Friend" : "Enemy"; + } UnitBase unitBase = unit.GetComponent(); if (unitBase != null) @@ -151,6 +251,13 @@ public class EnemyCastleAI : MonoBehaviour unitBase.InitializeFromPlayerData(data.GetMaxHealth(1), data.GetAttackPower(1), data.GetMoveSpeed(1)); } + return true; + } + + private bool Spawn(CharacterData data, int lane) + { + if (!SpawnAt(data, redSpawnPoint.position, redSpawnPoint.rotation, teamId)) return false; + if (targetController != null) { targetController.redTarget = FindLaneTarget(lane); diff --git a/Assets/Script/UnitBase.cs b/Assets/Script/UnitBase.cs index ee2fbb8..1a84c25 100644 --- a/Assets/Script/UnitBase.cs +++ b/Assets/Script/UnitBase.cs @@ -207,8 +207,17 @@ public class UnitBase : MonoBehaviour { navAgent.enabled = false; } - + animator.SetInteger("speed", 0); + + // 目标死亡、失活或离开攻击范围时,解除锁定并重新寻敌。 + if (!IsTargetValid(target) || !IsWithinAttackReach(target)) + { + target = null; + state = State.Alert; + return; + } + attackElapsedTime += Time.deltaTime; CheckAttackAnimationFinished(); return; @@ -221,8 +230,10 @@ public class UnitBase : MonoBehaviour return; } - //目标决策:周围有敌人打敌人,否则直奔全局攻击标志建筑(默认敌方主城堡) + // 移动/追踪状态持续重新评估目标,确保相遇时能及时攻击敌方单位。 + // Attack 状态在上方单独处理,因此不会被这里打断或切换目标。 target = FindTarget(); + if (target != null) { // 召唤师模式:不主动追击,站定且攻击开关开启时才攻击射程内目标。 @@ -247,21 +258,6 @@ public class UnitBase : MonoBehaviour } else { - //到达使命建筑发现是我方的(增援/占领完成):本单位使命转向敌方主城堡,不影响全局标志 - if (target.CompareTag("Building") && IsFriendlyBuilding(target)) - { - var myTeam = GetComponent(); - GameObject castle = AttackTargetController.Instance != null && myTeam != null - ? AttackTargetController.Instance.GetEnemyCastleOfTeam(myTeam.teamId) - : null; - if (castle != null) - { - missionTarget = castle; // 使命完成,改打主城堡(单位自身,一次性的) - target = castle; - move(castle.transform.position - transform.position); - return; - } - } attack(target); } @@ -279,6 +275,24 @@ public class UnitBase : MonoBehaviour public GameObject Target => target; + private bool IsTargetValid(GameObject candidate) + { + if (candidate == null || !candidate.activeInHierarchy) + { + return false; + } + + WorldHealthBar healthBar = candidate.GetComponentInParent(); + if (healthBar != null && healthBar.IsDead) + { + return false; + } + + UnitTeam myTeam = GetComponent(); + UnitTeam targetTeam = candidate.GetComponentInParent(); + return myTeam == null || targetTeam == null || myTeam.IsEnemy(targetTeam.gameObject); + } + public GameObject FindNearestEnemyForSkill() { return FindNearestEnemyUnit(); @@ -378,16 +392,19 @@ public class UnitBase : MonoBehaviour GameObject enemy = FindNearestEnemyUnit(); if (enemy != null) return enemy; - // Phase 2: 出生使命建筑;被销毁时兜底敌方主城堡 - if (missionTarget != null && missionTarget.activeInHierarchy) + // Phase 2: 出生使命建筑;只有仍属于敌方的使命建筑才继续攻击。 + if (IsTargetValid(missionTarget)) { return missionTarget; } + // 使命建筑已被己方占领、失活或销毁时,改攻敌方主城堡。 var myTeam = GetComponent(); if (myTeam != null && AttackTargetController.Instance != null) { - return AttackTargetController.Instance.GetEnemyCastleOfTeam(myTeam.teamId); + GameObject castle = AttackTargetController.Instance.GetEnemyCastleOfTeam(myTeam.teamId); + missionTarget = castle; + return castle; } return null; } @@ -1064,17 +1081,16 @@ public class UnitBase : MonoBehaviour //攻击动画结束后恢复当前攻速倍率。 animator.speed = attackSpeedMultiplier; - //攻击动画结束后,目标仍在攻击范围内则立即进行下一次攻击。 - target = FindTarget(); - if (target != null && IsWithinAttackReach(target)) + //攻击结束后只继续攻击当前锁定目标,不在这里重新寻敌。 + if (IsTargetValid(target) && IsWithinAttackReach(target)) { - // 攻击结束后可能已切换到新的近距离目标,先更新朝向再开始下一次攻击。 FaceTarget(target); attack(target); return; } - //目标离开攻击范围,回到待机逻辑。 + //目标失效或离开攻击范围,解除锁定,下一帧重新寻敌。 + target = null; state = State.Alert; animator.SetInteger("speed", 0); } diff --git a/CODELY.md b/CODELY.md index 58f981d..7fc008d 100644 --- a/CODELY.md +++ b/CODELY.md @@ -83,6 +83,9 @@ - [2026-09-20 13:36:59] 2026-09-20 三路测试布局已在 CoreBattleTest.scene 临时搭建:蓝堡/红堡居中,水晶矿 X=-10、混沌之门 X=10、Z=35;两条临时长方体屏障位于 X=-5 和 X=5,均带 BoxCollider 与 NavMeshObstacle carving,形成左/中/右三条路。 - [2026-09-20 20:44:52] 2026-09-20 固定卡组与轮转已接入 CoreBattleTest:8卡组为人类士兵、弓箭手、盾卫、精锐士兵、法师、狂暴狼人、石巨人、弓箭猎人;底部显示4槽,出牌成功后当前卡进入队尾并由队列补下一张。BattleDeckCycle 挂在 CastleSpawnManager。当前弓箭猎人、狂暴狼人、石巨人的 CharacterData.unitPrefab 仍为空,暂不能正常出兵。 - [2026-09-20 21:43:39] 2026-09-20 卡牌故障修复:8卡组中的弓箭猎人、狂暴狼人、石巨人已分别绑定第二章对应 Prefab,轮转不会再因 unitPrefab 为空而卡死;CastleMageSkill、ArcherSkill、ArcherHunterSkill 和 UnitBase 已移除废弃的 GetOpponentTag/OpponentTag,统一使用 UnitTeam 阵营判断。 +- [2026-09-21 11:10:20] 2026-09-21 UnitBase 目标锁定规则已更新:单位只有在没有有效目标时才寻敌;锁定后移动和攻击期间保持当前目标,新敌人不打断;当前目标死亡、失活或攻击状态下离开攻击范围时才解除锁定,下一帧重新寻敌。单位与建筑目标统一使用同一规则,并通过 WorldHealthBar.IsDead 判断死亡。 +- [2026-09-21 11:16:19] 2026-09-21 UnitBase 目标锁定规则修正:攻击状态一旦进入就不再寻敌,保持当前目标,避免多个单位奔向同一建筑时擦肩后切换目标。仅当当前目标死亡/失活,或因技能等原因不再满足攻击距离时才退出攻击状态;退出后才重新寻敌。单位和建筑目标统一使用同一规则。 +- [2026-09-21 14:26:28] 2026-09-21 据点目标规则修复:CrystalMineCapture 占领后会实时把 UnitTeam.teamId 改为蓝方/红方/中立;UnitBase.IsTargetValid 必须同时检查目标存活状态和目标是否仍属于敌方。出生使命建筑若已被己方占领或变为中立,应立即失效并回退攻击敌方主城堡。 ### Reference - [2026-09-09 12:23:17] 飞行物脚本 Projectile.cs 负责锁敌追踪、碰撞检测和伤害结算。箭矢预制体 ArrowProjectile_White.prefab 已配置 Projectile 组件和 CapsuleCollider(Is Trigger)。远程单位需在攻击动画松弦帧添加 OnAttackEffect Event,并在 UnitBase 中设置 attackType=RangedProjectile、projectilePrefab、projectileSpawnPoint。 diff --git a/双路资源点地图示意.html b/双路资源点地图示意.html index bd3f09a..7337c8b 100644 --- a/双路资源点地图示意.html +++ b/双路资源点地图示意.html @@ -3,128 +3,158 @@ -召唤之战:双路与资源点布局 +召唤之战:地图纵深与公共资源布局
-

《召唤之战》双路与资源点布局

-

这张图回答一个核心问题:资源点不是必经路上的收费站,而是主路旁边的可选目标。

+

《召唤之战》地图纵深与公共资源布局

+

这是俯视图,Unity 坐标约定为:横向是 X,纵向是 Z;蓝方在下方,红方在上方。图中每个位置都标了建议坐标,方便直接对照 Unity Inspector。

-

推荐布局:两条主路 + 两个侧向资源支路

+

推荐方案:保留两个资源点,拉长 Z 轴纵深

- + - - - + + + - - 敌方区域 - 我方区域 - - - - - 左主路:可直接推向敌方城堡 - 右主路:可直接推向敌方城堡 + + + 红方区域 Z=80~120 + 蓝方区域 Z=0~40 - - - - + + + + + + Z=15Z=35Z=55Z=75Z=95Z=115 + - - - - 水晶矿 - 可选目标 - 混沌之门 - 可选目标 + + + + + + + + 左路 + 中路:主要交战区 + 右路 - - - - 我方城堡 - 敌方城堡 + + + 蓝方主城 + 建议 X=0, Z=8 + + 蓝方出生点 + X=0, Z=14 - - - - - - - 主动去争夺资源点 - 主动去争夺资源点 + + + 红方主城 + 建议 X=0, Z=108 + + 红方出生点 + X=0, Z=95 - - - 中间留出 - 独立战斗空间 + + + + + + 水晶矿 + X=-7, Z=45 + + 混沌之门 + X=7, Z=75 + + + + 中央交战区 Z≈45~75 + 资源点在主路外侧,不堵住中路 + + + + + + + + + X=-15X=0X=15 +
-
深灰色:两条主路。单位可以完全绕过资源点,直接推敌方城堡。
-
黄色:资源支路。只有玩家把目标指向资源点时,单位才会走这里。
-
蓝箭:我方从下向上推进。
-
红箭:敌方从上向下推进。
-
核心:两条主路不交叉,双方不会因为去资源点而必然汇合。
+
灰色:三条纵向路线。单位从主城出生后有较长行军距离。
+
黄色:公共资源点,放在主路外侧,不堵住直攻主城的路线。
+
蓝色:蓝方从下向上推进。
+
红色:红方从上向下推进。
+
虚线框:中央交战和资源争夺区域。
+
阅读方法:地图越往上 Z 越大。资源点坐标写在圆点内部,主城和出生点坐标写在对应标注中。
-

单位有两种选择

+

建议坐标表

- + - - - - + + + + + +
玩家目标单位实际路线结果
对象建议世界坐标作用
敌方城堡沿左主路或右主路直走直接推进,不经过资源点
水晶矿从左主路入口转入侧向支路争夺能量收益,占领后再决定是否回主路
混沌之门从右主路入口转入侧向支路争夺自动出兵收益,占领后再决定是否回主路
途中遇到敌军只在近距离视野内拦截detectionRange = 8,不会隔很远跨路追击
蓝方主城(0, 0, 8)蓝方防守核心
蓝方出生点(0, 0, 14)出生后先行军约 30 米再进入资源/交战区域
水晶矿(-7, 0, 45)左侧公共资源,主路外侧
混沌之门(7, 0, 75)右侧公共资源,主路外侧
红方出生点(0, 0, 95)出生后先行军约 20 米再进入交战区域
红方主城(0, 0, 108)红方防守核心
+

这是一套“地图长度约 100~120、宽度约 20~30”的示意坐标,不是要求你现在立即修改场景。先按图确认你想要的战略关系,再落地移动物体和调整 NavMesh。

-

不要采用的布局

-

资源点放在主路正中央:所有单位都必须经过资源点,资源点会变成必争的收费站,玩家无法选择速攻主城。

-

两条路在中央重新合并:即使起点分成左右两路,单位最终仍会在合流处相遇,分兵只是视觉上的分路。

-

两个资源点分别贴近己方城堡:双方到达时间不对称,资源点会天然属于距离更近的一方。

-
- -
-

推荐的 Unity 落地规则

-

先做两条互不交叉的 NavMesh 主路。资源点放在主路外侧的凹袋中,各自连接一条短支路。支路入口与敌方主路保持足够距离,避免资源点支路直接变成双方必经的交叉口。

-

资源点占领后,单位可以回到主路,也可以继续攻击城堡。第一版不需要做复杂的路口优先级,只要保证三件事:主路能绕过、支路可选择、两条主路不交叉。

+

相机只改 Z 轴范围的位置

+

选中 Hierarchy 的 Main Camera,在 Inspector 找到 CameraFollow > 边界限制:

+ + + + + + + +
字段当前值含义
Min Z-30相机允许移动到的最低 Z
Max Z91相机允许移动到的最高 Z
Orthographic Size另行设置镜头远近和可见范围,不属于 Z 轴边界
+

如果地图纵向扩大到 Z=108,至少要把 Max Z 调到能覆盖红方主城附近,例如 115。如果只想限制镜头移动范围,改 Min Z / Max Z;不要用它们来解决单位太小的问题。

diff --git a/召唤之战 - 核心玩法拆家版.md b/召唤之战 - 核心玩法拆家版.md index 05064e9..e5baa5a 100644 --- a/召唤之战 - 核心玩法拆家版.md +++ b/召唤之战 - 核心玩法拆家版.md @@ -6,98 +6,93 @@ > 核心特点:轻量操作、自由寻路、资源与出兵据点争夺、兵种克制,无复杂框选与编队 ## 一、核心定位 -本玩法为《召唤之战》的「拆家流」经典RTS版本,玩家通过消耗法力召唤单位,争夺战场上的水晶矿与随机出兵点,建立经济或兵力优势,最终突破防御摧毁敌方基地取胜。 +本玩法为《召唤之战》的「拆家流」经典RTS版本,玩家通过消耗法力召唤单位,争夺战场上的混沌之门,建立兵力优势,最终突破防御摧毁敌方基地取胜。 - 操作极简:仅需「点击建筑设定攻击标志 + 点击卡牌召唤单位」,无需框选、编队、驻守指令; - 单位全自动作战:复用自动寻路与寻敌逻辑,玩家不干预单位微操; -- 策略核心:出兵目标选择、兵种搭配、资源分配节奏、资源点与出兵点取舍,避免纯数值对撞。 +- 策略核心:出兵目标选择、兵种搭配、资源分配节奏、混沌之门争夺与绕路风险,避免纯数值对撞。 ## 二、战场地图设计 ### 2.1 整体布局 -采用**竖版9:16比例**,核心可通行区域为十字形,外围为不可通行障碍物(岩石/树林)。四个核心目标采用对角线布局,充分利用屏幕四角空间,同时保留中央交战区和多路径进攻空间。 +采用**竖版9:16比例**。地图保留一座公共混沌之门,主城堡与混沌之门不放在同一条短直线上,而是采用横向错位布局:主城堡偏右,混沌之门偏左。单位前往混沌之门需要绕行,不能出生后两步直接接触据点;单位也可以放弃据点,沿更直接的路线推进敌方主城堡。 ```text ┌─────────────────────────────────────┐ -│ │ -│ 【左上资源据点】 【敌方城堡】 │ -│ ● ■ │ -│ \ / │ -│ \ / │ -│ \ / │ -│ 【中央交战区】 │ -│ / \ │ -│ / \ │ -│ / \ │ -│ 【我方城堡】 【右下资源据点】 │ -│ ■ ● │ -│ │ +│ 【敌方城堡】 │ +│ ■ │ +│ 中央交战区 ╱ │ +│ ╱───────────────╲ │ +│ 【混沌之门】 ╲ │ +│ ● ╲ │ +│ 【我方城堡】 +│ ■ │ └─────────────────────────────────────┘ ``` -- 我方城堡位于左下,敌方城堡位于右上; -- 右下资源据点是我方近点、敌方远点; -- 左上资源据点是敌方近点、我方远点; -- 两个资源据点均可被双方争夺,不属于任何一方的专属区域; -- 中央区域作为双方部队交战和拦截远点部队的主要区域; -- 资源据点不应贴近屏幕边缘,应与城堡和中央交战区保持可寻路距离。 +- 双方主城堡位于地图纵向两端,并整体偏右; +- 混沌之门位于地图左侧公共区域,与主城堡形成横向错位; +- 主攻主城堡的单位走较直接的主路线;指定混沌之门的单位需要走左侧绕行路线; +- 混沌之门是唯一公共资源据点,不再保留水晶矿; +- 混沌之门既是争夺目标,也是占领后持续提供额外兵源的战略目标; +- 镜头保持近距离以保证兵种可读性,玩家通过上下拖动或镜头跟随查看纵向战场,不通过缩小镜头查看全图。 -建议先使用以下相对坐标进行场景布局,再根据NavMesh实际路径距离微调: +建议坐标仅作为布局方向,最终以实际路径长度和镜头范围调整: -| 目标 | 建议坐标(X, Z) | 距离关系 | -|------|------------------|----------| -| 我方城堡 | (-7, 8) | 我方出生点 | -| 右下资源据点 | (7, 24) | 我方近点、敌方远点 | -| 左上资源据点 | (-7, 46) | 敌方近点、我方远点 | -| 敌方城堡 | (7, 62) | 敌方出生点 | - -双方均可选择进攻任意资源据点。近点更容易快速占领和支援,远点需要承担更长的行军距离和被中央区域拦截的风险。 +| 目标 | 建议位置 | 距离关系 | +|------|----------|----------| +| 我方主城堡 | 右下侧,约 (6, 0, 8) | 我方出生点,主路线起点 | +| 混沌之门 | 左侧中部,约 (-6, 0, 45) | 公共绕路目标,距离双方出生点都应有行军时间 | +| 敌方主城堡 | 右上侧,约 (6, 0, 82) | 敌方出生点和最终拆家目标 | ### 2.2 区域说明 | 区域 | 位置 | 规则 | |------|------|------| -| 己方阵地 | 左下方 | 包含我方城堡,为单位出生点(城堡前方出兵点) | -| 敌方阵地 | 右上方 | 包含敌方城堡,为最终攻击目标 | -| 中央交战区 | 战场中央 | 双方部队交战、拦截和转向的主要区域 | -| 左上资源据点 | 敌方城堡一侧 | 可攻击争夺,双方均可进攻;作为敌方近点、我方远点 | -| 右下资源据点 | 我方城堡一侧 | 可攻击争夺,双方均可进攻;作为我方近点、我方远点 | -| 主通道 | 各目标之间 | 基于NavMesh自由寻路,不设置强制固定路线 | +| 己方阵地 | 右下方 | 包含我方城堡和出生点 | +| 敌方阵地 | 右上方 | 包含敌方城堡和出生点 | +| 中央交战区 | 地图中部 | 双方部队交战、拦截和分流的主要区域 | +| 混沌之门区域 | 左侧中部 | 唯一公共据点,双方均可争夺,占领后自动产兵 | +| 主通道 | 城堡之间偏右路线 | 直接推进敌方城堡,不必经过混沌之门 | +| 绕行通道 | 主通道向左侧延伸 | 前往混沌之门,距离更长但可换取持续兵源 | ### 2.3 寻路规则 -- 基于NavMesh实现自由寻路,仅十字形主通道和中央交战区为可通行烘焙区,外围障碍物完全阻挡; -- **建筑实体(城堡/水晶矿/混沌之门)使用独立 Buildings 层,排除在 NavMesh 烘焙之外**;建筑足迹通过运行时 NavMeshObstacle(圆柱 carve)挖空,单位绕行建筑不穿模、也不会被建筑模型的斜面"抬升"; -- 单位自动计算前往绑定目标的最短路径,途中自动避让友军与地形; +- 基于NavMesh实现自由寻路,主通道和混沌之门绕行通道均可通行,外围障碍物阻挡无关区域; +- **建筑实体(城堡/混沌之门)使用独立 Buildings 层,排除在 NavMesh 烘焙之外**;建筑足迹通过运行时 NavMeshObstacle(圆柱 carve)挖空,单位绕行建筑不穿模、也不会被建筑模型的斜面"抬升"; +- 单位自动计算前往绑定目标的路径,途中自动避让友军与地形; - 单位贴地统一使用 `NavMeshAgent.baseOffset = 0`,跟随平坦 NavMesh 高度,不再根据模型 Renderer 包围盒自动校准,避免模型原点偏移导致角色运行时抬升; -- 无强制固定路线,单位可在主通道和中央交战区内自由移动,远点部队可能在中央区域遭遇拦截。 +- 无强制固定路线,但主通道和绕行通道的实际长度应形成明显取舍:直推更快,绕门更远; +- 混沌之门被指定为目标时,单位必须完成绕行,不应因为据点摆放过近而两步接触。 ## 三、资源系统 ### 3.1 法力水晶 - 法力是玩家主动召唤单位、释放技能使用的基础资源; - 法力基础自动回复(3秒恢复1点),上限10点; - 主动出兵始终受到法力上限和回复速度限制,避免玩家无限堆叠单位; -- 水晶矿占领后,按固定间隔为当前归属方增加1点能量,不直接改变基础能量回复速度; +- 当前版本不再设置水晶矿,法力只通过基础回复获得;混沌之门提供的是额外自动兵源,不直接增加法力。 -### 3.2 资源据点争夺规则 -- 水晶矿与混沌之门均为可攻击目标,不设置生命值,使用**红蓝争夺进度条**(0~100); +### 3.2 混沌之门争夺规则 +- 混沌之门是唯一公共据点,使用红蓝争夺进度条(0~100),不设置普通生命值; - 进度条中立状态为50:50。以蓝方视角为例,100表示蓝方完全占领,0表示红方完全占领; - 己方单位攻击据点时,进度条向己方方向偏移;敌方攻击时,进度条向敌方方向偏移; -- 进度条达到己方100%时,据点归属己方并开始产出对应资源(据点 teamId 翻转,光球/裂缝变色); -- 据点归属后,即使敌方开始攻击并推动进度条反向移动,原占有方仍持续获得产出; -- 只有当进度条被对方完全推到100%(即原占有方进度为0)时,才中止原占有方产出并将据点翻转给对方; +- 进度条达到己方100%时,据点归属己方并开始自动出兵(据点teamId翻转,光球/裂缝变色); +- 据点归属后,即使敌方开始攻击并推动进度条反向移动,原占有方仍持续获得自动出兵; +- 只有当进度条被对方完全推到100%(即原占有方进度为0)时,才中止原占有方出兵并将据点翻转给对方; - 无任何单位攻击时,进度条保持当前数值不变; -- 据点翻转后,按相同规则由新的占有方获得对应产出。 -- **中立据点属于第三阵营(teamId=2),单位基础寻敌不会自动攻击中立据点**,只有玩家攻击标志指向它时才会被进攻(避免单位路过就绕去打矿)。 +- 中立据点属于第三阵营(teamId=2),单位基础寻敌不会自动攻击,只有攻击标志指向混沌之门时才会被进攻; +- 混沌之门位于主城堡主路线外侧,争夺它需要承担绕路和暴露兵线的风险。 -### 3.3 水晶矿产出 -- 水晶矿占领后,按固定间隔为当前归属方增加1点能量; -- 能量收益归属当前占领方,不改变基础能量回复速度; -- 水晶矿被对方完全翻转后,后续能量收益立即转移给新的占领方; -- 能量收益的间隔和单局上限根据战斗测试调整。 - -### 3.4 混沌之门产出(原随机出兵点) -- 混沌之门归属己方后,按固定间隔在门口自动生成1个单位; -- 产出单位从**当前玩家出战卡组**中随机抽取,不从所有已解锁或全体角色中抽取; -- 产出单位按当前玩家出战卡组进行概率抽取:低费用卡为主要产出,高费用卡仅以较低概率出现;具体概率按战斗测试调整; -- 产出单位不消耗玩家法力,价值定位低于玩家主动召唤,避免完全替代正常出兵; -- 产出单位默认执行混沌之门归属方当前的攻击标志目标;如无特殊设置,则默认目标为敌方主基地; +### 3.3 混沌之门自动出兵 +- 混沌之门归属己方后,按固定间隔在门附近自动生成1个单位; +- 产出单位从当前阵营的8卡组中抽取,并遵守当前4张手牌轮转,不从全体角色随机无限抽取; +- 出兵不消耗玩家法力,价值低于手动出兵,定位是占领据点后的持续兵源; +- 出兵费用上限随战斗时间提高: + - 0~30秒:最高3费; + - 30~75秒:最高4费; + - 75~120秒:最高5费; + - 120~180秒:最高6费; + - 180秒以后:最高8费; +- 正常情况下只允许当前4张手牌中不超过时间费用上限的卡; +- 若当前4张手牌全部高于时间费用上限,则临时放宽到当前手牌中的最低费用,保证混沌之门不会因卡组构成而完全停产; +- 低费用卡权重更高,高费用卡权重更低;解锁上限决定“能不能出”,概率权重决定“更容易出谁”; +- 自动出兵单位默认执行混沌之门归属方当前攻击标志目标;如无特殊设置,则默认目标为敌方主城堡; - 混沌之门被对方完全翻转后,自动出兵权转移给对方。 ## 四、单位系统 @@ -123,39 +118,59 @@ if (detectionRange 视野内有敌方战斗单位) { } else { 直奔出生使命建筑(出生时快照的攻击标志) 到达攻击范围后再判断建筑阵营: - - 非我方 → 攻击它(推争夺进度/拆城堡) + - 非我方 → 攻击它(推混沌之门争夺进度/拆城堡) - 我方(增援完成)→ 该单位使命转向敌方主城堡,继续走 } ``` - **视野内敌方单位永远最高优先级**:行军撞敌即停下互殴,击杀后继续执行原使命; -- 中立建筑(水晶矿/混沌之门)**不在自动寻敌范围内**,只能通过攻击标志指定进攻; -- 增援逻辑:玩家把标志点到己方已占领据点,新兵出生直奔该据点增援,到达后自动回归主城堡路线——玩家通过"改变出兵流"实现救援,无需驻守指令。 +- 中立建筑(混沌之门)**不在自动寻敌范围内**,只能通过攻击标志指定进攻; +- 增援逻辑:玩家把标志点到己方已占领混沌之门,新兵出生直奔该据点增援,到达后自动回归主城堡路线——玩家通过“改变出兵流”实现救援,无需驻守指令。 ### 5.2 攻击标志系统(已实现,替代原目标按钮组方案) 核心操作:**点击建筑切换攻击标志**,无按钮、无模式开关。 - **双阵营独立标志**:蓝方/红方各自一个攻击标志,默认指向敌方主城堡;本地玩家点击建筑只切换自己阵营的标志(敌方标志由敌方AI/玩家控制,互不干扰); - 标志视觉:建筑头顶倒锥箭头,颜色随标志所属阵营(蓝箭=蓝方目标,红箭=红方目标),双方标志指向同一建筑时并排错开显示; - 点击规则:可点敌方/中立/己方占领建筑(点己方建筑=增援信号);点自己方主城堡无效; -- **出生即使命**:单位出生时快照当时己方标志指向的建筑作为终身使命(missionTarget),之后标志切换**不影响已存活单位**——这是分兵的实现方式:先点水晶矿出2个兵,再点混沌之门出兵,两路各走各的; +- **出生即使命**:单位出生时快照当时己方标志指向的建筑作为终身使命,之后标志切换**不影响已存活单位**——这是分兵的实现方式:先点混沌之门出兵,再点敌方主城堡出兵,两路各走各的; - 使命兜底:使命建筑被销毁时,单位自动转向敌方主城堡。 ### 5.3 战斗行为示例 -1. 玩家点击水晶矿(蓝箭头从敌堡跳到水晶矿),召唤2个步兵; -2. 步兵出生即绑定使命=水晶矿,直奔水晶矿; +1. 玩家点击混沌之门(攻击标志从敌堡跳到混沌之门),召唤2个步兵; +2. 步兵出生即绑定使命=混沌之门,沿绕行路线前进; 3. 途中遭遇敌方单位,步兵立刻停下互殴(视野优先); -4. 敌人被击杀后,步兵继续前往水晶矿,攻击推进争夺进度; -5. 玩家再点击混沌之门(标志切换),召唤1个重甲,重甲使命=混沌之门,自动前往; -6. 此前的2个步兵仍继续攻击水晶矿,不受标志切换影响,实现分兵作战; -7. 敌方反扑我方占领的水晶矿:玩家点击该水晶矿(增援信号),新出的兵直奔水晶矿救援,清场后单位自动回归敌方主城堡路线; -8. 混沌之门归属己方期间,按固定间隔从玩家出战卡组随机生成单位,生成单位默认前往敌方主基地。 +4. 敌人被击杀后,步兵继续前往混沌之门,攻击推进争夺进度; +5. 玩家再点击敌方主城堡(标志切换),召唤1个重甲,重甲使命=敌方主城堡,沿主路线推进; +6. 此前的2个步兵仍继续攻击混沌之门,不受标志切换影响,实现分兵作战; +7. 敌方反扑己方混沌之门:玩家点击混沌之门(增援信号),持续出兵流增援; +8. 混沌之门归属己方期间,按时间费用上限和当前4张手牌轮转自动生成单位,生成单位默认前往敌方主城堡。 ### 5.4 建筑受击与攻击距离 -- 四个建筑(蓝/红城堡、水晶矿、混沌之门)统一 **Building Tag** + CapsuleCollider(圆柱,半径按模型网格实际顶点计算); +- **三个建筑目标**(蓝/红城堡、混沌之门)统一 `Building Tag` + CapsuleCollider;混沌之门使用 CrystalMineCapture 争夺进度,城堡使用 WorldHealthBar 血量; - 单位攻击建筑的判定距离用 **Collider.ClosestPoint 计算到建筑表面的水平距离**(城堡中心在模型内部,直接用中心距离会导致够不着/隔空打); - 攻击距离 = 单位 attackRange + 建筑专用小缓冲(0.2,贴近墙体攻击);单位互打保持 1.5 缓冲; - 伤害管线:UnitBase(普通单位)→ CrystalMineCapture(据点争夺)→ BuildingHealth(建筑)→ WorldHealthBar(通用血量),近战/技能/飞行物全类型支持。 -## 六、UI布局与操作 +## 六、弹道与技能表现 +### 6.1 视觉与逻辑分离 +- 近战单位:由攻击动画的 `OnAttackEffect` Event 直接结算伤害,不创建飞行物; +- 弓箭手、法师等远程单位:继续使用 `Projectile.cs` 负责锁敌、移动、碰撞和伤害结算,视觉不使用完整3D模型; +- 远程飞行物统一采用 **Quad + 透明贴图 + 共享轻量材质**,例如箭矢贴图、法师火球贴图;Quad只负责显示,Projectile只负责逻辑; +- 火球等贴图保持小尺寸、强轮廓、高对比,适配近距离2.5D镜头和移动端显示; +- 范围技能:使用 Particle System 表现范围效果,并由范围伤害脚本处理一次性或持续伤害; +- 大量同时存在的箭矢、火球或弹幕:使用 Projectile 对象池,发射时取出、命中或超时后归还,避免频繁 `Instantiate/Destroy`。 + +### 6.2 标准远程Prefab结构 +```text +Projectile_Fireball +├── Quad(透明火球贴图) +└── Projectile.cs(锁敌、飞行、命中、伤害) +``` +- 不为普通箭矢和火球配置骨骼、Animator或完整模型; +- 贴图正面朝向摄像机或根据飞行方向旋转; +- 逻辑碰撞体使用轻量 Collider/Trigger,视觉尺寸与碰撞尺寸分开调节; +- 对象池作为大量弹道时的后续性能优化,不改变现有伤害接口。 + +## 七、UI布局与操作 ### 6.1 整体布局 采用「上战场、下操作」的竖版经典结构,战斗视野完整,拇指可覆盖全部操作区。 ```text @@ -170,7 +185,7 @@ if (detectionRange 视野内有敌方战斗单位) { ``` ### 6.2 攻击标志(替代原目标按钮组) -- 交互:战斗区域内直接点击建筑(城堡/水晶矿/混沌之门),攻击标志(头顶箭头)跳转到该建筑; +- 交互:战斗区域内直接点击建筑(城堡/混沌之门),攻击标志(头顶箭头)跳转到该建筑; - 无按钮组、无模式开关、无额外UI——标志本身就是全局状态显示; - 逻辑:仅决定「下一次召唤单位的目标」,不影响场上已存活单位; - 默认标志=敌方主城堡,开局无需任何操作即可出兵推塔。