If you have comments or questions concerning this source file, discuss them in the forum.
/*
Copyright (c) 2002 Nicolai Haehnle
See the license.txt for details. If that file was not included in the
source distributions, please email <prefect@rtts.org>
*/
// ge_level.cpp - contains the ELevel
#include "game.h"
#include "ff_level.h"
/*
==============================================================================
ETileTypes IMPLEMENTATION
==============================================================================
*/
/*
ETileTypes doesn't remove unused tiles.
This isn't a problem, since it will be destroyed after a level finishes,
and there's hardly a reason to load/unload tiles in the middle of a level.
*/
/*
==============
ETileTypes::ETileTypes
The tile types container starts out empty
==============
*/
ETileTypes::ETileTypes()
{
m_iNumTT = 0;
m_pTT = 0;
}
/*
==============
ETileTypes::~ETileTypes
Free all tile types
==============
*/
ETileTypes::~ETileTypes()
{
unsigned i;
if (m_iNumTT) {
for(i = 0; i < m_iNumTT; i++)
m_pTT[i].pic->Release();
L_Free(m_pTT);
}
}
/*
==============
ETileTypes::Load
Name is just the plain name, without a path or extension.
Returns ~0 if the tile couldn't be loaded.
==============
*/
unsigned ETileTypes::Load(const char *name)
{
char path[MAX_OSPATH];
HPicture *pic;
tiletype_t *tt;
unsigned i;
snprintf(path, sizeof(path), "tiles/%s", name);
for(i = 0, tt = m_pTT; i < m_iNumTT; i++, tt++) {
if (!strcmp(path, tt->pic->m_szName))
return i;
}
try {
pic = HPicture::Get(path, pic_normal, 0);
} catch(LError &err) {
L_Printf("Couldn't load tile %s: %s\n", name, err.Get());
return ~0;
}
i = m_iNumTT++;
m_pTT = (tiletype_t *)L_Realloc(m_pTT, sizeof(tiletype_t)*m_iNumTT, TAG_MAP);
m_pTT[i].pic = pic;
return i;
}
/*
==============================================================================
airjob_t IMPLEMENTATION
==============================================================================
*/
airjob_t::airjob_t()
{
x = y = 0;
type = 0;
level = 0;
}
airjob_t::airjob_t(airunit_type_t *ntype)
{
x = y = 0;
type = ntype;
type->used++;
level = 0;
}
airjob_t::airjob_t(const airjob_t ©)
{
x = copy.x;
y = copy.y;
type = copy.type;
if (type)
type->used++;
level = copy.level;
nodes = copy.nodes;
}
airjob_t::~airjob_t()
{
if (type)
Air_FreeType(type);
}
airjob_t &airjob_t::operator=(const airjob_t ©)
{
if (type)
Air_FreeType(type);
x = copy.x;
y = copy.y;
type = copy.type;
if (type)
type->used++;
level = copy.level;
nodes = copy.nodes;
return *this;
}
/*
==============================================================================
ELevel IMPLEMENTATION
==============================================================================
*/
/*
==============
ELevel::ELevel
Creates an empty level of length 0
==============
*/
ELevel::ELevel(ETileTypes *pTileTypes)
{
m_bEdit = false;
m_iBaseLevel = -1;
m_szName[0] = 0;
m_szAuthor[0] = 0;
m_iMapLength = 0;
m_pTileTypes = pTileTypes;
m_pTiles = 0;
m_flLastDistance = m_flDistance = 0;
m_iNumBosses = 0;
}
/*
==============
ELevel::~ELevel
Free all allocated resources
==============
*/
ELevel::~ELevel()
{
Clear();
}
/*
==============
ELevel::Clear
Completely empties the level (length is reset to 0)
==============
*/
void ELevel::Clear()
{
m_AirSched.clear();
m_AirJobs.clear();
if (m_pTiles) {
L_Free(m_pTiles);
m_pTiles = 0;
}
m_iMapLength = 0;
m_flLastDistance = m_flDistance = 0;
m_iBaseLevel = -1;
m_szName[0] = 0;
m_szAuthor[0] = 0;
m_iNumBosses = 0;
}
/*
==============
ELevel::SetEditMode
Potentially rearrange pack/unpack data structures (not done right now).
==============
*/
void ELevel::SetEditMode(bool bEdit)
{
m_bEdit = bEdit;
}
/*
==============
ELevel::BeginPlay
ELevel::EndPlay
Called before and after the level is used for playing.
Used to prepare/finish airjob usage. level is the builtin
level (0..2)
==============
*/
struct airsched_compare {
bool operator() (const airsched_t &s1, const airsched_t &s2)
{
return s1.line > s2.line;
}
};
void ELevel::BeginPlay(int level)
{
m_AirSched.clear();
for(airjob_it job = m_AirJobs.begin(); job != m_AirJobs.end(); job++) {
airsched_t sched;
sched.job = &*job;
sched.line = (job->y + FIRSTSHADOWY(job->type->sprite->maxs[1])) / 32.0;
if (job->level <= level && sched.line >= m_flDistance+15)
m_AirSched.push_back(sched);
}
airsched_compare compare;
std::sort(m_AirSched.begin(), m_AirSched.end(), compare);
}
void ELevel::EndPlay()
{
}
/*
==============
ELevel::Logic
Perform in-game logic: Scroll the level, launched jobs. Called once
per game logic frame
==============
*/
void ELevel::Logic()
{
lassert(!m_bEdit);
m_flLastDistance = m_flDistance;
m_flDistance += 1.5 * LOGICTIME / 1000.0;
if (m_flDistance > m_iMapLength-15)
m_flDistance = m_iMapLength-15;
while(m_AirSched.size()) {
airsched_it sched = m_AirSched.end()-1;
if (sched->line > m_flDistance+15)
break;
ge->Air_Spawn(sched->job);
m_AirSched.pop_back();
}
}
/*
==============
ELevel::Draw
Draw the level. In edit mode, missing tiles are drawn in pink.
==============
*/
void ELevel::Draw()
{
float distance;
int x, y;
float scry, scrx;
tile_t *tile;
HPicture *pl;
// interpolate the effective distance
if (ge && m_flLastDistance != m_flDistance)
distance = LERP(m_flLastDistance, m_flDistance);
else
distance = m_flLastDistance;
scry = 480 + 32 * (distance - (int)distance);
y = (int)distance;
tile = &m_pTiles[y*MAPWIDTH];
while(y < m_iMapLength && scry >= 0)
{
scrx = 0;
scry -= 32;
if (y < 0) {
if (m_bEdit)
hal->FillRect(0, (int)scry, 640, 32, 0, 0, 0);
tile += MAPWIDTH;
y++;
continue;
}
for(x = 0; x < MAPWIDTH; x++) {
if (m_pTileTypes->ValidId(tile->gfx)) {
pl = m_pTileTypes->m_pTT[tile->gfx].pic;
pl->Draw(scrx, scry);
} else if (m_bEdit)
hal->FillRect((int)scrx, (int)scry, 32, 32, 255, 0, 255);
tile++;
scrx += 32;
}
y++;
}
if (scry > 0 && m_bEdit)
hal->FillRect(0, 0, 640, (int)scry, 0, 0, 0);
}
/*
==============
ELevel::SetDistance
Override the scrolling distance
==============
*/
void ELevel::SetDistance(float dist)
{
m_flLastDistance = m_flDistance = dist;
}
/*
==============
ELevel::TileAtPoint
ELevel::PointForTile
Determine which tile is below a certain coordinate and vice versa.
Note that coordinates are in screen coordinates.
==============
*/
void ELevel::TileAtPoint(float x, float y, int *ptx, int *pty)
{
y = -y + 480;
y = y / 32.0;
y += m_flDistance;
*pty = (int)y;
x = x / 32.0;
*ptx = (int)x;
}
void ELevel::PointForTile(int tx, int ty, float *px, float *py)
{
float x, y;
x = tx * 32;
y = (ty+1) - m_flDistance;
y = y * 32;
y = -y + 480;
*px = x;
*py = y;
}
/*
==============
ELevel::Resize
Add or remove lines to/from the level
==============
*/
void ELevel::Resize(bool start, int lines)
{
int x, y, dy;
int i;
int newlength;
int tiles;
tile_t *tile;
lassert(m_bEdit);
if (lines < -m_iMapLength)
lines = -m_iMapLength;
if (!lines)
return;
// Clear all the tiles properly to prevent leaks
if (lines < 0) {
if (start) {
y = 0;
dy = 1;
} else {
y = m_iMapLength - 1;
dy = -1;
}
i = -lines;
while(i--) {
for(x = 0; x < MAPWIDTH; x++)
SetTile(x, y, 0);
y += dy;
}
}
// Realloc tiles memory
newlength = m_iMapLength + lines;
tiles = lines * MAPWIDTH;
if (start) {
if (lines < 0)
memmove(m_pTiles, &m_pTiles[-tiles], (m_iMapLength+lines)*sizeof(tile_t)*MAPWIDTH);
m_pTiles = (tile_t *)L_Realloc(m_pTiles, sizeof(tile_t)*newlength*MAPWIDTH,
TAG_MAP);
if (lines > 0) {
memmove(&m_pTiles[tiles], m_pTiles, m_iMapLength*MAPWIDTH*sizeof(tile_t));
for(i = 0, tile = m_pTiles; i < tiles; i++, tile++)
tile->gfx = ~0;
}
SetDistance(m_flDistance + lines);
} else {
m_pTiles = (tile_t *)L_Realloc(m_pTiles, sizeof(tile_t)*(newlength*MAPWIDTH),
TAG_MAP);
if (lines > 0) {
for(i = 0, tile = &m_pTiles[m_iMapLength*MAPWIDTH]; i < tiles; i++, tile++)
tile->gfx = ~0;
}
}
m_iMapLength = newlength;
}
/*
==============
ELevel::SetTile
Sets a given tile to be of a specific type.
Ignore out-of-bounds coordinates.
==============
*/
void ELevel::SetTile(int x, int y, unsigned id)
{
tile_t *tile;
lassert(m_bEdit);
if (x < 0 || x >= MAPWIDTH || y < 0 || y >= m_iMapLength)
return;
tile = &m_pTiles[y*MAPWIDTH + x];
tile->gfx = id;
}
/*
==============
ELevel::GetTile
Retrieve the tiletype id.
==============
*/
unsigned ELevel::GetTile(int x, int y)
{
tile_t *tile;
lassert(m_bEdit);
if (x < 0 || x >= MAPWIDTH || y < 0 || y >= m_iMapLength)
return 0;
tile = &m_pTiles[y*MAPWIDTH + x];
return tile->gfx;
}
/*
==============
ELevel::SavePrepare
Optimize the level and check for problems
==============
*/
bool ELevel::SavePrepare()
{
bool playable;
tile_t *tile;
int tiles, i;
playable = true;
// 1) Check baselevel, name, length for validity
if (m_iMapLength < 30 || m_iBaseLevel < 0 || !m_szName[0] || !m_szAuthor[0])
playable = false;
// 2) Check for missing tiles
tiles = m_iMapLength * MAPWIDTH;
for(i = 0, tile = m_pTiles; i < tiles; i++, tile++) {
if (!m_pTileTypes->ValidId(tile->gfx)) {
playable = false;
break;
}
}
// 3) Map is invalid if there is no boss
if (!m_iNumBosses)
playable = false;
return playable;
}
/*
==============
ELevel::Save
Saves the level, returns true if the level is playable
==============
*/
bool ELevel::Save()
{
char buf[MAX_OSPATH];
LFileWrite fw;
bool playable;
tile_t *tile;
unsigned *map;
int offset, usedtypes;
int tiles, i;
playable = SavePrepare();
// 1) Write header
fw.Integer(LVL_MAGIC);
fw.Integer(LVL_VERSION);
fw.Integer(m_iBaseLevel);
fw.CString(m_szName);
fw.Byte(playable ? 1 : 0);
fw.Integer(LVL_FEAT_AIRJOBS|LVL_FEAT_LEVELS|LVL_FEAT_AUTHOR); // features flag
tiles = m_iMapLength * MAPWIDTH;
// 2) Write tiletype names
// The tiletype container may include many more tiletypes than are needed
// for the map. However, only those actually needed are written out.
// The map will later be used to translate between internal and on-disk
// tiletype IDs
offset = fw.filepos;
fw.Integer(0); // fix up later
map = (unsigned *)L_Malloc(sizeof(unsigned)*m_pTileTypes->m_iNumTT, TAG_MAP);
memset(map, -1, sizeof(unsigned)*m_pTileTypes->m_iNumTT);
usedtypes = 0;
for(i = 0, tile = m_pTiles; i < tiles; i++, tile++) {
if (!m_pTileTypes->ValidId(tile->gfx))
continue;
if (map[tile->gfx] == ~0) {
map[tile->gfx] = usedtypes++;
fw.CString(m_pTileTypes->m_pTT[tile->gfx].pic->m_szName + 6);
}
}
fw.Integer(usedtypes, offset); // fix up tile type count
// 3) Write actual tile info
fw.Integer(m_iMapLength);
for(i = 0, tile = m_pTiles; i < tiles; i++, tile++) {
if (m_pTileTypes->ValidId(tile->gfx))
fw.Integer(map[tile->gfx]);
else
fw.Integer(~0);
}
L_Free(map);
// 4) Write author
fw.CString(m_szAuthor);
// 5) Write airjobs
for(airjob_it aj = m_AirJobs.begin(); aj != m_AirJobs.end(); aj++) {
fw.CString(aj->type->name);
fw.Float(aj->x);
fw.Float(aj->y);
fw.Byte(aj->level);
fw.Byte(aj->nodes.size());
for(i = 0; i < aj->nodes.size(); i++) {
fw.Float(aj->nodes[i].dx);
fw.Float(aj->nodes[i].dy);
}
}
fw.CString(""); // end of list
// Write the map out to disk
snprintf(buf, sizeof(buf), "levels/%04i.%s.lvl", m_iBaseLevel+1, m_szName);
fw.Write(buf);
return playable;
}
/*
==============
ELevel::LoadTiles
Load tiletypes and tile data
==============
*/
bool ELevel::LoadTiles(LFileRead *fr)
{
bool playable;
unsigned *map, id;
unsigned numtypes;
tile_t *tile;
int tiles, i;
playable = true;
numtypes = fr->Integer();
try {
// Read tile types
// A file -> internal gfx mapping is needed (like in Save, but the other way round)
map = (unsigned *)L_Malloc(sizeof(unsigned)*numtypes, TAG_MAP);
for(id = 0; id < numtypes; id++)
map[id] = m_pTileTypes->Load(fr->CString());
// Read map
m_iMapLength = fr->Integer();
tiles = m_iMapLength * MAPWIDTH;
m_pTiles = (tile_t *)L_Malloc(sizeof(tile_t)*tiles, TAG_MAP);
memset(m_pTiles, -1, sizeof(tile_t)*tiles);
for(i = 0, tile = m_pTiles; i < tiles; i++, tile++) {
id = fr->Integer();
if (id >= numtypes) {
tile->gfx = ~0;
playable = false;
} else
tile->gfx = map[id];
}
} catch(...) {
if (map)
L_Free(map);
throw;
}
if (map)
L_Free(map);
return playable;
}
/*
==============
ELevel::LoadAirjobs
Load airjobs from the given file
==============
*/
void ELevel::LoadAirjobs(LFileRead *fr, unsigned features)
{
const char *tname;
int count;
int i;
float x, y;
airunit_type_t *type;
airjob_t *aj;
for(;;) {
tname = fr->CString();
if (!*tname)
break;
type = Air_GetType(tname);
x = fr->Float();
y = fr->Float();
aj = AddAirJob(type, x, y);
if (features & LVL_FEAT_LEVELS)
aj->level = fr->Byte();
else
aj->level = 0;
if (aj->level > 2)
aj->level = 2;
Air_FreeType(type);
count = fr->Byte();
for(i = 0; i < count; i++) {
jobnode_t jn;
jn.dx = fr->Float();
jn.dy = fr->Float();
if (fabs(jn.dx) > 0.1 || fabs(jn.dy) > 0.1)
aj->nodes.push_back(jn);
}
}
}
/*
==============
ELevel::Load
Loads the level by filename. Returns true if the level is playable.
Throws an exception if the level file is corrupt.
==============
*/
bool ELevel::Load(const char *fname)
{
LFileRead fr;
bool playable;
unsigned features;
Clear(); // clear the current level, if any
try
{
fr.Open(fname);
// Process header
if (fr.Integer() != LVL_MAGIC)
throw LError("bad magic number");
if (fr.Integer() != LVL_VERSION)
throw LError("unsupported version");
m_iBaseLevel = fr.Integer();
xstrcpy(m_szName, sizeof(m_szName), fr.CString());
playable = fr.Byte() ? true : false;
features = fr.Integer();
if (features & ~(LVL_FEAT_AIRJOBS|LVL_FEAT_LEVELS|LVL_FEAT_AUTHOR))
throw LError("unsupported features: %08X", features);
// Read tile types and tile data
if (!LoadTiles(&fr))
playable = false;
// Read author
if (features & LVL_FEAT_AUTHOR)
xstrcpy(m_szAuthor, sizeof(m_szAuthor), fr.CString());
// Read airjobs
if (features & LVL_FEAT_AIRJOBS)
LoadAirjobs(&fr, features);
}
catch(...) {
Clear();
throw;
}
return playable;
}
/*
==============
ELevel::ScreenToLevel
ELevel::LevelToScreen
Convert screen-based coordinates to level-based coordinates,
where 0 is at the bottom of the level, and vice versa
==============
*/
void ELevel::ScreenToLevel(float sx, float sy, float *plx, float *ply)
{
*plx = sx;
*ply = -sy+480 + m_flDistance*32;
}
void ELevel::LevelToScreen(float lx, float ly, float *psx, float *psy)
{
*psx = lx;
*psy = -ly+480 + m_flDistance*32;
}
//============================================================================
// AIRJOB EDITING
/*
==============
ELevel::AddAirJob
Add an airjob at the given location and return it.
x/y is in level coordinates.
Returns 0 if the airjob wasn't created (e.g. due to invalid coords)
Type ownership isn't transferred.
==============
*/
airjob_t *ELevel::AddAirJob(airunit_type_t *type, float x, float y)
{
if (x < 0 || x > 640 || y < 0 || y > m_iMapLength*32)
return 0;
airjob_t aj(type);
aj.x = x;
aj.y = y;
aj.level = 0;
if (type->bBoss)
m_iNumBosses++;
m_AirJobs.push_back(aj);
return &m_AirJobs.back();
}
/*
===============
ELevel::AddAirjobs
Add the airjobs from the given list to the airjobs in this level
===============
*/
void ELevel::AddAirjobs(const std::vector<airjob_t> &jobs, float x, float y)
{
for(airjob_cit job = jobs.begin(); job != jobs.end(); job++) {
airjob_t aj = *job;
aj.x += x;
aj.y += y;
if (aj.x < 0 || aj.x > 640 || aj.y < 0 || aj.y > m_iMapLength*32)
continue;
if (aj.type->bBoss)
m_iNumBosses++;
m_AirJobs.push_back(aj);
}
}
/*
===============
ELevel::DelAirJob
Deletes an airjob from a level
===============
*/
void ELevel::DelAirJob(airjob_t *job)
{
lassert(job);
if (job->type->bBoss)
m_iNumBosses--;
m_AirJobs.erase(airjob_it(job));
}
/*
===============
ELevel::DelAirjobs
Delete a list of airjobs safely from a level.
===============
*/
void ELevel::DelAirjobs(const std::vector<airjob_t*> &jobs)
{
// This is a stupid hack:
// Mark all airjobs in the list as "to delete" by setting their y-coordinate
// to -1 (which is invalid)
for(airjobp_cit p = jobs.begin(); p != jobs.end(); p++)
(*p)->y = -1;
int i = 0;
while(i < m_AirJobs.size()) {
if (m_AirJobs[i].y < 0)
m_AirJobs.erase(m_AirJobs.begin() + i);
else
i++;
}
}
/*
===============
ELevel::SetAirjobPos
Move an airjob to a new position. Point is in level coordinates
===============
*/
void ELevel::SetAirjobPos(airjob_t *aj, float x, float y)
{
if (x < 0 || x > 640 || y < 0 || y > m_iMapLength*32)
return;
aj->x = x;
aj->y = y;
}
/*
===============
ELevel::AirJobAtPoint
Walk the linked list in search of an airjob (bbox match)
Point is in level coordinates.
===============
*/
airjob_t *ELevel::AirJobAtPoint(float x, float y)
{
for(airjob_it aj = m_AirJobs.begin(); aj != m_AirJobs.end(); aj++) {
sprite_t *spr = aj->type->sprite;
if (x <= aj->x-spr->mins[0] && x >= aj->x-spr->maxs[0] &&
y <= aj->y-spr->mins[1] && y >= aj->y-spr->maxs[1])
return &*aj;
}
return 0;
}
/*
===============
ELevel::FindAirJobs
Look for airjobs in the given rectangle in level coordinates.
If list is not null, pointers to all airjobs are stored in that vector.
Returns true if airjobs were found.
===============
*/
bool ELevel::FindAirJobs(float x1, float y1, float x2, float y2, std::vector<airjob_t*> *list)
{
bool found = false;
if (x1 > x2)
std::swap(x1, x2);
if (y1 > y2)
std::swap(y1, y2);
for(airjob_it aj = m_AirJobs.begin(); aj != m_AirJobs.end(); aj++)
{
sprite_t *spr = aj->type->sprite;
if (aj->x+spr->mins[0] > x2 || aj->x+spr->maxs[0] < x1 ||
aj->y-spr->maxs[1] > y2 || aj->y-spr->mins[1] < y1) // y-axis is reversed!
continue;
if (list) {
list->push_back(&*aj);
found = true;
} else
return true;
}
return found;
}
/*
==============
ELevel::NodeAtPoint
Try to find a node at the given location (level coordinates).
This also returns the root node, which is not represented with
a structure. pidx is set to -1 for the root node
==============
*/
airjob_t *ELevel::NodeAtPoint(float x, float y, int *pidx)
{
jobnode_t *jn;
float tx, ty;
int i;
for(airjob_it aj = m_AirJobs.begin(); aj != m_AirJobs.end(); aj++) {
tx = aj->x;
ty = aj->y;
i = -1;
for(;;) {
if (x >= tx-4 && x <= tx+4 && y >= ty-4 && y <= ty+4) {
*pidx = i;
return &*aj;
}
i++;
if (i >= aj->nodes.size())
break;
jn = &aj->nodes[i];
tx += jn->dx;
ty -= jn->dy; // node dy is in screen coordinates!
}
}
return 0;
}
/*
==============
ELevel::NodeAtPoint
Try to find a node at the given location (level coordinates).
This also returns the root node, which is not represented with
a structure. pidx is set to -1 for the root node
Only looks at units in the list.
==============
*/
airjob_t *ELevel::NodeAtPoint(const std::vector<airjob_t*> &list, float x, float y, int *pidx)
{
jobnode_t *jn;
float tx, ty;
int i;
for(airjobp_cit aj = list.begin(); aj != list.end(); aj++) {
tx = (*aj)->x;
ty = (*aj)->y;
i = -1;
for(;;) {
if (x >= tx-4 && x <= tx+4 && y >= ty-4 && y <= ty+4) {
*pidx = i;
return *aj;
}
i++;
if (i >= (*aj)->nodes.size())
break;
jn = &(*aj)->nodes[i];
tx += jn->dx;
ty -= jn->dy; // node dy is in screen coordinates!
}
}
return 0;
}