#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