1 /*
2 C-Dogs SDL
3 A port of the legendary (and fun) action/arcade cdogs.
4 Copyright (C) 1995 Ronny Wester
5 Copyright (C) 2003 Jeremy Chin
6 Copyright (C) 2003-2007 Lucas Martin-King
7
8 This program is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 2 of the License, or
11 (at your option) any later version.
12
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, write to the Free Software
20 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
22 This file incorporates work covered by the following copyright and
23 permission notice:
24
25 Copyright (c) 2013-2021 Cong Xu
26 All rights reserved.
27
28 Redistribution and use in source and binary forms, with or without
29 modification, are permitted provided that the following conditions are met:
30
31 Redistributions of source code must retain the above copyright notice, this
32 list of conditions and the following disclaimer.
33 Redistributions in binary form must reproduce the above copyright notice,
34 this list of conditions and the following disclaimer in the documentation
35 and/or other materials provided with the distribution.
36
37 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
38 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
39 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
40 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
41 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
42 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
43 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
44 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
45 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
46 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
47 POSSIBILITY OF SUCH DAMAGE.
48 */
49 #include "objs.h"
50
51 #include <assert.h>
52
53 #include "bullet_class.h"
54 #include "damage.h"
55 #include "gamedata.h"
56 #include "log.h"
57 #include "net_util.h"
58 #include "pickup.h"
59
60 CArray gObjs;
61 CArray gMobObjs;
62 static unsigned int sObjUIDs = 0;
63 static unsigned int sMobObjUIDs = 0;
64
65 // Draw functions
66
MapObjectDraw(GraphicsDevice * g,const int id,const struct vec2i pos)67 static void MapObjectDraw(
68 GraphicsDevice *g, const int id, const struct vec2i pos)
69 {
70 const TObject *obj = CArrayGet(&gObjs, id);
71 CASSERT(obj->isInUse, "Cannot draw non-existent map object");
72 CPicDrawContext c = CPicDrawContextNew();
73 c.Offset = obj->Class->Offset;
74 CPicDraw(g, &obj->thing.CPic, pos, &c);
75 }
76
77 #define NUM_SPALL_PARTICLES 3
78 #define SPALL_IMPULSE_FACTOR 1.0f
DamageObject(const NThingDamage d)79 void DamageObject(const NThingDamage d)
80 {
81 TObject *o = ObjGetByUID(d.UID);
82 // Don't bother if object already destroyed
83 if (o->Health <= 0)
84 {
85 return;
86 }
87
88 // Create damage spall
89 Emitter em;
90 EmitterInit(&em, NULL, svec2_zero(), -0.5f, 0.5f, 1, 4, 0, 0, 0);
91 AddParticle ap;
92 memset(&ap, 0, sizeof ap);
93 ap.Pos = o->thing.Pos;
94 ap.Angle = NAN;
95 ap.Z = 10;
96 ap.Vel =
97 svec2_scale(svec2_normalize(NetToVec2(d.Vel)), SPALL_IMPULSE_FACTOR);
98 // Generate spall
99 for (int i = 0; i < MIN(o->Health, d.Power); i++)
100 {
101 char buf[256];
102 sprintf(buf, "spall%d", rand() % NUM_SPALL_PARTICLES + 1);
103 ap.Class = StrParticleClass(&gParticleClasses, buf);
104 // Choose random colour from object
105 ap.Mask = PicGetRandomColor(CPicGetPic(&o->Class->Pic, 0));
106 EmitterStart(&em, &ap);
107 }
108
109 o->Health -= d.Power;
110
111 // Destroying objects and all the wonderful things that happen
112 if (o->Health <= 0)
113 {
114 if (!gCampaign.IsClient)
115 {
116 GameEvent e = GameEventNew(GAME_EVENT_MAP_OBJECT_REMOVE);
117 e.u.MapObjectRemove.UID = o->uid;
118 e.u.MapObjectRemove.ActorUID = d.SourceActorUID;
119 e.u.MapObjectRemove.Flags = d.Flags;
120 GameEventsEnqueue(&gGameEvents, e);
121 }
122
123 // Exploding spall
124 EmitterInit(&em, NULL, svec2_zero(), -1.0f, 1.0f, 1, 16, 0, 0, 0);
125 ap.Vel = svec2_scale(ap.Vel, 0.5f);
126 for (int i = 0; i < 20; i++)
127 {
128 char buf[256];
129 sprintf(buf, "spall%d", rand() % NUM_SPALL_PARTICLES + 1);
130 ap.Class = StrParticleClass(&gParticleClasses, buf);
131 // Choose random colour from object
132 ap.Mask = PicGetRandomColor(CPicGetPic(&o->Class->Pic, 0));
133 EmitterStart(&em, &ap);
134 }
135 }
136 }
137
AddPickupAtObject(const TObject * o,const PickupType type)138 static void AddPickupAtObject(const TObject *o, const PickupType type)
139 {
140 GameEvent e = GameEventNew(GAME_EVENT_ADD_PICKUP);
141 switch (type)
142 {
143 case PICKUP_HEALTH:
144 if (!ConfigGetBool(&gConfig, "Game.HealthPickups"))
145 {
146 return;
147 }
148 strcpy(e.u.AddPickup.PickupClass, "health");
149 break;
150 case PICKUP_AMMO:
151 if (!gCampaign.Setting.Ammo)
152 {
153 return;
154 }
155 // Pick a random ammo type and spawn it
156 {
157 const int ammoId = rand() % AmmoGetNumClasses(&gAmmo);
158 const Ammo *a = AmmoGetById(&gAmmo, ammoId);
159 sprintf(e.u.AddPickup.PickupClass, "ammo_%s", a->Name);
160 }
161 break;
162 case PICKUP_GUN:
163 // Pick a random mission gun type and spawn it
164 {
165 const int gunId = (int)(rand() % gMission.Weapons.size);
166 const WeaponClass **wc = CArrayGet(&gMission.Weapons, gunId);
167 sprintf(e.u.AddPickup.PickupClass, "gun_%s", (*wc)->name);
168 }
169 break;
170 default:
171 CASSERT(false, "unexpected pickup type");
172 break;
173 }
174 e.u.AddPickup.Pos = Vec2ToNet(o->thing.Pos);
175 e.u.AddPickup.IsRandomSpawned = true;
176 GameEventsEnqueue(&gGameEvents, e);
177 }
178
179 static void PlaceWreck(const char *wreckClass, const Thing *ti);
ObjRemove(const NMapObjectRemove mor)180 void ObjRemove(const NMapObjectRemove mor)
181 {
182 TObject *o = ObjGetByUID(mor.UID);
183 o->Health = 0;
184
185 if (!gCampaign.IsClient)
186 {
187 // Update objective
188 UpdateMissionObjective(
189 &gMission, o->thing.flags, OBJECTIVE_DESTROY, 1);
190 const TActor *a = ActorGetByUID(mor.ActorUID);
191 const int playerUID = a != NULL ? a->PlayerUID : -1;
192 // Extra score if objective
193 if ((o->thing.flags & THING_OBJECTIVE) && playerUID >= 0)
194 {
195 GameEvent e = GameEventNew(GAME_EVENT_SCORE);
196 e.u.Score.PlayerUID = playerUID;
197 e.u.Score.Score = OBJECT_SCORE;
198 GameEventsEnqueue(&gGameEvents, e);
199 }
200
201 // Weapons that go off when this object is destroyed
202 CA_FOREACH(const WeaponClass *, wc, o->Class->DestroyGuns)
203 CASSERT((*wc)->Type != GUNTYPE_MULTI, "unexpected gun type");
204 WeaponClassFire(
205 *wc, o->thing.Pos, 0, 0, mor.Flags, mor.ActorUID, true, false);
206 CA_FOREACH_END()
207
208 // Random chance to add pickups in single player modes
209 if (!IsPVP(gCampaign.Entry.Mode))
210 {
211 CA_FOREACH(
212 const MapObjectDestroySpawn, mods, o->Class->DestroySpawn)
213 const double chance = (double)rand() / RAND_MAX;
214 if (chance < mods->SpawnChance)
215 {
216 AddPickupAtObject(o, mods->Type);
217 }
218 CA_FOREACH_END()
219 }
220
221 if (strlen(o->Class->Wreck.Bullet) > 0)
222 {
223 // A wreck left after the destruction of this object
224 // TODO: doesn't need to be network event
225 GameEvent e = GameEventNew(GAME_EVENT_ADD_BULLET);
226 e.u.AddBullet.UID = MobObjsObjsGetNextUID();
227 strcpy(e.u.AddBullet.BulletClass, o->Class->Wreck.Bullet);
228 e.u.AddBullet.MuzzlePos = Vec2ToNet(o->thing.Pos);
229 GameEventsEnqueue(&gGameEvents, e);
230 }
231 }
232
233 SoundPlayAt(&gSoundDevice, o->Class->Wreck.Sound, o->thing.Pos);
234
235 // If wreck is available spawn it in the exact same position
236 PlaceWreck(o->Class->Wreck.MO, &o->thing);
237
238 ObjDestroy(o);
239
240 if (o->thing.flags & THING_IMPASSABLE)
241 {
242 // Update pathfinding cache if this object blocked a path before
243 PathCacheClear(&gPathCache);
244 }
245 }
PlaceWreck(const char * wreckClass,const Thing * ti)246 static void PlaceWreck(const char *wreckClass, const Thing *ti)
247 {
248 if (wreckClass == NULL)
249 {
250 return;
251 }
252 GameEvent e = GameEventNew(GAME_EVENT_MAP_OBJECT_ADD);
253 e.u.MapObjectAdd.UID = ObjsGetNextUID();
254 const MapObject *mo = StrMapObject(wreckClass);
255 CASSERT(mo != NULL, "cannot find wreck");
256 if (mo == NULL)
257 {
258 LOG(LM_MAIN, LL_ERROR, "wreck (%s) not found", wreckClass);
259 return;
260 }
261 strcpy(e.u.MapObjectAdd.MapObjectClass, mo->Name);
262 e.u.MapObjectAdd.Pos = Vec2ToNet(ti->Pos);
263 e.u.MapObjectAdd.ThingFlags = MapObjectGetFlags(mo);
264 e.u.MapObjectAdd.Health = mo->Health;
265 GameEventsEnqueue(&gGameEvents, e);
266 }
267
CanHit(const BulletClass * b,const int flags,const int uid,const Thing * target)268 bool CanHit(
269 const BulletClass *b, const int flags, const int uid, const Thing *target)
270 {
271 switch (target->kind)
272 {
273 case KIND_CHARACTER:
274 return b->Hit.Flesh.Hit &&
275 CanHitCharacter(flags, uid, CArrayGet(&gActors, target->id));
276 case KIND_OBJECT:
277 return b->Hit.Object.Hit;
278 default:
279 CASSERT(false, "cannot damage tile item kind");
280 break;
281 }
282 return false;
283 }
HasHitSound(const ThingKind targetKind,const int targetUID,const special_damage_e special,const bool allowFriendlyHitSound)284 bool HasHitSound(
285 const ThingKind targetKind, const int targetUID,
286 const special_damage_e special, const bool allowFriendlyHitSound)
287 {
288 switch (targetKind)
289 {
290 case KIND_CHARACTER: {
291 const TActor *a = ActorGetByUID(targetUID);
292 return allowFriendlyHitSound || ActorTakesDamage(a, special);
293 }
294 case KIND_OBJECT:
295 return true;
296 default:
297 CASSERT(false, "cannot damage tile item kind");
298 break;
299 }
300 return false;
301 }
302
303 static void DoDamageThing(
304 const ThingKind targetKind, const int targetUID, const TActor *source,
305 const int flags, const BulletClass *bullet, const bool canDamage,
306 const struct vec2 hitVector);
307 static void DoDamageCharacter(
308 const TActor *actor, const TActor *source, const struct vec2 hitVector,
309 const BulletClass *bullet, const int flags);
Damage(const struct vec2 hitVector,const BulletClass * bullet,const int flags,const TActor * source,const ThingKind targetKind,const int targetUID)310 void Damage(
311 const struct vec2 hitVector, const BulletClass *bullet, const int flags,
312 const TActor *source, const ThingKind targetKind, const int targetUID)
313 {
314 switch (targetKind)
315 {
316 case KIND_CHARACTER: {
317 const TActor *actor = ActorGetByUID(targetUID);
318 DoDamageCharacter(actor, source, hitVector, bullet, flags);
319 }
320 break;
321 case KIND_OBJECT:
322 DoDamageThing(
323 targetKind, targetUID, source, flags, bullet, true, hitVector);
324 break;
325 default:
326 CASSERT(false, "cannot damage tile item kind");
327 break;
328 }
329 }
DoDamageThing(const ThingKind targetKind,const int targetUID,const TActor * source,const int flags,const BulletClass * bullet,const bool canDamage,const struct vec2 hitVector)330 static void DoDamageThing(
331 const ThingKind targetKind, const int targetUID, const TActor *source,
332 const int flags, const BulletClass *bullet, const bool canDamage,
333 const struct vec2 hitVector)
334 {
335 GameEvent e = GameEventNew(GAME_EVENT_THING_DAMAGE);
336 e.u.ThingDamage.UID = targetUID;
337 e.u.ThingDamage.Kind = targetKind;
338 e.u.ThingDamage.SourceActorUID = source ? source->uid : -1;
339 e.u.ThingDamage.Flags = flags;
340 e.u.ThingDamage.Special = bullet->Special.Effect;
341 e.u.ThingDamage.SpecialTicks = bullet->Special.Ticks;
342 e.u.ThingDamage.Power = canDamage ? bullet->Power : 0;
343 e.u.ThingDamage.Mass = bullet->Mass;
344 e.u.ThingDamage.Vel = Vec2ToNet(hitVector);
345 GameEventsEnqueue(&gGameEvents, e);
346 }
DoDamageCharacter(const TActor * actor,const TActor * source,const struct vec2 hitVector,const BulletClass * bullet,const int flags)347 static void DoDamageCharacter(
348 const TActor *actor, const TActor *source, const struct vec2 hitVector,
349 const BulletClass *bullet, const int flags)
350 {
351 // Create events: hit, damage, score
352 CASSERT(actor->isInUse, "Cannot damage nonexistent player");
353 CASSERT(
354 CanHitCharacter(flags, source ? source->uid : -1, actor),
355 "damaging undamageable actor");
356
357 // Shot pushback, based on mass and velocity
358 const float impulseFactor = bullet->Mass * SHOT_IMPULSE_FACTOR *
359 CHARACTER_DEFAULT_MASS /
360 ActorGetCharacter(actor)->Class->Mass;
361 const struct vec2 vel = svec2_scale(hitVector, impulseFactor);
362 if (!svec2_is_zero(vel))
363 {
364 GameEvent ei = GameEventNew(GAME_EVENT_ACTOR_IMPULSE);
365 ei.u.ActorImpulse.UID = actor->uid;
366 ei.u.ActorImpulse.Vel = Vec2ToNet(vel);
367 ei.u.ActorImpulse.Pos = Vec2ToNet(actor->Pos);
368 GameEventsEnqueue(&gGameEvents, ei);
369 }
370
371 const bool canDamage =
372 CanDamageCharacter(flags, source, actor, bullet->Special.Effect);
373
374 DoDamageThing(
375 KIND_CHARACTER, actor->uid, source, flags, bullet, canDamage,
376 hitVector);
377
378 if (canDamage)
379 {
380 // Don't score for friendly, unpiloted vehicle, or player hits
381 const bool isFriendly =
382 (actor->flags & FLAGS_GOOD_GUY) ||
383 actor->pilotUID == -1 ||
384 (!IsPVP(gCampaign.Entry.Mode) && actor->PlayerUID >= 0);
385 if (source && source->PlayerUID >= 0 && bullet->Power != 0 &&
386 !isFriendly)
387 {
388 // Calculate score based on
389 // if they hit a penalty character
390 GameEvent e = GameEventNew(GAME_EVENT_SCORE);
391 e.u.Score.PlayerUID = source->PlayerUID;
392 if (actor->flags & FLAGS_PENALTY)
393 {
394 e.u.Score.Score = PENALTY_MULTIPLIER * bullet->Power;
395 }
396 else
397 {
398 e.u.Score.Score = bullet->Power;
399 }
400 GameEventsEnqueue(&gGameEvents, e);
401 }
402 }
403 }
404
UpdateMobileObjects(int ticks)405 void UpdateMobileObjects(int ticks)
406 {
407 CA_FOREACH(TMobileObject, obj, gMobObjs)
408 if (!obj->isInUse)
409 {
410 continue;
411 }
412 if (!BulletUpdate(obj, ticks) && !gCampaign.IsClient)
413 {
414 GameEvent e = GameEventNew(GAME_EVENT_REMOVE_BULLET);
415 e.u.RemoveBullet.UID = obj->UID;
416 GameEventsEnqueue(&gGameEvents, e);
417 continue;
418 }
419 CA_FOREACH_END()
420 }
421
ObjsInit(void)422 void ObjsInit(void)
423 {
424 CArrayInit(&gObjs, sizeof(TObject));
425 CArrayReserve(&gObjs, 1024);
426 sObjUIDs = 0;
427 }
ObjsTerminate(void)428 void ObjsTerminate(void)
429 {
430 CA_FOREACH(TObject, o, gObjs)
431 if (o->isInUse)
432 {
433 ObjDestroy(o);
434 }
435 CA_FOREACH_END()
436 CArrayTerminate(&gObjs);
437 }
ObjsGetNextUID(void)438 int ObjsGetNextUID(void)
439 {
440 return sObjUIDs++;
441 }
442
ObjAdd(const NMapObjectAdd amo)443 void ObjAdd(const NMapObjectAdd amo)
444 {
445 // Don't add if UID exists
446 if (ObjGetByUID(amo.UID) != NULL)
447 {
448 LOG(LM_MAIN, LL_DEBUG, "object uid(%d) already exists; not adding",
449 (int)amo.UID);
450 return;
451 }
452 // Find an empty slot in object list
453 TObject *o = NULL;
454 int i;
455 for (i = 0; i < (int)gObjs.size; i++)
456 {
457 TObject *obj = CArrayGet(&gObjs, i);
458 if (!obj->isInUse)
459 {
460 o = obj;
461 break;
462 }
463 }
464 if (o == NULL)
465 {
466 TObject obj;
467 memset(&obj, 0, sizeof obj);
468 CArrayPushBack(&gObjs, &obj);
469 i = (int)gObjs.size - 1;
470 o = CArrayGet(&gObjs, i);
471 }
472 memset(o, 0, sizeof *o);
473 o->uid = amo.UID;
474 o->Class = StrMapObject(amo.MapObjectClass);
475 switch (o->Class->Type)
476 {
477 case MAP_OBJECT_TYPE_NORMAL:
478 // do nothing
479 break;
480 case MAP_OBJECT_TYPE_PICKUP_SPAWNER:
481 // do nothing
482 break;
483 case MAP_OBJECT_TYPE_ACTOR_SPAWNER:
484 o->counter = o->Class->u.Character.Counter;
485 break;
486 default:
487 CASSERT(false, "unknown map object type");
488 break;
489 }
490 ThingInit(&o->thing, i, KIND_OBJECT, o->Class->Size, amo.ThingFlags);
491 o->Health = amo.Health;
492 o->thing.CPic = o->Class->Pic;
493 o->thing.CPic.Mask = Net2Color(amo.Mask);
494 if (ColorEquals(o->thing.CPic.Mask, colorTransparent))
495 {
496 o->thing.CPic.Mask = o->Class->Pic.Mask;
497 }
498 o->thing.CPicFunc = MapObjectDraw;
499 MapTryMoveThing(&gMap, &o->thing, NetToVec2(amo.Pos));
500 EmitterInit(
501 &o->damageSmoke, StrParticleClass(&gParticleClasses, "smoke_big"),
502 svec2_zero(), -0.05f, 0.05f, 3, 3, 0, 0, 20);
503 o->isInUse = true;
504 LOG(LM_MAIN, LL_DEBUG,
505 "added object uid(%d) class(%s) health(%d) pos(%d, %d)", (int)amo.UID,
506 amo.MapObjectClass, amo.Health, (int)amo.Pos.x, (int)amo.Pos.y);
507
508 if (o->thing.flags & THING_IMPASSABLE)
509 {
510 // Update pathfinding cache if this object blocked a path before
511 PathCacheClear(&gPathCache);
512 }
513 }
514
ObjDestroy(TObject * o)515 void ObjDestroy(TObject *o)
516 {
517 CASSERT(o->isInUse, "Destroying in-use object");
518 MapRemoveThing(&gMap, &o->thing);
519 o->isInUse = false;
520 }
521
ObjIsDangerous(const TObject * o)522 bool ObjIsDangerous(const TObject *o)
523 {
524 // TODO: something more sophisticated? Check if weapon is dangerous
525 return o->Class->DestroyGuns.size > 0;
526 }
527
UpdateObjects(const int ticks)528 void UpdateObjects(const int ticks)
529 {
530 CA_FOREACH(TObject, obj, gObjs)
531 if (!obj->isInUse)
532 {
533 continue;
534 }
535 ThingUpdate(&obj->thing, ticks);
536 switch (obj->Class->Type)
537 {
538 case MAP_OBJECT_TYPE_NORMAL:
539 // Emit smoke when damaged
540 if (obj->Class->DamageSmoke.HealthThreshold >= 0 &&
541 obj->Health <=
542 obj->Class->Health * obj->Class->DamageSmoke.HealthThreshold)
543 {
544 AddParticle ap;
545 memset(&ap, 0, sizeof ap);
546 ap.Pos = svec2_add(
547 obj->thing.Pos,
548 svec2(
549 RAND_FLOAT(-obj->thing.size.x / 4, obj->thing.size.x / 4),
550 RAND_FLOAT(
551 -obj->thing.size.y / 4, obj->thing.size.y / 4)));
552 ap.Mask = colorWhite;
553 EmitterUpdate(&obj->damageSmoke, &ap, ticks);
554 }
555 break;
556 case MAP_OBJECT_TYPE_PICKUP_SPAWNER:
557 if (gCampaign.IsClient)
558 break;
559 // If counter -1, it is inactive i.e. already spawned
560 if (obj->counter == -1)
561 {
562 break;
563 }
564 obj->counter -= ticks;
565 if (obj->counter <= 0)
566 {
567 // Deactivate spawner by setting counter to -1
568 // Spawner reactivated only when ammo taken
569 obj->counter = -1;
570 GameEvent e = GameEventNew(GAME_EVENT_ADD_PICKUP);
571 strcpy(e.u.AddPickup.PickupClass, obj->Class->u.PickupClass->Name);
572 e.u.AddPickup.SpawnerUID = obj->uid;
573 e.u.AddPickup.Pos = Vec2ToNet(obj->thing.Pos);
574 GameEventsEnqueue(&gGameEvents, e);
575 }
576 break;
577 case MAP_OBJECT_TYPE_ACTOR_SPAWNER:
578 if (gCampaign.IsClient)
579 break;
580 // If counter -1, it is inactive i.e. already spawned
581 if (obj->counter == -1)
582 {
583 break;
584 }
585 obj->counter -= ticks;
586 if (obj->counter <= 0)
587 {
588 // Deactivate spawner by setting counter to -1
589 obj->counter = -1;
590 GameEvent e = GameEventNewActorAdd(
591 obj->thing.Pos,
592 CArrayGet(
593 &gCampaign.Setting.characters.OtherChars,
594 obj->Class->u.Character.CharId),
595 true);
596 e.u.ActorAdd.CharId = obj->Class->u.Character.CharId;
597 GameEventsEnqueue(&gGameEvents, e);
598
599 // Destroy object
600 // TODO: persistent actor spawners
601 e = GameEventNew(GAME_EVENT_MAP_OBJECT_REMOVE);
602 e.u.MapObjectRemove.UID = obj->uid;
603 GameEventsEnqueue(&gGameEvents, e);
604 }
605 break;
606 default:
607 // Do nothing
608 break;
609 }
610 CA_FOREACH_END()
611 }
612
ObjGetByUID(const int uid)613 TObject *ObjGetByUID(const int uid)
614 {
615 CA_FOREACH(TObject, o, gObjs)
616 if (o->uid == uid)
617 {
618 return o;
619 }
620 CA_FOREACH_END()
621 return NULL;
622 }
623
MobObjsInit(void)624 void MobObjsInit(void)
625 {
626 CArrayInit(&gMobObjs, sizeof(TMobileObject));
627 CArrayReserve(&gMobObjs, 1024);
628 sMobObjUIDs = 0;
629 }
MobObjsTerminate(void)630 void MobObjsTerminate(void)
631 {
632 CA_FOREACH(TMobileObject, m, gMobObjs)
633 if (m->isInUse)
634 {
635 BulletDestroy(m);
636 }
637 CA_FOREACH_END()
638 CArrayTerminate(&gMobObjs);
639 }
MobObjsObjsGetNextUID(void)640 int MobObjsObjsGetNextUID(void)
641 {
642 return sMobObjUIDs++;
643 }
MobObjGetByUID(const int uid)644 TMobileObject *MobObjGetByUID(const int uid)
645 {
646 CA_FOREACH(TMobileObject, o, gMobObjs)
647 if (o->UID == uid)
648 {
649 return o;
650 }
651 CA_FOREACH_END()
652 return NULL;
653 }
654