mirror of
https://github.com/spiffcode/hostile-takeover.git
synced 2025-12-16 12:08:36 +00:00
3049 lines
82 KiB
C++
3049 lines
82 KiB
C++
#include "ht.h"
|
|
|
|
namespace wi {
|
|
|
|
// Each element of this table was calculated as:
|
|
// gawcDiagonalDist[i] = floor((cos(PI / 4) * i) + 0.5)
|
|
|
|
/* jscript to generate the table:
|
|
for (j = 0; j < 4; j++) {
|
|
var str = "";
|
|
for (i = 0; i < 16; i++)
|
|
str += Math.floor((Math.cos(Math.PI / 4) * ((j * 16) + i) + .5)) + ", ";
|
|
WScript.Echo(str);
|
|
}
|
|
*/
|
|
static byte gawcDiagonalDist[64] = {
|
|
0, 1, 1, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9, 10, 11,
|
|
11, 12, 13, 13, 14, 15, 16, 16, 17, 18, 18, 19, 20, 21, 21, 22,
|
|
23, 23, 24, 25, 25, 26, 27, 28, 28, 29, 30, 30, 31, 32, 33, 33,
|
|
34, 35, 35, 36, 37, 37, 38, 39, 40, 40, 41, 42, 42, 43, 44, 45,
|
|
};
|
|
|
|
// Each element of this table was calculated as:
|
|
// gaDiv256byNWithRounding[i] = (256 + (i / 2)) / i
|
|
|
|
// The "+ (i / 2)" part of this rounds the # of steps rather than
|
|
// truncating it which gives gives better results when there is a partial
|
|
// step remainder.
|
|
|
|
/* javascript to generate the table:
|
|
for (j = 0; j < 4; j++) {
|
|
var str = "";
|
|
for (i = 0; i < 16; i++) {
|
|
var wcMoveDist = ((j * 16) + i);
|
|
str += Math.floor((256 + (wcMoveDist / 2)) / wcMoveDist) + ", ";
|
|
}
|
|
WScript.Echo(str);
|
|
}
|
|
*/
|
|
static byte gaDiv256byNWithRounding[64] = {
|
|
0, 255, // Fudge to keep within 8-bit range
|
|
128, 85, 64, 51, 43, 37, 32, 28, 26, 23, 21, 20, 18, 17,
|
|
16, 15, 14, 13, 13, 12, 12, 11, 11, 10, 10, 9, 9, 9, 9, 8,
|
|
8, 8, 8, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 5,
|
|
5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4,
|
|
};
|
|
|
|
int g_mpDirToDx[8] = { 0, 1, 1, 1, 0, -1, -1, -1 };
|
|
int g_mpDirToDy[8] = { -1, -1, 0, 1, 1, 1, 0, -1 };
|
|
AnimationData *g_panidMoveTarget;
|
|
static AnimationData *s_panidVehicleExplosion;
|
|
|
|
#define GetIdleCountdown() ((GetRandom() % 50) + 50) // somewhere between 4 & 8 seconds
|
|
|
|
const int kcFireCountdown = 6; // 6 updates (~.5 secs)
|
|
|
|
Path *MobileUnitGob::s_apathCached[kcPathsCache];
|
|
int MobileUnitGob::s_cpathCached;
|
|
|
|
//===========================================================================
|
|
// MobileUnitGob implementation
|
|
|
|
bool MobileUnitGob::InitClass(MobileUnitConsts *pmuntc, IniReader *pini)
|
|
{
|
|
if (!UnitGob::InitClass(pmuntc, pini))
|
|
return false;
|
|
|
|
char szTemplate[10];
|
|
itoa(pmuntc->gt, szTemplate, 10);
|
|
|
|
// Required properties
|
|
|
|
int nT;
|
|
if (pini->GetPropertyValue(szTemplate, "MoveRate", "%d", &nT) != 1)
|
|
return false;
|
|
pmuntc->wcMoveDistPerUpdate = (WCoord)nT;
|
|
Assert(pmuntc->wcMoveDistPerUpdate < 64); // gawcDiagonalDist only has 64 entries
|
|
gwcMoveDistPerUpdateMin = _min(gwcMoveDistPerUpdateMin, pmuntc->wcMoveDistPerUpdate);
|
|
gwcMoveDistPerUpdateMax = _max(gwcMoveDistPerUpdateMax, pmuntc->wcMoveDistPerUpdate);
|
|
|
|
int nArmorStrength;
|
|
if (pini->GetPropertyValue(szTemplate, "ArmorStrength", "%d", &nArmorStrength) != 1)
|
|
return false;
|
|
Assert(nArmorStrength < 1 << 10);
|
|
pmuntc->fxArmorStrength = itofx(nArmorStrength);
|
|
gfxMobileUnitArmorStrengthMin = _min(gfxMobileUnitArmorStrengthMin, pmuntc->fxArmorStrength);
|
|
gfxMobileUnitArmorStrengthMax = _max(gfxMobileUnitArmorStrengthMax, pmuntc->fxArmorStrength);
|
|
|
|
// Optional properties
|
|
|
|
if (pini->GetPropertyValue(szTemplate, "ArmorStrengthMP", "%d", &nArmorStrength) != 1) {
|
|
pmuntc->fxArmorStrengthMP = pmuntc->fxArmorStrength;
|
|
} else {
|
|
Assert(nArmorStrength < 1 << 10);
|
|
pmuntc->fxArmorStrengthMP = itofx(nArmorStrength);
|
|
}
|
|
|
|
if (pini->GetPropertyValue(szTemplate, "MoveRateMP", "%d", &nT) != 1)
|
|
pmuntc->wcMoveDistPerUpdateMP = pmuntc->wcMoveDistPerUpdate;
|
|
else
|
|
pmuntc->wcMoveDistPerUpdateMP = (WCoord)nT;
|
|
Assert(pmuntc->wcMoveDistPerUpdateMP < 64); // gawcDiagonalDist only has 64 entries
|
|
|
|
// Preload the unit's menu form
|
|
|
|
if (!LoadMenu(pmuntc, pini, szTemplate, kidfUnitMenu))
|
|
return false;
|
|
|
|
// MobileUnitGob owns the 'move target' and 'vehicle explosion' AnimationData
|
|
|
|
if (g_panidMoveTarget == NULL) {
|
|
g_panidMoveTarget = LoadAnimationData("movetarget.anir");
|
|
if (g_panidMoveTarget == NULL)
|
|
return false;
|
|
}
|
|
|
|
if (s_panidVehicleExplosion == NULL) {
|
|
s_panidVehicleExplosion = LoadAnimationData("vexplosion.anir");
|
|
if (s_panidVehicleExplosion == NULL)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void MobileUnitGob::ExitClass(MobileUnitConsts *pmuntc)
|
|
{
|
|
// Clear this out so MobileUnitGob derivatives can safely call MobileUnitGob::ExitClass
|
|
|
|
delete s_panidVehicleExplosion;
|
|
s_panidVehicleExplosion = NULL;
|
|
|
|
// Clear this out so MobileUnitGob derivatives can safely call MobileUnitGob::ExitClass
|
|
|
|
delete g_panidMoveTarget;
|
|
g_panidMoveTarget = NULL;
|
|
|
|
UnitGob::ExitClass(pmuntc);
|
|
}
|
|
|
|
MobileUnitGob::MobileUnitGob(MobileUnitConsts *pmuntc) : UnitGob(pmuntc)
|
|
{
|
|
m_ff |= kfGobMobileUnit;
|
|
m_tLastFire = 0;
|
|
m_gidTarget = kgidNull;
|
|
m_wptTarget.wx = kwxInvalid; // kxInvalid = no target location
|
|
m_cMoveStepsRemaining = 0;
|
|
m_txDst = 0;
|
|
m_tyDst = 0;
|
|
m_wcMoveDistPerUpdate = 0;
|
|
m_mua = kmuaNone;
|
|
m_muaPending = kmuaNone;
|
|
m_wfMunt = kfMuntReturnFire | kfMuntAttackEnemiesWhenGuarding | kfMuntAttackEnemiesWhenMoving;
|
|
m_stPending = kstReservedNull;
|
|
m_itptPath = 0;
|
|
m_ppathUnit = NULL;
|
|
m_ppathAvoid = NULL;
|
|
m_nSeqMoveAside = 0;
|
|
|
|
// Just to be clear this Unit or its nearby allies hasn't been hit for awhile
|
|
|
|
m_cupdLastHitOrNearbyAllyHit = -1000000;
|
|
}
|
|
|
|
MobileUnitGob::~MobileUnitGob()
|
|
{
|
|
delete m_ppathUnit;
|
|
}
|
|
|
|
void MobileUnitGob::Activate()
|
|
{
|
|
TCoord tx = TcFromWc(m_wx);
|
|
TCoord ty = TcFromWc(m_wy);
|
|
Assert(!IsTileReserved(tx, ty));
|
|
ReserveTile(tx, ty, true);
|
|
UnitGob::Activate();
|
|
ggobm.MoveGobBetweenAreas(m_gid, 0, ggobm.CalcAreaMask(tx, ty));
|
|
}
|
|
|
|
void MobileUnitGob::Deactivate()
|
|
{
|
|
if (m_wfMunt & kfMuntDestinationReserved) {
|
|
Assert(IsTileReserved(m_txDst, m_tyDst));
|
|
ReserveTile(m_txDst, m_tyDst, false);
|
|
} else {
|
|
#ifndef MP_DEBUG_SHAREDMEM
|
|
// DWM: We always hit it when a Processor containing a Bullpup is destroyed.
|
|
// We should fix that but in the meantime I need to release a DEBUG build
|
|
// that won't trip up on this case so I'm commenting it out for now.
|
|
// Assert(IsTileReserved(TcFromWc(m_wx), TcFromWc(m_wy)));
|
|
#endif
|
|
ReserveTile(TcFromWc(m_wx), TcFromWc(m_wy), false);
|
|
}
|
|
UnitGob::Deactivate();
|
|
ggobm.MoveGobBetweenAreas(m_gid, ggobm.CalcAreaMask(TcFromWc(m_wx), TcFromWc(m_wy)), 0);
|
|
}
|
|
|
|
word AggBitsFromAgg(int nAggressiveness)
|
|
{
|
|
switch (nAggressiveness) {
|
|
case knAggressivenessCoward:
|
|
return kfMuntRunAwayWhenHit;
|
|
|
|
case knAggressivenessSelfDefense:
|
|
return kfMuntReturnFire | kfMuntStayPut;
|
|
|
|
case knAggressivenessDefender:
|
|
return kfMuntReturnFire | kfMuntAttackEnemiesWhenGuarding | kfMuntAttackEnemiesWhenMoving;
|
|
|
|
case knAggressivenessPitbull:
|
|
return kfMuntReturnFire | kfMuntAttackEnemiesWhenGuarding | kfMuntAttackEnemiesWhenMoving | kfMuntChaseEnemies;
|
|
|
|
// Implicit
|
|
// case knAggressivenessPacifist:
|
|
// break; // non-aggressive
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool MobileUnitGob::Init(IniReader *pini, FindProp *pfind, const char *pszName)
|
|
{
|
|
// UnitGob::Init(pini, ...) calls the overridden UnitGob::Init(wx, ...) below
|
|
|
|
if (!UnitGob::Init(pini, pfind, pszName))
|
|
return false;
|
|
|
|
// Note that this MobileUnitGob has already been Activated by this point
|
|
|
|
// Translate an aggressiveness type into the MobileUnit flags that actually
|
|
// determine its behaviour.
|
|
|
|
int nAggressiveness;
|
|
char szAction[100];
|
|
int cArgs = pini->GetPropertyValue(pfind, "%*d ,%*d ,%*d ,%*d ,%*d ,%*d, %d ,%a", &nAggressiveness, szAction);
|
|
if (cArgs > 0)
|
|
m_wfMunt = (m_wfMunt & ~kfMuntAggressivenessBits) | AggBitsFromAgg(nAggressiveness);
|
|
|
|
// Only computer-controlled units respond to their initial Action
|
|
|
|
if (m_pplr->GetFlags() & kfPlrComputer) {
|
|
if (cArgs > 1)
|
|
PerformAction(szAction);
|
|
|
|
// Human-controlled units don't attack while executing a move command
|
|
|
|
} else {
|
|
m_wfMunt &= ~kfMuntAttackEnemiesWhenMoving;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool MobileUnitGob::Init(WCoord wx, WCoord wy, Player *pplr, fix fxHealth, dword ff, const char *pszName)
|
|
{
|
|
if (!UnitGob::Init(wx, wy, pplr, fxHealth, ff, pszName))
|
|
return false;
|
|
|
|
// Center Unit within tile
|
|
|
|
m_wx += kwcTileHalf;
|
|
m_wy += kwcTileHalf;
|
|
|
|
Activate();
|
|
|
|
// Human-controlled units don't attack while executing a move command
|
|
|
|
if (IsHumanOrGodControlled())
|
|
m_wfMunt &= ~kfMuntAttackEnemiesWhenMoving;
|
|
|
|
// Notify nearby enemy gobs that they might want to attack this gob
|
|
|
|
NotifyEnemyNearby();
|
|
|
|
return true;
|
|
}
|
|
|
|
#define knVerMobileUnitGobState 10
|
|
bool MobileUnitGob::LoadState(Stream *pstm)
|
|
{
|
|
byte nVer = pstm->ReadByte();
|
|
if (nVer != knVerMobileUnitGobState)
|
|
return false;
|
|
m_dir = (Direction)pstm->ReadByte();
|
|
m_dirNext = (Direction)pstm->ReadByte();
|
|
m_gidTarget = pstm->ReadWord();
|
|
m_tLastFire = gsim.GetTickCount() - pstm->ReadDword();
|
|
pstm->Read(&m_msgPending, sizeof(m_msgPending));
|
|
pstm->Read(&m_msgAction, sizeof(m_msgAction));
|
|
m_cCountdown = pstm->ReadWord();
|
|
m_cMoveStepsRemaining = pstm->ReadWord();
|
|
m_tptChaseInitial.tx = pstm->ReadWord();
|
|
m_tptChaseInitial.ty = pstm->ReadWord();
|
|
#ifdef DRAW_PATHS
|
|
m_wxDst = pstm->ReadWord();
|
|
m_wyDst = pstm->ReadWord();
|
|
#endif
|
|
m_txDst = pstm->ReadWord();
|
|
m_tyDst = pstm->ReadWord();
|
|
m_wptTarget.wx = pstm->ReadWord();
|
|
m_wptTarget.wy = pstm->ReadWord();
|
|
if (pstm->ReadByte() != 0) {
|
|
m_ppathUnit = new Path;
|
|
if (m_ppathUnit == NULL)
|
|
return false;
|
|
if (!m_ppathUnit->LoadState(gsim.GetLevel()->GetTerrainMap(), pstm))
|
|
return false;
|
|
m_itptPath = pstm->ReadWord();
|
|
}
|
|
if (pstm->ReadByte() != 0) {
|
|
m_ppathAvoid = new Path;
|
|
if (m_ppathAvoid == NULL)
|
|
return false;
|
|
if (!m_ppathAvoid->LoadState(gsim.GetLevel()->GetTerrainMap(), pstm))
|
|
return false;
|
|
}
|
|
m_mua = (MobileUnitAction)pstm->ReadByte();
|
|
m_muaPending = (MobileUnitAction)pstm->ReadByte();
|
|
m_wfMunt = pstm->ReadWord();
|
|
m_stPending = (State)pstm->ReadByte();
|
|
m_wptTargetCenter.wx = pstm->ReadWord();
|
|
m_wptTargetCenter.wy = pstm->ReadWord();
|
|
m_tcTargetRadius = pstm->ReadWord();
|
|
m_wcMoveDistPerUpdate = pstm->ReadWord();
|
|
m_cupdLastHitOrNearbyAllyHit = pstm->ReadDword();
|
|
return UnitGob::LoadState(pstm);
|
|
}
|
|
|
|
bool MobileUnitGob::SaveState(Stream *pstm)
|
|
{
|
|
pstm->WriteByte(knVerMobileUnitGobState);
|
|
pstm->WriteByte(m_dir);
|
|
pstm->WriteByte(m_dirNext);
|
|
pstm->WriteWord(m_gidTarget);
|
|
pstm->WriteDword((dword)(gsim.GetTickCount() - m_tLastFire));
|
|
pstm->Write(&m_msgPending, sizeof(m_msgPending));
|
|
pstm->Write(&m_msgAction, sizeof(m_msgAction));
|
|
pstm->WriteWord(m_cCountdown);
|
|
pstm->WriteWord(m_cMoveStepsRemaining);
|
|
pstm->WriteWord(m_tptChaseInitial.tx);
|
|
pstm->WriteWord(m_tptChaseInitial.ty);
|
|
#ifdef DRAW_PATHS
|
|
pstm->WriteWord(m_wxDst);
|
|
pstm->WriteWord(m_wyDst);
|
|
#endif
|
|
pstm->WriteWord(m_txDst);
|
|
pstm->WriteWord(m_tyDst);
|
|
pstm->WriteWord(m_wptTarget.wx);
|
|
pstm->WriteWord(m_wptTarget.wy);
|
|
if (m_ppathUnit == NULL) {
|
|
pstm->WriteByte(0);
|
|
} else {
|
|
pstm->WriteByte(1);
|
|
m_ppathUnit->SaveState(pstm);
|
|
pstm->WriteWord(m_itptPath);
|
|
}
|
|
if (m_ppathAvoid == NULL) {
|
|
pstm->WriteByte(0);
|
|
} else {
|
|
pstm->WriteByte(1);
|
|
m_ppathAvoid->SaveState(pstm);
|
|
}
|
|
pstm->WriteByte(m_mua);
|
|
pstm->WriteByte(m_muaPending);
|
|
pstm->WriteWord(m_wfMunt);
|
|
pstm->WriteByte(m_stPending);
|
|
pstm->WriteWord(m_wptTargetCenter.wx);
|
|
pstm->WriteWord(m_wptTargetCenter.wy);
|
|
pstm->WriteWord(m_tcTargetRadius);
|
|
pstm->WriteWord(m_wcMoveDistPerUpdate);
|
|
pstm->WriteDword(m_cupdLastHitOrNearbyAllyHit);
|
|
return UnitGob::SaveState(pstm);
|
|
}
|
|
|
|
void MobileUnitGob::PerformAction(char *szAction)
|
|
{
|
|
int nUnitAction;
|
|
if (IniScanf(szAction, "%d", &nUnitAction) == 0) {
|
|
Assert(false);
|
|
return;
|
|
}
|
|
|
|
switch (nUnitAction) {
|
|
case knMoveUnitAction:
|
|
{
|
|
int nArea;
|
|
if (IniScanf(szAction, "%*d ,%d", &nArea) == 0) {
|
|
Assert(false);
|
|
return;
|
|
}
|
|
TRect trc;
|
|
ggobm.GetAreaRect(nArea, &trc);
|
|
Point ptCenter;
|
|
trc.GetCenter(&ptCenter);
|
|
SendMoveAction(m_gid, WcFromTc(ptCenter.x), WcFromTc(ptCenter.y), 1, m_pmuntc->GetMoveDistPerUpdate());
|
|
}
|
|
break;
|
|
|
|
case knGuardUnitAction:
|
|
break;
|
|
|
|
case knGuardVicinityUnitAction:
|
|
SendGuardVicinityAction(m_gid);
|
|
break;
|
|
|
|
case knGuardAreaUnitAction:
|
|
{
|
|
int nArea;
|
|
if (IniScanf(szAction, "%*d ,%d", &nArea) == 0) {
|
|
Assert(false);
|
|
return;
|
|
}
|
|
SendGuardAreaAction(m_gid, nArea);
|
|
}
|
|
break;
|
|
|
|
case knHuntEnemiesUnitAction:
|
|
{
|
|
UnitMask um;
|
|
if (IniScanf(szAction, "%*d ,%d", &um) == 0) {
|
|
Assert(false);
|
|
return;
|
|
}
|
|
SendHuntEnemiesAction(m_gid, um);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
#ifdef DRAW_PATHS
|
|
void MobileUnitGob::DrawPath(DibBitmap *pbm, WCoord wxViewOrigin, WCoord wyViewOrigin)
|
|
{
|
|
if (m_ppathUnit != NULL)
|
|
m_ppathUnit->Draw(pbm, PcFromWc(wxViewOrigin), PcFromWc(wyViewOrigin), GetSide());
|
|
}
|
|
#endif
|
|
|
|
#ifdef DRAW_LINES
|
|
void MobileUnitGob::DrawTargetLine(DibBitmap *pbm, int xViewOrigin, int yViewOrigin)
|
|
{
|
|
if (m_wptTarget.wx == kwxInvalid)
|
|
return;
|
|
|
|
pbm->DrawLine(PcFromUwc(m_wx) - xViewOrigin, PcFromUwc(m_wy) - yViewOrigin,
|
|
PcFromUwc(m_wptTarget.wx) - xViewOrigin, PcFromUwc(m_wptTarget.wy) - yViewOrigin,
|
|
GetSideColor(m_pplr->GetSide()));
|
|
}
|
|
#endif
|
|
|
|
bool MobileUnitGob::IsIdle()
|
|
{
|
|
// UNDONE: introduce kstIdle?
|
|
return (m_wfMunt & kfMuntCommandPending) == 0 && m_st == kstGuard;
|
|
}
|
|
|
|
// Handle rotating toward the target and limiting the firing rate
|
|
|
|
bool MobileUnitGob::Fire(UnitGob *puntTarget, WCoord wx, WCoord wy, WCoord wdx, WCoord wdy)
|
|
{
|
|
// Make sure we're facing the way we want to fire before we try to fire
|
|
|
|
Direction dirFire = CalcDir(wdx, wdy);
|
|
if (m_dir != dirFire) {
|
|
m_dir = TurnToward(dirFire, m_dir);
|
|
SetAnimationStrip(&m_ani, m_pmuntc->anMovingStripIndices[m_dir]);
|
|
m_unvl.MinSkip();
|
|
return false;
|
|
}
|
|
|
|
// Firing rate is limited by ctFiringRate
|
|
|
|
long t = gsim.GetTickCount();
|
|
|
|
int ctWait = m_pmuntc->ctFiringRate;
|
|
int ctRemaining = ctWait - (int)(t - m_tLastFire);
|
|
if (ctRemaining > 0) {
|
|
m_unvl.MinSkip((ctRemaining + (kctUpdate / 2)) / kctUpdate - 1);
|
|
return false;
|
|
}
|
|
|
|
m_tLastFire = t;
|
|
|
|
// Play firing animation (start on frame 1 where the action is)
|
|
|
|
StartAnimation(&m_ani, m_pmuntc->anFiringStripIndices[m_dir], 1, kfAniIgnoreFirstAdvance | kfAniResetWhenDone);
|
|
m_wfMunt |= kfMuntFiring;
|
|
gsmm.SendDelayedMsg(kmidFireComplete, m_ani.GetRemainingStripTime(), m_gid, m_gid);
|
|
|
|
return true;
|
|
}
|
|
|
|
void MobileUnitGob::Idle()
|
|
{
|
|
}
|
|
|
|
void MobileUnitGob::SetTarget(Gid gid, WCoord wx, WCoord wy, WCoord wxCenter, WCoord wyCenter, TCoord tcRadius, WCoord wcMoveDistPerUpdate)
|
|
{
|
|
if (gid == kgidNull) {
|
|
// Send formed command
|
|
|
|
Message msgT;
|
|
msgT.mid = kmidMoveCommand;
|
|
msgT.smidSender = kgidNull;
|
|
msgT.smidReceiver = m_gid;
|
|
msgT.MoveCommand.wptTarget.wx = wx;
|
|
msgT.MoveCommand.wptTarget.wy = wy;
|
|
msgT.MoveCommand.gidTarget = kgidNull;
|
|
msgT.MoveCommand.wptTargetCenter.wx = wxCenter;
|
|
msgT.MoveCommand.wptTargetCenter.wy = wyCenter;
|
|
msgT.MoveCommand.tcTargetRadius = tcRadius;
|
|
msgT.MoveCommand.wcMoveDistPerUpdate = wcMoveDistPerUpdate;
|
|
gcmdq.Enqueue(&msgT);
|
|
return;
|
|
}
|
|
|
|
// If the target no longer exists, discard the command
|
|
|
|
Gob *pgobTarget = ggobm.GetGob(gid);
|
|
if (pgobTarget == NULL)
|
|
return;
|
|
|
|
// If the target is a Replicator move to its input
|
|
|
|
if (pgobTarget->GetType() == kgtReplicator) {
|
|
pgobTarget->SetFlags(pgobTarget->GetFlags() | kfGobFlashing);
|
|
|
|
// Send formed command
|
|
|
|
ReplicatorGob *prep = (ReplicatorGob *)pgobTarget;
|
|
TPoint tpt;
|
|
prep->GetInputTilePosition(&tpt);
|
|
Message msgT;
|
|
msgT.mid = kmidMoveCommand;
|
|
msgT.smidSender = kgidNull;
|
|
msgT.smidReceiver = m_gid;
|
|
msgT.MoveCommand.wptTarget.wx = WcFromTc(tpt.tx) + kwcTileHalf;
|
|
msgT.MoveCommand.wptTarget.wy = WcFromTc(tpt.ty) + kwcTileHalf;
|
|
msgT.MoveCommand.gidTarget = kgidNull;
|
|
msgT.MoveCommand.wptTargetCenter.wx = msgT.MoveCommand.wptTarget.wx;
|
|
msgT.MoveCommand.wptTargetCenter.wy = msgT.MoveCommand.wptTarget.wy;
|
|
msgT.MoveCommand.tcTargetRadius = 0;
|
|
msgT.MoveCommand.wcMoveDistPerUpdate = wcMoveDistPerUpdate;
|
|
gcmdq.Enqueue(&msgT);
|
|
return;
|
|
}
|
|
|
|
// Get target coords if none passed in
|
|
|
|
if (wx == 0 && wy == 0) {
|
|
Assert(pgobTarget->GetFlags() & kfGobUnit);
|
|
UnitGob *puntTarget = (UnitGob *)pgobTarget;
|
|
WPoint wpt;
|
|
puntTarget->GetAttackPoint(&wpt);
|
|
wx = wpt.wx;
|
|
wy = wpt.wy;
|
|
}
|
|
|
|
// Friend or Foe? No special action for friends
|
|
|
|
if (IsAlly(pgobTarget->GetSide()))
|
|
return;
|
|
|
|
// Foe -- attack!
|
|
// Flash the target Gob
|
|
|
|
if (m_pplr == gpplrLocal)
|
|
pgobTarget->Flash();
|
|
|
|
// Queue attack command
|
|
|
|
Message msgT;
|
|
msgT.mid = kmidAttackCommand;
|
|
msgT.smidSender = m_gid;
|
|
msgT.smidReceiver = m_gid;
|
|
msgT.AttackCommand.wptTarget.wx = wx;
|
|
msgT.AttackCommand.wptTarget.wy = wy;
|
|
msgT.AttackCommand.gidTarget = gid;
|
|
msgT.AttackCommand.wptTargetCenter.wx = wxCenter;
|
|
msgT.AttackCommand.wptTargetCenter.wy = wyCenter;
|
|
msgT.AttackCommand.tcTargetRadius = tcRadius;
|
|
msgT.AttackCommand.wcMoveDistPerUpdate = wcMoveDistPerUpdate;
|
|
gcmdq.Enqueue(&msgT);
|
|
}
|
|
|
|
void SendAttackCommand(Gid gidReceiver, Gid gidTarget)
|
|
{
|
|
// Bail if target is already gone
|
|
|
|
Gob *pgobTarget = ggobm.GetGob(gidTarget);
|
|
if (pgobTarget == NULL)
|
|
return;
|
|
Assert(pgobTarget->GetFlags() & kfGobUnit);
|
|
UnitGob *puntTarget = (UnitGob *)pgobTarget;
|
|
|
|
Message msgT;
|
|
msgT.mid = kmidAttackCommand;
|
|
msgT.smidSender = gidReceiver;
|
|
msgT.smidReceiver = gidReceiver;
|
|
puntTarget->GetAttackPoint(&msgT.AttackCommand.wptTarget);
|
|
msgT.AttackCommand.gidTarget = gidTarget;
|
|
msgT.AttackCommand.tcTargetRadius = 0;
|
|
msgT.AttackCommand.wptTargetCenter = msgT.AttackCommand.wptTarget;
|
|
msgT.AttackCommand.wcMoveDistPerUpdate = 0;
|
|
gsmm.SendMsg(&msgT);
|
|
}
|
|
|
|
void MobileUnitGob::GetAttackPoint(WPoint *pwpt)
|
|
{
|
|
// The attack point must be terrain accessible.
|
|
|
|
if (m_wfMunt & kfMuntDestinationReserved) {
|
|
pwpt->wx = WcFromTc(m_txDst);
|
|
pwpt->wy = WcFromTc(m_tyDst);
|
|
} else {
|
|
pwpt->wx = m_wx;
|
|
pwpt->wy = m_wy;
|
|
}
|
|
}
|
|
|
|
// UNDONE: so far behavior can be easily parameterized
|
|
// Parameters:
|
|
// target hit animation (piff) (optional)
|
|
// move animation (w/ table for 8 directions)
|
|
// fire animation (w/ table for 8 directions)
|
|
// idle animation (w/ table for 8 directions)
|
|
// death animation
|
|
// ShotGob (optional) -- NOTE: may handle target hit animation
|
|
// other stuff already in MobileUnitConsts
|
|
|
|
// If a path is being followed we have to wait until a tile center is
|
|
// reached before acting on a new move command.
|
|
// Returns true if command is processed (new state is set)
|
|
|
|
bool MobileUnitGob::PendOrProcessCommand(Message *pmsg, State stNew)
|
|
{
|
|
if (!IsReadyForCommand()) {
|
|
m_msgPending = *pmsg;
|
|
m_wfMunt |= kfMuntCommandPending;
|
|
return false;
|
|
} else {
|
|
m_gidTarget = kgidNull;
|
|
SetState(stNew);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool MobileUnitGob::PendOrProcessAction(Message *pmsg, State stNew, MobileUnitAction mua)
|
|
{
|
|
if (!IsReadyForCommand()) {
|
|
m_muaPending = mua;
|
|
m_msgPending = *pmsg;
|
|
m_wfMunt |= kfMuntCommandPending;
|
|
return false;
|
|
} else {
|
|
m_mua = mua;
|
|
m_gidTarget = kgidNull;
|
|
m_msgAction = *pmsg;
|
|
SetState(stNew);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool MobileUnitGob::IsTargetInRange()
|
|
{
|
|
Gob *pgobTarget = ggobm.GetGob(m_gidTarget);
|
|
if (pgobTarget == NULL)
|
|
return false;
|
|
return IsGobWithinRange(pgobTarget, m_pmuntc->tcFiringRange);
|
|
}
|
|
|
|
bool MobileUnitGob::IsStandingOnActivator()
|
|
{
|
|
for (Gid gid = ggobm.GetFirstGid(TcFromWc(m_wx), TcFromWc(m_wy)); gid != kgidNull; gid = ggobm.GetNextGid(gid)) {
|
|
Gob *pgob = ggobm.GetGob(gid, false);
|
|
if (pgob == NULL)
|
|
continue;
|
|
if (pgob->GetType() == kgtActivator)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void MobileUnitGob::SetStatePendingFireComplete(State st)
|
|
{
|
|
if (m_wfMunt & kfMuntFiring) {
|
|
m_stPending = st;
|
|
SetState(kstChangeStatePendingFireComplete);
|
|
} else {
|
|
SetState(st);
|
|
}
|
|
}
|
|
|
|
void MobileUnitGob::ContinueActionPendingFireComplete()
|
|
{
|
|
if (m_wfMunt & kfMuntFiring) {
|
|
SetState(kstContinueActionPendingFireComplete);
|
|
} else {
|
|
gsmm.SendMsg(&m_msgAction);
|
|
}
|
|
}
|
|
|
|
bool MobileUnitGob::IsAttackPointWithinFiringRangeOfTarget(UnitGob *puntTarget)
|
|
{
|
|
WPoint wptAttack;
|
|
puntTarget->GetAttackPoint(&wptAttack);
|
|
return IsTargetWithinRange(&wptAttack, puntTarget, m_pmuntc->tcFiringRange);
|
|
}
|
|
|
|
// TUNE:
|
|
|
|
const TCoord ktcVicinity = 5;
|
|
const long kcupdAggressivenessBoost = 63; // 5 seconds
|
|
|
|
int MobileUnitGob::ProcessStateMachineMessage(State st, Message *pmsg)
|
|
{
|
|
BeginStateMachine
|
|
OnEnter
|
|
m_dir = kdirS;
|
|
SetState(kstGuard);
|
|
|
|
// Actions are sent by Trigger/UnitActions or the Overmind
|
|
|
|
OnMsg(kmidMoveAction)
|
|
if (PendOrProcessAction(pmsg, kstMove, kmuaMove))
|
|
goto lbMoveCommand;
|
|
|
|
OnMsg(kmidAttackAction)
|
|
if (PendOrProcessAction(pmsg, kstAttack, kmuaAttack))
|
|
goto lbAttackCommand;
|
|
|
|
OnMsg(kmidGuardAction)
|
|
PendOrProcessAction(pmsg, kstGuard, kmuaGuard);
|
|
|
|
OnMsg(kmidGuardVicinityAction)
|
|
PendOrProcessAction(pmsg, kstGuard, kmuaGuardVicinity);
|
|
|
|
OnMsg(kmidGuardAreaAction)
|
|
PendOrProcessAction(pmsg, kstGuard, kmuaGuardArea);
|
|
|
|
OnMsg(kmidHuntEnemiesAction)
|
|
PendOrProcessAction(pmsg, kstHuntEnemies, kmuaHuntEnemies);
|
|
|
|
// Commands are the result of player interactions with the SimUI
|
|
|
|
OnMsg(kmidMoveCommand)
|
|
if (PendOrProcessCommand(pmsg, kstMove)) {
|
|
lbMoveCommand:
|
|
// Stash move parameters to be picked up by the move state
|
|
// Adjust target x/y to point to the closest tile center. We like tile centers.
|
|
|
|
m_wptTarget.wx = WcTrunc(pmsg->MoveCommand.wptTarget.wx) + kwcTileHalf;
|
|
m_wptTarget.wy = WcTrunc(pmsg->MoveCommand.wptTarget.wy) + kwcTileHalf;
|
|
m_wptTargetCenter.wx = WcTrunc(pmsg->MoveCommand.wptTargetCenter.wx) + kwcTileHalf;
|
|
m_wptTargetCenter.wy = WcTrunc(pmsg->MoveCommand.wptTargetCenter.wy) + kwcTileHalf;
|
|
m_tcTargetRadius = pmsg->MoveCommand.tcTargetRadius;
|
|
m_wcMoveDistPerUpdate = pmsg->MoveCommand.wcMoveDistPerUpdate;
|
|
}
|
|
|
|
OnMsg(kmidAttackCommand)
|
|
if (PendOrProcessCommand(pmsg, kstAttack)) {
|
|
lbAttackCommand:
|
|
// stash attack parameters to be picked up by the attack state
|
|
|
|
m_gidTarget = pmsg->AttackCommand.gidTarget;
|
|
m_wptTarget.wx = WcTrunc(pmsg->AttackCommand.wptTarget.wx) + kwcTileHalf;
|
|
m_wptTarget.wy = WcTrunc(pmsg->AttackCommand.wptTarget.wy) + kwcTileHalf;
|
|
m_wptTargetCenter.wx = WcTrunc(pmsg->AttackCommand.wptTargetCenter.wx) + kwcTileHalf;
|
|
m_wptTargetCenter.wy = WcTrunc(pmsg->AttackCommand.wptTargetCenter.wy) + kwcTileHalf;
|
|
m_tcTargetRadius = pmsg->AttackCommand.tcTargetRadius;
|
|
m_wcMoveDistPerUpdate = pmsg->AttackCommand.wcMoveDistPerUpdate;
|
|
}
|
|
|
|
// Other messages are inter and intra-Unit communication, sometimes delayed
|
|
|
|
OnMsg(kmidNearbyAllyHit)
|
|
m_cupdLastHitOrNearbyAllyHit = gsim.GetUpdateCount();
|
|
|
|
// Respond pretty much as if self was the one hit
|
|
|
|
goto lbHitNoDamage;
|
|
|
|
OnMsg(kmidHit)
|
|
m_cupdLastHitOrNearbyAllyHit = gsim.GetUpdateCount();
|
|
|
|
// apply damage
|
|
// NOTE: This scoping is for the benefit of gcc which is concerned about
|
|
// the above goto skipping the initialization of fxDamage
|
|
|
|
{
|
|
fix fxDamage = itofx(pmsg->Hit.nDamage);
|
|
if (m_pplr->GetHandicap() & kfHcapIncreasedArmor)
|
|
fxDamage = (fix)mulfx(fxDamage, (itofx(knDecreasedDamagePercent) / 100));
|
|
SetHealth(subfx(m_fxHealth, fxDamage));
|
|
}
|
|
|
|
if (m_fxHealth <= 0) {
|
|
Player *pplr = gplrm.GetPlayer(pmsg->Hit.sideAssailant);
|
|
pplr->IncEnemyMobileUnitsKilled();
|
|
m_pplr->IncMobileUnitsLost();
|
|
|
|
SetState(kstDying);
|
|
|
|
// Think about fighting back
|
|
|
|
} else {
|
|
ShowDamageIndicator();
|
|
|
|
lbHitNoDamage:
|
|
// UNDONE: knAggressivenessCoward
|
|
if (m_wfMunt & kfMuntRunAwayWhenHit) {
|
|
|
|
|
|
// If we're supposed to return fire and we know who fired at us...
|
|
|
|
} else if ((m_wfMunt & kfMuntReturnFire) && pmsg->Hit.gidAssailant != kgidNull) {
|
|
|
|
// and we're not already attacking an in-range target...
|
|
|
|
if (m_gidTarget == kgidNull || !IsTargetInRange()) {
|
|
|
|
// and we're not on a human directed mission...
|
|
|
|
if ((m_pplr->GetFlags() & kfPlrComputer) || (m_st == kstGuard)) {
|
|
|
|
// Fight back!
|
|
|
|
SendAttackCommand(m_gid, pmsg->Hit.gidAssailant);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Because this logic is reused by the kmidNearbyAllyHit handler
|
|
|
|
if (pmsg->mid != kmidNearbyAllyHit)
|
|
NotifyNearbyAlliesOfHit(pmsg->Hit.gidAssailant);
|
|
|
|
OnMsg(kmidEnemyNearby)
|
|
// Notification that an enemy is nearby
|
|
|
|
RememberEnemyNearby(pmsg->EnemyNearby.gidEnemy);
|
|
|
|
// Wake up and check out the enemy
|
|
|
|
m_unvl.MinSkip();
|
|
|
|
OnMsg(kmidMoveWaitingNearby)
|
|
// We're waiting on a gob that has decided to either go into transition
|
|
// or stop Wake up and check what to do
|
|
|
|
m_unvl.MinSkip();
|
|
|
|
OnMsg(kmidFireComplete)
|
|
m_wfMunt &= ~kfMuntFiring;
|
|
m_unvl.MinSkip();
|
|
|
|
OnMsg(kmidDelete)
|
|
Assert("Shouldn't receive kmidDelete when not in kstDying state");
|
|
|
|
//-----------------------------------------------------------------------
|
|
|
|
State(kstGuard)
|
|
OnEnter
|
|
// Play idle animation
|
|
|
|
StartAnimation(&m_ani, m_pmuntc->anIdleStripIndices[m_dir], 0, kfAniDone);
|
|
m_wptTarget.wx = kwxInvalid;
|
|
m_gidTarget = kgidNull;
|
|
m_cCountdown = GetIdleCountdown();
|
|
if (m_wfMunt & kfMuntMoveWaitingNearby)
|
|
NotifyMoveWaitingNearby(m_wx, m_wy);
|
|
Assert(!(m_wfMunt & (kfMuntPathPending | kfMuntMoveWaitingNearby)));
|
|
|
|
OnUpdate
|
|
// Note if an enemy is nearby the update interval is min-ed
|
|
// by the enemy nearby notification, causing this path
|
|
// to execute right away. Otherwise most of the time the
|
|
// state machine is sleeping while in guard state.
|
|
|
|
// A command might have been pended while we waited for the
|
|
// firing animation to complete.
|
|
|
|
if (m_wfMunt & kfMuntCommandPending && IsReadyForCommand()) {
|
|
m_wfMunt &= ~kfMuntCommandPending;
|
|
gsmm.SendMsg(&m_msgPending);
|
|
return knHandled;
|
|
}
|
|
|
|
// if enemy in range and we're not already firing at something
|
|
|
|
if ((m_wfMunt & (kfMuntAttackEnemiesWhenGuarding | kfMuntFiring)) == kfMuntAttackEnemiesWhenGuarding) {
|
|
TCoord tcSightRange;
|
|
if (m_mua == kmuaGuardVicinity)
|
|
tcSightRange = ktcVicinity;
|
|
else
|
|
tcSightRange = m_pmuntc->tcFiringRange;
|
|
UnitGob *puntTarget = FindEnemyNearby(tcSightRange);
|
|
if (puntTarget != NULL) {
|
|
if (m_pplr->GetFlags() & kfPlrComputer) {
|
|
puntTarget->GetAttackPoint(&m_wptTarget);
|
|
m_gidTarget = puntTarget->GetId();
|
|
m_wptTargetCenter = m_wptTarget;
|
|
m_tcTargetRadius = 0;
|
|
m_wcMoveDistPerUpdate = m_pmuntc->GetMoveDistPerUpdate();
|
|
SetStatePendingFireComplete(kstAttack);
|
|
|
|
// Pretend we're hit so nearby units will come help
|
|
|
|
NotifyNearbyAlliesOfHit(m_gidTarget);
|
|
return knHandled;
|
|
}
|
|
|
|
// Play firing animation
|
|
|
|
puntTarget->GetCenter(&m_wptTarget);
|
|
Fire(puntTarget, m_wptTarget.wx, m_wptTarget.wy, m_wptTarget.wx - m_wx, m_wptTarget.wy - m_wy);
|
|
m_cCountdown = GetIdleCountdown();
|
|
|
|
// Pretend we're hit so nearby units will come help
|
|
|
|
NotifyNearbyAlliesOfHit(puntTarget->GetId());
|
|
|
|
} else if (m_mua == kmuaGuardArea) {
|
|
// Find a valid target
|
|
|
|
UnitGob *puntTarget = FindValidTargetInArea(m_msgAction.GuardAreaCommand.nArea);
|
|
if (puntTarget != NULL) {
|
|
puntTarget->GetAttackPoint(&m_wptTarget);
|
|
m_gidTarget = puntTarget->GetId();
|
|
m_wptTargetCenter = m_wptTarget;
|
|
m_tcTargetRadius = 0;
|
|
m_wcMoveDistPerUpdate = m_pmuntc->GetMoveDistPerUpdate();
|
|
SetStatePendingFireComplete(kstAttack);
|
|
return knHandled;
|
|
}
|
|
|
|
} else {
|
|
m_wptTarget.wx = kwxInvalid;
|
|
m_gidTarget = kgidNull;
|
|
}
|
|
}
|
|
|
|
// Animate (idle or firing)
|
|
|
|
AdvanceAnimation(&m_ani);
|
|
|
|
m_cCountdown -= m_unvl.GetUpdateCount();
|
|
if (m_cCountdown < 0) {
|
|
Idle();
|
|
m_cCountdown = GetIdleCountdown();
|
|
}
|
|
m_unvl.MinSkip(m_cCountdown);
|
|
|
|
// Handle flashing
|
|
|
|
DefUpdate();
|
|
|
|
//-----------------------------------------------------------------------
|
|
|
|
State(kstMove)
|
|
OnEnter
|
|
MoveEnter();
|
|
|
|
OnExit
|
|
MoveExit();
|
|
|
|
OnUpdate
|
|
if (!(m_wfMunt & kfMuntCommandPending) && !InTransition()) {
|
|
|
|
// If the Unit is sufficiently aggressive and sees an enemy
|
|
// unit it should attack it.
|
|
|
|
if (m_wfMunt & kfMuntAttackEnemiesWhenMoving) {
|
|
UnitGob *puntTarget = FindEnemyNearby(m_pmuntc->tcFiringRange);
|
|
if (puntTarget != NULL) {
|
|
m_gidTarget = puntTarget->GetId();
|
|
puntTarget->GetAttackPoint(&m_wptTarget);
|
|
m_wptTargetCenter = m_wptTarget;
|
|
m_tcTargetRadius = 0;
|
|
m_wcMoveDistPerUpdate = m_pmuntc->GetMoveDistPerUpdate();
|
|
SetState(kstAttack);
|
|
return knHandled;
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (MoveUpdate()) {
|
|
case knMoveTargetReached:
|
|
if (m_mua == kmuaMove)
|
|
m_mua = kmuaNone; // Move action complete
|
|
|
|
SetState(kstGuard);
|
|
break;
|
|
|
|
case knMoveStuck:
|
|
// UNDONE: what to do? Doing nothing means we repath every
|
|
// Update (jogging in place) until a valid path can be determined.
|
|
|
|
// For now, give up and return to Guard state
|
|
|
|
if (m_mua == kmuaMove)
|
|
m_mua = kmuaNone; // Move action complete
|
|
|
|
SetState(kstGuard);
|
|
break;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------
|
|
|
|
State(kstAttack)
|
|
OnExit
|
|
// Send notifications to any gobs waiting on this gob to get out
|
|
// of attack state
|
|
|
|
if (m_wfMunt & kfMuntMoveWaitingNearby)
|
|
NotifyMoveWaitingNearby(m_wx, m_wy);
|
|
|
|
OnUpdate
|
|
Assert(m_gidTarget != kgidNull);
|
|
|
|
// A command might have been pended while we waited for the
|
|
// firing animation to complete.
|
|
|
|
if (m_wfMunt & kfMuntCommandPending && IsReadyForCommand()) {
|
|
m_wfMunt &= ~kfMuntCommandPending;
|
|
gsmm.SendMsg(&m_msgPending);
|
|
return knHandled;
|
|
}
|
|
|
|
// if enemy dead/gone or taken over, go to guard mode
|
|
|
|
UnitGob *puntTarget = (UnitGob *)ggobm.GetGob(m_gidTarget);
|
|
if (puntTarget == NULL || !IsValidTarget(puntTarget)) {
|
|
|
|
// If an Attack action is being carried out and successfully
|
|
// completed, indicate that it is done.
|
|
|
|
if (m_mua == kmuaAttack && m_gidTarget == m_msgAction.AttackCommand.gidTarget) {
|
|
m_mua = kmuaNone; // Action complete
|
|
|
|
// If some other action was interrupted to attack, return to it
|
|
|
|
} else if (m_mua != kmuaNone) {
|
|
m_gidTarget = kgidNull;
|
|
ContinueActionPendingFireComplete();
|
|
return knHandled;
|
|
}
|
|
|
|
m_gidTarget = kgidNull;
|
|
SetStatePendingFireComplete(kstGuard);
|
|
|
|
} else {
|
|
// if enemy not in range think about chasing it
|
|
|
|
if (!IsGobWithinRange(puntTarget, m_pmuntc->tcFiringRange)) {
|
|
// Test actions and situations under which it is a good idea to chase
|
|
|
|
if (!(m_wfMunt & kfMuntStayPut) && (m_mua != kmuaNone || !IsStandingOnActivator()) &&
|
|
(m_wfMunt & kfMuntChaseEnemies ||
|
|
(gsim.GetUpdateCount() - m_cupdLastHitOrNearbyAllyHit <= kcupdAggressivenessBoost) ||
|
|
(m_mua == kmuaAttack && m_gidTarget == m_msgAction.AttackCommand.gidTarget) ||
|
|
(m_mua == kmuaGuardArea && ggobm.IsGobWithinArea(puntTarget, m_msgAction.GuardAreaCommand.nArea)) ||
|
|
(m_mua == kmuaGuardVicinity || m_mua == kmuaHuntEnemies) ||
|
|
IsHumanOrGodControlled()) && IsAttackPointWithinFiringRangeOfTarget(puntTarget)) {
|
|
|
|
SetStatePendingFireComplete(kstChase);
|
|
|
|
// For all other situations we return to the action in-progress
|
|
|
|
} else if (m_mua != kmuaNone) {
|
|
ContinueActionPendingFireComplete();
|
|
return knHandled;
|
|
|
|
// Or if no action, just fall back to guard state
|
|
|
|
} else {
|
|
SetStatePendingFireComplete(kstGuard);
|
|
}
|
|
} else {
|
|
// Check for assumptions
|
|
|
|
Assert(m_ppathUnit == NULL);
|
|
Assert(!IsMoveWaiting());
|
|
|
|
// Otherwise, fire at the enemy
|
|
|
|
if (!(m_wfMunt & kfMuntFiring)) {
|
|
puntTarget->GetAttackPoint(&m_wptTarget);
|
|
WPoint wptFire;
|
|
puntTarget->GetCenter(&wptFire);
|
|
Fire(puntTarget, wptFire.wx, wptFire.wy, wptFire.wx - m_wx, wptFire.wy - m_wy);
|
|
}
|
|
}
|
|
}
|
|
|
|
AdvanceAnimation(&m_ani);
|
|
|
|
// Handle flashing
|
|
|
|
DefUpdate();
|
|
|
|
//-----------------------------------------------------------------------
|
|
|
|
State(kstChase)
|
|
OnEnter
|
|
MoveEnter();
|
|
|
|
// Remember where we found the target last. If it moves position we'll repath
|
|
|
|
Gob *pgobTarget = ggobm.GetGob(m_gidTarget);
|
|
if (pgobTarget != NULL) {
|
|
Assert(pgobTarget->GetFlags() & kfGobUnit);
|
|
UnitGob *puntTarget = (UnitGob *)pgobTarget;
|
|
WPoint wptTarget;
|
|
puntTarget->GetAttackPoint(&wptTarget);
|
|
m_tptChaseInitial.tx = TcFromWc(wptTarget.wx);
|
|
m_tptChaseInitial.ty = TcFromWc(wptTarget.wy);
|
|
}
|
|
|
|
OnExit
|
|
MoveExit();
|
|
|
|
OnUpdate
|
|
if (IsReadyForCommand()) {
|
|
if (m_wfMunt & kfMuntCommandPending) {
|
|
m_wfMunt &= ~kfMuntCommandPending;
|
|
gsmm.SendMsg(&m_msgPending);
|
|
return knHandled;
|
|
}
|
|
|
|
// If the Unit is executing the AttackAction and is sufficiently
|
|
// aggressive and sees an enemy unit it should attack it.
|
|
|
|
if (m_mua == kmuaAttack) {
|
|
if (m_wfMunt & kfMuntAttackEnemiesWhenMoving) {
|
|
UnitGob *puntTarget = FindEnemyNearby(m_pmuntc->tcFiringRange);
|
|
if (puntTarget != NULL) {
|
|
m_gidTarget = puntTarget->GetId();
|
|
puntTarget->GetAttackPoint(&m_wptTarget);
|
|
SetState(kstAttack);
|
|
return knHandled;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify the target still exists
|
|
|
|
Gob *pgobTarget = ggobm.GetGob(m_gidTarget);
|
|
if (pgobTarget == NULL || !IsValidTarget(pgobTarget)) {
|
|
|
|
// If some other action was interrupted to attack/chase, return to it
|
|
|
|
if (m_mua != kmuaNone) {
|
|
m_gidTarget = kgidNull;
|
|
gsmm.SendMsg(&m_msgAction);
|
|
return knHandled;
|
|
}
|
|
|
|
m_gidTarget = kgidNull;
|
|
SetState(kstGuard);
|
|
return knHandled;
|
|
}
|
|
|
|
// if enemy is in range, drop back into Attack mode
|
|
|
|
if (IsGobWithinRange(pgobTarget, m_pmuntc->tcFiringRange)) {
|
|
SetState(kstAttack);
|
|
return knHandled;
|
|
|
|
} else if (!(m_wfMunt & kfMuntChaseEnemies) && !IsHumanOrGodControlled()) {
|
|
|
|
// If we're trying to guard an area and the enemy has left it let it go.
|
|
|
|
if (m_mua == kmuaGuardArea) {
|
|
if (!ggobm.IsGobWithinArea(pgobTarget, m_msgAction.GuardAreaCommand.nArea)) {
|
|
SetState(kstGuard);
|
|
return knHandled;
|
|
}
|
|
|
|
// If we're trying to guard an expanded radius and the enemy has left it let it go.
|
|
|
|
} else if (m_mua == kmuaGuardVicinity) {
|
|
if (!IsGobWithinRange(pgobTarget, ktcVicinity) && gsim.GetUpdateCount() - m_cupdLastHitOrNearbyAllyHit > kcupdAggressivenessBoost) {
|
|
SetState(kstGuard);
|
|
return knHandled;
|
|
}
|
|
|
|
// These are inherently chase modes. Stick with it.
|
|
|
|
} else if (m_mua == kmuaHuntEnemies || m_mua == kmuaAttack) {
|
|
|
|
// If we're not acting under an aggressiveness boost anymore drop back to the
|
|
// prior action or Guard
|
|
|
|
} else if (gsim.GetUpdateCount() - m_cupdLastHitOrNearbyAllyHit > kcupdAggressivenessBoost) {
|
|
if (m_mua != kmuaNone)
|
|
gsmm.SendMsg(&m_msgAction);
|
|
else
|
|
SetState(kstGuard);
|
|
return knHandled;
|
|
}
|
|
}
|
|
|
|
// If the target moved, force a new path to get calced
|
|
|
|
Assert(pgobTarget->GetFlags() & kfGobUnit);
|
|
UnitGob *puntTarget = (UnitGob *)pgobTarget;
|
|
WPoint wptTarget;
|
|
puntTarget->GetAttackPoint(&wptTarget);
|
|
TCoord tx = TcFromWc(wptTarget.wx);
|
|
TCoord ty = TcFromWc(wptTarget.wy);
|
|
if (tx != m_tptChaseInitial.tx || ty != m_tptChaseInitial.ty) {
|
|
m_tptChaseInitial.tx = tx;
|
|
m_tptChaseInitial.ty = ty;
|
|
m_wptTarget = wptTarget;
|
|
MoveExit();
|
|
}
|
|
}
|
|
|
|
switch (MoveUpdate()) {
|
|
case knMoveTargetReached:
|
|
SetState(kstAttack);
|
|
return knHandled;
|
|
|
|
case knMoveStuck:
|
|
// Next few updates, use the target gob's center
|
|
{
|
|
Gob *pgobTarget = ggobm.GetGob(m_gidTarget);
|
|
Assert(pgobTarget->GetFlags() & kfGobUnit);
|
|
UnitGob *puntTarget = (UnitGob *)pgobTarget;
|
|
if (puntTarget != NULL) {
|
|
WPoint wptTarget;
|
|
puntTarget->GetAttackPoint(&wptTarget);
|
|
if (WcTrunc(wptTarget.wx) != WcTrunc(m_wptTarget.wx) || WcTrunc(wptTarget.wy) != WcTrunc(m_wptTarget.wy)) {
|
|
m_wptTarget = wptTarget;
|
|
m_unvl.MinSkip(12);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// UNDONE: Put in waiting code so a new path doesn't get searched
|
|
// every update
|
|
|
|
m_gidTarget = kgidNull;
|
|
|
|
#if 0
|
|
// Causes looping problems (kstChase->kstAttack->kstChase). Plus "stuck" cases happen less often, so try
|
|
// taking this out and revert to guard mode to see how it feels.
|
|
|
|
// Return to any action in progress
|
|
|
|
if (m_mua != kmuaNone) {
|
|
gsmm.SendMsg(&m_msgAction);
|
|
return knHandled;
|
|
}
|
|
#endif
|
|
|
|
// UNDONE: Perhaps in this case try searching around waiting gobs.
|
|
|
|
SetState(kstGuard);
|
|
return knHandled;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------
|
|
// Hunting units sit idle if they can't find a target and won't do
|
|
// anything except return fire (same as knAggressivenessSelfDefense)
|
|
|
|
State(kstHuntEnemies)
|
|
OnEnter
|
|
// Play idle animation
|
|
|
|
StartAnimation(&m_ani, m_pmuntc->anIdleStripIndices[m_dir], 0, kfAniDone);
|
|
m_wptTarget.wx = kwxInvalid;
|
|
m_gidTarget = kgidNull;
|
|
m_cCountdown = GetIdleCountdown();
|
|
|
|
OnUpdate
|
|
Gob *pgobTarget = ggobm.GetGob(m_gidTarget);
|
|
if (pgobTarget == NULL || !IsValidTarget(pgobTarget)) {
|
|
int cpuntFound = 0;
|
|
Assert(gcbScratch >= kcpgobMax * sizeof(UnitGob *));
|
|
UnitGob **apuntFound = (UnitGob **)gpbScratch;
|
|
|
|
// UNDONE: only do this every few updates
|
|
// Find an enemy to attack
|
|
|
|
Gob *pgob = ggobm.GetFirstGob();
|
|
for (; pgob != NULL; pgob = ggobm.GetNextGob(pgob)) {
|
|
// Is it an active enemy that this unit type is allowed to attack?
|
|
|
|
if (!IsValidTarget(pgob))
|
|
continue; // no
|
|
|
|
// Only Units pass the IsValidTarget test
|
|
|
|
UnitGob *punt = (UnitGob *)pgob;
|
|
|
|
// Is it one of the type of units we're supposed to hunt?
|
|
|
|
if (!(punt->GetConsts()->um & m_msgAction.HuntEnemiesCommand.um))
|
|
continue;
|
|
|
|
apuntFound[cpuntFound++] = punt;
|
|
}
|
|
|
|
if (cpuntFound > 0) {
|
|
UnitGob *punt = apuntFound[GetRandom() % cpuntFound];
|
|
punt->GetAttackPoint(&m_wptTarget);
|
|
m_gidTarget = punt->GetId();
|
|
m_wptTargetCenter = m_wptTarget;
|
|
m_tcTargetRadius = 0;
|
|
m_wcMoveDistPerUpdate = m_pmuntc->GetMoveDistPerUpdate();
|
|
SetState(kstAttack);
|
|
return knHandled;
|
|
}
|
|
}
|
|
|
|
AdvanceAnimation(&m_ani); // idle animation
|
|
|
|
m_cCountdown -= m_unvl.GetUpdateCount();
|
|
if (m_cCountdown < 0) {
|
|
Idle();
|
|
m_cCountdown = GetIdleCountdown();
|
|
}
|
|
m_unvl.MinSkip(m_cCountdown);
|
|
|
|
// Handle flashing
|
|
|
|
DefUpdate();
|
|
|
|
//-----------------------------------------------------------------------
|
|
|
|
State(kstDying)
|
|
OnEnter
|
|
TRect trc;
|
|
GetTileRect(&trc);
|
|
|
|
Deactivate();
|
|
|
|
// Redraw this part of minimap. It will skip inactive munts
|
|
|
|
gpmm->RedrawTRect(&trc);
|
|
|
|
#ifdef DEBUG
|
|
if (m_wfMunt & kfMuntDestinationReserved) {
|
|
Assert(!IsTileReserved(m_txDst, m_tyDst));
|
|
} else {
|
|
Assert(!IsTileReserved(TcFromWc(m_wx), TcFromWc(m_wy)));
|
|
}
|
|
#endif
|
|
|
|
m_ff ^= kfGobLayerDepthSorted | kfGobLayerSurfaceDecal;
|
|
|
|
gsndm.PlaySfx(SfxFromCategory(m_pmuntc->sfxcDestroyed));
|
|
|
|
// remove corpse in .2 seconds
|
|
|
|
gsmm.SendDelayedMsg(kmidDelete, 20, m_gid, m_gid);
|
|
|
|
Gob *pgobExpl = CreateAnimGob(m_wx, m_wy, kfAnmDeleteWhenDone | kfAnmSmokeFireLayer, NULL, s_panidVehicleExplosion);
|
|
if (pgobExpl != NULL)
|
|
pgobExpl->SetOwner(m_pplr);
|
|
|
|
OnUpdate
|
|
// Handle flashing
|
|
|
|
DefUpdate();
|
|
|
|
OnMsg(kmidDelete)
|
|
Delete();
|
|
return knDeleted;
|
|
|
|
// Eat all other messages (e.g., kmidHit, kmidNearbyAllyHit)
|
|
|
|
DiscardMsgs
|
|
|
|
//-----------------------------------------------------------------------
|
|
|
|
State(kstChangeStatePendingFireComplete)
|
|
OnUpdate
|
|
if (!(m_wfMunt & kfMuntFiring)) {
|
|
SetState(m_stPending);
|
|
m_stPending = kstReservedNull;
|
|
}
|
|
|
|
// Will advance any ongoing firing animation
|
|
|
|
DefUpdate();
|
|
|
|
State(kstContinueActionPendingFireComplete)
|
|
OnUpdate
|
|
if (!(m_wfMunt & kfMuntFiring)) {
|
|
gsmm.SendMsg(&m_msgAction);
|
|
return knHandled;
|
|
}
|
|
|
|
// Will advance any ongoing firing animation
|
|
|
|
DefUpdate();
|
|
|
|
EndStateMachine
|
|
}
|
|
|
|
void MobileUnitGob::ReserveTile(TCoord tx, TCoord ty, bool fReserve)
|
|
{
|
|
if (fReserve) {
|
|
gsim.GetLevel()->GetTerrainMap()->SetFlags(tx, ty, 1, 1, kbfMobileUnit);
|
|
} else {
|
|
gsim.GetLevel()->GetTerrainMap()->ClearFlags(tx, ty, 1, 1, kbfMobileUnit);
|
|
}
|
|
}
|
|
|
|
bool MobileUnitGob::IsTileReserved(TCoord tx, TCoord ty)
|
|
{
|
|
return gsim.GetLevel()->GetTerrainMap()->TestFlags(tx, ty, 1, 1, kbfMobileUnit);
|
|
}
|
|
|
|
byte MobileUnitGob::GetTileFlags(TCoord tx, TCoord ty)
|
|
{
|
|
byte bf;
|
|
bool f = gsim.GetLevel()->GetTerrainMap()->GetFlags(tx, ty, &bf);
|
|
if (!f)
|
|
bf = kbfStructure;
|
|
return bf;
|
|
}
|
|
|
|
bool MobileUnitGob::InTransition()
|
|
{
|
|
if (m_ppathUnit == NULL)
|
|
return false;
|
|
|
|
return m_cMoveStepsRemaining != 0;
|
|
}
|
|
|
|
bool MobileUnitGob::IsReadyForCommand()
|
|
{
|
|
if (m_wfMunt & kfMuntFiring)
|
|
return false;
|
|
return !InTransition();
|
|
}
|
|
|
|
void MobileUnitGob::NotifyMoveWaitingNearby(WCoord wx, WCoord wy)
|
|
{
|
|
Assert(m_wfMunt & kfMuntMoveWaitingNearby);
|
|
m_wfMunt &= ~kfMuntMoveWaitingNearby;
|
|
|
|
// Look around our dst to see if there are units trying to get in to our dst tile
|
|
|
|
TCoord txDst = TcFromWc(wx);
|
|
TCoord tyDst = TcFromWc(wy);
|
|
|
|
TCoord ctx, cty;
|
|
ggobm.GetMapSize(&ctx, &cty);
|
|
|
|
for (int n = 0; n < ARRAYSIZE(g_mpDirToDx); n++) {
|
|
// Get valid TCoord to check
|
|
|
|
TCoord tx = txDst + g_mpDirToDx[n];
|
|
TCoord ty = tyDst + g_mpDirToDy[n];
|
|
if (tx < 0 || tx >= ctx)
|
|
continue;
|
|
if (ty < 0 || ty >= cty)
|
|
continue;
|
|
|
|
// Check it for gobs that are waiting on this gob
|
|
|
|
for (Gid gid = ggobm.GetFirstGid(tx, ty); gid != kgidNull; gid = ggobm.GetNextGid(gid)) {
|
|
// Any mobile units in this tile?
|
|
|
|
MobileUnitGob *pgob = (MobileUnitGob *)ggobm.GetGob(gid);
|
|
if (pgob == NULL)
|
|
continue;
|
|
if (!(pgob->m_ff & kfGobMobileUnit))
|
|
continue;
|
|
|
|
// See if this mobile unit is in transition towards our desired tile
|
|
|
|
if (pgob->m_txDst == txDst && pgob->m_tyDst == tyDst) {
|
|
if (pgob->IsMoveWaiting()) {
|
|
gsmm.SendMsg(kmidMoveWaitingNearby, pgob->GetId());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
MobileUnitGob *MobileUnitGob::GetReservingUnit(TCoord tx, TCoord ty)
|
|
{
|
|
// Need to enumerate all mobile units in this tile, there can be more than one
|
|
// in certain transition situations
|
|
|
|
for (Gid gid = ggobm.GetFirstGid(tx, ty); gid != kgidNull; gid = ggobm.GetNextGid(gid)) {
|
|
// Any mobile units in this tile?
|
|
|
|
MobileUnitGob *pmunt = (MobileUnitGob *)ggobm.GetGob(gid);
|
|
if (pmunt == NULL)
|
|
continue;
|
|
if (!(pmunt->m_ff & kfGobMobileUnit))
|
|
continue;
|
|
|
|
// It is the reserving gob if it either isn't in transition or
|
|
// is in transition to this tile
|
|
|
|
if (!pmunt->InTransition() || (pmunt->m_txDst == tx && pmunt->m_tyDst == ty)) {
|
|
Assert(IsTileReserved(tx, ty));
|
|
return pmunt;
|
|
}
|
|
}
|
|
|
|
// Now look for units transitioning into this tile
|
|
|
|
MobileUnitGob *pmunt = AnyTransitionsIntoTile(tx, ty, NULL);
|
|
Assert((pmunt != NULL) == IsTileReserved(tx, ty));
|
|
return pmunt;
|
|
}
|
|
|
|
MobileUnitGob *MobileUnitGob::AnyTransitionsIntoTile(TCoord txDst, TCoord tyDst, Gob *pgobIgnore)
|
|
{
|
|
// Look around our dst to see if there are units trying to get in to our dst tile
|
|
|
|
TCoord ctx, cty;
|
|
ggobm.GetMapSize(&ctx, &cty);
|
|
|
|
for (int n = 0; n < ARRAYSIZE(g_mpDirToDx); n++) {
|
|
// Get valid TCoord to check
|
|
|
|
TCoord tx = txDst + g_mpDirToDx[n];
|
|
TCoord ty = tyDst + g_mpDirToDy[n];
|
|
if (tx < 0 || tx >= ctx)
|
|
continue;
|
|
if (ty < 0 || ty >= cty)
|
|
continue;
|
|
|
|
// Check it for gobs that are entering our dst
|
|
|
|
for (Gid gid = ggobm.GetFirstGid(tx, ty); gid != kgidNull; gid = ggobm.GetNextGid(gid)) {
|
|
// Any mobile units in this tile?
|
|
|
|
MobileUnitGob *pgob = (MobileUnitGob *)ggobm.GetGob(gid);
|
|
if (pgob == NULL || pgob == pgobIgnore)
|
|
continue;
|
|
if (!(pgob->m_ff & kfGobMobileUnit))
|
|
continue;
|
|
|
|
// See if this mobile unit is in transition towards our desired tile
|
|
|
|
if (pgob->m_txDst == txDst && pgob->m_tyDst == tyDst) {
|
|
if (pgob->InTransition()) {
|
|
return pgob;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void MobileUnitGob::MoveEnter(bool fReset)
|
|
{
|
|
// Catch movewait bits that are set but shouldn't be
|
|
|
|
Assert(!IsMoveWaiting());
|
|
|
|
// Make sure we're not in transition
|
|
|
|
Assert(!InTransition());
|
|
StartAnimation(&m_ani, m_pmuntc->anMovingStripIndices[m_dir], 0, kfAniLoop);
|
|
|
|
// These get reset when this move isn't aware of group behavior
|
|
|
|
if (fReset) {
|
|
m_wptTargetCenter = m_wptTarget;
|
|
m_tcTargetRadius = 0;
|
|
}
|
|
if (m_wcMoveDistPerUpdate == 0)
|
|
m_wcMoveDistPerUpdate = m_pmuntc->GetMoveDistPerUpdate();
|
|
|
|
// If the target is in blocked area, walk back toward this unit to find the first free tile,
|
|
// in a straight line.
|
|
|
|
TerrainMap *ptrmap = gsim.GetLevel()->GetTerrainMap();
|
|
TCoord txTo = TcFromWc(m_wptTarget.wx);
|
|
TCoord tyTo = TcFromWc(m_wptTarget.wy);
|
|
TCoord txFree, tyFree;
|
|
if (ptrmap->IsBlocked(txTo, tyTo, 0)) {
|
|
if (ptrmap->FindFirstUnoccupied(txTo, tyTo, TcFromWc(m_wx), TcFromWc(m_wy), &txFree, &tyFree)) {
|
|
m_wptTarget.wx = WcFromTc(txFree) + kwcTileHalf;
|
|
m_wptTarget.wy = WcFromTc(tyFree) + kwcTileHalf;
|
|
}
|
|
}
|
|
|
|
// Let movement code know that a path is pending for this gob so proper waiting semantics can be
|
|
// performed.
|
|
|
|
m_wfMunt |= kfMuntPathPending;
|
|
m_wfMunt &= ~kfMuntStuck;
|
|
}
|
|
|
|
void MobileUnitGob::MoveExit()
|
|
{
|
|
m_wfMunt &= ~(kfMuntMoveWait | kfMuntPathPending);
|
|
delete m_ppathUnit;
|
|
m_ppathUnit = NULL;
|
|
delete m_ppathAvoid;
|
|
m_ppathAvoid = NULL;
|
|
|
|
// Send notifications to any gobs waiting on this gob
|
|
|
|
if (m_wfMunt & kfMuntMoveWaitingNearby)
|
|
NotifyMoveWaitingNearby(m_wx, m_wy);
|
|
}
|
|
|
|
Path *MobileUnitGob::FindPath(TCoord txFrom, TCoord tyFrom, TCoord txTo, TCoord tyTo)
|
|
{
|
|
TerrainMap *ptrmap = gsim.GetLevel()->GetTerrainMap();
|
|
|
|
#ifdef __CPU_68K
|
|
// See if we have a cached path that we can reuse - 68K only since this
|
|
// sucks
|
|
|
|
#define ktcPathLengthReuse 35
|
|
#define ktcPathEndPointReuse 7
|
|
|
|
word nDistT = ganSquared[abs(txFrom - txTo)] + ganSquared[abs(tyFrom - tyTo)];
|
|
if (GetType() != kgtGalaxMiner && nDistT >= ganSquared[ktcPathLengthReuse]) {
|
|
for (int nPath = 0; nPath < s_cpathCached; nPath++) {
|
|
// Long enough to be worth it?
|
|
|
|
Path *ppathT = s_apathCached[nPath];
|
|
if (ppathT->GetCount() < ktcPathLengthReuse)
|
|
continue;
|
|
|
|
// In the general vicinity of our start / end points
|
|
|
|
TPoint tptStart;
|
|
ppathT->GetStartPoint(&tptStart);
|
|
TPoint tptEnd;
|
|
ppathT->GetPointRaw(ppathT->GetCount() - 1, &tptEnd);
|
|
word nDistStart = ganSquared[abs(tptStart.tx - txFrom)] + ganSquared[abs(tptStart.ty - tyFrom)];
|
|
word nDistEnd = ganSquared[abs(tptEnd.tx - txTo)] + ganSquared[abs(tptEnd.ty - tyTo)];
|
|
word nDistReuse = ganSquared[ktcPathEndPointReuse] * 2;
|
|
if (nDistStart > nDistReuse || nDistEnd > nDistReuse)
|
|
continue;
|
|
|
|
// See if our closest approach is free of obstacles
|
|
|
|
TPoint tptClosest;
|
|
if (ppathT->FindClosestPoint(txFrom, tyFrom, 0, 5, &tptClosest) == -1)
|
|
continue;
|
|
if (ptrmap->IsLineOccupied(txFrom, tyFrom, tptClosest.tx, tptClosest.ty, 0))
|
|
continue;
|
|
|
|
// Looking good so far. See if our closest end point is free of obstacles
|
|
|
|
TPoint tptDestClosest;
|
|
int itptDestClosest = ppathT->FindClosestPoint(txTo, tyTo, 0, ppathT->GetCount(), &tptDestClosest);
|
|
if (itptDestClosest == -1)
|
|
continue;
|
|
if (ptrmap->IsLineOccupied(tptDestClosest.tx, tptDestClosest.ty, txTo, tyTo, 0))
|
|
continue;
|
|
|
|
// We're ready to use this path. First create a copy
|
|
|
|
ppathT = ppathT->Clone();
|
|
if (ppathT == NULL)
|
|
continue;
|
|
|
|
// If the ending is the same, use it
|
|
|
|
if (tptDestClosest.tx == txTo && tptDestClosest.ty == tyTo)
|
|
return ppathT;
|
|
|
|
// Now lop off the end and amend it with our new dest
|
|
// If this doesn't work, use it as is
|
|
|
|
if (!ppathT->TrimEnd(itptDestClosest))
|
|
return ppathT;
|
|
|
|
// Add our new ending. If we run into something, use the path as is
|
|
|
|
Path *ppathAppend = ptrmap->FindLinePath(tptDestClosest.tx, tptDestClosest.ty, txTo, tyTo, kbfStructure);
|
|
if (ppathAppend == NULL)
|
|
return ppathT;
|
|
|
|
// Append this to our ppath
|
|
|
|
ppathT->Append(ppathAppend);
|
|
delete ppathAppend;
|
|
|
|
// Done
|
|
|
|
return ppathT;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// No cached paths that satisfy our needs. Make a new one.
|
|
// If it's close first try an easy path
|
|
|
|
#define ktcFastPathRangeMax 20
|
|
Path *ppathT = NULL;
|
|
if (ganSquared[abs(txFrom - txTo)] + ganSquared[abs(tyFrom - tyTo)] < ganSquared[ktcFastPathRangeMax] * 2)
|
|
ppathT = ptrmap->FindLinePath(txFrom, tyFrom, txTo, tyTo, kbfStructure);
|
|
if (ppathT == NULL)
|
|
ppathT = ptrmap->FindPath(txFrom, tyFrom, txTo, tyTo, kbfStructure);
|
|
|
|
#ifdef __CPU_68K
|
|
// Cache this path if it is long enough to be interesting
|
|
|
|
if (ppathT != NULL) {
|
|
if (ppathT->GetCount() >= ktcPathLengthReuse) {
|
|
// Move the existing paths that we have down one
|
|
|
|
Path *ppathCache = ppathT->Clone();
|
|
if (ppathCache != NULL) {
|
|
if (s_cpathCached == ARRAYSIZE(s_apathCached)) {
|
|
delete s_apathCached[ARRAYSIZE(s_apathCached) - 1];
|
|
} else {
|
|
s_cpathCached++;
|
|
}
|
|
memmove(&s_apathCached[1], &s_apathCached[0], (ARRAYSIZE(s_apathCached) - 1) * ELEMENTSIZE(s_apathCached));
|
|
s_apathCached[0] = ppathCache;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return ppathT;
|
|
}
|
|
|
|
void MobileUnitGob::FreeCachedPaths()
|
|
{
|
|
for (int nPath = 0; nPath < s_cpathCached; nPath++)
|
|
delete s_apathCached[nPath];
|
|
s_cpathCached = NULL;
|
|
}
|
|
|
|
bool MobileUnitGob::PrepPath(WCoord wxDst, WCoord wyDst)
|
|
{
|
|
// Get a path.
|
|
|
|
TCoord txFrom = TcFromWc(m_wx);
|
|
TCoord tyFrom = TcFromWc(m_wy);
|
|
TCoord txTo = TcFromWc(wxDst);
|
|
TCoord tyTo = TcFromWc(wyDst);
|
|
Path *ppathT = FindPath(txFrom, tyFrom, txTo, tyTo);
|
|
|
|
// Prep unit path
|
|
|
|
delete m_ppathAvoid;
|
|
m_ppathAvoid = NULL;
|
|
delete m_ppathUnit;
|
|
m_ppathUnit = ppathT;
|
|
m_itptPath = 0;
|
|
m_wfMunt &= ~kfMuntPathPending;
|
|
|
|
#ifdef DRAW_PATHS
|
|
m_wxDst = m_wx;
|
|
m_wyDst = m_wy;
|
|
#endif
|
|
m_cMoveStepsRemaining = 0;
|
|
|
|
return m_ppathUnit != NULL;
|
|
}
|
|
|
|
int MobileUnitGob::MoveUpdate()
|
|
{
|
|
// If a command is pending and now is a good time to handle it, do so.
|
|
|
|
if ((m_wfMunt & kfMuntCommandPending) && IsReadyForCommand()) {
|
|
m_wfMunt &= ~kfMuntCommandPending;
|
|
gsmm.SendMsg(&m_msgPending);
|
|
return knMoveMoving;
|
|
}
|
|
|
|
// Initialize a path if we haven't yet. Don't start moving on it yet so
|
|
// that all mobile units in a group move have paths. This establishes
|
|
// proper waiting behavior
|
|
|
|
if (m_ppathUnit == NULL) {
|
|
// Check if already there
|
|
|
|
if (CheckDestinationReached())
|
|
return knMoveTargetReached;
|
|
|
|
// No, so calc a path
|
|
|
|
if (!PrepPath(m_wptTarget.wx, m_wptTarget.wy)) {
|
|
// Trace("0x%08lx: 0x%08lx @ %d,%d calc path failed", HostGetTickCount(), this, TcFromWc(m_wx), TcFromWc(m_wy));
|
|
m_wfMunt |= kfMuntStuck;
|
|
return knMoveStuck;
|
|
}
|
|
// Trace("0x%08lx: 0x%08lx @ %d,%d calc path succeeded", HostGetTickCount(), this, TcFromWc(m_wx), TcFromWc(m_wy));
|
|
}
|
|
|
|
// We have a path. If we're not in transition, prepare the next transition
|
|
|
|
if (!InTransition()) {
|
|
// Trace("0x%08lx: 0x%08lx @ %d,%d not in transition.", HostGetTickCount(), this, TcFromWc(m_wx), TcFromWc(m_wy));
|
|
|
|
// If the next transition could not be prepared, error out
|
|
|
|
TPoint tptNext;
|
|
int nMoveResult;
|
|
if (!PrepareNextTransition(&tptNext, &nMoveResult)) {
|
|
#if 0 // def DEBUG
|
|
static char *s_aszMoveResult[] = { "knMoveMoving", "knMoveTargetReached", "knMoveStuck", "knMoveWaiting" };
|
|
Trace("0x%08lx @ %d,%d returning %s", this, TcFromWc(m_wx), TcFromWc(m_wy), s_aszMoveResult[nMoveResult]);
|
|
if (nMoveResult == knMoveWaiting)
|
|
Trace(" (waiting on %d, %d, reserving munt: 0x%08lx)", m_txDst, m_tyDst, GetReservingUnit(m_txDst, m_tyDst));
|
|
#endif
|
|
return nMoveResult;
|
|
}
|
|
|
|
// Looks good, enter transition
|
|
|
|
// Trace("0x%08lx @ %d,%d -> %d, %d entering transition.", this, TcFromWc(m_wx), TcFromWc(m_wy), tptNext.tx, tptNext.ty);
|
|
EnterTransition(&tptNext);
|
|
}
|
|
|
|
// We're in transition; move through the transition
|
|
|
|
ContinueTransition();
|
|
return knMoveMoving;
|
|
}
|
|
|
|
void MobileUnitGob::EnterTransition(TPoint *ptptNext)
|
|
{
|
|
// Attempt to enter the next transition
|
|
|
|
Assert(!InTransition());
|
|
Assert(ptptNext->tx != TcFromWc(m_wx) || ptptNext->ty != TcFromWc(m_wy));
|
|
|
|
// Clear the replicator input spot. This gob isn't a viable replication candidate in transition.
|
|
|
|
m_wfMunt &= ~kfMuntAtReplicatorInput;
|
|
|
|
// Remember where this unit is going
|
|
|
|
m_txDst = ptptNext->tx;
|
|
m_tyDst = ptptNext->ty;
|
|
|
|
// Remember this for "draw paths" feature
|
|
|
|
#ifdef DRAW_PATHS
|
|
m_wxDst = WcFromTc(m_txDst) + kwcTileHalf;
|
|
m_wyDst = WcFromTc(m_tyDst) + kwcTileHalf;
|
|
#endif
|
|
|
|
// Unreserve current tile, reserve new tile
|
|
|
|
TCoord txOld = TcFromWc(m_wx);
|
|
TCoord tyOld = TcFromWc(m_wy);
|
|
Assert(IsTileReserved(txOld, tyOld));
|
|
ReserveTile(txOld, tyOld, false);
|
|
m_dirNext = DirectionFromLocations(txOld, tyOld, ptptNext->tx, ptptNext->ty);
|
|
Assert(m_dirNext != kdirInvalid);
|
|
|
|
// Tile is assumed to be available; reserve it and start a transition
|
|
|
|
Assert(!IsTileReserved(ptptNext->tx, ptptNext->ty));
|
|
ReserveTile(ptptNext->tx, ptptNext->ty, true);
|
|
|
|
// Mark that we've reserved the dest tile so it can be unreserved if
|
|
// the unit becomes deactivated.
|
|
|
|
m_wfMunt |= kfMuntDestinationReserved;
|
|
|
|
// Send notifications to any gobs waiting on this gob's tile to clear, which it
|
|
// just did.
|
|
|
|
if (m_wfMunt & kfMuntMoveWaitingNearby)
|
|
NotifyMoveWaitingNearby(m_wx, m_wy);
|
|
|
|
// Prepare the appropriate animation
|
|
|
|
StartAnimation(&m_ani, m_pmuntc->anMovingStripIndices[m_dir], 0, kfAniLoop | kfAniIgnoreFirstAdvance);
|
|
|
|
// Calc move steps to next tile. Adjust for diagonal.
|
|
// This unit is now "in transition".
|
|
|
|
WCoord wcMoveDist = m_wcMoveDistPerUpdate;
|
|
if (m_dirNext & 1)
|
|
wcMoveDist = gawcDiagonalDist[wcMoveDist];
|
|
m_cMoveStepsRemaining = gaDiv256byNWithRounding[wcMoveDist];
|
|
}
|
|
|
|
void MobileUnitGob::ContinueTransition()
|
|
{
|
|
// We're in transition and simply moving to the next tile
|
|
|
|
Assert(InTransition());
|
|
|
|
// Transition movement doesn't skip updates
|
|
|
|
m_unvl.MinSkip();
|
|
|
|
// Make sure we're facing the way we want to go before we go
|
|
|
|
if (m_dirNext != m_dir) {
|
|
m_dir = TurnToward(m_dirNext, m_dir);
|
|
StartAnimation(&m_ani, m_pmuntc->anMovingStripIndices[m_dir], 0, kfAniLoop | kfAniIgnoreFirstAdvance);
|
|
return;
|
|
}
|
|
|
|
WCoord wxNew = m_wx;
|
|
WCoord wyNew = m_wy;
|
|
|
|
m_cMoveStepsRemaining--;
|
|
if (m_cMoveStepsRemaining == 0) {
|
|
|
|
// Hop to the exact center of the destination tile
|
|
|
|
wxNew = WcTrunc(m_wx) + kwcTileHalf;
|
|
wyNew = WcTrunc(m_wy) + kwcTileHalf;
|
|
|
|
// No longer in transition; we're at the destination. Clear this bit
|
|
// so we know which tile is reserved
|
|
|
|
m_wfMunt &= ~kfMuntDestinationReserved;
|
|
} else {
|
|
WCoord wcMoveDist = m_wcMoveDistPerUpdate;
|
|
|
|
// Are we moving diagonally?
|
|
|
|
if (m_dir & 1)
|
|
wcMoveDist = gawcDiagonalDist[wcMoveDist];
|
|
|
|
int dx = g_mpDirToDx[m_dir];
|
|
if (dx == -1) {
|
|
wxNew -= wcMoveDist;
|
|
} else if (dx == 1) {
|
|
wxNew += wcMoveDist;
|
|
}
|
|
|
|
int dy = g_mpDirToDy[m_dir];
|
|
if (dy == -1) {
|
|
wyNew -= wcMoveDist;
|
|
} else if (dy == 1) {
|
|
wyNew += wcMoveDist;
|
|
}
|
|
}
|
|
|
|
// Move here
|
|
|
|
SetPosition(wxNew, wyNew);
|
|
|
|
// Animate the little fella running there.
|
|
|
|
AdvanceAnimation(&m_ani);
|
|
|
|
// Handle flashing, etc
|
|
|
|
DefUpdate();
|
|
}
|
|
|
|
bool MobileUnitGob::IsMoveWaitCycle(MobileUnitGob *pmunt)
|
|
{
|
|
// This routine detects move wait loops (units waiting on units waiting on
|
|
// units, etc). If there is a loop, this move wait isn't valid.
|
|
|
|
// Quick outs. If nobody is waiting on this unit, then by implication this won't
|
|
// be a cycle. Likewise if the unit we want to wait on isn't waiting, then by
|
|
// implication this won't be a cycle.
|
|
|
|
if (!(m_wfMunt & kfMuntMoveWaitingNearby))
|
|
return false;
|
|
|
|
// Special case: if pmunt is attacking then not a wait cycle since it
|
|
// isn't mobile.
|
|
|
|
if (pmunt->m_st == kstAttack)
|
|
return false;
|
|
|
|
// Start at pmunt and see if we loop back to the current unit
|
|
|
|
int cCycle = 0;
|
|
MobileUnitGob *pmuntT = pmunt;
|
|
while (pmuntT != NULL) {
|
|
// If we've looped back to ourselves, this is a cycle
|
|
|
|
if (pmuntT == this)
|
|
return true;
|
|
|
|
// If this unit isn't move waiting, not a cycle
|
|
|
|
if (!(pmuntT->m_wfMunt & kfMuntMoveWait))
|
|
return false;
|
|
|
|
// Cycle to who pmuntT is waiting on
|
|
// Because of evaulation order it is possible that a pmuntT is waiting on a
|
|
// tile that is not reserved because the reserving munt started a transition
|
|
// during this update cycle before pmuntT gob executed. This is a break in the cycle;
|
|
// whatever gob takes this tile eventually will recheck for cycles, so it is
|
|
// ok to assume this condition is not a cycle.
|
|
|
|
pmuntT = GetReservingUnit(pmuntT->m_txDst, pmuntT->m_tyDst);
|
|
if (pmuntT == NULL)
|
|
return false;
|
|
|
|
// Break out of we've looped too much.
|
|
|
|
if (cCycle++ > 15)
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool MobileUnitGob::MoveWaitForUnit(MobileUnitGob *pmunt, TCoord tx, TCoord ty)
|
|
{
|
|
// Invalid if this creates a cycle
|
|
|
|
if (IsMoveWaitCycle(pmunt))
|
|
return false;
|
|
|
|
// This unit will "move wait".
|
|
|
|
m_wfMunt |= kfMuntMoveWait;
|
|
m_txDst = tx;
|
|
m_tyDst = ty;
|
|
pmunt->m_wfMunt |= kfMuntMoveWaitingNearby;
|
|
return true;
|
|
}
|
|
|
|
bool MobileUnitGob::CheckDestinationReached()
|
|
{
|
|
// If we're where we want to be, we're done
|
|
|
|
TCoord tx = TcFromWc(m_wx);
|
|
TCoord ty = TcFromWc(m_wy);
|
|
if (tx == TcFromWc(m_wptTarget.wx) && ty == TcFromWc(m_wptTarget.wy))
|
|
return true;
|
|
|
|
// Not at dest if we have no path
|
|
|
|
if (m_ppathUnit == NULL)
|
|
return false;
|
|
|
|
// See if we're "close enough"
|
|
|
|
TCoord txTargetCenter = TcFromWc(m_wptTargetCenter.wx);
|
|
TCoord tyTargetCenter = TcFromWc(m_wptTargetCenter.wy);
|
|
int dtx = abs(tx - txTargetCenter);
|
|
if (dtx >= ARRAYSIZE(gmpDistFromDxy))
|
|
return false;
|
|
int dty = abs(ty - tyTargetCenter);
|
|
if (dty >= ARRAYSIZE(gmpDistFromDxy))
|
|
return false;
|
|
if (gmpDistFromDxy[dtx][dty] > m_tcTargetRadius)
|
|
return false;
|
|
|
|
// We're close enough; need to check if there is terrain between this unit
|
|
// and the center. If so we're not at the dest.
|
|
|
|
TerrainMap *ptrmap = gsim.GetLevel()->GetTerrainMap();
|
|
if (ptrmap->IsLineOccupied(tx, ty, txTargetCenter, tyTargetCenter, 0))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool MobileUnitGob::PrepareNextTransition(TPoint *ptptNext, int *pnMoveResult)
|
|
{
|
|
Assert(!InTransition());
|
|
|
|
// First clear move wait since we'll be making a fresh decision
|
|
|
|
m_wfMunt &= ~kfMuntMoveWait;
|
|
|
|
// Get the viable transition locations
|
|
|
|
TPoint atpt[8];
|
|
int ctpt = GetNextLocations(atpt);
|
|
if (ctpt == 0) {
|
|
// Check to see if we're at a replicator input point. If so move wait until the replicator
|
|
// takes over
|
|
|
|
if (CheckReplicatorPoint()) {
|
|
m_wfMunt |= kfMuntAtReplicatorInput | kfMuntMoveWait;
|
|
*pnMoveResult = knMoveTargetReached;
|
|
return false;
|
|
}
|
|
|
|
// No move candidates. Are we close enough to the target?
|
|
|
|
if (CheckDestinationReached()) {
|
|
*pnMoveResult = knMoveTargetReached;
|
|
return false;
|
|
}
|
|
|
|
// No such luck. We're stuck.
|
|
|
|
m_wfMunt |= kfMuntStuck;
|
|
*pnMoveResult = knMoveStuck;
|
|
return false;
|
|
}
|
|
|
|
// See if our first choice is clear; if so use it (common case)
|
|
|
|
byte bf = GetTileFlags(atpt[0].tx, atpt[0].ty);
|
|
if (!(bf & (kbfStructure | kbfMobileUnit))) {
|
|
*ptptNext = atpt[0];
|
|
m_unvl.MinSkip();
|
|
*pnMoveResult = knMoveMoving;
|
|
return true;
|
|
}
|
|
|
|
// Find reserving munts once
|
|
|
|
MobileUnitGob *apmunt[8];
|
|
byte abf[8];
|
|
for (int itpt = 0; itpt < ctpt; itpt++) {
|
|
// Structure or mobile unit?
|
|
|
|
abf[itpt] = GetTileFlags(atpt[itpt].tx, atpt[itpt].ty);
|
|
if (abf[itpt] & kbfMobileUnit) {
|
|
apmunt[itpt] = GetReservingUnit(atpt[itpt].tx, atpt[itpt].ty);
|
|
#ifdef DEBUG
|
|
if (apmunt[itpt] == NULL) {
|
|
Assert();
|
|
}
|
|
if (apmunt[itpt] == this) {
|
|
Assert();
|
|
}
|
|
#endif
|
|
//Assert(apmunt[itpt] != NULL && apmunt[itpt] != this);
|
|
} else {
|
|
apmunt[itpt] = NULL;
|
|
}
|
|
}
|
|
|
|
// Check open tiles or move wait opportunities
|
|
|
|
if (ContinueMoveWaiting(apmunt, abf, atpt, ctpt, ptptNext)) {
|
|
if (IsMoveWaiting()) {
|
|
*pnMoveResult = knMoveWaiting;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// All the move locations are currently occupied. Let's see if we're close
|
|
// enough to the destination already.
|
|
|
|
if (CheckDestinationReached()) {
|
|
*pnMoveResult = knMoveTargetReached;
|
|
return false;
|
|
}
|
|
|
|
// Need to deal with blockers.
|
|
|
|
if (HandleCollision(apmunt, abf, atpt, ctpt)) {
|
|
// If move waiting, go into wait mode
|
|
|
|
if (IsMoveWaiting()) {
|
|
*pnMoveResult = knMoveWaiting;
|
|
return false;
|
|
}
|
|
|
|
// Otherwise wait an update so that the new path options get
|
|
// re-evaluated
|
|
|
|
m_unvl.MinSkip();
|
|
*pnMoveResult = knMoveMoving;
|
|
return false;
|
|
}
|
|
|
|
// Nothing!
|
|
|
|
m_wfMunt |= kfMuntStuck;
|
|
*pnMoveResult = knMoveStuck;
|
|
return false;
|
|
}
|
|
|
|
bool MobileUnitGob::HandleCollision(MobileUnitGob **apmunt, byte *abf, TPoint *atpt, int ctpt)
|
|
{
|
|
// All locations are blocked. We need to know who we're colliding with
|
|
// Ask the standing blocker to move out of the way. This usually works
|
|
// if the unit isn't an enemy and isn't attacking
|
|
|
|
int itpt;
|
|
for (itpt = 0; itpt < ctpt; itpt++) {
|
|
MobileUnitGob *pmunt = apmunt[itpt];
|
|
if (pmunt == NULL)
|
|
continue;
|
|
if (!pmunt->IsMobile()) {
|
|
if (PlanMoveAsidePath(pmunt, atpt[itpt].tx, atpt[itpt].ty))
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Try to locally path around. Not important to check all
|
|
// units since the first path will work around the other choices.
|
|
|
|
if (FindLocalAvoidPath(atpt[0].tx, atpt[0].ty))
|
|
return true;
|
|
|
|
// Find a friendly attacking unit to wait on
|
|
|
|
for (itpt = 0; itpt < ctpt; itpt++) {
|
|
if (apmunt[itpt] == NULL)
|
|
continue;
|
|
if (apmunt[itpt]->m_st == kstAttack && apmunt[itpt]->IsAlly(GetSide()))
|
|
if (MoveWaitForUnit(apmunt[itpt], atpt[itpt].tx, atpt[itpt].ty))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool MobileUnitGob::ContinueMoveWaiting(MobileUnitGob **apmunt, byte *abf, TPoint *atpt, int ctpt, TPoint *ptptNext)
|
|
{
|
|
int itpt;
|
|
#if 0
|
|
for (itpt = 0; itpt < ctpt; itpt++) {
|
|
// If the dest is not reserved, take it
|
|
|
|
byte bf = abf[itpt];
|
|
if (!(bf & (kbfStructure | kbfMobileUnit))) {
|
|
*ptptNext = atpt[itpt];
|
|
return true;
|
|
}
|
|
|
|
// If not a mobile unit, can't move wait on it
|
|
|
|
if (!(bf & kbfMobileUnit))
|
|
continue;
|
|
|
|
// Deprioritize moving waiting on a munt that is still in transition
|
|
|
|
MobileUnitGob *pmunt = apmunt[itpt];
|
|
if (!pmunt->InTransition() && pmunt->IsMobile()) {
|
|
if (MoveWaitForUnit(pmunt, atpt[itpt].tx, atpt[itpt].ty))
|
|
return true;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Didn't find anything; try move waiting on the first possible unit
|
|
|
|
for (itpt = 0; itpt < ctpt; itpt++) {
|
|
// If the dest is not reserved, take it
|
|
|
|
byte bf = abf[itpt];
|
|
if (!(bf & (kbfStructure | kbfMobileUnit))) {
|
|
*ptptNext = atpt[itpt];
|
|
return true;
|
|
}
|
|
|
|
// Deprioritize moving waiting on a munt that is still in transition
|
|
|
|
MobileUnitGob *pmunt = apmunt[itpt];
|
|
if (pmunt != NULL && pmunt->IsMobile()) {
|
|
if (MoveWaitForUnit(pmunt, atpt[itpt].tx, atpt[itpt].ty))
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool MobileUnitGob::FindLocalAvoidPath(TCoord tx, TCoord ty)
|
|
{
|
|
// Get the trackpoint for the current location
|
|
|
|
TCoord txFrom = TcFromWc(m_wx);
|
|
TCoord tyFrom = TcFromWc(m_wy);
|
|
TrackPoint trkpStart;
|
|
if (!trkpStart.Init(m_ppathUnit, txFrom, tyFrom, m_itptPath, 3))
|
|
return false;
|
|
Direction dirBlocked = DirectionFromLocations(txFrom, tyFrom, tx, ty);
|
|
|
|
// Find a closer track point.
|
|
|
|
#define kcStepsAvoid 15
|
|
|
|
// Try clockwise first
|
|
|
|
TPoint atptCW[kcStepsAvoid];
|
|
TrackPoint trkpCW;
|
|
int ctptCW = FindCloserTrackPoint(&trkpStart, dirBlocked, true, kcStepsAvoid, atptCW, &trkpCW);
|
|
|
|
// Counter-clockwise
|
|
|
|
TPoint atptCCW[kcStepsAvoid];
|
|
TrackPoint trkpCCW;
|
|
int ctptCCW = FindCloserTrackPoint(&trkpStart, dirBlocked, false, kcStepsAvoid, atptCCW, &trkpCCW);
|
|
|
|
// Which one to use?
|
|
|
|
int ctptUse;
|
|
TPoint *atptUse;
|
|
bool fSuccess = true;
|
|
if (ctptCCW > 0) {
|
|
// CCW is valid. Is CW valid? If so compare length
|
|
|
|
if (ctptCW > 0) {
|
|
// Both valid. Which is shorter?
|
|
|
|
if (ctptCW < ctptCCW) {
|
|
ctptUse = ctptCW;
|
|
atptUse = atptCW;
|
|
} else {
|
|
ctptUse = ctptCCW;
|
|
atptUse = atptCCW;
|
|
}
|
|
|
|
} else {
|
|
// CW is invalid, CCW is valid
|
|
|
|
ctptUse = ctptCCW;
|
|
atptUse = atptCCW;
|
|
}
|
|
} else {
|
|
// CCW is invalid. If CW is valid; use it
|
|
|
|
if (ctptCW > 0) {
|
|
ctptUse = ctptCW;
|
|
atptUse = atptCW;
|
|
} else {
|
|
// Both invalid
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Create a path from this and use it
|
|
// If our edge finder approach worked, use it
|
|
|
|
TerrainMap *ptrmap = gsim.GetLevel()->GetTerrainMap();
|
|
Path *ppath = NULL;
|
|
if (fSuccess) {
|
|
Assert(ctptUse != 0);
|
|
ppath = CreatePath(ptrmap, txFrom, tyFrom, atptUse, ctptUse);
|
|
if (ppath != NULL) {
|
|
// Success. This "avoid" path gets us further along the unit path.
|
|
|
|
delete m_ppathAvoid;
|
|
m_ppathAvoid = ppath;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Map the last direction travelled to next directions to scan
|
|
|
|
Direction gmpDirLastDirScanStartCW[] = { kdirNE, kdirSE, kdirSE, kdirSW, kdirSW, kdirNW, kdirNW, kdirNE };
|
|
Direction gmpDirLastDirScanEndCW[] = { (kdirNE - 5) & 7, (kdirSE - 5) & 7, (kdirSE - 5) & 7, (kdirSW - 5) & 7, (kdirSW - 5) & 7, (kdirNW - 5) & 7, (kdirNW - 5) & 7, (kdirNE - 5) & 7};
|
|
Direction gmpDirLastDirScanStartCCW[] = { kdirNW, kdirNW, kdirNE, kdirNE, kdirSE, kdirSE, kdirSW, kdirSW };
|
|
Direction gmpDirLastDirScanEndCCW[] = { (kdirNW + 5) & 7, (kdirNW + 5) & 7, (kdirNE + 5) & 7, (kdirNE + 5) & 7, (kdirSE + 5) & 7, (kdirSE + 5) & 7, (kdirSW + 5) & 7, (kdirSW + 5) & 7 };
|
|
|
|
int MobileUnitGob::FindCloserTrackPoint(TrackPoint *ptrkpStart, Direction dirBlocked, bool fClockwise, int ctpt, TPoint *atpt, TrackPoint *ptrkpNew)
|
|
{
|
|
Direction *pmpDirLastDirScanStart;
|
|
Direction *pmpDirLastDirScanEnd;
|
|
Direction dirStart, dirEnd;
|
|
int ddir;
|
|
if (fClockwise) {
|
|
pmpDirLastDirScanStart = gmpDirLastDirScanStartCW;
|
|
pmpDirLastDirScanEnd = gmpDirLastDirScanEndCW;
|
|
dirStart = (dirBlocked - 1) & 7;
|
|
dirEnd = (dirStart - 7) & 7;
|
|
ddir = -1;
|
|
} else {
|
|
pmpDirLastDirScanStart = gmpDirLastDirScanStartCCW;
|
|
pmpDirLastDirScanEnd = gmpDirLastDirScanEndCCW;
|
|
dirStart = (dirBlocked + 1) & 7;
|
|
dirEnd = (dirStart + 7) & 7;
|
|
ddir = 1;
|
|
}
|
|
|
|
TCoord ctx, cty;
|
|
ggobm.GetMapSize(&ctx, &cty);
|
|
TerrainMap *ptrmap = gsim.GetLevel()->GetTerrainMap();
|
|
int itptClosestSearch = m_itptPath > 0 ? m_itptPath - 1 : 0;
|
|
ptrkpNew->InitFrom(ptrkpStart);
|
|
|
|
int ctptLastGood = 0;
|
|
TCoord txFrom = TcFromWc(m_wx);
|
|
TCoord tyFrom = TcFromWc(m_wy);
|
|
TCoord txLast = txFrom;
|
|
TCoord tyLast = tyFrom;
|
|
for (int itpt = 0; itpt < ctpt; itpt++) {
|
|
Direction dirBest = kdirInvalid;
|
|
for (Direction dirNew = dirStart; dirNew != dirEnd; dirNew = (dirNew + ddir) & 7) {
|
|
// Valid TCoord?
|
|
|
|
TCoord txNew = txLast + g_mpDirToDx[dirNew];
|
|
TCoord tyNew = tyLast + g_mpDirToDy[dirNew];
|
|
if (txNew < 0 || txNew >= ctx)
|
|
continue;
|
|
if (tyNew < 0 || tyNew >= cty)
|
|
continue;
|
|
|
|
// Is tile blocked? Can't rely on moving or standing units getting
|
|
// out of the way because they may not be able to
|
|
|
|
if (ptrmap->IsBlocked(txNew, tyNew, kbfStructure | kbfMobileUnit))
|
|
continue;
|
|
|
|
// Open tile, we're there
|
|
|
|
dirBest = dirNew;
|
|
break;
|
|
}
|
|
|
|
// Anything?
|
|
|
|
if (dirBest == kdirInvalid)
|
|
return ctptLastGood;
|
|
|
|
// If this point is further along the path than our last track point
|
|
// then remember this position
|
|
|
|
atpt[itpt].tx = txLast + g_mpDirToDx[dirBest];
|
|
atpt[itpt].ty = tyLast + g_mpDirToDy[dirBest];
|
|
TrackPoint trkpT;
|
|
if (trkpT.Init(m_ppathUnit, atpt[itpt].tx, atpt[itpt].ty, itptClosestSearch, ctpt)) {
|
|
// Remember this as the last best progress point
|
|
|
|
if (ptrkpNew->IsProgress(&trkpT)) {
|
|
#if 0
|
|
// Remembers a "last best" progress point without knowing access to the unit
|
|
// path is possible from there
|
|
|
|
ptrkpNew->InitFrom(&trkpT);
|
|
ctptLastGood = itpt + 1;
|
|
#else
|
|
// If the unit path is accessible from here, return this path
|
|
|
|
TPoint tptClosest;
|
|
trkpT.GetClosestPoint(&tptClosest);
|
|
if (!ptrmap->IsLineOccupied(atpt[itpt].tx, atpt[itpt].ty, tptClosest.tx, tptClosest.ty, kbfStructure)) {
|
|
ptrkpNew->InitFrom(&trkpT);
|
|
return itpt + 1;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// Go to next point in path
|
|
|
|
txLast = atpt[itpt].tx;
|
|
tyLast = atpt[itpt].ty;
|
|
dirStart = pmpDirLastDirScanStart[dirBest];
|
|
dirEnd = pmpDirLastDirScanEnd[dirBest];
|
|
}
|
|
|
|
return ctptLastGood;
|
|
}
|
|
|
|
MoveDirections s_movd[50];
|
|
word s_nSeqMoveAside;
|
|
bool MobileUnitGob::PlanMoveAsidePath(MobileUnitGob *pmuntBlocking, TCoord txTo, TCoord tyTo)
|
|
{
|
|
// Enemies won't move aside
|
|
|
|
if (!IsAlly(pmuntBlocking->GetSide()))
|
|
return false;
|
|
|
|
// If attacking, won't move aside
|
|
|
|
if (pmuntBlocking->m_st == kstAttack)
|
|
return false;
|
|
|
|
// Calc the direction this unit is trying to move. In general we prioritize
|
|
// moving orthogonal to this direction, then backwards, then forwards.
|
|
|
|
TCoord txFrom = TcFromWc(m_wx);
|
|
TCoord tyFrom = TcFromWc(m_wy);
|
|
Direction dirMover = DirectionFromLocations(txFrom, tyFrom, txTo, tyTo);
|
|
|
|
// Before starting inc the sequence number; this'll make sure we don't
|
|
// visit gobs that have already been enumerated.
|
|
|
|
s_nSeqMoveAside++;
|
|
|
|
// Mark this unit so that it isn't asked to move aside (no backwards
|
|
// support at the moment)
|
|
|
|
m_nSeqMoveAside = s_nSeqMoveAside;
|
|
|
|
// Add the first one to the list
|
|
|
|
MoveDirections *pmovd = s_movd;
|
|
pmuntBlocking->m_nSeqMoveAside = s_nSeqMoveAside;
|
|
if (!pmuntBlocking->GetMoveAsideDirections(dirMover, pmovd))
|
|
return false;
|
|
|
|
// Enumerate mobile units in a chain until an open space to move to is
|
|
// found.
|
|
|
|
TCoord ctx, cty;
|
|
ggobm.GetMapSize(&ctx, &cty);
|
|
bool fFound = false;
|
|
while (true) {
|
|
// Enumerate the possible moving directions for this gob.
|
|
|
|
TCoord txUnit = TcFromWc(pmovd->pmunt->m_wx);
|
|
TCoord tyUnit = TcFromWc(pmovd->pmunt->m_wy);
|
|
MoveDirections *pmovdT = pmovd;
|
|
for (; pmovd->idir < pmovd->cdir; pmovd->idir++) {
|
|
// Enumerate the directions in order provided, get that munt
|
|
|
|
Direction dir = pmovd->adir[pmovd->idir];
|
|
TCoord txNext = txUnit + g_mpDirToDx[dir];
|
|
TCoord tyNext = tyUnit + g_mpDirToDy[dir];
|
|
if (txNext < 0 || txNext >= ctx || tyNext < 0 || tyNext >= cty)
|
|
continue;
|
|
MobileUnitGob *pmuntNext = GetMobileUnitAt(txNext, tyNext);
|
|
|
|
// If this space is open we're done and we have a path we
|
|
// can create
|
|
|
|
if (pmuntNext == NULL) {
|
|
fFound = true;
|
|
break;
|
|
}
|
|
|
|
// Is there room to add another? If not then continue.
|
|
|
|
if ((pmovd + 1) - s_movd >= ARRAYSIZE(s_movd))
|
|
continue;
|
|
|
|
// Is it an ally? Enemies won't move aside
|
|
|
|
if (!IsAlly(pmuntNext->GetSide()))
|
|
continue;
|
|
|
|
// Attacking? If attacking, won't move aside
|
|
|
|
if (pmuntNext->m_st == kstAttack)
|
|
continue;
|
|
|
|
// Try to add it to the list. It'll get added if it can move
|
|
// in this direction.
|
|
|
|
if (!pmuntNext->GetMoveAsideDirections(dirMover, pmovd + 1))
|
|
continue;
|
|
|
|
// Added ok; advance pmovd to this new entry, break out of
|
|
// loop to re-enter
|
|
|
|
pmovd++;
|
|
break;
|
|
}
|
|
|
|
// If another was added to the list, enumerate it
|
|
|
|
if (pmovd != pmovdT)
|
|
continue;
|
|
|
|
// If we found a path, we're done; break out
|
|
|
|
if (fFound)
|
|
break;
|
|
|
|
// Couldn't find a munt from this munt that would move aside. Go back
|
|
// to the previous munt and continue enumerating if there are any left
|
|
|
|
if (pmovd == s_movd)
|
|
break;
|
|
pmovd--;
|
|
pmovd->idir++;
|
|
}
|
|
if (!fFound)
|
|
return false;
|
|
|
|
|
|
// Walk backwards through the move directions asking units to move aside
|
|
|
|
for (MoveDirections *pmovdT = pmovd; pmovdT >= s_movd; pmovdT--) {
|
|
if (!pmovdT->pmunt->AcceptMoveAsideRequest(pmovdT->adir[pmovdT->idir]))
|
|
return false;
|
|
}
|
|
|
|
// This unit needs to move wait on the column that is moving aside
|
|
|
|
MoveWaitForUnit(pmuntBlocking, txTo, tyTo);
|
|
return true;
|
|
}
|
|
|
|
MobileUnitGob *MobileUnitGob::GetMobileUnitAt(TCoord tx, TCoord ty)
|
|
{
|
|
// Enum gids here
|
|
|
|
for (Gid gid = ggobm.GetFirstGid(tx, ty); gid != kgidNull; gid = ggobm.GetNextGid(gid)) {
|
|
// Any mobile units in this tile?
|
|
|
|
MobileUnitGob *pmunt = (MobileUnitGob *)ggobm.GetGob(gid);
|
|
if (pmunt == NULL)
|
|
continue;
|
|
if (!(pmunt->m_ff & kfGobMobileUnit))
|
|
continue;
|
|
if (pmunt->IsMobile())
|
|
continue;
|
|
if (pmunt->IsMoveWaiting())
|
|
continue;
|
|
return pmunt;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
// Move asides look directional independently in this order: sides first, backwards, then forwards
|
|
|
|
Direction s_mpDirToDirsSorted[8][8] = {
|
|
{ kdirW, kdirE, kdirSW, kdirSE, kdirNW, kdirNE, kdirS, kdirN, },
|
|
{ kdirNW, kdirSE, kdirW, kdirS, kdirN, kdirE, kdirSW, kdirNE, },
|
|
{ kdirN, kdirS, kdirNW, kdirSW, kdirNE, kdirSE, kdirW, kdirE, },
|
|
{ kdirNE, kdirSW, kdirN, kdirW, kdirE, kdirS, kdirNW, kdirSE, },
|
|
{ kdirE, kdirW, kdirNE, kdirNW, kdirSE, kdirSW, kdirN, kdirS, },
|
|
{ kdirSE, kdirNW, kdirE, kdirN, kdirS, kdirW, kdirNE, kdirSW, },
|
|
{ kdirS, kdirN, kdirSE, kdirNE, kdirSW, kdirNW, kdirE, kdirW, },
|
|
{ kdirSW, kdirNE, kdirS, kdirE, kdirW, kdirN, kdirSE, kdirNW, },
|
|
};
|
|
|
|
// NOTE: Should this be overridable (virtual or a message)?
|
|
|
|
bool MobileUnitGob::GetMoveAsideDirections(Direction dirMover, MoveDirections *pmovd)
|
|
{
|
|
// Special hack for miners
|
|
|
|
if (m_st == kstMinerRotateForEntry)
|
|
return false;
|
|
|
|
// Initialize
|
|
|
|
pmovd->pmunt = this;
|
|
pmovd->cdir = 0;
|
|
pmovd->idir = 0;
|
|
|
|
// If this unit is being asked for move aside directions it has already been
|
|
// marked, assert if not true.
|
|
|
|
Assert(m_nSeqMoveAside == s_nSeqMoveAside);
|
|
|
|
// Fill in reasonable move directons into pmovd.
|
|
|
|
TCoord ctx, cty;
|
|
ggobm.GetMapSize(&ctx, &cty);
|
|
Direction *adir = s_mpDirToDirsSorted[dirMover];
|
|
TCoord txFrom = TcFromWc(m_wx);
|
|
TCoord tyFrom = TcFromWc(m_wy);
|
|
|
|
// Enumerate directions
|
|
|
|
TerrainMap *ptrmap = gsim.GetLevel()->GetTerrainMap();
|
|
for (int idir = 0; idir < 8; idir++) {
|
|
// Get mobile unit at this location
|
|
|
|
Direction dir = adir[idir];
|
|
TCoord tx = txFrom + g_mpDirToDx[dir];
|
|
TCoord ty = tyFrom + g_mpDirToDy[dir];
|
|
if (tx < 0 || tx >= ctx || ty < 0 || ty >= cty)
|
|
continue;
|
|
|
|
// If in attack state this new move aside position is only
|
|
// plausible if it is still in range
|
|
|
|
// Is this location blocked by terrain?
|
|
|
|
if (ptrmap->IsBlocked(tx, ty, kbfStructure))
|
|
continue;
|
|
|
|
// See what munt is there if any
|
|
|
|
MobileUnitGob *pmunt = GetMobileUnitAt(tx, ty);
|
|
|
|
// Prioritize empty
|
|
|
|
if (pmunt == NULL) {
|
|
// Even if there is no munt there, the tile may be reserved by
|
|
// another munt that is in transition to this tile. If the tile is
|
|
// reserved, it is not a suitable destination
|
|
|
|
if (IsTileReserved(tx, ty))
|
|
continue;
|
|
pmovd->adir[0] = dir;
|
|
pmovd->cdir = 1;
|
|
return true;
|
|
}
|
|
|
|
// Already been asked? If not it is fresh meat
|
|
|
|
if (pmunt->m_nSeqMoveAside == s_nSeqMoveAside)
|
|
continue;
|
|
pmunt->m_nSeqMoveAside = s_nSeqMoveAside;
|
|
|
|
// Mobile? Only non-mobile munts are candidates
|
|
|
|
if (pmunt->IsMobile())
|
|
continue;
|
|
|
|
// Either a viable munt is there or nothing is there, in any case add
|
|
// it to the list
|
|
|
|
pmovd->adir[pmovd->cdir++] = dir;
|
|
}
|
|
|
|
return pmovd->cdir != 0;
|
|
}
|
|
|
|
bool MobileUnitGob::AcceptMoveAsideRequest(Direction dir)
|
|
{
|
|
// This unit has agreed to move aside in this direction and is now being
|
|
// asked to do so.
|
|
|
|
// Two cases: either the unit is standing or move waiting. If it is
|
|
// standing then it can just move out of the way.
|
|
|
|
TCoord txFrom = TcFromWc(m_wx);
|
|
TCoord tyFrom = TcFromWc(m_wy);
|
|
TCoord txTo = txFrom + g_mpDirToDx[dir];
|
|
TCoord tyTo = tyFrom + g_mpDirToDy[dir];
|
|
|
|
// Unit not move waiting.
|
|
// Move in this direction. This has already been evaluated to be safe and
|
|
// sane.
|
|
|
|
Assert(m_ppathUnit == NULL);
|
|
Assert(!IsMoveWaiting());
|
|
|
|
WCoord wxDst = WcFromTc(txTo) + kwcTileHalf;
|
|
WCoord wyDst = WcFromTc(tyTo) + kwcTileHalf;
|
|
|
|
// Issue move command. These occur in order so earlier units can move wait
|
|
// on later units. We don't want overridden behavior (such as the miner
|
|
// mining galaxite) to interfere with move aside, so call
|
|
// MobileUnitGob::SetTarget directly.
|
|
|
|
MobileUnitGob::SetTarget(kgidNull, WcFromTc(txTo) + kwcTileHalf, WcFromTc(tyTo) + kwcTileHalf);
|
|
return true;
|
|
}
|
|
|
|
int MobileUnitGob::GetNextLocations(TPoint *atpt)
|
|
{
|
|
// Use avoid path if it exists
|
|
|
|
int cStepsFurtherStop = 3;
|
|
if (m_ppathAvoid != NULL) {
|
|
// Are we at the destination yet?
|
|
|
|
TCoord txFrom = TcFromWc(m_wx);
|
|
TCoord tyFrom = TcFromWc(m_wy);
|
|
TPoint tptT;
|
|
bool fEnd = false;
|
|
if (m_ppathAvoid->GetPoint(m_ppathAvoid->GetCount() - 1, &tptT, 0)) {
|
|
if (tptT.tx == txFrom && tptT.ty == tyFrom)
|
|
fEnd = true;
|
|
} else {
|
|
fEnd = true;
|
|
}
|
|
|
|
// If we still have more to avoid, do it
|
|
|
|
if (!fEnd) {
|
|
int itptDummy = 0;
|
|
int ctpt = GetNextLocations2(m_ppathAvoid, &itptDummy, m_ppathAvoid->GetCount(), atpt);
|
|
if (ctpt != 0)
|
|
return ctpt;
|
|
}
|
|
|
|
// Avoid path is done; delete it
|
|
|
|
cStepsFurtherStop = _min(cStepsFurtherStop, m_ppathAvoid->GetCount());
|
|
delete m_ppathAvoid;
|
|
m_ppathAvoid = NULL;
|
|
}
|
|
|
|
// Use unit path
|
|
|
|
int ctpt = GetNextLocations2(m_ppathUnit, &m_itptPath, cStepsFurtherStop, atpt);
|
|
if (ctpt == 0)
|
|
return 0;
|
|
if (m_itptPath > 1)
|
|
m_ppathUnit->SetCacheIndex(m_itptPath - 2);
|
|
return ctpt;
|
|
}
|
|
|
|
int MobileUnitGob::GetNextLocations2(Path *ppath, int *pitptStart, int cStepsFurtherStop, TPoint *atpt)
|
|
{
|
|
// The current progress is measured relative to the unit path. Then
|
|
// surrounding locations are profiled for making better progress. Positions
|
|
// with better progress are returned as move destinations.
|
|
|
|
// Initialize a trackpoint
|
|
|
|
TCoord txFrom = TcFromWc(m_wx);
|
|
TCoord tyFrom = TcFromWc(m_wy);
|
|
Assert(GetReservingUnit(txFrom, tyFrom) == this);
|
|
Assert(IsTileReserved(txFrom, tyFrom));
|
|
TrackPoint trkpStart;
|
|
if (!trkpStart.Init(ppath, txFrom, tyFrom, *pitptStart, cStepsFurtherStop))
|
|
return false;
|
|
TPoint tptClosest;
|
|
trkpStart.GetClosestPoint(&tptClosest);
|
|
int itptClosest = trkpStart.GetClosestPointIndex();
|
|
*pitptStart = itptClosest;
|
|
|
|
// If we're at the end we're done
|
|
|
|
if (itptClosest == ppath->GetCount() - 1) {
|
|
if (tptClosest.tx == txFrom && tptClosest.ty == tyFrom)
|
|
return 0;
|
|
}
|
|
|
|
// See if we're close to the next step already
|
|
|
|
TPoint tptNext;
|
|
if (ppath->GetPoint(itptClosest + 1, &tptNext, kbfStructure | kbfMobileUnit)) {
|
|
// Have a next step and it is free of obstacles. If we're on the path
|
|
// then we're close to it, return it
|
|
|
|
if (abs(tptNext.tx - txFrom) <= 1 && abs(tptNext.ty - tyFrom) <= 1) {
|
|
Assert(tptNext.tx != txFrom || tptNext.ty != tyFrom);
|
|
atpt[0] = tptNext;
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Maybe not actually on "next" but closer to closest. If so take it
|
|
|
|
byte bf = GetTileFlags(tptClosest.tx, tptClosest.ty);
|
|
if (!(bf & (kbfStructure | kbfMobileUnit))) {
|
|
if (abs(tptClosest.tx - txFrom) <= 1 && abs(tptClosest.ty - tyFrom) <= 1) {
|
|
Assert(tptClosest.tx != txFrom || tptClosest.ty != tyFrom);
|
|
atpt[0] = tptClosest;
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Get after point of trackpoint
|
|
|
|
TPoint tptAfter;
|
|
trkpStart.GetAfterPoint(&tptAfter);
|
|
|
|
// Enumerate the surrounding tiles and measure each for progress along the
|
|
// unit path.
|
|
|
|
int itptClosestMeasure = (itptClosest == 0 ? 0 : itptClosest - 1);
|
|
TrackPoint atrkp[8];
|
|
int ctpt = 0;
|
|
TCoord ctx, cty;
|
|
ggobm.GetMapSize(&ctx, &cty);
|
|
TerrainMap *ptrmap = gsim.GetLevel()->GetTerrainMap();
|
|
for (Direction dir = 0; dir < 8; dir++) {
|
|
TCoord tx = txFrom + g_mpDirToDx[dir];
|
|
TCoord ty = tyFrom + g_mpDirToDy[dir];
|
|
if (tx < 0 || tx >= ctx || ty < 0 || ty >= cty)
|
|
continue;
|
|
if (ptrmap->IsBlocked(tx, ty, 0))
|
|
continue;
|
|
atrkp[ctpt].Init(ppath, tx, ty, itptClosestMeasure, 1);
|
|
if (trkpStart.IsProgress(&atrkp[ctpt])) {
|
|
// If blocked by terrain, not valid progress
|
|
|
|
if (ptrmap->IsLineOccupied(tx, ty, tptAfter.tx, tptAfter.ty, 0))
|
|
continue;
|
|
|
|
// Looks like a good candidate
|
|
|
|
ctpt++;
|
|
}
|
|
}
|
|
|
|
// If there are no unblocked positions, take the next one even if it is
|
|
// blocked and callers will try and move around it.
|
|
|
|
if (ctpt == 0) {
|
|
for (Direction dir = 0; dir < 8; dir++) {
|
|
TCoord tx = txFrom + g_mpDirToDx[dir];
|
|
TCoord ty = tyFrom + g_mpDirToDy[dir];
|
|
if (tx < 0 || tx >= ctx || ty < 0 || ty >= cty)
|
|
continue;
|
|
atrkp[0].Init(ppath, tx, ty, itptClosestMeasure, 1);
|
|
if (trkpStart.IsProgress(&atrkp[0])) {
|
|
atpt[0].tx = tx;
|
|
atpt[0].ty = ty;
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort the candidates.
|
|
|
|
int an[8];
|
|
int i;
|
|
for (i = 0; i < ctpt; i++)
|
|
an[i] = i;
|
|
for (i = ctpt - 1; i >= 0; i--) {
|
|
for (int j = 1; j <= i; j++) {
|
|
if (atrkp[an[j - 1]].IsBetterSort(&atrkp[an[j]])) {
|
|
int n = an[j - 1];
|
|
an[j - 1] = an[j];
|
|
an[j] = n;
|
|
}
|
|
}
|
|
}
|
|
for (i = 0; i < ctpt; i++)
|
|
atrkp[an[i]].GetInitialPoint(&atpt[i]);
|
|
|
|
// Done
|
|
|
|
return ctpt;
|
|
}
|
|
|
|
bool MobileUnitGob::IsMobile()
|
|
{
|
|
if (m_ppathUnit == NULL && !(m_wfMunt & (kfMuntPathPending | kfMuntAtReplicatorInput)))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
bool MobileUnitGob::CheckReplicatorPoint()
|
|
{
|
|
TCoord tx = TcFromWc(m_wx);
|
|
TCoord ty = TcFromWc(m_wy);
|
|
int cReplicators = ReplicatorGob::GetReplicatorCount();
|
|
for (int n = 0; n < cReplicators; n++) {
|
|
TPoint tptReplicator;
|
|
ReplicatorGob::GetReplicatorInputPoint(n, &tptReplicator);
|
|
if (tptReplicator.tx == tx && tptReplicator.ty == ty)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void MobileUnitGob::SetPosition(WCoord wx, WCoord wy)
|
|
{
|
|
// Check if nothing to do
|
|
|
|
if (m_wx == wx && m_wy == wy)
|
|
return;
|
|
|
|
// Notify waiters
|
|
|
|
if (!InTransition()) {
|
|
if (m_wfMunt & kfMuntMoveWaitingNearby)
|
|
NotifyMoveWaitingNearby(m_wx, m_wy);
|
|
}
|
|
|
|
// Mark for redraw if this gob has changed pixel locations
|
|
|
|
if (PcFromWc(wx) != PcFromWc(m_wx) || PcFromWc(wy) != PcFromWc(m_wy))
|
|
MarkRedraw();
|
|
|
|
// Be sure not to sleep on this important new development
|
|
|
|
m_unvl.MinSkip();
|
|
|
|
// Keep GobMgr in the loop so it can maintain proper depth sorting
|
|
|
|
bool fTileChange = ggobm.MoveGob(this, m_wx, m_wy, wx, wy);
|
|
WCoord wxOld = m_wx;
|
|
WCoord wyOld = m_wy;
|
|
m_wx = wx;
|
|
m_wy = wy;
|
|
|
|
// Special tasks if we've moved between tiles
|
|
|
|
if (fTileChange) {
|
|
// Reveal fog
|
|
|
|
if (m_pplr == gpplrLocal) {
|
|
WCoord wxView, wyView;
|
|
gsim.GetViewPos(&wxView, &wyView);
|
|
FogMap *pfogm = gsim.GetLevel()->GetFogMap();
|
|
TPoint tpt;
|
|
GetTilePosition(&tpt);
|
|
RevealPattern *prvlp = (RevealPattern *)(m_puntc->wf & kfUntcLargeDefog ? grvlpLarge : grvlp);
|
|
pfogm->Reveal(tpt.tx, tpt.ty, prvlp, gpupdSim, wxView, wyView);
|
|
}
|
|
|
|
// Update minimap
|
|
|
|
if (gpmm != NULL) {
|
|
TRect trc;
|
|
trc.left = wxOld < wx ? TcFromWc(wxOld) : TcFromWc(wx);
|
|
trc.top = wyOld < wy ? TcFromWc(wyOld) : TcFromWc(wy);
|
|
trc.right = (wxOld > wx ? TcFromWc(wxOld) : TcFromWc(wx)) + 1;
|
|
trc.bottom = (wyOld > wy ? TcFromWc(wyOld) : TcFromWc(wy)) + 1;
|
|
gpmm->RedrawTRect(&trc);
|
|
}
|
|
|
|
// Notify enemy of this gob nearby
|
|
|
|
NotifyEnemyNearby();
|
|
|
|
// Move between areas
|
|
|
|
AreaMask amOld = ggobm.CalcAreaMask(TcFromWc(wxOld), TcFromWc(wyOld));
|
|
AreaMask amNew = ggobm.CalcAreaMask(TcFromWc(m_wx), TcFromWc(m_wy));
|
|
ggobm.MoveGobBetweenAreas(m_gid, amOld, amNew);
|
|
}
|
|
}
|
|
|
|
UnitGob *MobileUnitGob::FindValidTargetInArea(int nArea)
|
|
{
|
|
Enum enm;
|
|
while (true) {
|
|
Gob *puntTarget = ggobm.EnumGobsInArea(&enm, m_msgAction.GuardAreaCommand.nArea, ~m_pplr->GetAllies(), kumMobileUnits);
|
|
if (puntTarget == NULL)
|
|
return NULL;
|
|
if (IsValidTarget(puntTarget)) {
|
|
Assert(puntTarget->GetFlags() & kfGobUnit);
|
|
return (UnitGob *)puntTarget;
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef MP_DEBUG_SHAREDMEM
|
|
void MobileUnitGob::MPValidate()
|
|
{
|
|
MPValidateGobMember(MobileUnitGob, m_dir);
|
|
MPValidateGobMember(MobileUnitGob, m_dirNext);
|
|
MPValidateGobMember(MobileUnitGob, m_gidTarget);
|
|
MPValidateGobMember(MobileUnitGob, m_tLastFire);
|
|
MPValidateGobMember(MobileUnitGob, m_msgPending);
|
|
MPValidateGobMember(MobileUnitGob, m_msgAction);
|
|
MPValidateGobMember(MobileUnitGob, m_cCountdown);
|
|
MPValidateGobMember(MobileUnitGob, m_cMoveStepsRemaining);
|
|
MPValidateGobMember(MobileUnitGob, m_tptChaseInitial);
|
|
MPValidateGobMember(MobileUnitGob, m_mua);
|
|
MPValidateGobMember(MobileUnitGob, m_muaPending);
|
|
MPValidateGobMember(MobileUnitGob, m_wfMunt);
|
|
MPValidateGobMember(MobileUnitGob, m_stPending);
|
|
MPValidateGobMember(MobileUnitGob, m_cupdLastHitOrNearbyAllyHit);
|
|
MPValidateGobMember(MobileUnitGob, m_wxDst);
|
|
MPValidateGobMember(MobileUnitGob, m_wyDst);
|
|
MPValidateGobMember(MobileUnitGob, m_txDst);
|
|
MPValidateGobMember(MobileUnitGob, m_tyDst);
|
|
MPValidateGobMember(MobileUnitGob, m_wptTarget);
|
|
MPValidateGobMember(MobileUnitGob, m_nSeqMoveAside);
|
|
MPValidateGobMember(MobileUnitGob, m_itptPath);
|
|
MPValidateGobMember(MobileUnitGob, m_wptTargetCenter);
|
|
MPValidateGobMember(MobileUnitGob, m_tcTargetRadius);
|
|
MPValidateGobMember(MobileUnitGob, m_wcMoveDistPerUpdate);
|
|
UnitGob::MPValidate();
|
|
}
|
|
#endif
|
|
|
|
} // namespace wi
|