hostile-takeover/game/Unit.cpp
2016-08-31 23:55:30 -04:00

1372 lines
37 KiB
C++

#include "ht.h"
namespace wi {
TBitmap *gaptbmScorches[4];
UnitConsts *gapuntc[kutMax];
int gnUnitCostMin = 32767;
int gnUnitCostMax = -1;
int gnUnitCostMPMin = 32767;
int gnUnitCostMPMax = -1;
fix gfxMobileUnitArmorStrengthMin = itofx(511);
fix gfxMobileUnitArmorStrengthMax = itofx(-1);
fix gfxStructureArmorStrengthMin = itofx(511);
fix gfxStructureArmorStrengthMax = itofx(-1);
fix gfxInfantryDamageMin = itofx(511);
fix gfxInfantryDamageMax = itofx(-1);
fix gfxVehicleDamageMin = itofx(511);
fix gfxVehicleDamageMax = itofx(-1);
fix gfxStructureDamageMin = itofx(511);
fix gfxStructureDamageMax = itofx(-1);
TCoord gtcFiringRangeMin = 127;
TCoord gtcFiringRangeMax = -1;
WCoord gwcMoveDistPerUpdateMin = 32767;
WCoord gwcMoveDistPerUpdateMax = -1;
int gnPowerSupplyMin = 32767;
int gnPowerSupplyMax = -1;
int gnPowerDemandMin = 32767;
int gnPowerDemandMax = -1;
UnitConsts *GetUnitConsts(GobType gt)
{
for (int i = 0; i < kutMax; i++) {
if (gapuntc[i]->gt == gt)
return gapuntc[i];
}
return NULL;
}
void GetPrerequisiteString(char *psz, UnitConsts *puntc)
{
*psz = 0;
int i;
for (i = 0; i < kutMax; i++) {
if (puntc->umPrerequisites & (1UL << i)) {
if (*psz != 0)
strcat(psz, ", ");
strcat(psz, gapuntc[i]->szLongName);
}
}
for (i = 0; i < kupgtMax; i++) {
if (puntc->upgmPrerequisites & (1UL << i)) {
if (*psz != 0)
strcat(psz, ", ");
strcat(psz, gaupg[i].szName);
strcat(psz, " upgrade");
}
}
}
//===========================================================================
// UnitGob implementation
bool UnitGob::InitClass(UnitConsts *puntc, IniReader *pini)
{
gapuntc[puntc->ut] = puntc;
puntc->um = 1 << puntc->ut;
char szTemplate[10];
itoa(puntc->gt, szTemplate, 10);
puntc->nInfantryDamage = 0;
puntc->nVehicleDamage = 0;
puntc->nStructureDamage = 0;
puntc->nInfantryDamageMP = 0;
puntc->nVehicleDamageMP = 0;
puntc->nStructureDamageMP = 0;
puntc->ctFiringRate = 0;
puntc->tcFiringRange = 0;
// Required properties
if (pini->GetPropertyValue(szTemplate, "Cost", "%d", &puntc->nCost) != 1)
return false;
gnUnitCostMin = _min(gnUnitCostMin, puntc->nCost);
gnUnitCostMax = _max(gnUnitCostMax, puntc->nCost);
int csecTimeToBuild;
if (pini->GetPropertyValue(szTemplate, "TimeToBuild", "%d", &csecTimeToBuild) != 1)
return false;
puntc->cupdTimeToBuild = UpdFromSec(csecTimeToBuild);
if (pini->GetPropertyValue(szTemplate, "TimeToBuildMP", "%d", &csecTimeToBuild) != 1)
puntc->cupdTimeToBuildMP = puntc->cupdTimeToBuild;
else
puntc->cupdTimeToBuildMP = UpdFromSec(csecTimeToBuild);
int pcT;
if (pini->GetPropertyValue(szTemplate, "UIBoundsLeft", "%d", &pcT) != 1)
return false;
puntc->wrcUIBounds.left = pcT * kwcTile16th; // HACK: so we can keep existing GobTemplate UIBounds
if (pini->GetPropertyValue(szTemplate, "UIBoundsTop", "%d", &pcT) != 1)
return false;
puntc->wrcUIBounds.top = pcT * kwcTile16th; // HACK: so we can keep existing GobTemplate UIBounds
if (pini->GetPropertyValue(szTemplate, "UIBoundsRight", "%d", &pcT) != 1)
return false;
puntc->wrcUIBounds.right = pcT * kwcTile16th; // HACK: so we can keep existing GobTemplate UIBounds
if (pini->GetPropertyValue(szTemplate, "UIBoundsBottom", "%d", &pcT) != 1)
return false;
puntc->wrcUIBounds.bottom = pcT * kwcTile16th; // HACK: so we can keep existing GobTemplate UIBounds
if (pini->GetPropertyValue(szTemplate, "SortOffset", "%d", &puntc->wdySortOffset) != 1)
return false;
puntc->wdySortOffset *= 16; // HACK: so we can keep existing GobTemplate sort offsets
if (pini->GetPropertyValue(szTemplate, "Animation", "%s", puntc->szAniName) != 1)
return false;
if (pini->GetPropertyValue(szTemplate, "Name", "%s", puntc->szName) != 1) {
Assert("Struct/Unit must have 'Name' property");
return false;
}
Assert(strlen(puntc->szName) < kcbStructUnitName);
// Optional properties
int cmsFiringRate;
if (pini->GetPropertyValue(szTemplate, "FiringRate", "%d", &cmsFiringRate) == 1)
puntc->ctFiringRate = cmsFiringRate / 10;
fix fxDamagePerSec;
if (pini->GetPropertyValue(szTemplate, "InfantryDamage", "%d", &puntc->nInfantryDamage)) {
if (puntc->nInfantryDamage != 0) {
fxDamagePerSec = (fix)mulfx(itofx32(puntc->nInfantryDamage), divfx(itofx(1000), itofx(cmsFiringRate)));
gfxInfantryDamageMin = _min(gfxInfantryDamageMin, fxDamagePerSec);
gfxInfantryDamageMax = _max(gfxInfantryDamageMax, fxDamagePerSec);
}
}
if (pini->GetPropertyValue(szTemplate, "InfantryDamageMP", "%d", &puntc->nInfantryDamageMP) != 1)
puntc->nInfantryDamageMP = puntc->nInfantryDamage;
if (pini->GetPropertyValue(szTemplate, "VehicleDamage", "%d", &puntc->nVehicleDamage)) {
if (puntc->nVehicleDamage != 0) {
fxDamagePerSec = (fix)mulfx(itofx32(puntc->nVehicleDamage), divfx(itofx(1000), itofx(cmsFiringRate)));
gfxVehicleDamageMin = _min(gfxVehicleDamageMin, fxDamagePerSec);
gfxVehicleDamageMax = _max(gfxVehicleDamageMax, fxDamagePerSec);
}
}
if (pini->GetPropertyValue(szTemplate, "VehicleDamageMP", "%d", &puntc->nVehicleDamageMP) != 1)
puntc->nVehicleDamageMP = puntc->nVehicleDamage;
if (pini->GetPropertyValue(szTemplate, "StructureDamage", "%d", &puntc->nStructureDamage)) {
if (puntc->nStructureDamage != 0) {
fxDamagePerSec = (fix)mulfx(itofx32(puntc->nStructureDamage), divfx(itofx(1000), itofx(cmsFiringRate)));
gfxStructureDamageMin = _min(gfxStructureDamageMin, fxDamagePerSec);
gfxStructureDamageMax = _max(gfxStructureDamageMax, fxDamagePerSec);
}
}
if (pini->GetPropertyValue(szTemplate, "StructureDamageMP", "%d", &puntc->nStructureDamageMP) != 1)
puntc->nStructureDamageMP = puntc->nStructureDamage;
pini->GetPropertyValue(szTemplate, "FiringRange", "%d", &puntc->tcFiringRange);
gtcFiringRangeMin = _min((int)gtcFiringRangeMin, puntc->tcFiringRange);
gtcFiringRangeMax = _max((int)gtcFiringRangeMax, puntc->tcFiringRange);
char szT[300];
#ifdef DEBUG
szT[sizeof(szT) - 1] = 0; // guard
#endif
if (pini->GetPropertyValue(szTemplate, "Description", szT, sizeof(szT))) {
// We dynamically allocate szDescription to save .bss space
Assert(szT[sizeof(szT) - 1] == 0);
puntc->szDescription = new char[strlen(szT) + 1];
if (puntc->szDescription != NULL)
strcpy(puntc->szDescription, szT);
}
if (!pini->GetPropertyValue(szTemplate, "LongName", puntc->szLongName, sizeof(puntc->szLongName)))
strcpy(puntc->szLongName, puntc->szName);
if (pini->GetPropertyValue(szTemplate, "CostMP", "%d", &puntc->nCostMP) != 1)
puntc->nCostMP = puntc->nCost;
gnUnitCostMPMin = _min(gnUnitCostMPMin, puntc->nCostMP);
gnUnitCostMPMax = _max(gnUnitCostMPMax, puntc->nCostMP);
puntc->panid = LoadAnimationData(puntc->szAniName);
Assert(puntc->panid != NULL);
if (puntc->panid == NULL)
return false;
InitFingerUIBounds(puntc);
// If they aren't already loaded, load the scorch marks
if (gaptbmScorches[0] == NULL) {
gaptbmScorches[0] = LoadTBitmap("scorch_8x8.tbm");
gaptbmScorches[1] = LoadTBitmap("scorch_16x16.tbm");
gaptbmScorches[2] = LoadTBitmap("scorch_32x16.tbm");
gaptbmScorches[3] = LoadTBitmap("scorch_48x48.tbm");
}
return true;
}
void UnitGob::InitFingerUIBounds(UnitConsts *puntc)
{
// Finger UI Bounds inflates the UI bounds for finger hit testing
WRect wrc = puntc->wrcUIBounds;
// Make sure height is kwcTile * 4, then adjust width to keep aspect
// ratio the same
int cy = wrc.Height();
int cx = wrc.Width();
if (cy < kwcTile * 4) {
cy = kwcTile * 4;
cx = cx * cy * 64 / wrc.Height() / 64;
}
wrc.Inflate((cx - wrc.Width()) / 2, (cy - wrc.Height()) / 2);
puntc->wrcUIBoundsFinger = wrc;
}
void UnitGob::ExitClass(UnitConsts *puntc)
{
// If they aren't already deleted, delete the scorch marks
if (gaptbmScorches[0] != NULL) {
for (int i = 0; i < sizeof(gaptbmScorches) / sizeof(TBitmap *); i++) {
delete gaptbmScorches[i];
gaptbmScorches[i] = NULL;
}
}
delete puntc->panid;
puntc->panid = NULL;
delete puntc->pfrmMenu;
puntc->pfrmMenu = NULL;
delete[] puntc->szDescription;
puntc->szDescription = NULL;
}
UnitGob::UnitGob(UnitConsts *puntc)
{
m_ff |= kfGobUnit | kfGobStateMachine | kfGobLayerDepthSorted;
m_puntc = puntc;
m_wfUnit = 0;
for (int n = 0; n < ARRAYSIZE(m_agidEnemyNearby); n++)
m_agidEnemyNearby[n] = kgidNull;
// Just to be clear this Unit hasn't notified anyone about being hit for awhile
m_cupdLastHitNotify = -1000000;
m_panispr = NULL;
}
UnitGob::~UnitGob()
{
delete m_panispr;
m_panispr = NULL;
}
bool UnitGob::Init(IniReader *pini, FindProp *pfind, const char *pszName)
{
int wf = 0;
int side, tx, ty;
int nHealth;
int cArgs = pini->GetPropertyValue(pfind, "%*d ,%d ,%d ,%d ,%d ,%d", &side, &tx, &ty, &wf, &nHealth);
if (cArgs < 5) {
Assert("UnitGob requires at least 5 valid initialization parameters");
return false;
}
fix fxHealth = (fix)((m_puntc->GetArmorStrength() * (long)nHealth) / 100L);
return Init(WcFromTc(tx), WcFromTc(ty), gplrm.GetPlayer(side), fxHealth, wf, pszName);
}
bool UnitGob::Init(WCoord wx, WCoord wy, Player *pplr, fix fxHealth, dword ff, const char *pszName)
{
// UNDONE: stash Name away somewhere
m_pplr = pplr;
m_wx = WcTrunc(wx);
m_wy = WcTrunc(wy);
m_ff |= ff;
m_ani.Init(m_puntc->panid);
StartAnimation(&m_ani, 0, 0, kfAniLoop | kfAniIgnoreFirstAdvance);
// Initial health is derived from nArmorStrength
if (fxHealth == 0)
m_fxHealth = m_puntc->GetArmorStrength();
else
m_fxHealth = fxHealth;
// Reveal this part of the map
if (m_pplr == gpplrLocal) {
WCoord wxView, wyView;
gsim.GetViewPos(&wxView, &wyView);
FogMap *pfogm = gsim.GetLevel()->GetFogMap();
WPoint wpt;
GetCenter(&wpt);
RevealPattern *prvlp = (RevealPattern *)(m_puntc->wf & kfUntcLargeDefog ? grvlpLarge : grvlp);
pfogm->Reveal(TcFromWc(wpt.wx), TcFromWc(wpt.wy), prvlp, gpupdSim, wxView, wyView);
}
// Add the fresh Gob to the GobMgr.
ggobm.AddGob(this);
// Redraw this part of minimap
TRect trc;
GetTileRect(&trc);
gpmm->RedrawTRect(&trc);
// Initialize the state machine by sending the first Initialize msg
gsmm.SendMsg(kmidReservedEnter, m_gid, m_gid);
return true;
}
void UnitGob::Delete()
{
// Get trect for minimap redrawing
TRect trc;
GetTileRect(&trc);
// Remove from gid map
ggobm.RemoveGob(this);
delete this;
// Redraw this part of minimap after the gob is gone from gobmgr.
gpmm->RedrawTRect(&trc);
}
#define knVerUnitGobState 4
bool UnitGob::LoadState(Stream *pstm)
{
byte nVer = pstm->ReadByte();
if (nVer != knVerUnitGobState)
return false;
m_fxHealth = (fix)pstm->ReadWord();
m_wfUnit = pstm->ReadWord();
pstm->Read(m_agidEnemyNearby, sizeof(m_agidEnemyNearby));
m_ani.Init(m_puntc->panid);
m_ani.LoadState(pstm);
m_cupdLastHitNotify = (int)pstm->ReadDword();
m_cDamageCountdown = pstm->ReadWord();
return Gob::LoadState(pstm);
}
bool UnitGob::SaveState(Stream *pstm)
{
pstm->WriteByte(knVerUnitGobState);
pstm->WriteWord(m_fxHealth);
pstm->WriteWord(m_wfUnit);
pstm->Write(m_agidEnemyNearby, sizeof(m_agidEnemyNearby));
m_ani.SaveState(pstm);
pstm->WriteDword(m_cupdLastHitNotify);
pstm->WriteWord(m_cDamageCountdown);
return Gob::SaveState(pstm);
}
GobType UnitGob::GetType()
{
return m_puntc->gt;
}
bool UnitGob::IsAlly(Side side)
{
if (m_pplr->GetAllies() & GetSideMask(side))
return true;
return false;
}
void UnitGob::Activate()
{
m_ff |= kfGobActive;
m_pplr->IncUnitCount(m_puntc->ut);
// Best place to do this is here, after the unit has been
// fully built, it is counted as having been built.
m_pplr->IncUnitBuiltCount(m_puntc->ut);
}
void UnitGob::Deactivate()
{
Assert(m_ff & kfGobActive);
// make sure this unit's menu is not left up
UnitConsts *puntc = GetUnitConsts(GetType());
Assert(puntc->pfrmMenu != NULL);
if (puntc->pfrmMenu->GetOwner() == (UnitGob *)this)
puntc->pfrmMenu->EndForm(kidcCancel);
m_ff &= ~(kfGobActive | kfGobDrawFlashed);
Select(false);
m_pplr->DecUnitCount(m_puntc->ut);
ClearDamageIndicator();
// Delete the highlight sprite
delete m_panispr;
m_panispr = NULL;
}
bool UnitGob::IsIdle()
{
return true;
}
bool UnitGob::IsValidTarget(Gob *pgobTarget)
{
return (pgobTarget->GetFlags() & (kfGobUnit | kfGobActive)) == (kfGobUnit | kfGobActive)
&& !IsAlly(pgobTarget->GetSide());
}
void UnitGob::SetTarget(Gid gid, WCoord wxTarget, WCoord wyTarget, WCoord wxCenter, WCoord wyCenter, TCoord tcRange, WCoord wcMoveDistPerUpdate)
{
}
int UnitGob::GetDamageTo(UnitGob *puntTarget)
{
UnitMask umTarget = puntTarget->GetConsts()->um;
int nDamage = 0;
if (umTarget & kumInfantry) {
nDamage = m_puntc->GetInfantryDamage();
} else if (umTarget & kumVehicles) {
nDamage = m_puntc->GetVehicleDamage();
} else if (umTarget & kumStructures) {
nDamage = m_puntc->GetStructureDamage();
}
if (m_pplr->GetHandicap() & kfHcapIncreasedFirePower)
nDamage = ((nDamage * (100 + knIncreasedFirePowerPercent)) + 50) / 100; // +50 for rounding
return nDamage;
}
void UnitGob::Draw(DibBitmap *pbm, int xViewOrigin, int yViewOrigin, int nLayer)
{
// When a unit is displayed as a corpse we want other units draw on top of
// it UNDONE: this affects all unit types and structures that don't
// override ::Draw. It may be better to do this is a more localized way
// (e.g., specific units spawn a corpse/SurfaceDecalGob upon death)
if ((nLayer == knLayerDepthSorted && m_st != kstDying) ||
(nLayer == knLayerSurfaceDecal && m_st == kstDying)) {
#ifdef DRAW_OCCUPIED_TILE_INDICATOR
{
WRect wrcT;
GetTilePaddedWRect(&wrcT);
Rect rcT;
rcT.FromWorldRect(&wrcT);
rcT.Offset(-xViewOrigin, -yViewOrigin);
DrawBorder(pbm, &rcT, 1, GetColor(kiclrWhite));
}
#endif
Side side = m_pplr->GetSide();
if (m_ff & kfGobDrawFlashed)
side = (Side)-1;
else if (m_ff & kfGobBeingBuilt)
side = ksideNeutral;
int xT = PcFromUwc(m_wx) - xViewOrigin;
int yT = PcFromUwc(m_wy) - yViewOrigin;
m_ani.Draw(pbm, xT, yT, side);
} else if (nLayer == knLayerSelection) {
if (m_ff & kfGobSelected) {
Rect rcT;
rcT.FromWorldRect(&m_puntc->wrcUIBounds);
rcT.Offset(-xViewOrigin + PcFromWc(m_wx), -yViewOrigin + PcFromWc(m_wy));
DrawSelectionIndicator(pbm, &rcT, m_fxHealth, m_puntc->GetArmorStrength());
// If this Gob was hit recently show its health
} else if (m_wfUnit & kfUnitDrawHealthIndicator) {
Rect rcT;
rcT.FromWorldRect(&m_puntc->wrcUIBounds);
rcT.Offset(-xViewOrigin + PcFromWc(m_wx),
-yViewOrigin + PcFromWc(m_wy));
DrawHealthIndicator(pbm, &rcT, m_fxHealth,
m_puntc->GetArmorStrength());
}
// If this gob is hilighted, draw the hilight indicator
if (m_wfUnit & kfUnitHilighted) {
if (m_panispr != NULL) {
int xT = PcFromUwc(m_wx) - xViewOrigin;
int yT = PcFromUwc(m_wy) - yViewOrigin;
m_panispr->SetPosition(xT, yT);
m_panispr->SetScale((float)(kwcTile * 4) / (float)m_puntc->wrcUIBounds.Height());
m_panispr->CaptureFrame(this);
}
}
}
}
dword UnitGob::GetSortKey()
{
int wy = m_wy;
int wySortOffset = wy + m_puntc->wdySortOffset;
int key = MakeSortKey(wySortOffset, m_gid);
return (dword)key;
}
// See Gob::GetClippingBounds for a description of what the clipping
// bounds is and how it is used.
// NOTE: the values returned are in pixel coordinates
void UnitGob::GetClippingBounds(Rect *prc)
{
// Union of animation and selection
m_ani.GetBounds(prc);
if (m_ff & kfGobSelected) {
if (gwfPerfOptions & kfPerfSelectionBrackets) {
Rect rcSel;
rcSel.FromWorldRect(&m_puntc->wrcUIBounds);
rcSel.top -= kcyHealthBar;
if (m_puntc->wf & kfUntcHasFullnessIndicator)
rcSel.bottom += kcyFullnessBar;
prc->Union(&rcSel);
} else {
Rect rcSel;
rcSel.FromWorldRect(&m_puntc->wrcUIBounds);
prc->top = rcSel.top - kcyHealthBar;
prc->left = _min(prc->left, rcSel.left);
prc->right = _max(prc->right, rcSel.right);
if (m_puntc->wf & kfUntcHasFullnessIndicator)
prc->bottom = _max(prc->bottom, rcSel.bottom + kcyFullnessBar);
}
} else if (m_ff & (kfGobBeingUpgraded | kfGobBeingBuilt) || (m_wfUnit & kfUnitDrawHealthIndicator)) {
// Health is drawn above gob when being upgraded or built
// or if it has been recently damaged
Rect rcSel;
rcSel.FromWorldRect(&m_puntc->wrcUIBounds);
prc->top = rcSel.top - kcyHealthBar;
prc->left = _min(prc->left, rcSel.left);
prc->right = _max(prc->right, rcSel.right);
}
prc->Offset(PcFromUwc(m_wx), PcFromUwc(m_wy));
}
// See Gob::GetUIBounds for a description of what the UI bounds
// is and how it is used.
// NOTE: the values returned are in world coordinates
void UnitGob::GetUIBounds(WRect *pwrc)
{
// Fudging to make it look/feel good.
// These fudge-factors are read from GobTemplates.ini
pwrc->left = m_wx + m_puntc->wrcUIBounds.left;
pwrc->top = m_wy + m_puntc->wrcUIBounds.top;
pwrc->right = m_wx + m_puntc->wrcUIBounds.right;
pwrc->bottom = m_wy + m_puntc->wrcUIBounds.bottom;
}
// UNDONE: OPT: not necessarily the fastest way to return the center
void UnitGob::GetCenter(WPoint *pwpt)
{
pwpt->wx = m_wx + m_puntc->wrcUIBounds.left + (m_puntc->wrcUIBounds.right - m_puntc->wrcUIBounds.left) / 2;
pwpt->wy = m_wy + m_puntc->wrcUIBounds.top + (m_puntc->wrcUIBounds.bottom - m_puntc->wrcUIBounds.top) / 2;
}
void UnitGob::GetAttackPoint(WPoint *pwpt)
{
GetCenter(pwpt);
}
dword UnitGob::GetAnimationHash()
{
int nFrame = m_ani.GetFrame();
int nStrip = m_ani.GetStrip();
return ((nFrame << 16) | nStrip) ^ (dword)(unsigned long)this ^ (GetId() << 16);
}
void UnitGob::GetAnimationBounds(Rect *prc, bool fBase)
{
m_ani.GetBounds(prc);
}
void UnitGob::DrawAnimation(DibBitmap *pbm, int x, int y)
{
m_ani.Draw(pbm, x, y, m_pplr->GetSide());
}
void UnitGob::Select(bool fSelect)
{
// Set bits
if (fSelect) {
if (m_ff & kfGobSelected) {
return;
}
m_ff |= kfGobSelected | kfGobLayerSelection;
} else {
if (!(m_ff & kfGobSelected)) {
return;
}
m_ff &= ~kfGobSelected;
if ((m_wfUnit & kfUnitHilighted) == 0) {
m_ff &= ~kfGobLayerSelection;
}
}
MarkRedraw();
// Redraw this part of minimap
TRect trc;
GetTileRect(&trc);
gpmm->RedrawTRect(&trc);
}
void UnitGob::Hilight(bool fHilight)
{
if (fHilight) {
if (m_wfUnit & kfUnitHilighted) {
return;
}
m_wfUnit |= kfUnitHilighted;
m_ff |= kfGobLayerSelection;
m_panispr = CreateHilightSprite();
// Only need to do this so the sprite captures the current frame
MarkRedraw();
} else {
if (!(m_wfUnit & kfUnitHilighted)) {
return;
}
m_wfUnit &= ~kfUnitHilighted;
if ((m_ff & kfGobSelected) == 0) {
m_ff &= ~kfGobLayerSelection;
}
delete m_panispr;
m_panispr = NULL;
}
}
AnimSprite *UnitGob::CreateHilightSprite()
{
AnimSprite *panispr = gpsprm->CreateAnimSprite();
if (panispr != NULL) {
panispr->SetPalette(gsim.GetLevel()->GetPalette());
}
return panispr;
}
void UnitGob::SetHealth(fix fxHealth)
{
if (m_wfUnit & kfUnitInvulnerable)
return;
if (m_fxHealth != fxHealth) {
m_fxHealth = fxHealth;
if ((m_ff & (kfGobBeingBuilt | kfGobSelected)) || (m_wfUnit & kfUnitDrawHealthIndicator))
MarkRedraw();
}
}
void UnitGob::DefUpdate()
{
// Handle flashing
if (m_ff & kfGobFlashing) {
m_ff &= ~kfGobFlashing;
m_ff |= kfGobDrawFlashed | kfGobRedraw;
m_unvl.MinSkip();
gevm.SetRedrawFlags(kfRedrawBeforeTimer);
} else if (m_ff & kfGobDrawFlashed) {
m_ff &= ~kfGobDrawFlashed;
m_ff |= kfGobRedraw;
m_unvl.MinSkip();
}
// Handle damage countdown
if (m_cDamageCountdown != 0 && !(m_ff & kfGobBeingBuilt)) {
m_cDamageCountdown -= m_unvl.GetUpdateCount();
if (m_cDamageCountdown <= 0) {
m_cDamageCountdown = 0;
// Time to redraw w/ no damage indicator
ClearDamageIndicator();
} else {
m_unvl.MinSkip(m_cDamageCountdown);
}
}
}
void UnitGob::PopupMenu()
{
if (m_puntc->pfrmMenu == NULL)
return;
// UNDONE: show a special being-built menu (Abort only)
if (m_ff & kfGobBeingBuilt)
return;
// Show the menu and get the user's selection
Assert(!(m_puntc->pfrmMenu->GetFlags() & kfFrmDoModal));
m_puntc->pfrmMenu->SetOwner(this);
gpmfrmm->AddForm(m_puntc->pfrmMenu);
int idc;
m_puntc->pfrmMenu->DoModal(&idc);
gpmfrmm->RemoveForm(m_puntc->pfrmMenu);
// Act on the user's selection
OnMenuItemSelected(idc);
}
bool UnitGob::LoadMenu(UnitConsts *puntc, IniReader *pini, char *pszTemplate, int idfDefault)
{
int idf;
if (pini->GetPropertyValue(pszTemplate, "Menu", "%d", &idf) != 1)
idf = idfDefault;
puntc->pfrmMenu = new UnitMenu();
if (puntc->pfrmMenu == NULL)
return false;
if (!puntc->pfrmMenu->Init(gpmfrmm, gpiniForms, idf))
return false;
// UNDONE: override title label
gpmfrmm->RemoveForm(puntc->pfrmMenu);
return true;
}
TCoord UnitGob::CalcRange(TCoord tx, TCoord ty, Gob *pgob)
{
TPoint tpt;
pgob->GetTilePosition(&tpt);
TCoord tcx = abs(tx - tpt.tx);
TCoord tcy = abs(ty - tpt.ty);
if (tcx >= 10 || tcy >= 10)
// Lame rectangular range calculator good enough for stuff that
// is almost certainly out of interesting range anyway and it
// allows us to keep the size of the Euclidean distance table down.
return tcx > tcy ? tcx : tcy;
else
// Nice accurate euclidean distance calculator
return gmpDistFromDxy[tcx][tcy];
}
void UnitGob::RememberEnemyNearby(Gid gidEnemy)
{
// If this gob is already in our list, nothing to do
int n;
for (n = 0; n < ARRAYSIZE(m_agidEnemyNearby); n++) {
if (m_agidEnemyNearby[n] == gidEnemy)
return;
}
// We remember a limited # of gobs, so replace the first one
// we find that is further away than the new enemy. Sometimes
// the gid ids can be recycled into gobs on the same side, so check for that.
UnitGob *puntEnemy = (UnitGob *)ggobm.GetGob(gidEnemy);
if (puntEnemy == NULL)
return;
TCoord txThis = TcFromWc(m_wx);
TCoord tyThis = TcFromWc(m_wy);
Side sideThis = GetSide();
TCoord tcEnemy = CalcRange(txThis, tyThis, puntEnemy);
for (n = 0; n < ARRAYSIZE(m_agidEnemyNearby); n++) {
Gob *puntEnemyNearby = ggobm.GetGob(m_agidEnemyNearby[n]);
if (puntEnemyNearby == NULL || puntEnemyNearby->GetSide() == sideThis) {
m_agidEnemyNearby[n] = gidEnemy;
m_ff &= ~kfGobNoEnemiesNearby;
return;
}
TCoord tc = CalcRange(txThis, tyThis, puntEnemyNearby);
if (tc > tcEnemy) {
m_agidEnemyNearby[n] = gidEnemy;
m_ff &= ~kfGobNoEnemiesNearby;
return;
}
}
// NOTE: might want to set a "rescan" bit if an enemy doesn't fit in the
// list, the issue being we don't want that bit set most of the time.
}
UnitGob *UnitGob::FindEnemyNearby(TCoord tcRange)
{
if (m_ff & kfGobNoEnemiesNearby)
return NULL;
TCoord txThis = TcFromWc(m_wx);
TCoord tyThis = TcFromWc(m_wy);
Side sideThis = GetSide();
int cEmpty = 0;
TCoord tcClosest = tcRange;
UnitGob *puntEnemyClosest = NULL;
for (int n = 0; n < ARRAYSIZE(m_agidEnemyNearby); n++) {
Gid gidEnemyNearby = m_agidEnemyNearby[n];
if (gidEnemyNearby == kgidNull) {
cEmpty++;
continue;
}
UnitGob *puntEnemyNearby = (UnitGob *)ggobm.GetGob(gidEnemyNearby);
if (puntEnemyNearby == NULL || puntEnemyNearby->GetSide() == sideThis) {
m_agidEnemyNearby[n] = kgidNull;
cEmpty++;
continue;
}
TCoord tc = CalcRange(txThis, tyThis, puntEnemyNearby);
if (tc <= tcClosest) {
tcClosest = tc;
puntEnemyClosest = puntEnemyNearby;
}
}
if (puntEnemyClosest != NULL)
return puntEnemyClosest;
if (cEmpty == ARRAYSIZE(m_agidEnemyNearby)) {
#if defined(DEBUG) && defined(WIN)
// Assert(ggobm.FindEnemyWithinRange(this, tcRange) == NULL, "Lost track of a nearby enemy!");
#endif
m_ff |= kfGobNoEnemiesNearby;
}
return NULL;
}
// NOTE: Max sight distance assumption
#define ktcSightRangeMax 5
void UnitGob::NotifyEnemyNearby()
{
// OPT: Could scan leading edges only (when moving), as long as
// the edge is 2 tiles thick. Would need to break out cases where the whole
// rect needs to be scanned.
TCoord ctx, cty;
ggobm.GetMapSize(&ctx, &cty);
TCoord tx = TcFromWc(m_wx);
TCoord ty = TcFromWc(m_wy);
TCoord txLeft = tx - ktcSightRangeMax;
if (txLeft < 0)
txLeft = 0;
TCoord txRight = tx + ktcSightRangeMax + 1;
if (txRight > ctx)
txRight = ctx;
TCoord tyTop = ty - ktcSightRangeMax;
if (tyTop < 0)
tyTop = 0;
TCoord tyBottom = ty + ktcSightRangeMax + 1;
if (tyBottom > cty)
tyBottom = cty;
// Let's check for 'discovery' while we're at it
SideMask sidmAlreadyDiscovered = m_pplr->GetDiscoveredSides();
for (TCoord tyT = tyTop; tyT < tyBottom; tyT++) {
for (TCoord txT = txLeft; txT < txRight; txT++) {
// Any enemies here that want to know about mobile units?
Side side = m_pplr->GetSide();
bool fPreped = false;
for (Gid gid = ggobm.GetFirstGid(txT, tyT); gid != kgidNull; gid = ggobm.GetNextGid(gid)) {
UnitGob *punt = (UnitGob *)ggobm.GetGob(gid);
if (punt == NULL)
continue;
dword ffUnit = punt->GetFlags();
if (!(ffUnit & kfGobUnit))
continue;
// Has side A already discovered side B?
SideMask sidm = GetSideMask(punt->GetSide());
if (!(sidmAlreadyDiscovered & sidm)) {
// No
sidmAlreadyDiscovered |= sidm;
m_pplr->SetDiscoveredSides(sidmAlreadyDiscovered);
TPoint tpt;
punt->GetTilePosition(&tpt);
m_pplr->SetDiscoverPoint(&tpt);
// Then side B has just discovered side A too.
Player *pplrB = punt->m_pplr;
pplrB->SetDiscoveredSides(pplrB->GetDiscoveredSides() | GetSideMask(side));
pplrB->SetDiscoverPoint(&tpt);
}
// Any enemies in this tile that want to know about mobile units?
if (punt->IsAlly(side))
continue;
// Notify this enemy
Gid gidFound = punt->GetId();
Message msg;
if (!fPreped) {
fPreped = true;
memset(&msg, 0, sizeof(msg));
msg.mid = kmidEnemyNearby;
msg.smidSender = m_gid;
}
// Notify this enemy of the current gob if it wants to know
if (gapuntc[punt->GetUnitType()]->wf & kfUntcNotifyEnemyNearby) {
msg.EnemyNearby.gidEnemy = m_gid;
msg.smidReceiver = gidFound;
gsmm.SendMsg(&msg);
}
// Notify the current gob of this enemy, only if this enemy is a mobile unit
// (so that the current gob doesn't get notified of towers).
if (ffUnit & kfGobMobileUnit) {
if (m_puntc->wf & kfUntcNotifyEnemyNearby) {
msg.EnemyNearby.gidEnemy = gidFound;
msg.smidReceiver = m_gid;
gsmm.SendMsg(&msg);
}
}
}
}
}
}
void UnitGob::RecalcEnemyNearby(bool fClearOnly)
{
// NOTE: Could set a lazy rescan bit instead of forcing a rescan.
// Only if perf is an issue.
if (fClearOnly) {
// Wipe the enemies list clean and recalc
m_ff &= ~kfGobNoEnemiesNearby;
for (int n = 0; n < ARRAYSIZE(m_agidEnemyNearby); n++)
m_agidEnemyNearby[n] = kgidNull;
return;
}
// Calc enemy nearby
NotifyEnemyNearby();
}
// TUNE:
const TCoord ktcNotifyAlliesOfHitRange = 4;
void UnitGob::NotifyNearbyAlliesOfHit(Gid gidAssailant)
{
// Only report getting nailed once every other update
int cupd = gsim.GetUpdateCount();
if (cupd - m_cupdLastHitNotify < 2)
return;
m_cupdLastHitNotify = cupd;
TCoord ctx, cty;
ggobm.GetMapSize(&ctx, &cty);
TCoord tx = TcFromWc(m_wx);
TCoord ty = TcFromWc(m_wy);
TCoord txLeft = tx - ktcNotifyAlliesOfHitRange;
if (txLeft < 0)
txLeft = 0;
TCoord txRight = tx + ktcNotifyAlliesOfHitRange + 1;
if (txRight > ctx)
txRight = ctx;
TCoord tyTop = ty - ktcNotifyAlliesOfHitRange;
if (tyTop < 0)
tyTop = 0;
TCoord tyBottom = ty + ktcNotifyAlliesOfHitRange + 1;
if (tyBottom > cty)
tyBottom = cty;
Message msg;
memset(&msg, 0, sizeof(msg));
msg.mid = kmidNearbyAllyHit;
msg.smidSender = m_gid;
msg.Hit.gidAssailant = gidAssailant;
for (TCoord tyT = tyTop; tyT < tyBottom; tyT++) {
for (TCoord txT = txLeft; txT < txRight; txT++) {
// Any MobileGob allies around?
Side side = m_pplr->GetSide();
bool fPreped = false;
for (Gid gid = ggobm.GetFirstGid(txT, tyT); gid != kgidNull; gid = ggobm.GetNextGid(gid)) {
// Don't notify self!
if (gid == m_gid)
continue;
UnitGob *punt = (UnitGob *)ggobm.GetGob(gid);
if (punt == NULL)
continue;
dword ffUnit = punt->GetFlags();
if (!(ffUnit & kfGobMobileUnit))
continue;
if (!punt->IsAlly(side))
continue;
// OPT: don't notify MobileUnits that are already attacking a target?
// Notify this ally
msg.smidReceiver = punt->GetId();
gsmm.SendMsg(&msg);
}
}
}
}
void UnitGob::ShowDamageIndicator()
{
// Only show enemy units' health
if (m_pplr == gpplrLocal)
return;
// Only if turned on
if (!(gwfPerfOptions & kfPerfEnemyDamageIndicator))
return;
// Enable health indicator drawing
m_wfUnit |= kfUnitDrawHealthIndicator;
m_ff |= kfGobLayerSelection;
MarkRedraw();
m_cDamageCountdown = UpdFromMsec(kcmsDamageIndicatorTimeout);
m_unvl.MinSkip(m_cDamageCountdown);
}
void UnitGob::ClearDamageIndicator()
{
if ((m_ff & kfGobSelected) == 0)
m_ff &= ~kfGobLayerSelection;
m_wfUnit &= ~kfUnitDrawHealthIndicator;
MarkRedraw();
}
//===========================================================================
// UnitMenu implementation
UnitGob *UnitMenu::GetOwner()
{
return m_punt;
}
bool UnitMenu::DoModal(int *pnResult, Sfx sfxShow, Sfx sfxHide)
{
gtimm.AddTimer(this, 25);
bool f = Form::DoModal(pnResult, sfxShow, sfxHide);
gtimm.RemoveTimer(this);
return f;
}
void UnitMenu::OnTimer(long tCurrent)
{
// Initialize on a timer so that options show / hide while the form
// is visible
m_punt->InitMenu(this);
}
const int kcxButtonSpace = 0;
const int kcyTitleMargin = 3;
const int kcxTitleMargin = 5;
void UnitMenu::SetOwner(UnitGob *punt, bool fPerUnitInit)
{
m_punt = punt;
// Let the owning Gob initialize the menu state (hide/show/disable buttons)
if (fPerUnitInit) {
m_punt->InitMenu(this);
}
char *pszTitle = m_punt->GetConsts()->szName;
// Calc the width/height of the 'button bar' (all visible buttons)
int cButtons = 0;
int cxButton = 0, cyButton = 0;
int i = 0;
for (i = 0; i < m_cctl; i++) {
Control *pctl = m_apctl[i];
word id = pctl->GetId();
if (id < kidcRelocButtonMin || id >= kidcRelocButtonMax)
continue;
// While we're at it force each button to draw using the 'side1' colors
// This is because some of them use side1's dark blues.
word wf = pctl->GetFlags();
pctl->SetFlags(wf | kfCtlUseSide1Colors);
if (!(wf & kfCtlVisible))
continue;
if (cButtons == 0) {
Rect rcT;
pctl->GetRect(&rcT);
cxButton = rcT.Width();
cyButton = rcT.Height();
}
cButtons++;
}
int cxButtons = ((cxButton + kcxButtonSpace) * cButtons) - kcxButtonSpace;
// Calc the width/height of the title bar
int cxTitleText = gapfnt[kifntTitle]->GetTextExtent(pszTitle);
int cxTitleWhole = cxTitleText + kcxTitleMargin * 2;
int cyFont = gapfnt[kifntTitle]->GetHeight();
int cyTitle = cyFont + kcyTitleMargin * 2;
// Get Gob's UIBounds in screen coordinates which becomes the interior
// rect around which the title and button bar are formatted.
// This somewhat elaborate means of producing the screen-coord rcUIBounds
// is designed to perfectly match the coordinate conversion pipeline
// used to draw the selection rect. This way the tile/button bar are
// locked right with the selection rect it regardless of resolution.
WCoord wxViewOrigin, wyViewOrigin;
gsim.GetViewPos(&wxViewOrigin, &wyViewOrigin);
int xViewOrigin = PcFromUwc(wxViewOrigin) & 0xfffe;
int yViewOrigin = PcFromUwc(wyViewOrigin) & 0xfffe;
Rect rcUIBounds;
rcUIBounds.FromWorldRect(&m_punt->GetConsts()->wrcUIBounds);
WPoint wpt;
m_punt->GetPosition(&wpt);
int xPos = PcFromWc(wpt.wx) - xViewOrigin;
int yPos = PcFromWc(wpt.wy) - yViewOrigin;
rcUIBounds.Offset(xPos, yPos);
// Set form size/position
// Put title on top of hilight sprite, if there is one.
Rect rcForm;
AnimSprite *panispr = punt->GetAnimSprite();
if (panispr != NULL) {
Rect rcSpriteBounds;
panispr->GetBounds(&rcSpriteBounds);
rcSpriteBounds.Offset(xPos, yPos);
rcForm.top = rcSpriteBounds.top - cyTitle - kcyHealthBar;
} else {
rcForm.top = rcUIBounds.top - cyTitle - kcyHealthBar;
}
rcForm.bottom = rcUIBounds.bottom + cyButton;
int cxForm = _max(rcUIBounds.Width(), cxTitleWhole);
if (cxForm < cxButtons)
cxForm = cxButtons;
rcForm.left = rcUIBounds.left + (rcUIBounds.Width() / 2) - (cxForm / 2);
rcForm.right = rcForm.left + cxForm;
int cyForm = rcForm.Height();
// Keep the form on screen
Size sizPlayfield;
ggame.GetPlayfieldSize(&sizPlayfield);
int dx = 0, dy = 0;
if (rcForm.left < 0)
dx = -rcForm.left;
else if (rcForm.right > sizPlayfield.cx)
dx = sizPlayfield.cx - rcForm.right;
if (rcForm.top < 0)
dy = -rcForm.top;
else if (rcForm.bottom > sizPlayfield.cy)
dy = sizPlayfield.cy - rcForm.bottom;
rcForm.Offset(dx, dy);
SetRect(&rcForm);
// Set title size/position. dx/dy (amount of form that would have
// been off-screen) are cleverly used to keep the buttons as close
// to the partially off-screen unit as possible.
LabelControl *plblTitle = (LabelControl *)GetControlPtr(kidcTitle);
Rect rcTitle;
rcTitle.left = (cxForm - cxTitleText) / 2 - dx;
if (rcTitle.left < 0)
rcTitle.left = 0;
else if (rcTitle.left + cxTitleText > cxForm)
rcTitle.left += cxForm - (rcTitle.left + cxTitleText);
rcTitle.top = kcyTitleMargin - dy;
if (rcTitle.top < kcyTitleMargin)
rcTitle.top = kcyTitleMargin;
else if (rcTitle.top - kcyTitleMargin + cyTitle > cyForm - cyButton)
rcTitle.top += (cyForm - cyButton) - (rcTitle.top - kcyTitleMargin + cyTitle);
rcTitle.right = rcTitle.left + cxTitleText;
rcTitle.bottom = rcTitle.top + cyFont;
plblTitle->SetRect(&rcTitle);
// Set the titlebar text
plblTitle->SetText(pszTitle);
// Set the button positions. dx/dy are used as above for the title.
Rect rcButton;
rcButton.left = (cxForm - cxButtons) / 2 - dx;
if (rcButton.left < 0)
rcButton.left = 0;
else if (rcButton.left + cxButtons > cxForm)
rcButton.left += cxForm - (rcButton.left + cxButtons);
rcButton.top = cyTitle + kcyHealthBar + rcUIBounds.Height() - dy;
if (rcButton.top < cyTitle)
rcButton.top = cyTitle;
else if (rcButton.top + cyButton > cyForm)
rcButton.top = cyForm - cyButton;
rcButton.bottom = rcButton.top + cyButton;
// Stash away for painting later
m_rcButtons = rcButton;
m_rcButtons.right = m_rcButtons.left + cxButtons;
m_rcButtons.Offset(m_rc.left, m_rc.top);
// Position the buttons
for (i = 0; i < m_cctl; i++) {
Control *pctl = m_apctl[i];
word id = pctl->GetId();
if (id < kidcRelocButtonMin || id >= kidcRelocButtonMax)
continue;
if (!(pctl->GetFlags() & kfCtlVisible))
continue;
rcButton.right = rcButton.left + cxButton;
pctl->SetRect(&rcButton);
rcButton.left += cxButton + kcxButtonSpace;
}
// HACK: if there is only one button it's a Transform or Deliver button
// and we don't want to draw a border around it.
if (cButtons == 1)
m_rcButtons.SetEmpty();
// menus are AutoTakedown
m_wf |= kfFrmAutoTakedown | kfFrmTranslucent;
}
void UnitMenu::OnPaintBackground(DibBitmap *pbm, UpdateMap *pupd)
{
// Draw title background
LabelControl *plblTitle = (LabelControl *)GetControlPtr(kidcTitle);
Rect rc;
plblTitle->GetRect(&rc);
rc.Offset(m_rc.left, m_rc.top);
rc.Inflate(kcxTitleMargin - 1, kcyTitleMargin - 1); // leave 1 pixel space for black border
ShadowHelper(pbm, pupd, &rc);
// Draw border around title
rc.Inflate(1, 1);
DrawBorder(pbm, &rc, 1, GetColor(kiclrBlack), pupd);
// Draw border around buttons
if (!m_rcButtons.IsEmpty())
DrawBorder(pbm, &m_rcButtons, 1, GetColor(kiclrBlack), pupd);
// Draw border around whole thing for testing
// DrawBorder(pbm, &m_rc, 1, GetColor(kiclrRed), pupd);
}
void UnitMenu::OnPaint(DibBitmap *pbm)
{
Form::OnPaint(pbm);
}
// Treat everything except a tap on one of the button controls as a 'miss'
// and auto-takedown.
bool UnitMenu::OnHitTest(Event *pevt)
{
if (!(m_wf & kfFrmVisible))
return false;
for (int n = m_cctl - 1; n >= 0; n--) {
// Is it on this control?
Control *pctl = m_apctl[n];
if (pctl->OnHitTest(pevt) < 0)
continue;
// Yes, keep it
return true;
}
// we're AutoTakedown and hit not on one of the controls
// Assert(pevt->eType != penHoldEvent && pevt->eType != penUpEvent);
if (pevt->eType == penDownEvent) {
// Take down and let the event pass through
EndForm(kidcCancel);
return false;
}
// As long as this form stays up it should swallow any other
// events to avoid surprising the forms/objects underneath.
return true;
}
#ifdef MP_DEBUG_SHAREDMEM
void UnitGob::MPValidate()
{
// Warehouse sets animation state during draw
// m_ani.MPValidate((Animation *)(((byte *)MPGetGobPtr(m_gid)) + OFFSETOF(UnitGob, m_ani)));
MPValidateGobMember(UnitGob, m_agidEnemyNearby);
MPValidateGobMember(UnitGob, m_fxHealth);
MPValidateGobMember(UnitGob, m_cupdLastHitNotify);
// gpplrLocal specific
// MPValidateGobMember(UnitGob, m_cDamageCountdown);
// MPValidateGobMember(UnitGob, m_wfUnit);
MPValidateGobMember(UnitGob, m_unvl);
}
#endif
} // namespace wi