/** ** Check for dead goal. ** ** @warning The caller must check, if he likes the restored SavedOrder! ** ** @todo If a unit enters into a building, then the caster chooses an ** other goal, perhaps it is better to wait for the goal? ** ** @param unit Unit using the goal. ** ** @return true if order have changed, false else. */ bool COrder_SpellCast::CheckForDeadGoal(CUnit &unit) { const CUnit *goal = this->GetGoal(); // Position or valid target, it is ok. if (!goal || goal->IsVisibleAsGoal(*unit.Player)) { return false; } // Goal could be destroyed or unseen // So, cannot use type. this->goalPos = goal->tilePos; this->Range = 0; this->ClearGoal(); // If we have a saved order continue this saved order. if (unit.RestoreOrder()) { return true; } return false; }
/** ** Repair a unit. ** ** @param unit unit repairing ** @param goal unit being repaired */ static void RepairUnit(CUnit &unit, CUnit &goal) { CPlayer *player; int animlength; int hp; char buf[100]; player = unit.Player; if (goal.CurrentAction() != UnitActionBuilt) { // // Calculate the repair costs. // Assert(goal.Stats->Variables[HP_INDEX].Max); // // Check if enough resources are available // for (int i = 1; i < MaxCosts; ++i) { if (player->Resources[i] < goal.Type->RepairCosts[i]) { snprintf(buf, 100, _("We need more %s for repair!"), DefaultResourceNames[i].c_str()); player->Notify(NotifyYellow, unit.tilePos.x, unit.tilePos.y, buf); if (player->AiEnabled) { // FIXME: call back to AI? unit.CurrentOrder()->ClearGoal(); if (!unit.RestoreOrder()) { unit.ClearAction(); unit.State = 0; } } // FIXME: We shouldn't animate if no resources are available. return; } } // // Subtract the resources // player->SubCosts(goal.Type->RepairCosts); goal.Variable[HP_INDEX].Value += goal.Type->RepairHP; if (goal.Variable[HP_INDEX].Value > goal.Variable[HP_INDEX].Max) { goal.Variable[HP_INDEX].Value = goal.Variable[HP_INDEX].Max; } } else { int costs = goal.Stats->Costs[TimeCost] * 600; // hp is the current damage taken by the unit. hp = (goal.Data.Built.Progress * goal.Variable[HP_INDEX].Max) / costs - goal.Variable[HP_INDEX].Value; // // Calculate the length of the attack (repair) anim. // animlength = unit.Data.Repair.Cycles; unit.Data.Repair.Cycles = 0; // FIXME: implement this below: //unit.Data.Built.Worker->Type->BuilderSpeedFactor; goal.Data.Built.Progress += 100 * animlength * SpeedBuild; // Keep the same level of damage while increasing HP. goal.Variable[HP_INDEX].Value = (goal.Data.Built.Progress * goal.Stats->Variables[HP_INDEX].Max) / costs - hp; if (goal.Variable[HP_INDEX].Value > goal.Variable[HP_INDEX].Max) { goal.Variable[HP_INDEX].Value = goal.Variable[HP_INDEX].Max; } } }
/** ** Unit repairs ** ** @param unit Unit, for that the attack is handled. */ void HandleActionRepair(CUnit &unit) { CUnit *goal; int err; switch (unit.SubAction) { case 0: NewResetPath(unit); unit.SubAction = 1; // FALL THROUGH // // Move near to target. // case 1: // FIXME: RESET FIRST!! Why? We move first and than check if // something is in sight. err = DoActionMove(unit); if (!unit.Anim.Unbreakable) { // // No goal: if meeting damaged building repair it. // goal = unit.CurrentOrder()->GetGoal(); // // Target is dead, choose new one. // // Check if goal is correct unit. if (goal) { if (!goal->IsVisibleAsGoal(unit.Player)) { DebugPrint("repair target gone.\n"); unit.CurrentOrder()->goalPos = goal->tilePos; // FIXME: should I clear this here? unit.CurrentOrder()->ClearGoal(); goal = NULL; NewResetPath(unit); } } else if (unit.Player->AiEnabled) { // Ai players workers should stop if target is killed err = -1; } // // Have reached target? FIXME: could use return value // if (goal && unit.MapDistanceTo(*goal) <= unit.Type->RepairRange && goal->Variable[HP_INDEX].Value < goal->Variable[HP_INDEX].Max) { unit.State = 0; unit.SubAction = 2; unit.Data.Repair.Cycles = 0; const Vec2i dir = goal->tilePos + goal->Type->GetHalfTileSize() - unit.tilePos; UnitHeadingFromDeltaXY(unit, dir); } else if (err < 0) { unit.CurrentOrder()->ClearGoal(); if (!unit.RestoreOrder()) { unit.ClearAction(); unit.State = 0; } return; } // FIXME: Should be it already? Assert(unit.CurrentAction() == UnitActionRepair); } break; // // Repair the target. // case 2: AnimateActionRepair(unit); unit.Data.Repair.Cycles++; if (!unit.Anim.Unbreakable) { goal = unit.CurrentOrder()->GetGoal(); // // Target is dead, choose new one. // // Check if goal is correct unit. // FIXME: should I do a function for this? if (goal) { if (!goal->IsVisibleAsGoal(unit.Player)) { DebugPrint("repair goal is gone\n"); unit.CurrentOrder()->goalPos = goal->tilePos; // FIXME: should I clear this here? unit.CurrentOrder()->ClearGoal(); goal = NULL; NewResetPath(unit); } else { int dist = unit.MapDistanceTo(*goal); if (dist <= unit.Type->RepairRange) { RepairUnit(unit, *goal); goal = unit.CurrentOrder()->GetGoal(); } else if (dist > unit.Type->RepairRange) { // If goal has move, chase after it unit.State = 0; unit.SubAction = 0; } } } // // Target is fine, choose new one. // if (!goal || goal->Variable[HP_INDEX].Value >= goal->Variable[HP_INDEX].Max) { unit.CurrentOrder()->ClearGoal(); if (!unit.RestoreOrder()) { unit.ClearAction(); unit.State = 0; } return; } // FIXME: automatic repair } break; } }
/** ** Handle attacking the target. ** ** @param unit Unit, for that the attack is handled. */ void COrder_Attack::AttackTarget(CUnit &unit) { Assert(this->HasGoal() || Map.Info.IsPointOnMap(this->goalPos)); AnimateActionAttack(unit, *this); if (unit.Anim.Unbreakable) { return; } if (!this->HasGoal() && (this->Action == UnitActionAttackGround || Map.WallOnMap(this->goalPos))) { return; } // Target is dead ? Change order ? if (CheckForDeadGoal(unit)) { return; } CUnit *goal = this->GetGoal(); bool dead = !goal || goal->IsAlive() == false; // No target choose one. if (!goal) { goal = AttackUnitsInReactRange(unit); // No new goal, continue way to destination. if (!goal) { // Return to old task ? if (unit.RestoreOrder()) { return; } this->State = MOVE_TO_TARGET; return; } // Save current command to come back. COrder *savedOrder = COrder::NewActionAttack(unit, this->goalPos); if (unit.CanStoreOrder(savedOrder) == false) { delete savedOrder; savedOrder = NULL; } else { unit.SavedOrder = savedOrder; } this->SetGoal(goal); this->goalPos = goal->tilePos; this->MinRange = unit.Type->MinAttackRange; this->Range = unit.Stats->Variables[ATTACKRANGE_INDEX].Max; this->State |= WEAK_TARGET; // Have a weak target, try a better target. // FIXME: if out of range also try another target quick } else { if ((this->State & WEAK_TARGET)) { CUnit *newTarget = AttackUnitsInReactRange(unit); if (newTarget && ThreatCalculate(unit, *newTarget) < ThreatCalculate(unit, *goal)) { if (unit.CanStoreOrder(this)) { unit.SavedOrder = this->Clone(); } goal = newTarget; this->SetGoal(newTarget); this->goalPos = newTarget->tilePos; this->MinRange = unit.Type->MinAttackRange; this->State = MOVE_TO_TARGET; } } } // Still near to target, if not goto target. const int dist = unit.MapDistanceTo(*goal); if (dist > unit.Stats->Variables[ATTACKRANGE_INDEX].Max) { // towers don't chase after goal if (unit.CanMove()) { if (unit.CanStoreOrder(this)) { if (dead) { unit.SavedOrder = COrder::NewActionAttack(unit, this->goalPos); } else { unit.SavedOrder = this->Clone(); } } } unit.Frame = 0; this->State &= WEAK_TARGET; this->State |= MOVE_TO_TARGET; } if (dist < unit.Type->MinAttackRange) { this->State = MOVE_TO_TARGET; } // Turn always to target if (goal) { const Vec2i dir = goal->tilePos + goal->Type->GetHalfTileSize() - unit.tilePos; UnitHeadingFromDeltaXY(unit, dir); } }
/** ** Controls moving a unit to its target when attacking ** ** @param unit Unit that is attacking and moving */ void COrder_Attack::MoveToTarget(CUnit &unit) { Assert(!unit.Type->Vanishes && !unit.Destroyed && !unit.Removed); Assert(unit.CurrentOrder() == this); Assert(unit.CanMove()); Assert(this->HasGoal() || Map.Info.IsPointOnMap(this->goalPos)); int err = DoActionMove(unit); if (unit.Anim.Unbreakable) { return; } // Look if we have reached the target. if (err == 0 && !this->HasGoal()) { // Check if we're in range when attacking a location and we are waiting if (unit.MapDistanceTo(this->goalPos) <= unit.Stats->Variables[ATTACKRANGE_INDEX].Max) { err = PF_REACHED; } } if (err >= 0) { if (CheckForTargetInRange(unit)) { return; } return; } if (err == PF_REACHED) { CUnit *goal = this->GetGoal(); // Have reached target? FIXME: could use the new return code? if (goal && unit.MapDistanceTo(*goal) <= unit.Stats->Variables[ATTACKRANGE_INDEX].Max) { // Reached another unit, now attacking it const Vec2i dir = goal->tilePos + goal->Type->GetHalfTileSize() - unit.tilePos; UnitHeadingFromDeltaXY(unit, dir); this->State++; return; } // Attacking wall or ground. if (((goal && goal->Type && goal->Type->Wall) || (!goal && (Map.WallOnMap(this->goalPos) || this->Action == UnitActionAttackGround))) && unit.MapDistanceTo(this->goalPos) <= unit.Stats->Variables[ATTACKRANGE_INDEX].Max) { // Reached wall or ground, now attacking it UnitHeadingFromDeltaXY(unit, this->goalPos - unit.tilePos); this->State &= WEAK_TARGET; this->State |= ATTACK_TARGET; return; } } // Unreachable. if (err == PF_UNREACHABLE) { if (!this->HasGoal()) { // When attack-moving we have to allow a bigger range this->Range++; unit.Wait = 5; return; } else { this->ClearGoal(); } } // Return to old task? if (!unit.RestoreOrder()) { this->Finished = true; } }
/* virtual */ void COrder_SpellCast::Execute(CUnit &unit) { COrder_SpellCast &order = *this; if (unit.Wait) { unit.Wait--; return ; } const SpellType &spell = order.GetSpell(); switch (this->State) { case 0: // Check if we can cast the spell. if (!CanCastSpell(unit, spell, order.GetGoal(), order.goalPos)) { // Notify player about this problem if (unit.Variable[MANA_INDEX].Value < spell.ManaCost) { unit.Player->Notify(NotifyYellow, unit.tilePos, _("%s: not enough mana for spell: %s"), unit.Type->Name.c_str(), spell.Name.c_str()); } else if (unit.SpellCoolDownTimers[spell.Slot]) { unit.Player->Notify(NotifyYellow, unit.tilePos, _("%s: spell is not ready yet: %s"), unit.Type->Name.c_str(), spell.Name.c_str()); } else if (unit.Player->CheckCosts(spell.Costs, false)) { unit.Player->Notify(NotifyYellow, unit.tilePos, _("%s: not enough resources to cast spell: %s"), unit.Type->Name.c_str(), spell.Name.c_str()); } else { unit.Player->Notify(NotifyYellow, unit.tilePos, _("%s: can't cast spell: %s"), unit.Type->Name.c_str(), spell.Name.c_str()); } if (unit.Player->AiEnabled) { DebugPrint("FIXME: do we need an AI callback?\n"); } if (!unit.RestoreOrder()) { this->Finished = true; } return ; } if (CheckForDeadGoal(unit)) { return; } // FIXME FIXME FIXME: Check if already in range and skip straight to 2(casting) unit.ReCast = 0; // repeat spell on next pass? (defaults to `no') this->State = 1; // FALL THROUGH case 1: // Move to the target. if (spell.Range && spell.Range != INFINITE_RANGE) { if (SpellMoveToTarget(unit) == true) { if (!unit.RestoreOrder()) { this->Finished = true; } return ; } return ; } else { this->State = 2; } // FALL THROUGH case 2: // Cast spell on the target. if (!spell.IsCasterOnly() || spell.ForceUseAnimation) { AnimateActionSpellCast(unit, *this); if (unit.Anim.Unbreakable) { return ; } } else { // FIXME: what todo, if unit/goal is removed? CUnit *goal = order.GetGoal(); if (goal && goal != &unit && !goal->IsVisibleAsGoal(*unit.Player)) { unit.ReCast = 0; } else { unit.ReCast = SpellCast(unit, spell, goal, order.goalPos); } } // Target is dead ? Change order ? if (CheckForDeadGoal(unit)) { return; } // Check, if goal has moved (for ReCast) if (unit.ReCast && order.GetGoal() && unit.MapDistanceTo(*order.GetGoal()) > this->Range) { this->State = 0; return; } if (!unit.ReCast && unit.CurrentAction() != UnitActionDie) { if (!unit.RestoreOrder()) { this->Finished = true; } return ; } break; default: this->State = 0; // Reset path, than move to target break; } }