1051 lines
46 KiB
C#
1051 lines
46 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
using UnityEngine.SceneManagement;
|
||
|
||
using RuntimeData;
|
||
using Logic;
|
||
using Logic.Action;
|
||
using Logic.AI;
|
||
using Logic.CrashSight;
|
||
using TH1_Anim;
|
||
using TH1_Core.Managers;
|
||
using TH1_Logic.Core;
|
||
using TH1_Presentation.Sequencer.Task;
|
||
using TH1_Renderer;
|
||
using TH1Resource;
|
||
using UnityEngine.UI;
|
||
|
||
namespace TH1Renderer
|
||
{
|
||
|
||
public class MapRenderer
|
||
{
|
||
private Main _main;
|
||
private static List<GridData> _aroundBuf;
|
||
private MapData _mapData;
|
||
private Transform _unitRenderMap;
|
||
private Transform _gridRenderMap;
|
||
public Transform HintRenderMap;
|
||
|
||
|
||
//BubbleManager
|
||
public InGameBubbleManager InGameBubbleManager;
|
||
|
||
Transform _cityInfoRenderMap;
|
||
public CameraController CameraController;
|
||
public EffectManager EffectManager; //特效管理器,管理目前所有特效
|
||
public ProjectileManager ProjectileManager;
|
||
|
||
private Transform _projectileRenderMap;
|
||
|
||
//存储map对应的主要GameObject
|
||
public GameObject ROMap;
|
||
|
||
private GameObject _unitPrefab; //承载单位图像的prefab
|
||
private GameObject _gridPrefab; //承载grid的prefab
|
||
private GameObject _cityInfoPrefab; //承载城市图形信息、人口条的prefab
|
||
|
||
public Dictionary<uint, GridRenderer> ROGridMap;
|
||
public Dictionary<uint, CityInfoRenderer> ROCityInfoMap;
|
||
public Dictionary<uint, UnitRenderer> ROUnitMap;
|
||
|
||
bool aniFlagExplorer = false; //用于探险家动画的标识bool
|
||
GameObject explorerUnit; //用于存储探险家图像
|
||
Vector2Int explorerNowPos; //用于存储探险家Data当前的位置
|
||
|
||
// 临时探索者游戏对象
|
||
private GameObject _temporaryExplorer = null;
|
||
private Vector3 _temporaryExplorerStartPos;
|
||
private float _temporaryExplorerTime = 0f;
|
||
private bool _temporaryExplorerActive = false;
|
||
|
||
//-------- 表现层数据map交互部分 --------//
|
||
//移动和攻击的高亮RenderData,set中存放了所有需要更新的对象
|
||
public HashSet<uint> HighlightGridIdSet = new HashSet<uint>();
|
||
public bool HighlightGridIdSetRenderMark = false;
|
||
public HashSet<uint> HighlightUnitIdSet = new HashSet<uint>();
|
||
public bool HighlightUnitIdSetRenderMark = false;
|
||
public UnitData SelectUnitData;
|
||
|
||
//-------- 伤害预测悬停 --------//
|
||
private uint _dmgPreviewHoverGridId;
|
||
private float _dmgPreviewHoverTimer;
|
||
private bool _dmgPreviewShowing;
|
||
private uint _dmgPreviewSelfUnitId;
|
||
private uint _dmgPreviewTargetUnitId;
|
||
private const float DmgPreviewHoverThreshold = 1.0f;
|
||
|
||
// 添加移动相关的成员变量
|
||
private Vector3 _explorerMoveStartPos;
|
||
private Vector3 _explorerMoveTargetPos;
|
||
private float _explorerMoveTime;
|
||
private float _explorerMoveDuration;
|
||
private bool _explorerIsMoving;
|
||
|
||
// 添加成员变量来记录上一个位置
|
||
private Vector2Int _explorerPreviousPosition = new Vector2Int(-1, -1); // 初始化为无效位置
|
||
|
||
// 添加成员变量来跟踪移动次数和存储当前目标格子
|
||
private int _explorerMoveCount = 0;
|
||
private int _explorerMaxMoves = 15;
|
||
private GridData _currentTargetGridData;
|
||
|
||
private static MapRenderer _instance;
|
||
public static MapRenderer Instance
|
||
{
|
||
get
|
||
{
|
||
if (_instance == null)
|
||
{
|
||
Debug.LogError("MapRenderer not initialized. Call Initialize(main, mapData) first.");
|
||
}
|
||
return _instance;
|
||
}
|
||
}
|
||
|
||
public static void Initialize(Main main, MapData mapData)
|
||
{
|
||
if (_instance != null)
|
||
{
|
||
Debug.LogWarning("MapRenderer already initialized. Reinitializing...");
|
||
}
|
||
_instance = new MapRenderer(main, mapData);
|
||
}
|
||
|
||
public static void Dispose()
|
||
{
|
||
_instance = null;
|
||
}
|
||
|
||
public static void TryUpdate()
|
||
{
|
||
if (_instance != null)
|
||
{
|
||
_instance.Update();
|
||
}
|
||
}
|
||
|
||
private MapRenderer(Main main, MapData mapData)
|
||
{
|
||
_main = main;
|
||
_mapData = mapData;
|
||
ROMap = main.ROMapRenderer;
|
||
//初始化Data->Renderer的dict
|
||
ROGridMap = new Dictionary<uint, GridRenderer>();
|
||
ROCityInfoMap = new Dictionary<uint, CityInfoRenderer>();
|
||
ROUnitMap = new Dictionary<uint, UnitRenderer>();
|
||
|
||
// 初始化主要的渲染模块(grid city unit projectileManager)
|
||
InitGridCityUnitObject();
|
||
|
||
//初始化相机模块
|
||
InitCameraObject();
|
||
|
||
//初始化BubbleManager
|
||
InGameBubbleManager = new InGameBubbleManager();
|
||
InGameBubbleManager.Init(ROMap.transform.Find("HintMap"));
|
||
}
|
||
|
||
|
||
//当退出游戏或者结束游戏的时候出发,用来清理一些视觉对象
|
||
|
||
//InGame游戏开始的生命周期
|
||
public static void OnMatchStart(Main main, MapData mapData)
|
||
{
|
||
Dispose();
|
||
Initialize(main,mapData);
|
||
_instance.InGameBubbleManager.OnGameStart();
|
||
|
||
// 根据地图尺寸动态计算相机边界
|
||
if (_instance.CameraController != null && mapData?.MapConfig != null)
|
||
{
|
||
_instance.CameraController.CalculateBoundsFromMapSize(mapData.MapConfig.Width, mapData.MapConfig.Height);
|
||
}
|
||
}
|
||
|
||
//InGame游戏结束时的生命周期
|
||
public void OnMatchEnd()
|
||
{
|
||
InGameBubbleManager.OnGameClosed();
|
||
}
|
||
private void ClearAllChildren(Transform parent)
|
||
{
|
||
if (parent == null) return;
|
||
for (int i = parent.childCount - 1; i >= 0; i--)
|
||
{
|
||
GameObject.Destroy(parent.GetChild(i).gameObject);
|
||
}
|
||
}
|
||
|
||
public bool InitGridCityUnitObject()
|
||
{
|
||
|
||
|
||
//重新建立新的关联
|
||
if (ROMap == null) return false;
|
||
|
||
_unitRenderMap = ROMap.transform.Find("UnitMap");
|
||
_gridRenderMap = ROMap.transform.Find("GridMap");
|
||
|
||
_cityInfoRenderMap = ROMap.transform.Find("CityInfoMap")?.GetComponent<Transform>();
|
||
|
||
//清理战场,把上一局Grid里面留下的东西清理干净
|
||
ClearAllChildren(_unitRenderMap);
|
||
ClearAllChildren(_gridRenderMap);
|
||
ClearAllChildren(_cityInfoRenderMap);
|
||
ClearAllChildren(HintRenderMap);
|
||
ClearAllChildren(ROMap.transform.Find("ProjectileMap"));
|
||
|
||
_unitPrefab = Resources.Load<GameObject>($"Prefab/unitPrefab");
|
||
_gridPrefab = Resources.Load<GameObject>($"Prefab/tilePrefab");
|
||
_cityInfoPrefab = Resources.Load<GameObject>($"Prefab/cityInfoMapPrefab");
|
||
//建立子模块的manager,目前仅有projectile
|
||
//TODO 这里要改成ProjectManager依赖注入,然后要上面ClearALL要把Projectilemanager算进去
|
||
ProjectileManager = new ProjectileManager();
|
||
ProjectileManager.Init(ROMap.transform.Find("ProjectileMap"));
|
||
return true;
|
||
}
|
||
|
||
public bool InitCameraObject()
|
||
{
|
||
var cameraObject = GameObject.Find("Main Camera");
|
||
if (cameraObject == null) return false;
|
||
CameraController = cameraObject.GetComponent<CameraController>();
|
||
return true;
|
||
}
|
||
|
||
|
||
public void Update()
|
||
{
|
||
|
||
//处理所有飞行道具。这个不会被暂停
|
||
ProjectileManager.Update();
|
||
|
||
//HintManager
|
||
InGameBubbleManager.Update();
|
||
|
||
// 处理临时探索者的浮动动画
|
||
if (_temporaryExplorerActive && _temporaryExplorer != null)
|
||
{
|
||
_temporaryExplorerTime += Time.deltaTime;
|
||
float yOffset = 0.2f * Mathf.Sin(2f * _temporaryExplorerTime); // 0.2f是浮动幅度,2f是浮动频率
|
||
|
||
// 如果正在移动,则不应用浮动效果
|
||
if (!_explorerIsMoving)
|
||
{
|
||
_temporaryExplorer.transform.position = new Vector3(
|
||
_temporaryExplorerStartPos.x,
|
||
_temporaryExplorerStartPos.y + yOffset,
|
||
_temporaryExplorerStartPos.z
|
||
);
|
||
}
|
||
}
|
||
|
||
//-------- 处理UI数据 ---------//
|
||
//处理高亮
|
||
if (HighlightGridIdSetRenderMark)
|
||
{
|
||
HighlightGridIdSetRenderMark = false;
|
||
foreach (var gid in HighlightGridIdSet)
|
||
ROGridMap[gid].RenderUpdataHighlight();
|
||
HighlightGridIdSet.Clear();
|
||
}
|
||
if (HighlightUnitIdSetRenderMark)
|
||
{
|
||
HighlightUnitIdSetRenderMark = false;
|
||
foreach (var uid in HighlightUnitIdSet)
|
||
if(ROUnitMap.TryGetValue(uid, out var unit))
|
||
unit.RenderUpdataHighlight();
|
||
|
||
|
||
HighlightUnitIdSet.Clear();
|
||
}
|
||
|
||
//处理CityMap或者UnitMap新增/删除的city和unit
|
||
if (_mapData.CityMap.CityMapRenderMark)
|
||
{
|
||
_mapData.CityMap.CityMapRenderMark = false;
|
||
RenderUpdateCityMap();
|
||
}
|
||
|
||
//-------- 处理子节点的Update -------//
|
||
|
||
|
||
foreach (var roUnit in ROUnitMap.Values)
|
||
roUnit.Update();
|
||
|
||
|
||
|
||
foreach (var roGrid in ROGridMap.Values)
|
||
roGrid.Update();
|
||
foreach (var roCityInfo in ROCityInfoMap.Values)
|
||
roCityInfo.Update();
|
||
|
||
//伤害预测悬停检测
|
||
UpdateDamagePreview();
|
||
}
|
||
|
||
#region [-------------------- Damage Preview --------------------]
|
||
|
||
private void UpdateDamagePreview()
|
||
{
|
||
// 前提:有选中的己方单位
|
||
if (SelectUnitData == null || !SelectUnitData.IsAlive())
|
||
{
|
||
if (_dmgPreviewShowing) ClearDamagePreview();
|
||
return;
|
||
}
|
||
|
||
// 获取鼠标当前grid
|
||
if (Camera.main == null) return;
|
||
Vector3 mouseWorldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
|
||
Vector2Int cellPos = Table.Instance.WorldToGrid(mouseWorldPos);
|
||
if (cellPos.x < 0 || cellPos.x >= _mapData.MapConfig.Width ||
|
||
cellPos.y < 0 || cellPos.y >= _mapData.MapConfig.Height)
|
||
{
|
||
if (_dmgPreviewShowing) ClearDamagePreview();
|
||
return;
|
||
}
|
||
|
||
_mapData.GridMap.GetGridDataByPos(cellPos.x, cellPos.y, out var hoverGrid);
|
||
if (hoverGrid == null)
|
||
{
|
||
if (_dmgPreviewShowing) ClearDamagePreview();
|
||
return;
|
||
}
|
||
|
||
// 格子变了 → 清除并重置计时
|
||
if (hoverGrid.Id != _dmgPreviewHoverGridId)
|
||
{
|
||
if (_dmgPreviewShowing) ClearDamagePreview();
|
||
_dmgPreviewHoverGridId = hoverGrid.Id;
|
||
_dmgPreviewHoverTimer = 0f;
|
||
return;
|
||
}
|
||
|
||
// 已经在显示中,不需要重复计算
|
||
if (_dmgPreviewShowing) return;
|
||
|
||
// 累加计时
|
||
_dmgPreviewHoverTimer += Time.deltaTime;
|
||
if (_dmgPreviewHoverTimer < DmgPreviewHoverThreshold) return;
|
||
|
||
// 到达0.5s,检查是否有敌方可见单位
|
||
var selfPlayer = _mapData.PlayerMap.SelfPlayerData;
|
||
if (!hoverGrid.VisibleUnit(_mapData, selfPlayer, out var targetUnit)) return;
|
||
// 必须是敌方单位(非同盟)
|
||
if (_mapData.SameUnionByUnitId(SelectUnitData.Id, targetUnit.Id)) return;
|
||
// 必须在视野内
|
||
if (!selfPlayer.Sight.CheckIsInSight(hoverGrid.Id)) return;
|
||
|
||
// 通过DeepCopy map + 模拟真实攻击来获取准确伤害预测
|
||
int attackerHpBefore = SelectUnitData.Health;
|
||
int defenderHpBefore = targetUnit.Health;
|
||
|
||
AIActionScoreCalculator.RefreshCalMap(_mapData);
|
||
var calMap = AIActionScoreCalculator.CalMap;
|
||
calMap.DeepCopy(_mapData);
|
||
|
||
if (!calMap.GetPlayerDataByUnitId(SelectUnitData.Id, out var calPlayer)) return;
|
||
if (!calMap.UnitMap.GetUnitDataByUnitId(SelectUnitData.Id, out var calAttacker)) return;
|
||
if (!calMap.UnitMap.GetUnitDataByUnitId(targetUnit.Id, out var calDefender)) return;
|
||
|
||
var attackId = new CommonActionId { ActionType = CommonActionType.UnitAttack };
|
||
var attackAction = new UnitAttackAction(attackId);
|
||
var param = new CommonActionParams(
|
||
mapData: calMap,
|
||
playerData: calPlayer,
|
||
unitData: calAttacker,
|
||
targetUnit: calDefender,
|
||
mainObjectType: MainObjectType.Unit
|
||
);
|
||
param.OnParamChanged();
|
||
attackAction.CompleteExecute(param);
|
||
|
||
// 从模拟结果读取双方实际伤害
|
||
int dmgToTarget = defenderHpBefore - (calDefender.IsAlive() ? calDefender.Health : 0);
|
||
int dmgToSelf = attackerHpBefore - (calAttacker.IsAlive() ? calAttacker.Health : 0);
|
||
|
||
// 显示:敌方头上显示我方对其伤害(红色)
|
||
if (dmgToTarget > 0 && ROUnitMap.TryGetValue(targetUnit.Id, out var targetRenderer))
|
||
targetRenderer.ShowDamagePreview(dmgToTarget, Color.red);
|
||
|
||
// 己方头上显示反击伤害(橙色)
|
||
if (dmgToSelf > 0 && ROUnitMap.TryGetValue(SelectUnitData.Id, out var selfRenderer))
|
||
selfRenderer.ShowDamagePreview(dmgToSelf, new Color(1f, 0.5f, 0f));
|
||
|
||
_dmgPreviewShowing = true;
|
||
_dmgPreviewSelfUnitId = SelectUnitData.Id;
|
||
_dmgPreviewTargetUnitId = targetUnit.Id;
|
||
}
|
||
|
||
public void ClearDamagePreview()
|
||
{
|
||
if (!_dmgPreviewShowing) return;
|
||
|
||
if (ROUnitMap.TryGetValue(_dmgPreviewTargetUnitId, out var targetRenderer))
|
||
targetRenderer.HideDamagePreview();
|
||
if (ROUnitMap.TryGetValue(_dmgPreviewSelfUnitId, out var selfRenderer))
|
||
selfRenderer.HideDamagePreview();
|
||
|
||
_dmgPreviewShowing = false;
|
||
_dmgPreviewSelfUnitId = 0;
|
||
_dmgPreviewTargetUnitId = 0;
|
||
}
|
||
|
||
#endregion
|
||
|
||
//当gridMap出现新的对象时,新建对象
|
||
public void RenderUpdateGridMap()
|
||
{
|
||
foreach (var gridData in Main.MapData.GridMap.GridList)
|
||
{
|
||
|
||
ROGridMap[gridData.Id] =
|
||
new GridRenderer(_gridPrefab, _gridRenderMap, gridData.Id, Main.MapData, _main);
|
||
|
||
}
|
||
}
|
||
|
||
//当cityMap出现新的对象时,新建对象
|
||
public void RenderUpdateCityMap()
|
||
{
|
||
foreach (var cityData in Main.MapData.CityMap.CityList)
|
||
if(!ROCityInfoMap.ContainsKey(cityData.Id))
|
||
{
|
||
//生成城镇图像
|
||
RenderUpdateCityBuildings(cityData.Id);
|
||
//生成城镇名称和人口条
|
||
ROCityInfoMap[cityData.Id] = new CityInfoRenderer(_cityInfoPrefab,_cityInfoRenderMap,cityData.Id,Main.MapData,_main);
|
||
cityData.SetCityRenderer(Main.MapData);
|
||
}
|
||
}
|
||
|
||
public void UpdateCityInfoAllCoinTech()
|
||
{
|
||
foreach (var cityInfo in ROCityInfoMap.Values)
|
||
cityInfo.InstantUpdateCityCoinTechCulture();
|
||
}
|
||
|
||
//当unitMap出现新的对象时,新建对象
|
||
public void RenderUpdateUnitMap()
|
||
{
|
||
foreach (var unitData in Main.MapData.UnitMap.UnitList)
|
||
if(!ROUnitMap.ContainsKey(unitData.Id))
|
||
{
|
||
//生成单位图像
|
||
ROUnitMap[unitData.Id] = new UnitRenderer(_unitPrefab,_unitRenderMap,unitData.Id);
|
||
//立刻更新每个unit的视觉
|
||
ROUnitMap[unitData.Id].InstantUpdateUnit(true);
|
||
}
|
||
}
|
||
|
||
//当projectileMap出现新的对象时,新建对象
|
||
public void RenderUpdateProjectileMap()
|
||
{
|
||
|
||
}
|
||
|
||
// 初次渲染地图
|
||
public void FirstRenderMap()
|
||
{
|
||
//初次渲染grid
|
||
RenderUpdateGridMap();
|
||
//初次渲染city
|
||
RenderUpdateCityMap();
|
||
//初次渲染所有unit
|
||
RenderUpdateUnitMap();
|
||
}
|
||
|
||
public void RenderUpdateBorders() //更新全地图的所有边界
|
||
{
|
||
foreach (var gridData in Main.MapData.GridMap.GridList)
|
||
ROGridMap[gridData.Id].UpdateBorder();
|
||
}
|
||
|
||
private void RenderUpdateCityBuildings(uint cityId) //更新主城建筑的渲染
|
||
{
|
||
Main.MapData.GetGridIdByCityId(cityId, out var gridId);
|
||
ROGridMap[gridId].RenderUpdateCityBuilding(cityId);
|
||
}
|
||
|
||
public bool SetUnitAllMoveAttackTargetHighlight(uint uid) //渲染所有可移动位置的高亮,其中可以攻击的位置要标红,如果是自己人或者敌人在移动范围内但是不在攻击范围内,则不能高亮
|
||
{
|
||
//Step #1 计算MoveInfo
|
||
bool ret = false;
|
||
Main.UnitLogic.CalcUnitMoveInfo(Main.MapData, uid);
|
||
Main.MapData.UnitMap.GetUnitDataByUnitId(uid, out var unitData);
|
||
Main.MapData.GetGridDataByUnitId(uid, out var gridData);
|
||
Main.MapData.GetPlayerDataByUnitId(uid, out var playerData);
|
||
//unitLogic.DebugOutputMoveInfo();
|
||
int r = Mathf.Max((Table.Instance.UnitTypeDataAssets.GetUnitTypeInfo(unitData.UnitType,unitData.GiantType,unitData.UnitLevel,out var info)?info.MoveRange:0) * 3 + 1,
|
||
Table.Instance.UnitTypeDataAssets.GetUnitTypeInfo(unitData.UnitType,unitData.GiantType,unitData.UnitLevel,out var info2)?info2.AttackRange:0) * 2;
|
||
|
||
//Step #2 遍历所有范围内格子,逐个判定是attack move还是别的
|
||
//Step #2 - 1 如果有citytransport或者是allytransport或者moriyaknightmove,要把r设置为全图大小
|
||
if(unitData.IsCanTransport())
|
||
r = (int)(_mapData.MapConfig.Width > _mapData.MapConfig.Height ? _mapData.MapConfig.Width : _mapData.MapConfig.Height);
|
||
|
||
var targetGridDataList = Main.MapData.GridMap.GetAroundGridDataSet_NOCENTER(r,r,gridData);
|
||
|
||
//Step #2 - 2 特殊处理kanako
|
||
if (unitData.GetSkill(SkillType.KANAKOBATTLEFIELDPRO, out var _) &&
|
||
unitData.GetSkill(SkillType.KANAKOSITTING, out var _))
|
||
{
|
||
foreach (var tmpunit in Main.MapData.UnitMap.UnitList)
|
||
if(tmpunit.Player(Main.MapData) == playerData &&
|
||
(tmpunit.GiantType != GiantType.None || tmpunit.UnitType == UnitType.MoriyaHebi))
|
||
{
|
||
if (tmpunit.UnitType == UnitType.MoriyaHebi && tmpunit.UnitLevel < 3) continue;
|
||
if (tmpunit.Grid(Main.MapData).Feature != TerrainFeature.Mountain) continue;
|
||
var tmpSet = Main.MapData.GridMap.GetAroundGridDataSet_NOCENTER(1,1,tmpunit.Grid(Main.MapData));
|
||
foreach(var tmpGrid in tmpSet)
|
||
targetGridDataList.Add(tmpGrid);
|
||
}
|
||
}
|
||
|
||
bool hasUtsuhoBase = unitData.GetSkill(SkillType.UtsuhoBase, out var _);
|
||
foreach(var targetGridData in targetGridDataList)
|
||
{
|
||
//如果不在视野 跳过(UtsuhoBase无视视野限制)
|
||
if (!hasUtsuhoBase && !playerData.Sight.CheckIsInSight(targetGridData.Id)) continue;
|
||
|
||
// --------------------------------------------------------------------------------------------------------别找了 在这呢 --------------------------------------------------------------------------------------------------------
|
||
//获得该格子具体的可操作情况(移动?攻击?友方施法?)
|
||
var sig = Main.UnitLogic.CheckUnitCanMoveOrAttack(Main.MapData, unitData, targetGridData);
|
||
//如果是移动目标,且unit的MP>0
|
||
if ((sig is MoveAttackType.Move or MoveAttackType.MoveToPort or MoveAttackType.MoveAshore or MoveAttackType.MoveTeleport) && unitData.GetActionPoint(ActionPointType.Move) > 0)
|
||
{
|
||
ret = true;
|
||
ROGridMap[targetGridData.Id].SetMoveHighlight(true);
|
||
}
|
||
//如果是攻击目标,且unit的AP>0
|
||
if (sig == MoveAttackType.Attack && unitData.GetActionPoint(ActionPointType.Attack) > 0 && !unitData.IsLimitSelfAttack(Main.MapData))
|
||
{
|
||
ret = true;
|
||
if (!targetGridData.MainSelfPlayerVisibleUnit( out var unitDataB))
|
||
continue;
|
||
|
||
//如果可以杀死,做一个提示
|
||
if (Table.Instance.CalcDamage(Main.MapData, unitData, unitDataB) >= unitDataB.Health)
|
||
{
|
||
targetGridData.Renderer(Main.MapData)?.PlayVFXInSight(new GridVFXParams(GridVFXType.DieHint),GridVFXPlayType.Play);
|
||
}
|
||
//如果会被杀死
|
||
else if(Main.UnitLogic.CanCounter(Main.MapData, unitData, unitDataB) &&
|
||
Table.Instance.CalcCounterDamage(Main.MapData, unitData,unitDataB) >= unitData.Health
|
||
&& unitData.UnitFullType.UnitType != UnitType.Minder)
|
||
{
|
||
targetGridData.Renderer(Main.MapData)?.PlayVFXInSight(new GridVFXParams(GridVFXType.CounterDieHint),GridVFXPlayType.Play);
|
||
}
|
||
ROUnitMap[unitDataB.Id].SetAttackHighlight(true);
|
||
}
|
||
//如果是友军目标,且unit的AP>0
|
||
if (sig == MoveAttackType.Ally && unitData.GetActionPoint(ActionPointType.Attack) > 0)
|
||
{
|
||
ret = true;
|
||
if (!targetGridData.MainSelfPlayerVisibleUnit(out var unitDataB))
|
||
continue;
|
||
|
||
ROUnitMap[unitDataB.Id].SetAllyHighlight(true);
|
||
}
|
||
//如果是攻击ground目标,且unit的AP>0
|
||
if (sig == MoveAttackType.AttackGround && unitData.GetActionPoint(ActionPointType.Attack) > 0)
|
||
{
|
||
ret = true;
|
||
ROGridMap[targetGridData.Id].SetAttackHighlight(true);
|
||
}
|
||
// UtsuhoBase特殊处理:移动范围内有单位的格子也显示可移动高亮
|
||
if (sig == MoveAttackType.None && unitData.GetSkill(SkillType.UtsuhoBase, out var _)
|
||
&& unitData.GetActionPoint(ActionPointType.Move) > 0
|
||
&& Main.UnitLogic.IsFinalReachable(targetGridData.Pos.X, targetGridData.Pos.Y))
|
||
{
|
||
ret = true;
|
||
ROGridMap[targetGridData.Id].SetMoveHighlight(true);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
return ret;
|
||
}
|
||
//返回一个Unit是否存在移动或者攻击目标
|
||
public bool CheckUnitHasMoveAttackTarget(uint uid)
|
||
{
|
||
Main.UnitLogic.CalcUnitMoveInfo(Main.MapData, uid);
|
||
Main.MapData.UnitMap.GetUnitDataByUnitId(uid, out var unitData);
|
||
Main.MapData.GetGridDataByUnitId(uid, out var gridData);
|
||
Main.MapData.GetPlayerDataByUnitId(uid, out var playerData);
|
||
if (unitData == null || gridData == null || playerData == null) return false;
|
||
if (unitData.GetActionPoint(ActionPointType.Attack) <= 0 && unitData.GetActionPoint(ActionPointType.Move) <= 0) return false;
|
||
//unitLogic.DebugOutputMoveInfo();
|
||
int r = Mathf.Max((Table.Instance.UnitTypeDataAssets.GetUnitTypeInfo(unitData.UnitType,unitData.GiantType,unitData.UnitLevel,out var info)?info.MoveRange:0) * 2,
|
||
Table.Instance.UnitTypeDataAssets.GetUnitTypeInfo(unitData.UnitType,unitData.GiantType,unitData.UnitLevel,out var info2)?info2.AttackRange:0);
|
||
_aroundBuf ??= new List<GridData>();
|
||
_aroundBuf.Clear();
|
||
Main.MapData.GridMap.GetAroundGridDataSet_NOCENTER(r,r,gridData, _aroundBuf);
|
||
bool hasUtsuhoBase2 = unitData.GetSkill(SkillType.UtsuhoBase, out var _);
|
||
foreach(var targetGridData in _aroundBuf)
|
||
{
|
||
//如果不在视野 跳过(UtsuhoBase无视视野限制)
|
||
if (!hasUtsuhoBase2 && !playerData.Sight.CheckIsInSight(targetGridData.Id)) continue;
|
||
var sig = Main.UnitLogic.CheckUnitCanMoveOrAttack(Main.MapData, unitData, targetGridData);
|
||
//如果是移动目标,且unit的MP>0
|
||
if ((sig == MoveAttackType.Move || sig == MoveAttackType.MoveToPort ||
|
||
sig == MoveAttackType.MoveAshore) && unitData.GetActionPoint(ActionPointType.Move) > 0)
|
||
return true;
|
||
|
||
//如果是攻击目标,且unit的AP>0
|
||
if (sig == MoveAttackType.Attack && unitData.GetActionPoint(ActionPointType.Attack) > 0 && !unitData.IsLimitSelfAttack(Main.MapData))
|
||
return true;
|
||
//如果是Ally目标,且unit的AP>0
|
||
if (sig == MoveAttackType.Ally && unitData.GetActionPoint(ActionPointType.Attack) > 0 )
|
||
return true;
|
||
//如果是AttackGround,且unit的AP>0
|
||
if (sig == MoveAttackType.AttackGround && unitData.GetActionPoint(ActionPointType.Attack) > 0 )
|
||
return true;
|
||
// UtsuhoBase特殊处理:移动范围内有单位的格子也视为有目标
|
||
if (sig == MoveAttackType.None && unitData.GetSkill(SkillType.UtsuhoBase, out var _)
|
||
&& unitData.GetActionPoint(ActionPointType.Move) > 0
|
||
&& Main.UnitLogic.IsFinalReachable(targetGridData.Pos.X, targetGridData.Pos.Y))
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
//返回一个Unit是否当前可以占领遗迹/城市/收集starfish
|
||
public bool CheckUnitHasSpecialUnitActionTarget(uint uid)
|
||
{
|
||
if (!_mapData.UnitMap.GetUnitDataByUnitId(uid, out var unit)) return false;
|
||
if (!_mapData.GetGridDataByUnitId(uid, out var grid)) return false;
|
||
if (!_mapData.GetPlayerDataByUnitId(uid, out var player)) return false;
|
||
if (unit.GetActionPoint(ActionPointType.Capture) <= 0) return false;
|
||
//宝藏
|
||
if (grid.Resource == ResourceType.Treasure) return true;
|
||
//海星
|
||
if (grid.Resource == ResourceType.Starfish && player.TechTree.CheckIfHasTech(TechType.Navigation)) return true;
|
||
//城市
|
||
if (grid.Resource == ResourceType.CityCenter)
|
||
{
|
||
_mapData.GetPlayerDataByTerritoryGridId(grid.Id, out var gridPlayer);
|
||
//村庄或者他国城市
|
||
if (gridPlayer == null) return true;
|
||
if (gridPlayer.Id != player.Id) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 创建临时探索者并向视野最少的方向移动
|
||
public void CreateTemporaryExplorer(MapData map,GridData gridData, float maxDuration)
|
||
{
|
||
// Step #1 鲁棒性判断
|
||
if (!Main.MapData.GetPlayerDataByTerritoryGridId(gridData.Id, out var playerData))
|
||
{
|
||
Debug.LogWarning("无法获取玩家数据,无法创建探索者");
|
||
return;
|
||
}
|
||
|
||
// Step #2 如果不是自己的玩家,直接计算10步探索并更新视野,不进行渲染
|
||
|
||
// 重置移动计数和当前目标格子
|
||
_explorerMoveCount = 0;
|
||
_currentTargetGridData = gridData;
|
||
_explorerPreviousPosition = new Vector2Int(-1, -1);
|
||
|
||
|
||
GameObject spriteObject;
|
||
//如果是真人玩家,提前设置对象
|
||
if (playerData.IsSelfPlayer())
|
||
{
|
||
_temporaryExplorer = new GameObject("TemporaryExplorer");
|
||
_temporaryExplorer.transform.position = Table.Instance.GridToWorld(gridData, "isUnit");
|
||
_temporaryExplorer.transform.SetParent(_unitRenderMap, false);
|
||
spriteObject = new GameObject("UnitSprite");
|
||
spriteObject.transform.SetParent(_temporaryExplorer.transform, false);
|
||
SpriteRenderer spriteRenderer = spriteObject.AddComponent<SpriteRenderer>();
|
||
//把小人图像位置下移一点
|
||
spriteObject.transform.localScale = new Vector3(0.8f, 0.8f, 0.8f);
|
||
var t = spriteObject.transform.localPosition;
|
||
t.y -= 0.4f;
|
||
spriteObject.transform.localPosition = t;
|
||
|
||
// 设置explorer的sprite
|
||
UnitType explorerUnitType = UnitType.Warrior; // 使用战士单位类型
|
||
if (Table.Instance.UnitTypeDataAssets.GetUnitTypeInfo(explorerUnitType, GiantType.None, 0, out var unitTypeInfo)
|
||
&& Table.Instance.UnitTypeDataAssets.GetUnitSpriteByInfo(unitTypeInfo, playerData, out var sprite))
|
||
spriteRenderer.sprite = sprite;
|
||
}
|
||
|
||
// 直接计算15步探索
|
||
for (int i = 0; i < _explorerMaxMoves; i++)
|
||
{
|
||
// 如果当前目标格子为空,结束探索
|
||
if (_currentTargetGridData == null)
|
||
break;
|
||
|
||
// 计算下一步移动
|
||
Vector2Int currentPos = new Vector2Int(_currentTargetGridData.Pos.X, _currentTargetGridData.Pos.Y);
|
||
|
||
|
||
// 保存当前位置作为上一个位置
|
||
Vector2Int prevPos = currentPos;
|
||
_explorerPreviousPosition = prevPos;
|
||
|
||
// 计算下一步移动方向(不渲染,只计算)
|
||
CalculateNextExplorerMove(map,_currentTargetGridData, playerData, out var nextGridData);
|
||
|
||
//Step #5 处理视觉(只有自己玩家才会做
|
||
if (playerData.IsSelfPlayer())
|
||
{
|
||
// 如果无法移动,结束探索 TODO 有一点点风险,之后处理吧 现在无法移动就改成原地移动了
|
||
if (nextGridData == null)
|
||
nextGridData = _currentTargetGridData;
|
||
|
||
bool lastMove = i + 1 == _explorerMaxMoves;
|
||
FragmentMoveExplorerType moveType = FragmentMoveExplorerType.NormalMove;
|
||
if(i == 0)
|
||
moveType = FragmentMoveExplorerType.FirstMove;
|
||
if (i + 1 == _explorerMaxMoves)
|
||
moveType = FragmentMoveExplorerType.LastMove;
|
||
var data = FragmentDataFactory.Create(FragmentType.MoveExplorer,_temporaryExplorer,_currentTargetGridData,nextGridData,moveType);
|
||
PresentationManager.EnqueueTask(new FragmentSequencerTask(FragmentFactory.Create(FragmentType.MoveExplorer,data)));
|
||
|
||
}
|
||
|
||
//Step #6 更新视野
|
||
var radius = 1;
|
||
if (nextGridData.Feature == TerrainFeature.Mountain) radius = 2;
|
||
Main.PlayerLogic.UpdateSightByRadius_LogicView(Main.MapData, playerData, nextGridData, radius);
|
||
|
||
|
||
// 更新当前目标格子
|
||
_currentTargetGridData = nextGridData;
|
||
_explorerMoveCount++;
|
||
}
|
||
|
||
// 完成探索后解锁输入 ,只有是玩家才需要解锁输入
|
||
if(playerData.Id == Main.MapData.PlayerMap.SelfPlayerId)
|
||
_main.InputLogic.UnlockInput();
|
||
}
|
||
|
||
// 修复GetDirectionIndex方法中的错误
|
||
private int GetDirectionIndex(int dx, int dy)
|
||
{
|
||
// 将dx和dy归一化为-1, 0, 1
|
||
int nx = dx == 0 ? 0 : (dx > 0 ? 1 : -1);
|
||
int ny = dy == 0 ? 0 : (dy > 0 ? 1 : -1);
|
||
|
||
// 根据归一化后的方向确定索引
|
||
switch (nx)
|
||
{
|
||
case 1:
|
||
switch (ny)
|
||
{
|
||
case 1: return 1; // 右上
|
||
case 0: return 0; // 右
|
||
case -1: return 7; // 右下
|
||
}
|
||
break;
|
||
case 0:
|
||
switch (ny)
|
||
{
|
||
case 1: return 2; // 上
|
||
case -1: return 6; // 下
|
||
}
|
||
break;
|
||
case -1:
|
||
switch (ny)
|
||
{
|
||
case 1: return 3; // 左上
|
||
case 0: return 4; // 左
|
||
case -1: return 5; // 左下
|
||
}
|
||
break;
|
||
}
|
||
|
||
// 默认返回右方向
|
||
return 0;
|
||
}
|
||
|
||
// 根据方向索引获取移动方向
|
||
private Vector2Int GetMoveDirectionFromIndex(int dirIndex)
|
||
{
|
||
switch (dirIndex)
|
||
{
|
||
case 0: return new Vector2Int(1, 0); // 右
|
||
case 1: return new Vector2Int(1, 1); // 右上
|
||
case 2: return new Vector2Int(0, 1); // 上
|
||
case 3: return new Vector2Int(-1, 1); // 左上
|
||
case 4: return new Vector2Int(-1, 0); // 左
|
||
case 5: return new Vector2Int(-1, -1); // 左下
|
||
case 6: return new Vector2Int(0, -1); // 下
|
||
case 7: return new Vector2Int(1, -1); // 右下
|
||
default: return new Vector2Int(0, 0); // 默认不移动
|
||
}
|
||
}
|
||
|
||
|
||
// -------------------------- 以下是一组给explorere计算使用的参数 ----------------------------
|
||
private bool CheckCanMove(PlayerData player, Vector2Int pos)
|
||
{
|
||
if (!Main.MapData.GridMap.GetGridDataByPos(pos.x, pos.y, out var grid)) return false;
|
||
return Main.UnitLogic.CheckUnitAbleForGrid_OfflineStatus(Main.MapData,player,null, grid);
|
||
}
|
||
|
||
|
||
|
||
|
||
//返回一个格子的"探索值“,即最近的未探索格子的寻路距离
|
||
private int CalcExploreValue(MapData map,PlayerData player,Vector2Int startPos)
|
||
{
|
||
int[,] CanMove = new int[map.MapConfig.Width, map.MapConfig.Height];
|
||
for(int i = 0;i < map.MapConfig.Width;i++)
|
||
for (int j = 0; j < map.MapConfig.Height; j++)
|
||
CanMove[i, j] = -1;
|
||
// 2. 边界和起始点有效性检查
|
||
if (startPos.x < 0 || startPos.x >= 30 || startPos.y < 0 || startPos.y >= 30 || !CheckCanMove(player,startPos))
|
||
{
|
||
return 1000 ; // 起始点不合法,直接返回
|
||
}
|
||
// 3. 初始化队列并将起点加入
|
||
var queue = new Queue<Vector2Int>();
|
||
queue.Enqueue(startPos);
|
||
CanMove[startPos.x, startPos.y] = 0; // 标记起点已访问
|
||
// 4. BFS主循环
|
||
while (queue.Count > 0)
|
||
{
|
||
var current = queue.Dequeue();
|
||
// 检查8个方向的邻居
|
||
for (int i = 0; i < 8; i++)
|
||
{
|
||
var dir = GetMoveDirectionFromIndex(i);
|
||
var neighbor = current + dir;
|
||
// 5. 检查邻居的合法性:在界内、之前未访问过、且可以通过CheckCan
|
||
if (neighbor.x < 0 || neighbor.x >= map.MapConfig.Width ||
|
||
neighbor.y < 0 || neighbor.y >= map.MapConfig.Height) continue;
|
||
if (CanMove[neighbor.x, neighbor.y] != -1) continue;
|
||
if (!map.GridMap.GetGridDataByPos(neighbor.x, neighbor.y, out var gridData)) continue;
|
||
|
||
//找到一个没有开视野的格子,返回!
|
||
if (!player.Sight.CheckIsInSight(gridData.Id))
|
||
{
|
||
return CanMove[current.x, current.y];
|
||
}
|
||
|
||
if(CheckCanMove(player, neighbor))
|
||
{
|
||
CanMove[neighbor.x, neighbor.y] = CanMove[current.x, current.y] + 1; // 标记为可达/已访问
|
||
queue.Enqueue(neighbor); // 加入队列等待处理
|
||
}
|
||
}
|
||
}
|
||
return 999;
|
||
|
||
}
|
||
// -------------------------- 以上 ----------------------------
|
||
|
||
|
||
|
||
// 新增辅助方法:计算下一步移动但不执行渲染
|
||
private bool CalculateNextExplorerMove(MapData map,GridData startGridData, PlayerData playerData, out GridData nextGridData)
|
||
{
|
||
nextGridData = null;
|
||
|
||
// 当前位置
|
||
Vector2Int currentPos = new Vector2Int(startGridData.Pos.X, startGridData.Pos.Y);
|
||
|
||
// 地图尺寸
|
||
int mapWidth = (int)Main.MapData.MapConfig.Width;
|
||
int mapHeight = (int)Main.MapData.MapConfig.Height;
|
||
|
||
// 存储每个方向的最近未探索格子距离
|
||
float[] directionMinDistance = new float[8];
|
||
for (int i = 0; i < 8; i++)
|
||
{
|
||
directionMinDistance[i] = float.MaxValue; // 初始化为最大值
|
||
}
|
||
|
||
// 找出未探索格子最近的方向
|
||
int minDistance = 1000;
|
||
List<int> bestDirections = new List<int>();
|
||
|
||
for (int i = 0; i < 8; i++)
|
||
{
|
||
|
||
Vector2Int dirVector = GetMoveDirectionFromIndex(i);
|
||
Vector2Int nextPos = currentPos + dirVector;
|
||
|
||
//Step #1 该方向首先得能走,不能走直接continue
|
||
if (!Main.MapData.GridMap.GetGridDataByPos(nextPos.x, nextPos.y, out var tempNextGridData)) continue;
|
||
if(!IsTerrainPassable(tempNextGridData, playerData)) continue;
|
||
|
||
//Step #2 获得改方向的探索值
|
||
int v = CalcExploreValue(map, playerData, nextPos);
|
||
if (v < minDistance)
|
||
{
|
||
bestDirections.Clear();
|
||
bestDirections.Add(i);
|
||
minDistance = v;
|
||
}
|
||
else if(v == minDistance)
|
||
{
|
||
bestDirections.Add(i);
|
||
}
|
||
}
|
||
|
||
// 如果没有找到最佳方向,尝试任何可行方向(除了上一个位置)
|
||
if (bestDirections.Count == 0)
|
||
{
|
||
for (int i = 0; i < 8; i++)
|
||
{
|
||
// 计算该方向的下一个位置
|
||
Vector2Int dirVector2 = GetMoveDirectionFromIndex(i);
|
||
Vector2Int nextPos = currentPos + dirVector2;
|
||
|
||
// 检查该位置是否在地图范围内
|
||
if (nextPos.x < 0 || nextPos.x >= mapWidth || nextPos.y < 0 || nextPos.y >= mapHeight)
|
||
continue;
|
||
|
||
// 检查该位置是否是上一个位置
|
||
/*if (_explorerPreviousPosition.x == nextPos.x && _explorerPreviousPosition.y == nextPos.y)
|
||
continue;*/
|
||
|
||
// 检查该位置是否可通行
|
||
GridData tempNextGridData;
|
||
if (!Main.MapData.GridMap.GetGridDataByPos(nextPos.x, nextPos.y, out tempNextGridData))
|
||
continue;
|
||
|
||
// 检查地形是否可通行
|
||
if (!IsTerrainPassable(tempNextGridData, playerData))
|
||
continue;
|
||
|
||
bestDirections.Add(i);
|
||
}
|
||
}
|
||
|
||
// 如果仍然没有找到可行方向,返回失败
|
||
if (bestDirections.Count == 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 随机选择一个最佳方向
|
||
|
||
var value = map.Net.GetRandom(map).Next(0, bestDirections.Count);
|
||
int selectedDirIndex = bestDirections[value];
|
||
|
||
// 根据选择的方向确定移动的目标位置
|
||
Vector2Int finalMoveDir = GetMoveDirectionFromIndex(selectedDirIndex);
|
||
Vector2Int targetPos = currentPos + finalMoveDir;
|
||
|
||
// 获取目标格子数据
|
||
return Main.MapData.GridMap.GetGridDataByPos(targetPos.x, targetPos.y, out nextGridData);
|
||
}
|
||
|
||
// 新增方法:检查地形是否可通行
|
||
private bool IsTerrainPassable(GridData gridData, PlayerData playerData)
|
||
{
|
||
// 检查地形是否可通行
|
||
if (gridData.Terrain == TerrainType.DeepSea && !playerData.TechTree.CheckIfHasTechAtom(TechAtom.UnitSkillOCEANMOVE))
|
||
return false;
|
||
if (gridData.Terrain == TerrainType.ShallowSea && !playerData.TechTree.CheckIfHasTechAtom(TechAtom.UnitSkillWATERMOVE))
|
||
return false;
|
||
if (gridData.Feature == TerrainFeature.Mountain && !playerData.TechTree.CheckIfHasTechAtom(TechAtom.UnitSkillMOUNTAINMOVE))
|
||
return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
// 新增方法:使用BFS检查格子是否可达
|
||
private bool IsGridReachable(GridData startGrid, GridData targetGrid, PlayerData playerData, int maxSteps)
|
||
{
|
||
// 如果起点和终点相同,直接返回true
|
||
if (startGrid.Id == targetGrid.Id)
|
||
return true;
|
||
|
||
// 如果目标格子地形不可通行,直接返回false
|
||
if (!IsTerrainPassable(targetGrid, playerData))
|
||
return false;
|
||
|
||
// 使用BFS搜索
|
||
Queue<GridData> queue = new Queue<GridData>();
|
||
HashSet<uint> visited = new HashSet<uint>();
|
||
Dictionary<uint, int> steps = new Dictionary<uint, int>();
|
||
|
||
queue.Enqueue(startGrid);
|
||
visited.Add(startGrid.Id);
|
||
steps[startGrid.Id] = 0;
|
||
|
||
while (queue.Count > 0)
|
||
{
|
||
GridData current = queue.Dequeue();
|
||
int currentSteps = steps[current.Id];
|
||
|
||
// 如果已经超过最大步数,跳过
|
||
if (currentSteps >= maxSteps)
|
||
continue;
|
||
|
||
// 获取当前格子的八个方向的相邻格子
|
||
Vector2Int currentPos = new Vector2Int(current.Pos.X, current.Pos.Y);
|
||
|
||
for (int i = 0; i < 8; i++)
|
||
{
|
||
Vector2Int dirVector = GetMoveDirectionFromIndex(i);
|
||
Vector2Int nextPos = currentPos + dirVector;
|
||
|
||
// 检查位置是否在地图范围内
|
||
if (nextPos.x < 0 || nextPos.x >= Main.MapData.MapConfig.Width ||
|
||
nextPos.y < 0 || nextPos.y >= Main.MapData.MapConfig.Height)
|
||
continue;
|
||
|
||
// 获取相邻格子数据
|
||
if (!Main.MapData.GridMap.GetGridDataByPos(nextPos.x, nextPos.y, out var nextGrid))
|
||
continue;
|
||
|
||
// 如果已经访问过,跳过
|
||
if (visited.Contains(nextGrid.Id))
|
||
continue;
|
||
|
||
// 检查地形是否可通行
|
||
if (!IsTerrainPassable(nextGrid, playerData))
|
||
continue;
|
||
|
||
// 如果找到目标格子,返回true
|
||
if (nextGrid.Id == targetGrid.Id)
|
||
return true;
|
||
|
||
// 将相邻格子加入队列
|
||
queue.Enqueue(nextGrid);
|
||
visited.Add(nextGrid.Id);
|
||
steps[nextGrid.Id] = currentSteps + 1;
|
||
}
|
||
}
|
||
|
||
// 搜索完毕仍未找到目标格子,返回false
|
||
return false;
|
||
}
|
||
|
||
public bool UpdateAroundHighlight(MapData mapData, GridData grid)
|
||
{
|
||
//如果不是真map,不操作
|
||
if (mapData != Main.MapData) return false;
|
||
// 使用局部List,避免与CheckUnitHasMoveAttackTarget等方法共享static _aroundBuf导致foreach中集合被修改
|
||
var localBuf = new List<GridData>();
|
||
mapData.GridMap.GetAroundGridDataSet_NOCENTER(3, 3, grid, localBuf);
|
||
foreach (var aroundGrid in localBuf)
|
||
{
|
||
//如果是玩家的unit 那么更新高亮
|
||
if (aroundGrid.MainSelfPlayerVisibleUnit( out var unit)
|
||
&& mapData.GetPlayerIdByUnitId(unit.Id,out var pid)
|
||
&& pid == mapData.PlayerMap.SelfPlayerId)
|
||
unit.Renderer(mapData)?.InstantUpdateUnit(true);
|
||
}
|
||
return true;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
} |