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 "map.h"
50
51 #include <assert.h>
52 #include <stdlib.h>
53 #include <string.h>
54
55 #include "actors.h"
56 #include "algorithms.h"
57 #include "ammo.h"
58 #include "collision/collision.h"
59 #include "config.h"
60 #include "door.h"
61 #include "game_events.h"
62 #include "gamedata.h"
63 #include "log.h"
64 #include "los.h"
65 #include "map_build.h"
66 #include "map_cave.h"
67 #include "map_classic.h"
68 #include "map_static.h"
69 #include "mission.h"
70 #include "net_util.h"
71 #include "objs.h"
72 #include "pic_manager.h"
73 #include "pickup.h"
74 #include "sounds.h"
75 #include "utils.h"
76
77 Map gMap;
78
IMapTypeStr(IMapType t)79 const char *IMapTypeStr(IMapType t)
80 {
81 switch (t)
82 {
83 T2S(MAP_FLOOR, "Floor");
84 T2S(MAP_WALL, "Wall");
85 T2S(MAP_DOOR, "Door");
86 T2S(MAP_ROOM, "Room");
87 T2S(MAP_NOTHING, "Nothing");
88 T2S(MAP_SQUARE, "Square");
89 default:
90 return "";
91 }
92 }
StrIMapType(const char * s)93 IMapType StrIMapType(const char *s)
94 {
95 S2T(MAP_FLOOR, "Floor");
96 S2T(MAP_WALL, "Wall");
97 S2T(MAP_DOOR, "Door");
98 S2T(MAP_ROOM, "Room");
99 S2T(MAP_NOTHING, "Nothing");
100 S2T(MAP_SQUARE, "Square");
101 return MAP_FLOOR;
102 }
103
GetAccessMask(const int k)104 uint16_t GetAccessMask(const int k)
105 {
106 if (k == -1)
107 {
108 return 0;
109 }
110 return MAP_ACCESS_YELLOW << k;
111 }
112
MapGetTile(const Map * map,const struct vec2i pos)113 Tile *MapGetTile(const Map *map, const struct vec2i pos)
114 {
115 if (!MapIsTileIn(map, pos))
116 {
117 return NULL;
118 }
119 return CArrayGet(&map->Tiles, pos.y * map->Size.x + pos.x);
120 }
121
MapIsTileIn(const Map * map,const struct vec2i pos)122 bool MapIsTileIn(const Map *map, const struct vec2i pos)
123 {
124 // Check that the tile pos is within the interior of the map
125 return Rect2iIsInside(Rect2iNew(svec2i_zero(), map->Size), pos);
126 }
MapIsPosIn(const Map * map,const struct vec2 pos)127 static bool MapIsPosIn(const Map *map, const struct vec2 pos)
128 {
129 // Check that the pos is within the interior of the map
130 return pos.x >= 0 && pos.y >= 0 && MapIsTileIn(map, Vec2ToTile(pos));
131 }
132
133 // If thing is in the exit-index exit
134 // If exit = -1 then check any exit
135 // Returns the exit index or -1 if not in any exit
MapIsTileInExit(const Map * map,const Thing * ti,const int exit)136 int MapIsTileInExit(const Map *map, const Thing *ti, const int exit)
137 {
138 if (exit < 0)
139 {
140 for (int i = 0; i < (int)map->exits.size; i++)
141 {
142 if (MapIsTileInExit(map, ti, i) != -1)
143 {
144 return i;
145 }
146 }
147 return -1;
148 }
149 const struct vec2i tilePos = Vec2ToTile(ti->Pos);
150 const Exit *e = CArrayGet(&map->exits, exit);
151 // Outer edge is also in exit
152 const Rect2i r = Rect2iNew(e->R.Pos, svec2i_add(e->R.Size, svec2i_one()));
153 return Rect2iIsInside(r, tilePos) ? exit : -1;
154 }
155
MapGetTileOfItem(Map * map,Thing * t)156 static Tile *MapGetTileOfItem(Map *map, Thing *t)
157 {
158 const struct vec2i pos = Vec2ToTile(t->Pos);
159 return MapGetTile(map, pos);
160 }
161
162 static void AddItemToTile(Thing *t, Tile *tile);
MapTryMoveThing(Map * map,Thing * t,const struct vec2 pos)163 bool MapTryMoveThing(Map *map, Thing *t, const struct vec2 pos)
164 {
165 // Check if we can move to new position
166 if (!MapIsPosIn(map, pos))
167 {
168 return false;
169 }
170 t->LastPos = t->Pos;
171 // When first initialised, position is -1
172 const bool doRemove = t->Pos.x >= 0 && t->Pos.y >= 0;
173 const struct vec2i t1 = Vec2ToTile(t->Pos);
174 const struct vec2i t2 = Vec2ToTile(pos);
175 // If we'll be in the same tile, do nothing
176 if (svec2i_is_equal(t1, t2) && doRemove)
177 {
178 t->Pos = pos;
179 return true;
180 }
181 // Moving; remove from old tile...
182 if (doRemove)
183 {
184 MapRemoveThing(map, t);
185 }
186 // ...move and add to new tile
187 t->Pos = pos;
188 AddItemToTile(t, MapGetTile(map, t2));
189 return true;
190 }
AddItemToTile(Thing * t,Tile * tile)191 static void AddItemToTile(Thing *t, Tile *tile)
192 {
193 ThingId tid;
194 tid.Id = t->id;
195 tid.Kind = t->kind;
196 CASSERT(tid.Id >= 0, "invalid ThingId");
197 CASSERT(tid.Kind >= 0 && tid.Kind <= KIND_PICKUP, "unknown thing kind");
198 CArrayPushBack(&tile->things, &tid);
199 }
200
MapRemoveThing(Map * map,Thing * t)201 void MapRemoveThing(Map *map, Thing *t)
202 {
203 if (!MapIsPosIn(map, t->Pos))
204 {
205 return;
206 }
207 Tile *tile = MapGetTileOfItem(map, t);
208 CA_FOREACH(ThingId, tid, tile->things)
209 if (tid->Id == t->id && tid->Kind == t->kind)
210 {
211 CArrayDelete(&tile->things, _ca_index);
212 return;
213 }
214 CA_FOREACH_END()
215 CASSERT(false, "Did not find element to delete");
216 }
217
MapGetRandomTile(const Map * map)218 struct vec2i MapGetRandomTile(const Map *map)
219 {
220 return svec2i(rand() % map->Size.x, rand() % map->Size.y);
221 }
222
MapGetRandomPos(const Map * map)223 struct vec2 MapGetRandomPos(const Map *map)
224 {
225 for (;;)
226 {
227 const struct vec2 pos = svec2(
228 RAND_FLOAT(0, map->Size.x * TILE_WIDTH),
229 RAND_FLOAT(0, map->Size.y * TILE_HEIGHT));
230 // RAND_FLOAT can sometimes produce the max size
231 if (pos.x < map->Size.x * TILE_WIDTH &&
232 pos.y < map->Size.y * TILE_HEIGHT)
233 {
234 return pos;
235 }
236 }
237 }
238
MapChangeFloor(Map * map,const struct vec2i pos,const TileClass * normal,const TileClass * shadow)239 static void MapChangeFloor(
240 Map *map, const struct vec2i pos, const TileClass *normal,
241 const TileClass *shadow)
242 {
243 const Tile *tAbove = MapGetTile(map, svec2i(pos.x, pos.y - 1));
244 const int canSeeTileAbove = !(pos.y > 0 && TileIsOpaque(tAbove));
245 Tile *t = MapGetTile(map, pos);
246 if (t == NULL || t->Class->Type != TILE_CLASS_FLOOR)
247 {
248 return;
249 }
250 if (!canSeeTileAbove)
251 {
252 t->Class = shadow;
253 }
254 else
255 {
256 t->Class = normal;
257 }
258 }
259
MapHasExits(const Map * m)260 bool MapHasExits(const Map *m)
261 {
262 return m->exits.size > 0 && !IsPVP(gCampaign.Entry.Mode);
263 }
264
265 // Change the perimeter of tiles around the exit area
MapShowExitArea(Map * map,const int i)266 void MapShowExitArea(Map *map, const int i)
267 {
268 const Exit *exit = CArrayGet(&map->exits, i);
269 const int left = exit->R.Pos.x;
270 const int right = left + exit->R.Size.x;
271 const int top = exit->R.Pos.y;
272 const int bottom = top + exit->R.Size.y;
273
274 const TileClass *exitClass = TileClassesGetExit(
275 &gTileClasses, &gPicManager, gMission.missionData->ExitStyle, false);
276 const TileClass *exitShadowClass = TileClassesGetExit(
277 &gTileClasses, &gPicManager, gMission.missionData->ExitStyle, true);
278
279 struct vec2i v;
280 v.y = top;
281 for (v.x = left; v.x <= right; v.x++)
282 {
283 MapChangeFloor(map, v, exitClass, exitShadowClass);
284 }
285 v.y = bottom;
286 for (v.x = left; v.x <= right; v.x++)
287 {
288 MapChangeFloor(map, v, exitClass, exitShadowClass);
289 }
290 v.x = left;
291 for (v.y = top + 1; v.y < bottom; v.y++)
292 {
293 MapChangeFloor(map, v, exitClass, exitShadowClass);
294 }
295 v.x = right;
296 for (v.y = top + 1; v.y < bottom; v.y++)
297 {
298 MapChangeFloor(map, v, exitClass, exitShadowClass);
299 }
300 }
301
MapGetExitPos(const Map * m,const int i)302 struct vec2 MapGetExitPos(const Map *m, const int i)
303 {
304 const Exit *exit = CArrayGet(&m->exits, i);
305 return svec2_assign_vec2i(Vec2iCenterOfTile(Rect2iCenter(exit->R)));
306 }
307
MapHasLockedRooms(const Map * map)308 bool MapHasLockedRooms(const Map *map)
309 {
310 CA_FOREACH(const uint16_t, a, map->access)
311 if (*a != 0)
312 {
313 return true;
314 }
315 CA_FOREACH_END()
316 return false;
317 }
318
MapGetAccessLevel(const Map * map,const struct vec2i pos)319 uint16_t MapGetAccessLevel(const Map *map, const struct vec2i pos)
320 {
321 const uint16_t t =
322 *(uint16_t *)CArrayGet(&map->access, pos.y * map->Size.x + pos.x);
323 return AccessCodeToFlags(t);
324 }
325
MapTileIsInLockedRoom(const Map * map,const struct vec2i tilePos)326 static bool MapTileIsInLockedRoom(const Map *map, const struct vec2i tilePos)
327 {
328 return MapGetAccessLevel(map, tilePos) != 0;
329 }
330
MapPosIsInLockedRoom(const Map * map,const struct vec2 pos)331 bool MapPosIsInLockedRoom(const Map *map, const struct vec2 pos)
332 {
333 const struct vec2i tilePos = Vec2ToTile(pos);
334 return MapTileIsInLockedRoom(map, tilePos);
335 }
336
MapPlaceCollectible(const Mission * m,const int objective,const struct vec2 pos)337 void MapPlaceCollectible(
338 const Mission *m, const int objective, const struct vec2 pos)
339 {
340 const Objective *o = CArrayGet(&m->Objectives, objective);
341 GameEvent e = GameEventNew(GAME_EVENT_ADD_PICKUP);
342 strcpy(e.u.AddPickup.PickupClass, o->u.Pickup->Name);
343 e.u.AddPickup.ThingFlags = ObjectiveToThing(objective);
344 e.u.AddPickup.Pos = Vec2ToNet(pos);
345 GameEventsEnqueue(&gGameEvents, e);
346 }
347
MapGenerateFreePosition(Map * map,const struct vec2i size)348 struct vec2 MapGenerateFreePosition(Map *map, const struct vec2i size)
349 {
350 for (int i = 0; i < 100; i++)
351 {
352 const struct vec2 v = MapGetRandomPos(map);
353 if (!IsCollisionWithWall(v, size))
354 {
355 return v;
356 }
357 }
358 return svec2_zero();
359 }
360
MapPlaceKey(MapBuilder * mb,const struct vec2i tilePos,const int keyIndex)361 void MapPlaceKey(
362 MapBuilder *mb, const struct vec2i tilePos, const int keyIndex)
363 {
364 GameEvent e = GameEventNew(GAME_EVENT_ADD_PICKUP);
365 strcpy(
366 e.u.AddPickup.PickupClass,
367 KeyPickupClass(mb->mission->KeyStyle, keyIndex)->Name);
368 e.u.AddPickup.Pos = Vec2ToNet(Vec2CenterOfTile(tilePos));
369 GameEventsEnqueue(&gGameEvents, e);
370 }
371
GetPlacementRetries(const Map * map,const PlacementAccessFlags paFlags,bool * locked,bool * unlocked)372 static int GetPlacementRetries(
373 const Map *map, const PlacementAccessFlags paFlags, bool *locked,
374 bool *unlocked)
375 {
376 // Try more times if we need to place in a locked room or unlocked place
377 *locked = paFlags == PLACEMENT_ACCESS_LOCKED && MapHasLockedRooms(map);
378 *unlocked = paFlags == PLACEMENT_ACCESS_NOT_LOCKED;
379 return (*locked || *unlocked) ? 1000 : 100;
380 }
381
MapPlaceRandomTile(MapBuilder * mb,const PlacementAccessFlags paFlags,bool (* tryPlaceFunc)(MapBuilder *,const struct vec2i,void *),void * data)382 bool MapPlaceRandomTile(
383 MapBuilder *mb, const PlacementAccessFlags paFlags,
384 bool (*tryPlaceFunc)(MapBuilder *, const struct vec2i, void *), void *data)
385 {
386 // Try a bunch of times to place something on a random tile
387 bool locked, unlocked;
388 const int retries =
389 GetPlacementRetries(mb->Map, paFlags, &locked, &unlocked);
390 for (int i = 0; i < retries; i++)
391 {
392 const struct vec2i tilePos = MapGetRandomTile(mb->Map);
393 const bool isInLocked = MapTileIsInLockedRoom(mb->Map, tilePos);
394 if ((!locked || isInLocked) && (!unlocked || !isInLocked))
395 {
396 if (tryPlaceFunc(mb, tilePos, data))
397 {
398 return true;
399 }
400 }
401 }
402 return false;
403 }
MapPlaceRandomPos(const Map * map,const PlacementAccessFlags paFlags,bool (* tryPlaceFunc)(const Map *,const struct vec2,void *),void * data)404 bool MapPlaceRandomPos(
405 const Map *map, const PlacementAccessFlags paFlags,
406 bool (*tryPlaceFunc)(const Map *, const struct vec2, void *), void *data)
407 {
408 // Try a bunch of times to place something at a random location
409 bool locked, unlocked;
410 const int retries = GetPlacementRetries(map, paFlags, &locked, &unlocked);
411 for (int i = 0; i < retries; i++)
412 {
413 const struct vec2 v = MapGetRandomPos(map);
414 const bool isInLocked = MapPosIsInLockedRoom(map, v);
415 if ((!locked || isInLocked) && (!unlocked || !isInLocked))
416 {
417 if (tryPlaceFunc(map, v, data))
418 {
419 return true;
420 }
421 }
422 }
423 return false;
424 }
425
426 // TODO: use enum instead of flag for map access
AccessCodeToFlags(const uint16_t code)427 uint16_t AccessCodeToFlags(const uint16_t code)
428 {
429 if (code & MAP_ACCESS_RED)
430 return FLAGS_KEYCARD_RED;
431 if (code & MAP_ACCESS_BLUE)
432 return FLAGS_KEYCARD_BLUE;
433 if (code & MAP_ACCESS_GREEN)
434 return FLAGS_KEYCARD_GREEN;
435 if (code & MAP_ACCESS_YELLOW)
436 return FLAGS_KEYCARD_YELLOW;
437 return 0;
438 }
439
440 // Need to check the flags around the door tile because it's the
441 // triggers that contain the right flags
442 // TODO: refactor door
MapGetDoorKeycardFlag(Map * map,struct vec2i pos)443 int MapGetDoorKeycardFlag(Map *map, struct vec2i pos)
444 {
445 int l = MapGetAccessLevel(map, pos);
446 if (l)
447 return l;
448 l = MapGetAccessLevel(map, svec2i(pos.x - 1, pos.y));
449 if (l)
450 return l;
451 l = MapGetAccessLevel(map, svec2i(pos.x + 1, pos.y));
452 if (l)
453 return l;
454 l = MapGetAccessLevel(map, svec2i(pos.x, pos.y - 1));
455 if (l)
456 return l;
457 return MapGetAccessLevel(map, svec2i(pos.x, pos.y + 1));
458 }
459
MapTerminate(Map * map)460 void MapTerminate(Map *map)
461 {
462 CA_FOREACH(Trigger *, t, map->triggers)
463 TriggerTerminate(*t);
464 CA_FOREACH_END()
465 CArrayTerminate(&map->triggers);
466 CArrayTerminate(&map->exits);
467 struct vec2i v;
468 for (v.y = 0; v.y < map->Size.y; v.y++)
469 {
470 for (v.x = 0; v.x < map->Size.x; v.x++)
471 {
472 Tile *t = MapGetTile(map, v);
473 TileDestroy(t);
474 }
475 }
476 CArrayTerminate(&map->Tiles);
477 LOSTerminate(&map->LOS);
478 CArrayTerminate(&map->access);
479 PathCacheTerminate(&gPathCache);
480 }
481
MapInit(Map * map,const struct vec2i size)482 void MapInit(Map *map, const struct vec2i size)
483 {
484 MapTerminate(map);
485
486 // Init map
487 memset(map, 0, sizeof *map);
488 CArrayInit(&map->Tiles, sizeof(Tile));
489 map->Size = size;
490 LOSInit(map);
491 CArrayInitFillZero(&map->access, sizeof(uint16_t), size.x * size.y);
492 CArrayInit(&map->triggers, sizeof(Trigger *));
493 CArrayInit(&map->exits, sizeof(Exit));
494 PathCacheInit(&gPathCache, map);
495
496 struct vec2i v;
497 for (v.y = 0; v.y < map->Size.y; v.y++)
498 {
499 for (v.x = 0; v.x < map->Size.x; v.x++)
500 {
501 Tile t;
502 TileInit(&t);
503 CArrayPushBack(&map->Tiles, &t);
504 }
505 }
506 }
507
MapPrintDebug(const Map * m)508 void MapPrintDebug(const Map *m)
509 {
510 if (LogModuleGetLevel(LM_MAP) > LL_TRACE)
511 {
512 return;
513 }
514 char *buf;
515 CCALLOC(buf, m->Size.x + 1);
516 char *bufP = buf;
517 struct vec2i v;
518 for (v.y = 0; v.y < m->Size.y; v.y++)
519 {
520 for (v.x = 0; v.x < m->Size.x; v.x++)
521 {
522 const TileClass *t = MapGetTile(m, v)->Class;
523 switch (t->Type)
524 {
525 case TILE_CLASS_FLOOR:
526 *bufP++ = t->IsRoom ? '-' : '.';
527 break;
528 case TILE_CLASS_WALL:
529 *bufP++ = '#';
530 break;
531 case TILE_CLASS_DOOR:
532 *bufP++ = '+';
533 break;
534 case TILE_CLASS_NOTHING:
535 *bufP++ = ' ';
536 break;
537 default:
538 *bufP++ = '?';
539 break;
540 }
541 }
542 LOG(LM_MAP, LL_TRACE, buf);
543 *buf = '\0';
544 bufP = buf;
545 }
546 LOG_FLUSH();
547 CFREE(buf);
548 }
549
MapIsPosOKForPlayer(const Map * map,const struct vec2 pos,const bool allowAllTiles)550 bool MapIsPosOKForPlayer(
551 const Map *map, const struct vec2 pos, const bool allowAllTiles)
552 {
553 if (!MapIsTileAreaClear(map, pos, svec2i(ACTOR_W, ACTOR_H)))
554 {
555 return false;
556 }
557 // Don't put players in locked rooms
558 if (MapPosIsInLockedRoom(map, pos))
559 {
560 return false;
561 }
562 const struct vec2i tilePos = Vec2ToTile(pos);
563 const Tile *tile = MapGetTile(map, tilePos);
564 if (tile->Class->Type == TILE_CLASS_FLOOR)
565 {
566 return true;
567 }
568 else if (allowAllTiles)
569 {
570 return TileCanWalk(tile);
571 }
572 return false;
573 }
574
575 // Check if the target position is completely clear
576 // This includes collisions that make the target illegal, such as walls
577 // But it also includes item collisions, whether or not the collisions
578 // are legal, e.g. item pickups, friendly collisions
MapIsTileAreaClear(const Map * map,const struct vec2 pos,const struct vec2i size)579 bool MapIsTileAreaClear(
580 const Map *map, const struct vec2 pos, const struct vec2i size)
581 {
582 // Wall collision
583 if (IsCollisionWithWall(pos, size))
584 {
585 return false;
586 }
587
588 // Item collision
589 const struct vec2i tv = Vec2ToTile(pos);
590 struct vec2i dv;
591 // Check collisions with all other items on this tile, in all 8 directions
592 for (dv.y = -1; dv.y <= 1; dv.y++)
593 {
594 for (dv.x = -1; dv.x <= 1; dv.x++)
595 {
596 const struct vec2i dtv = svec2i_add(tv, dv);
597 if (!MapIsTileIn(map, dtv))
598 {
599 continue;
600 }
601 const CArray *tileThings = &MapGetTile(map, dtv)->things;
602 if (tileThings == NULL)
603 {
604 continue;
605 }
606 for (int i = 0; i < (int)tileThings->size; i++)
607 {
608 const Thing *ti = ThingIdGetThing(CArrayGet(tileThings, i));
609 if (AABBOverlap(pos, ti->Pos, size, ti->size))
610 {
611 if (ti->kind == KIND_OBJECT)
612 {
613 const TObject *tobj = CArrayGet(&gObjs, ti->id);
614 if (tobj->Health <= 0)
615 {
616 continue;
617 }
618 }
619 return false;
620 }
621 }
622 }
623 }
624
625 return true;
626 }
627
MapMarkAsVisited(Map * map,struct vec2i pos)628 void MapMarkAsVisited(Map *map, struct vec2i pos)
629 {
630 Tile *t = MapGetTile(map, pos);
631 if (!t->isVisited && TileCanWalk(t))
632 {
633 map->tilesSeen++;
634 }
635 t->isVisited = true;
636 }
637
MapMarkAllAsVisited(Map * map)638 void MapMarkAllAsVisited(Map *map)
639 {
640 struct vec2i pos;
641 for (pos.y = 0; pos.y < map->Size.y; pos.y++)
642 {
643 for (pos.x = 0; pos.x < map->Size.x; pos.x++)
644 {
645 MapMarkAsVisited(map, pos);
646 }
647 }
648 }
649
MapGetExploredPercentage(Map * map)650 int MapGetExploredPercentage(Map *map)
651 {
652 return (100 * map->tilesSeen) / map->NumExplorableTiles;
653 }
654
MapSearchTileAround(Map * map,struct vec2i start,TileSelectFunc func)655 struct vec2i MapSearchTileAround(
656 Map *map, struct vec2i start, TileSelectFunc func)
657 {
658 if (func(map, start))
659 {
660 return start;
661 }
662 // Search using an expanding box pattern around the goal
663 for (int radius = 1; radius < MAX(map->Size.x, map->Size.y); radius++)
664 {
665 struct vec2i tile;
666 for (tile.x = start.x - radius; tile.x <= start.x + radius; tile.x++)
667 {
668 if (tile.x < 0)
669 continue;
670 if (tile.x >= map->Size.x)
671 break;
672 for (tile.y = start.y - radius; tile.y <= start.y + radius;
673 tile.y++)
674 {
675 if (tile.y < 0)
676 continue;
677 if (tile.y >= map->Size.y)
678 break;
679 // Check box; don't check inside
680 if (tile.x != start.x - radius && tile.x != start.x + radius &&
681 tile.y != start.y - radius && tile.y != start.y + radius)
682 {
683 continue;
684 }
685 if (func(map, tile))
686 {
687 return tile;
688 }
689 }
690 }
691 }
692 // Should never reach this point; something is very wrong
693 CASSERT(false, "failed to find tile around tile");
694 return svec2i_zero();
695 }
MapTileIsUnexplored(Map * map,struct vec2i tile)696 bool MapTileIsUnexplored(Map *map, struct vec2i tile)
697 {
698 const Tile *t = MapGetTile(map, tile);
699 return !t->isVisited && TileCanWalk(t);
700 }
701
702 // Only creates the trigger, but does not place it
MapNewTrigger(Map * map)703 Trigger *MapNewTrigger(Map *map)
704 {
705 Trigger *t = TriggerNew();
706 CArrayPushBack(&map->triggers, &t);
707 t->id = map->triggerId++;
708 return t;
709 }
710