mirror of https://gitlab.com/nakst/essence
943 lines
26 KiB
C++
943 lines
26 KiB
C++
// TODO Icon.
|
|
// TODO Music.
|
|
// TODO Draw() with rotation.
|
|
// TODO Saving with new file IO.
|
|
|
|
#include <essence.h>
|
|
|
|
EsInstance *instance;
|
|
uint32_t backgroundColor;
|
|
int musicIndex;
|
|
bool keysPressedSpace, keysPressedEscape, keysPressedX;
|
|
uint32_t previousControllerButtons[ES_GAME_CONTROLLER_MAX_COUNT];
|
|
uint32_t *targetBits;
|
|
size_t targetWidth, targetHeight, targetStride;
|
|
double updateTimeAccumulator;
|
|
float gameScale;
|
|
uint32_t gameOffsetX, gameOffsetY, gameWidth, gameHeight;
|
|
|
|
#define GAME_SIZE (380)
|
|
#define BRAND "fly"
|
|
|
|
struct Texture {
|
|
uint32_t width, height;
|
|
uint32_t *bits;
|
|
};
|
|
|
|
void Transform(float *destination, float *left, float *right) {
|
|
float d[6];
|
|
d[0] = left[0] * right[0] + left[1] * right[3];
|
|
d[1] = left[0] * right[1] + left[1] * right[4];
|
|
d[2] = left[0] * right[2] + left[1] * right[5] + left[2];
|
|
d[3] = left[3] * right[0] + left[4] * right[3];
|
|
d[4] = left[3] * right[1] + left[4] * right[4];
|
|
d[5] = left[3] * right[2] + left[4] * right[5] + left[5];
|
|
destination[0] = d[0];
|
|
destination[1] = d[1];
|
|
destination[2] = d[2];
|
|
destination[3] = d[3];
|
|
destination[4] = d[4];
|
|
destination[5] = d[5];
|
|
}
|
|
|
|
__attribute__((optimize("-O2")))
|
|
void Draw(Texture *texture,
|
|
float x, float y, float w = -1, float h = -1,
|
|
float sx = 0, float sy = 0, float sw = -1, float sh = -1,
|
|
float _a = 1, float _r = 1, float _g = 1, float _b = 1,
|
|
float rot = 0) {
|
|
(void) rot;
|
|
|
|
if (sw == -1 && sh == -1) sw = texture->width, sh = texture->height;
|
|
if (w == -1 && h == -1) w = sw, h = sh;
|
|
if (_a <= 0) return;
|
|
|
|
if (x + w < 0 || y + h < 0 || x > GAME_SIZE || y > GAME_SIZE) return;
|
|
|
|
x *= gameScale, y *= gameScale, w *= gameScale, h *= gameScale;
|
|
|
|
float m[6] = {};
|
|
float m1[6] = { sw / w, 0, 0, 0, sh / h, 0 };
|
|
float m2[6] = { 1, 0, -x * (sw / w), 0, 1, -y * (sh / h) };
|
|
|
|
Transform(m, m2, m1);
|
|
|
|
intptr_t yStart = y - 1, yEnd = y + h + 1, xStart = x - 1, xEnd = x + w + 1;
|
|
// if (rot) yStart -= h * 0.45f, yEnd += h * 0.45f, xStart -= w * 0.45f, xEnd += w * 0.45f;
|
|
if (yStart < 0) yStart = 0;
|
|
if (yStart > gameHeight) yStart = gameHeight;
|
|
if (yEnd < 0) yEnd = 0;
|
|
if (yEnd > gameHeight) yEnd = gameHeight;
|
|
if (xStart < 0) xStart = 0;
|
|
if (xStart > gameWidth) xStart = gameWidth;
|
|
if (xEnd < 0) xEnd = 0;
|
|
if (xEnd > gameWidth) xEnd = gameWidth;
|
|
m[2] += m[0] * xStart, m[5] += m[3] * xStart;
|
|
|
|
uint32_t *scanlineStart = targetBits + (gameOffsetY + yStart) * targetStride / 4 + (gameOffsetX + xStart);
|
|
|
|
uint32_t r = _r * 255, g = _g * 255, b = _b * 255, a = _a * 255;
|
|
|
|
for (intptr_t y = yStart; y < yEnd; y++, scanlineStart += targetStride / 4) {
|
|
uint32_t *output = scanlineStart;
|
|
|
|
float tx = m[1] * y + m[2];
|
|
float ty = m[4] * y + m[5];
|
|
|
|
for (intptr_t x = xStart; x < xEnd; x++, output++, tx += m[0], ty += m[3]) {
|
|
if (tx + sx < 0 || ty + sy < 0) continue;
|
|
uintptr_t txi = tx + sx, tyi = ty + sy;
|
|
if (txi < sx || tyi < sy || txi >= sx + sw || tyi >= sy + sh) continue;
|
|
uint32_t modified = texture->bits[tyi * texture->width + txi];
|
|
if (!(modified & 0xFF000000)) continue;
|
|
uint32_t original = *output;
|
|
uint32_t alpha1 = (((modified & 0xFF000000) >> 24) * a) >> 8;
|
|
uint32_t alpha2 = (255 - alpha1) << 8;
|
|
uint32_t r2 = alpha2 * ((original & 0x00FF0000) >> 16);
|
|
uint32_t g2 = alpha2 * ((original & 0x0000FF00) >> 8);
|
|
uint32_t b2 = alpha2 * ((original & 0x000000FF) >> 0);
|
|
uint32_t r1 = r * alpha1 * ((modified & 0x00FF0000) >> 16);
|
|
uint32_t g1 = g * alpha1 * ((modified & 0x0000FF00) >> 8);
|
|
uint32_t b1 = b * alpha1 * ((modified & 0x000000FF) >> 0);
|
|
*output = 0xFF000000
|
|
| (0x00FF0000 & ((r1 + r2) << 0))
|
|
| (0x0000FF00 & ((g1 + g2) >> 8))
|
|
| (0x000000FF & ((b1 + b2) >> 16));
|
|
}
|
|
}
|
|
}
|
|
|
|
void CreateTexture(Texture *texture, const char *cName) {
|
|
size_t dataBytes;
|
|
const void *data = EsEmbeddedFileGet(cName, -1, &dataBytes);
|
|
texture->bits = (uint32_t *) EsImageLoad(data, dataBytes, &texture->width, &texture->height, 4);
|
|
EsAssert(texture->bits);
|
|
}
|
|
|
|
void ExitGame() {
|
|
EsInstanceDestroy(instance);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////
|
|
|
|
#define TAG_ALL (-1)
|
|
#define TAG_SOLID (-2)
|
|
#define TAG_KILL (-3)
|
|
#define TAG_WORLD (-4)
|
|
|
|
#define TILE_SIZE (16)
|
|
|
|
struct Entity {
|
|
uint8_t tag;
|
|
uint8_t layer;
|
|
bool solid, kill, hide;
|
|
char symbol;
|
|
int8_t frameCount, stepsPerFrame;
|
|
Texture *texture;
|
|
float texOffX, texOffY, w, h;
|
|
int uid;
|
|
|
|
void (*create)(Entity *);
|
|
void (*destroy)(Entity *);
|
|
void (*stepAlways)(Entity *); // Called even if level editing.
|
|
void (*step)(Entity *);
|
|
void (*draw)(Entity *);
|
|
void (*drawAfter)(Entity *);
|
|
|
|
float x, y, px, py;
|
|
int stepIndex;
|
|
bool isUsed, isDestroyed;
|
|
|
|
union {
|
|
struct {
|
|
float cx, cy;
|
|
float th, dth;
|
|
float dths;
|
|
int respawn, respawnGrow, dash;
|
|
} player;
|
|
|
|
struct {
|
|
int random;
|
|
} block;
|
|
|
|
struct {
|
|
float vel;
|
|
} moveBlock;
|
|
|
|
struct {
|
|
float th, cx, cy;
|
|
} spin;
|
|
|
|
struct {
|
|
float vx, vy, th, dth;
|
|
int life;
|
|
uint32_t col;
|
|
} star;
|
|
};
|
|
|
|
void Destroy() {
|
|
if (isDestroyed) return;
|
|
isDestroyed = true;
|
|
if (destroy) destroy(this);
|
|
}
|
|
};
|
|
|
|
Texture textureDashMessage, textureKeyMessage, textureKey, textureControlsMessage, textureWhite;
|
|
|
|
Entity typePlayer, typeBlock, typeCheck, typeMoveH, typeStar, typeMoveV, typePowerup, typeShowMessage, typeKey, typeLock, typeSpin, typeEnd;
|
|
|
|
// All the entities that can be loaded from the rooms.
|
|
Entity *entityTypes[] = {
|
|
&typeBlock, &typeCheck, &typeMoveH, &typeMoveV, &typePowerup, &typeKey, &typeLock, &typeSpin, &typeEnd,
|
|
nullptr,
|
|
};
|
|
|
|
int levelTick;
|
|
bool justLoaded = true;
|
|
|
|
#define MAX_ENTITIES (1000)
|
|
|
|
struct SaveState {
|
|
float checkX, checkY;
|
|
int roomX, roomY;
|
|
bool hasDash, hasKey;
|
|
int deathCount;
|
|
uint32_t check;
|
|
};
|
|
|
|
struct GameState : SaveState {
|
|
Entity entities[MAX_ENTITIES];
|
|
int world;
|
|
Entity *player;
|
|
};
|
|
|
|
GameState state;
|
|
|
|
Entity *AddEntity(Entity *templateEntity, float x, float y, int uid = 0) {
|
|
for (int i = 0; i < MAX_ENTITIES; i++) {
|
|
if (!state.entities[i].isUsed) {
|
|
EsMemoryCopy(state.entities + i, templateEntity, sizeof(Entity));
|
|
state.entities[i].isUsed = true;
|
|
if (!state.entities[i].frameCount) state.entities[i].frameCount = 1;
|
|
if (!state.entities[i].stepsPerFrame) state.entities[i].stepsPerFrame = 1;
|
|
state.entities[i].x = state.entities[i].px = x;
|
|
state.entities[i].y = state.entities[i].py = y;
|
|
if (!state.entities[i].w) state.entities[i].w = state.entities[i].texture->width - 1;
|
|
if (!state.entities[i].h) state.entities[i].h = state.entities[i].texture->height / state.entities[i].frameCount - 1;
|
|
state.entities[i].uid = uid;
|
|
if (state.entities[i].create) state.entities[i].create(state.entities + i);
|
|
return state.entities + i;
|
|
}
|
|
}
|
|
|
|
static Entity fake = {};
|
|
return &fake;
|
|
}
|
|
|
|
Entity *FindEntity(float x, float y, float w, float h, int tag, Entity *exclude) {
|
|
for (int i = 0; i < MAX_ENTITIES; i++) {
|
|
if (state.entities[i].isUsed && !state.entities[i].isDestroyed
|
|
&& (state.entities[i].tag == tag
|
|
|| tag == TAG_ALL
|
|
|| (tag == TAG_SOLID && state.entities[i].solid)
|
|
|| (tag == TAG_KILL && state.entities[i].kill)
|
|
)
|
|
&& state.entities + i != exclude) {
|
|
if (x <= state.entities[i].x + state.entities[i].w && state.entities[i].x <= x + w
|
|
&& y <= state.entities[i].y + state.entities[i].h && state.entities[i].y <= y + h) {
|
|
return state.entities + i;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
char roomName[16];
|
|
|
|
void UpdateRoomName() {
|
|
roomName[0] = 'R';
|
|
roomName[1] = (state.roomX / 10) + '0';
|
|
roomName[2] = (state.roomX % 10) + '0';
|
|
roomName[3] = '_';
|
|
roomName[4] = (state.roomY / 10) + '0';
|
|
roomName[5] = (state.roomY % 10) + '0';
|
|
roomName[6] = '.';
|
|
roomName[7] = 'D';
|
|
roomName[8] = 'A';
|
|
roomName[9] = 'T';
|
|
roomName[10] = 0;
|
|
}
|
|
|
|
#define ROOM_ID ((state.roomX - 7) + (state.roomY - 9) * 6)
|
|
|
|
void LoadRoom() {
|
|
state.world = 0;
|
|
|
|
UpdateRoomName();
|
|
|
|
roomName[6] = '_';
|
|
const uint8_t *buffer = (const uint8_t *) EsEmbeddedFileGet(roomName, -1);
|
|
|
|
for (int i = 0; i < MAX_ENTITIES; i++) {
|
|
if (!state.entities[i].isUsed || state.entities[i].isDestroyed) continue;
|
|
|
|
if (state.entities[i].tag != typePlayer.tag) {
|
|
state.entities[i].Destroy();
|
|
} else {
|
|
state.entities[i].px = state.entities[i].x;
|
|
state.entities[i].py = state.entities[i].y;
|
|
}
|
|
}
|
|
|
|
int p = 0;
|
|
int iir = 0;
|
|
|
|
while (true) {
|
|
uint8_t tag = buffer[p++];
|
|
if (!tag) break;
|
|
float x = *(float *) (buffer + p); p += 4;
|
|
float y = *(float *) (buffer + p); p += 4;
|
|
|
|
if (tag == (uint8_t) TAG_WORLD) {
|
|
state.world = x;
|
|
}
|
|
|
|
for (int i = 0; entityTypes[i]; i++) {
|
|
if (entityTypes[i]->tag == tag) {
|
|
AddEntity(entityTypes[i], x, y, (state.roomX << 24) | (state.roomY << 16) | iir);
|
|
iir++;
|
|
}
|
|
}
|
|
}
|
|
|
|
musicIndex = state.world;
|
|
}
|
|
|
|
void CalculateCheck() {
|
|
state.check = 0;
|
|
|
|
uint8_t *buffer = (uint8_t *) &state;
|
|
uint32_t check = 0x1D471D47;
|
|
|
|
for (uintptr_t i = 0; i < sizeof(SaveState); i++) {
|
|
check ^= ((uint32_t) buffer[i] + 10) * (i + 100);
|
|
}
|
|
|
|
state.check = check;
|
|
}
|
|
|
|
float FadeInOut(float t) {
|
|
if (t < 0.3f) return t / 0.3f;
|
|
else if (t < 0.7f) return 1;
|
|
else return 1 - (t - 0.7f) / 0.3f;
|
|
}
|
|
|
|
void InitialiseGame() {
|
|
state.roomX = 10;
|
|
state.roomY = 10;
|
|
|
|
CreateTexture(&textureWhite, "white_png");
|
|
CreateTexture(&textureKey, "key_png");
|
|
CreateTexture(&textureDashMessage, "dash_msg_png");
|
|
CreateTexture(&textureKeyMessage, "key_msg_png");
|
|
CreateTexture(&textureControlsMessage, "controls_png");
|
|
|
|
int tag = 1;
|
|
|
|
{
|
|
static Texture texture;
|
|
CreateTexture(&texture, "player_png");
|
|
typePlayer.tag = tag++;
|
|
typePlayer.texture = &texture;
|
|
typePlayer.frameCount = 6;
|
|
typePlayer.stepsPerFrame = 5;
|
|
typePlayer.layer = 1;
|
|
|
|
typePlayer.step = [] (Entity *entity) {
|
|
if (entity->player.respawn) {
|
|
if (entity->stepIndex > entity->player.respawn) {
|
|
entity->player.respawn = 0;
|
|
entity->hide = false;
|
|
entity->player.cx = state.checkX;
|
|
entity->player.cy = state.checkY;
|
|
entity->player.respawnGrow = entity->stepIndex + 20;
|
|
entity->player.dash = 0;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!entity->player.respawnGrow && !entity->player.dash) {
|
|
if (keysPressedSpace) {
|
|
entity->player.cx += (entity->x - entity->player.cx) * 2;
|
|
entity->player.cy += (entity->y - entity->player.cy) * 2;
|
|
entity->player.th += 3.15f;
|
|
entity->player.dth = -entity->player.dth;
|
|
entity->player.dths = 5;
|
|
} else if (keysPressedX && state.hasDash) {
|
|
entity->player.dash = 10;
|
|
}
|
|
}
|
|
|
|
float rd = 40;
|
|
|
|
if (entity->player.respawnGrow) {
|
|
if (entity->stepIndex > entity->player.respawnGrow) {
|
|
entity->player.respawnGrow = 0;
|
|
} else {
|
|
rd *= 1 - (entity->player.respawnGrow - entity->stepIndex) / 20.0f;
|
|
}
|
|
}
|
|
|
|
entity->x = rd * EsCRTcosf(entity->player.th) + entity->player.cx;
|
|
entity->y = rd * EsCRTsinf(entity->player.th) + entity->player.cy + 5 * EsCRTsinf(4.71f + entity->stepIndex * 0.1f);
|
|
|
|
if (entity->player.dash) {
|
|
float pt = entity->player.th - entity->player.dth;
|
|
float px = rd * EsCRTcosf(pt) + entity->player.cx;
|
|
float py = rd * EsCRTsinf(pt) + entity->player.cy + 5 * EsCRTsinf(4.71f + (entity->stepIndex - 1) * 0.1f);
|
|
float dx = entity->x - px;
|
|
float dy = entity->y - py;
|
|
entity->player.cx += dx * 3.2f;
|
|
entity->player.cy += dy * 3.2f;
|
|
entity->player.dash--;
|
|
AddEntity(&typeStar, entity->x + 8, entity->y + 8)->star.col = 0xFF;
|
|
} else {
|
|
entity->player.th += entity->player.dth;
|
|
}
|
|
|
|
if (entity->player.respawnGrow) {
|
|
entity->px = entity->x;
|
|
entity->py = entity->y;
|
|
}
|
|
|
|
if (entity->player.dths) {
|
|
entity->player.th += entity->player.dth;
|
|
entity->player.dths--;
|
|
entity->stepIndex = 5 * 3;
|
|
}
|
|
|
|
if (FindEntity(entity->x + 5, entity->y + 5, entity->w - 10, entity->h - 10, TAG_KILL, 0)) {
|
|
for (int i = 0; i < 20; i++) {
|
|
AddEntity(&typeStar, entity->x + 8, entity->y + 8)->star.col = 0xFF0000;
|
|
}
|
|
|
|
entity->hide = true;
|
|
entity->player.respawn = entity->stepIndex + 20;
|
|
state.deathCount++;
|
|
}
|
|
|
|
if (entity->x > GAME_SIZE - 8) {
|
|
entity->x -= GAME_SIZE;
|
|
entity->player.cx -= GAME_SIZE;
|
|
state.checkX -= GAME_SIZE;
|
|
state.roomX++;
|
|
LoadRoom();
|
|
} else if (entity->x < -8) {
|
|
entity->x += GAME_SIZE;
|
|
entity->player.cx += GAME_SIZE;
|
|
state.checkX += GAME_SIZE;
|
|
state.roomX--;
|
|
LoadRoom();
|
|
}
|
|
|
|
if (entity->y > GAME_SIZE - 8) {
|
|
entity->y -= GAME_SIZE;
|
|
entity->player.cy -= GAME_SIZE;
|
|
state.checkY -= GAME_SIZE;
|
|
state.roomY++;
|
|
LoadRoom();
|
|
} else if (entity->y < -8) {
|
|
entity->y += GAME_SIZE;
|
|
entity->player.cy += GAME_SIZE;
|
|
state.checkY += GAME_SIZE;
|
|
state.roomY--;
|
|
LoadRoom();
|
|
}
|
|
};
|
|
|
|
typePlayer.create = [] (Entity *entity) {
|
|
state.player = entity;
|
|
entity->player.cx = entity->x;
|
|
entity->player.cy = entity->y;
|
|
entity->player.dth = 0.06f;
|
|
};
|
|
}
|
|
|
|
{
|
|
static Texture texture;
|
|
CreateTexture(&texture, "block_png");
|
|
|
|
static Texture block2;
|
|
CreateTexture(&block2, "block2_png");
|
|
|
|
typeBlock.tag = tag++;
|
|
typeBlock.texture = &texture;
|
|
typeBlock.kill = true;
|
|
typeBlock.hide = true;
|
|
typeBlock.layer = 2;
|
|
|
|
typeBlock.create = [] (Entity *entity) {
|
|
uint8_t r = EsRandomU8();
|
|
|
|
if (r < 20) {
|
|
entity->block.random = 1;
|
|
} else if (r < 50) {
|
|
entity->block.random = 2;
|
|
} else {
|
|
entity->block.random = 0;
|
|
}
|
|
};
|
|
|
|
typeBlock.stepAlways = [] (Entity *entity) {
|
|
if (FindEntity(entity->x + 1, entity->y + 1, entity->w - 2, entity->h - 2, entity->tag, entity)) {
|
|
entity->Destroy();
|
|
}
|
|
};
|
|
|
|
typeBlock.drawAfter = [] (Entity *entity) {
|
|
if (!FindEntity(entity->x - 16, entity->y, 1, 1, entity->tag, entity)) {
|
|
Draw(&block2, entity->x - 16, entity->y, 16, 16, 0, entity->block.random * 16, 16, 16, 1);
|
|
}
|
|
|
|
if (!FindEntity(entity->x + 16, entity->y, 1, 1, entity->tag, entity)) {
|
|
Draw(&block2, entity->x + 16, entity->y, 16, 16, 32, entity->block.random * 16, 16, 16, 1);
|
|
}
|
|
|
|
if (!FindEntity(entity->x, entity->y - 16, 1, 1, entity->tag, entity)) {
|
|
Draw(&block2, entity->x, entity->y - 16, 16, 16, 16, entity->block.random * 16, 16, 16, 1);
|
|
}
|
|
|
|
if (!FindEntity(entity->x, entity->y + 16, 1, 1, entity->tag, entity)) {
|
|
Draw(&block2, entity->x, entity->y + 16, 16, 16, 48, entity->block.random * 16, 16, 16, 1);
|
|
}
|
|
};
|
|
}
|
|
|
|
{
|
|
static Texture check1, check2;
|
|
CreateTexture(&check1, "check1_png");
|
|
CreateTexture(&check2, "check2_png");
|
|
typeCheck.tag = tag++;
|
|
typeCheck.texture = &check1;
|
|
|
|
typeCheck.step = [] (Entity *entity) {
|
|
if (state.checkX == entity->x && state.checkY == entity->y) {
|
|
entity->texture = &check2;
|
|
} else {
|
|
entity->texture = &check1;
|
|
}
|
|
|
|
if (FindEntity(entity->x - 4, entity->y - 4, entity->w + 8, entity->h + 8, typePlayer.tag, 0)) {
|
|
if (state.checkX != entity->x || state.checkY != entity->y) {
|
|
for (int i = 0; i < 10; i++) AddEntity(&typeStar, entity->x + 8, entity->y + 8)->star.col = 0xFFFFFF;
|
|
}
|
|
|
|
state.checkX = entity->x;
|
|
state.checkY = entity->y;
|
|
|
|
CalculateCheck();
|
|
EsFileWriteAll("|Settings:/Save.dat", -1, &state, sizeof(SaveState));
|
|
}
|
|
};
|
|
}
|
|
|
|
{
|
|
static Texture texture;
|
|
CreateTexture(&texture, "moveblock_png");
|
|
typeMoveH.tag = tag++;
|
|
typeMoveH.texture = &texture;
|
|
typeMoveH.kill = true;
|
|
typeMoveH.w = 16;
|
|
typeMoveH.h = 16;
|
|
typeMoveH.texOffX = -4;
|
|
typeMoveH.texOffY = -4;
|
|
|
|
typeMoveH.create = [] (Entity *entity) {
|
|
entity->moveBlock.vel = -4;
|
|
};
|
|
|
|
typeMoveH.step = [] (Entity *entity) {
|
|
entity->x += entity->moveBlock.vel;
|
|
|
|
if (FindEntity(entity->x, entity->y, entity->w, entity->h, typeBlock.tag, 0)) {
|
|
entity->moveBlock.vel = -entity->moveBlock.vel;
|
|
}
|
|
};
|
|
}
|
|
|
|
{
|
|
// Removed entity.
|
|
tag++;
|
|
}
|
|
|
|
{
|
|
static Texture texture;
|
|
CreateTexture(&texture, "star_png");
|
|
typeStar.texture = &texture;
|
|
typeStar.tag = tag++;
|
|
typeStar.hide = true; // Draw manually.
|
|
typeStar.layer = 2;
|
|
|
|
typeStar.create = [] (Entity *entity) {
|
|
float th = EsRandomU8() / 255.0 * 6.24;
|
|
float sp = EsRandomU8() / 255.0 * 0.5 + 0.5;
|
|
entity->star.vx = sp * EsCRTcosf(th);
|
|
entity->star.vy = sp * EsCRTsinf(th);
|
|
entity->star.life = EsRandomU8();
|
|
entity->star.dth = (EsRandomU8() / 255.0f - 0.5f) * 0.2f;
|
|
};
|
|
|
|
typeStar.step = [] (Entity *entity) {
|
|
entity->x += entity->star.vx;
|
|
entity->y += entity->star.vy;
|
|
entity->star.th += entity->star.dth;
|
|
|
|
if (entity->star.life < entity->stepIndex) {
|
|
entity->Destroy();
|
|
}
|
|
};
|
|
|
|
typeStar.drawAfter = [] (Entity *entity) {
|
|
Draw(entity->texture, entity->x - 4, entity->y - 4, -1, -1, 0, 0, -1, -1,
|
|
1.0 - (float) entity->stepIndex / entity->star.life,
|
|
((entity->star.col >> 16) & 0xFF) / 255.0f,
|
|
((entity->star.col >> 8) & 0xFF) / 255.0f,
|
|
((entity->star.col >> 0) & 0xFF) / 255.0f,
|
|
entity->star.th);
|
|
};
|
|
}
|
|
|
|
{
|
|
static Texture texture;
|
|
CreateTexture(&texture, "moveblock_png");
|
|
typeMoveV.tag = tag++;
|
|
typeMoveV.texture = &texture;
|
|
typeMoveV.kill = true;
|
|
typeMoveV.w = 16;
|
|
typeMoveV.h = 16;
|
|
typeMoveV.texOffX = -4;
|
|
typeMoveV.texOffY = -4;
|
|
|
|
typeMoveV.create = [] (Entity *entity) {
|
|
entity->moveBlock.vel = -4;
|
|
};
|
|
|
|
typeMoveV.step = [] (Entity *entity) {
|
|
entity->y += entity->moveBlock.vel;
|
|
|
|
if (FindEntity(entity->x, entity->y, entity->w, entity->h, typeBlock.tag, 0)) {
|
|
entity->moveBlock.vel = -entity->moveBlock.vel;
|
|
}
|
|
};
|
|
}
|
|
|
|
{
|
|
static Texture texture;
|
|
CreateTexture(&texture, "powerup_png");
|
|
typePowerup.tag = tag++;
|
|
typePowerup.texture = &texture;
|
|
|
|
typePowerup.step = [] (Entity *entity) {
|
|
if (state.hasDash) {
|
|
entity->Destroy();
|
|
return;
|
|
}
|
|
|
|
if (FindEntity(entity->x, entity->y, entity->w, entity->h, typePlayer.tag, 0)) {
|
|
state.hasDash = true;
|
|
AddEntity(&typeShowMessage, 0, 0)->texture = &textureDashMessage;
|
|
entity->Destroy();
|
|
}
|
|
};
|
|
}
|
|
|
|
{
|
|
typeShowMessage.texture = &textureKeyMessage;
|
|
typeShowMessage.tag = tag++;
|
|
typeShowMessage.hide = true;
|
|
|
|
typeShowMessage.draw = [] (Entity *entity) {
|
|
Draw(entity->texture, entity->x, entity->y, -1, -1, 0, 0, -1, -1, FadeInOut(entity->stepIndex / 180.0));
|
|
if (entity->stepIndex > 180) entity->Destroy();
|
|
};
|
|
}
|
|
|
|
{
|
|
typeKey.tag = tag++;
|
|
typeKey.texture = &textureKey;
|
|
|
|
typeKey.step = [] (Entity *entity) {
|
|
if (state.hasKey) {
|
|
entity->Destroy();
|
|
} else if (FindEntity(entity->x, entity->y, entity->w, entity->h, typePlayer.tag, 0)) {
|
|
state.hasKey = true;
|
|
AddEntity(&typeShowMessage, 0, 0)->texture = &textureKeyMessage;
|
|
entity->Destroy();
|
|
for (int i = 0; i < 10; i++) AddEntity(&typeStar, entity->x + 8, entity->y + 8)->star.col = 0xFFFFFF;
|
|
}
|
|
};
|
|
}
|
|
|
|
{
|
|
static Texture texture;
|
|
CreateTexture(&texture, "lock_png");
|
|
typeLock.tag = tag++;
|
|
typeLock.texture = &texture;
|
|
typeLock.kill = true;
|
|
|
|
typeLock.step = [] (Entity *entity) {
|
|
if (state.hasKey) {
|
|
for (int i = 0; i < 1; i++) AddEntity(&typeStar, entity->x + 8, entity->y + 8)->star.col = 0x000000;
|
|
entity->Destroy();
|
|
}
|
|
};
|
|
}
|
|
|
|
{
|
|
static Texture texture;
|
|
CreateTexture(&texture, "moveblock_png");
|
|
typeSpin.tag = tag++;
|
|
typeSpin.texture = &texture;
|
|
typeSpin.kill = true;
|
|
typeSpin.w = 16;
|
|
typeSpin.h = 16;
|
|
typeSpin.texOffX = -4;
|
|
typeSpin.texOffY = -4;
|
|
|
|
typeSpin.create = [] (Entity *entity) {
|
|
entity->spin.cx = entity->x;
|
|
entity->spin.cy = entity->y;
|
|
};
|
|
|
|
typeSpin.step = [] (Entity *entity) {
|
|
entity->x = 60 * EsCRTcosf(entity->spin.th) + entity->spin.cx;
|
|
entity->y = 60 * EsCRTsinf(entity->spin.th) + entity->spin.cy;
|
|
entity->spin.th += 0.04f;
|
|
};
|
|
}
|
|
|
|
{
|
|
static Texture msg1, msg2, msg3, num;
|
|
CreateTexture(&msg1, "end1_png");
|
|
CreateTexture(&msg2, "end2_png");
|
|
CreateTexture(&msg3, "end3_png");
|
|
CreateTexture(&num, "numbers_png");
|
|
|
|
typeEnd.tag = tag++;
|
|
typeEnd.texture = &textureKey;
|
|
typeEnd.hide = true;
|
|
|
|
typeEnd.create = [] (Entity *) {
|
|
state.player->Destroy();
|
|
};
|
|
|
|
typeEnd.draw = [] (Entity *entity) {
|
|
float t = entity->stepIndex / 180.0f;
|
|
|
|
if (t < 1) {
|
|
Draw(&msg1, 40, 150, -1, -1, 0, 0, -1, -1, FadeInOut(t));
|
|
} else if (t < 2) {
|
|
Draw(&msg2, 40, 150, -1, -1, 0, 0, -1, -1, FadeInOut(t - 1));
|
|
} else if (t < 3) {
|
|
Draw(&msg3, 40, 150, -1, -1, 0, 0, -1, -1, FadeInOut(t - 2));
|
|
|
|
int p = state.deathCount;
|
|
char digits[10];
|
|
int dc = 0;
|
|
|
|
if (p == 0) {
|
|
digits[dc++] = 0;
|
|
} else {
|
|
while (p) {
|
|
digits[dc++] = p % 10;
|
|
p /= 10;
|
|
}
|
|
}
|
|
|
|
int w = dc * 16;
|
|
|
|
for (int i = dc - 1; i >= 0; i--) {
|
|
Draw(&num, 40 + 150 - w / 2 + (dc - 1 - i) * 16,
|
|
150 + 33, 16, 30, 16 * digits[i], 0, 16, 30, FadeInOut(t - 2));
|
|
}
|
|
} else {
|
|
ExitGame();
|
|
}
|
|
};
|
|
}
|
|
|
|
state.checkX = GAME_SIZE / 2;
|
|
state.checkY = GAME_SIZE / 2 - 20;
|
|
|
|
size_t loadedStateBytes;
|
|
SaveState *loadedState = (SaveState *) EsFileReadAll("|Settings:/Save.dat", -1, &loadedStateBytes);
|
|
bool noSave = true;
|
|
|
|
if (loadedStateBytes == sizeof(SaveState)) {
|
|
EsMemoryCopy(&state, loadedState, loadedStateBytes);
|
|
|
|
uint32_t oldCheck = state.check;
|
|
CalculateCheck();
|
|
EsAssert(oldCheck == state.check);
|
|
|
|
noSave = false;
|
|
}
|
|
|
|
LoadRoom();
|
|
|
|
AddEntity(&typePlayer, state.checkX, state.checkY);
|
|
|
|
if (noSave) {
|
|
AddEntity(&typeShowMessage, 0, GAME_SIZE - 65)->texture = &textureControlsMessage;
|
|
}
|
|
}
|
|
|
|
void UpdateGame() {
|
|
if (keysPressedEscape) {
|
|
ExitGame();
|
|
}
|
|
|
|
for (int i = 0; i < MAX_ENTITIES; i++) {
|
|
if (state.entities[i].isUsed) {
|
|
state.entities[i].stepIndex++;
|
|
|
|
state.entities[i].px += (state.entities[i].x - state.entities[i].px) * 0.5f;
|
|
state.entities[i].py += (state.entities[i].y - state.entities[i].py) * 0.5f;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < MAX_ENTITIES; i++) if (state.entities[i].isUsed && state.entities[i].stepAlways) state.entities[i].stepAlways(state.entities + i);
|
|
|
|
for (int i = 0; i < MAX_ENTITIES; i++) if (state.entities[i].isUsed && state.entities[i].step) {
|
|
state.entities[i].step(state.entities + i);
|
|
}
|
|
|
|
for (int i = 0; i < MAX_ENTITIES; i++) {
|
|
if (state.entities[i].isUsed && state.entities[i].isDestroyed) state.entities[i].isUsed = false;
|
|
}
|
|
|
|
levelTick++;
|
|
|
|
state.world %= 3;
|
|
|
|
if (state.world == 0) {
|
|
backgroundColor = 0xbef1b1;
|
|
} else if (state.world == 1) {
|
|
backgroundColor = 0xcee5f1;
|
|
} else if (state.world == 2) {
|
|
backgroundColor = 0xf3bdf6;
|
|
}
|
|
}
|
|
|
|
void RenderGame() {
|
|
for (int layer = -1; layer <= 3; layer++) {
|
|
for (int i = 0; i < MAX_ENTITIES; i++) {
|
|
Entity *entity = state.entities + i;
|
|
if (!entity->isUsed) continue;
|
|
if (entity->layer != layer) continue;
|
|
if (!entity->texture) continue;
|
|
if (entity->hide) continue;
|
|
|
|
int frame = entity->stepsPerFrame >= 0 ? ((entity->stepIndex / entity->stepsPerFrame) % entity->frameCount) : (-entity->stepsPerFrame - 1);
|
|
Draw(entity->texture, (int) (entity->px + entity->texOffX + 0.5f),
|
|
(int) (entity->py + entity->texOffY + 0.5f),
|
|
entity->texture->width, entity->texture->height / entity->frameCount,
|
|
0, entity->texture->height / entity->frameCount * frame,
|
|
entity->texture->width, entity->texture->height / entity->frameCount);
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < MAX_ENTITIES; i++) if (state.entities[i].isUsed && state.entities[i].draw ) state.entities[i].draw (state.entities + i);
|
|
for (int i = 0; i < MAX_ENTITIES; i++) if (state.entities[i].isUsed && state.entities[i].drawAfter ) state.entities[i].drawAfter (state.entities + i);
|
|
|
|
if (state.hasKey) {
|
|
Draw(&textureKey, GAME_SIZE - 20, 4);
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////
|
|
|
|
int ProcessCanvasMessage(EsElement *element, EsMessage *message) {
|
|
if (message->type == ES_MSG_ANIMATE) {
|
|
message->animate.complete = false;
|
|
updateTimeAccumulator += message->animate.deltaMs / 1000.0;
|
|
|
|
while (updateTimeAccumulator > 1 / 60.0) {
|
|
{
|
|
EsGameControllerState state[ES_GAME_CONTROLLER_MAX_COUNT];
|
|
size_t count = EsGameControllerStatePoll(state);
|
|
|
|
for (uintptr_t i = 0; i < count; i++) {
|
|
if (state[i].buttons & (1 << 0) && (~previousControllerButtons[i] & (1 << 0))) {
|
|
keysPressedSpace = true;
|
|
} else if (state[i].buttons & (1 << 1) && (~previousControllerButtons[i] & (1 << 1))) {
|
|
keysPressedX = true;
|
|
}
|
|
|
|
previousControllerButtons[i] = state[i].buttons;
|
|
}
|
|
}
|
|
|
|
UpdateGame();
|
|
updateTimeAccumulator -= 1 / 60.0;
|
|
keysPressedSpace = keysPressedEscape = keysPressedX = false;
|
|
}
|
|
|
|
EsElementRepaint(element);
|
|
} else if (message->type == ES_MSG_KEY_DOWN && !message->keyboard.repeat) {
|
|
if (message->keyboard.scancode == ES_SCANCODE_SPACE || message->keyboard.scancode == ES_SCANCODE_Z) {
|
|
keysPressedSpace = true;
|
|
} else if (message->keyboard.scancode == ES_SCANCODE_ESCAPE) {
|
|
keysPressedEscape = true;
|
|
} else if (message->keyboard.scancode == ES_SCANCODE_X) {
|
|
keysPressedX = true;
|
|
}
|
|
} else if (message->type == ES_MSG_PAINT) {
|
|
EsPainter *painter = message->painter;
|
|
EsPaintTargetStartDirectAccess(painter->target, &targetBits, nullptr, nullptr, &targetStride);
|
|
targetBits = (uint32_t *) ((uint8_t *) targetBits + targetStride * painter->offsetY + 4 * painter->offsetX);
|
|
targetWidth = painter->width, targetHeight = painter->height;
|
|
|
|
gameScale = (float) painter->width / GAME_SIZE;
|
|
if (gameScale * GAME_SIZE > painter->height) gameScale = (float) painter->height / GAME_SIZE;
|
|
if (gameScale > 1) gameScale = EsCRTfloorf(gameScale);
|
|
gameWidth = GAME_SIZE * gameScale, gameHeight = GAME_SIZE * gameScale;
|
|
gameOffsetX = painter->width / 2 - gameWidth / 2;
|
|
gameOffsetY = painter->height / 2 - gameHeight / 2;
|
|
|
|
// TODO Clear margins.
|
|
|
|
Draw(&textureWhite, 0, 0, GAME_SIZE, GAME_SIZE, 0, 0, 1, 1, 1,
|
|
((backgroundColor >> 16) & 0xFF) / 255.0f,
|
|
((backgroundColor >> 8) & 0xFF) / 255.0f,
|
|
((backgroundColor >> 0) & 0xFF) / 255.0f);
|
|
|
|
RenderGame();
|
|
|
|
EsPaintTargetEndDirectAccess(painter->target);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void _start() {
|
|
_init();
|
|
|
|
while (true) {
|
|
EsMessage *message = EsMessageReceive();
|
|
|
|
if (message->type == ES_MSG_INSTANCE_CREATE) {
|
|
instance = EsInstanceCreate(message, BRAND);
|
|
EsWindow *window = instance->window;
|
|
EsPanel *container = EsPanelCreate(window, ES_CELL_FILL, ES_STYLE_PANEL_WINDOW_DIVIDER);
|
|
EsElement *canvas = EsCustomElementCreate(container, ES_CELL_FILL | ES_ELEMENT_FOCUSABLE, {});
|
|
canvas->messageUser = ProcessCanvasMessage;
|
|
EsElementStartAnimating(canvas);
|
|
EsElementFocus(canvas);
|
|
InitialiseGame();
|
|
}
|
|
}
|
|
}
|