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