```
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);
|
||||
}
|
||||
|
||||
@@ -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。
|
||||
+111
-81
@@ -3,128 +3,158 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>召唤之战:双路与资源点布局</title>
|
||||
<title>召唤之战:地图纵深与公共资源布局</title>
|
||||
<style>
|
||||
:root { --bg:#101722; --panel:#182332; --ink:#e8eef5; --muted:#a9b6c5; --blue:#3da5ff; --red:#ff5c68; --road:#637386; --side:#d9a441; --bad:#da6870; --good:#54c48a; }
|
||||
:root { --bg:#101722; --panel:#182332; --ink:#e8eef5; --muted:#a9b6c5; --blue:#3da5ff; --red:#ff5c68; --gold:#e8b94e; --road:#68798d; --line:#304052; --green:#54c48a; }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; background:var(--bg); color:var(--ink); font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
|
||||
main { max-width:1100px; margin:0 auto; padding:32px 20px 56px; }
|
||||
h1 { margin:0 0 8px; font-size:30px; }
|
||||
h2 { margin:28px 0 10px; font-size:22px; }
|
||||
main { max-width:1180px; margin:0 auto; padding:28px 18px 52px; }
|
||||
h1 { margin:0 0 6px; font-size:30px; }
|
||||
h2 { margin:26px 0 10px; font-size:21px; }
|
||||
p { margin:8px 0; color:var(--muted); }
|
||||
.intro { margin-bottom:22px; }
|
||||
.panel { background:var(--panel); border:1px solid #2c3a4c; border-radius:8px; padding:18px; margin-top:18px; }
|
||||
.map-wrap { display:grid; grid-template-columns:minmax(0, 1fr) 260px; gap:18px; align-items:start; }
|
||||
.intro { margin-bottom:18px; }
|
||||
.panel { background:var(--panel); border:1px solid #2c3a4c; border-radius:8px; padding:18px; margin-top:16px; }
|
||||
.map-wrap { display:grid; grid-template-columns:minmax(0, 1fr) 300px; gap:18px; align-items:start; }
|
||||
svg { display:block; width:100%; height:auto; background:#dce5d1; border-radius:6px; }
|
||||
.legend { display:grid; gap:10px; color:var(--muted); font-size:14px; }
|
||||
.legend div { display:flex; align-items:center; gap:8px; }
|
||||
.swatch { width:25px; height:12px; border-radius:3px; display:inline-block; flex:0 0 auto; }
|
||||
.legend { display:grid; gap:12px; color:var(--muted); font-size:14px; }
|
||||
.legend div { display:flex; align-items:flex-start; gap:8px; }
|
||||
.swatch { width:26px; height:12px; border-radius:3px; display:inline-block; flex:0 0 auto; margin-top:5px; }
|
||||
.swatch.main { background:var(--road); }
|
||||
.swatch.side { background:var(--side); }
|
||||
.swatch.resource { background:var(--gold); }
|
||||
.swatch.blue { background:var(--blue); }
|
||||
.swatch.red { background:var(--red); }
|
||||
.good { border-left:4px solid var(--good); padding-left:12px; }
|
||||
.warn { border-left:4px solid var(--bad); padding-left:12px; }
|
||||
.swatch.boundary { background:#8c6b50; }
|
||||
.good { border-left:4px solid var(--green); padding-left:12px; }
|
||||
.note { background:#223044; border-left:4px solid var(--gold); padding:10px 12px; color:var(--muted); }
|
||||
table { width:100%; border-collapse:collapse; margin-top:12px; }
|
||||
th,td { text-align:left; padding:9px 10px; border-bottom:1px solid #304052; }
|
||||
th,td { text-align:left; padding:9px 10px; border-bottom:1px solid var(--line); }
|
||||
th { color:#fff; background:#223044; }
|
||||
td { color:var(--muted); }
|
||||
code { color:#ffd77a; }
|
||||
@media (max-width:760px) { .map-wrap { grid-template-columns:1fr; } h1 { font-size:25px; } }
|
||||
.coords { color:#fff; font-weight:600; white-space:nowrap; }
|
||||
@media (max-width:820px) { .map-wrap { grid-template-columns:1fr; } h1 { font-size:25px; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>《召唤之战》双路与资源点布局</h1>
|
||||
<p class="intro">这张图回答一个核心问题:<strong>资源点不是必经路上的收费站,而是主路旁边的可选目标。</strong></p>
|
||||
<h1>《召唤之战》地图纵深与公共资源布局</h1>
|
||||
<p class="intro">这是<strong>俯视图</strong>,Unity 坐标约定为:横向是 X,纵向是 Z;蓝方在下方,红方在上方。图中每个位置都标了建议坐标,方便直接对照 Unity Inspector。</p>
|
||||
|
||||
<section class="panel">
|
||||
<h2>推荐布局:两条主路 + 两个侧向资源支路</h2>
|
||||
<h2>推荐方案:保留两个资源点,拉长 Z 轴纵深</h2>
|
||||
<div class="map-wrap">
|
||||
<svg viewBox="0 0 700 900" role="img" aria-label="双路资源点布局俯视图">
|
||||
<svg viewBox="0 0 760 1080" role="img" aria-label="按 Unity X Z 坐标标注的战场布局">
|
||||
<defs>
|
||||
<marker id="arrowBlue" markerWidth="9" markerHeight="9" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 Z" fill="#1976d2"/></marker>
|
||||
<marker id="arrowRed" markerWidth="9" markerHeight="9" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 Z" fill="#d83242"/></marker>
|
||||
<marker id="arrowGold" markerWidth="9" markerHeight="9" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 Z" fill="#a97909"/></marker>
|
||||
<marker id="blueArrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto"><path d="M0,0 L10,5 L0,10 Z" fill="#1976d2"/></marker>
|
||||
<marker id="redArrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto"><path d="M0,0 L10,5 L0,10 Z" fill="#d83242"/></marker>
|
||||
<marker id="goldArrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto"><path d="M0,0 L10,5 L0,10 Z" fill="#9b710b"/></marker>
|
||||
</defs>
|
||||
<rect x="22" y="22" width="656" height="856" rx="12" fill="#b9cba9" stroke="#73866d" stroke-width="3"/>
|
||||
<text x="350" y="55" text-anchor="middle" font-size="18" font-weight="700" fill="#263728">敌方区域</text>
|
||||
<text x="350" y="860" text-anchor="middle" font-size="18" font-weight="700" fill="#263728">我方区域</text>
|
||||
|
||||
<!-- two independent main roads -->
|
||||
<path d="M205 790 L205 120" stroke="#415164" stroke-width="86" stroke-linecap="round"/>
|
||||
<path d="M495 790 L495 120" stroke="#415164" stroke-width="86" stroke-linecap="round"/>
|
||||
<path d="M205 790 L205 120 M495 790 L495 120" stroke="#7e8da0" stroke-width="4" stroke-dasharray="15 13"/>
|
||||
<text x="205" y="475" text-anchor="middle" font-size="18" font-weight="700" fill="#fff" transform="rotate(-90 205 475)">左主路:可直接推向敌方城堡</text>
|
||||
<text x="495" y="475" text-anchor="middle" font-size="18" font-weight="700" fill="#fff" transform="rotate(90 495 475)">右主路:可直接推向敌方城堡</text>
|
||||
<!-- 坐标换算:X=-15..15 映射到 95..665;Z=0..120 映射到 1015..65 -->
|
||||
<rect x="75" y="35" width="610" height="980" rx="10" fill="#b9cba9" stroke="#73866d" stroke-width="3"/>
|
||||
<text x="380" y="61" text-anchor="middle" font-size="17" font-weight="700" fill="#263728">红方区域 Z=80~120</text>
|
||||
<text x="380" y="1000" text-anchor="middle" font-size="17" font-weight="700" fill="#263728">蓝方区域 Z=0~40</text>
|
||||
|
||||
<!-- resource side pockets -->
|
||||
<path d="M205 355 C155 355 115 350 82 315" fill="none" stroke="#d9a441" stroke-width="34" stroke-linecap="round"/>
|
||||
<path d="M495 545 C545 545 585 550 618 585" fill="none" stroke="#d9a441" stroke-width="34" stroke-linecap="round"/>
|
||||
<path d="M205 355 C155 355 115 350 82 315 M495 545 C545 545 585 550 618 585" fill="none" stroke="#ffe29a" stroke-width="3" stroke-dasharray="10 9"/>
|
||||
<!-- horizontal coordinate guides -->
|
||||
<g stroke="#91a88b" stroke-width="1" stroke-dasharray="5 7" opacity=".75">
|
||||
<path d="M95 891 H665"/><path d="M95 728 H665"/><path d="M95 565 H665"/><path d="M95 402 H665"/><path d="M95 239 H665"/><path d="M95 76 H665"/>
|
||||
</g>
|
||||
<g fill="#40513e" font-size="12">
|
||||
<text x="82" y="895" text-anchor="end">Z=15</text><text x="82" y="732" text-anchor="end">Z=35</text><text x="82" y="569" text-anchor="end">Z=55</text><text x="82" y="406" text-anchor="end">Z=75</text><text x="82" y="243" text-anchor="end">Z=95</text><text x="82" y="80" text-anchor="end">Z=115</text>
|
||||
</g>
|
||||
|
||||
<!-- resource nodes -->
|
||||
<circle cx="70" cy="302" r="43" fill="#e8b94e" stroke="#895f0a" stroke-width="5"/>
|
||||
<circle cx="630" cy="598" r="43" fill="#e8b94e" stroke="#895f0a" stroke-width="5"/>
|
||||
<text x="70" y="298" text-anchor="middle" font-size="16" font-weight="700" fill="#3e2d08">水晶矿</text>
|
||||
<text x="70" y="319" text-anchor="middle" font-size="13" fill="#3e2d08">可选目标</text>
|
||||
<text x="630" y="594" text-anchor="middle" font-size="16" font-weight="700" fill="#3e2d08">混沌之门</text>
|
||||
<text x="630" y="615" text-anchor="middle" font-size="13" fill="#3e2d08">可选目标</text>
|
||||
<!-- three lanes -->
|
||||
<path d="M205 935 L205 135" stroke="#68798d" stroke-width="78" stroke-linecap="round"/>
|
||||
<path d="M380 935 L380 135" stroke="#68798d" stroke-width="78" stroke-linecap="round"/>
|
||||
<path d="M555 935 L555 135" stroke="#68798d" stroke-width="78" stroke-linecap="round"/>
|
||||
<g stroke="#aab7c4" stroke-width="3" stroke-dasharray="13 12" opacity=".85">
|
||||
<path d="M205 935 L205 135"/><path d="M380 935 L380 135"/><path d="M555 935 L555 135"/>
|
||||
</g>
|
||||
<text x="205" y="540" text-anchor="middle" font-size="16" font-weight="700" fill="#fff" transform="rotate(-90 205 540)">左路</text>
|
||||
<text x="380" y="540" text-anchor="middle" font-size="16" font-weight="700" fill="#fff" transform="rotate(-90 380 540)">中路:主要交战区</text>
|
||||
<text x="555" y="540" text-anchor="middle" font-size="16" font-weight="700" fill="#fff" transform="rotate(90 555 540)">右路</text>
|
||||
|
||||
<!-- castles -->
|
||||
<rect x="145" y="755" width="120" height="70" rx="12" fill="#3189d7" stroke="#14558d" stroke-width="5"/>
|
||||
<rect x="435" y="75" width="120" height="70" rx="12" fill="#e34b58" stroke="#962b35" stroke-width="5"/>
|
||||
<text x="205" y="797" text-anchor="middle" font-size="18" font-weight="700" fill="#fff">我方城堡</text>
|
||||
<text x="495" y="117" text-anchor="middle" font-size="18" font-weight="700" fill="#fff">敌方城堡</text>
|
||||
<!-- blue side -->
|
||||
<rect x="132" y="920" width="146" height="54" rx="9" fill="#3189d7" stroke="#14558d" stroke-width="4"/>
|
||||
<text x="205" y="943" text-anchor="middle" font-size="16" font-weight="700" fill="#fff">蓝方主城</text>
|
||||
<text x="205" y="962" text-anchor="middle" font-size="12" fill="#fff">建议 X=0, Z=8</text>
|
||||
<circle cx="380" cy="870" r="25" fill="#3da5ff" stroke="#14558d" stroke-width="4"/>
|
||||
<text x="380" y="866" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">蓝方</text><text x="380" y="883" text-anchor="middle" font-size="12" fill="#fff">出生点</text>
|
||||
<text x="398" y="905" font-size="12" fill="#14558d">X=0, Z=14</text>
|
||||
|
||||
<!-- blue / red direct routes -->
|
||||
<path d="M205 745 L205 160" fill="none" stroke="#1976d2" stroke-width="5" marker-end="url(#arrowBlue)"/>
|
||||
<path d="M495 155 L495 740" fill="none" stroke="#d83242" stroke-width="5" marker-end="url(#arrowRed)"/>
|
||||
<!-- optional resource routes -->
|
||||
<path d="M175 705 C135 600 115 425 78 340" fill="none" stroke="#a97909" stroke-width="4" stroke-dasharray="12 8" marker-end="url(#arrowGold)"/>
|
||||
<path d="M525 195 C565 300 585 475 622 560" fill="none" stroke="#a97909" stroke-width="4" stroke-dasharray="12 8" marker-end="url(#arrowGold)"/>
|
||||
<text x="94" y="520" font-size="14" font-weight="700" fill="#6c5009" transform="rotate(-78 94 520)">主动去争夺资源点</text>
|
||||
<text x="607" y="390" font-size="14" font-weight="700" fill="#6c5009" transform="rotate(78 607 390)">主动去争夺资源点</text>
|
||||
<!-- red side -->
|
||||
<rect x="132" y="76" width="146" height="54" rx="9" fill="#e34b58" stroke="#962b35" stroke-width="4"/>
|
||||
<text x="205" y="99" text-anchor="middle" font-size="16" font-weight="700" fill="#fff">红方主城</text>
|
||||
<text x="205" y="118" text-anchor="middle" font-size="12" fill="#fff">建议 X=0, Z=108</text>
|
||||
<circle cx="380" cy="185" r="25" fill="#ff5c68" stroke="#962b35" stroke-width="4"/>
|
||||
<text x="380" y="181" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">红方</text><text x="380" y="198" text-anchor="middle" font-size="12" fill="#fff">出生点</text>
|
||||
<text x="398" y="220" font-size="12" fill="#962b35">X=0, Z=95</text>
|
||||
|
||||
<!-- labels -->
|
||||
<rect x="285" y="420" width="130" height="60" rx="8" fill="#eef4e9" stroke="#80947b"/>
|
||||
<text x="350" y="445" text-anchor="middle" font-size="15" font-weight="700" fill="#2b3b2a">中间留出</text>
|
||||
<text x="350" y="466" text-anchor="middle" font-size="15" font-weight="700" fill="#2b3b2a">独立战斗空间</text>
|
||||
<!-- resources -->
|
||||
<path d="M205 625 C250 625 275 600 300 565" fill="none" stroke="#d9a441" stroke-width="26" stroke-linecap="round"/>
|
||||
<path d="M555 505 C510 505 485 530 460 565" fill="none" stroke="#d9a441" stroke-width="26" stroke-linecap="round"/>
|
||||
<path d="M205 625 C250 625 275 600 300 565 M555 505 C510 505 485 530 460 565" fill="none" stroke="#ffe29a" stroke-width="3" stroke-dasharray="9 8"/>
|
||||
<circle cx="170" cy="655" r="46" fill="#e8b94e" stroke="#895f0a" stroke-width="5"/>
|
||||
<text x="170" y="650" text-anchor="middle" font-size="15" font-weight="700" fill="#3e2d08">水晶矿</text>
|
||||
<text x="170" y="670" text-anchor="middle" font-size="12" fill="#3e2d08">X=-7, Z=45</text>
|
||||
<circle cx="590" cy="475" r="46" fill="#e8b94e" stroke="#895f0a" stroke-width="5"/>
|
||||
<text x="590" y="470" text-anchor="middle" font-size="15" font-weight="700" fill="#3e2d08">混沌之门</text>
|
||||
<text x="590" y="490" text-anchor="middle" font-size="12" fill="#3e2d08">X=7, Z=75</text>
|
||||
|
||||
<!-- battle zone -->
|
||||
<rect x="120" y="500" width="520" height="130" rx="12" fill="none" stroke="#8c6b50" stroke-width="3" stroke-dasharray="10 8"/>
|
||||
<text x="380" y="553" text-anchor="middle" font-size="17" font-weight="700" fill="#6b513d">中央交战区 Z≈45~75</text>
|
||||
<text x="380" y="578" text-anchor="middle" font-size="13" fill="#6b513d">资源点在主路外侧,不堵住中路</text>
|
||||
|
||||
<!-- movement arrows -->
|
||||
<path d="M380 850 L380 690" fill="none" stroke="#1976d2" stroke-width="5" marker-end="url(#blueArrow)"/>
|
||||
<path d="M380 235 L380 395" fill="none" stroke="#d83242" stroke-width="5" marker-end="url(#redArrow)"/>
|
||||
<path d="M235 640 L290 585" fill="none" stroke="#9b710b" stroke-width="4" stroke-dasharray="10 7" marker-end="url(#goldArrow)"/>
|
||||
<path d="M525 490 L470 545" fill="none" stroke="#9b710b" stroke-width="4" stroke-dasharray="10 7" marker-end="url(#goldArrow)"/>
|
||||
|
||||
<!-- x axis labels -->
|
||||
<g fill="#40513e" font-size="12"><text x="95" y="1032" text-anchor="middle">X=-15</text><text x="380" y="1032" text-anchor="middle">X=0</text><text x="665" y="1032" text-anchor="middle">X=15</text></g>
|
||||
</svg>
|
||||
|
||||
<div class="legend">
|
||||
<div><span class="swatch main"></span><span><strong>深灰色:</strong>两条主路。单位可以完全绕过资源点,直接推敌方城堡。</span></div>
|
||||
<div><span class="swatch side"></span><span><strong>黄色:</strong>资源支路。只有玩家把目标指向资源点时,单位才会走这里。</span></div>
|
||||
<div><span class="swatch blue"></span><span><strong>蓝箭:</strong>我方从下向上推进。</span></div>
|
||||
<div><span class="swatch red"></span><span><strong>红箭:</strong>敌方从上向下推进。</span></div>
|
||||
<div class="good"><strong>核心:</strong>两条主路不交叉,双方不会因为去资源点而必然汇合。</div>
|
||||
<div><span class="swatch main"></span><span><strong>灰色:</strong>三条纵向路线。单位从主城出生后有较长行军距离。</span></div>
|
||||
<div><span class="swatch resource"></span><span><strong>黄色:</strong>公共资源点,放在主路外侧,不堵住直攻主城的路线。</span></div>
|
||||
<div><span class="swatch blue"></span><span><strong>蓝色:</strong>蓝方从下向上推进。</span></div>
|
||||
<div><span class="swatch red"></span><span><strong>红色:</strong>红方从上向下推进。</span></div>
|
||||
<div><span class="swatch boundary"></span><span><strong>虚线框:</strong>中央交战和资源争夺区域。</span></div>
|
||||
<div class="good"><strong>阅读方法:</strong>地图越往上 Z 越大。资源点坐标写在圆点内部,主城和出生点坐标写在对应标注中。</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>单位有两种选择</h2>
|
||||
<h2>建议坐标表</h2>
|
||||
<table>
|
||||
<thead><tr><th>玩家目标</th><th>单位实际路线</th><th>结果</th></tr></thead>
|
||||
<thead><tr><th>对象</th><th>建议世界坐标</th><th>作用</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>敌方城堡</td><td>沿左主路或右主路直走</td><td>直接推进,不经过资源点</td></tr>
|
||||
<tr><td>水晶矿</td><td>从左主路入口转入侧向支路</td><td>争夺能量收益,占领后再决定是否回主路</td></tr>
|
||||
<tr><td>混沌之门</td><td>从右主路入口转入侧向支路</td><td>争夺自动出兵收益,占领后再决定是否回主路</td></tr>
|
||||
<tr><td>途中遇到敌军</td><td>只在近距离视野内拦截</td><td><code>detectionRange = 8</code>,不会隔很远跨路追击</td></tr>
|
||||
<tr><td>蓝方主城</td><td class="coords">(0, 0, 8)</td><td>蓝方防守核心</td></tr>
|
||||
<tr><td>蓝方出生点</td><td class="coords">(0, 0, 14)</td><td>出生后先行军约 30 米再进入资源/交战区域</td></tr>
|
||||
<tr><td>水晶矿</td><td class="coords">(-7, 0, 45)</td><td>左侧公共资源,主路外侧</td></tr>
|
||||
<tr><td>混沌之门</td><td class="coords">(7, 0, 75)</td><td>右侧公共资源,主路外侧</td></tr>
|
||||
<tr><td>红方出生点</td><td class="coords">(0, 0, 95)</td><td>出生后先行军约 20 米再进入交战区域</td></tr>
|
||||
<tr><td>红方主城</td><td class="coords">(0, 0, 108)</td><td>红方防守核心</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="note">这是一套“地图长度约 100~120、宽度约 20~30”的示意坐标,不是要求你现在立即修改场景。先按图确认你想要的战略关系,再落地移动物体和调整 NavMesh。</p>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>不要采用的布局</h2>
|
||||
<p class="warn"><strong>资源点放在主路正中央:</strong>所有单位都必须经过资源点,资源点会变成必争的收费站,玩家无法选择速攻主城。</p>
|
||||
<p class="warn"><strong>两条路在中央重新合并:</strong>即使起点分成左右两路,单位最终仍会在合流处相遇,分兵只是视觉上的分路。</p>
|
||||
<p class="warn"><strong>两个资源点分别贴近己方城堡:</strong>双方到达时间不对称,资源点会天然属于距离更近的一方。</p>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>推荐的 Unity 落地规则</h2>
|
||||
<p>先做两条互不交叉的 NavMesh 主路。资源点放在主路外侧的凹袋中,各自连接一条短支路。支路入口与敌方主路保持足够距离,避免资源点支路直接变成双方必经的交叉口。</p>
|
||||
<p>资源点占领后,单位可以回到主路,也可以继续攻击城堡。第一版不需要做复杂的路口优先级,只要保证三件事:<strong>主路能绕过、支路可选择、两条主路不交叉</strong>。</p>
|
||||
<h2>相机只改 Z 轴范围的位置</h2>
|
||||
<p>选中 Hierarchy 的 <code>Main Camera</code>,在 Inspector 找到 <code>CameraFollow > 边界限制</code>:</p>
|
||||
<table>
|
||||
<thead><tr><th>字段</th><th>当前值</th><th>含义</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>Min Z</code></td><td class="coords">-30</td><td>相机允许移动到的最低 Z</td></tr>
|
||||
<tr><td><code>Max Z</code></td><td class="coords">91</td><td>相机允许移动到的最高 Z</td></tr>
|
||||
<tr><td><code>Orthographic Size</code></td><td>另行设置</td><td>镜头远近和可见范围,不属于 Z 轴边界</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>如果地图纵向扩大到 Z=108,至少要把 <code>Max Z</code> 调到能覆盖红方主城附近,例如 <code>115</code>。如果只想限制镜头移动范围,改 <code>Min Z / Max Z</code>;不要用它们来解决单位太小的问题。</p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
+90
-75
@@ -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——标志本身就是全局状态显示;
|
||||
- 逻辑:仅决定「下一次召唤单位的目标」,不影响场上已存活单位;
|
||||
- 默认标志=敌方主城堡,开局无需任何操作即可出兵推塔。
|
||||
|
||||
Reference in new issue
Block a user