```
feat(gameplay): 添加水晶矿资源产出系统和改进AI卡组管理 - 实现水晶矿占领后的能量资源产出功能,包括manaInterval和manaAmount配置 - 添加敌方AI的能量管理系统,支持通过AddMana方法增加能量 - 实现AI卡组轮转机制,包含手牌管理和抽卡队列功能 - 优化单位AI的目标选择和战斗逻辑,修复目标切换问题 - 水晶矿现在可以按设定间隔为所属阵营提供能量资源 -
This commit is contained in:
1 parent
b185e5768b
commit
06940a849c
21 files changed
+555
-520
No files matched your search
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: XS9MvS7/U3m6dqDMGv9c/gMis+P/vXKeKPOgPN5GaQgqPD66VCPaI9g=
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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: []
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: DC8X4yivUH0YWIxSi3xxgZIYGN3DwMGCn0PJg22u/mzP6FQrGaqOtlY=
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -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:
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.6 MiB |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 855 KiB |
@@ -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:
|
||||
Binary file not shown.
@@ -0,0 +1,130 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 混沌之门:被占领后按固定间隔从所属阵营卡组随机出兵。
|
||||
/// </summary>
|
||||
[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<CrystalMineCapture>();
|
||||
unitTeam = GetComponent<UnitTeam>();
|
||||
spawnTimer = spawnInterval;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (unitTeam == null)
|
||||
{
|
||||
unitTeam = GetComponent<UnitTeam>();
|
||||
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<EnemyCastleAI>();
|
||||
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<CharacterData>("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<UnitTeam>();
|
||||
if (team != null)
|
||||
{
|
||||
team.teamId = 0;
|
||||
unit.tag = "Friend";
|
||||
}
|
||||
|
||||
UnitBase unitBase = unit.GetComponent<UnitBase>();
|
||||
if (unitBase != null)
|
||||
{
|
||||
unitBase.InitializeFromPlayerData(data.GetMaxHealth(1), data.GetAttackPower(1), data.GetMoveSpeed(1));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: WnsW5HmqByrprA9NWfcR8/RRH5kmtPm0TGQevEsLKuNlTEbLzjkqpVA=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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<EnemyCastleAI>();
|
||||
enemyAI?.AddMana(manaAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ReceiveCaptureDamage(float damage, GameObject attacker)
|
||||
{
|
||||
if (damage <= 0f || attacker == null) return false;
|
||||
|
||||
+118
-11
@@ -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<CharacterData> hand = new List<CharacterData>();
|
||||
private readonly Queue<CharacterData> drawQueue = new Queue<CharacterData>();
|
||||
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<CharacterData>("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<CharacterData> affordable = new List<CharacterData>();
|
||||
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<CharacterData> 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<CharacterData>();
|
||||
foreach (string dataName in deckDataNames)
|
||||
{
|
||||
CharacterData data = Resources.Load<CharacterData>("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<UnitTeam>();
|
||||
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<UnitBase>();
|
||||
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);
|
||||
|
||||
+41
-25
@@ -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<UnitTeam>();
|
||||
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<WorldHealthBar>();
|
||||
if (healthBar != null && healthBar.IsDead)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UnitTeam myTeam = GetComponent<UnitTeam>();
|
||||
UnitTeam targetTeam = candidate.GetComponentInParent<UnitTeam>();
|
||||
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<UnitTeam>();
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user