1 /*
2 This file is part of Warzone 2100.
3 Copyright (C) 1999-2004 Eidos Interactive
4 Copyright (C) 2005-2020 Warzone 2100 Project
5
6 Warzone 2100 is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10
11 Warzone 2100 is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Warzone 2100; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20 /**
21 * @file droid.c
22 *
23 * Droid method functions.
24 *
25 */
26 #include "lib/framework/frame.h"
27 #include "lib/framework/math_ext.h"
28 #include "lib/framework/geometry.h"
29 #include "lib/framework/strres.h"
30
31 #include "lib/gamelib/gtime.h"
32 #include "lib/ivis_opengl/piematrix.h"
33 #include "lib/ivis_opengl/ivisdef.h"
34 #include "lib/sound/audio.h"
35 #include "lib/sound/audio_id.h"
36 #include "lib/netplay/netplay.h"
37
38 #include "objects.h"
39 #include "loop.h"
40 #include "visibility.h"
41 #include "map.h"
42 #include "droid.h"
43 #include "hci.h"
44 #include "power.h"
45 #include "effects.h"
46 #include "feature.h"
47 #include "action.h"
48 #include "order.h"
49 #include "move.h"
50 #include "geometry.h"
51 #include "display.h"
52 #include "console.h"
53 #include "component.h"
54 #include "lighting.h"
55 #include "multiplay.h"
56 #include "warcam.h"
57 #include "display3d.h"
58 #include "group.h"
59 #include "text.h"
60 #include "cmddroid.h"
61 #include "fpath.h"
62 #include "projectile.h"
63 #include "mission.h"
64 #include "levels.h"
65 #include "transporter.h"
66 #include "selection.h"
67 #include "difficulty.h"
68 #include "edit3d.h"
69 #include "scores.h"
70 #include "research.h"
71 #include "combat.h"
72 #include "template.h"
73 #include "qtscript.h"
74
75 #define DEFAULT_RECOIL_TIME (GAME_TICKS_PER_SEC/4)
76 #define DROID_DAMAGE_SPREAD (16 - rand()%32)
77 #define DROID_REPAIR_SPREAD (20 - rand()%40)
78
79 // store the experience of recently recycled droids
80 static std::priority_queue<int> recycled_experience[MAX_PLAYERS];
81
82 /** Height the transporter hovers at above the terrain. */
83 #define TRANSPORTER_HOVER_HEIGHT 10
84
85 // the structure that was last hit
86 DROID *psLastDroidHit;
87
88 //determines the best IMD to draw for the droid - A TEMP MEASURE!
89 static void groupConsoleInformOfSelection(UDWORD groupNumber);
90 static void groupConsoleInformOfCreation(UDWORD groupNumber);
91 static void groupConsoleInformOfCentering(UDWORD groupNumber);
92
93 static void droidUpdateDroidSelfRepair(DROID *psRepairDroid);
94 static UDWORD calcDroidBaseBody(DROID *psDroid);
95
cancelBuild(DROID * psDroid)96 void cancelBuild(DROID *psDroid)
97 {
98 if (psDroid->order.type == DORDER_NONE || psDroid->order.type == DORDER_PATROL || psDroid->order.type == DORDER_HOLD || psDroid->order.type == DORDER_SCOUT || psDroid->order.type == DORDER_GUARD)
99 {
100 objTrace(psDroid->id, "Droid build action cancelled");
101 psDroid->order.psObj = nullptr;
102 psDroid->action = DACTION_NONE;
103 setDroidActionTarget(psDroid, nullptr, 0);
104 return; // Don't cancel orders.
105 }
106
107 if (orderDroidList(psDroid))
108 {
109 objTrace(psDroid->id, "Droid build order cancelled - changing to next order");
110 }
111 else
112 {
113 objTrace(psDroid->id, "Droid build order cancelled");
114 psDroid->action = DACTION_NONE;
115 psDroid->order = DroidOrder(DORDER_NONE);
116 setDroidActionTarget(psDroid, nullptr, 0);
117
118 // The droid has no more build orders, so halt in place rather than clumping around the build objective
119 moveStopDroid(psDroid);
120
121 triggerEventDroidIdle(psDroid);
122 }
123 }
124
droidBodyUpgrade(DROID * psDroid)125 static void droidBodyUpgrade(DROID *psDroid)
126 {
127 const int factor = 10000; // use big numbers to scare away rounding errors
128 int prev = psDroid->originalBody;
129 psDroid->originalBody = calcDroidBaseBody(psDroid);
130 int increase = psDroid->originalBody * factor / prev;
131 psDroid->body = MIN(psDroid->originalBody, (psDroid->body * increase) / factor + 1);
132 if (isTransporter(psDroid))
133 {
134 for (DROID *psCurr = psDroid->psGroup->psList; psCurr != nullptr; psCurr = psCurr->psGrpNext)
135 {
136 if (psCurr != psDroid)
137 {
138 droidBodyUpgrade(psCurr);
139 }
140 }
141 }
142 }
143
144 // initialise droid module
droidInit()145 bool droidInit()
146 {
147 for (int i = 0; i < MAX_PLAYERS; i++)
148 {
149 recycled_experience[i] = std::priority_queue <int>(); // clear it
150 }
151 psLastDroidHit = nullptr;
152
153 return true;
154 }
155
droidReloadBar(const BASE_OBJECT * psObj,const WEAPON * psWeap,int weapon_slot)156 int droidReloadBar(const BASE_OBJECT *psObj, const WEAPON *psWeap, int weapon_slot)
157 {
158 WEAPON_STATS *psStats;
159 bool bSalvo;
160 int firingStage, interval;
161
162 if (psWeap->nStat == 0) // no weapon
163 {
164 return -1;
165 }
166 psStats = asWeaponStats + psWeap->nStat;
167
168 /* Justifiable only when greater than a one second reload or intra salvo time */
169 bSalvo = (psStats->upgrade[psObj->player].numRounds > 1);
170 if ((bSalvo && psStats->upgrade[psObj->player].reloadTime > GAME_TICKS_PER_SEC)
171 || psStats->upgrade[psObj->player].firePause > GAME_TICKS_PER_SEC
172 || (psObj->type == OBJ_DROID && isVtolDroid((const DROID *)psObj)))
173 {
174 if (psObj->type == OBJ_DROID && isVtolDroid((const DROID *)psObj))
175 {
176 //deal with VTOLs
177 firingStage = getNumAttackRuns((const DROID *)psObj, weapon_slot) - ((const DROID *)psObj)->asWeaps[weapon_slot].usedAmmo;
178
179 //compare with max value
180 interval = getNumAttackRuns((const DROID *)psObj, weapon_slot);
181 }
182 else
183 {
184 firingStage = gameTime - psWeap->lastFired;
185 interval = bSalvo ? weaponReloadTime(psStats, psObj->player) : weaponFirePause(psStats, psObj->player);
186 }
187 if (firingStage < interval && interval > 0)
188 {
189 return PERCENT(firingStage, interval);
190 }
191 return 100;
192 }
193 return -1;
194 }
195
196 #define UNIT_LOST_DELAY (5*GAME_TICKS_PER_SEC)
197 /* Deals damage to a droid
198 * \param psDroid droid to deal damage to
199 * \param damage amount of damage to deal
200 * \param weaponClass the class of the weapon that deals the damage
201 * \param weaponSubClass the subclass of the weapon that deals the damage
202 * \param angle angle of impact (from the damage dealing projectile in relation to this droid)
203 * \return > 0 when the dealt damage destroys the droid, < 0 when the droid survives
204 *
205 */
droidDamage(DROID * psDroid,unsigned damage,WEAPON_CLASS weaponClass,WEAPON_SUBCLASS weaponSubClass,unsigned impactTime,bool isDamagePerSecond,int minDamage)206 int32_t droidDamage(DROID *psDroid, unsigned damage, WEAPON_CLASS weaponClass, WEAPON_SUBCLASS weaponSubClass, unsigned impactTime, bool isDamagePerSecond, int minDamage)
207 {
208 int32_t relativeDamage;
209
210 CHECK_DROID(psDroid);
211
212 // VTOLs (and transporters in MP) on the ground take triple damage
213 if ((isVtolDroid(psDroid) || (isTransporter(psDroid) && bMultiPlayer)) && (psDroid->sMove.Status == MOVEINACTIVE))
214 {
215 damage *= 3;
216 }
217
218 relativeDamage = objDamage(psDroid, damage, psDroid->originalBody, weaponClass, weaponSubClass, isDamagePerSecond, minDamage);
219
220 if (relativeDamage > 0)
221 {
222 // reset the attack level
223 if (secondaryGetState(psDroid, DSO_ATTACK_LEVEL) == DSS_ALEV_ATTACKED)
224 {
225 secondarySetState(psDroid, DSO_ATTACK_LEVEL, DSS_ALEV_ALWAYS);
226 }
227 // Now check for auto return on droid's secondary orders (i.e. return on medium/heavy damage)
228 secondaryCheckDamageLevel(psDroid);
229
230 CHECK_DROID(psDroid);
231 }
232 else if (relativeDamage < 0)
233 {
234 // Droid destroyed
235 debug(LOG_ATTACK, "droid (%d): DESTROYED", psDroid->id);
236
237 // Deal with score increase/decrease and messages to the player
238 if (psDroid->player == selectedPlayer)
239 {
240 // TRANSLATORS: Refers to the loss of a single unit, known by its name
241 CONPRINTF(_("%s Lost!"), objInfo(psDroid));
242 scoreUpdateVar(WD_UNITS_LOST);
243 audio_QueueTrackMinDelayPos(ID_SOUND_UNIT_DESTROYED, UNIT_LOST_DELAY,
244 psDroid->pos.x, psDroid->pos.y, psDroid->pos.z);
245 }
246 else
247 {
248 scoreUpdateVar(WD_UNITS_KILLED);
249 }
250
251 // Do we have a dying animation?
252 if (psDroid->sDisplay.imd->objanimpie[ANIM_EVENT_DYING] && psDroid->animationEvent != ANIM_EVENT_DYING)
253 {
254 bool useDeathAnimation = true;
255 //Babas should not burst into flames from non-heat weapons
256 if (psDroid->droidType == DROID_PERSON)
257 {
258 if (weaponClass == WC_HEAT)
259 {
260 // NOTE: 3 types of screams are available ID_SOUND_BARB_SCREAM - ID_SOUND_BARB_SCREAM3
261 audio_PlayObjDynamicTrack(psDroid, ID_SOUND_BARB_SCREAM + (rand() % 3), nullptr);
262 }
263 else
264 {
265 useDeathAnimation = false;
266 }
267 }
268 if (useDeathAnimation)
269 {
270 debug(LOG_DEATH, "%s droid %d (%p) is starting death animation", objInfo(psDroid), (int)psDroid->id, static_cast<void *>(psDroid));
271 psDroid->timeAnimationStarted = gameTime;
272 psDroid->animationEvent = ANIM_EVENT_DYING;
273 }
274 }
275 // Otherwise use the default destruction animation
276 if (psDroid->animationEvent != ANIM_EVENT_DYING)
277 {
278 debug(LOG_DEATH, "%s droid %d (%p) is toast", objInfo(psDroid), (int)psDroid->id, static_cast<void *>(psDroid));
279 // This should be sent even if multi messages are turned off, as the group message that was
280 // sent won't contain the destroyed droid
281 if (bMultiPlayer && !bMultiMessages)
282 {
283 bMultiMessages = true;
284 destroyDroid(psDroid, impactTime);
285 bMultiMessages = false;
286 }
287 else
288 {
289 destroyDroid(psDroid, impactTime);
290 }
291 }
292 }
293
294 return relativeDamage;
295 }
296
DROID(uint32_t id,unsigned player)297 DROID::DROID(uint32_t id, unsigned player)
298 : BASE_OBJECT(OBJ_DROID, id, player)
299 , droidType(DROID_ANY)
300 , psGroup(nullptr)
301 , psGrpNext(nullptr)
302 , secondaryOrder(DSS_ARANGE_LONG | DSS_REPLEV_NEVER | DSS_ALEV_ALWAYS | DSS_HALT_GUARD)
303 , secondaryOrderPending(DSS_ARANGE_LONG | DSS_REPLEV_NEVER | DSS_ALEV_ALWAYS | DSS_HALT_GUARD)
304 , secondaryOrderPendingCount(0)
305 , action(DACTION_NONE)
306 , actionPos(0, 0)
307 {
308 memset(aName, 0, sizeof(aName));
309 memset(asBits, 0, sizeof(asBits));
310 pos = Vector3i(0, 0, 0);
311 rot = Vector3i(0, 0, 0);
312 order.type = DORDER_NONE;
313 order.pos = Vector2i(0, 0);
314 order.pos2 = Vector2i(0, 0);
315 order.direction = 0;
316 order.psObj = nullptr;
317 order.psStats = nullptr;
318 sMove.Status = MOVEINACTIVE;
319 listSize = 0;
320 listPendingBegin = 0;
321 iAudioID = NO_SOUND;
322 group = UBYTE_MAX;
323 psBaseStruct = nullptr;
324 sDisplay.frameNumber = 0; // it was never drawn before
325 for (unsigned vPlayer = 0; vPlayer < MAX_PLAYERS; ++vPlayer)
326 {
327 visible[vPlayer] = hasSharedVision(vPlayer, player) ? UINT8_MAX : 0;
328 }
329 memset(seenThisTick, 0, sizeof(seenThisTick));
330 periodicalDamageStart = 0;
331 periodicalDamage = 0;
332 sDisplay.screenX = OFF_SCREEN;
333 sDisplay.screenY = OFF_SCREEN;
334 sDisplay.screenR = 0;
335 sDisplay.imd = nullptr;
336 illumination = UBYTE_MAX;
337 resistance = ACTION_START_TIME; // init the resistance to indicate no EW performed on this droid
338 lastFrustratedTime = 0; // make sure we do not start the game frustrated
339 }
340
341 /* DROID::~DROID: release all resources associated with a droid -
342 * should only be called by objmem - use vanishDroid preferably
343 */
~DROID()344 DROID::~DROID()
345 {
346 // Make sure to get rid of some final references in the sound code to this object first
347 // In BASE_OBJECT::~BASE_OBJECT() is too late for this, since some callbacks require us to still be a DROID.
348 audio_RemoveObj(this);
349
350 DROID *psDroid = this;
351 DROID *psCurr, *pNextGroupDroid = nullptr;
352
353 if (isTransporter(psDroid))
354 {
355 if (psDroid->psGroup)
356 {
357 //free all droids associated with this Transporter
358 for (psCurr = psDroid->psGroup->psList; psCurr != nullptr && psCurr != psDroid; psCurr = pNextGroupDroid)
359 {
360 pNextGroupDroid = psCurr->psGrpNext;
361 delete psCurr;
362 }
363 }
364 }
365
366 fpathRemoveDroidData(psDroid->id);
367
368 // leave the current group if any
369 if (psDroid->psGroup)
370 {
371 psDroid->psGroup->remove(psDroid);
372 }
373 }
374
copy_experience_queue(int player)375 std::priority_queue<int> copy_experience_queue(int player)
376 {
377 return recycled_experience[player];
378 }
379
add_to_experience_queue(int player,int value)380 void add_to_experience_queue(int player, int value)
381 {
382 recycled_experience[player].push(value);
383 }
384
385 // recycle a droid (retain it's experience and some of it's cost)
recycleDroid(DROID * psDroid)386 void recycleDroid(DROID *psDroid)
387 {
388 CHECK_DROID(psDroid);
389
390 // store the droids kills
391 if (psDroid->experience > 0)
392 {
393 recycled_experience[psDroid->player].push(psDroid->experience);
394 }
395
396 // return part of the cost of the droid
397 int cost = calcDroidPower(psDroid);
398 cost = (cost / 2) * psDroid->body / psDroid->originalBody;
399 addPower(psDroid->player, (UDWORD)cost);
400
401 // hide the droid
402 memset(psDroid->visible, 0, sizeof(psDroid->visible));
403
404 if (psDroid->psGroup)
405 {
406 psDroid->psGroup->remove(psDroid);
407 }
408
409 triggerEvent(TRIGGER_OBJECT_RECYCLED, psDroid);
410 vanishDroid(psDroid);
411
412 Vector3i position = psDroid->pos.xzy();
413 addEffect(&position, EFFECT_EXPLOSION, EXPLOSION_TYPE_DISCOVERY, false, nullptr, false, gameTime - deltaGameTime + 1);
414
415 CHECK_DROID(psDroid);
416 }
417
418
removeDroidBase(DROID * psDel)419 bool removeDroidBase(DROID *psDel)
420 {
421 CHECK_DROID(psDel);
422
423 if (isDead(psDel))
424 {
425 // droid has already been killed, quit
426 syncDebug("droid already dead");
427 return true;
428 }
429
430 syncDebugDroid(psDel, '#');
431
432 //kill all the droids inside the transporter
433 if (isTransporter(psDel))
434 {
435 if (psDel->psGroup)
436 {
437 //free all droids associated with this Transporter
438 DROID *psNext;
439 for (auto psCurr = psDel->psGroup->psList; psCurr != nullptr && psCurr != psDel; psCurr = psNext)
440 {
441 psNext = psCurr->psGrpNext;
442
443 /* add droid to droid list then vanish it - hope this works! - GJ */
444 addDroid(psCurr, apsDroidLists);
445 vanishDroid(psCurr);
446 }
447 }
448 }
449
450 // leave the current group if any
451 if (psDel->psGroup)
452 {
453 psDel->psGroup->remove(psDel);
454 psDel->psGroup = nullptr;
455 }
456
457 /* Put Deliv. Pts back into world when a command droid dies */
458 if (psDel->droidType == DROID_COMMAND)
459 {
460 for (auto psStruct = apsStructLists[psDel->player]; psStruct; psStruct = psStruct->psNext)
461 {
462 // alexl's stab at a right answer.
463 if (StructIsFactory(psStruct)
464 && psStruct->pFunctionality->factory.psCommander == psDel)
465 {
466 assignFactoryCommandDroid(psStruct, nullptr);
467 }
468 }
469 }
470
471 // Check to see if constructor droid currently trying to find a location to build
472 if (psDel->player == selectedPlayer && psDel->selected && isConstructionDroid(psDel))
473 {
474 // If currently trying to build, kill off the placement
475 if (tryingToGetLocation())
476 {
477 int numSelectedConstructors = 0;
478 for (DROID *psDroid = apsDroidLists[psDel->player]; psDroid != nullptr; psDroid = psDroid->psNext)
479 {
480 numSelectedConstructors += psDroid->selected && isConstructionDroid(psDroid);
481 }
482 if (numSelectedConstructors <= 1) // If we were the last selected construction droid.
483 {
484 kill3DBuilding();
485 }
486 }
487 }
488
489 if (psDel->player == selectedPlayer)
490 {
491 intRefreshScreen();
492 }
493
494 killDroid(psDel);
495 return true;
496 }
497
removeDroidFX(DROID * psDel,unsigned impactTime)498 static void removeDroidFX(DROID *psDel, unsigned impactTime)
499 {
500 Vector3i pos;
501
502 CHECK_DROID(psDel);
503
504 // only display anything if the droid is visible
505 if (!psDel->visible[selectedPlayer])
506 {
507 return;
508 }
509
510 if (psDel->animationEvent != ANIM_EVENT_DYING)
511 {
512 compPersonToBits(psDel);
513 }
514
515 /* if baba then squish */
516 if (psDel->droidType == DROID_PERSON)
517 {
518 // The barbarian has been run over ...
519 audio_PlayStaticTrack(psDel->pos.x, psDel->pos.y, ID_SOUND_BARB_SQUISH);
520 }
521 else
522 {
523 destroyFXDroid(psDel, impactTime);
524 pos.x = psDel->pos.x;
525 pos.z = psDel->pos.y;
526 pos.y = psDel->pos.z;
527 if (psDel->droidType == DROID_SUPERTRANSPORTER)
528 {
529 addEffect(&pos, EFFECT_EXPLOSION, EXPLOSION_TYPE_LARGE, false, nullptr, 0, impactTime);
530 }
531 else
532 {
533 addEffect(&pos, EFFECT_DESTRUCTION, DESTRUCTION_TYPE_DROID, false, nullptr, 0, impactTime);
534 }
535 audio_PlayStaticTrack(psDel->pos.x, psDel->pos.y, ID_SOUND_EXPLOSION);
536 }
537 }
538
destroyDroid(DROID * psDel,unsigned impactTime)539 bool destroyDroid(DROID *psDel, unsigned impactTime)
540 {
541 ASSERT(gameTime - deltaGameTime < impactTime, "Expected %u < %u, gameTime = %u, bad impactTime", gameTime - deltaGameTime, impactTime, gameTime);
542
543 if (psDel->lastHitWeapon == WSC_LAS_SAT) // darken tile if lassat.
544 {
545 UDWORD width, breadth, mapX, mapY;
546 MAPTILE *psTile;
547
548 mapX = map_coord(psDel->pos.x);
549 mapY = map_coord(psDel->pos.y);
550 for (width = mapX - 1; width <= mapX + 1; width++)
551 {
552 for (breadth = mapY - 1; breadth <= mapY + 1; breadth++)
553 {
554 psTile = mapTile(width, breadth);
555 if (TEST_TILE_VISIBLE(selectedPlayer, psTile))
556 {
557 psTile->illumination /= 2;
558 }
559 }
560 }
561 }
562
563 removeDroidFX(psDel, impactTime);
564 removeDroidBase(psDel);
565 psDel->died = impactTime;
566 return true;
567 }
568
vanishDroid(DROID * psDel)569 void vanishDroid(DROID *psDel)
570 {
571 removeDroidBase(psDel);
572 }
573
574 /* Remove a droid from the List so doesn't update or get drawn etc
575 TAKE CARE with removeDroid() - usually want droidRemove since it deal with grid code*/
576 //returns false if the droid wasn't removed - because it died!
droidRemove(DROID * psDroid,DROID * pList[MAX_PLAYERS])577 bool droidRemove(DROID *psDroid, DROID *pList[MAX_PLAYERS])
578 {
579 CHECK_DROID(psDroid);
580
581 if (isDead(psDroid))
582 {
583 // droid has already been killed, quit
584 return false;
585 }
586
587 // leave the current group if any - not if its a Transporter droid
588 if (!isTransporter(psDroid) && psDroid->psGroup)
589 {
590 psDroid->psGroup->remove(psDroid);
591 psDroid->psGroup = nullptr;
592 }
593
594 // reset the baseStruct
595 setDroidBase(psDroid, nullptr);
596
597 removeDroid(psDroid, pList);
598
599 if (psDroid->player == selectedPlayer)
600 {
601 intRefreshScreen();
602 }
603
604 return true;
605 }
606
_syncDebugDroid(const char * function,DROID const * psDroid,char ch)607 void _syncDebugDroid(const char *function, DROID const *psDroid, char ch)
608 {
609 if (psDroid->type != OBJ_DROID) {
610 ASSERT(false, "%c Broken psDroid->type %u!", ch, psDroid->type);
611 syncDebug("Broken psDroid->type %u!", psDroid->type);
612 }
613 int list[] =
614 {
615 ch,
616
617 (int)psDroid->id,
618
619 psDroid->player,
620 psDroid->pos.x, psDroid->pos.y, psDroid->pos.z,
621 psDroid->rot.direction, psDroid->rot.pitch, psDroid->rot.roll,
622 (int)psDroid->order.type, psDroid->order.pos.x, psDroid->order.pos.y, psDroid->listSize,
623 (int)psDroid->action,
624 (int)psDroid->secondaryOrder,
625 (int)psDroid->body,
626 (int)psDroid->sMove.Status,
627 psDroid->sMove.speed, psDroid->sMove.moveDir,
628 psDroid->sMove.pathIndex, (int)psDroid->sMove.asPath.size(),
629 psDroid->sMove.src.x, psDroid->sMove.src.y, psDroid->sMove.target.x, psDroid->sMove.target.y, psDroid->sMove.destination.x, psDroid->sMove.destination.y,
630 psDroid->sMove.bumpDir, (int)psDroid->sMove.bumpTime, (int)psDroid->sMove.lastBump, (int)psDroid->sMove.pauseTime, psDroid->sMove.bumpPos.x, psDroid->sMove.bumpPos.y, (int)psDroid->sMove.shuffleStart,
631 (int)psDroid->experience,
632 };
633 _syncDebugIntList(function, "%c droid%d = p%d;pos(%d,%d,%d),rot(%d,%d,%d),order%d(%d,%d)^%d,action%d,secondaryOrder%X,body%d,sMove(status%d,speed%d,moveDir%d,path%d/%d,src(%d,%d),target(%d,%d),destination(%d,%d),bump(%d,%d,%d,%d,(%d,%d),%d)),exp%u", list, ARRAY_SIZE(list));
634 }
635
636 /* The main update routine for all droids */
droidUpdate(DROID * psDroid)637 void droidUpdate(DROID *psDroid)
638 {
639 Vector3i dv;
640 UDWORD percentDamage, emissionInterval;
641 BASE_OBJECT *psBeingTargetted = nullptr;
642 unsigned i;
643
644 CHECK_DROID(psDroid);
645
646 #ifdef DEBUG
647 // Check that we are (still) in the sensor list
648 if (psDroid->droidType == DROID_SENSOR)
649 {
650 BASE_OBJECT *psSensor;
651
652 for (psSensor = apsSensorList[0]; psSensor; psSensor = psSensor->psNextFunc)
653 {
654 if (psSensor == (BASE_OBJECT *)psDroid)
655 {
656 break;
657 }
658 }
659 ASSERT(psSensor == (BASE_OBJECT *)psDroid, "%s(%p) not in sensor list!",
660 droidGetName(psDroid), static_cast<void *>(psDroid));
661 }
662 #endif
663
664 syncDebugDroid(psDroid, '<');
665
666 if (psDroid->flags.test(OBJECT_FLAG_DIRTY))
667 {
668 visTilesUpdate(psDroid);
669 droidBodyUpgrade(psDroid);
670 psDroid->flags.set(OBJECT_FLAG_DIRTY, false);
671 }
672
673 // Save old droid position, update time.
674 psDroid->prevSpacetime = getSpacetime(psDroid);
675 psDroid->time = gameTime;
676 for (i = 0; i < MAX(1, psDroid->numWeaps); ++i)
677 {
678 psDroid->asWeaps[i].prevRot = psDroid->asWeaps[i].rot;
679 }
680
681 if (psDroid->animationEvent != ANIM_EVENT_NONE)
682 {
683 iIMDShape *imd = psDroid->sDisplay.imd->objanimpie[psDroid->animationEvent];
684 if (imd && imd->objanimcycles > 0 && gameTime > psDroid->timeAnimationStarted + imd->objanimtime * imd->objanimcycles)
685 {
686 // Done animating (animation is defined by body - other components should follow suit)
687 if (psDroid->animationEvent == ANIM_EVENT_DYING)
688 {
689 debug(LOG_DEATH, "%s (%d) died to burn anim (died=%d)", objInfo(psDroid), (int)psDroid->id, (int)psDroid->died);
690 destroyDroid(psDroid, gameTime);
691 return;
692 }
693 psDroid->animationEvent = ANIM_EVENT_NONE;
694 }
695 }
696 else if (psDroid->animationEvent == ANIM_EVENT_DYING)
697 {
698 return; // rest below is irrelevant if dead
699 }
700
701 // ai update droid
702 aiUpdateDroid(psDroid);
703
704 // Update the droids order.
705 orderUpdateDroid(psDroid);
706
707 // update the action of the droid
708 actionUpdateDroid(psDroid);
709
710 syncDebugDroid(psDroid, 'M');
711
712 // update the move system
713 moveUpdateDroid(psDroid);
714
715 /* Only add smoke if they're visible */
716 if ((psDroid->visible[selectedPlayer]) && psDroid->droidType != DROID_PERSON)
717 {
718 // need to clip this value to prevent overflow condition
719 percentDamage = 100 - clip<UDWORD>(PERCENT(psDroid->body, psDroid->originalBody), 0, 100);
720
721 // Is there any damage?
722 if (percentDamage >= 25)
723 {
724 if (percentDamage >= 100)
725 {
726 percentDamage = 99;
727 }
728
729 emissionInterval = CALC_DROID_SMOKE_INTERVAL(percentDamage);
730
731 uint32_t effectTime = std::max(gameTime - deltaGameTime + 1, psDroid->lastEmission + emissionInterval);
732 if (gameTime >= effectTime)
733 {
734 dv.x = psDroid->pos.x + DROID_DAMAGE_SPREAD;
735 dv.z = psDroid->pos.y + DROID_DAMAGE_SPREAD;
736 dv.y = psDroid->pos.z;
737
738 dv.y += (psDroid->sDisplay.imd->max.y * 2);
739 addEffect(&dv, EFFECT_SMOKE, SMOKE_TYPE_DRIFTING_SMALL, false, nullptr, 0, effectTime);
740 psDroid->lastEmission = effectTime;
741 }
742 }
743 }
744
745 // -----------------
746 /* Are we a sensor droid or a command droid? Show where we target for selectedPlayer. */
747 if (psDroid->player == selectedPlayer && (psDroid->droidType == DROID_SENSOR || psDroid->droidType == DROID_COMMAND))
748 {
749 /* If we're attacking or sensing (observing), then... */
750 if ((psBeingTargetted = orderStateObj(psDroid, DORDER_ATTACK))
751 || (psBeingTargetted = orderStateObj(psDroid, DORDER_OBSERVE)))
752 {
753 psBeingTargetted->flags.set(OBJECT_FLAG_TARGETED, true);
754 }
755 else if (secondaryGetState(psDroid, DSO_HALTTYPE) != DSS_HALT_PURSUE &&
756 psDroid->psActionTarget[0] != nullptr &&
757 validTarget(psDroid, psDroid->psActionTarget[0], 0) &&
758 (psDroid->action == DACTION_ATTACK ||
759 psDroid->action == DACTION_OBSERVE ||
760 orderState(psDroid, DORDER_HOLD)))
761 {
762 psBeingTargetted = psDroid->psActionTarget[0];
763 psBeingTargetted->flags.set(OBJECT_FLAG_TARGETED, true);
764 }
765 }
766 // -----------------
767
768 // See if we can and need to self repair.
769 if (!isVtolDroid(psDroid) && psDroid->body < psDroid->originalBody && psDroid->asBits[COMP_REPAIRUNIT] != 0 && selfRepairEnabled(psDroid->player))
770 {
771 droidUpdateDroidSelfRepair(psDroid);
772 }
773
774 /* Update the fire damage data */
775 if (psDroid->periodicalDamageStart != 0 && psDroid->periodicalDamageStart != gameTime - deltaGameTime) // -deltaGameTime, since projectiles are updated after droids.
776 {
777 // The periodicalDamageStart has been set, but is not from the previous tick, so we must be out of the fire.
778 psDroid->periodicalDamage = 0; // Reset periodical damage done this tick.
779 if (psDroid->periodicalDamageStart + BURN_TIME < gameTime)
780 {
781 // Finished periodical damaging.
782 psDroid->periodicalDamageStart = 0;
783 }
784 else
785 {
786 // do hardcoded burn damage (this damage automatically applied after periodical damage finished)
787 droidDamage(psDroid, BURN_DAMAGE, WC_HEAT, WSC_FLAME, gameTime - deltaGameTime / 2 + 1, true, BURN_MIN_DAMAGE);
788 }
789 }
790
791 // At this point, the droid may be dead due to periodical damage or hardcoded burn damage.
792 if (isDead(psDroid))
793 {
794 return;
795 }
796
797 calcDroidIllumination(psDroid);
798
799 // Check the resistance level of the droid
800 if ((psDroid->id + gameTime) / 833 != (psDroid->id + gameTime - deltaGameTime) / 833)
801 {
802 // Zero resistance means not currently been attacked - ignore these
803 if (psDroid->resistance && psDroid->resistance < droidResistance(psDroid))
804 {
805 // Increase over time if low
806 psDroid->resistance++;
807 }
808 }
809
810 syncDebugDroid(psDroid, '>');
811
812 CHECK_DROID(psDroid);
813 }
814
815 /* See if a droid is next to a structure */
droidNextToStruct(DROID * psDroid,STRUCTURE * psStruct)816 static bool droidNextToStruct(DROID *psDroid, STRUCTURE *psStruct)
817 {
818 CHECK_DROID(psDroid);
819
820 auto pos = map_coord(psDroid->pos);
821 int minX = std::max(pos.x - 1, 0);
822 int minY = std::max(pos.y - 1, 0);
823 int maxX = std::min(pos.x + 1, mapWidth);
824 int maxY = std::min(pos.y + 1, mapHeight);
825 for (int y = minY; y <= maxY; ++y)
826 {
827 for (int x = minX; x <= maxX; ++x)
828 {
829 if (TileHasStructure(mapTile(x, y)) &&
830 getTileStructure(x, y) == psStruct)
831 {
832 return true;
833 }
834 }
835 }
836
837 return false;
838 }
839
droidCheckBuildStillInProgress(void * psObj)840 static bool droidCheckBuildStillInProgress(void *psObj)
841 {
842 if (psObj == nullptr)
843 {
844 return false;
845 }
846
847 auto psDroid = (DROID *)psObj;
848 CHECK_DROID(psDroid);
849
850 return !psDroid->died && psDroid->action == DACTION_BUILD;
851 }
852
droidBuildStartAudioCallback(void * psObj)853 static bool droidBuildStartAudioCallback(void *psObj)
854 {
855 auto psDroid = (DROID *)psObj;
856
857 if (psDroid != nullptr && psDroid->visible[selectedPlayer])
858 {
859 audio_PlayObjDynamicTrack(psDroid, ID_SOUND_CONSTRUCTION_LOOP, droidCheckBuildStillInProgress);
860 }
861
862 return true;
863 }
864
865
866 /* Set up a droid to build a structure - returns true if successful */
droidStartBuild(DROID * psDroid)867 DroidStartBuild droidStartBuild(DROID *psDroid)
868 {
869 STRUCTURE *psStruct = nullptr;
870 ASSERT_OR_RETURN(DroidStartBuildFailed, psDroid != nullptr, "Bad Droid");
871 CHECK_DROID(psDroid);
872
873 /* See if we are starting a new structure */
874 if (psDroid->order.psObj == nullptr &&
875 (psDroid->order.type == DORDER_BUILD ||
876 psDroid->order.type == DORDER_LINEBUILD))
877 {
878 STRUCTURE_STATS *psStructStat = psDroid->order.psStats;
879
880 ItemAvailability ia = (ItemAvailability)apStructTypeLists[psDroid->player][psStructStat - asStructureStats];
881 if (ia != AVAILABLE && ia != REDUNDANT)
882 {
883 ASSERT(false, "Cannot build \"%s\" for player %d.", psStructStat->name.toUtf8().c_str(), psDroid->player);
884 cancelBuild(psDroid);
885 objTrace(psDroid->id, "DroidStartBuildFailed: not researched");
886 return DroidStartBuildFailed;
887 }
888
889 //need to check structLimits have not been exceeded
890 if (psStructStat->curCount[psDroid->player] >= psStructStat->upgrade[psDroid->player].limit)
891 {
892 cancelBuild(psDroid);
893 objTrace(psDroid->id, "DroidStartBuildFailed: structure limits");
894 return DroidStartBuildFailed;
895 }
896 // Can't build on burning oil derricks.
897 if (psStructStat->type == REF_RESOURCE_EXTRACTOR && fireOnLocation(psDroid->order.pos.x, psDroid->order.pos.y))
898 {
899 // Don't cancel build, since we can wait for it to stop burning.
900 objTrace(psDroid->id, "DroidStartBuildPending: burning");
901 return DroidStartBuildPending;
902 }
903 //ok to build
904 psStruct = buildStructureDir(psStructStat, psDroid->order.pos.x, psDroid->order.pos.y, psDroid->order.direction, psDroid->player, false);
905 if (!psStruct)
906 {
907 cancelBuild(psDroid);
908 objTrace(psDroid->id, "DroidStartBuildFailed: buildStructureDir failed");
909 return DroidStartBuildFailed;
910 }
911 psStruct->body = (psStruct->body + 9) / 10; // Structures start at 10% health. Round up.
912 }
913 else
914 {
915 /* Check the structure is still there to build (joining a partially built struct) */
916 psStruct = castStructure(psDroid->order.psObj);
917 if (psStruct == nullptr)
918 {
919 psStruct = castStructure(worldTile(psDroid->actionPos)->psObject);
920 }
921 if (psStruct && !droidNextToStruct(psDroid, psStruct))
922 {
923 /* Nope - stop building */
924 debug(LOG_NEVER, "not next to structure");
925 objTrace(psDroid->id, "DroidStartBuildSuccess: not next to structure");
926 }
927 }
928
929 //check structure not already built, and we still 'own' it
930 if (psStruct)
931 {
932 if (psStruct->status != SS_BUILT && aiCheckAlliances(psStruct->player, psDroid->player))
933 {
934 psDroid->actionStarted = gameTime;
935 psDroid->actionPoints = 0;
936 setDroidTarget(psDroid, psStruct);
937 setDroidActionTarget(psDroid, psStruct, 0);
938 objTrace(psDroid->id, "DroidStartBuild: set target");
939 }
940
941 if (psStruct->visible[selectedPlayer])
942 {
943 audio_PlayObjStaticTrackCallback(psDroid, ID_SOUND_CONSTRUCTION_START, droidBuildStartAudioCallback);
944 }
945 }
946 CHECK_DROID(psDroid);
947
948 objTrace(psDroid->id, "DroidStartBuildSuccess");
949 return DroidStartBuildSuccess;
950 }
951
droidAddWeldSound(Vector3i iVecEffect)952 static void droidAddWeldSound(Vector3i iVecEffect)
953 {
954 int iAudioID = ID_SOUND_CONSTRUCTION_1 + (rand() % 4);
955
956 audio_PlayStaticTrack(iVecEffect.x, iVecEffect.z, iAudioID);
957 }
958
addConstructorEffect(STRUCTURE * psStruct)959 static void addConstructorEffect(STRUCTURE *psStruct)
960 {
961 if ((ONEINTEN) && (psStruct->visible[selectedPlayer]))
962 {
963 /* This needs fixing - it's an arse effect! */
964 const Vector2i size = psStruct->size() * TILE_UNITS / 4;
965 Vector3i temp;
966 temp.x = psStruct->pos.x + ((rand() % (2 * size.x)) - size.x);
967 temp.y = map_TileHeight(map_coord(psStruct->pos.x), map_coord(psStruct->pos.y)) + (psStruct->sDisplay.imd->max.y / 6);
968 temp.z = psStruct->pos.y + ((rand() % (2 * size.y)) - size.y);
969 if (rand() % 2)
970 {
971 droidAddWeldSound(temp);
972 }
973 }
974 }
975
976 /* Update a construction droid while it is building
977 returns true while building continues */
droidUpdateBuild(DROID * psDroid)978 bool droidUpdateBuild(DROID *psDroid)
979 {
980 CHECK_DROID(psDroid);
981 ASSERT_OR_RETURN(false, psDroid->action == DACTION_BUILD, "%s (order %s) has wrong action for construction: %s",
982 droidGetName(psDroid), getDroidOrderName(psDroid->order.type), getDroidActionName(psDroid->action));
983
984 STRUCTURE *psStruct = castStructure(psDroid->order.psObj);
985 if (psStruct == nullptr)
986 {
987 // Target missing, stop trying to build it.
988 psDroid->action = DACTION_NONE;
989 return false;
990 }
991
992 ASSERT_OR_RETURN(false, psStruct->type == OBJ_STRUCTURE, "target is not a structure");
993 ASSERT_OR_RETURN(false, psDroid->asBits[COMP_CONSTRUCT] < numConstructStats, "Invalid construct pointer for unit");
994
995 // First check the structure hasn't been completed by another droid
996 if (psStruct->status == SS_BUILT)
997 {
998 // Check if line order build is completed, or we are not carrying out a line order build
999 if (psDroid->order.type != DORDER_LINEBUILD ||
1000 map_coord(psDroid->order.pos) == map_coord(psDroid->order.pos2))
1001 {
1002 cancelBuild(psDroid);
1003 }
1004 else
1005 {
1006 psDroid->action = DACTION_NONE; // make us continue line build
1007 setDroidTarget(psDroid, nullptr);
1008 setDroidActionTarget(psDroid, nullptr, 0);
1009 }
1010 return false;
1011 }
1012
1013 // make sure we still 'own' the building in question
1014 if (!aiCheckAlliances(psStruct->player, psDroid->player))
1015 {
1016 cancelBuild(psDroid); // stop what you are doing fool it isn't ours anymore!
1017 return false;
1018 }
1019
1020 unsigned constructPoints = constructorPoints(asConstructStats + psDroid->
1021 asBits[COMP_CONSTRUCT], psDroid->player);
1022
1023 unsigned pointsToAdd = constructPoints * (gameTime - psDroid->actionStarted) /
1024 GAME_TICKS_PER_SEC;
1025
1026 structureBuild(psStruct, psDroid, pointsToAdd - psDroid->actionPoints, constructPoints);
1027
1028 //store the amount just added
1029 psDroid->actionPoints = pointsToAdd;
1030
1031 addConstructorEffect(psStruct);
1032
1033 return true;
1034 }
1035
droidUpdateDemolishing(DROID * psDroid)1036 bool droidUpdateDemolishing(DROID *psDroid)
1037 {
1038 CHECK_DROID(psDroid);
1039
1040 ASSERT_OR_RETURN(false, psDroid->action == DACTION_DEMOLISH, "unit is not demolishing");
1041 STRUCTURE *psStruct = (STRUCTURE *)psDroid->order.psObj;
1042 ASSERT_OR_RETURN(false, psStruct->type == OBJ_STRUCTURE, "target is not a structure");
1043
1044 int constructRate = 5 * constructorPoints(asConstructStats + psDroid->asBits[COMP_CONSTRUCT], psDroid->player);
1045 int pointsToAdd = gameTimeAdjustedAverage(constructRate);
1046
1047 structureDemolish(psStruct, psDroid, pointsToAdd);
1048
1049 addConstructorEffect(psStruct);
1050
1051 CHECK_DROID(psDroid);
1052
1053 return true;
1054 }
1055
droidStartAction(DROID * psDroid)1056 void droidStartAction(DROID *psDroid)
1057 {
1058 psDroid->actionStarted = gameTime;
1059 psDroid->actionPoints = 0;
1060 }
1061
1062 /*continue restoring a structure*/
droidUpdateRestore(DROID * psDroid)1063 bool droidUpdateRestore(DROID *psDroid)
1064 {
1065 CHECK_DROID(psDroid);
1066
1067 ASSERT_OR_RETURN(false, psDroid->action == DACTION_RESTORE, "Unit is not restoring");
1068 STRUCTURE *psStruct = (STRUCTURE *)psDroid->order.psObj;
1069 ASSERT_OR_RETURN(false, psStruct->type == OBJ_STRUCTURE, "Target is not a structure");
1070 ASSERT_OR_RETURN(false, psDroid->asWeaps[0].nStat > 0, "Droid doesn't have any weapons");
1071
1072 unsigned compIndex = psDroid->asWeaps[0].nStat;
1073 ASSERT_OR_RETURN(false, compIndex < numWeaponStats, "Invalid range referenced for numWeaponStats, %u > %u", compIndex, numWeaponStats);
1074 WEAPON_STATS *psStats = &asWeaponStats[compIndex];
1075
1076 ASSERT_OR_RETURN(false, psStats->weaponSubClass == WSC_ELECTRONIC, "unit's weapon is not EW");
1077
1078 unsigned restorePoints = calcDamage(weaponDamage(psStats, psDroid->player),
1079 psStats->weaponEffect, (BASE_OBJECT *)psStruct);
1080
1081 unsigned pointsToAdd = restorePoints * (gameTime - psDroid->actionStarted) /
1082 GAME_TICKS_PER_SEC;
1083
1084 psStruct->resistance = (SWORD)(psStruct->resistance + (pointsToAdd - psDroid->actionPoints));
1085
1086 //store the amount just added
1087 psDroid->actionPoints = pointsToAdd;
1088
1089 CHECK_DROID(psDroid);
1090
1091 /* check if structure is restored */
1092 if (psStruct->resistance < (SDWORD)structureResistance(psStruct->
1093 pStructureType, psStruct->player))
1094 {
1095 return true;
1096 }
1097 else
1098 {
1099 addConsoleMessage(_("Structure Restored") , DEFAULT_JUSTIFY, SYSTEM_MESSAGE);
1100 psStruct->resistance = (UWORD)structureResistance(psStruct->pStructureType,
1101 psStruct->player);
1102 return false;
1103 }
1104 }
1105
1106 // Declared in weapondef.h.
getRecoil(WEAPON const & weapon)1107 int getRecoil(WEAPON const &weapon)
1108 {
1109 if (weapon.nStat != 0)
1110 {
1111 // We have a weapon.
1112 if (graphicsTime >= weapon.lastFired && graphicsTime < weapon.lastFired + DEFAULT_RECOIL_TIME)
1113 {
1114 int recoilTime = graphicsTime - weapon.lastFired;
1115 int recoilAmount = DEFAULT_RECOIL_TIME / 2 - abs(recoilTime - DEFAULT_RECOIL_TIME / 2);
1116 int maxRecoil = asWeaponStats[weapon.nStat].recoilValue; // Max recoil is 1/10 of this value.
1117 return maxRecoil * recoilAmount / (DEFAULT_RECOIL_TIME / 2 * 10);
1118 }
1119 // Recoil effect is over.
1120 }
1121 return 0;
1122 }
1123
1124
droidUpdateRepair(DROID * psDroid)1125 bool droidUpdateRepair(DROID *psDroid)
1126 {
1127 CHECK_DROID(psDroid);
1128
1129 ASSERT_OR_RETURN(false, psDroid->action == DACTION_REPAIR, "unit does not have repair order");
1130 STRUCTURE *psStruct = (STRUCTURE *)psDroid->psActionTarget[0];
1131
1132 ASSERT_OR_RETURN(false, psStruct->type == OBJ_STRUCTURE, "target is not a structure");
1133 int iRepairRate = constructorPoints(asConstructStats + psDroid->asBits[COMP_CONSTRUCT], psDroid->player);
1134
1135 /* add points to structure */
1136 structureRepair(psStruct, psDroid, iRepairRate);
1137
1138 /* if not finished repair return true else complete repair and return false */
1139 if (psStruct->body < structureBody(psStruct))
1140 {
1141 return true;
1142 }
1143 else
1144 {
1145 objTrace(psDroid->id, "Repaired of %s all done with %u", objInfo(psStruct), iRepairRate);
1146 return false;
1147 }
1148 }
1149
1150 /*Updates a Repair Droid working on a damaged droid*/
droidUpdateDroidRepairBase(DROID * psRepairDroid,DROID * psDroidToRepair)1151 static bool droidUpdateDroidRepairBase(DROID *psRepairDroid, DROID *psDroidToRepair)
1152 {
1153 CHECK_DROID(psRepairDroid);
1154
1155 int iRepairRateNumerator = repairPoints(asRepairStats + psRepairDroid->asBits[COMP_REPAIRUNIT], psRepairDroid->player);
1156 int iRepairRateDenominator = 1;
1157
1158 //if self repair then add repair points depending on the time delay for the stat
1159 if (psRepairDroid == psDroidToRepair)
1160 {
1161 iRepairRateNumerator *= GAME_TICKS_PER_SEC;
1162 iRepairRateDenominator *= (asRepairStats + psRepairDroid->asBits[COMP_REPAIRUNIT])->time;
1163 }
1164
1165 int iPointsToAdd = gameTimeAdjustedAverage(iRepairRateNumerator, iRepairRateDenominator);
1166
1167 psDroidToRepair->body = clip<UDWORD>(psDroidToRepair->body + iPointsToAdd, 0, psDroidToRepair->originalBody);
1168
1169 /* add plasma repair effect whilst being repaired */
1170 if ((ONEINFIVE) && (psDroidToRepair->visible[selectedPlayer]))
1171 {
1172 Vector3i iVecEffect = (psDroidToRepair->pos + Vector3i(DROID_REPAIR_SPREAD, DROID_REPAIR_SPREAD, rand() % 8)).xzy();
1173 effectGiveAuxVar(90 + rand() % 20);
1174 addEffect(&iVecEffect, EFFECT_EXPLOSION, EXPLOSION_TYPE_LASER, false, nullptr, 0, gameTime - deltaGameTime + 1 + rand() % deltaGameTime);
1175 droidAddWeldSound(iVecEffect);
1176 }
1177
1178 CHECK_DROID(psRepairDroid);
1179
1180 /* if not finished repair return true else complete repair and return false */
1181 return psDroidToRepair->body < psDroidToRepair->originalBody;
1182 }
1183
droidUpdateDroidRepair(DROID * psRepairDroid)1184 bool droidUpdateDroidRepair(DROID *psRepairDroid)
1185 {
1186 ASSERT_OR_RETURN(false, psRepairDroid->action == DACTION_DROIDREPAIR, "Unit does not have unit repair order");
1187 ASSERT_OR_RETURN(false, psRepairDroid->asBits[COMP_REPAIRUNIT] != 0, "Unit does not have a repair turret");
1188
1189 DROID *psDroidToRepair = (DROID *)psRepairDroid->psActionTarget[0];
1190 ASSERT_OR_RETURN(false, psDroidToRepair->type == OBJ_DROID, "Target is not a unit");
1191
1192 return droidUpdateDroidRepairBase(psRepairDroid, psDroidToRepair);
1193 }
1194
droidUpdateDroidSelfRepair(DROID * psRepairDroid)1195 static void droidUpdateDroidSelfRepair(DROID *psRepairDroid)
1196 {
1197 droidUpdateDroidRepairBase(psRepairDroid, psRepairDroid);
1198 }
1199
1200 // return whether a droid is IDF
idfDroid(DROID * psDroid)1201 bool idfDroid(DROID *psDroid)
1202 {
1203 //add Cyborgs
1204 //if (psDroid->droidType != DROID_WEAPON)
1205 if (!(psDroid->droidType == DROID_WEAPON || psDroid->droidType == DROID_CYBORG ||
1206 psDroid->droidType == DROID_CYBORG_SUPER))
1207 {
1208 return false;
1209 }
1210
1211 return !proj_Direct(psDroid->asWeaps[0].nStat + asWeaponStats);
1212 }
1213
1214 /* Return the type of a droid */
droidType(DROID * psDroid)1215 DROID_TYPE droidType(DROID *psDroid)
1216 {
1217 return psDroid->droidType;
1218 }
1219
1220 /* Return the type of a droid from it's template */
droidTemplateType(const DROID_TEMPLATE * psTemplate)1221 DROID_TYPE droidTemplateType(const DROID_TEMPLATE *psTemplate)
1222 {
1223 DROID_TYPE type = DROID_DEFAULT;
1224
1225 if (psTemplate->droidType == DROID_PERSON ||
1226 psTemplate->droidType == DROID_CYBORG ||
1227 psTemplate->droidType == DROID_CYBORG_SUPER ||
1228 psTemplate->droidType == DROID_CYBORG_CONSTRUCT ||
1229 psTemplate->droidType == DROID_CYBORG_REPAIR ||
1230 psTemplate->droidType == DROID_TRANSPORTER ||
1231 psTemplate->droidType == DROID_SUPERTRANSPORTER)
1232 {
1233 type = psTemplate->droidType;
1234 }
1235 else if (psTemplate->asParts[COMP_BRAIN] != 0)
1236 {
1237 type = DROID_COMMAND;
1238 }
1239 else if ((asSensorStats + psTemplate->asParts[COMP_SENSOR])->location == LOC_TURRET)
1240 {
1241 type = DROID_SENSOR;
1242 }
1243 else if ((asECMStats + psTemplate->asParts[COMP_ECM])->location == LOC_TURRET)
1244 {
1245 type = DROID_ECM;
1246 }
1247 else if (psTemplate->asParts[COMP_CONSTRUCT] != 0)
1248 {
1249 type = DROID_CONSTRUCT;
1250 }
1251 else if ((asRepairStats + psTemplate->asParts[COMP_REPAIRUNIT])->location == LOC_TURRET)
1252 {
1253 type = DROID_REPAIR;
1254 }
1255 else if (psTemplate->asWeaps[0] != 0)
1256 {
1257 type = DROID_WEAPON;
1258 }
1259 /* with more than weapon is still a DROID_WEAPON */
1260 else if (psTemplate->numWeaps > 1)
1261 {
1262 type = DROID_WEAPON;
1263 }
1264
1265 return type;
1266 }
1267
1268 template <typename F, typename G>
calcSum(const uint8_t (& asParts)[DROID_MAXCOMP],int numWeaps,const uint32_t (& asWeaps)[MAX_WEAPONS],F func,G propulsionFunc)1269 static unsigned calcSum(const uint8_t (&asParts)[DROID_MAXCOMP], int numWeaps, const uint32_t (&asWeaps)[MAX_WEAPONS], F func, G propulsionFunc)
1270 {
1271 unsigned sum =
1272 func(asBrainStats [asParts[COMP_BRAIN]]) +
1273 func(asSensorStats [asParts[COMP_SENSOR]]) +
1274 func(asECMStats [asParts[COMP_ECM]]) +
1275 func(asRepairStats [asParts[COMP_REPAIRUNIT]]) +
1276 func(asConstructStats[asParts[COMP_CONSTRUCT]]) +
1277 propulsionFunc(asBodyStats[asParts[COMP_BODY]], asPropulsionStats[asParts[COMP_PROPULSION]]);
1278 for (int i = 0; i < numWeaps; ++i)
1279 {
1280 sum += func(asWeaponStats[asWeaps[i]]);
1281 }
1282 return sum;
1283 }
1284
1285 template <typename F, typename G>
calcUpgradeSum(const uint8_t (& asParts)[DROID_MAXCOMP],int numWeaps,const uint32_t (& asWeaps)[MAX_WEAPONS],int player,F func,G propulsionFunc)1286 static unsigned calcUpgradeSum(const uint8_t (&asParts)[DROID_MAXCOMP], int numWeaps, const uint32_t (&asWeaps)[MAX_WEAPONS], int player, F func, G propulsionFunc)
1287 {
1288 unsigned sum =
1289 func(asBrainStats [asParts[COMP_BRAIN]].upgrade[player]) +
1290 func(asSensorStats [asParts[COMP_SENSOR]].upgrade[player]) +
1291 func(asECMStats [asParts[COMP_ECM]].upgrade[player]) +
1292 func(asRepairStats [asParts[COMP_REPAIRUNIT]].upgrade[player]) +
1293 func(asConstructStats[asParts[COMP_CONSTRUCT]].upgrade[player]) +
1294 propulsionFunc(asBodyStats[asParts[COMP_BODY]].upgrade[player], asPropulsionStats[asParts[COMP_PROPULSION]].upgrade[player]);
1295 for (int i = 0; i < numWeaps; ++i)
1296 {
1297 // asWeaps[i] > 0 check only needed for droids, not templates.
1298 if (asWeaps[i] > 0)
1299 {
1300 sum += func(asWeaponStats[asWeaps[i]].upgrade[player]);
1301 }
1302 }
1303 return sum;
1304 }
1305
1306 struct FilterDroidWeaps
1307 {
FilterDroidWeapsFilterDroidWeaps1308 FilterDroidWeaps(unsigned numWeaps, const WEAPON (&asWeaps)[MAX_WEAPONS])
1309 {
1310 std::transform(asWeaps, asWeaps + numWeaps, this->asWeaps, [](const WEAPON &weap) {
1311 return weap.nStat;
1312 });
1313 this->numWeaps = std::remove_if(this->asWeaps, this->asWeaps + numWeaps, [](uint32_t stat) {
1314 return stat == 0;
1315 }) - this->asWeaps;
1316 }
1317
1318 unsigned numWeaps;
1319 uint32_t asWeaps[MAX_WEAPONS];
1320 };
1321
1322 template <typename F, typename G>
calcSum(const DROID_TEMPLATE * psTemplate,F func,G propulsionFunc)1323 static unsigned calcSum(const DROID_TEMPLATE *psTemplate, F func, G propulsionFunc)
1324 {
1325 return calcSum(psTemplate->asParts, psTemplate->numWeaps, psTemplate->asWeaps, func, propulsionFunc);
1326 }
1327
1328 template <typename F, typename G>
calcSum(const DROID * psDroid,F func,G propulsionFunc)1329 static unsigned calcSum(const DROID *psDroid, F func, G propulsionFunc)
1330 {
1331 FilterDroidWeaps f = {psDroid->numWeaps, psDroid->asWeaps};
1332 return calcSum(psDroid->asBits, f.numWeaps, f.asWeaps, func, propulsionFunc);
1333 }
1334
1335 template <typename F, typename G>
calcUpgradeSum(const DROID_TEMPLATE * psTemplate,int player,F func,G propulsionFunc)1336 static unsigned calcUpgradeSum(const DROID_TEMPLATE *psTemplate, int player, F func, G propulsionFunc)
1337 {
1338 return calcUpgradeSum(psTemplate->asParts, psTemplate->numWeaps, psTemplate->asWeaps, player, func, propulsionFunc);
1339 }
1340
1341 template <typename F, typename G>
calcUpgradeSum(const DROID * psDroid,int player,F func,G propulsionFunc)1342 static unsigned calcUpgradeSum(const DROID *psDroid, int player, F func, G propulsionFunc)
1343 {
1344 FilterDroidWeaps f = {psDroid->numWeaps, psDroid->asWeaps};
1345 return calcUpgradeSum(psDroid->asBits, f.numWeaps, f.asWeaps, player, func, propulsionFunc);
1346 }
1347
1348 /* Calculate the weight of a droid from it's template */
calcDroidWeight(const DROID_TEMPLATE * psTemplate)1349 UDWORD calcDroidWeight(const DROID_TEMPLATE *psTemplate)
1350 {
1351 return calcSum(psTemplate, [](COMPONENT_STATS const &stat) {
1352 return stat.weight;
1353 }, [](BODY_STATS const &bodyStat, PROPULSION_STATS const &propStat) {
1354 // Propulsion weight is a percentage of the body weight.
1355 return bodyStat.weight * (100 + propStat.weight) / 100;
1356 });
1357 }
1358
1359 template <typename T>
calcBody(T * obj,int player)1360 static uint32_t calcBody(T *obj, int player)
1361 {
1362 int hitpoints = calcUpgradeSum(obj, player, [](COMPONENT_STATS::UPGRADE const &upgrade) {
1363 return upgrade.hitpoints;
1364 }, [](BODY_STATS::UPGRADE const &bodyUpgrade, PROPULSION_STATS::UPGRADE const &propUpgrade) {
1365 // propulsion hitpoints can be a percentage of the body's hitpoints
1366 return bodyUpgrade.hitpoints * (100 + propUpgrade.hitpointPctOfBody) / 100 + propUpgrade.hitpoints;
1367 });
1368
1369 int hitpointPct = calcUpgradeSum(obj, player, [](COMPONENT_STATS::UPGRADE const &upgrade) {
1370 return upgrade.hitpointPct - 100;
1371 }, [](BODY_STATS::UPGRADE const &bodyUpgrade, PROPULSION_STATS::UPGRADE const &propUpgrade) {
1372 return bodyUpgrade.hitpointPct - 100 + propUpgrade.hitpointPct - 100;
1373 });
1374
1375 // Final adjustment based on the hitpoint modifier
1376 return hitpoints * (100 + hitpointPct) / 100;
1377 }
1378
1379 // Calculate the body points of a droid from its template
calcTemplateBody(const DROID_TEMPLATE * psTemplate,UBYTE player)1380 UDWORD calcTemplateBody(const DROID_TEMPLATE *psTemplate, UBYTE player)
1381 {
1382 if (psTemplate == nullptr)
1383 {
1384 ASSERT(false, "null template");
1385 return 0;
1386 }
1387
1388 return calcBody(psTemplate, player);
1389 }
1390
1391 // Calculate the base body points of a droid with upgrades
calcDroidBaseBody(DROID * psDroid)1392 static UDWORD calcDroidBaseBody(DROID *psDroid)
1393 {
1394 return calcBody(psDroid, psDroid->player);
1395 }
1396
1397
1398 /* Calculate the base speed of a droid from it's template */
calcDroidBaseSpeed(const DROID_TEMPLATE * psTemplate,UDWORD weight,UBYTE player)1399 UDWORD calcDroidBaseSpeed(const DROID_TEMPLATE *psTemplate, UDWORD weight, UBYTE player)
1400 {
1401 unsigned speed = asPropulsionTypes[asPropulsionStats[psTemplate->asParts[COMP_PROPULSION]].propulsionType].powerRatioMult *
1402 bodyPower(&asBodyStats[psTemplate->asParts[COMP_BODY]], player) / MAX(1, weight);
1403
1404 // reduce the speed of medium/heavy VTOLs
1405 if (asPropulsionStats[psTemplate->asParts[COMP_PROPULSION]].propulsionType == PROPULSION_TYPE_LIFT)
1406 {
1407 if (asBodyStats[psTemplate->asParts[COMP_BODY]].size == SIZE_HEAVY)
1408 {
1409 speed /= 4;
1410 }
1411 else if (asBodyStats[psTemplate->asParts[COMP_BODY]].size == SIZE_MEDIUM)
1412 {
1413 speed = speed * 3 / 4;
1414 }
1415 }
1416
1417 // applies the engine output bonus if output > weight
1418 if (asBodyStats[psTemplate->asParts[COMP_BODY]].base.power > weight)
1419 {
1420 speed = speed * 3 / 2;
1421 }
1422
1423 return speed;
1424 }
1425
1426
1427 /* Calculate the speed of a droid over a terrain */
calcDroidSpeed(UDWORD baseSpeed,UDWORD terrainType,UDWORD propIndex,UDWORD level)1428 UDWORD calcDroidSpeed(UDWORD baseSpeed, UDWORD terrainType, UDWORD propIndex, UDWORD level)
1429 {
1430 PROPULSION_STATS const &propulsion = asPropulsionStats[propIndex];
1431
1432 // Factor in terrain
1433 unsigned speed = baseSpeed * getSpeedFactor(terrainType, propulsion.propulsionType) / 100;
1434
1435 // Need to ensure doesn't go over the max speed possible for this propulsion
1436 speed = std::min(speed, propulsion.maxSpeed);
1437
1438 // Factor in experience
1439 speed *= 100 + EXP_SPEED_BONUS * level;
1440 speed /= 100;
1441
1442 return speed;
1443 }
1444
1445 template <typename T>
calcBuild(T * obj)1446 static uint32_t calcBuild(T *obj)
1447 {
1448 return calcSum(obj, [](COMPONENT_STATS const &stat) {
1449 return stat.buildPoints;
1450 }, [](BODY_STATS const &bodyStat, PROPULSION_STATS const &propStat) {
1451 // Propulsion power points are a percentage of the body's build points.
1452 return bodyStat.buildPoints * (100 + propStat.buildPoints) / 100;
1453 });
1454 }
1455
1456 /* Calculate the points required to build the template - used to calculate time*/
calcTemplateBuild(const DROID_TEMPLATE * psTemplate)1457 UDWORD calcTemplateBuild(const DROID_TEMPLATE *psTemplate)
1458 {
1459 return calcBuild(psTemplate);
1460 }
1461
calcDroidPoints(DROID * psDroid)1462 UDWORD calcDroidPoints(DROID *psDroid)
1463 {
1464 return calcBuild(psDroid);
1465 }
1466
1467 template <typename T>
calcPower(const T * obj)1468 static uint32_t calcPower(const T *obj)
1469 {
1470 ASSERT_NOT_NULLPTR_OR_RETURN(0, obj);
1471 return calcSum(obj, [](COMPONENT_STATS const &stat) {
1472 return stat.buildPower;
1473 }, [](BODY_STATS const &bodyStat, PROPULSION_STATS const &propStat) {
1474 // Propulsion power points are a percentage of the body's power points.
1475 return bodyStat.buildPower * (100 + propStat.buildPower) / 100;
1476 });
1477 }
1478
1479 /* Calculate the power points required to build/maintain a template */
calcTemplatePower(const DROID_TEMPLATE * psTemplate)1480 UDWORD calcTemplatePower(const DROID_TEMPLATE *psTemplate)
1481 {
1482 return calcPower(psTemplate);
1483 }
1484
1485
1486 /* Calculate the power points required to build/maintain a droid */
calcDroidPower(const DROID * psDroid)1487 UDWORD calcDroidPower(const DROID *psDroid)
1488 {
1489 return calcPower(psDroid);
1490 }
1491
1492 //Builds an instance of a Droid - the x/y passed in are in world coords.
reallyBuildDroid(const DROID_TEMPLATE * pTemplate,Position pos,UDWORD player,bool onMission,Rotation rot)1493 DROID *reallyBuildDroid(const DROID_TEMPLATE *pTemplate, Position pos, UDWORD player, bool onMission, Rotation rot)
1494 {
1495 // Don't use this assertion in single player, since droids can finish building while on an away mission
1496 ASSERT(!bMultiPlayer || worldOnMap(pos.x, pos.y), "the build locations are not on the map");
1497
1498 DROID *psDroid = new DROID(generateSynchronisedObjectId(), player);
1499 droidSetName(psDroid, getStatsName(pTemplate));
1500
1501 // Set the droids type
1502 psDroid->droidType = droidTemplateType(pTemplate); // Is set again later to the same thing, in droidSetBits.
1503 psDroid->pos = pos;
1504 psDroid->rot = rot;
1505
1506 //don't worry if not on homebase cos not being drawn yet
1507 if (!onMission)
1508 {
1509 //set droid height
1510 psDroid->pos.z = map_Height(psDroid->pos.x, psDroid->pos.y);
1511 }
1512
1513 if (isTransporter(psDroid) || psDroid->droidType == DROID_COMMAND)
1514 {
1515 DROID_GROUP *psGrp = grpCreate();
1516 psGrp->add(psDroid);
1517 }
1518
1519 // find the highest stored experience
1520 // Unless game time is stopped, then we're hopefully loading a game and
1521 // don't want to use up recycled experience for the droids we just loaded.
1522 if (!gameTimeIsStopped() &&
1523 (psDroid->droidType != DROID_CONSTRUCT) &&
1524 (psDroid->droidType != DROID_CYBORG_CONSTRUCT) &&
1525 (psDroid->droidType != DROID_REPAIR) &&
1526 (psDroid->droidType != DROID_CYBORG_REPAIR) &&
1527 !isTransporter(psDroid) &&
1528 !recycled_experience[psDroid->player].empty())
1529 {
1530 psDroid->experience = recycled_experience[psDroid->player].top();
1531 recycled_experience[psDroid->player].pop();
1532 }
1533 else
1534 {
1535 psDroid->experience = 0;
1536 }
1537 psDroid->kills = 0;
1538
1539 droidSetBits(pTemplate, psDroid);
1540
1541 //calculate the droids total weight
1542 psDroid->weight = calcDroidWeight(pTemplate);
1543
1544 // Initialise the movement stuff
1545 psDroid->baseSpeed = calcDroidBaseSpeed(pTemplate, psDroid->weight, (UBYTE)player);
1546
1547 initDroidMovement(psDroid);
1548
1549 //allocate 'easy-access' data!
1550 psDroid->body = calcDroidBaseBody(psDroid); // includes upgrades
1551 ASSERT(psDroid->body > 0, "Invalid number of hitpoints");
1552 psDroid->originalBody = psDroid->body;
1553
1554 /* Set droid's initial illumination */
1555 psDroid->sDisplay.imd = BODY_IMD(psDroid, psDroid->player);
1556
1557 //don't worry if not on homebase cos not being drawn yet
1558 if (!onMission)
1559 {
1560 /* People always stand upright */
1561 if (psDroid->droidType != DROID_PERSON)
1562 {
1563 updateDroidOrientation(psDroid);
1564 }
1565 visTilesUpdate(psDroid);
1566 }
1567
1568 /* transporter-specific stuff */
1569 if (isTransporter(psDroid))
1570 {
1571 //add transporter launch button if selected player and not a reinforcable situation
1572 if (player == selectedPlayer && !missionCanReEnforce())
1573 {
1574 (void)intAddTransporterLaunch(psDroid);
1575 }
1576
1577 //set droid height to be above the terrain
1578 psDroid->pos.z += TRANSPORTER_HOVER_HEIGHT;
1579
1580 /* reset halt secondary order from guard to hold */
1581 secondarySetState(psDroid, DSO_HALTTYPE, DSS_HALT_HOLD);
1582 }
1583
1584 if (player == selectedPlayer)
1585 {
1586 scoreUpdateVar(WD_UNITS_BUILT);
1587 }
1588
1589 // Avoid droid appearing to jump or turn on spawn.
1590 psDroid->prevSpacetime.pos = psDroid->pos;
1591 psDroid->prevSpacetime.rot = psDroid->rot;
1592
1593 debug(LOG_LIFE, "created droid for player %d, droid = %p, id=%d (%s): position: x(%d)y(%d)z(%d)", player, static_cast<void *>(psDroid), (int)psDroid->id, psDroid->aName, psDroid->pos.x, psDroid->pos.y, psDroid->pos.z);
1594
1595 return psDroid;
1596 }
1597
buildDroid(DROID_TEMPLATE * pTemplate,UDWORD x,UDWORD y,UDWORD player,bool onMission,const INITIAL_DROID_ORDERS * initialOrders,Rotation rot)1598 DROID *buildDroid(DROID_TEMPLATE *pTemplate, UDWORD x, UDWORD y, UDWORD player, bool onMission, const INITIAL_DROID_ORDERS *initialOrders, Rotation rot)
1599 {
1600 // ajl. droid will be created, so inform others
1601 if (bMultiMessages)
1602 {
1603 // Only sends if it's ours, otherwise the owner should send the message.
1604 SendDroid(pTemplate, x, y, player, generateNewObjectId(), initialOrders);
1605 return nullptr;
1606 }
1607 else
1608 {
1609 return reallyBuildDroid(pTemplate, Position(x, y, 0), player, onMission, rot);
1610 }
1611 }
1612
1613 //initialises the droid movement model
initDroidMovement(DROID * psDroid)1614 void initDroidMovement(DROID *psDroid)
1615 {
1616 psDroid->sMove.asPath.clear();
1617 psDroid->sMove.pathIndex = 0;
1618 }
1619
1620 // Set the asBits in a DROID structure given it's template.
droidSetBits(const DROID_TEMPLATE * pTemplate,DROID * psDroid)1621 void droidSetBits(const DROID_TEMPLATE *pTemplate, DROID *psDroid)
1622 {
1623 psDroid->droidType = droidTemplateType(pTemplate);
1624 psDroid->numWeaps = pTemplate->numWeaps;
1625 psDroid->body = calcTemplateBody(pTemplate, psDroid->player);
1626 psDroid->originalBody = psDroid->body;
1627 psDroid->expectedDamageDirect = 0; // Begin life optimistically.
1628 psDroid->expectedDamageIndirect = 0; // Begin life optimistically.
1629 psDroid->time = gameTime - deltaGameTime + 1; // Start at beginning of tick.
1630 psDroid->prevSpacetime.time = psDroid->time - 1; // -1 for interpolation.
1631
1632 //create the droids weapons
1633 for (int inc = 0; inc < MAX_WEAPONS; inc++)
1634 {
1635 psDroid->psActionTarget[inc] = nullptr;
1636 psDroid->asWeaps[inc].lastFired = 0;
1637 psDroid->asWeaps[inc].shotsFired = 0;
1638 // no weapon (could be a construction droid for example)
1639 // this is also used to check if a droid has a weapon, so zero it
1640 psDroid->asWeaps[inc].nStat = 0;
1641 psDroid->asWeaps[inc].ammo = 0;
1642 psDroid->asWeaps[inc].rot.direction = 0;
1643 psDroid->asWeaps[inc].rot.pitch = 0;
1644 psDroid->asWeaps[inc].rot.roll = 0;
1645 psDroid->asWeaps[inc].prevRot = psDroid->asWeaps[inc].rot;
1646 psDroid->asWeaps[inc].origin = ORIGIN_UNKNOWN;
1647 if (inc < pTemplate->numWeaps)
1648 {
1649 psDroid->asWeaps[inc].nStat = pTemplate->asWeaps[inc];
1650 psDroid->asWeaps[inc].ammo = (asWeaponStats + psDroid->asWeaps[inc].nStat)->upgrade[psDroid->player].numRounds;
1651 }
1652 psDroid->asWeaps[inc].usedAmmo = 0;
1653 }
1654 memcpy(psDroid->asBits, pTemplate->asParts, sizeof(psDroid->asBits));
1655
1656 switch (getPropulsionStats(psDroid)->propulsionType) // getPropulsionStats(psDroid) only defined after psDroid->asBits[COMP_PROPULSION] is set.
1657 {
1658 case PROPULSION_TYPE_LIFT:
1659 psDroid->blockedBits = AIR_BLOCKED;
1660 break;
1661 case PROPULSION_TYPE_HOVER:
1662 psDroid->blockedBits = FEATURE_BLOCKED;
1663 break;
1664 case PROPULSION_TYPE_PROPELLOR:
1665 psDroid->blockedBits = FEATURE_BLOCKED | LAND_BLOCKED;
1666 break;
1667 default:
1668 psDroid->blockedBits = FEATURE_BLOCKED | WATER_BLOCKED;
1669 break;
1670 }
1671 }
1672
1673
1674 // Sets the parts array in a template given a droid.
templateSetParts(const DROID * psDroid,DROID_TEMPLATE * psTemplate)1675 void templateSetParts(const DROID *psDroid, DROID_TEMPLATE *psTemplate)
1676 {
1677 psTemplate->numWeaps = 0;
1678 psTemplate->droidType = psDroid->droidType;
1679 for (int inc = 0; inc < MAX_WEAPONS; inc++)
1680 {
1681 //this should fix the NULL weapon stats for empty weaponslots
1682 psTemplate->asWeaps[inc] = 0;
1683 if (psDroid->asWeaps[inc].nStat > 0)
1684 {
1685 psTemplate->numWeaps += 1;
1686 psTemplate->asWeaps[inc] = psDroid->asWeaps[inc].nStat;
1687 }
1688 }
1689 memcpy(psTemplate->asParts, psDroid->asBits, sizeof(psDroid->asBits));
1690 }
1691
1692 /* Make all the droids for a certain player a member of a specific group */
assignDroidsToGroup(UDWORD playerNumber,UDWORD groupNumber,bool clearGroup)1693 void assignDroidsToGroup(UDWORD playerNumber, UDWORD groupNumber, bool clearGroup)
1694 {
1695 DROID *psDroid;
1696 bool bAtLeastOne = false;
1697 FLAG_POSITION *psFlagPos;
1698
1699 if (groupNumber < UBYTE_MAX)
1700 {
1701 /* Run through all the droids */
1702 for (psDroid = apsDroidLists[playerNumber]; psDroid != nullptr; psDroid = psDroid->psNext)
1703 {
1704 /* Clear out the old ones */
1705 if (clearGroup && psDroid->group == groupNumber)
1706 {
1707 psDroid->group = UBYTE_MAX;
1708 }
1709
1710 /* Only assign the currently selected ones */
1711 if (psDroid->selected)
1712 {
1713 /* Set them to the right group - they can only be a member of one group */
1714 psDroid->group = (UBYTE)groupNumber;
1715 bAtLeastOne = true;
1716 }
1717 }
1718 }
1719 if (bAtLeastOne)
1720 {
1721 //clear the Deliv Point if one
1722 for (psFlagPos = apsFlagPosLists[selectedPlayer]; psFlagPos;
1723 psFlagPos = psFlagPos->psNext)
1724 {
1725 psFlagPos->selected = false;
1726 }
1727 groupConsoleInformOfCreation(groupNumber);
1728 secondarySetAverageGroupState(selectedPlayer, groupNumber);
1729 }
1730 }
1731
1732
activateGroupAndMove(UDWORD playerNumber,UDWORD groupNumber)1733 bool activateGroupAndMove(UDWORD playerNumber, UDWORD groupNumber)
1734 {
1735 DROID *psDroid, *psCentreDroid = nullptr;
1736 bool selected = false;
1737 FLAG_POSITION *psFlagPos;
1738
1739 if (groupNumber < UBYTE_MAX)
1740 {
1741 for (psDroid = apsDroidLists[playerNumber]; psDroid != nullptr; psDroid = psDroid->psNext)
1742 {
1743 /* Wipe out the ones in the wrong group */
1744 if (psDroid->selected && psDroid->group != groupNumber)
1745 {
1746 DeSelectDroid(psDroid);
1747 }
1748 /* Get the right ones */
1749 if (psDroid->group == groupNumber)
1750 {
1751 SelectDroid(psDroid);
1752 psCentreDroid = psDroid;
1753 }
1754 }
1755
1756 /* There was at least one in the group */
1757 if (psCentreDroid)
1758 {
1759 //clear the Deliv Point if one
1760 for (psFlagPos = apsFlagPosLists[selectedPlayer]; psFlagPos;
1761 psFlagPos = psFlagPos->psNext)
1762 {
1763 psFlagPos->selected = false;
1764 }
1765
1766 selected = true;
1767 if (getWarCamStatus())
1768 {
1769 camToggleStatus(); // messy - fix this
1770 processWarCam(); //odd, but necessary
1771 camToggleStatus(); // messy - FIXME
1772 }
1773 else
1774 {
1775 /* Centre display on him if warcam isn't active */
1776 setViewPos(map_coord(psCentreDroid->pos.x), map_coord(psCentreDroid->pos.y), true);
1777 }
1778 }
1779 }
1780
1781 if (selected)
1782 {
1783 groupConsoleInformOfCentering(groupNumber);
1784 }
1785
1786 return selected;
1787 }
1788
activateGroup(UDWORD playerNumber,UDWORD groupNumber)1789 bool activateGroup(UDWORD playerNumber, UDWORD groupNumber)
1790 {
1791 DROID *psDroid;
1792 bool selected = false;
1793 FLAG_POSITION *psFlagPos;
1794
1795 if (groupNumber < UBYTE_MAX)
1796 {
1797 for (psDroid = apsDroidLists[playerNumber]; psDroid; psDroid = psDroid->psNext)
1798 {
1799 /* Wipe out the ones in the wrong group */
1800 if (psDroid->selected && psDroid->group != groupNumber)
1801 {
1802 DeSelectDroid(psDroid);
1803 }
1804 /* Get the right ones */
1805 if (psDroid->group == groupNumber)
1806 {
1807 SelectDroid(psDroid);
1808 selected = true;
1809 }
1810 }
1811 }
1812
1813 if (selected)
1814 {
1815 //clear the Deliv Point if one
1816 for (psFlagPos = apsFlagPosLists[selectedPlayer]; psFlagPos;
1817 psFlagPos = psFlagPos->psNext)
1818 {
1819 psFlagPos->selected = false;
1820 }
1821 groupConsoleInformOfSelection(groupNumber);
1822 }
1823 return selected;
1824 }
1825
groupConsoleInformOfSelection(UDWORD groupNumber)1826 void groupConsoleInformOfSelection(UDWORD groupNumber)
1827 {
1828 unsigned int num_selected = selNumSelected(selectedPlayer);
1829
1830 CONPRINTF(ngettext("Group %u selected - %u Unit", "Group %u selected - %u Units", num_selected), groupNumber, num_selected);
1831 }
1832
groupConsoleInformOfCreation(UDWORD groupNumber)1833 void groupConsoleInformOfCreation(UDWORD groupNumber)
1834 {
1835 if (!getWarCamStatus())
1836 {
1837 unsigned int num_selected = selNumSelected(selectedPlayer);
1838
1839 CONPRINTF(ngettext("%u unit assigned to Group %u", "%u units assigned to Group %u", num_selected), num_selected, groupNumber);
1840 }
1841
1842 }
1843
groupConsoleInformOfCentering(UDWORD groupNumber)1844 void groupConsoleInformOfCentering(UDWORD groupNumber)
1845 {
1846 unsigned int num_selected = selNumSelected(selectedPlayer);
1847
1848 if (!getWarCamStatus())
1849 {
1850 CONPRINTF(ngettext("Centered on Group %u - %u Unit", "Centered on Group %u - %u Units", num_selected), groupNumber, num_selected);
1851 }
1852 else
1853 {
1854 CONPRINTF(ngettext("Aligning with Group %u - %u Unit", "Aligning with Group %u - %u Units", num_selected), groupNumber, num_selected);
1855 }
1856 }
1857
1858 /**
1859 * calculate muzzle base location in 3d world
1860 */
calcDroidMuzzleBaseLocation(const DROID * psDroid,Vector3i * muzzle,int weapon_slot)1861 bool calcDroidMuzzleBaseLocation(const DROID *psDroid, Vector3i *muzzle, int weapon_slot)
1862 {
1863 const iIMDShape *psBodyImd = BODY_IMD(psDroid, psDroid->player);
1864
1865 CHECK_DROID(psDroid);
1866
1867 if (psBodyImd && psBodyImd->nconnectors)
1868 {
1869 Vector3i barrel(0, 0, 0);
1870
1871 Affine3F af;
1872
1873 af.Trans(psDroid->pos.x, -psDroid->pos.z, psDroid->pos.y);
1874
1875 //matrix = the center of droid
1876 af.RotY(psDroid->rot.direction);
1877 af.RotX(psDroid->rot.pitch);
1878 af.RotZ(-psDroid->rot.roll);
1879 af.Trans(psBodyImd->connectors[weapon_slot].x, -psBodyImd->connectors[weapon_slot].z,
1880 -psBodyImd->connectors[weapon_slot].y);//note y and z flipped
1881
1882 *muzzle = (af * barrel).xzy();
1883 muzzle->z = -muzzle->z;
1884 }
1885 else
1886 {
1887 *muzzle = psDroid->pos + Vector3i(0, 0, psDroid->sDisplay.imd->max.y);
1888 }
1889
1890 CHECK_DROID(psDroid);
1891
1892 return true;
1893 }
1894
1895 /**
1896 * calculate muzzle tip location in 3d world
1897 */
calcDroidMuzzleLocation(const DROID * psDroid,Vector3i * muzzle,int weapon_slot)1898 bool calcDroidMuzzleLocation(const DROID *psDroid, Vector3i *muzzle, int weapon_slot)
1899 {
1900 const iIMDShape *psBodyImd = BODY_IMD(psDroid, psDroid->player);
1901
1902 CHECK_DROID(psDroid);
1903
1904 if (psBodyImd && psBodyImd->nconnectors)
1905 {
1906 char debugStr[250], debugLen = 0; // Each "(%d,%d,%d)" uses up to 34 bytes, for very large values. So 250 isn't exaggerating.
1907
1908 Vector3i barrel(0, 0, 0);
1909 const iIMDShape *psWeaponImd = nullptr, *psMountImd = nullptr;
1910
1911 if (psDroid->asWeaps[weapon_slot].nStat)
1912 {
1913 psMountImd = WEAPON_MOUNT_IMD(psDroid, weapon_slot);
1914 psWeaponImd = WEAPON_IMD(psDroid, weapon_slot);
1915 }
1916
1917 Affine3F af;
1918
1919 af.Trans(psDroid->pos.x, -psDroid->pos.z, psDroid->pos.y);
1920
1921 //matrix = the center of droid
1922 af.RotY(psDroid->rot.direction);
1923 af.RotX(psDroid->rot.pitch);
1924 af.RotZ(-psDroid->rot.roll);
1925 af.Trans(psBodyImd->connectors[weapon_slot].x, -psBodyImd->connectors[weapon_slot].z,
1926 -psBodyImd->connectors[weapon_slot].y);//note y and z flipped
1927 debugLen += sprintf(debugStr + debugLen, "connect:body[%d]=(%d,%d,%d)", weapon_slot, psBodyImd->connectors[weapon_slot].x, -psBodyImd->connectors[weapon_slot].z, -psBodyImd->connectors[weapon_slot].y);
1928
1929 //matrix = the weapon[slot] mount on the body
1930 af.RotY(psDroid->asWeaps[weapon_slot].rot.direction); // +ve anticlockwise
1931
1932 // process turret mount
1933 if (psMountImd && psMountImd->nconnectors)
1934 {
1935 af.Trans(psMountImd->connectors->x, -psMountImd->connectors->z, -psMountImd->connectors->y);
1936 debugLen += sprintf(debugStr + debugLen, ",turret=(%d,%d,%d)", psMountImd->connectors->x, -psMountImd->connectors->z, -psMountImd->connectors->y);
1937 }
1938
1939 //matrix = the turret connector for the gun
1940 af.RotX(psDroid->asWeaps[weapon_slot].rot.pitch); // +ve up
1941
1942 //process the gun
1943 if (psWeaponImd && psWeaponImd->nconnectors)
1944 {
1945 unsigned int connector_num = 0;
1946
1947 // which barrel is firing if model have multiple muzzle connectors?
1948 if (psDroid->asWeaps[weapon_slot].shotsFired && (psWeaponImd->nconnectors > 1))
1949 {
1950 // shoot first, draw later - substract one shot to get correct results
1951 connector_num = (psDroid->asWeaps[weapon_slot].shotsFired - 1) % (psWeaponImd->nconnectors);
1952 }
1953
1954 barrel = Vector3i(psWeaponImd->connectors[connector_num].x,
1955 -psWeaponImd->connectors[connector_num].z,
1956 -psWeaponImd->connectors[connector_num].y);
1957 debugLen += sprintf(debugStr + debugLen, ",barrel[%u]=(%d,%d,%d)", connector_num, psWeaponImd->connectors[connector_num].x, -psWeaponImd->connectors[connector_num].y, -psWeaponImd->connectors[connector_num].z);
1958 }
1959
1960 *muzzle = (af * barrel).xzy();
1961 muzzle->z = -muzzle->z;
1962 sprintf(debugStr + debugLen, ",muzzle=(%d,%d,%d)", muzzle->x, muzzle->y, muzzle->z);
1963
1964 syncDebug("%s", debugStr);
1965 }
1966 else
1967 {
1968 *muzzle = psDroid->pos + Vector3i(0, 0, psDroid->sDisplay.imd->max.y);
1969 }
1970
1971 CHECK_DROID(psDroid);
1972
1973 return true;
1974 }
1975
1976 struct rankMap
1977 {
1978 unsigned int kills; // required minimum amount of kills to reach this rank
1979 unsigned int commanderKills; // required minimum amount of kills for a commander (or sensor) to reach this rank
1980 const char *name; // name of this rank
1981 };
1982
getDroidLevel(const DROID * psDroid)1983 unsigned int getDroidLevel(const DROID *psDroid)
1984 {
1985 unsigned int numKills = psDroid->experience / 65536;
1986 unsigned int i;
1987
1988 // Search through the array of ranks until one is found
1989 // which requires more kills than the droid has.
1990 // Then fall back to the previous rank.
1991 const BRAIN_STATS *psStats = getBrainStats(psDroid);
1992 auto &vec = psStats->upgrade[psDroid->player].rankThresholds;
1993 for (i = 1; i < vec.size(); ++i)
1994 {
1995 if (numKills < vec.at(i))
1996 {
1997 return i - 1;
1998 }
1999 }
2000
2001 // If the criteria of the last rank are met, then select the last one
2002 return vec.size() - 1;
2003 }
2004
getDroidEffectiveLevel(const DROID * psDroid)2005 UDWORD getDroidEffectiveLevel(const DROID *psDroid)
2006 {
2007 UDWORD level = getDroidLevel(psDroid);
2008 UDWORD cmdLevel = 0;
2009
2010 // get commander level
2011 if (hasCommander(psDroid))
2012 {
2013 cmdLevel = cmdGetCommanderLevel(psDroid);
2014
2015 // Commanders boost units' effectiveness just by being assigned to it
2016 level++;
2017 }
2018
2019 return MAX(level, cmdLevel);
2020 }
2021
getDroidLevelName(const DROID * psDroid)2022 const char *getDroidLevelName(const DROID *psDroid)
2023 {
2024 const BRAIN_STATS *psStats = getBrainStats(psDroid);
2025 return PE_("rank", psStats->rankNames[getDroidLevel(psDroid)].c_str());
2026 }
2027
getNumDroidsForLevel(uint32_t player,UDWORD level)2028 UDWORD getNumDroidsForLevel(uint32_t player, UDWORD level)
2029 {
2030 DROID *psDroid;
2031 UDWORD count;
2032
2033 ASSERT_OR_RETURN(0, player < MAX_PLAYERS, "invalid player idx: %" PRIu32 "", player);
2034
2035 for (psDroid = apsDroidLists[player], count = 0;
2036 psDroid; psDroid = psDroid->psNext)
2037 {
2038 if (getDroidLevel(psDroid) == level)
2039 {
2040 count++;
2041 }
2042 }
2043
2044 return count;
2045 }
2046
2047 // Get the name of a droid from it's DROID structure.
2048 //
droidGetName(const DROID * psDroid)2049 const char *droidGetName(const DROID *psDroid)
2050 {
2051 ASSERT_NOT_NULLPTR_OR_RETURN("", psDroid);
2052 return psDroid->aName;
2053 }
2054
2055 //
2056 // Set the name of a droid in it's DROID structure.
2057 //
2058 // - only possible on the PC where you can adjust the names,
2059 //
droidSetName(DROID * psDroid,const char * pName)2060 void droidSetName(DROID *psDroid, const char *pName)
2061 {
2062 sstrcpy(psDroid->aName, pName);
2063 }
2064
2065 // ////////////////////////////////////////////////////////////////////////////
2066 // returns true when no droid on x,y square.
noDroid(UDWORD x,UDWORD y)2067 bool noDroid(UDWORD x, UDWORD y)
2068 {
2069 unsigned int i;
2070
2071 // check each droid list
2072 for (i = 0; i < MAX_PLAYERS; ++i)
2073 {
2074 const DROID *psDroid;
2075 for (psDroid = apsDroidLists[i]; psDroid; psDroid = psDroid->psNext)
2076 {
2077 if (map_coord(psDroid->pos.x) == x
2078 && map_coord(psDroid->pos.y) == y)
2079 {
2080 return false;
2081 }
2082 }
2083 }
2084 return true;
2085 }
2086
2087 // ////////////////////////////////////////////////////////////////////////////
2088 // returns true when at most one droid on x,y square.
oneDroidMax(UDWORD x,UDWORD y)2089 static bool oneDroidMax(UDWORD x, UDWORD y)
2090 {
2091 UDWORD i;
2092 bool bFound = false;
2093 DROID *pD;
2094 // check each droid list
2095 for (i = 0; i < MAX_PLAYERS; i++)
2096 {
2097 for (pD = apsDroidLists[i]; pD ; pD = pD->psNext)
2098 {
2099 if (map_coord(pD->pos.x) == x
2100 && map_coord(pD->pos.y) == y)
2101 {
2102 if (bFound)
2103 {
2104 return false;
2105 }
2106
2107 bFound = true;//first droid on this square so continue
2108 }
2109 }
2110 }
2111 return true;
2112 }
2113
2114 // ////////////////////////////////////////////////////////////////////////////
2115 // returns true if it's a sensible place to put that droid.
sensiblePlace(SDWORD x,SDWORD y,PROPULSION_TYPE propulsion)2116 static bool sensiblePlace(SDWORD x, SDWORD y, PROPULSION_TYPE propulsion)
2117 {
2118 // not too near the edges.
2119 if ((x < TOO_NEAR_EDGE) || (x > (SDWORD)(mapWidth - TOO_NEAR_EDGE)))
2120 {
2121 return false;
2122 }
2123 if ((y < TOO_NEAR_EDGE) || (y > (SDWORD)(mapHeight - TOO_NEAR_EDGE)))
2124 {
2125 return false;
2126 }
2127
2128 // not on a blocking tile.
2129 if (fpathBlockingTile(x, y, propulsion))
2130 {
2131 return false;
2132 }
2133
2134 return true;
2135 }
2136
2137 // ------------------------------------------------------------------------------------
2138 // Should stop things being placed in inaccessible areas? Assume wheeled propulsion.
zonedPAT(UDWORD x,UDWORD y)2139 bool zonedPAT(UDWORD x, UDWORD y)
2140 {
2141 return sensiblePlace(x, y, PROPULSION_TYPE_WHEELED) && noDroid(x, y);
2142 }
2143
canFitDroid(UDWORD x,UDWORD y)2144 static bool canFitDroid(UDWORD x, UDWORD y)
2145 {
2146 return sensiblePlace(x, y, PROPULSION_TYPE_WHEELED) && oneDroidMax(x, y);
2147 }
2148
2149 /// find a tile for which the function will return true
pickATileGen(UDWORD * x,UDWORD * y,UBYTE numIterations,bool (* function)(UDWORD x,UDWORD y))2150 bool pickATileGen(UDWORD *x, UDWORD *y, UBYTE numIterations,
2151 bool (*function)(UDWORD x, UDWORD y))
2152 {
2153 return pickATileGenThreat(x, y, numIterations, -1, -1, function);
2154 }
2155
pickATileGen(Vector2i * pos,unsigned numIterations,bool (* function)(UDWORD x,UDWORD y))2156 bool pickATileGen(Vector2i *pos, unsigned numIterations, bool (*function)(UDWORD x, UDWORD y))
2157 {
2158 UDWORD x = pos->x, y = pos->y;
2159 bool ret = pickATileGenThreat(&x, &y, numIterations, -1, -1, function);
2160 *pos = Vector2i(x, y);
2161 return ret;
2162 }
2163
ThreatInRange(SDWORD player,SDWORD range,SDWORD rangeX,SDWORD rangeY,bool bVTOLs)2164 static bool ThreatInRange(SDWORD player, SDWORD range, SDWORD rangeX, SDWORD rangeY, bool bVTOLs)
2165 {
2166 UDWORD i, structType;
2167 STRUCTURE *psStruct;
2168 DROID *psDroid;
2169
2170 const int tx = map_coord(rangeX);
2171 const int ty = map_coord(rangeY);
2172
2173 for (i = 0; i < MAX_PLAYERS; i++)
2174 {
2175 if ((alliances[player][i] == ALLIANCE_FORMED) || (i == player))
2176 {
2177 continue;
2178 }
2179
2180 //check structures
2181 for (psStruct = apsStructLists[i]; psStruct; psStruct = psStruct->psNext)
2182 {
2183 if (psStruct->visible[player] || psStruct->born == 2) // if can see it or started there
2184 {
2185 if (psStruct->status == SS_BUILT)
2186 {
2187 structType = psStruct->pStructureType->type;
2188
2189 switch (structType) //dangerous to get near these structures
2190 {
2191 case REF_DEFENSE:
2192 case REF_CYBORG_FACTORY:
2193 case REF_FACTORY:
2194 case REF_VTOL_FACTORY:
2195 case REF_REARM_PAD:
2196
2197 if (range < 0
2198 || world_coord(hypotf(tx - map_coord(psStruct->pos.x), ty - map_coord(psStruct->pos.y))) < range) //enemy in range
2199 {
2200 return true;
2201 }
2202
2203 break;
2204 }
2205 }
2206 }
2207 }
2208
2209 //check droids
2210 for (psDroid = apsDroidLists[i]; psDroid; psDroid = psDroid->psNext)
2211 {
2212 if (psDroid->visible[player]) //can see this droid?
2213 {
2214 if (!objHasWeapon((BASE_OBJECT *)psDroid))
2215 {
2216 continue;
2217 }
2218
2219 //if VTOLs are excluded, skip them
2220 if (!bVTOLs && ((asPropulsionStats[psDroid->asBits[COMP_PROPULSION]].propulsionType == PROPULSION_TYPE_LIFT) || isTransporter(psDroid)))
2221 {
2222 continue;
2223 }
2224
2225 if (range < 0
2226 || world_coord(hypotf(tx - map_coord(psDroid->pos.x), ty - map_coord(psDroid->pos.y))) < range) //enemy in range
2227 {
2228 return true;
2229 }
2230 }
2231 }
2232 }
2233
2234 return false;
2235 }
2236
2237 /// find a tile for which the passed function will return true without any threat in the specified range
pickATileGenThreat(UDWORD * x,UDWORD * y,UBYTE numIterations,SDWORD threatRange,SDWORD player,bool (* function)(UDWORD x,UDWORD y))2238 bool pickATileGenThreat(UDWORD *x, UDWORD *y, UBYTE numIterations, SDWORD threatRange,
2239 SDWORD player, bool (*function)(UDWORD x, UDWORD y))
2240 {
2241 SDWORD i, j;
2242 SDWORD startX, endX, startY, endY;
2243 UDWORD passes;
2244 Vector3i origin(world_coord(*x), world_coord(*y), 0);
2245
2246 ASSERT_OR_RETURN(false, *x < mapWidth, "x coordinate is off-map for pickATileGen");
2247 ASSERT_OR_RETURN(false, *y < mapHeight, "y coordinate is off-map for pickATileGen");
2248
2249 if (function(*x, *y) && ((threatRange <= 0) || (!ThreatInRange(player, threatRange, *x, *y, false)))) //TODO: vtol check really not needed?
2250 {
2251 return (true);
2252 }
2253
2254 /* Initial box dimensions and set iteration count to zero */
2255 startX = endX = *x; startY = endY = *y; passes = 0;
2256
2257 /* Keep going until we get a tile or we exceed distance */
2258 while (passes < numIterations)
2259 {
2260 /* Process whole box */
2261 for (i = startX; i <= endX; i++)
2262 {
2263 for (j = startY; j <= endY; j++)
2264 {
2265 /* Test only perimeter as internal tested previous iteration */
2266 if (i == startX || i == endX || j == startY || j == endY)
2267 {
2268 Vector3i newPos(world_coord(i), world_coord(j), 0);
2269
2270 /* Good enough? */
2271 if (function(i, j)
2272 && fpathCheck(origin, newPos, PROPULSION_TYPE_WHEELED)
2273 && ((threatRange <= 0) || (!ThreatInRange(player, threatRange, world_coord(i), world_coord(j), false))))
2274 {
2275 /* Set exit conditions and get out NOW */
2276 *x = i; *y = j;
2277 return true;
2278 }
2279 }
2280 }
2281 }
2282 /* Expand the box out in all directions - off map handled by tileAcceptable */
2283 startX--; startY--; endX++; endY++; passes++;
2284 }
2285 /* If we got this far, then we failed - passed in values will be unchanged */
2286 return false;
2287
2288 }
2289
2290 /// find a tile for a wheeled droid with only one other droid present
pickHalfATile(UDWORD * x,UDWORD * y,UBYTE numIterations)2291 PICKTILE pickHalfATile(UDWORD *x, UDWORD *y, UBYTE numIterations)
2292 {
2293 return pickATileGen(x, y, numIterations, canFitDroid) ? FREE_TILE : NO_FREE_TILE;
2294 }
2295
2296 /* Looks through the players list of droids to see if any of them are
2297 building the specified structure - returns true if finds one*/
checkDroidsBuilding(STRUCTURE * psStructure)2298 bool checkDroidsBuilding(STRUCTURE *psStructure)
2299 {
2300 DROID *psDroid;
2301
2302 for (psDroid = apsDroidLists[psStructure->player]; psDroid != nullptr; psDroid =
2303 psDroid->psNext)
2304 {
2305 //check DORDER_BUILD, HELP_BUILD is handled the same
2306 BASE_OBJECT *const psStruct = orderStateObj(psDroid, DORDER_BUILD);
2307 if ((STRUCTURE *)psStruct == psStructure)
2308 {
2309 return true;
2310 }
2311 }
2312 return false;
2313 }
2314
2315 /* Looks through the players list of droids to see if any of them are
2316 demolishing the specified structure - returns true if finds one*/
checkDroidsDemolishing(STRUCTURE * psStructure)2317 bool checkDroidsDemolishing(STRUCTURE *psStructure)
2318 {
2319 DROID *psDroid;
2320
2321 for (psDroid = apsDroidLists[psStructure->player]; psDroid != nullptr; psDroid =
2322 psDroid->psNext)
2323 {
2324 //check DORDER_DEMOLISH
2325 BASE_OBJECT *const psStruct = orderStateObj(psDroid, DORDER_DEMOLISH);
2326 if ((STRUCTURE *)psStruct == psStructure)
2327 {
2328 return true;
2329 }
2330 }
2331 return false;
2332 }
2333
2334
nextModuleToBuild(STRUCTURE const * psStruct,int lastOrderedModule)2335 int nextModuleToBuild(STRUCTURE const *psStruct, int lastOrderedModule)
2336 {
2337 int order = 0;
2338 UDWORD i = 0;
2339
2340 ASSERT_OR_RETURN(0, psStruct != nullptr && psStruct->pStructureType != nullptr, "Invalid structure pointer");
2341
2342 int next = psStruct->status == SS_BUILT ? 1 : 0; // If complete, next is one after the current number of modules, otherwise next is the one we're working on.
2343 int max;
2344 switch (psStruct->pStructureType->type)
2345 {
2346 case REF_POWER_GEN:
2347 //check room for one more!
2348 max = std::max<int>(psStruct->capacity + next, lastOrderedModule + 1);
2349 if (max <= 1)
2350 {
2351 i = powerModuleStat;
2352 order = max;
2353 }
2354 break;
2355 case REF_FACTORY:
2356 case REF_VTOL_FACTORY:
2357 //check room for one more!
2358 max = std::max<int>(psStruct->capacity + next, lastOrderedModule + 1);
2359 if (max <= NUM_FACTORY_MODULES)
2360 {
2361 i = factoryModuleStat;
2362 order = max;
2363 }
2364 break;
2365 case REF_RESEARCH:
2366 //check room for one more!
2367 max = std::max<int>(psStruct->capacity + next, lastOrderedModule + 1);
2368 if (max <= 1)
2369 {
2370 i = researchModuleStat;
2371 order = max; // Research modules are weird. Build one, get three free.
2372 }
2373 break;
2374 default:
2375 //no other structures can have modules attached
2376 break;
2377 }
2378
2379 if (order)
2380 {
2381 // Check availability of Module
2382 if (!((i < numStructureStats) &&
2383 (apStructTypeLists[psStruct->player][i] == AVAILABLE)))
2384 {
2385 order = 0;
2386 }
2387 }
2388
2389 return order;
2390 }
2391
2392 /*Deals with building a module - checking if any droid is currently doing this
2393 - if so, helping to build the current one*/
setUpBuildModule(DROID * psDroid)2394 void setUpBuildModule(DROID *psDroid)
2395 {
2396 Vector2i tile = map_coord(psDroid->order.pos);
2397
2398 //check not another Truck started
2399 STRUCTURE *psStruct = getTileStructure(tile.x, tile.y);
2400 if (psStruct)
2401 {
2402 // if a droid is currently building, or building is in progress of being built/upgraded the droid's order should be DORDER_HELPBUILD
2403 if (checkDroidsBuilding(psStruct) || !psStruct->status)
2404 {
2405 //set up the help build scenario
2406 psDroid->order.type = DORDER_HELPBUILD;
2407 setDroidTarget(psDroid, (BASE_OBJECT *)psStruct);
2408 if (droidStartBuild(psDroid))
2409 {
2410 psDroid->action = DACTION_BUILD;
2411 return;
2412 }
2413 }
2414 else
2415 {
2416 if (nextModuleToBuild(psStruct, -1) > 0)
2417 {
2418 //no other droids building so just start it off
2419 if (droidStartBuild(psDroid))
2420 {
2421 psDroid->action = DACTION_BUILD;
2422 return;
2423 }
2424 }
2425 }
2426 }
2427 cancelBuild(psDroid);
2428 }
2429
2430 /* Just returns true if the droid's present body points aren't as high as the original*/
droidIsDamaged(const DROID * psDroid)2431 bool droidIsDamaged(const DROID *psDroid)
2432 {
2433 if (psDroid->body < psDroid->originalBody)
2434 {
2435 return (true);
2436 }
2437 else
2438 {
2439 return (false);
2440 }
2441 }
2442
2443
getDroidResourceName(char const * pName)2444 char const *getDroidResourceName(char const *pName)
2445 {
2446 /* See if the name has a string resource associated with it by trying
2447 * to get the string resource.
2448 */
2449 return strresGetString(psStringRes, pName);
2450 }
2451
2452
2453 /*checks to see if an electronic warfare weapon is attached to the droid*/
electronicDroid(const DROID * psDroid)2454 bool electronicDroid(const DROID *psDroid)
2455 {
2456 CHECK_DROID(psDroid);
2457
2458 //use slot 0 for now
2459 if (psDroid->numWeaps > 0 && asWeaponStats[psDroid->asWeaps[0].nStat].weaponSubClass == WSC_ELECTRONIC)
2460 {
2461 return true;
2462 }
2463
2464 if (psDroid->droidType == DROID_COMMAND && psDroid->psGroup && psDroid->psGroup->psCommander == psDroid)
2465 {
2466 // if a commander has EW units attached it is electronic
2467 for (const DROID *psCurr = psDroid->psGroup->psList; psCurr; psCurr = psCurr->psGrpNext)
2468 {
2469 if (psDroid != psCurr && electronicDroid(psCurr))
2470 {
2471 return true;
2472 }
2473 }
2474 }
2475
2476 return false;
2477 }
2478
2479 /*checks to see if the droid is currently being repaired by another*/
droidUnderRepair(const DROID * psDroid)2480 bool droidUnderRepair(const DROID *psDroid)
2481 {
2482 CHECK_DROID(psDroid);
2483
2484 //droid must be damaged
2485 if (droidIsDamaged(psDroid))
2486 {
2487 //look thru the list of players droids to see if any are repairing this droid
2488 for (const DROID *psCurr = apsDroidLists[psDroid->player]; psCurr != nullptr; psCurr = psCurr->psNext)
2489 {
2490 if ((psCurr->droidType == DROID_REPAIR || psCurr->droidType ==
2491 DROID_CYBORG_REPAIR) && psCurr->action ==
2492 DACTION_DROIDREPAIR && psCurr->order.psObj == psDroid)
2493 {
2494 return true;
2495 }
2496 }
2497 }
2498 return false;
2499 }
2500
2501 //count how many Command Droids exist in the world at any one moment
checkCommandExist(UBYTE player)2502 UBYTE checkCommandExist(UBYTE player)
2503 {
2504 UBYTE quantity = 0;
2505
2506 for (DROID *psDroid = apsDroidLists[player]; psDroid != nullptr; psDroid = psDroid->psNext)
2507 {
2508 if (psDroid->droidType == DROID_COMMAND)
2509 {
2510 quantity++;
2511 }
2512 }
2513 return quantity;
2514 }
2515
isTransporter(DROID_TYPE type)2516 static inline bool isTransporter(DROID_TYPE type)
2517 {
2518 return type == DROID_TRANSPORTER || type == DROID_SUPERTRANSPORTER;
2519 }
2520
isTransporter(DROID const * psDroid)2521 bool isTransporter(DROID const *psDroid)
2522 {
2523 return isTransporter(psDroid->droidType);
2524 }
2525
isTransporter(DROID_TEMPLATE const * psTemplate)2526 bool isTransporter(DROID_TEMPLATE const *psTemplate)
2527 {
2528 return isTransporter(psTemplate->droidType);
2529 }
2530
2531 //access functions for vtols
isVtolDroid(const DROID * psDroid)2532 bool isVtolDroid(const DROID *psDroid)
2533 {
2534 return asPropulsionStats[psDroid->asBits[COMP_PROPULSION]].propulsionType == PROPULSION_TYPE_LIFT
2535 && !isTransporter(psDroid);
2536 }
2537
2538 /* returns true if the droid has lift propulsion and is moving */
isFlying(const DROID * psDroid)2539 bool isFlying(const DROID *psDroid)
2540 {
2541 return (asPropulsionStats + psDroid->asBits[COMP_PROPULSION])->propulsionType == PROPULSION_TYPE_LIFT
2542 && (psDroid->sMove.Status != MOVEINACTIVE || isTransporter(psDroid));
2543 }
2544
2545 /* returns true if it's a VTOL weapon droid which has completed all runs */
vtolEmpty(const DROID * psDroid)2546 bool vtolEmpty(const DROID *psDroid)
2547 {
2548 CHECK_DROID(psDroid);
2549
2550 if (!isVtolDroid(psDroid))
2551 {
2552 return false;
2553 }
2554 if (psDroid->droidType != DROID_WEAPON)
2555 {
2556 return false;
2557 }
2558
2559 for (int i = 0; i < psDroid->numWeaps; i++)
2560 {
2561 if (asWeaponStats[psDroid->asWeaps[i].nStat].vtolAttackRuns > 0 &&
2562 psDroid->asWeaps[i].usedAmmo < getNumAttackRuns(psDroid, i))
2563 {
2564 return false;
2565 }
2566 }
2567
2568 return true;
2569 }
2570
2571 /* returns true if it's a VTOL weapon droid which still has full ammo */
vtolFull(const DROID * psDroid)2572 bool vtolFull(const DROID *psDroid)
2573 {
2574 CHECK_DROID(psDroid);
2575
2576 if (!isVtolDroid(psDroid))
2577 {
2578 return false;
2579 }
2580 if (psDroid->droidType != DROID_WEAPON)
2581 {
2582 return false;
2583 }
2584
2585 for (int i = 0; i < psDroid->numWeaps; i++)
2586 {
2587 if (asWeaponStats[psDroid->asWeaps[i].nStat].vtolAttackRuns > 0 &&
2588 psDroid->asWeaps[i].usedAmmo > 0)
2589 {
2590 return false;
2591 }
2592 }
2593
2594 return true;
2595 }
2596
2597 // true if a vtol is waiting to be rearmed by a particular rearm pad
vtolReadyToRearm(DROID * psDroid,STRUCTURE * psStruct)2598 bool vtolReadyToRearm(DROID *psDroid, STRUCTURE *psStruct)
2599 {
2600 CHECK_DROID(psDroid);
2601
2602 if (!isVtolDroid(psDroid) || psDroid->action != DACTION_WAITFORREARM)
2603 {
2604 return false;
2605 }
2606
2607 // If a unit has been ordered to rearm make sure it goes to the correct base
2608 STRUCTURE *psRearmPad = castStructure(orderStateObj(psDroid, DORDER_REARM));
2609 if (psRearmPad && psRearmPad != psStruct && !vtolOnRearmPad(psRearmPad, psDroid))
2610 {
2611 // target rearm pad is clear - let it go there
2612 objTrace(psDroid->id, "rearm pad at %d,%d won't snatch us - we already have another available at %d,%d", psStruct->pos.x / TILE_UNITS, psStruct->pos.y / TILE_UNITS, psRearmPad->pos.x / TILE_UNITS, psRearmPad->pos.y / TILE_UNITS);
2613 return false;
2614 }
2615
2616 if (vtolHappy(psDroid) && vtolOnRearmPad(psStruct, psDroid))
2617 {
2618 // there is a vtol on the pad and this vtol is already rearmed
2619 // don't bother shifting the other vtol off
2620 objTrace(psDroid->id, "rearm pad at %d,%d won't snatch us - we're rearmed and pad is busy", psStruct->pos.x / TILE_UNITS, psStruct->pos.y / TILE_UNITS);
2621 return false;
2622 }
2623
2624 STRUCTURE *psTarget = castStructure(psDroid->psActionTarget[0]);
2625 if (psTarget && psTarget->pFunctionality->rearmPad.psObj == psDroid)
2626 {
2627 // vtol is rearming at a different base, leave it alone
2628 objTrace(psDroid->id, "rearm pad at %d,%d won't snatch us - we already are snatched by %d,%d", psStruct->pos.x / TILE_UNITS, psStruct->pos.y / TILE_UNITS, psTarget->pos.x / TILE_UNITS, psTarget->pos.y / TILE_UNITS);
2629 return false;
2630 }
2631
2632 return true;
2633 }
2634
2635 // true if a vtol droid currently returning to be rearmed
vtolRearming(const DROID * psDroid)2636 bool vtolRearming(const DROID *psDroid)
2637 {
2638 CHECK_DROID(psDroid);
2639
2640 if (!isVtolDroid(psDroid))
2641 {
2642 return false;
2643 }
2644 if (psDroid->droidType != DROID_WEAPON)
2645 {
2646 return false;
2647 }
2648
2649 if (psDroid->action == DACTION_MOVETOREARM ||
2650 psDroid->action == DACTION_WAITFORREARM ||
2651 psDroid->action == DACTION_MOVETOREARMPOINT ||
2652 psDroid->action == DACTION_WAITDURINGREARM)
2653 {
2654 return true;
2655 }
2656
2657 return false;
2658 }
2659
2660 // true if a droid is currently attacking
droidAttacking(const DROID * psDroid)2661 bool droidAttacking(const DROID *psDroid)
2662 {
2663 CHECK_DROID(psDroid);
2664
2665 //what about cyborgs?
2666 if (!(psDroid->droidType == DROID_WEAPON || psDroid->droidType == DROID_CYBORG ||
2667 psDroid->droidType == DROID_CYBORG_SUPER))
2668 {
2669 return false;
2670 }
2671
2672 if (psDroid->action == DACTION_ATTACK ||
2673 psDroid->action == DACTION_MOVETOATTACK ||
2674 psDroid->action == DACTION_ROTATETOATTACK ||
2675 psDroid->action == DACTION_VTOLATTACK ||
2676 psDroid->action == DACTION_MOVEFIRE)
2677 {
2678 return true;
2679 }
2680
2681 return false;
2682 }
2683
2684 // see if there are any other vtols attacking the same target
2685 // but still rearming
allVtolsRearmed(const DROID * psDroid)2686 bool allVtolsRearmed(const DROID *psDroid)
2687 {
2688 CHECK_DROID(psDroid);
2689
2690 // ignore all non vtols
2691 if (!isVtolDroid(psDroid))
2692 {
2693 return true;
2694 }
2695
2696 bool stillRearming = false;
2697 for (const DROID *psCurr = apsDroidLists[psDroid->player]; psCurr; psCurr = psCurr->psNext)
2698 {
2699 if (vtolRearming(psCurr) &&
2700 psCurr->order.type == psDroid->order.type &&
2701 psCurr->order.psObj == psDroid->order.psObj)
2702 {
2703 stillRearming = true;
2704 break;
2705 }
2706 }
2707
2708 return !stillRearming;
2709 }
2710
2711
2712 /*returns a count of the base number of attack runs for the weapon attached to the droid*/
getNumAttackRuns(const DROID * psDroid,int weapon_slot)2713 UWORD getNumAttackRuns(const DROID *psDroid, int weapon_slot)
2714 {
2715 ASSERT_OR_RETURN(0, isVtolDroid(psDroid), "not a VTOL Droid");
2716 // if weapon is a salvo weapon, then number of shots that can be fired = vtolAttackRuns * numRounds
2717 if (asWeaponStats[psDroid->asWeaps[weapon_slot].nStat].upgrade[psDroid->player].reloadTime)
2718 {
2719 return asWeaponStats[psDroid->asWeaps[weapon_slot].nStat].upgrade[psDroid->player].numRounds
2720 * asWeaponStats[psDroid->asWeaps[weapon_slot].nStat].vtolAttackRuns;
2721 }
2722 return asWeaponStats[psDroid->asWeaps[weapon_slot].nStat].vtolAttackRuns;
2723 }
2724
2725 /*Checks a vtol for being fully armed and fully repaired to see if ready to
2726 leave reArm pad */
vtolHappy(const DROID * psDroid)2727 bool vtolHappy(const DROID *psDroid)
2728 {
2729 CHECK_DROID(psDroid);
2730
2731 ASSERT_OR_RETURN(false, isVtolDroid(psDroid), "not a VTOL droid");
2732
2733 if (psDroid->body < psDroid->originalBody)
2734 {
2735 // VTOLs with less health than their original aren't happy
2736 return false;
2737 }
2738
2739 if (psDroid->droidType != DROID_WEAPON)
2740 {
2741 // Not an armed droid, so don't check the (non-existent) weapons
2742 return true;
2743 }
2744
2745 /* NOTE: Previous code (r5410) returned false if a droid had no weapon,
2746 * which IMO isn't correct, but might be expected behaviour. I'm
2747 * also not sure if weapon droids (see the above droidType check)
2748 * can even have zero weapons. -- Giel
2749 */
2750 ASSERT_OR_RETURN(false, psDroid->numWeaps > 0, "VTOL weapon droid without weapons found!");
2751
2752 //check full complement of ammo
2753 for (int i = 0; i < psDroid->numWeaps; ++i)
2754 {
2755 if (asWeaponStats[psDroid->asWeaps[i].nStat].vtolAttackRuns > 0
2756 && psDroid->asWeaps[i].usedAmmo != 0)
2757 {
2758 return false;
2759 }
2760 }
2761
2762 return true;
2763 }
2764
2765 /*checks if the droid is a VTOL droid and updates the attack runs as required*/
updateVtolAttackRun(DROID * psDroid,int weapon_slot)2766 void updateVtolAttackRun(DROID *psDroid, int weapon_slot)
2767 {
2768 if (isVtolDroid(psDroid))
2769 {
2770 if (psDroid->numWeaps > 0)
2771 {
2772 if (asWeaponStats[psDroid->asWeaps[weapon_slot].nStat].vtolAttackRuns > 0)
2773 {
2774 ++psDroid->asWeaps[weapon_slot].usedAmmo;
2775 if (psDroid->asWeaps[weapon_slot].usedAmmo == getNumAttackRuns(psDroid, weapon_slot))
2776 {
2777 psDroid->asWeaps[weapon_slot].ammo = 0;
2778 }
2779 //quick check doesn't go over limit
2780 ASSERT(psDroid->asWeaps[weapon_slot].usedAmmo < UWORD_MAX, "too many attack runs");
2781 }
2782 }
2783 }
2784 }
2785
2786 //assign rearmPad to the VTOL
assignVTOLPad(DROID * psNewDroid,STRUCTURE * psReArmPad)2787 void assignVTOLPad(DROID *psNewDroid, STRUCTURE *psReArmPad)
2788 {
2789 ASSERT_OR_RETURN(, isVtolDroid(psNewDroid), "%s is not a VTOL droid", objInfo(psNewDroid));
2790 ASSERT_OR_RETURN(, psReArmPad->type == OBJ_STRUCTURE && psReArmPad->pStructureType->type == REF_REARM_PAD,
2791 "%s cannot rearm", objInfo(psReArmPad));
2792
2793 setDroidBase(psNewDroid, psReArmPad);
2794 }
2795
2796 /*compares the droid sensor type with the droid weapon type to see if the
2797 FIRE_SUPPORT order can be assigned*/
droidSensorDroidWeapon(const BASE_OBJECT * psObj,const DROID * psDroid)2798 bool droidSensorDroidWeapon(const BASE_OBJECT *psObj, const DROID *psDroid)
2799 {
2800 const SENSOR_STATS *psStats = nullptr;
2801 int compIndex;
2802
2803 CHECK_DROID(psDroid);
2804
2805 if (!psObj || !psDroid)
2806 {
2807 return false;
2808 }
2809
2810 //first check if the object is a droid or a structure
2811 if ((psObj->type != OBJ_DROID) &&
2812 (psObj->type != OBJ_STRUCTURE))
2813 {
2814 return false;
2815 }
2816 //check same player
2817 if (psObj->player != psDroid->player)
2818 {
2819 return false;
2820 }
2821 //check obj is a sensor droid/structure
2822 switch (psObj->type)
2823 {
2824 case OBJ_DROID:
2825 if (((const DROID *)psObj)->droidType != DROID_SENSOR &&
2826 ((const DROID *)psObj)->droidType != DROID_COMMAND)
2827 {
2828 return false;
2829 }
2830 compIndex = ((const DROID *)psObj)->asBits[COMP_SENSOR];
2831 ASSERT_OR_RETURN(false, compIndex < numSensorStats, "Invalid range referenced for numSensorStats, %d > %d", compIndex, numSensorStats);
2832 psStats = asSensorStats + compIndex;
2833 break;
2834 case OBJ_STRUCTURE:
2835 psStats = ((const STRUCTURE *)psObj)->pStructureType->pSensor;
2836 if ((psStats == nullptr) ||
2837 (psStats->location != LOC_TURRET))
2838 {
2839 return false;
2840 }
2841 break;
2842 default:
2843 break;
2844 }
2845
2846 //check droid is a weapon droid - or Cyborg!!
2847 if (!(psDroid->droidType == DROID_WEAPON || psDroid->droidType ==
2848 DROID_CYBORG || psDroid->droidType == DROID_CYBORG_SUPER))
2849 {
2850 return false;
2851 }
2852
2853 //finally check the right droid/sensor combination
2854 // check vtol droid with commander
2855 if ((isVtolDroid(psDroid) || !proj_Direct(asWeaponStats + psDroid->asWeaps[0].nStat)) &&
2856 psObj->type == OBJ_DROID && ((const DROID *)psObj)->droidType == DROID_COMMAND)
2857 {
2858 return true;
2859 }
2860
2861 //check vtol droid with vtol sensor
2862 if (isVtolDroid(psDroid) && psDroid->asWeaps[0].nStat > 0)
2863 {
2864 if (psStats->type == VTOL_INTERCEPT_SENSOR || psStats->type == VTOL_CB_SENSOR || psStats->type == SUPER_SENSOR /*|| psStats->type == RADAR_DETECTOR_SENSOR*/)
2865 {
2866 return true;
2867 }
2868 return false;
2869 }
2870
2871 // Check indirect weapon droid with standard/CB/radar detector sensor
2872 if (!proj_Direct(asWeaponStats + psDroid->asWeaps[0].nStat))
2873 {
2874 if (psStats->type == STANDARD_SENSOR || psStats->type == INDIRECT_CB_SENSOR || psStats->type == SUPER_SENSOR /*|| psStats->type == RADAR_DETECTOR_SENSOR*/)
2875 {
2876 return true;
2877 }
2878 return false;
2879 }
2880 return false;
2881 }
2882
2883 // return whether a droid has a CB sensor on it
cbSensorDroid(const DROID * psDroid)2884 bool cbSensorDroid(const DROID *psDroid)
2885 {
2886 if (psDroid->droidType != DROID_SENSOR)
2887 {
2888 return false;
2889 }
2890 if (asSensorStats[psDroid->asBits[COMP_SENSOR]].type == VTOL_CB_SENSOR
2891 || asSensorStats[psDroid->asBits[COMP_SENSOR]].type == INDIRECT_CB_SENSOR)
2892 {
2893 return true;
2894 }
2895
2896 return false;
2897 }
2898
2899 // return whether a droid has a standard sensor on it (standard, VTOL strike, or wide spectrum)
standardSensorDroid(const DROID * psDroid)2900 bool standardSensorDroid(const DROID *psDroid)
2901 {
2902 if (psDroid->droidType != DROID_SENSOR)
2903 {
2904 return false;
2905 }
2906 if (asSensorStats[psDroid->asBits[COMP_SENSOR]].type == VTOL_INTERCEPT_SENSOR
2907 || asSensorStats[psDroid->asBits[COMP_SENSOR]].type == STANDARD_SENSOR
2908 || asSensorStats[psDroid->asBits[COMP_SENSOR]].type == SUPER_SENSOR)
2909 {
2910 return true;
2911 }
2912
2913 return false;
2914 }
2915
2916 // ////////////////////////////////////////////////////////////////////////////
2917 // Give a droid from one player to another - used in Electronic Warfare and multiplayer.
2918 // Got to destroy the droid and build another since there are too many complications otherwise.
2919 // Returns the droid created.
giftSingleDroid(DROID * psD,UDWORD to,bool electronic)2920 DROID *giftSingleDroid(DROID *psD, UDWORD to, bool electronic)
2921 {
2922 CHECK_DROID(psD);
2923 ASSERT_OR_RETURN(nullptr, !isDead(psD), "Cannot gift dead unit");
2924 ASSERT_OR_RETURN(psD, psD->player != to, "Cannot gift to self");
2925
2926 // Check unit limits (multiplayer only)
2927 syncDebug("Limits: %u/%d %u/%d %u/%d", getNumDroids(to), getMaxDroids(to), getNumConstructorDroids(to), getMaxConstructors(to), getNumCommandDroids(to), getMaxCommanders(to));
2928 if (bMultiPlayer
2929 && ((int)getNumDroids(to) >= getMaxDroids(to)
2930 || ((psD->droidType == DROID_CYBORG_CONSTRUCT || psD->droidType == DROID_CONSTRUCT)
2931 && (int)getNumConstructorDroids(to) >= getMaxConstructors(to))
2932 || (psD->droidType == DROID_COMMAND && (int)getNumCommandDroids(to) >= getMaxCommanders(to))))
2933 {
2934 if (to == selectedPlayer || psD->player == selectedPlayer)
2935 {
2936 CONPRINTF("%s", _("Unit transfer failed -- unit limits exceeded"));
2937 }
2938 return nullptr;
2939 }
2940
2941 // electronic or campaign will destroy and recreate the droid.
2942 if (electronic || !bMultiPlayer)
2943 {
2944 DROID_TEMPLATE sTemplate;
2945 DROID *psNewDroid;
2946
2947 templateSetParts(psD, &sTemplate); // create a template based on the droid
2948 sTemplate.name = WzString::fromUtf8(psD->aName); // copy the name across
2949 // update score
2950 if (psD->player == selectedPlayer && to != selectedPlayer && !bMultiPlayer)
2951 {
2952 scoreUpdateVar(WD_UNITS_LOST);
2953 }
2954 // make the old droid vanish (but is not deleted until next tick)
2955 adjustDroidCount(psD, -1);
2956 vanishDroid(psD);
2957 // create a new droid
2958 psNewDroid = reallyBuildDroid(&sTemplate, Position(psD->pos.x, psD->pos.y, 0), to, false, psD->rot);
2959 ASSERT_OR_RETURN(nullptr, psNewDroid, "Unable to build unit");
2960
2961 addDroid(psNewDroid, apsDroidLists);
2962 adjustDroidCount(psNewDroid, 1);
2963
2964 psNewDroid->body = clip((psD->body*psNewDroid->originalBody + psD->originalBody/2)/std::max(psD->originalBody, 1u), 1u, psNewDroid->originalBody);
2965 psNewDroid->experience = psD->experience;
2966 psNewDroid->kills = psD->kills;
2967
2968 if (!(psNewDroid->droidType == DROID_PERSON || cyborgDroid(psNewDroid) || isTransporter(psNewDroid)))
2969 {
2970 updateDroidOrientation(psNewDroid);
2971 }
2972
2973 triggerEventObjectTransfer(psNewDroid, psD->player);
2974 return psNewDroid;
2975 }
2976
2977 int oldPlayer = psD->player;
2978
2979 // reset the assigned state of units attached to a leader
2980 for (DROID *psCurr = apsDroidLists[oldPlayer]; psCurr != nullptr; psCurr = psCurr->psNext)
2981 {
2982 BASE_OBJECT *psLeader;
2983
2984 if (hasCommander(psCurr))
2985 {
2986 psLeader = (BASE_OBJECT *)psCurr->psGroup->psCommander;
2987 }
2988 else
2989 {
2990 //psLeader can be either a droid or a structure
2991 psLeader = orderStateObj(psCurr, DORDER_FIRESUPPORT);
2992 }
2993
2994 if (psLeader && psLeader->id == psD->id)
2995 {
2996 psCurr->selected = false;
2997 orderDroid(psCurr, DORDER_STOP, ModeQueue);
2998 }
2999 }
3000
3001 visRemoveVisibility((BASE_OBJECT *)psD);
3002 psD->selected = false;
3003
3004 adjustDroidCount(psD, -1);
3005 scriptRemoveObject(psD); //Remove droid from any script groups
3006
3007 if (droidRemove(psD, apsDroidLists))
3008 {
3009 psD->player = to;
3010
3011 addDroid(psD, apsDroidLists);
3012 adjustDroidCount(psD, 1);
3013
3014 // the new player may have different default sensor/ecm/repair components
3015 if ((asSensorStats + psD->asBits[COMP_SENSOR])->location == LOC_DEFAULT)
3016 {
3017 if (psD->asBits[COMP_SENSOR] != aDefaultSensor[psD->player])
3018 {
3019 psD->asBits[COMP_SENSOR] = aDefaultSensor[psD->player];
3020 }
3021 }
3022 if ((asECMStats + psD->asBits[COMP_ECM])->location == LOC_DEFAULT)
3023 {
3024 if (psD->asBits[COMP_ECM] != aDefaultECM[psD->player])
3025 {
3026 psD->asBits[COMP_ECM] = aDefaultECM[psD->player];
3027 }
3028 }
3029 if ((asRepairStats + psD->asBits[COMP_REPAIRUNIT])->location == LOC_DEFAULT)
3030 {
3031 if (psD->asBits[COMP_REPAIRUNIT] != aDefaultRepair[psD->player])
3032 {
3033 psD->asBits[COMP_REPAIRUNIT] = aDefaultRepair[psD->player];
3034 }
3035 }
3036 }
3037 else
3038 {
3039 // if we couldn't remove it, then get rid of it.
3040 return nullptr;
3041 }
3042
3043 // Update visibility
3044 visTilesUpdate((BASE_OBJECT*)psD);
3045
3046 // check through the players, and our allies, list of droids to see if any are targetting it
3047 for (unsigned int i = 0; i < MAX_PLAYERS; ++i)
3048 {
3049 if (!aiCheckAlliances(i, to))
3050 {
3051 continue;
3052 }
3053
3054 for (DROID *psCurr = apsDroidLists[i]; psCurr != nullptr; psCurr = psCurr->psNext)
3055 {
3056 if (psCurr->order.psObj == psD || psCurr->psActionTarget[0] == psD)
3057 {
3058 orderDroid(psCurr, DORDER_STOP, ModeQueue);
3059 break;
3060 }
3061 for (unsigned iWeap = 0; iWeap < psCurr->numWeaps; ++iWeap)
3062 {
3063 if (psCurr->psActionTarget[iWeap] == psD)
3064 {
3065 orderDroid(psCurr, DORDER_STOP, ModeImmediate);
3066 break;
3067 }
3068 }
3069 // check through order list
3070 orderClearTargetFromDroidList(psCurr, (BASE_OBJECT *)psD);
3071 }
3072 }
3073
3074 for (unsigned int i = 0; i < MAX_PLAYERS; ++i)
3075 {
3076 if (!aiCheckAlliances(i, to))
3077 {
3078 continue;
3079 }
3080
3081 // check through the players list, and our allies, of structures to see if any are targetting it
3082 for (STRUCTURE *psStruct = apsStructLists[i]; psStruct != nullptr; psStruct = psStruct->psNext)
3083 {
3084 if (psStruct->psTarget[0] == psD)
3085 {
3086 setStructureTarget(psStruct, nullptr, 0, ORIGIN_UNKNOWN);
3087 }
3088 }
3089 }
3090
3091 triggerEventObjectTransfer(psD, oldPlayer);
3092 return psD;
3093 }
3094
3095 /*calculates the electronic resistance of a droid based on its experience level*/
droidResistance(const DROID * psDroid)3096 SWORD droidResistance(const DROID *psDroid)
3097 {
3098 CHECK_DROID(psDroid);
3099 const BODY_STATS *psStats = asBodyStats + psDroid->asBits[COMP_BODY];
3100 int resistance = psDroid->experience / (65536 / MAX(1, psStats->upgrade[psDroid->player].resistance));
3101 // ensure resistance is a base minimum
3102 resistance = MAX(resistance, psStats->upgrade[psDroid->player].resistance);
3103 return MIN(resistance, INT16_MAX);
3104 }
3105
3106 /*this is called to check the weapon is 'allowed'. Check if VTOL, the weapon is
3107 direct fire. Also check numVTOLattackRuns for the weapon is not zero - return
3108 true if valid weapon*/
3109 /* this will be buggy if the droid being checked has both AA weapon and non-AA weapon
3110 Cannot think of a solution without adding additional return value atm.
3111 */
checkValidWeaponForProp(DROID_TEMPLATE * psTemplate)3112 bool checkValidWeaponForProp(DROID_TEMPLATE *psTemplate)
3113 {
3114 PROPULSION_STATS *psPropStats;
3115
3116 //check propulsion stat for vtol
3117 psPropStats = asPropulsionStats + psTemplate->asParts[COMP_PROPULSION];
3118
3119 ASSERT_OR_RETURN(false, psPropStats != nullptr, "invalid propulsion stats pointer");
3120
3121 // if there are no weapons, then don't even bother continuing
3122 if (psTemplate->numWeaps == 0)
3123 {
3124 return false;
3125 }
3126
3127 if (asPropulsionTypes[psPropStats->propulsionType].travel == AIR)
3128 {
3129 //check weapon stat for indirect
3130 if (!proj_Direct(asWeaponStats + psTemplate->asWeaps[0])
3131 || !asWeaponStats[psTemplate->asWeaps[0]].vtolAttackRuns)
3132 {
3133 return false;
3134 }
3135 }
3136 else
3137 {
3138 // VTOL weapons do not go on non-AIR units.
3139 if (asWeaponStats[psTemplate->asWeaps[0]].vtolAttackRuns)
3140 {
3141 return false;
3142 }
3143 }
3144
3145 //also checks that there is no other system component
3146 if (psTemplate->asParts[COMP_BRAIN] != 0
3147 && asWeaponStats[psTemplate->asWeaps[0]].weaponSubClass != WSC_COMMAND)
3148 {
3149 assert(false);
3150 return false;
3151 }
3152
3153 return true;
3154 }
3155
3156 // Check if a droid can be selected.
isSelectable(DROID const * psDroid)3157 bool isSelectable(DROID const *psDroid)
3158 {
3159 if (psDroid->flags.test(OBJECT_FLAG_UNSELECTABLE))
3160 {
3161 return false;
3162 }
3163
3164 // we shouldn't ever control the transporter in SP games
3165 if (isTransporter(psDroid) && !bMultiPlayer)
3166 {
3167 return false;
3168 }
3169
3170 return true;
3171 }
3172
3173 // Select a droid and do any necessary housekeeping.
3174 //
SelectDroid(DROID * psDroid)3175 void SelectDroid(DROID *psDroid)
3176 {
3177 if (!isSelectable(psDroid))
3178 {
3179 return;
3180 }
3181
3182 psDroid->selected = true;
3183 intRefreshScreen();
3184 triggerEventSelected();
3185 jsDebugSelected(psDroid);
3186 }
3187
3188 // De-select a droid and do any necessary housekeeping.
3189 //
DeSelectDroid(DROID * psDroid)3190 void DeSelectDroid(DROID *psDroid)
3191 {
3192 psDroid->selected = false;
3193 intRefreshScreen();
3194 triggerEventSelected();
3195 }
3196
3197 /** Callback function for stopped audio tracks
3198 * Sets the droid's current track id to NO_SOUND
3199 * \return true on success, false on failure
3200 */
droidAudioTrackStopped(void * psObj)3201 bool droidAudioTrackStopped(void *psObj)
3202 {
3203 DROID *psDroid;
3204
3205 psDroid = (DROID *)psObj;
3206 if (psDroid == nullptr)
3207 {
3208 debug(LOG_ERROR, "droid pointer invalid");
3209 return false;
3210 }
3211
3212 if (psDroid->type != OBJ_DROID || psDroid->died)
3213 {
3214 return false;
3215 }
3216
3217 psDroid->iAudioID = NO_SOUND;
3218 return true;
3219 }
3220
3221 /*returns true if droid type is one of the Cyborg types*/
cyborgDroid(const DROID * psDroid)3222 bool cyborgDroid(const DROID *psDroid)
3223 {
3224 return (psDroid->droidType == DROID_CYBORG
3225 || psDroid->droidType == DROID_CYBORG_CONSTRUCT
3226 || psDroid->droidType == DROID_CYBORG_REPAIR
3227 || psDroid->droidType == DROID_CYBORG_SUPER);
3228 }
3229
isConstructionDroid(DROID const * psDroid)3230 bool isConstructionDroid(DROID const *psDroid)
3231 {
3232 return psDroid->droidType == DROID_CONSTRUCT || psDroid->droidType == DROID_CYBORG_CONSTRUCT;
3233 }
3234
isConstructionDroid(BASE_OBJECT const * psObject)3235 bool isConstructionDroid(BASE_OBJECT const *psObject)
3236 {
3237 return isDroid(psObject) && isConstructionDroid(castDroid(psObject));
3238 }
3239
droidOnMap(const DROID * psDroid)3240 bool droidOnMap(const DROID *psDroid)
3241 {
3242 if (psDroid->died == NOT_CURRENT_LIST || isTransporter(psDroid)
3243 || psDroid->pos.x == INVALID_XY || psDroid->pos.y == INVALID_XY || missionIsOffworld()
3244 || mapHeight == 0)
3245 {
3246 // Off world or on a transport or is a transport or in mission list, or on a mission, or no map - ignore
3247 return true;
3248 }
3249 return worldOnMap(psDroid->pos.x, psDroid->pos.y);
3250 }
3251
3252 /** Teleport a droid to a new position on the map */
droidSetPosition(DROID * psDroid,int x,int y)3253 void droidSetPosition(DROID *psDroid, int x, int y)
3254 {
3255 psDroid->pos.x = x;
3256 psDroid->pos.y = y;
3257 psDroid->pos.z = map_Height(psDroid->pos.x, psDroid->pos.y);
3258 initDroidMovement(psDroid);
3259 visTilesUpdate((BASE_OBJECT *)psDroid);
3260 }
3261
3262 /** Check validity of a droid. Crash hard if it fails. */
checkDroid(const DROID * droid,const char * const location,const char * function,const int recurse)3263 void checkDroid(const DROID *droid, const char *const location, const char *function, const int recurse)
3264 {
3265 if (recurse < 0)
3266 {
3267 return;
3268 }
3269
3270 ASSERT_HELPER(droid != nullptr, location, function, "CHECK_DROID: NULL pointer");
3271 ASSERT_HELPER(droid->type == OBJ_DROID, location, function, "CHECK_DROID: Not droid (type %d)", (int)droid->type);
3272 ASSERT_HELPER(droid->numWeaps <= MAX_WEAPONS, location, function, "CHECK_DROID: Bad number of droid weapons %d", (int)droid->numWeaps);
3273 ASSERT_HELPER((unsigned)droid->listSize <= droid->asOrderList.size() && (unsigned)droid->listPendingBegin <= droid->asOrderList.size(), location, function, "CHECK_DROID: Bad number of droid orders %d %d %d", (int)droid->listSize, (int)droid->listPendingBegin, (int)droid->asOrderList.size());
3274 ASSERT_HELPER(droid->player < MAX_PLAYERS, location, function, "CHECK_DROID: Bad droid owner %d", (int)droid->player);
3275 ASSERT_HELPER(droidOnMap(droid), location, function, "CHECK_DROID: Droid off map");
3276 ASSERT_HELPER(droid->body <= droid->originalBody, location, function, "CHECK_DROID: More body points (%u) than original body points (%u).", (unsigned)droid->body, (unsigned)droid->originalBody);
3277
3278 for (int i = 0; i < MAX_WEAPONS; ++i)
3279 {
3280 ASSERT_HELPER(droid->asWeaps[i].lastFired <= gameTime, location, function, "CHECK_DROID: Bad last fired time for turret %u", i);
3281 }
3282 }
3283
droidSqDist(DROID * psDroid,BASE_OBJECT * psObj)3284 int droidSqDist(DROID *psDroid, BASE_OBJECT *psObj)
3285 {
3286 PROPULSION_STATS *psPropStats = asPropulsionStats + psDroid->asBits[COMP_PROPULSION];
3287
3288 if (!fpathCheck(psDroid->pos, psObj->pos, psPropStats->propulsionType))
3289 {
3290 return -1;
3291 }
3292 return objPosDiffSq(psDroid->pos, psObj->pos);
3293 }
3294