#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