/include/fworld.h (3f52dfb14f266304d8ab27b80a24f11a24d6611a) (68954 bytes) (mode 100755) (type blob)

#ifndef FWORLD_H
#define FWORLD_H

#if !defined(FG2_H) && !defined(FG3_H) && !defined(FG4_H)
#  error Include fg2.h, fg3.h, or fg4.h before fworld.h so graphics can be displayed.
#endif

#ifndef _TM_JSON_H_INCLUDED_
#  error Include tm_json.h before fworld.h so maps can be loaded.
#endif

#include <stdio.h>

#include <cmath>

#include <algorithm>
//#include <charconv>
#include <functional>
#include <map>
#include <string>
#include <utility>
#include <vector>

#if defined(FG2_H)
	#define fgl fg2
#elif defined(FG3_H)
	#define fgl fg3
#elif defined(FG4_H)
	#define fgl fg4
#endif

// Moddable fopen.
#ifndef FWORLD_FOPEN
	#define FWORLD_FOPEN fopen
#endif

namespace fworld {

bool verbose = true;

static const double epsilon = 0.0001;

// Animation mode flags.
static const int
	ANIMATION_MANUAL   = -2,
	ANIMATION_AUTOPLAY = -1,
	ANIMATION_RPG      =  0,
	ANIMATION_BEATEMUP =  1;

// Task flags.
static const int
	TASK_NONE = 0,
	TASK_TALK = 1,
	TASK_BUMP = 2,
	TASK_SLEEP = 3,
	TASK_MELEE = 4,
	TASK_DROP = 5;

// TODO: std::pair or std::map?
struct Image {
	std::string imageName;
	fgl::Texture imageTexture;
};

// `mesa` is a flat map class.
template<typename K,typename V>
class mesa : public std::vector<std::pair<K,V>> {
	public:
		bool contains( K key ){
			for( auto &item : *this ){
				if( item.first == key ){
					return true;
				}
			}
			return false;
		}
		V& get( K key ){
			for( auto &item : *this ){
				if( item.first == key ){
					return item.second;
				}
			}
			V* val = nullptr;
			return *val;
		}
		void set( K key, V value ){
			for( auto &item : *this ){
				if( item.first == key ){
					item.second = value;
					return;
				}
			}
			this->push_back( { key, value } );
		}
};

typedef mesa<std::string,double> Inventory;

typedef std::vector<std::vector<int>> MapLayer;

class Entity {
public:
	fgl::Texture sprite;
	fgl::Texture meleeSprite;
	fgl::Texture bribeSprite;
	fgl::Texture shadow;
	fgl::Texture sleep;
	std::string name;
	std::string type;
	std::string itemName;
	std::string dialogue;
	std::string deathDialogue;
	std::string bribeNewDialogue;
	std::string bribeSuccess;
	std::string bribeFail;
	Inventory inventory;
	Inventory bribeItems;
	double speed = 0.0;
	double speedFactor = 0.0;
	double x = 0.0;
	double y = 0.0;
	double last_x = 0.0;
	double last_y = 0.0;
	double collisionRadius = 0.0;
	double fps = 0.0;
	double meleeRecovery = 0.0;
	double age = 0.0;
	double frame = 0.0;
	double boredom = 0.0;
	double stun = 0.0;
	bool walking = false;
	bool staticCollisions = false;
	int health = 0;
	int money = 0;
	int karma = 0;
	int sweetTooth = 0;
	int lactoseTolerance = 0;
	int meleeDamage = 0;
	int animationMode = 0;
	int task = 0;
	int direction = 0;
	int width = 0;
	int height = 0;
#ifdef GRINNINGLIZARD_MICROPATHER_INCLUDED
	MP_VECTOR<void*> path;
#else
	std::vector<void*> path;
#endif
	Entity(){
		inventory = {};
		bribeItems = {};
		path = {};
	}
};

// For depth sorting.
struct EntityIndex {
	double depth;
	long long pos;
	Entity *ptr;
};

class Item {
	public:
		std::map<std::string,std::string> names;
		fgl::Texture icon;
		int stack;
		int flavor;
		int appearance;
		int sweetness;
		int lactose;
		int price;
		int yield;
		std::string script;
		std::map<std::string,int> recipe, byproducts;
		Entity entity;
	Item(){
		entity = Entity();
	}
};

#ifdef GRINNINGLIZARD_MICROPATHER_INCLUDED

class World : micropather::Graph {
	private:
		micropather::MicroPather* pather;
		MP_VECTOR<void*> pathBuffer;
		
		void NodeToXY( void* node, intptr_t* x, intptr_t* y ){
			intptr_t index = (intptr_t)node;
			size_t MapW = mapInfo[0].size();
			*y = index / MapW;
			*x = index - *y * MapW;
		}
		
		void* XYToNode( intptr_t x, intptr_t y ){
			return (void*)( y * mapInfo[0].size() + x );
		}
		
		virtual float LeastCostEstimate( void* nodeStart, void* nodeEnd ){
			intptr_t xStart, yStart, xEnd, yEnd;
			NodeToXY( nodeStart, &xStart, &yStart );
			NodeToXY( nodeEnd, &xEnd, &yEnd );
			
			// Compute the minimum path cost using distance measurement. It is possible
			// to compute the exact minimum path using the fact that you can move only
			// on a straight line or on a diagonal, and this will yield a better result.
			intptr_t dx = xStart - xEnd;
			intptr_t dy = yStart - yEnd;
			return std::sqrt( (float)( dx * dx ) + (float)( dy * dy ) );
		}
		
		virtual void AdjacentCost( void* node, MP_VECTOR<micropather::StateCost>* neighbors ){
			// 5 6 7
			// 4 + 0
			// 3 2 1
			const intptr_t dx[8] = { 1,  1,  0, -1, -1, -1,  0,  1 };
			const intptr_t dy[8] = { 0,  1,  1,  1,  0, -1, -1, -1 };
			const float cost[8] = { 1.0f, 1.41f, 1.0f, 1.41f, 1.0f, 1.41f, 1.0f, 1.41f };
			bool blocked[8];
			
			intptr_t x, y;
			NodeToXY( node, &x, &y );
			
			for( int i = 0; i < 8; i++ ){
				blocked[i] = tileBlocking( x + dx[i], y + dy[i], true );
			}
			
			if( blocked[0] || blocked[2] ){
				blocked[1] = true; // Bottom right.
			}
			if( blocked[2] || blocked[4] ){
				blocked[3] = true; // Bottom left.
			}
			if( blocked[4] || blocked[6] ){
				blocked[5] = true; // Top left.
			}
			if( blocked[6] || blocked[0] ){
				blocked[7] = true; // Top right.
			}
			
			for( int i = 0; i < 8; i++ ){
				intptr_t nx = x + dx[i];
				intptr_t ny = y + dy[i];
				neighbors->push_back( {
					XYToNode( nx, ny ),
					blocked[i] ? FLT_MAX : cost[i]
				} );
			}
		}
		
		virtual void PrintStateInfo( void* node ){
			intptr_t x, y;
			NodeToXY( node, &x, &y );
			printf( "(%ld,%ld)", (long int)x, (long int)y );
		}

#else

class World {
	private:

#endif

		std::string viewToString( JsonStringView str ){
			// Convert JsonStringView to std::string.
			return std::string( str.data, str.size );
		}
		std::vector<EntityIndex> entityIndices;
		bool cursorOverSprite( double posX, double posY, double spriteScale, double sourceW, double sourceH );
		void calculateWorldCursor();
		void tileCollisions( Entity &ent );
		void entityCollisions( size_t pos, bool recurse = true );
		void drawMapLayer( size_t pos );
	public:
		std::string mapFile;
		std::vector<Image> images;
		#ifdef FG3_H
			fgl::InstanceBuffer instanceBuf;
		#endif
		fgl::Texture tileset;
		fgl::Texture tilesetMip;
		std::string tilesetDefinition;
		std::vector<std::vector<unsigned char>> mapInfo;
		std::vector<std::vector<unsigned char>> mapEntities;
		std::vector<MapLayer> map;
		std::vector<Entity> entities;
		std::map<std::string,Item> items;
		linalg::mat<double,4,4> spriteRotationMat;
		linalg::mat<double,4,4> viewMat;
		bool mapChanged;
		int tileSize;
		int screenWidth;
		int screenHeight;
		int followEntity;
		int facingEntity;
		int cursorOverEntity;
		double cursorX;
		double cursorY;
		double cursorWorldX;
		double cursorWorldY;
		double cameraX;
		double cameraY;
		double scale;
		double runningTime;
		double mapFps;
		void (*portalCallback)(std::string);
		long double safeStold( const std::string &str );
		std::string safeDtos( double num );
		fgl::Texture getTexture( std::string basePath, std::string imageName, bool filter );
		bool tileBlocking( long long x, long long y, bool checkEntities = false );
		void loadMap( std::string dataPath, std::string filePath );
		void unloadMap();
		void loadItems( std::string filePath );
		void deleteInventoryZeroes( Inventory &inv );
		char craft( int entIndex, std::vector<std::string> itemNames, int max_slots, std::string &resultName );
		std::string encodeInventory( Inventory inv );
		Inventory decodeInventory( std::string str_inv );
		bool inventoryCanTakeAny( Inventory &inv_a, Inventory &inv_b, int max_slots );
		bool inventoryCanTakeAll( Inventory &inv_a, Inventory &inv_b, int max_slots );
		void transferInventory( Inventory &inv_a, Inventory &inv_b, int max_slots );
		JsonValue getProp( JsonValue parent, const char* prop );
		Entity parseEntity( const JsonValueStruct &o, std::string dataPath );
		std::string serializeEntity( size_t idx );
		void saveMap( std::string filePath );
		void drawSprite( fgl::Texture &tex, double posX, double posY, double imageScale, int sourceX, int sourceY, int sourceW, int sourceH, bool bind = true );
		void moveEntity( Entity &ent, double moveX, double moveY, double d );
		void simulate( double d );
		void drawMap();
		void drawEntities( const std::vector<Entity> &extraEntities = {} );
		void draw( double d );
		void removeEntity( int entity_index );
		void recalculateMapEntities();
		bool solveEntityPath( Entity &ent, long long x, long long y, bool skipBadPath );
		bool reachable( long long startX, long long startY, long long endX, long long endY, long long radius, bool checkEntities = false );
		bool entitiesBumping( Entity &ent1, Entity &ent2 );
		bool entityFacing( Entity &ent1, Entity &ent2 );
		void entityLookAt( Entity &ent1, Entity &ent2 );
		int clearZone( int zone );
		int getEntityZone( int entity_index );
		int xyToDirection( double x, double y );
};

#ifdef GRINNINGLIZARD_MICROPATHER_INCLUDED

bool World::solveEntityPath( Entity &ent, long long x, long long y, bool skipBadPath ){
	if( x < 0 ) x = 0;
	if( y < 0 ) y = 0;
	if( x > (long long)mapInfo[0].size() - 1 ) x = mapInfo[0].size() - 1;
	if( y > (long long)mapInfo.size() - 1 ) y = mapInfo.size() - 1;
	
	intptr_t entX = ent.x + 0.5, entY = ent.y + 0.5;
	if( entX < 0 ) entX = 0;
	if( entY < 0 ) entY = 0;
	if( entX > (intptr_t)mapInfo[0].size() - 1 ) entX = mapInfo[0].size() - 1;
	if( entY > (intptr_t)mapInfo.size() - 1 ) entY = mapInfo.size() - 1;
	
	auto nodeStart = XYToNode( entX, entY );
	auto nodeEnd   = XYToNode( x, y );
	
	bool pathAvailable = false;
	
	if( nodeStart != nodeEnd && !tileBlocking( x, y, true ) ){
		// This is checked before pather->Solve to avoid infinite loops.
		// Complexity is typically (radius * 2 + 1)^2, so with a radius
		// of 15 it's 961 tileBlocking calls + <= 961 floodfill cycles.
		// Doing 2000 things ain't no picnic, so if it lags this number
		// can be lowered.
		pathAvailable = reachable( entX, entY, x, y, 15, true );
	}
	
	float totalCost = 0.0f;
	int result = micropather::MicroPather::NO_SOLUTION;
	
	// If a path is available, solve it.
	if( pathAvailable ){
		result = pather->Solve( nodeStart, nodeEnd, &pathBuffer, &totalCost );
	}
	
	if( result == micropather::MicroPather::SOLVED || !skipBadPath ){
		ent.path.clear();
		// Manually copy pointers from one MP_VECTOR to the other.
		for( size_t i = 0; i < pathBuffer.size(); i++ ){
			ent.path.push_back( pathBuffer[i] );
		}
	}
	pathBuffer.clear();
	
	if( verbose ){
		std::string pathText =
			( ent.name.length() > 0 ? ent.name : "entity" ) + "'s path from " +
			std::to_string( entX ) + "," + std::to_string( entY ) + " to " +
			std::to_string( x ) + "," + std::to_string( y ) + "\n";
		if( result == micropather::MicroPather::SOLVED ){
			printf( "Solved %s", pathText.c_str() );
			for( size_t i = 0; i < ent.path.size(); i++ )
				PrintStateInfo( ent.path[i] );
			printf( "\n" );
		}else{
			printf( "No way to solve %s", pathText.c_str() );
		}
	}
	return result == micropather::MicroPather::SOLVED;
}

#else

bool World::solveEntityPath( Entity &ent, long long x, long long y, bool skipBadPath ){
	(void)ent;
	(void)x;
	(void)y;
	(void)skipBadPath;
	return false;
}

#endif

// Based on floodFillScanlineStack from here:
// https://lodev.org/cgtutor/floodfill.html
bool World::reachable( long long startX, long long startY, long long endX, long long endY, long long radius, bool checkEntities ){
	const long long
		minX = std::max<long long>( startX - radius, 0 ),
		minY = std::max<long long>( startY - radius, 0 ),
		maxX = std::min<long long>( startX + radius, mapInfo[0].size() - 1 ),
		maxY = std::min<long long>( startY + radius, mapInfo.size() - 1 );
	
	const long long
		bw = maxX - minX + 1,
		bh = maxY - minY + 1;
	
	std::vector<bool> blockBuffer( bw * bh );
	
	auto getBlock = [&]( long long x, long long y ){
		return blockBuffer[x - minX + ( y - minY ) * bw];
	};
	
	auto setBlock = [&]( long long x, long long y, bool block ){
		blockBuffer[x - minX + ( y - minY ) * bw] = block;
	};
	
	for( long long iy = minY; iy <= maxY; iy++ ){
		for( long long ix = minX; ix <= maxX; ix++ ){
			if( tileBlocking( ix, iy, checkEntities ) ){
				setBlock( ix, iy, true );
			}
		}
	}
	
	long long
		x = startX,
		y = startY;
	
	bool spanAbove, spanBelow;

	std::vector<long long> stack;
	stack.push_back( x );
	stack.push_back( y );
	while( stack.size() >= 2 ){
		y = stack.back();
		stack.pop_back();
		x = stack.back();
		stack.pop_back();
		while( x >= minX && !getBlock( x, y ) ){
			x--;
		}
		x++;
		spanAbove = spanBelow = false;
		while( x <= maxX && !getBlock( x, y ) ){
			if( x == endX && y == endY ){
				return true;
			}
			setBlock( x, y, true );
			if( !spanAbove && y > minY && !getBlock( x, y - 1 ) ){
				stack.push_back( x );
				stack.push_back( y - 1 );
				spanAbove = true;
			}else if( spanAbove && y > minY && getBlock( x, y - 1 ) ){
				spanAbove = false;
			}
			if( !spanBelow && y < maxY && !getBlock( x, y + 1 ) ){
				stack.push_back( x );
				stack.push_back( y + 1 );
				spanBelow = true;
			}else if( spanBelow && y < maxY && getBlock( x, y + 1 ) ){
				spanBelow = false;
			}
			x++;
		}
	}
	return false;
}

bool World::entitiesBumping( Entity &ent1, Entity &ent2 ){
	// Check if the entities are within a forgiving distance. :/
	return std::abs( ent1.x - ent2.x ) <= 1.0
		&& std::abs( ent1.y - ent2.y ) <= 1.0;
}

// Determine if ent1 is facing ent2 and close enough to be touching.
bool World::entityFacing( Entity &ent1, Entity &ent2 ){
	static const double // Up, down, right, left.
		offset_x[] = { 0.5, 0.5, 1.0, 0.0 },
		offset_y[] = { 0.0, 1.0, 0.5, 0.5 };

	double
		facing_x = ent1.x + offset_x[ent1.direction],
		facing_y = ent1.y + offset_y[ent1.direction];

	// Use a small margin to make the function more generous.
	return facing_x > ent2.x - 0.1 && facing_x < ent2.x + 1.1
		&& facing_y > ent2.y - 0.1 && facing_y < ent2.y + 1.1;
}

// Make ent1 face ent2.
void World::entityLookAt( Entity &ent1, Entity &ent2 ){
	// Return if entity must look at itself.
	if( &ent1 == &ent2 ) return;
	if( ent1.animationMode == ANIMATION_RPG ){
		ent1.direction =
			xyToDirection( ent2.x - ent1.x, ent2.y - ent1.y );
	}else if( ent1.animationMode == ANIMATION_BEATEMUP ){
		if( ent2.x > ent1.x ){
			ent1.direction = 2;
		}else{
			ent1.direction = 3;
		}
	}
}

// Remove all entities from the specified zone.
int World::clearZone( int zone ){
	if( zone < 0 || (size_t)zone >= entities.size() ) return zone;
	auto &zone_ent = entities[zone];
	double
		x1 = zone_ent.x + 1.0,
		y1 = zone_ent.y + 1.0,
		x2 = zone_ent.x + zone_ent.width / tileSize + 1.0,
		y2 = zone_ent.y + zone_ent.height / tileSize + 1.0;
	// Iterate backwards to safely remove entities.
	for( int i = (int)entities.size() - 1; i >= 0; i-- ){
		if( i != zone && i != followEntity ){
			auto &ent = entities[i];
			if( ent.x >= x1 && ent.y >= y1
				&& ent.x < x2 && ent.y < y2
				&& ( ent.type.length() < 4 || ent.type.substr( 0, 4 ) != "WARP" ) ){
				// Remove the entity.
				removeEntity( i );
				// Handle index offsets.
				if( i < zone ) zone--;
			}
		}
	}
	// Return the (possibly) adjusted zone index.
	return zone;
}

// Get the first zone the specified entity is in, or -1 if not in a zone.
int World::getEntityZone( int entity_index ){
	if( entity_index < 0 || (size_t)entity_index >= entities.size() )
		return -1;
	double
		x = entities[entity_index].x,
		y = entities[entity_index].y;
	double s = 1.0 / tileSize;
	for( size_t i = 0; i < entities.size(); i++ ){
		auto &ent = entities[i];
		if( !ent.sprite.success
			&& ent.type.length() >= 4
			&& ent.type.substr( 0, 4 ) == "zone"
			&& x >= ent.x && y >= ent.y
			&& x < ent.x + ent.width * s + 0.5
			&& y < ent.y + ent.height * s + 0.5 ){
			// The specified entity is in the zone.
			return i;
		}
	}
	return -1;
}

int World::xyToDirection( double x, double y ){
	double a = std::atan2( y, x );
	if( std::abs( a - -1.57 ) < 0.786 ){
		return 0; // Up
	}else if( std::abs( a - 1.57 ) < 0.786 ){
		return 1; // Down
	}else if( std::abs( a ) < 0.786 ){
		return 2; // Right
	}else{
		return 3; // Left
	}
}

void World::recalculateMapEntities(){
	#ifdef GRINNINGLIZARD_MICROPATHER_INCLUDED
		pather->Reset();
	#endif
	
	mapEntities = mapInfo;
	
	for( auto &ent : entities ){
		if( ent.staticCollisions ){
			bool collider = std::abs( ent.collisionRadius ) > epsilon;
			intptr_t x = ent.x + 0.5, y = ent.y + 0.5;
			if( x < 0 ) x = 0;
			if( y < 0 ) y = 0;
			if( x > (intptr_t)mapInfo[0].size() - 1 ) x = mapInfo[0].size() - 1;
			if( y > (intptr_t)mapInfo.size() - 1 ) y = mapInfo.size() - 1;
			if( collider ){
				// Only center the entity on the tile if it's a collider.
				ent.x = x;
				ent.y = y;
			}
			auto infoTile = mapInfo[y][x];
			if( infoTile >= 0xB0 && infoTile < 0xC0 ){
				// Map tile is already blocking, so pass it up.
				mapEntities[y][x] = infoTile;
			}else if( infoTile >= 0xA0 && infoTile < 0xB0 && collider ){
				// Only block the tile if it's a collider.
				// Change from "allow" to "block" on the entities layer.
				mapEntities[y][x] = infoTile + 0x10;
			}else{
				// A non-animated, non-colliding static entity.
				mapEntities[y][x] = 0xF0;
			}
		}
	}
}

long double World::safeStold( const std::string &str ){
	char* p;
	long double num = std::strtold( str.c_str(), &p );
	if( *p ){
		// Non-numeric value.
		return 0.0;
	}else{
		// Numeric value.
		return num;
	}
}

std::string safeDtos( double num ){
	// TODO: C++17 only, and only in the ultra deluxe platinum edition.
	/*
	char buf[64];
	char *last = buf + sizeof(buf);
	auto result = std::to_chars( (char*)buf, (char*)last, (double)num );
	if( result.ptr == last ) return "0.0"; // Error.
	return std::string( buf, result.ptr - buf );
	*/
	return std::to_string( num ); // Locale-dependent.
}

fgl::Texture World::getTexture( std::string basePath, std::string imageName, bool filter ){
	if( imageName.length() < 1 ){
		return fgl::newTexture;
	}
	for( Image &i : images ){
		if( i.imageName == imageName ){
			return i.imageTexture;
		}
	}
	#ifdef STBI_INCLUDE_STB_IMAGE_H
		// Load the image from the base path but only record the image's canonical name.
		std::string fileName = basePath + "/" + imageName;
		int imgWidth = 0, imgHeight = 0, imgChannels = 0;
		unsigned char *imgData = nullptr;
		FILE *file = FWORLD_FOPEN( fileName.c_str(), "rb" );
		if( file ){
			imgData = stbi_load_from_file( file, &imgWidth, &imgHeight, &imgChannels, 0 );
			fclose( file );
		}
		fgl::Texture imgTexture = fgl::loadTexture( imgData, imgWidth, imgHeight, imgChannels, false, filter );
		stbi_image_free( imgData );
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
		images.push_back( { imageName, imgTexture } );
		if( !imgTexture.success ){
			fprintf( stderr, "Failed to load image %s\n", fileName.c_str() );
		}
		return imgTexture;
	#else
		fprintf( stderr, "STBI_INCLUDE_STB_IMAGE_H not defined.\n" );
		return fgl::newTexture;
	#endif
}

bool World::tileBlocking( long long x, long long y, bool checkEntities ){
	if( x < 0 ) x = 0;
	if( y < 0 ) y = 0;
	if( x > (long long)mapInfo[0].size() - 1 ) x = mapInfo[0].size() - 1;
	if( y > (long long)mapInfo.size() - 1 ) y = mapInfo.size() - 1;
	
	auto tileType = checkEntities ? mapEntities[y][x] : mapInfo[y][x];
	return tileType >= 0xB0 && tileType < 0xC0;
}

void World::loadMap( std::string dataPath, std::string filePath ){
	// dataPath: The full path (relative or absolute) to the game data.
	// filePath: The full path (relative or absolute) to the map file.
	// The map file can be anywhere, and all required files will be
	// loaded from dataPath.

	unloadMap();

	mapFile = filePath;

	// Load the map JSON file.
	FILE* file = FWORLD_FOPEN( filePath.c_str(), "rb" );
	if( !file ){
		fprintf( stderr, "Failed to open %s\n", filePath.c_str() );
		return;
	}
	std::string text = "";
	char buf[4096];
	while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
		text += std::string( buf, len );
	}
	fclose( file );

	// Parse the map JSON file.
	auto allocatedDocument = jsonAllocateDocumentEx( text.c_str(), text.size(), JSON_READER_ALL );

	if( allocatedDocument.document.error.type != JSON_OK ){
		fprintf( stderr, "JSON error: %s at line %lu:%lu of %s\n\n",
			jsonGetErrorString( allocatedDocument.document.error.type ),
			(long unsigned int)allocatedDocument.document.error.line,
			(long unsigned int)allocatedDocument.document.error.column,
			filePath.c_str() );
		jsonFreeDocument( &allocatedDocument );
		return;
	}

	auto &info = allocatedDocument.document.root; // Map root.

	tileSize = info["tilewidth"].getInt();

	std::vector<unsigned char> tilesetInfo;

	auto ts = info["tilesets"].getArray();
	if( ts.size() > 0 ){
		// Get the path to the tileset JSON file.
		tilesetDefinition = viewToString( ts[0]["source"].getString() );
		std::string tsPath = dataPath + "/" + tilesetDefinition;

		// Load the tileset JSON file.
		file = FWORLD_FOPEN( tsPath.c_str(), "rb" );
		if( !file ){
			fprintf( stderr, "Failed to open %s\n", tsPath.c_str() );
			jsonFreeDocument( &allocatedDocument );
			unloadMap();
			return;
		}
		std::string tsText = "";
		char buf[4096];
		while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
			tsText += std::string( buf, len );
		}
		fclose( file );

		// Parse the tileset JSON file.
		auto tsDocument = jsonAllocateDocumentEx( tsText.c_str(), tsText.size(), JSON_READER_ALL );
		if( tsDocument.document.error.type != JSON_OK ){
			fprintf( stderr, "JSON error: %s at line %lu:%lu of %s\n\n",
				jsonGetErrorString( tsDocument.document.error.type ),
				(long unsigned int)tsDocument.document.error.line,
				(long unsigned int)tsDocument.document.error.column,
				tsPath.c_str() );
			jsonFreeDocument( &tsDocument );
			jsonFreeDocument( &allocatedDocument );
			unloadMap();
			return;
		}

		// Tileset root.
		auto &tsRoot = tsDocument.document.root;

		// Load the image indicated by the tileset definition.
		std::string tsName = viewToString( tsRoot["image"].getString() );
		tileset = getTexture( dataPath, tsName, false );
		if( tsName.find( "." ) != std::string::npos ){
			// Load the tileset mipmap PNG.
			std::string mipName =
				tsName.substr( 0, tsName.find_last_of( "." ) ) + "-mip.png";
			tilesetMip = getTexture( dataPath, mipName, false );
		}
		// Tiled indices start at 1; offsetting to compensate.
		tilesetInfo.resize( tsRoot["tilecount"].getUInt() + 1 );
		auto tileProperties = tsRoot["tileproperties"].getObject();
		auto tiles = tsRoot["tiles"].getObject();
		auto tileArray = tsRoot["tiles"].getArray();
		for( size_t i = 1; i < tilesetInfo.size(); i++ ){
			auto &ti = tilesetInfo[i];
			auto t = tiles[std::to_string( i - 1 ).c_str()];
			// Look up by integer id if key lookup fails.
			if( !t ){
				for( auto candidate : tileArray ){
					if( candidate["id"].getUInt64() == i - 1 ){
						t = candidate;
						break;
					}
				}
			 }
			auto tp = tileProperties[std::to_string( i - 1 ).c_str()];
			// Support the new format, where "tiles" and
			// "tileproperties" are combined.
			if( !tp ) tp = t["properties"];
			// Tiles initially become 0xB0 if "block" or 0xA0 otherwise.
			ti = getProp(tp,"block").getBool() ? 0xB0 : 0xA0;
			// Number of animation frames gets added to 0xA0 or 0xB0.
			size_t ct = 0, id = 0, lastId = 0;
			bool reverse = false;
			for( auto f : t["animation"].getArray() ){
				ct++;
				id = f["tileid"].getUInt64();
				if( id < lastId ) reverse = true;
				lastId = id;
				if( mapFps == 0.0 ){
					mapFps = 1000.0 / (double)f["duration"].getInt();
				}
			}
			// 2-8 frames (forward: 1-7, reverse: 8-E)
			if( ct > 0 ){
				ct--;
			}
			ti += ct + ( reverse ? 7 : 0 );
		}
		jsonFreeDocument( &tsDocument );
	}

	// Loop through map layers.
	for( auto &l : info["layers"].getArray() ){
		size_t width = l["width"].getUInt64();
		size_t height = l["height"].getUInt64();

		map.emplace_back();
		MapLayer &layer = map.back();
		layer.resize( height );

		if( height > mapInfo.size() ){
			mapInfo.resize( height );
		}

		size_t posX = 0, posY = 0;

		if( height > 0 ){
			// Loop through the layer's tiles.
			for( auto &d : l["data"].getArray() ){
				size_t idx = d.getInt();
				layer[posY].push_back( idx );
				// Only add to mapInfo as needed.
				if( posX + 1 > mapInfo[posY].size() ){
					mapInfo[posY].resize( posX + 1 );
				}
				if( tilesetInfo.size() > idx ){
					auto &oldInfo = mapInfo[posY][posX];
					auto newInfo = tilesetInfo[idx];
					// Higher info values have more significant effects.
					if( newInfo > oldInfo ){
						oldInfo = newInfo;
					}
				}
				posX++;
				if( posX >= width ){
					posX = 0;
					posY++;
				}
			}
		}

		// Loop through the layer's objects.
		for( auto &o : l["objects"].getArray() ){
			entities.push_back( parseEntity( o, dataPath ) );
			if( entities.back().type == "spawn" )
				followEntity = entities.size() - 1;
		}
	}

	jsonFreeDocument( &allocatedDocument );

	#ifdef GRINNINGLIZARD_MICROPATHER_INCLUDED
		// MicroPather does not have a destructor, so the size (acreage)
		// of the first map should be similar to that of all subsequent
		// maps loaded.
		if( !pather ){
			size_t allocate = mapInfo[0].size() * mapInfo.size() / 4;
			if( allocate < 250 ){
				allocate = 250;
			}
			// Parameters: class, allocation size, adjacent states, caching
			pather = new micropather::MicroPather( this, allocate, 8, true );
		}
	#endif

	recalculateMapEntities();
}

// Reset the map data.
void World::unloadMap(){
	mapFile = "";

	spriteRotationMat = linalg::rotation_matrix( fgl::eulerToQuat( 0.15, 0.0, 0.0 ) );

	viewMat = linalg::pose_matrix(
		fgl::eulerToQuat( 0.15, 0.0, 0.0 ),
		linalg::vec<double,3>( 0.0, -0.23, 1.3 )
	);

	mapInfo.clear();
	mapEntities.clear();
	map.clear();
	entities.clear();

	mapChanged = false;

	followEntity = -1;
	facingEntity = -1;
}

void World::loadItems( std::string filePath ){
	// Load the item table JSON file.
	FILE* file = FWORLD_FOPEN( filePath.c_str(), "rb" );
	if( !file ){
		fprintf( stderr, "Failed to open %s\n", filePath.c_str() );
		return;
	}
	std::string text = "";
	char buf[4096];
	while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
		text += std::string( buf, len );
	}
	fclose( file );

	// Parse the item table JSON file.
	auto allocatedDocument = jsonAllocateDocumentEx( text.c_str(), text.size(), JSON_READER_ALL );

	if( allocatedDocument.document.error.type != JSON_OK ){
		fprintf( stderr, "JSON error: %s at line %lu:%lu of %s\n\n",
			jsonGetErrorString( allocatedDocument.document.error.type ),
			(long unsigned int)allocatedDocument.document.error.line,
			(long unsigned int)allocatedDocument.document.error.column,
			filePath.c_str() );
		jsonFreeDocument( &allocatedDocument );
		return;
	}
	
	// Store the items directory for loading constituent files.
	std::string dataPath = filePath.substr( 0, filePath.find_last_of( '/' ) + 1 );
	
	// Loop through the item definitions.
	for( auto &itemDef : allocatedDocument.document.root.getObject() ){
		std::string itemName = viewToString( itemDef.name );
		// Find localized names for the item, starting with English.
		std::map<std::string,std::string> localizedNames;
		localizedNames["en"] = itemName;
		for( auto &prop : itemDef.value.getObject() ){
			std::string propName = viewToString( prop.name );
			if( propName.length() > 5 && propName.substr( 0, 5 ) == "name_" ){
				localizedNames[propName.substr( 5 )] =
					viewToString( prop.value.getString() );
			}
		}
		std::string itemIcon = viewToString( itemDef.value["icon"].getString() );
		std::map<std::string,int> itemRecipe, itemByproducts;
		// Read the recipe.
		for( auto &ingredient : itemDef.value["recipe"].getObject() ){
			itemRecipe[viewToString( ingredient.name )] = ingredient.value.getInt();
		}
		// Read the byproducts.
		for( auto &thing : itemDef.value["byproducts"].getObject() ){
			itemByproducts[viewToString( thing.name )] = thing.value.getInt();
		}
		// Parse the entity.
		auto entity = parseEntity( itemDef.value["entity"], dataPath );
		if( entity.itemName.empty() ) entity.itemName = itemName;
		if( !entity.sprite.success ){
			// Entity is a no-go. Generate an entity with the item icon.
			entity.sprite = getTexture( dataPath, itemIcon, false );
			entity.width = entity.sprite.width;
			entity.height = entity.sprite.height;
			entity.type = "pickup";
			entity.inventory.set( itemName, 1.0 );
			entity.name = itemName;
		}
		// Add the item.
		Item item = Item();
		item.names = localizedNames;
		item.icon = getTexture( dataPath, itemIcon, false );
		item.stack = itemDef.value["stack"] ? itemDef.value["stack"].getInt() : 1;
		item.flavor = itemDef.value["flavor"].getInt();
		item.appearance = itemDef.value["appearance"].getInt();
		item.sweetness = itemDef.value["sweetness"].getInt();
		item.lactose = itemDef.value["lactose"].getInt();
		item.price = itemDef.value["price"].getInt();
		item.yield = itemDef.value["yield"].getInt();
		item.script = viewToString( itemDef.value["script"].getString() );
		item.recipe = itemRecipe;
		item.byproducts = itemByproducts;
		item.entity = entity;
		items[itemName] = item;
	}
	
	jsonFreeDocument( &allocatedDocument );
}

void World::deleteInventoryZeroes( Inventory &inv ){
	// Delete zero-quantity items from the inventory.
	for( auto it = inv.begin(); it != inv.end(); ){
		if( it->second == 0.0 ){
			it = inv.erase( it );
		}else{
			it++;
		}
	}
}

char World::craft(
		int entIndex,
		std::vector<std::string> itemNames,
		int max_slots,
		std::string &resultName ){

	auto &ent = entities[entIndex];
	// Find a recipe with the same ingredients.
	std::string target = "";
	resultName = "";
	for( auto &itemInfo : items ){
		if( itemInfo.second.recipe.size() == itemNames.size() ){
			for( auto &name : itemNames ){
				if( itemInfo.second.recipe.find( name )
					== itemInfo.second.recipe.end() ){
					// Mismatch.
					goto nextItem;
				}
			}
			target = itemInfo.first;
			break;
		}
		nextItem:
		continue;
	}
	if( target.length() > 0 ){
		auto &recipe = items[target].recipe;
		// Remove non-consumable items from the list.
		for( size_t i = 0; i < itemNames.size(); i++ ){
			auto &name = itemNames[i];
			if( recipe[name] < 1.0 ){
				// The recipe does not require a quantity of the item.
				itemNames.erase( itemNames.begin() + i );
			}else if( !ent.inventory.contains( name )
				|| ent.inventory.get( name ) < recipe[name] ){
				// There is an insufficient quantity of the item.
				return 'i';
			}
		}
		////////////////////////////////////////////////////////////////
		// Add the target item and byproducts to an Inventory.
		Inventory inv;
		inv.set( target, items[target].yield );
		for( auto &bp : items[target].byproducts ){
			inv.set( bp.first, bp.second );
		}

		// Verify there will be enough space in the entity's inventory.
		Inventory future_inv = ent.inventory;
		for( std::string &name : itemNames ){
			future_inv.get( name ) -= recipe[name];
		}
		deleteInventoryZeroes( future_inv );
		if( !inventoryCanTakeAll( future_inv, inv, max_slots ) ){
			// There is insufficient space.
			return 'i';
		}
		// Good to go! Transfer items to the future inventory.
		transferInventory( inv, future_inv, max_slots );
		// The future is now the present.
		ent.inventory = future_inv;
		////////////////////////////////////////////////////////////////
		// Check if there are sufficient ingredients for a second run.
		bool sufficient = true;
		for( auto &name : itemNames ){
			if(	!ent.inventory.contains( name )
				|| ent.inventory.get( name ) < recipe[name] ){
				// There is no longer enough to make another item.
				sufficient = false;
			}
		}
		////////////////////////////////////////////////////////////////
		// Success.
		resultName = target;
		// Either we're good to continue or stuff is used up.
		return sufficient ? 'a' : 'b';
	}
	// No match.
	return 'n';
}

std::string World::encodeInventory( Inventory inv ){
	std::string str_inv;
	size_t i = 0;
	for( auto &inv_item : inv ){
		str_inv += inv_item.first + ";" + std::to_string( inv_item.second );
		if( i < inv.size() - 1 ){
			str_inv += ",";
		}
		i++;
	}
	return str_inv;
}

Inventory World::decodeInventory( std::string str_inv ){
	Inventory inv;
	auto AddItem = [&]( std::string item ){
		// Split an item declaration between name and count.
		size_t semi_at = item.find( ';' );
		std::pair<std::string,double> inv_item;
		if( semi_at > 0 && semi_at < item.size() - 1 ){
			inv_item = std::make_pair(
				item.substr( 0, semi_at ),
				(double)safeStold( item.substr( semi_at + 1 ) )
			);
			// Only add the item if the count is positive or negative.
			if( inv_item.second != 0.0 ){
				inv.push_back( inv_item );
			}
		}
	};
	size_t start = 0, end = 0;
	while( ( end = str_inv.find( ',', start ) ) != std::string::npos ){
		AddItem( str_inv.substr( start, end - start ) );
		start = end + 1;
	}
	AddItem( str_inv.substr( start ) );
	return inv;
}

// inv_a can fit any one of the items from inv_b.
bool World::inventoryCanTakeAny( Inventory &inv_a, Inventory &inv_b, int max_slots ){
	for( auto &item : inv_b ){
		if( item.second > 0.0 ){
			// The item has a positive quantity.
			if( inv_a.contains( item.first ) ){
				// The item already exists in inventory A.
				if( inv_a.get( item.first ) < items[item.first].stack ){
					// There is room on the stack.
					return true;
				}
			}else if( inv_a.size() < (size_t)max_slots ){
				// inv_a can fit the item in an empty slot.
				return true;
			}
		}
	}
	return false;
}

// inv_a can fit all of the items from inv_b.
bool World::inventoryCanTakeAll( Inventory &inv_a, Inventory &inv_b, int max_slots ){
	for( auto &item : inv_b ){
		if( item.second > 0.0 ){
			// The item has a positive quantity.
			if( inv_a.contains( item.first ) ){
				// The item already exists in inventory A.
				if( inv_a.get( item.first ) >= items[item.first].stack ){
					// No more room on the stack!
					return false;
				}
			}else if( inv_a.size() >= (size_t)max_slots ){
				// No more empty slots!
				return false;
			}
		}
	}
	return true;
}

void World::transferInventory( Inventory &inv_a, Inventory &inv_b, int max_slots ){
	// Move all items possible from inv_a to inv_b.
	for( auto &item_a : inv_a ){
		if( item_a.second > 0.0 ){
			// item_a has a positive quantity.
			if( inv_b.contains( item_a.first ) ){
				// item_a already exists in inventory B.
				for( auto &item_b : inv_b ){
					if( item_a.first == item_b.first // Same item.
						&& item_b.second < items[item_b.first].stack ){
						// There is room on the stack for more.
						double transfer_count = std::min( item_a.second,
							items[item_b.first].stack - item_b.second );
						// Put the items into inventory B.
						item_b.second += transfer_count;
						// Remove the items from inventory A.
						item_a.second -= transfer_count;
					}
				}
			}else{
				// Item does not yet exist in inventory B.
				if( (int)inv_b.size() < max_slots ){
					// Inventory B has a free slot.
					double transfer_count =
						std::min( item_a.second, (double)items[item_a.first].stack );
					// Put the items into inv_b.
					inv_b.push_back( { item_a.first, transfer_count } );
					// Remove the items from inv_a.
					item_a.second -= transfer_count;
				}
			}
		}
	}
	deleteInventoryZeroes( inv_a );
	deleteInventoryZeroes( inv_b );
}

JsonValue World::getProp( JsonValue parent, const char* prop ){
	auto p = parent[prop];
	// Hopefully p exists.
	if( p ) return p;
	// With the new map format, complexity increases exponentially.
	// Find the object for the property and return its value.
	auto a = parent.getArray();
	for( auto &candidate : a ){
		auto key = candidate["name"];
		// Does the object have a "name" field?
		if( key ){
			auto s = key.getString();
			// Does the object's name match prop?
			if( strncmp( s.data, prop, s.size ) == 0 )
				return candidate["value"];
		}
	}
	return p;
}

Entity World::parseEntity( const JsonValueStruct &o, std::string dataPath ){
	std::string type = viewToString( o["type"].getString() );
	auto properties = o["properties"];
	std::string dialogueId = viewToString( getProp(properties,"dialogue").getString() );
	size_t pos = dialogueId.find_last_of( "/\\" );
	if( pos != std::string::npos ){
		dialogueId.erase( 0, pos + 1 );
	}
	std::string deathDialogueId = viewToString( getProp(properties,"deathDialogue").getString() );
	pos = deathDialogueId.find_last_of( "/\\" );
	if( pos != std::string::npos ){
		deathDialogueId.erase( 0, pos + 1 );
	}
	std::string bribeNewDialogueId = viewToString( getProp(properties,"bribeNewDialogue").getString() );
	pos = bribeNewDialogueId.find_last_of( "/\\" );
	if( pos != std::string::npos ){
		bribeNewDialogueId.erase( 0, pos + 1 );
	}
	double speed = getProp(properties,"speed").getDouble();
	// Default to 32px tile size if 0. TODO: This is not ideal.
	if( tileSize == 0 ) tileSize = 32;
	// Convert pixels to tile units.
	double x = o["x"].getDouble() / (double)tileSize;
	double y = o["y"].getDouble() / (double)tileSize - 1.0;
	bool staticCol = type == "static" || type == "sentry"
		|| type == "flora" || type == "pickup" || type == "regrow";
	double collisionSize = getProp(properties,"collisionSize").getDouble();
	if( getProp(properties,"block").getBool() ){
		staticCol = true;
		if( !collisionSize ) collisionSize = (double)tileSize;
	}
	collisionSize *= 0.5 / (double)tileSize;
	double fps = getProp(properties,"fps").getDouble();
	int spriteWidth = getProp(properties,"spriteWidth").getInt();
	int spriteHeight = getProp(properties,"spriteHeight").getInt();
	// Construct the entity.
	Entity ent = Entity();
	ent.sprite = getTexture( dataPath, viewToString( getProp(properties,"spriteImage").getString() ), false );
	ent.meleeSprite = getTexture( dataPath, viewToString( getProp(properties,"meleeSpriteImage").getString() ), false );
	ent.bribeSprite = getTexture( dataPath, viewToString( getProp(properties,"bribeSpriteImage").getString() ), false );
	ent.shadow = getTexture( dataPath, viewToString( getProp(properties,"shadowImage").getString() ), true );
	ent.sleep = getTexture( dataPath, viewToString( getProp(properties,"sleepImage").getString() ), false );
	ent.name = viewToString( o["name"].getString() );
	ent.type = type;
	ent.itemName = viewToString( getProp(properties,"itemName").getString() );
	ent.dialogue = dialogueId;
	ent.deathDialogue = deathDialogueId;
	ent.bribeNewDialogue = bribeNewDialogueId;
	ent.bribeSuccess = viewToString( getProp(properties,"bribeSuccess").getString() );
	ent.bribeFail = viewToString( getProp(properties,"bribeFail").getString() );
	ent.inventory = decodeInventory( viewToString( getProp(properties,"inventory").getString() ) );
	ent.bribeItems = decodeInventory( viewToString( getProp(properties,"bribeItems").getString() ) );
	ent.speed = speed ? speed : 3.0;
	ent.speedFactor = 1.0;
	ent.x = x;
	ent.y = y;
	ent.last_x = x;
	ent.last_y = y;
	ent.collisionRadius = collisionSize;
	ent.fps = fps ? fps : mapFps;
	ent.meleeRecovery = getProp(properties,"meleeRecovery").getDouble();
	ent.age = getProp(properties,"age").getDouble();
	ent.staticCollisions = staticCol;
	ent.health = getProp(properties,"health").getInt();
	ent.money = getProp(properties,"money").getInt();
	ent.karma = getProp(properties,"karma").getInt();
	ent.sweetTooth = getProp(properties,"sweetTooth").getInt();
	ent.lactoseTolerance = getProp(properties,"lactoseTolerance").getInt();
	ent.meleeDamage = getProp(properties,"meleeDamage").getInt();
	ent.animationMode = getProp(properties,"animationMode").getInt();
	ent.task = type == "corpse" ? TASK_SLEEP : TASK_NONE;
	ent.direction = getProp(properties,"direction").getInt();
	ent.width = spriteWidth ? spriteWidth : o["width"].getInt();
	ent.height = spriteHeight ? spriteHeight : o["height"].getInt();
	ent.path = {};
	return ent;
}

std::string World::serializeEntity( size_t idx ){
	Entity &ent = entities[idx];
	// Convert tile units to pixels.
	std::string str_x = std::to_string( (long long)( ent.x * (double)tileSize ) );
	std::string str_y = std::to_string( (long long)( ( ent.y + 1.0 ) * (double)tileSize ) );
	// Convert the entity's collision radius to a diameter in pixels.
	std::string str_col = std::to_string( ent.collisionRadius * 2.0 * (double)tileSize );
	// Prepend the dialogue folder to the file name if there is dialogue.
	std::string str_dialogue = ent.dialogue.size() ? ent.dialogue : "";
	std::string str_deathDialogue = ent.deathDialogue.size() ? ent.deathDialogue : "";
	std::string str_bribeNewDialogue = ent.bribeNewDialogue.size() ? ent.bribeNewDialogue : "";
	// Find the file names for the entity's textures.
	auto FindTextureName = [&]( fgl::Texture tex ){
		if( tex.success ){
			for( auto &img : images ){
				if( tex.texture == img.imageTexture.texture ){
					return img.imageName;
				}
			}
		}
		return std::string();
	};
	std::string str_meleesprite = FindTextureName( ent.meleeSprite );
	std::string str_bribesprite = FindTextureName( ent.bribeSprite );
	std::string str_shadow = FindTextureName( ent.shadow );
	std::string str_sprite = FindTextureName( ent.sprite );
	std::string str_sleep = FindTextureName( ent.sleep );
	// Put the entity's attributes into a string.
	return std::string( "{" )
		+ "\n\"id\": " + std::to_string( idx ) + ","
		+ "\n\"gid\": 1,"
		+ "\n\"name\": \"" + ent.name + "\","
		+ "\n\"type\": \"" + ent.type + "\","
		+ "\n\"visible\": true,"
		+ "\n\"x\": " + str_x + ","
		+ "\n\"y\": " + str_y + ","
		+ "\n\"width\": " + std::to_string( tileSize ) + ","
		+ "\n\"height\": " + std::to_string( tileSize ) + ","
		+ "\n\"properties\": {"
		+ "\n \"age\": " + std::to_string( ent.age ) + ","
		+ "\n \"animationMode\": " + std::to_string( ent.animationMode ) + ","
		+ "\n \"block\": " + ( ent.staticCollisions && ent.collisionRadius > epsilon ? "true" : "false" ) + ","
		+ "\n \"bribeFail\": \"" + ent.bribeFail + "\","
		+ "\n \"bribeItems\": \"" + encodeInventory( ent.bribeItems ) + "\","
		+ "\n \"bribeNewDialogue\": \"" + str_bribeNewDialogue + "\","
		+ "\n \"bribeSpriteImage\": \"" + str_bribesprite + "\","
		+ "\n \"bribeSuccess\": \"" + ent.bribeSuccess + "\","
		+ "\n \"collisionSize\": " + str_col + ","
		+ "\n \"deathDialogue\": \"" + str_deathDialogue + "\","
		+ "\n \"dialogue\": \"" + str_dialogue + "\","
		+ "\n \"direction\": " + std::to_string( ent.direction ) + ","
		+ "\n \"fps\": " + std::to_string( ent.fps ) + ","
		+ "\n \"health\": " + std::to_string( ent.health ) + ","
		+ "\n \"inventory\": \"" + encodeInventory( ent.inventory ) + "\","
		+ "\n \"itemName\": \"" + ent.itemName + "\","
		+ "\n \"karma\": " + std::to_string( ent.karma ) + ","
		+ "\n \"lactoseTolerance\": " + std::to_string( ent.lactoseTolerance ) + ","
		+ "\n \"meleeDamage\": " + std::to_string( ent.meleeDamage ) + ","
		+ "\n \"meleeRecovery\": " + std::to_string( ent.meleeRecovery ) + ","
		+ "\n \"meleeSpriteImage\": \"" + str_meleesprite + "\","
		+ "\n \"money\": " + std::to_string( ent.money ) + ","
		+ "\n \"shadowImage\": \"" + str_shadow + "\","
		+ "\n \"sleepImage\": \"" + str_sleep + "\","
		+ "\n \"speed\": " + std::to_string( ent.speed ) + ","
		+ "\n \"spriteHeight\": " + std::to_string( ent.height ) + ","
		+ "\n \"spriteImage\": \"" + str_sprite + "\","
		+ "\n \"spriteWidth\": " + std::to_string( ent.width ) + ","
		+ "\n \"sweetTooth\": " + std::to_string( ent.sweetTooth )
		+ "\n},"
		+ "\n\"propertytypes\": {"
		+ "\n \"age\": \"float\","
		+ "\n \"animationMode\": \"int\","
		+ "\n \"block\": \"bool\","
		+ "\n \"bribeFail\": \"string\","
		+ "\n \"bribeItems\": \"string\","
		+ "\n \"bribeNewDialogue\": \"file\","
		+ "\n \"bribeSpriteImage\": \"file\","
		+ "\n \"bribeSuccess\": \"string\","
		+ "\n \"collisionSize\": \"float\","
		+ "\n \"deathDialogue\": \"file\","
		+ "\n \"dialogue\": \"file\","
		+ "\n \"direction\": \"int\","
		+ "\n \"fps\": \"float\","
		+ "\n \"health\": \"int\","
		+ "\n \"inventory\": \"string\","
		+ "\n \"itemName\": \"string\","
		+ "\n \"karma\": \"int\","
		+ "\n \"lactoseTolerance\": \"int\","
		+ "\n \"meleeDamage\": \"int\","
		+ "\n \"meleeRecovery\": \"float\","
		+ "\n \"meleeSpriteImage\": \"file\","
		+ "\n \"money\": \"int\","
		+ "\n \"shadowImage\": \"file\","
		+ "\n \"sleepImage\": \"file\","
		+ "\n \"speed\": \"float\","
		+ "\n \"spriteHeight\": \"int\","
		+ "\n \"spriteImage\": \"file\","
		+ "\n \"spriteWidth\": \"int\","
		+ "\n \"sweetTooth\": \"int\""
		+ "\n}"
		+ "\n}";
}

void World::saveMap( std::string filePath ){
	std::string result = std::string( "{" )
		+ "\n\"tiledversion\": \"1.0.3\"," // Change this as necessary.
		+ "\n\"version\": 1,"
		+ "\n\"type\": \"map\","
		+ "\n\"orientation\": \"orthogonal\","
		+ "\n\"renderorder\": \"right-down\","
		+ "\n\"width\": " + std::to_string( mapInfo[0].size() ) + ","
		+ "\n\"height\": " + std::to_string( mapInfo.size() ) + ","
		+ "\n\"tilewidth\": " + std::to_string( tileSize ) + ","
		+ "\n\"tileheight\": " + std::to_string( tileSize ) + ","
		+ "\n\"nextobjectid\": " + std::to_string( entities.size() ) + ","
		+ "\n\"tilesets\": [{"
		+ "\n\"firstgid\": 1," // May change to 0 in later Tiled versions.
		+ "\n\"source\": \"" + tilesetDefinition + "\""
		+ "\n}],"
		+ "\n\"layers\": [\n";
	size_t layerIndex = 0;
	for( auto &layer : map ){
		if( layer.empty() ){
			continue;
		}
		layerIndex++;
		result += std::string( "{" )
			+ "\n\"name\": \"Tile Layer " + std::to_string( layerIndex ) + "\","
			+ "\n\"type\": \"tilelayer\","
			+ "\n\"visible\": true,"
			+ "\n\"opacity\": 1,"
			+ "\n\"x\": 0,"
			+ "\n\"y\": 0,"
			+ "\n\"width\": " + std::to_string( layer[0].size() ) + ","
			+ "\n\"height\": " + std::to_string( layer.size() ) + ","
			+ "\n\"data\": [\n";
		for( size_t y = 0; y < layer.size(); y++ ){
			auto &row = layer[y];
			for( size_t x = 0; x < row.size(); x++ ){
				result += std::to_string( row[x] );
				if( x < row.size() - 1 || y < layer.size() - 1 ){
					result += ", ";
				}
			}
		}
		result += "\n]\n},\n";
	}
	result += std::string( "{" )
		+ "\n\"name\": \"Object Layer 1\","
		+ "\n\"type\": \"objectgroup\","
		+ "\n\"visible\": true,"
		+ "\n\"opacity\": 1,"
		+ "\n\"x\": 0,"
		+ "\n\"y\": 0,"
		+ "\n\"draworder\": \"topdown\","
		+ "\n\"objects\": [\n";
	for( size_t i = 0; i < entities.size(); i++ ){
		result += serializeEntity( i );
		if( i < entities.size() - 1 ){
			result += ",\n";
		}
	}
	result += "]\n}\n]\n}\n";
	
	// Save the map JSON file.
	FILE* file = FWORLD_FOPEN( filePath.c_str(), "wb" );
	if( file ){
		// Treat the file as a regular string for string-like output.
		fputs( result.c_str(), file );
		fclose( file );
	}else{
		fprintf( stderr, "Failed to open %s for writing\n", filePath.c_str() );
	}
}

void World::drawSprite( fgl::Texture &tex, double posX, double posY, double imageScale, int sourceX, int sourceY, int sourceW, int sourceH, bool bind ){
	bool flipX = false, flipY = false;
	if( sourceW < 0 ){
		sourceW *= -1;
		flipX = true;
	}
	if( sourceH < 0 ){
		sourceH *= -1;
		flipY = true;
	}
	posX += (double)sourceW * 0.5 * imageScale;
	posY += (double)sourceH * 0.5 * imageScale;
	// Sorry, but this over-stretching thing is the only way to hide tile seams on low-precision OpenGL implementations.
	fgl::setTextureMatrix( linalg::mul(
		linalg::translation_matrix( linalg::vec<double,3>(
			(double)sourceX / (double)tex.width + 0.0001,
			(double)sourceY / (double)tex.height + 0.0001,
			0.0
		) ),
		linalg::scaling_matrix( linalg::vec<double,3>(
			(double)sourceW / (double)tex.width * ( flipX ? -0.995 : 0.995 ),
			(double)sourceH / (double)tex.height * ( flipY ? -0.995 : 0.995 ),
			1.0
		) )
	) );
	linalg::mat<double,4,4> m = linalg::mul(
		linalg::translation_matrix( linalg::vec<double,3>(
			posX / (double)screenHeight * 2.0 - (double)screenWidth / (double)screenHeight,
			posY / (double)screenHeight * -2.0 + 1.0,
			bind ? (double)sourceH * imageScale / (double)screenHeight * 0.1 : 0.0
		) ),
		linalg::mul(
			bind ? spriteRotationMat : linalg::identity,
			linalg::scaling_matrix( linalg::vec<double,3>(
				sourceW / (double)screenHeight * imageScale * 2.0,
				sourceH / (double)screenHeight * imageScale * 2.0,
				1.0
			) )
		)
	);
	#ifdef FG3_H
	if( bind ){
	#endif
		// Draw directly.
		if( !tex.success ) return;
		fgl::setTexture( tex, 0 );
		fgl::drawMesh(
			fgl::planeMesh,
			m,
			//linalg::identity,
			viewMat,
			//linalg::scaling_matrix( linalg::vec<double,3>( (double)screenHeight / (double)screenWidth, 1.0, 1.0 ) )
			linalg::perspective_matrix( 68.5 * 0.01745, (double)screenWidth / (double)screenHeight, 0.1, 44.0 )
		);
		fgl::setTextureMatrix( linalg::identity );
	#ifdef FG3_H
	}else{
		// Draw instanced.
		instanceBuf.push( fgl::fogColor, m, fgl::getTextureMatrix() );
	}
	#endif
}

bool World::cursorOverSprite( double posX, double posY, double spriteScale, double sourceW, double sourceH ){
	// Perform the same transformations drawSprite does.
	auto modelMat = linalg::mul(
		linalg::translation_matrix( linalg::vec<double,3>(
			( posX + sourceW * 0.5 * spriteScale ) / (double)screenHeight * 2.0 - (double)screenWidth / (double)screenHeight,
			( posY + sourceH * 0.5 * spriteScale ) / (double)screenHeight * -2.0 + 1.0,
			sourceH * spriteScale / (double)screenHeight * 0.099
		) ),
		linalg::mul(
			spriteRotationMat,
			linalg::scaling_matrix( linalg::vec<double,3>(
				( sourceW + 1.0 ) / (double)screenHeight * spriteScale * 2.0,
				( sourceH + 1.0 ) / (double)screenHeight * spriteScale * 2.0,
				1.0
			) )
		)
	);
	auto projMat = linalg::perspective_matrix( 68.5 * 0.01745, (double)screenWidth / (double)screenHeight, 0.1, 44.0 );
	auto mvpMat = linalg::mul( projMat, linalg::mul( linalg::inverse( viewMat ), modelMat ) );
	// Transform sprite corners the same way OpenGL does.
	auto topLeft = linalg::mul( mvpMat, linalg::vec<double,4>( -0.5, 0.5, 0.0, 1.0 ) );
	topLeft /= topLeft.w;
	auto bottomRight = linalg::mul( mvpMat, linalg::vec<double,4>( 0.5, -0.5, 0.0, 1.0 ) );
	bottomRight /= bottomRight.w;
	double
		x0 = ( topLeft.x + 1.0 ) * 0.5 * (double)screenWidth,
		y0 = ( 1.0 - topLeft.y ) * 0.5 * (double)screenHeight,
		x1 = ( bottomRight.x + 1.0 ) * 0.5 * (double)screenWidth,
		y1 = ( 1.0 - bottomRight.y ) * 0.5 * (double)screenHeight;
	// Compare with the cursor position.
	return cursorX >= x0 && cursorY >= y0 && cursorX < x1 && cursorY < y1;
}

void World::calculateWorldCursor(){
	// https://www.gamedev.net/forums/topic/323817-ray-plane-intersection-and-ray-triangle-intersection/
	// https://antongerdelan.net/opengl/raycasting.html
	// https://www.physicsforums.com/threads/what-does-this-mean-when-it-is-over-a-value.406598/
	// https://en.wikipedia.org/wiki/Line–plane_intersection
	// https://en.wikipedia.org/wiki/Vector_notation
	// https://www.cs.princeton.edu/courses/archive/fall00/cs426/lectures/raycast/sld008.htm
	// https://www.cs.princeton.edu/courses/archive/fall00/cs426/lectures/raycast/sld017.htm
	// https://sites.math.washington.edu/~king/coursedir/m445w04/notes/vector/equations.html
	// https://www.songho.ca/math/plane/plane.html

	// Get the projection matrix.
	auto projMat = linalg::perspective_matrix( 68.5 * 0.01745, (double)screenWidth / (double)screenHeight, 0.1, 44.0 );

	// Transform screen coordinates to clip coordinates.
	double cursorClipX = ( 2.0 * cursorX ) / (double)screenWidth - 1.0;
	double cursorClipY = 1.0 - ( 2.0 * cursorY ) / (double)screenHeight;

	// Eye ray part 1. Multiply inverse projection matrix by (modified) clip coordinates.
	auto eyeRay = linalg::mul(
		linalg::inverse( projMat ),
		linalg::vec<double,4>( cursorClipX, -1.0 * cursorClipY, -1.0, 1.0 )
	);

	// Eye ray part 2. Set ray Z and W coordinates to -1.0 and 0.0.
	eyeRay = linalg::vec<double,4>( eyeRay.x, eyeRay.y, -1.0, 0.0 );

	// World ray V is inverse view matrix * eye ray.
	auto V = linalg::mul(
		linalg::inverse( viewMat ),
		eyeRay
	).xyz();

	// Normalize world ray.
	V = linalg::normalize( V );

	// Ray origin.
	auto O = viewMat[3].xyz();

	// P points along the plane.
	auto P = linalg::vec<double,3>( 0.0, 1.0, 0.0 );

	// Plane normal points away from camera.
	auto N = linalg::vec<double,3>( 0.0, 0.0, -1.0 );

	double d = -1.0 * linalg::dot( P, N );

	// Length of ray to intersection.
	double t = -1.0 * ( linalg::dot( O, N ) + d ) / linalg::dot( V, N );

	auto worldCursor = O + V * linalg::vec<double,3>( 1.0, -1.0, 1.0 ) * t;

	double tilesPerScreen = (double)screenHeight / scale / (double)tileSize;

	cursorWorldX = worldCursor.x * tilesPerScreen * 0.5 + cameraX;
	cursorWorldY = worldCursor.y * tilesPerScreen * -0.5 + cameraY;
}

void World::drawMapLayer( size_t pos ){
	if( tileset.width < 1 ){
		fprintf( stderr, "Cannot draw map without a valid tileset image.\n" );
		fgl::end();
	}

	// 840 is evenly divisible by everything here.
	int f = std::fmod( runningTime, 840.0 / mapFps ) * mapFps;
	int animOffsets[16] = {
		0,
		f % 2,
		f % 3,
		f % 4,
		f % 5,
		f % 6,
		f % 7,
		f % 8,
		0 - ( f % 2 ),
		0 - ( f % 3 ),
		0 - ( f % 4 ),
		0 - ( f % 5 ),
		0 - ( f % 6 ),
		0 - ( f % 7 ),
		0 - ( f % 8 ),
		0 // TODO: Animation that skips tiles.
	};

	double offsetX = (double)screenWidth * 0.5 - cameraX * (double)tileSize * scale;
	double offsetY = (double)screenHeight * 0.5 - cameraY * (double)tileSize * scale;

	double tileScreenSize = (double)tileSize * scale;
	double tileScreenScale = std::ceil( tileScreenSize ) / tileSize;

	// Switch to the unlit instance pipeline.
	fgl::Pipeline old_pipeline = fgl::drawPipeline;
	fgl::setPipeline( fgl::unlitInstancePipeline );

	// Use the tileset mipmap when less than half scale.
	// Don't overthink mip levels. Used for minimaps and tiny screens.
	fgl::setTexture(
		( tilesetMip.success && scale < 0.5 ) ? tilesetMip : tileset,
		0
	);

	MapLayer &layer = map[pos];

	if( pos == 0 ){
		// For performance.
		glDisable( GL_BLEND );
	}else{
		glEnable( GL_BLEND );
	}

	for( size_t y = 0; y < layer.size(); y++ ){
		double tileScreenY = (double)y * tileScreenSize + offsetY;
		if( tileScreenY + tileScreenSize > 0 && tileScreenY < screenHeight ){
			for( size_t x = 0; x < layer[y].size(); x++ ){
				double tileScreenX = (double)x * tileScreenSize + offsetX;
				if( tileScreenX + tileScreenSize > 0 && tileScreenX < screenWidth ){
					int tileNum = layer[y][x] - 1;
					if( tileNum > 0 ){
						auto ti = mapInfo[y][x];
						if( ti >= 0xA0 && ti < 0xC0 ){
							ti -= ti >= 0xB0 ? 0xB0 : 0xA0;
							tileNum += animOffsets[ti];
						}
						drawSprite(
							tileset,
							tileScreenX,
							tileScreenY,
							tileScreenScale,
							( tileNum * tileSize ) % tileset.width,
							std::floor( tileNum * tileSize / tileset.width ) * tileSize,
							tileSize,
							tileSize,
							false // Turning rebind off enables instancing.
						);
					}
				}
			}
		}
	}

	#ifdef FG3_H
		// Disable fog for the unlit instance pipeline.
		fgl::Color old_fog = fgl::fogColor;
		fgl::setFog( { 0.0f, 0.0f, 0.0f, 0.0f } );

		// Draw and clear the instance buffer.
		instanceBuf.upload();
		auto projMat = linalg::perspective_matrix( 68.5 * 0.01745, (double)screenWidth / (double)screenHeight, 0.1, 44.0 );
		instanceBuf.draw( fgl::planeMesh, viewMat, projMat, fgl::getLightMatrix() );
		instanceBuf.clear();

		// Set fog to its old color.
		fgl::setFog( old_fog );

		// Switch to the old pipeline.
		fgl::setPipeline( old_pipeline );
	#endif
}

void World::moveEntity( Entity &ent, double moveX, double moveY, double d ){
	// Infer stick-based movement if axis measurements are non-uniform.
	bool use_stick =
		( std::abs( moveX ) > epsilon && std::abs( moveX ) < 1.0 ) ||
		( std::abs( moveY ) > epsilon && std::abs( moveY ) < 1.0 );
	auto newVec = linalg::vec<double,2>( moveX, moveY );
	if( linalg::length( newVec ) > epsilon ){
		ent.walking = true;
		newVec = linalg::normalize( newVec );
		ent.x += newVec.x * ent.speed * ent.speedFactor * d;
		ent.y += newVec.y * ent.speed * ent.speedFactor * d;
	}else if( ent.path.size() == 0 ){
		ent.walking = false;
		if( ent.animationMode != ANIMATION_AUTOPLAY
			&& ent.task != TASK_MELEE ){
			ent.frame = 0.0;
		}
	}

	if( ent.animationMode == ANIMATION_RPG && use_stick ){
		// RPG movement with a joystick.
		ent.direction = xyToDirection( newVec.x, newVec.y );
	}else if( ent.animationMode == ANIMATION_RPG ){
		// RPG movement with digital directions.
		auto oldVec = linalg::vec<double,2>( 0.0, 0.0 );
		switch( ent.direction ){
			case 3: oldVec = { -1.0,  0.0  };
				break;
			case 2: oldVec = {  1.0,  0.0  };
				break;
			case 1: oldVec = {  0.0,  1.0  };
				break;
			case 0: oldVec = {  0.0, -1.0  };
		}
		// https://www.euclideanspace.com/maths/algebra/vectors/angleBetween/
		if( std::acos( linalg::dot( oldVec, newVec ) ) >= 3.14 * 0.5 ){
			// There is at least a 90 degree angular distance.
			if( newVec.x < -epsilon ) ent.direction = 3;
			if( newVec.x >  epsilon ) ent.direction = 2;
			if( newVec.y >  epsilon ) ent.direction = 1;
			if( newVec.y < -epsilon ) ent.direction = 0;
		}
	}else if( ent.animationMode == ANIMATION_BEATEMUP ){
		// Beat-em-up movement.
		if( newVec.x < -epsilon ){
			ent.direction = 3;
		}
		if( newVec.x >  epsilon ){
			ent.direction = 2;
		}
	}
}

// Physics handling.
void World::simulate( double d ){
	// It's a cheap trick, but we're storing the length of the name of
	// the faced entity and prioritizing entities with longer names.
	size_t name_length = 0;
	
	// Reset facingEntity.
	facingEntity = -1;
	
	// Keep track of how long the simulation has been running.
	runningTime += d;
	
	// Handle entity movement and collisions, then store a list of
	// depths and indices.
	entityIndices.clear();
	for( size_t i = 0; i < entities.size(); i++ ){
		Entity &e = entities[i];
		// Decrement the entity's stun timer. For use in other source files.
		e.stun = std::max( e.stun - d, 0.0 );
		// Increment the entity's age if it's not in manual animation mode.
		if( e.animationMode != ANIMATION_MANUAL ){
			e.age += d;
		}
		if( e.task == TASK_MELEE ){
			// 3-frame animation for melee.
			e.frame = std::min( e.frame + e.fps * d, 2.0 );
		}else if( e.animationMode == ANIMATION_AUTOPLAY ){
			// 4-frame animation cycle for autoplay.
			e.frame = std::fmod( e.frame + e.fps * d, 4.0 );
		}
		if( !e.staticCollisions ){
			
			#ifdef GRINNINGLIZARD_MICROPATHER_INCLUDED
				if( e.path.size() > 0 ){
					// Get entity position.
					intptr_t entX = e.x + 0.5, entY = e.y + 0.5;
					
					// Get next tile position.
					size_t n;
					intptr_t nextX = entX, nextY = entY;
					
					// Check up to 5 tiles to avoid breakage on diagonal
					// movement.
					static const intptr_t
						xOff[5] = { 0,  1, -1,  0,  0 },
						yOff[5] = { 0,  0,  0,  1, -1 };
					
					for( int j = 0; j < 5; j++ ){
						intptr_t currentX = entX + xOff[j], currentY = entY + yOff[j];
						if( currentX < 0 ) currentX = 0;
						if( currentY < 0 ) currentY = 0;
						if( currentX > (intptr_t)mapInfo[0].size() - 1 ) currentX = mapInfo[0].size() - 1;
						if( currentY > (intptr_t)mapInfo.size() - 1 ) currentY = mapInfo.size() - 1;
						
						auto currentNode = XYToNode( currentX, currentY );
						
						for( n = 0; n < e.path.size(); n++ ){
							if( e.path[n] == currentNode ){
								break;
							}
						}
						
						if( n + 1 < e.path.size() ){
							NodeToXY( e.path[n + 1], &nextX, &nextY );
							break;
						}
					}
					
					// Walk towards the next tile.
					double moveX = 0.0, moveY = 0.0;
					if( nextX > entX ) moveX += 1.0;
					if( nextX < entX ) moveX -= 1.0;
					if( nextY > entY ) moveY += 1.0;
					if( nextY < entY ) moveY -= 1.0;
					if( std::sqrt( moveX * moveX + moveY * moveY ) < epsilon ) e.path.clear();
					moveEntity( e, moveX, moveY, d );
				}
			#endif
			
			entityCollisions( i );
			if( e.walking ){
				e.frame = std::fmod( e.frame + e.fps * d, 4.0 );
			}
		}
		// Push the index to the array for depth sorting.
		entityIndices.push_back( { e.y, (long long)i, &e } );
		
		// Handle player interactions.
		if( followEntity >= 0 && i != (size_t)followEntity ){
			Entity &player = entities[followEntity];
			// Determine which entity the player is facing.
			if( ( facingEntity == -1 || e.name.length() >= name_length )
				&& entityFacing( player, e ) ){
				name_length = e.name.length();
				facingEntity = i;
			}
			// Portals.
			if( portalCallback
				&& std::abs( player.x - e.x ) < 0.5
				&& std::abs( player.y - e.y ) < 0.5
				&& e.type.length() >= 6
				&& e.type.substr( 0, 6 ) == "portal" ){
				std::string param =
					e.type.length() >= 8 ? e.type.substr( 7 ) : "";
				(*portalCallback)( param );
			}
		}
	}
	
	// Set followEntity to -1 for a free-moving camera.
	if( followEntity >= 0 && entities.size() >= 1 ){
		cameraX = entities[followEntity].x + 0.5;
		cameraY = entities[followEntity].y + 0.5;
	}
}

// Draw the tiles.
void World::drawMap(){
	for( size_t i = 0; i < map.size(); i++ ){
		drawMapLayer( i );
	}
}

// Draw the entities.
void World::drawEntities( const std::vector<Entity> &extraEntities ){
	for( const Entity &e : extraEntities ){
		// Push the index to the array for depth sorting, using the
		// interpolated position.
		entityIndices.push_back( { e.last_y, -1, (Entity*)&e } );
	}

	// sort-em-up
	std::sort( entityIndices.begin(), entityIndices.end(),
		[]( const EntityIndex &a, const EntityIndex &b ){
		return a.depth < b.depth;
	} );
	
	calculateWorldCursor();
	
	cursorOverEntity = -1;
	
	// Calculate a screen margin of 3 tiles.
	double margin = tileSize * scale * 3.0;
	
	glEnable( GL_BLEND );
	for( EntityIndex &ei : entityIndices ){
		Entity &e = *ei.ptr;
		// Point sentries at followEntity.
		if( (  e.animationMode == ANIMATION_RPG
			|| e.animationMode == ANIMATION_BEATEMUP
			) && followEntity >= 0 && e.staticCollisions
			&& e.type == "sentry" ){
			entityLookAt( e, entities[followEntity] );
		}
		// Align the view position with the follow target if applicable.
		double screenX, screenY;
		if( ei.pos >= 0 && (int)ei.pos == followEntity ){
			screenX = ( screenWidth - (int)std::floor( (double)tileSize * scale ) ) / 2;
			screenY = screenHeight / 2 - (int)std::floor( ( e.height - (double)tileSize * 0.5 ) * scale );
		}else{
			// Show the interpolated positions for extraEntities.
			double x = ei.pos == -1 ? e.last_x : e.x;
			double y = ei.pos == -1 ? e.last_y : e.y;
			screenX = ( x - cameraX ) * (double)tileSize * scale + (double)screenWidth * 0.5;
			screenY = ( y - cameraY - (double)e.height / (double)tileSize + 1.0 ) * (double)tileSize * scale + (double)screenHeight * 0.5;
		}
		// Only draw on-screen entities within the margin.
		if( screenX < screenWidth + margin && screenY < screenHeight + margin
			&& screenX + e.width >= -margin && screenY + e.height >= -margin ){
			if( e.task == TASK_SLEEP ){
				// Entity is sleeping.
				screenX += ( e.sleep.width - tileSize ) * -0.5 * scale;
				screenY += ( e.height - (double)tileSize ) * scale;
				drawSprite(
					e.sleep,
					screenX,
					screenY,
					scale,
					0,
					0,
					e.sleep.width,
					e.sleep.height
				);
				if( cursorOverSprite( screenX, screenY, scale, tileSize, tileSize ) ){
					cursorOverEntity = ei.pos;
				}
			}else{
				// Entity is not sleeping.
				drawSprite(
					e.shadow,
					screenX - ( e.shadow.width - tileSize ) * 0.5 * scale,
					screenY + ( e.height - (double)tileSize ) * scale,
					scale,
					0,
					0,
					e.shadow.width,
					e.shadow.height
				);
				int entityFrame =
					e.task == TASK_MELEE ? (int)e.frame
					: [&](){
						switch( (int)std::ceil( e.frame ) ){
							case 1: return 1;
							case 2: return 0;
							case 3: return 2;
							default: return 0; // 4 or 0.
						}
					}();
				int scrollX = 0, scrollY = 0;
				int spriteWidth = e.width, spriteHeight = e.height;
				switch( e.animationMode ){
					case ANIMATION_RPG: // RPG style.
						scrollX = e.direction * e.width;
						scrollY = entityFrame * e.height;
						break;
					case ANIMATION_BEATEMUP: // Beat-em-up style.
						entityFrame = (int)std::ceil( e.frame ) % 4;
						if( entityFrame == 3 ){
							entityFrame = 1;
						}
						scrollX = entityFrame * e.width;
						if( e.direction == 3 ){
							scrollX += spriteWidth;
							spriteWidth *= -1;
						}
						break;
					default: // Left-to-right animation with 4 frames.
						scrollX = ( (int)std::ceil( e.frame ) % 4 ) * e.width;
						if( e.direction == 3 ){
							scrollX += spriteWidth;
							spriteWidth *= -1;
						}
				}
				// Select a sprite based on the entity's task.
				drawSprite(
					e.task == TASK_MELEE ? e.meleeSprite : e.sprite,
					screenX - ( e.width - tileSize ) * 0.5 * scale,
					screenY,
					scale,
					scrollX,
					scrollY,
					spriteWidth,
					spriteHeight
				);
				if( cursorOverSprite( screenX, screenY, scale, tileSize, spriteHeight ) ){
					cursorOverEntity = ei.pos;
				}
			}
		}
	}
}

// Simulate and draw everything. This is enough in some situations.
void World::draw( double d ){
	simulate( d );
	drawMap();
	drawEntities();
}

// Remove an entity from the world.
void World::removeEntity( int entity_index ){
	auto &ent = entities[entity_index];
	// If the entity has a collision radius and static collisions,
	// recalculate the map's collision data.
	if( std::abs( ent.collisionRadius ) > epsilon
		&& ent.staticCollisions ){
		recalculateMapEntities();
	}else{
		// No collisions? Then just set it to the info value.
		long long
			x = ent.x + 0.5,
			y = ent.y + 0.5;
		if( x < 0 ){
			x = 0;
		}else if( x > (long long)mapInfo[0].size() ){
			x = (long long)mapInfo[0].size();
		}
		if( y < 0 ){
			y = 0;
		}else if( y > (long long)mapInfo.size() ){
			y = (long long)mapInfo.size();
		}
		mapEntities[y][x] = mapInfo[y][x];
	}
	entities.erase( entities.begin() + entity_index );
	// Lower the followEntity index by 1 if necessary.
	if( followEntity > entity_index ){
		followEntity--;
	}
	// This flag signals that memory has changed and caution should be exercised.
	mapChanged = true;
}

void World::tileCollisions( Entity &ent ){
	bool
		b00 = tileBlocking( (long long)ent.x, (long long)ent.y ),
		b10 = tileBlocking( (long long)ent.x + 1, (long long)ent.y ),
		b01 = tileBlocking( (long long)ent.x, (long long)ent.y + 1 ),
		b11 = tileBlocking( (long long)ent.x + 1, (long long)ent.y + 1);
	if( b01 && b10 ){ // ascending diagonal blocks
		if( ent.x - std::floor( ent.x ) < 0.5 )
			b11 = true;
		else
			b00 = true;
	}
	if( b00 && b11 ){ // descending diagonal blocks
		if( ent.x - std::floor( ent.x ) < 0.5 )
			b10 = true;
		else
			b01 = true;
	}
	bool checked = false;
	if( b00 && b10 ){ // wall above entity
		double overlap = ( std::floor( ent.y ) + 0.5 ) - ( ent.y - ent.collisionRadius );
		if( overlap > 0.0 ) ent.y += overlap;
		checked = true;
	}
	if( b01 && b11 ){ // wall below entity
		double overlap = ( ent.y + ent.collisionRadius ) - ( std::floor( ent.y ) + 0.5 );
		if( overlap > 0.0 ) ent.y -= overlap;
		checked = true;
	}
	if( b00 && b01 ){ // wall left of entity
		double overlap = ( std::floor( ent.x ) + 0.5 ) - ( ent.x - ent.collisionRadius );
		if( overlap > 0.0 ) ent.x += overlap;
		checked = true;
	}
	if( b10 && b11 ){ // wall right of entity
		double overlap = ( ent.x + ent.collisionRadius ) - ( std::floor( ent.x ) + 0.5 );
		if( overlap > 0.0 ) ent.x -= overlap;
		checked = true;
	}
	if( !checked && ( b00 || b10 || b01 || b11 ) ){ // block is cornered
		double circleX = std::floor( ent.x ) + ( ( b10 || b11 ) ? 1.0 : 0.0 );
		double circleY = std::floor( ent.y ) + ( ( b01 || b11 ) ? 1.0 : 0.0 );
		double dist = std::sqrt( std::pow( ent.x - circleX, 2 ) + std::pow( ent.y - circleY, 2 ) );
		double diam = ent.collisionRadius + 0.5;
		if( dist < diam ){
			double angle = std::atan2( circleX - ent.x, circleY - ent.y );
			ent.x -= std::sin( angle ) * ( diam - dist );
			ent.y -= std::cos( angle ) * ( diam - dist );
		}
	}
}

void World::entityCollisions( size_t pos, bool recurse ){
	Entity &e1 = entities[pos];
	if( std::abs( e1.collisionRadius ) <= epsilon ){
		return;
	}
	if( recurse ){
		tileCollisions( e1 );
	}
	for( size_t i = 0; i < entities.size(); i++ ){
		Entity &e2 = entities[i];
		if( pos != i && std::abs( e2.collisionRadius ) > epsilon ){
			double dist = std::sqrt( std::pow( e1.x - e2.x, 2 ) + std::pow( e1.y - e2.y, 2 ) );
			double diam = e1.collisionRadius + e2.collisionRadius;
			if( dist < diam ){
				double angle = std::atan2( e2.x - e1.x, e2.y - e1.y );
				double pushX = std::sin( angle ) * ( diam - dist );
				double pushY = std::cos( angle ) * ( diam - dist );
				if( recurse ){
					if( e2.staticCollisions ){
						e1.x -= pushX;
						e1.y -= pushY;
					}else{
						e1.x -= pushX * 0.5;
						e2.x += pushX * 0.5;
						e1.y -= pushY * 0.5;
						e2.y += pushY * 0.5;
						tileCollisions( e2 );
					}
					entityCollisions( pos, false );
				}else{
					e1.x -= pushX;
					e1.y -= pushY;
					tileCollisions( e1 );
				}
			}
		}
	}
}

} // namespace fworld

#endif // FWORLD_H


Mode Type Size Ref File
100644 blob 98 227abf3bfa53b2530dcc74495da7bd0ccdcb0775 .gitignore
100644 blob 225 9b00c2c2e7b4f0c1e338fdead65f17ba0af089c1 COPYING
100755 blob 43 45aea818a4a3202b2467509f28a481cce08834d2 Confectioner.command
100644 blob 14015 649b7f0c112c3ac13287bfe88b949fec50356e4d Makefile
100644 blob 2723 b5a3f573f076ef740ca742ec9598043732e10c0e README.md
040000 tree - 6b3a1677d07517c1f83769dd7675fe6bb9d7a269 base
100755 blob 156 84cb1387849f2ca98e53e43536d00af2dfabf7d3 caveconfec
100755 blob 28 41b0ef285892c86306eaa269f366dd04cb633d21 caveconfec.bat
100644 blob 198037 a0180394c9bf29c02b7ef05916bd5573e3f37da2 confec.cpp
100644 blob 487269 29cfd3578eb40b1f039e271bcaa81af49d1b7f3c gamecontrollerdb.txt
040000 tree - 62e9d686bbab52d3d88886390b437a3ecef315de include
100755 blob 12081 ad29f012941aedfd4ee7232ed95fb68c8c5244c9 index-template.html
100755 blob 1065 a460e3c74b8fa53a6f609944ef7e41558479e73f libs.cpp
100755 blob 27581 8350a63e947e8a4a55608fd090d128fef7b969a1 micropather.cpp
100644 blob 141235 f54e2d2631a628876a631456c043b77da5db78bd openjdk.pem
100755 blob 8 e9a74187b02a27b165dfa4f93bf6f060376d0ee6 steam_appid.txt
Hints:
Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://rocketgit.com/user/mse/ConfectionerEngine

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@ssh.rocketgit.com/user/mse/ConfectionerEngine

Clone this repository using git:
git clone git://git.rocketgit.com/user/mse/ConfectionerEngine

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a merge request:
... clone the repository ...
... make some changes and some commits ...
git push origin main