feat(gameplay): 添加水晶矿资源产出系统和改进AI卡组管理

- 实现水晶矿占领后的能量资源产出功能,包括manaInterval和manaAmount配置
- 添加敌方AI的能量管理系统,支持通过AddMana方法增加能量
- 实现AI卡组轮转机制,包含手牌管理和抽卡队列功能
- 优化单位AI的目标选择和战斗逻辑,修复目标切换问题

- 水晶矿现在可以按设定间隔为所属阵营提供能量资源
-
This commit is contained in:
huadexuan committed 2026-09-21 18:17:05 +08:00
1 parent b185e5768b
commit 06940a849c
21 files changed
+555 -520

No files matched your search

-8
View File
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: XS9MvS7/U3m6dqDMGv9c/gMis+P/vXKeKPOgPN5GaQgqPD66VCPaI9g=
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
-83
View File
@@ -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.
-210
View File
@@ -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.
+130
View File
@@ -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;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: WnsW5HmqByrprA9NWfcR8/RRH5kmtPm0TGQevEsLKuNlTEbLzjkqpVA=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+33 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}