passage/gamma256/gameSource/World.cpp
2025-10-03 02:19:59 -04:00

963 lines
23 KiB
C++

#include "World.h"
#include "common.h"
#include "map.h"
#include "minorGems/graphics/Image.h"
#include "minorGems/util/SimpleVector.h"
class GraphicContainer {
public:
GraphicContainer( const char *inTGAFileName ) {
Image *image = readTGA( inTGAFileName );
mW = image->getWidth();
mH = image->getHeight();
int imagePixelCount = mW * mH;
mRed = new double[ imagePixelCount ];
mGreen = new double[ imagePixelCount ];
mBlue = new double[ imagePixelCount ];
for( int i=0; i<imagePixelCount; i++ ) {
mRed[i] = 255 * image->getChannel(0)[ i ];
mGreen[i] = 255 * image->getChannel(1)[ i ];
mBlue[i] = 255 * image->getChannel(2)[ i ];
}
delete image;
}
~GraphicContainer() {
delete [] mRed;
delete [] mGreen;
delete [] mBlue;
}
double *mRed;
double *mGreen;
double *mBlue;
int mW;
int mH;
};
GraphicContainer *tileContainer;
GraphicContainer *chestContainer;
GraphicContainer *spriteContainer;
GraphicContainer *spriteSadContainer;
GraphicContainer *spouseContainer;
GraphicContainer *prizeAnimationContainer;
GraphicContainer *dustAnimationContainer;
GraphicContainer *heartAnimationContainer;
// dimensions of one tile. TileImage contains 13 tiles, stacked vertically,
// with blank lines between tiles
int tileW = 8;
int tileH = 8;
int tileImageW;
int tileImageH;
int numTileSets;
// how wide the swath of a world is that uses a given tile set
int tileSetWorldSpan = 200;
// overlap during tile set transition
int tileSetWorldOverlap = 50;
// for testing
int tileSetSkip = 0;
int tileSetOrder[18] = { 0, 2, 10, 1, 8, 3, 5, 6, 4, 7, 13, 9, 15,
14, 16, 12, 11, 17 };
int mapTileSet( int inSetNumber ) {
// stay in bounds of tileSetOrder
inSetNumber = inSetNumber % 18;
int mappedSetNumber = tileSetOrder[ inSetNumber ];
// stay in bounds of tile set collection
return mappedSetNumber % numTileSets;
}
int chestW = 8;
int chestH = 8;
int numGems = 6;
int gemLocations[6] = { 41, 42, 43, 44, 45, 46 };
//int gemLocations[4] = { 10, 11, 12, 13 };
double gemColors[6][3] = { { 255, 0, 0 },
{ 0, 255, 0 },
{ 255, 160, 0 },
{ 0, 0, 255 },
{ 255, 255, 0 },
{ 255, 0, 255 } };
class Animation {
public:
Animation( int inX, int inY, int inFrameW, int inFrameH,
char inAutoStep,
char inRemoveAtEnd, GraphicContainer *inGraphics )
: mX( inX ), mY( inY ),
mFrameW( inFrameW ), mFrameH( inFrameH ),
mPageNumber( 0 ),
mFrameNumber( 0 ),
mAutoStep( inAutoStep ),
mRemoveAtEnd( inRemoveAtEnd ),
mGraphics( inGraphics ) {
mImageW = mGraphics->mW;
mNumFrames = ( mGraphics->mH - mFrameH ) / mFrameH + 1;
mNumPages = ( mGraphics->mW - mFrameW ) / mFrameW + 1;
}
// default constructor so that we can build a vector
// of Animations
Animation() {
}
// replaces graphics of animation
void swapGraphics( GraphicContainer *inNewGraphics ) {
mGraphics = inNewGraphics;
mImageW = mGraphics->mW;
mNumFrames = ( mGraphics->mH - mFrameH ) / mFrameH + 1;
mNumPages = ( mGraphics->mW - mFrameW ) / mFrameW + 1;
if( mPageNumber >= mNumPages ) {
mPageNumber = mNumPages - 1;
}
if( mFrameNumber >= mNumFrames ) {
mFrameNumber = mNumFrames - 1;
}
}
int mX, mY;
int mFrameW, mFrameH;
int mImageW;
// can blend between pages
double mPageNumber;
int mNumPages;
int mFrameNumber;
int mNumFrames;
char mAutoStep;
char mRemoveAtEnd;
GraphicContainer *mGraphics;
};
SimpleVector<Animation> animationList;
// pointers into vectors are unsafe
//Animation *spriteAnimation;
//Animation *spouseAnimation;
int spriteAnimationIndex;
int spouseAnimationIndex;
char playerDead = false;
char spouseDead = false;
char metSpouse = false;
void resetSampleHashTable();
void initWorld() {
resetMap();
resetSampleHashTable();
playerDead = false;
spouseDead = false;
metSpouse = false;
animationList.deleteAll();
tileImageW = tileContainer->mW;
tileImageH = tileContainer->mH;
numTileSets = (tileImageW + 1) / (tileW + 1);
Animation character( 0, 0, 8, 8, false, false, spriteContainer );
animationList.push_back( character );
// unsafe
// get pointer to animation in vector
//spriteAnimation = animationList.getElement( animationList.size() - 1 );
spriteAnimationIndex = animationList.size() - 1;
Animation spouse( 100, 7, 8, 8, false, false, spouseContainer );
spouse.mFrameNumber = 7;
animationList.push_back( spouse );
// unsafe!
// get pointer to animation in vector
//spouseAnimation = animationList.getElement( animationList.size() - 1 );
spouseAnimationIndex = animationList.size() - 1;
}
char isSpriteTransparent( GraphicContainer *inContainer, int inSpriteIndex ) {
// take transparent color from corner
return
inContainer->mRed[ inSpriteIndex ] ==
inContainer->mRed[ 0 ]
&&
inContainer->mGreen[ inSpriteIndex ] ==
inContainer->mGreen[ 0 ]
&&
inContainer->mBlue[ inSpriteIndex ] ==
inContainer->mBlue[ 0 ];
}
struct rgbColorStruct {
double r;
double g;
double b;
};
typedef struct rgbColorStruct rgbColor;
// outTransient set to true if sample returned is part of a transient
// world feature (character sprite, chest, etc.)
rgbColor sampleFromWorldNoWeight( int inX, int inY, char *outTransient );
// same, but wrapped in a hash table to store non-transient results
rgbColor sampleFromWorldNoWeightHash( int inX, int inY );
Uint32 sampleFromWorld( int inX, int inY, double inWeight ) {
rgbColor c = sampleFromWorldNoWeightHash( inX, inY );
unsigned char r = (unsigned char)( inWeight * c.r );
unsigned char g = (unsigned char)( inWeight * c.g );
unsigned char b = (unsigned char)( inWeight * c.b );
return r << 16 | g << 8 | b;
}
char isSpritePresent( int inX, int inY ) {
// look at animations
for( int i=0; i<animationList.size(); i++ ) {
Animation a = *( animationList.getElement( i ) );
int animW = a.mFrameW;
int animH = a.mFrameH;
// pixel position relative to animation center
// player position centered on sprint left-to-right
int animX = (int)( inX - a.mX + a.mFrameW / 2 );
int animY = (int)( inY - a.mY + a.mFrameH / 2 );
if( animX >= 0 && animX < animW
&&
animY >= 0 && animY < animH ) {
return true;
}
}
return false;
}
#include "HashTable.h"
HashTable<rgbColor> worldSampleHashTable( 30000 );
void resetSampleHashTable() {
worldSampleHashTable.clear();
}
rgbColor sampleFromWorldNoWeightHash( int inX, int inY ) {
char found;
rgbColor sample = worldSampleHashTable.lookup( inX, inY, &found );
if( isSpritePresent( inX, inY ) ) {
// don't consider cached result if a sprite is present
found = false;
}
if( found ) {
return sample;
}
// else not found
// call real function to get result
char transient;
sample = sampleFromWorldNoWeight( inX, inY, &transient );
// insert, but only if not transient
if( !transient ) {
worldSampleHashTable.insert( inX, inY, sample );
}
return sample;
}
rgbColor sampleFromWorldNoWeight( int inX, int inY, char *outTransient ) {
*outTransient = false;
rgbColor returnColor;
char isSampleAnim = false;
// consider sampling from an animation
// draw them in reverse order so that oldest sprites are drawn on top
for( int i=animationList.size() - 1; i>=0; i-- ) {
Animation a = *( animationList.getElement( i ) );
int animW = a.mFrameW;
int animH = a.mFrameH;
// pixel position relative to animation center
// player position centered on sprint left-to-right
int animX = (int)( inX - a.mX + a.mFrameW / 2 );
int animY = (int)( inY - a.mY + a.mFrameH / 2 );
if( animX >= 0 && animX < animW
&&
animY >= 0 && animY < animH ) {
// pixel is in animation frame
int animIndex = animY * a.mImageW + animX;
// skip to appropriate anim page
animIndex += (int)a.mPageNumber * (animW + 1);
// skip to appropriate anim frame
animIndex +=
a.mFrameNumber *
a.mImageW *
( animH + 1 );
// page to blend with
int animBlendIndex = animIndex + animW + 1;
double blendWeight = a.mPageNumber - (int)a.mPageNumber;
if( !isSpriteTransparent( a.mGraphics, animIndex ) ) {
// in non-transparent part
if( blendWeight != 0 ) {
returnColor.r =
( 1 - blendWeight ) * a.mGraphics->mRed[ animIndex ]
+
blendWeight * a.mGraphics->mRed[ animBlendIndex ];
returnColor.g =
( 1 - blendWeight ) * a.mGraphics->mGreen[ animIndex ]
+
blendWeight * a.mGraphics->mGreen[ animBlendIndex ];
returnColor.b =
( 1 - blendWeight ) * a.mGraphics->mBlue[ animIndex ]
+
blendWeight * a.mGraphics->mBlue[ animBlendIndex ];
}
else {
// no blend
returnColor.r = a.mGraphics->mRed[ animIndex ];
returnColor.g = a.mGraphics->mGreen[ animIndex ];
returnColor.b = a.mGraphics->mBlue[ animIndex ];
}
*outTransient = true;
// don't return here, because there might be other
// animations and sprites that are on top of
// this one
isSampleAnim = true;
}
}
}
if( isSampleAnim ) {
// we have already found an animation here that is on top
// of whatever map graphics we might sample below
// thus, we can return now
return returnColor;
}
int tileIndex;
char blocked = isBlocked( inX, inY );
if( !blocked ) {
// empty tile
tileIndex = 0;
}
else {
int neighborsBlockedBinary = 0;
if( isBlocked( inX, inY - tileH ) ) {
// top
neighborsBlockedBinary = neighborsBlockedBinary | 1;
}
if( isBlocked( inX + tileW, inY ) ) {
// right
neighborsBlockedBinary = neighborsBlockedBinary | 1 << 1;
}
if( isBlocked( inX, inY + tileH ) ) {
// bottom
neighborsBlockedBinary = neighborsBlockedBinary | 1 << 2;
}
if( isBlocked( inX - tileW, inY ) ) {
// left
neighborsBlockedBinary = neighborsBlockedBinary | 1 << 3;
}
// skip empty tile, treat as tile index
neighborsBlockedBinary += 1;
tileIndex = neighborsBlockedBinary;
}
// pick a tile set
int netWorldSpan = tileSetWorldSpan + tileSetWorldOverlap;
int tileSet = inX / netWorldSpan + tileSetSkip;
int overhang = inX % netWorldSpan;
if( inX < 0 ) {
// fix to a constant tile set below 0
overhang = 0;
tileSet = 0;
}
// is there blending with next tile set?
int blendTileSet = tileSet + 1;
double blendWeight = 0;
if( overhang > tileSetWorldSpan ) {
blendWeight = ( overhang - tileSetWorldSpan ) /
(double) tileSetWorldOverlap;
}
// else 100% blend of our first tile set
tileSet = tileSet % numTileSets;
blendTileSet = blendTileSet % numTileSets;
// make sure not negative
if( tileSet < 0 ) {
tileSet += numTileSets;
}
if( blendTileSet < 0 ) {
blendTileSet += numTileSets;
}
// apply mapping
tileSet = mapTileSet( tileSet );
blendTileSet = mapTileSet( blendTileSet );
// sample from tile image
int imageY = inY % tileH;
int imageX = inX % tileW;
if( imageX < 0 ) {
imageX += tileW;
}
if( imageY < 0 ) {
imageY += tileH;
}
// offset to top left corner of tile
int tileImageOffset = tileIndex * ( tileH + 1 ) * tileImageW
+ tileSet * (tileW + 1);
int blendTileImageOffset = tileIndex * ( tileH + 1 ) * tileImageW
+ blendTileSet * (tileW + 1);
int imageIndex = tileImageOffset + imageY * tileImageW + imageX;
int blendImageIndex = blendTileImageOffset + imageY * tileImageW + imageX;
returnColor.r =
(1-blendWeight) * tileContainer->mRed[ imageIndex ] +
blendWeight * tileContainer->mRed[ blendImageIndex ];
returnColor.g =
(1-blendWeight) * tileContainer->mGreen[ imageIndex ] +
blendWeight * tileContainer->mGreen[ blendImageIndex ];
returnColor.b =
(1-blendWeight) * tileContainer->mBlue[ imageIndex ] +
blendWeight * tileContainer->mBlue[ blendImageIndex ];
// only consider drawing chests in empty spots
if( !blocked && isChest( inX, inY ) ) {
// draw chest here
char chestType = isChest( inX, inY );
// sample from chest image
int imageY = inY % chestH;
int imageX = inX % chestW;
if( imageX < 0 ) {
imageX += chestW;
}
if( imageY < 0 ) {
imageY += chestH;
}
int spriteIndex = 0;
if( chestType == CHEST_OPEN ) {
spriteIndex = 1;
}
// skip to sub-sprite
int spriteOffset =
spriteIndex *
chestW *
( chestH + 1 );
int subSpriteIndex = imageY * chestW + imageX;
int imageIndex = spriteOffset + subSpriteIndex;
if( !isSpriteTransparent( chestContainer, imageIndex ) ) {
if( chestType == CHEST_CLOSED ) {
*outTransient = true;
}
// check if this is one of the gem locations
char isGem = false;
int gemNumber = 0;
for( int i=0; i<numGems && !isGem; i++ ) {
if( subSpriteIndex == gemLocations[ i ] ) {
isGem = true;
gemNumber = i;
}
}
char gemColorUsed = false;
if( isGem ) {
// check if our gem is turned on for this chest
unsigned char code = getChestCode( inX, inY );
if( code & 0x01 << gemNumber ) {
gemColorUsed = true;
returnColor.r = gemColors[ gemNumber ][ 0 ];
returnColor.g = gemColors[ gemNumber ][ 1 ];
returnColor.b = gemColors[ gemNumber ][ 2 ];
}
}
if( !gemColorUsed ) {
// use underlying chest color
returnColor.r = chestContainer->mRed[ imageIndex ];
returnColor.g = chestContainer->mGreen[ imageIndex ];
returnColor.b = chestContainer->mBlue[ imageIndex ];
}
}
}
return returnColor;
}
int getTileWidth() {
return tileW;
}
int getTileHeight() {
return tileH;
}
void destroyWorld() {
/*
printf( "%d hits, %d misses, %f hit ratio\n",
hitCount, missCount, hitCount / (double)( hitCount + missCount ) );
*/
}
void stepAnimations() {
for( int i=0; i<animationList.size(); i++ ) {
Animation *a = animationList.getElement( i );
if( a->mAutoStep ) {
if( a->mFrameNumber < a->mNumFrames - 1 ) {
a->mFrameNumber ++;
}
else if( a->mRemoveAtEnd ) {
// remove it
animationList.deleteElement( i );
// back up in list for next loop iteration
i--;
}
}
}
}
void startPrizeAnimation( int inX, int inY ) {
Animation a( inX, inY, 16, 16, true, true, prizeAnimationContainer );
animationList.push_back( a );
}
void startDustAnimation( int inX, int inY ) {
Animation a( inX, inY, 16, 16, true, true, dustAnimationContainer );
animationList.push_back( a );
}
void startHeartAnimation( int inX, int inY ) {
Animation a( inX, inY, 16, 16, true, true, heartAnimationContainer );
animationList.push_back( a );
}
#include <math.h>
void setPlayerPosition( int inX, int inY ) {
Animation *spriteAnimation =
animationList.getElement( spriteAnimationIndex );
char moving = false;
if( inX != spriteAnimation->mX ) {
moving = true;
}
spriteAnimation->mX = inX;
// player position centered at sprite's feet
int newSpriteY = inY - spriteAnimation->mFrameH / 2 + 1;
if( newSpriteY != spriteAnimation->mY ) {
moving = true;
}
spriteAnimation->mY = newSpriteY;
if( metSpouse && ! spouseDead ) {
Animation *spouseAnimation =
animationList.getElement( spouseAnimationIndex );
// spouse stands immediately in front of player
int desiredSpouseX = inX + spouseAnimation->mFrameH;
int desiredSpouseY = spriteAnimation->mY;
// gravitates there gradually one pixel at a time in each x and y
int dX = desiredSpouseX - spouseAnimation->mX;
int dY = desiredSpouseY - spouseAnimation->mY;
// convert to -1, 0, or +1
if( dX != 0 ) {
dX = (int)( dX / fabs( dX ) );
}
if( dY != 0 ) {
dY = (int)( dY / fabs( dY ) );
}
if( moving ) {
// only execute this transition when player is moving
spouseAnimation->mX += dX;
spouseAnimation->mY += dY;
}
// check for heart animation and have it track moving couple
for( int i=0; i<animationList.size(); i++ ) {
Animation *a = animationList.getElement( i );
if( a->mGraphics == heartAnimationContainer ) {
// move it halfway between player and spouse
a->mX = ( spouseAnimation->mX - spriteAnimation->mX ) / 2 +
spriteAnimation->mX;
a->mY = ( spouseAnimation->mY - spriteAnimation->mY ) / 2 +
spriteAnimation->mY + 1;
}
}
}
}
void setPlayerSpriteFrame( int inFrame ) {
Animation *spriteAnimation =
animationList.getElement( spriteAnimationIndex );
spriteAnimation->mFrameNumber = inFrame;
if( metSpouse && ! spouseDead ) {
Animation *spouseAnimation =
animationList.getElement( spouseAnimationIndex );
// spouse follows player
spouseAnimation->mFrameNumber = inFrame;
}
}
void setCharacterAges( double inAge ) {
Animation *spriteAnimation =
animationList.getElement( spriteAnimationIndex );
Animation *spouseAnimation =
animationList.getElement( spouseAnimationIndex );
// 0 -> 0.25, constant page 0
if( inAge <= 0.25 ) {
spriteAnimation->mPageNumber = 0;
spouseAnimation->mPageNumber = 0;
}
// 0.75 - 1.0, constant last page
else if( inAge >= 0.75 ) {
spriteAnimation->mPageNumber = spriteAnimation->mNumPages - 1;
spouseAnimation->mPageNumber = spouseAnimation->mNumPages - 1;
}
else {
// blend of pages in between
double blendingAge = ( inAge - 0.25 ) / 0.5;
spriteAnimation->mPageNumber =
blendingAge * ( spriteAnimation->mNumPages - 1 );
spouseAnimation->mPageNumber =
blendingAge * ( spouseAnimation->mNumPages - 1 );
}
}
void getSpousePosition( int *outX, int *outY ) {
Animation *spouseAnimation =
animationList.getElement( spouseAnimationIndex );
*outX = spouseAnimation->mX;
*outY = spouseAnimation->mY + spouseAnimation->mFrameH / 2 - 1;
}
char haveMetSpouse() {
return metSpouse && ! spouseDead;
}
void meetSpouse() {
metSpouse = true;
}
void diePlayer() {
Animation *spriteAnimation =
animationList.getElement( spriteAnimationIndex );
playerDead = true;
// tombstone
spriteAnimation->mFrameNumber = 8;
}
void dieSpouse() {
Animation *spriteAnimation =
animationList.getElement( spriteAnimationIndex );
Animation *spouseAnimation =
animationList.getElement( spouseAnimationIndex );
spouseDead = true;
// tombstone
spouseAnimation->mFrameNumber = 8;
if( metSpouse ) {
// swap player sprite with sad sprite
spriteAnimation->swapGraphics( spriteSadContainer );
}
}
char isPlayerDead() {
return playerDead;
}
char isSpouseDead() {
return spouseDead;
}
void loadWorldGraphics() {
tileContainer = new GraphicContainer( "tileSet.tga" );
chestContainer = new GraphicContainer( "chest.tga" );
spriteContainer = new GraphicContainer( "characterSprite.tga" );
spriteSadContainer = new GraphicContainer( "characterSpriteSad.tga" );
spouseContainer = new GraphicContainer( "spouseSprite.tga" );
prizeAnimationContainer = new GraphicContainer( "chestPrize.tga" );
dustAnimationContainer = new GraphicContainer( "chestDust.tga" );
heartAnimationContainer = new GraphicContainer( "heart.tga" );
}
void destroyWorldGraphics() {
delete tileContainer;
delete chestContainer;
delete spriteContainer;
delete spriteSadContainer;
delete spouseContainer;
delete prizeAnimationContainer;
delete dustAnimationContainer;
delete heartAnimationContainer;
}