/*
bigmap.c
Copyright 2015-2023 Matthew T. Pandina. All rights reserved.
This is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
License for more details.
You should have received a copy of the GNU General Public License
along with this. If not, see .
*/
#include
#include
#include
#include
#include
#include
#include
#include "stackmon.h"
#include "data/tileset_stripped.inc"
#include "data/patches.inc"
#include "data/spriteset.inc"
#include "data/midisong.inc"
#include "data/blitmaps.c"
#define OPTION_USE_SPRITE_FLICKERING (1) /* 0-1 */
#define OPTION_SF_CIRCULAR_SWIZZLE (0) /* 0-1 */
#define OPTION_SF_DOUBLE_MIXER_SWIZZLE (1) /* 0-1 */
#define OPTION_SWIZZLE_RAM_TILES (1) /* 0-1 */
#define DEBUG_PRINT_FATAL_ERRORS (1) /* 0-1 */
#define DEBUG_OUTPUT_INCLUDE_FILES (0) /* 0-1 */
#define DEBUG_DISSOLVING_TILE_ANIMATIONS (0) /* 0-3 */
#define DEBUG_MIN_MAX_VALID_VRAM (0) /* 0-1 */
#define DEBUG_PRINT_KILLED_BY (0) /* 0-1 */
#define DEBUG_SPAWNS (0) /* 0-1 */
#define DEBUG_COLLECTABLES_AND_HAZARDS (0) /* 0-1 */
#define DEBUG_SHOW_SPRITE_OPTIMIZATIONS (0) /* 0-1 */
#define DEBUG_PRINT_MIN_STACK_COUNT (0) /* 0-1 , Note: if enabled here, you must also enable its OBJECTS line in the Makefile */
#define DEBUG_MIN_JUMP_HEIGHT (0) /* 0-1 */
// TODO: Add DEBUG_SPRITE_FLICKERING?
#define NELEMS(x) (sizeof(x) / sizeof(x[0]))
#define LO8(x) ((uint8_t)((x) & 0xFF))
#define HI8(x) ((uint8_t)(((x) >> 8) & 0xFF))
#define MAKEWORD(x, y) ((uint16_t)((((uint8_t)y) << 8) | (uint8_t)(x)))
#define BITARRAY_SIZE(b) ((b) / 8 + (!!((b) % 8)))
#define BUILD_BUG_IF(condition) ((void)sizeof(char[1 - 2 * !!(condition)]))
#define NOT_POWER_OF_TWO(x) (((x) & ((x) - 1)) != 0)
#define UZEMCHR _SFR_IO8(26) // uzem whisper port for outputting characters to the console
#define UZEMHEX _SFR_IO8(25) // uzem whisper port for outputting hex bytes values to the console
#define PrintMinStackCountIfChanged() do { \
uint16_t stack = StackCount(); \
static uint16_t min_stack = 0xFFFF; \
if (stack < min_stack) { \
UZEMCHR = 'S'; UZEMCHR = 'T'; UZEMCHR = 'A'; UZEMCHR = 'C'; \
UZEMCHR = 'K'; UZEMCHR = ':'; \
UZEMHEX = HI8(stack); UZEMHEX = LO8(stack); UZEMCHR = '\n'; \
min_stack = stack; \
} \
} while (0)
#define SPRITE_BLANK (0)
#define TILE_LOLLIPOP_TOP_START (0)
#define TILE_LOLLIPOP_TOP_END (5)
#define TILE_PURE_SKY (17)
// The bottom rungs of the ladder are pure sky tiles and not considered ladder tiles at all.
// The purpose of this is to make it so when the front hooves of the climbing idle sprite look
// like they are off the ladder, the sprite falls and avoids hanging off the ladder by just
// the horn or empty space.
#define TILE_END_SKY (94 + 28)
// This is an actual ladder tile that can be grabbed onto
#define TILE_START_LADDER_SKY (TILE_END_SKY - 1)
#define TILE_END_LADDER_SKY (TILE_END_SKY)
#define TILE_START_ONE_WAY (TILE_END_SKY + 1)
#define TILE_END_ONE_WAY (TILE_END_SKY + 12)
// Dissolving tiles are defined in their respective section
#define TILE_START_SOLID (TILE_END_ONE_WAY + 3)
#define TILE_START_LADDER_SOLID (TILE_START_SOLID)
#define TILE_END_LADDER_SOLID (TILE_START_SOLID + 3)
#define NUM_SOLID_TILES (7)
#define NUM_HAZARDS (5)
#define NUM_TILES_AFTER_HAZARD (32)
#define TILE_START_HAZARD (TILESET_SIZE - (NUM_HAZARDS + NUM_TILES_AFTER_HAZARD))
#define TILE_SPAWN_START (TILESET_SIZE - NUM_TILES_AFTER_HAZARD)
#if (TILE_START_LADDER_SOLID + NUM_SOLID_TILES != TILE_START_HAZARD)
#error "Unauthorized tiles found"
#endif
#define VT2P(t) ((t) * (TILE_HEIGHT << FP_SHIFT))
#define HT2P(t) ((t) * (TILE_WIDTH << FP_SHIFT))
#define P2VT(p) ((p) / (TILE_HEIGHT << FP_SHIFT))
#define P2HT(p) ((p) / (TILE_WIDTH << FP_SHIFT))
#define NV(p) ((p) % (TILE_HEIGHT << FP_SHIFT))
#define NH(p) ((p) % (TILE_WIDTH << FP_SHIFT))
#define NEAREST_SCREEN_PIXEL(p) (((p) + (1 << (FP_SHIFT - 1))) >> FP_SHIFT)
#define FP_SHIFT (2)
#define WORLD_FPS (32)
#define WORLD_METER (10 << FP_SHIFT)
#define WORLD_GRAVITY (615)
#define WORLD_MAXDX (WORLD_METER * 7)
#define WORLD_MAXDY (WORLD_METER * 16)
#define WORLD_ACCEL (355)
#define WORLD_FRICTION (437)
#define WORLD_JUMP (17600)
#define WORLD_FALLING_GRACE_FRAMES (6)
#define WORLD_CUT_JUMP_SPEED_LIMIT (WORLD_GRAVITY / 10)
#define MAX_TREASURES_PER_LEVEL (32)
#define MAX_SPAWNS_PER_LEVEL (32)
#define MAX_ACTIVE_SPAWNS (14)
#define PLAYER_TILE_HEIGHT (2)
#define PLAYER_TILE_WIDTH (1)
extern void BlitSpritePart(u8 ramtileno, u16 flidx, u16 xy, u16 dxdy);
__attribute__((optimize("O3")))
void MyBlitSprite(const u8 flags, const u8 sprindex, const u8 xpos, const u8 ypos)
{
#if (OPTION_SWIZZLE_RAM_TILES == 1)
#if (RAM_TILES_COUNT == 32)
#define SWIZZLE_RAM_TILES(t) (((t) * 7) % 32)
#else
//uint8_t RamTileLUT[] = { 0, 7, 14, 21, 28, 3, 10, 17, 24, 31, 6, 13, 20, 27, 2, 9, 16, 23, 30, 5, 12, 19, 26, 1, 8, 15, 22, 29, 4, 11, 18, 25 };
//#define SWIZZLE_RAM_TILES(t) (RamTileLUT[(t)])
#error "Please define another swizzling function/LUT above"
#endif
#else
#define SWIZZLE_RAM_TILES(t) (t)
#endif
extern uint8_t user_ram_tiles_c;
extern uint8_t free_tile_index;
extern struct BgRestoreStruct ram_tiles_restore[];
u8 bx;
u8 by;
u8 dx;
u8 dy;
u8 bt;
u8 x;
u8 y;
u8 tx;
u8 ty;
u8 wx;
u8 wy;
u16 ramPtr;
u8 ssx;
#if (SCROLLING != 0)
u16 ssy;
#else
u8 ssy;
#endif
/* if sprite is off, then don't draw it */
//if ((flags & SPRITE_OFF) != 0U){ return; }
/* get tile's screen section offsets */
#if (SCROLLING != 0)
ssx = xpos + Screen.scrollX;
ssy = ypos + Screen.scrollY;
if (ypos > (u8)((Screen.scrollHeight << 3) - 1U)){
ssy += 0xFF00U; /* Sprite should clip on top */
}
#else
ssx = xpos;
ssy = ypos;
#endif
tx = 1U;
ty = 1U;
/* get the BG tiles that are overlapped by the sprite,
** supporting wrapping (so sprites located just below zero X
** or Y would clip on the left). In a scrolling config. only
** TILE_WIDTH = 8 is really supported due to the "weird" VRAM
** layout, VRAM_TILES_H is also fixed 32 this case. */
#if ((SCROLLING == 0) && (SCREEN_TILES_H < 32))
bx = ((u8)((ssx + TILE_WIDTH) & 0xFFU) / TILE_WIDTH) - 1U;
#else
bx = ssx / TILE_WIDTH;
#endif
dx = ssx % TILE_WIDTH;
if (dx != 0U){ tx++; }
#if (SCROLLING == 0)
by = ((u8)((ssy + TILE_HEIGHT) & 0xFFU) / TILE_HEIGHT) - 1U;
#else
by = ssy / TILE_HEIGHT;
#endif
dy = ssy % TILE_HEIGHT;
if (dy != 0U){ ty++; }
/* Output sprite tiles */
for (y = 0U; y < ty; y++){
wy = by + y;
#if (SCROLLING == 0)
if (wy < VRAM_TILES_V){
#else
if ( (Screen.scrollHeight != 0U) &&
((u8)((ypos + 7U + (y << 3) - dy) & 0xFFU) < (u8)((Screen.scrollHeight << 3) - 1U)) ){
while (wy >= Screen.scrollHeight){
wy -= Screen.scrollHeight;
}
#endif
for (x = 0U; x < tx; x++){
wx = bx + x;
#if (SCROLLING == 0)
if (wx < VRAM_TILES_H){
#else
wx = wx % VRAM_TILES_H;
#if (SCREEN_TILES_H < 32U)
if ((u8)((xpos + 7U + (x << 3) - dx) & 0xFFU) < (((SCREEN_TILES_H + 1U) << 3) - 1U)){
#else
{
#endif
#endif
#if (SCROLLING == 0)
ramPtr = (wy * VRAM_TILES_H) +
wx;
#else
ramPtr = ((u16)(wy >> 3) * 256U) +
(u8)(wx * 8U) + (u8)(wy & 0x07U);
#endif
bt = vram[ramPtr];
if ( ( (bt >= RAM_TILES_COUNT) |
(bt < user_ram_tiles_c)) &&
(free_tile_index < RAM_TILES_COUNT) ){ /* if no ram free ignore tile */
uint8_t swizzled_free_tile_index = SWIZZLE_RAM_TILES(free_tile_index);
if (bt >= RAM_TILES_COUNT){
/* tile is mapped to flash. Copy it to next free RAM tile. */
CopyFlashTile(bt - RAM_TILES_COUNT, swizzled_free_tile_index);
}else if (bt < user_ram_tiles_c){
/* tile is a user ram tile. Copy it to next free RAM tile. */
CopyRamTile(bt, swizzled_free_tile_index);
}
#if (RTLIST_ENABLE != 0)
ram_tiles_restore[free_tile_index].addr = (&vram[ramPtr]);
ram_tiles_restore[free_tile_index].tileIndex = bt;
#endif
bt = vram[ramPtr] = swizzled_free_tile_index;
free_tile_index++;
}
if (((flags & SPRITE_OFF) == 0U) && (bt < RAM_TILES_COUNT) &&
(bt >= user_ram_tiles_c) ){
BlitSpritePart(bt,
((u16)(flags) << 8) + sprindex,
((u16)(y) << 8) + x,
((u16)(dy) << 8) + dx);
}
}
} /* end for X */
}
} /* end for Y */
}
void BlitMyBlitMap(uint8_t spriteFlags, const int8_t *blitmap, uint8_t replaceBlit, uint8_t withBlit, uint8_t xmap, uint8_t ymap)
{
uint8_t xflip_len = (uint8_t)pgm_read_byte(&blitmap[0]);
int8_t len = xflip_len & 0x7F;
if (xflip_len & 0x80) { // If the high bit of len is set (baked into the blitmap), flip the entire sprite on X
if (spriteFlags & SPRITE_FLIP_X)
spriteFlags &= ~SPRITE_FLIP_X;
else
spriteFlags |= SPRITE_FLIP_X;
}
int8_t xdir;
int8_t xoff;
if (spriteFlags & SPRITE_FLIP_X) {
xdir = -1;
xoff = TILE_WIDTH * (PLAYER_TILE_WIDTH - 1);
} else {
xdir = 1;
xoff = 0;
}
int8_t ydir;
int8_t yoff;
if (spriteFlags & SPRITE_FLIP_Y) {
ydir = -1;
yoff = TILE_HEIGHT * (PLAYER_TILE_HEIGHT - 1);
} else {
ydir = 1;
yoff = 0;
}
for (int8_t i = 0, pos = 1; i < len; ++i, pos += 3) {
uint8_t tile = (uint8_t)pgm_read_byte(&blitmap[pos]);
if (tile == replaceBlit)
tile = withBlit;
MyBlitSprite(spriteFlags,
tile,
xmap + xoff + xdir * (int8_t)pgm_read_byte(&blitmap[pos + 1]),
ymap + yoff + ydir * (int8_t)pgm_read_byte(&blitmap[pos + 2]));
}
}
#if 0
void MyBlitSpriteMap(uint8_t spriteFlags, const char *map, uint8_t map_x, uint8_t map_y)
{
uint8_t mapWidth = pgm_read_byte(&map[0]);
uint8_t mapHeight = pgm_read_byte(&map[1]);
int8_t x, y, dx, dy, t;
if (spriteFlags & SPRITE_FLIP_X) {
x = mapWidth - 1;
dx = -1;
} else {
x = 0;
dx = 1;
}
if (spriteFlags & SPRITE_FLIP_Y) {
y = mapHeight - 1;
dy = -1;
} else {
y = 0;
dy = 1;
}
for (uint8_t cy = 0; cy < mapHeight; cy++) {
for( uint8_t cx = 0; cx < mapWidth; cx++) {
t = pgm_read_byte(&map[y * mapWidth + x + 2]);
if (t != SPRITE_BLANK)
MyBlitSprite(spriteFlags,
t,
map_x + (TILE_WIDTH * cx),
map_y + (TILE_HEIGHT * cy));
x += dx;
}
y += dy;
x = (spriteFlags & SPRITE_FLIP_X) ? mapWidth - 1 : 0;
}
}
void MyBlitSpriteMapWithCache(uint8_t spriteFlags, const char *map, uint8_t map_x, uint8_t map_y)
{
static uint8_t spriteFlagsCached;
static const char *mapCached;
static uint8_t map_xCached;
static uint8_t map_yCached;
if (map != 0) {
spriteFlagsCached = spriteFlags;
mapCached = map;
map_xCached = map_x;
map_yCached = map_y;
MyBlitSpriteMap(spriteFlagsCached | SPRITE_OFF,
mapCached,
map_xCached,
map_yCached);
} else
MyBlitSpriteMap(spriteFlagsCached,
mapCached,
map_xCached,
map_yCached);
}
#endif
////////////////////////////////////////////////////////////////////////////////
// Begin Immutable Sprite Data
////////////////////////////////////////////////////////////////////////////////
enum INITIAL_FLAGS;
typedef enum INITIAL_FLAGS INITIAL_FLAGS;
enum INITIAL_FLAGS
{
IFLAG_LEFT = 1,
IFLAG_RIGHT = 2,
IFLAG_UP = 4,
IFLAG_DOWN = 8,
IFLAG_NOINTERACT = 16,
IFLAG_INVINCIBLE = 32,
IFLAG_NOT_VISIBLE = 64,
IFLAG_NEVER_DESPAWN = 128,
//IFLAG_AUTORESPAWN = 64,
//IFLAG_SPRITE_FLIP_X = 128,
};
enum SPAWN_TYPE;
typedef enum SPAWN_TYPE SPAWN_TYPE;
enum SPAWN_TYPE
{
ST_BUTTERFLY,
ST_FALLING_SPIKE,
ST_CHARGING_TURTLE,
ST_FIREBALL,
// MOVING PLATFORMS MUST BE LAST FOR SPEED/CORRECTNESS
ST_HORIZONTAL_PLATFORM,
ST_VERTICAL_PLATFORM,
};
// TODO: For multiple levels, have a level offset table stored in PROGMEM
// and anytime you are reading from an ISD_ variable, add the level
// offset for the current level to the index. That way each varible
// can still be stored in its own array, and everything stays self
// documenting and easy to read.
const uint8_t ISD_spawn_type[] PROGMEM =
{
ST_BUTTERFLY,
ST_BUTTERFLY,
ST_BUTTERFLY,
ST_BUTTERFLY,
ST_BUTTERFLY,
ST_FALLING_SPIKE,
ST_CHARGING_TURTLE,
ST_CHARGING_TURTLE,
ST_FALLING_SPIKE,
ST_FIREBALL,
ST_FIREBALL,
ST_FIREBALL,
ST_FIREBALL,
ST_FIREBALL,
ST_FIREBALL,
ST_HORIZONTAL_PLATFORM,
ST_HORIZONTAL_PLATFORM,
ST_VERTICAL_PLATFORM,
};
const uint8_t ISD_backing_tile[] PROGMEM =
{
TILE_PURE_SKY,
TILE_PURE_SKY,
TILE_PURE_SKY,
TILE_PURE_SKY,
TILE_PURE_SKY,
TILE_START_HAZARD + 4,
TILE_PURE_SKY + 2,
TILE_PURE_SKY + 2,
TILE_START_HAZARD + 4,
TILE_START_SOLID + 4,
TILE_START_SOLID + 6,
TILE_START_SOLID + 4,
TILE_START_SOLID + 5,
TILE_START_SOLID + 4,
TILE_START_SOLID + 6,
TILE_PURE_SKY,
TILE_PURE_SKY,
TILE_PURE_SKY,
};
#if 1
#define ISD_X_FIREBALL_OFFSET 10
#define ISD_Y_FIREBALL_OFFSET 14
#else
#define ISD_X_FIREBALL_OFFSET 0
#define ISD_Y_FIREBALL_OFFSET 0
#endif
const int16_t ISD_x[] PROGMEM =
{
HT2P(0x11),
HT2P(0x2f),
HT2P(0x0e),
HT2P(0x19),
HT2P(0x36),
HT2P(0x1b),
HT2P(0x1d),
HT2P(0x0b),
HT2P(0x24),
HT2P(0x2c + ISD_X_FIREBALL_OFFSET) + (4 << FP_SHIFT),
HT2P(0x2c + ISD_X_FIREBALL_OFFSET) + (4 << FP_SHIFT),
HT2P(0x2c + ISD_X_FIREBALL_OFFSET) + (4 << FP_SHIFT),
HT2P(0x2c + ISD_X_FIREBALL_OFFSET) + (4 << FP_SHIFT),
HT2P(0x2c + ISD_X_FIREBALL_OFFSET) + (4 << FP_SHIFT),
HT2P(0x2c + ISD_X_FIREBALL_OFFSET) + (4 << FP_SHIFT),
HT2P(0x0a),
HT2P(0x33),
HT2P(0x16),
};
const int16_t ISD_y[] PROGMEM =
{
VT2P(0x07),
VT2P(0x1a),
VT2P(0x2e),
VT2P(0x2e),
VT2P(0x30),
VT2P(0x14),
VT2P(0x11),
VT2P(0x17),
VT2P(0x14),
VT2P(0x24 + ISD_Y_FIREBALL_OFFSET) + (4 << FP_SHIFT),
VT2P(0x24 + ISD_Y_FIREBALL_OFFSET) + (4 << FP_SHIFT),
VT2P(0x24 + ISD_Y_FIREBALL_OFFSET) + (4 << FP_SHIFT),
VT2P(0x24 + ISD_Y_FIREBALL_OFFSET) + (4 << FP_SHIFT),
VT2P(0x24 + ISD_Y_FIREBALL_OFFSET) + (4 << FP_SHIFT),
VT2P(0x24 + ISD_Y_FIREBALL_OFFSET) + (4 << FP_SHIFT),
VT2P(0x11),
VT2P(0x08),
VT2P(0x29),
};
const int16_t ISD_maxdx[] PROGMEM =
{
WORLD_METER * 3,
WORLD_METER * 3,
WORLD_METER * 3,
WORLD_METER * 3,
WORLD_METER * 3,
WORLD_MAXDY,
WORLD_METER * 2,
WORLD_METER * 2,
WORLD_MAXDY,
0,
0,
0,
0,
0,
0,
WORLD_METER * 2,
WORLD_METER * 3,
WORLD_METER * 2,
};
const int8_t ISD_lo_bound[] PROGMEM =
{
-3,
-6,
-8,
-3,
-4,
2,
-13,
-1,
2,
1,
1,
1,
1,
1,
1,
-4,
-48,
0,
};
#if 1
#define ISD_HI_BOUND_FIREBALL_RADIUS 22
#else
#define ISD_HI_BOUND_FIREBALL_RADIUS 48
#endif
const int8_t ISD_hi_bound[] PROGMEM =
{
12,
6,
-1,
4,
2,
6,
20,
6,
8,
0,
ISD_HI_BOUND_FIREBALL_RADIUS,
ISD_HI_BOUND_FIREBALL_RADIUS + ISD_HI_BOUND_FIREBALL_RADIUS,
ISD_HI_BOUND_FIREBALL_RADIUS + ISD_HI_BOUND_FIREBALL_RADIUS + ISD_HI_BOUND_FIREBALL_RADIUS,
ISD_HI_BOUND_FIREBALL_RADIUS + ISD_HI_BOUND_FIREBALL_RADIUS + ISD_HI_BOUND_FIREBALL_RADIUS + ISD_HI_BOUND_FIREBALL_RADIUS,
ISD_HI_BOUND_FIREBALL_RADIUS + ISD_HI_BOUND_FIREBALL_RADIUS + ISD_HI_BOUND_FIREBALL_RADIUS + ISD_HI_BOUND_FIREBALL_RADIUS + ISD_HI_BOUND_FIREBALL_RADIUS,
2,
1,
6,
};
const uint8_t ISD_initial_flags[] PROGMEM =
{
IFLAG_RIGHT | IFLAG_DOWN,
IFLAG_RIGHT | IFLAG_DOWN,
IFLAG_RIGHT | IFLAG_DOWN,
IFLAG_RIGHT | IFLAG_DOWN,
IFLAG_RIGHT | IFLAG_DOWN,
IFLAG_LEFT | IFLAG_INVINCIBLE | IFLAG_NOT_VISIBLE,
IFLAG_RIGHT,
IFLAG_RIGHT,
IFLAG_LEFT | IFLAG_INVINCIBLE | IFLAG_NOT_VISIBLE,
IFLAG_RIGHT | IFLAG_INVINCIBLE,
IFLAG_RIGHT | IFLAG_INVINCIBLE,
IFLAG_RIGHT | IFLAG_INVINCIBLE,
IFLAG_RIGHT | IFLAG_INVINCIBLE,
IFLAG_RIGHT | IFLAG_INVINCIBLE,
IFLAG_RIGHT | IFLAG_INVINCIBLE,
IFLAG_LEFT | IFLAG_NEVER_DESPAWN | IFLAG_INVINCIBLE | IFLAG_NOT_VISIBLE | IFLAG_NOINTERACT,
IFLAG_RIGHT | IFLAG_NEVER_DESPAWN | IFLAG_INVINCIBLE | IFLAG_NOT_VISIBLE | IFLAG_NOINTERACT,
IFLAG_DOWN /*| IFLAG_NEVER_DESPAWN*/ | IFLAG_INVINCIBLE | IFLAG_NOT_VISIBLE | IFLAG_NOINTERACT,
};
////////////////////////////////////////////////////////////////////////////////
// End Immutable Sprite Data
////////////////////////////////////////////////////////////////////////////////
struct LEVEL;
typedef struct LEVEL LEVEL;
struct CAMERA;
typedef struct CAMERA CAMERA;
struct DISSOLVING_TILE;
typedef struct DISSOLVING_TILE DISSOLVING_TILE;
struct BUTTON_INFO;
typedef struct BUTTON_INFO BUTTON_INFO;
struct PLAYER;
typedef struct PLAYER PLAYER;
struct ENTITY;
typedef struct ENTITY ENTITY;
struct ENTITY {
uint8_t id; // offset into the immutable entity data arrays, and entity_is_spawned bit array
int16_t x;
int16_t y;
int16_t dx;
int16_t dy;
uint8_t counter;
uint8_t animationFrameCounter;
uint8_t dead:1;
uint8_t left:1;
uint8_t right:1;
uint8_t up:1;
uint8_t down:1;
uint8_t interacts:1;
uint8_t visible:1; // if false, the entity's render function will not be called, and will not be blit
} __attribute__ ((packed));
struct LEVEL {
uint16_t width;
uint16_t height;
const char *data;
uint8_t collectable_exists[BITARRAY_SIZE(MAX_TREASURES_PER_LEVEL)];
uint8_t entity_is_spawned[BITARRAY_SIZE(MAX_SPAWNS_PER_LEVEL)];
uint8_t entity_is_dead[BITARRAY_SIZE(MAX_SPAWNS_PER_LEVEL)];
ENTITY entity[MAX_ACTIVE_SPAWNS];
uint8_t numSpawned;
CAMERA* camera;
PLAYER* player;
} __attribute__ ((packed));
struct BUTTON_INFO {
uint16_t held;
uint16_t prev;
uint16_t pressed;
uint16_t released;
} __attribute__ ((packed));
struct PLAYER {
void (*update)(PLAYER* const, CAMERA* const, DISSOLVING_TILE* const, LEVEL* const l);
LEVEL* level;
const int8_t* map; // which blitmap we rendered last (for proper z-order)
int16_t x;
int16_t y;
int16_t dx;
int16_t dy;
int16_t impulse;
uint8_t animationFrameCounter;
uint8_t framesFalling;
uint8_t flags; // sprite flags
uint8_t clopvol;
uint8_t frames_since_clop;
uint8_t attackCounter;
uint8_t ddrc_num;
uint8_t gulpedAtExpires;
uint16_t falling:1;
uint16_t jumping:1;
uint16_t left:1;
uint16_t right:1;
uint16_t up:1;
uint16_t down:1;
uint16_t jump:1;
uint16_t jumpReleased:1;
uint16_t clopsound:1;
uint16_t attackReleased:1;
uint16_t visible:1;
uint16_t dead:1;
uint16_t attacking:1;
uint16_t attackingLeft:1;
uint16_t invincible:1;
uint16_t gulped:1;
uint16_t onMovingPlatform:1;
uint8_t platformId;
uint8_t jumpCounter; // Enforce minimal jump height, because quick minimal jumps sometimes cause a
#if (DEBUG_MIN_JUMP_HEIGHT == 1) // fall-through of a vertical moving platform if the platform is near the
uint8_t debugJumpCounter; // bottom and it just changed directions while the player did a tiny jump.
#endif
BUTTON_INFO buttons;
} __attribute__ ((packed));
////////////////////////////////////////////////////////////////////////////////
// Begin Fast BitArray Operations
////////////////////////////////////////////////////////////////////////////////
const uint8_t oneLeftShiftedByIndexModEight[] PROGMEM =
{
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
};
const uint8_t oneLeftShiftedByIndexModEightXorFF[] PROGMEM =
{
0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f,
0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f,
0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f,
0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f,
0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f,
0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f,
0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f,
0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f,
0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f,
0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f,
0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f,
0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f,
0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f,
0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f,
0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f,
0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f,
};
__attribute__(( always_inline ))
static inline void BitArray_setBit(uint8_t* const array, uint8_t index)
{
uint8_t offset = index / 8;
uint8_t mask = pgm_read_byte(&oneLeftShiftedByIndexModEight[index]);
array[offset] |= mask;
}
__attribute__(( always_inline ))
static inline void BitArray_clearBit(uint8_t* const array, uint8_t index)
{
uint8_t offset = index / 8;
uint8_t mask = pgm_read_byte(&oneLeftShiftedByIndexModEightXorFF[index]);
array[offset] &= mask;
}
__attribute__(( always_inline ))
static inline bool BitArray_readBit(const uint8_t* const array, uint8_t index)
{
uint8_t offset = index / 8;
uint8_t mask = pgm_read_byte(&oneLeftShiftedByIndexModEight[index]);
return (bool)(array[offset] & mask);
}
////////////////////////////////////////////////////////////////////////////////
// End Fast BitArray Operations
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// Begin Collectable Operations
////////////////////////////////////////////////////////////////////////////////
// To generate these include files, set DEBUG_OUTPUT_INCLUDE_FILES to 1 above,
// run the emulator with whisper console support, and run the commands it prints
const uint16_t sorted_treasure_coords[] PROGMEM =
{
#include "data/treasure.inc"
};
const uint8_t sorted_treasure_tiles[] PROGMEM =
{
#include "data/treasur2.inc"
};
static int8_t BinarySearch(const uint16_t* array, const int8_t array_len, const uint16_t index)
{
int8_t low = 0;
int8_t high = array_len - 1;
while (low <= high) {
int8_t mid = ((uint8_t)low + (uint8_t)high) >> 1;
uint16_t i = pgm_read_word(&array[mid]);
if (i < index)
low = mid + 1;
else if (i > index)
high = mid - 1;
else
return mid;
}
return -1;
}
//typedef enum { COLLECTABLE_ACTION_QUERY = 0, COLLECTABLE_ACTION_CLEAR = 1 } COLLECTABLE_ACTION;
typedef bool COLLECTABLE_ACTION;
#define COLLECTABLE_ACTION_QUERY false
#define COLLECTABLE_ACTION_CLEAR true
bool Collectable_performAction(LEVEL* const l, COLLECTABLE_ACTION action, uint16_t index)
{
// TODO: In the future instead of passing NELEMS, let it be dynamic per level
int8_t offset = BinarySearch(sorted_treasure_coords, NELEMS(sorted_treasure_coords), index);
if (offset >= 0) {
if (action == COLLECTABLE_ACTION_CLEAR) {
BitArray_clearBit(l->collectable_exists, offset);
return false;
} else if (action == COLLECTABLE_ACTION_QUERY)
return BitArray_readBit(l->collectable_exists, offset);
}
#if (DEBUG_PRINT_FATAL_ERRORS == 1)
UZEMCHR = 'C'; UZEMCHR = 'E'; UZEMCHR = 'R'; UZEMCHR = 'R'; UZEMCHR = '\n';
#endif
return false; // Not found (this should never happen)
}
////////////////////////////////////////////////////////////////////////////////
// End Collectable Operations
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// Begin Level Operations
////////////////////////////////////////////////////////////////////////////////
void Entity_spawn(LEVEL* const l, uint16_t xy_index, uint8_t id);
void Level_initFromMap(LEVEL *l, const char *map, CAMERA* c, PLAYER* p)
{
l->width = pgm_read_byte(&map[0]);
l->height = pgm_read_byte(&map[1]);
l->data = map + 2;
memset(l->collectable_exists, 0xFF, sizeof(l->collectable_exists));
memset(l->entity_is_spawned, 0x00, sizeof(l->entity_is_spawned));
memset(l->entity_is_dead, 0x00, sizeof(l->entity_is_dead));
l->numSpawned = 0;
l->camera = c;
l->player = p;
#if (DEBUG_OUTPUT_INCLUDE_FILES == 1)
// Count how many lollipops are found, and error out if the bitarrays are not large enough to hold them
uint16_t count = 0;
for (uint8_t x = 0; x < l->width; ++x) {
for (uint8_t y = 0; y < l->height; ++y) {
uint16_t index = (uint16_t)y * l->width + x;
uint8_t t = pgm_read_byte(&l->data[index]);
if (/*t >= TILE_LOLLIPOP_TOP_START && */t <= TILE_LOLLIPOP_TOP_END)
count++;
}
}
if (count > MAX_TREASURES_PER_LEVEL) {
UZEMCHR = '\n'; UZEMCHR = '\n';
UZEMCHR = 'I'; UZEMCHR = 'n'; UZEMCHR = 'c'; UZEMCHR = 'r'; UZEMCHR = 'e'; UZEMCHR = 'a'; UZEMCHR = 's';
UZEMCHR = 'e'; UZEMCHR = ' '; UZEMCHR = 'M'; UZEMCHR = 'A'; UZEMCHR = 'X'; UZEMCHR = '_'; UZEMCHR = 'T';
UZEMCHR = 'R'; UZEMCHR = 'E'; UZEMCHR = 'A'; UZEMCHR = 'S'; UZEMCHR = 'U'; UZEMCHR = 'R'; UZEMCHR = 'E';
UZEMCHR = 'S'; UZEMCHR = '_'; UZEMCHR = 'P'; UZEMCHR = 'E'; UZEMCHR = 'R'; UZEMCHR = '_'; UZEMCHR = 'L';
UZEMCHR = 'E'; UZEMCHR = 'V'; UZEMCHR = 'E'; UZEMCHR = 'L'; UZEMCHR = '!'; UZEMCHR = '\n'; UZEMCHR = '\n';
return;
}
UZEMCHR = 'c'; UZEMCHR = 'a'; UZEMCHR = 't'; UZEMCHR = ' '; UZEMCHR = '<'; UZEMCHR = '<'; UZEMCHR = '\'';
UZEMCHR = 'E'; UZEMCHR = 'O'; UZEMCHR = 'F'; UZEMCHR = '\''; UZEMCHR = ' '; UZEMCHR = '|'; UZEMCHR = ' ';
UZEMCHR = 's'; UZEMCHR = 'o'; UZEMCHR = 'r'; UZEMCHR = 't'; UZEMCHR = ' '; UZEMCHR = '>'; UZEMCHR = ' ';
UZEMCHR = ' '; UZEMCHR = '.'; UZEMCHR = '/'; UZEMCHR = 'd'; UZEMCHR = 'a'; UZEMCHR = 't'; UZEMCHR= 'a';
UZEMCHR = '/'; UZEMCHR = 't'; UZEMCHR = 'r'; UZEMCHR = 'e'; UZEMCHR = 'a'; UZEMCHR = 's'; UZEMCHR = 'u';
UZEMCHR = 'r'; UZEMCHR = 'e'; UZEMCHR = '.'; UZEMCHR = 'i'; UZEMCHR = 'n'; UZEMCHR = 'c'; UZEMCHR = '\n';
for (uint8_t x = 0; x < l->width; ++x) {
for (uint8_t y = 0; y < l->height; ++y) {
uint16_t index = (uint16_t)y * l->width + x;
uint16_t xy_index = MAKEWORD(x, y);
uint8_t t = pgm_read_byte(&l->data[index]);
if (/*t >= TILE_LOLLIPOP_TOP_START && */t <= TILE_LOLLIPOP_TOP_END) {
UZEMCHR = '0'; UZEMCHR = 'x'; UZEMHEX = HI8(xy_index); UZEMHEX = LO8(xy_index); UZEMCHR = ','; UZEMCHR = '\n';
}
}
}
UZEMCHR = 'E'; UZEMCHR = 'O'; UZEMCHR = 'F'; UZEMCHR = '\n';
//| cut -d' ' -f2
UZEMCHR = 'c'; UZEMCHR = 'a'; UZEMCHR = 't'; UZEMCHR = ' '; UZEMCHR = '<'; UZEMCHR = '<'; UZEMCHR = '\'';
UZEMCHR = 'E'; UZEMCHR = 'O'; UZEMCHR = 'F'; UZEMCHR = '\''; UZEMCHR = ' '; UZEMCHR = '|'; UZEMCHR = ' ';
UZEMCHR = 's'; UZEMCHR = 'o'; UZEMCHR = 'r'; UZEMCHR = 't'; UZEMCHR = ' '; UZEMCHR = '|'; UZEMCHR = ' ';
UZEMCHR = 'c'; UZEMCHR = 'u'; UZEMCHR = 't'; UZEMCHR = ' '; UZEMCHR = '-'; UZEMCHR = 'd'; UZEMCHR = '\'';
UZEMCHR = ' '; UZEMCHR = '\''; UZEMCHR = ' '; UZEMCHR = '-'; UZEMCHR = 'f'; UZEMCHR = '2'; UZEMCHR = ' ';
UZEMCHR = '>'; UZEMCHR = ' '; UZEMCHR = '.'; UZEMCHR = '/'; UZEMCHR = 'd'; UZEMCHR = 'a'; UZEMCHR = 't'; UZEMCHR= 'a';
UZEMCHR = '/'; UZEMCHR = 't'; UZEMCHR = 'r'; UZEMCHR = 'e'; UZEMCHR = 'a'; UZEMCHR = 's'; UZEMCHR = 'u';
UZEMCHR = 'r'; UZEMCHR = '2'; UZEMCHR = '.'; UZEMCHR = 'i'; UZEMCHR = 'n'; UZEMCHR = 'c'; UZEMCHR = '\n';
for (uint8_t x = 0; x < l->width; ++x) {
for (uint8_t y = 0; y < l->height; ++y) {
uint16_t index = (uint16_t)y * l->width + x;
uint16_t xy_index = MAKEWORD(x, y);
uint8_t t = pgm_read_byte(&l->data[index]);
if (/*t >= TILE_LOLLIPOP_TOP_START && */t <= TILE_LOLLIPOP_TOP_END) {
UZEMCHR = '0'; UZEMCHR = 'x'; UZEMHEX = HI8(xy_index); UZEMHEX = LO8(xy_index); UZEMCHR = ','; UZEMCHR = ' ';
UZEMCHR = '0'; UZEMCHR = 'x'; UZEMHEX = t; UZEMCHR = ','; UZEMCHR = '\n';
}
}
}
UZEMCHR = 'E'; UZEMCHR = 'O'; UZEMCHR = 'F'; UZEMCHR = '\n';
#endif
}
// A lollipop takes up four 8x8 tiles, but only the xy_index of the
// top left corner (the lollipop head) is used to track its state. We
// still need to draw (or not draw) all four tiles depending on that
// state, so this array encodes a tile number into an offset to
// subtract from the xy_index of any of the four parts of the lollipop
// to get the correct xy_index of the lollipop head, which can be used
// for the lookup.
const uint16_t lollipop_tile_to_index_offset[] PROGMEM =
{
0, // tx , ty
0, // tx , ty
0, // tx , ty
0, // tx , ty
0, // tx , ty
0, // tx , ty
256 + 1, // tx + 1, ty + 1
256 + 1, // tx + 1, ty + 1
256 + 1, // tx + 1, ty + 1
256 + 1, // tx + 1, ty + 1
256 + 1, // tx + 1, ty + 1
256, // tx , ty + 1
256, // tx , ty + 1
256, // tx , ty + 1
256, // tx , ty + 1
256, // tx , ty + 1
1, // tx + 1, ty
};
// When a lollipop is collected, each of the four 8x8 tiles it is
// composed of needs to be converted into the corresponding tiles
// without the lollipop. This array encodes a tile number with part of
// a lollipop into the corresponding tile number without part of a
// lollipop.
const uint8_t lollipop_tile_to_sky_tile[] PROGMEM =
{
TILE_PURE_SKY, // red
TILE_PURE_SKY, // orange
TILE_PURE_SKY, // yellow
TILE_PURE_SKY, // green
TILE_PURE_SKY, // blue
TILE_PURE_SKY, // violet
TILE_PURE_SKY, // main stick
TILE_PURE_SKY + 2, // main stick middle grass
TILE_PURE_SKY + 4, // main stick right grass
TILE_PURE_SKY + 8, // main stick middle cloud
TILE_PURE_SKY + 9, // main stick right cloud
TILE_PURE_SKY + 1, // top right pixel stick left grass
TILE_PURE_SKY + 3, // top right pixel stick middle grass
TILE_PURE_SKY + 7, // top right pixel stick left cloud
TILE_PURE_SKY + 8, // top right pixel stick middle cloud
TILE_PURE_SKY, // top right pixel stick
TILE_PURE_SKY, // bottom left pixel stick
};
/* The idea here is we read the tile from the level, and if it is a collectable tile
we check to see if that collectable has already been collected, using its index
into the sorted list of collectables for a given level (using binary search so the
lookup is fast), and either draw the tile we read, or a replacement tile. The trick
here is that the collectable isn't just a single tile, so we use a lookup table to
first compute the offset to give us the correct index to the top left corner tile of
the collectable map).
*/
uint8_t Level_getTileAt(LEVEL *l, uint8_t x, uint8_t y)
{
uint16_t index = y * l->width + x;
uint8_t tile = pgm_read_byte(&l->data[index]);
if (tile < NELEMS(lollipop_tile_to_index_offset)) {
uint16_t xy_index = MAKEWORD(x, y);
return Collectable_performAction(l, COLLECTABLE_ACTION_QUERY, xy_index - pgm_read_word(&lollipop_tile_to_index_offset[tile])) ?
tile : pgm_read_byte(&lollipop_tile_to_sky_tile[tile]);
} else if (tile >= TILE_SPAWN_START) {
uint16_t xy_index = MAKEWORD(x, y);
Entity_spawn(l, xy_index, tile - TILE_SPAWN_START);
return pgm_read_byte(&ISD_backing_tile[tile - TILE_SPAWN_START]);
} else
return tile;
}
void Level_drawColumn(LEVEL *l, uint8_t x, uint8_t y, int16_t realX)
{
if (realX < 0 || (uint16_t)realX > l->width - 1)
return;
uint8_t tx = x % VRAM_TILES_H;
for (uint8_t i = 0; i < VRAM_TILES_V - 2; ++i) {
if ((y + i) > l->height - 1)
break;
uint8_t tile = Level_getTileAt(l, realX, (y + i));
SetTile(tx, (y + i) % VRAM_TILES_V, tile);
}
if (y > 1) {
uint8_t tile = Level_getTileAt(l, realX, (y - 2));
SetTile(tx, (y + 30) % VRAM_TILES_V, tile);
}
if (y > 0) {
uint8_t tile = Level_getTileAt(l, realX, (y - 1));
SetTile(tx, (y + 31) % VRAM_TILES_V, tile);
}
}
void Level_drawRow(LEVEL *l, uint8_t x, uint8_t y, int16_t realY)
{
if (realY < 0 || (uint16_t)realY > l->height - 1)
return;
uint8_t ty = y % VRAM_TILES_V;
for (uint8_t i = 0; i < VRAM_TILES_H - 2; ++i) {
if ((x + i) > l->width - 1)
break;
uint8_t tile = Level_getTileAt(l, (x + i), realY);
SetTile((x + i) % VRAM_TILES_H, ty, tile);
}
if (x > 1) {
uint8_t tile = Level_getTileAt(l, (x - 2), realY);
SetTile((x + 30) % VRAM_TILES_H, ty, tile);
}
if (x > 0) {
uint8_t tile = Level_getTileAt(l, (x - 1), realY);
SetTile((x + 31) % VRAM_TILES_H, ty, tile);
}
}
////////////////////////////////////////////////////////////////////////////////
// End Level Operations
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// Begin Camera Operations
////////////////////////////////////////////////////////////////////////////////
struct CAMERA {
int16_t x;
int16_t y;
LEVEL *level;
// The camera object now automatically keeps track of the world tile
// coordinates of the level data that is currently loaded into VRAM
uint8_t x_min;
uint8_t x_max;
uint8_t y_min;
uint8_t y_max;
bool is_new_col_frame; // if we loaded a new column of level data this frame
bool is_new_row_frame; // if we loaded a new row of level data this frame
} __attribute__ ((packed));
void Camera_init(CAMERA* const c, LEVEL* const l)
{
c->level = l;
c->x = c->y = 0;
Screen.scrollX = (uint8_t)c->x;
Screen.scrollY = (uint8_t)c->y;
c->x_min = 0;
c->x_max = 0;
c->y_min = 0;
c->y_max = 0;
}
void Camera_moveToX(CAMERA* const c, int16_t x)
{
if (x < 0) {
c->x = 0;
} else {
int16_t xMax = (c->level->width - SCREEN_TILES_H) * TILE_WIDTH;
if (x > xMax)
c->x = xMax;
else
c->x = x;
}
}
void Camera_moveToY(CAMERA* const c, int16_t y)
{
if (y < 0) {
c->y = 0;
} else {
int16_t yMax = (c->level->height - SCREEN_TILES_V) * TILE_HEIGHT;
if (y > yMax)
c->y = yMax;
else
c->y = y;
}
}
void Camera_fillVram(CAMERA* const c)
{
int16_t cxt = c->x / TILE_WIDTH;
int16_t cyt = c->y / TILE_HEIGHT;
// Initialize VRAM extents for X
if (cxt - 2 >= 0)
c->x_min = cxt - 2;
else if (cxt - 1 >= 0)
c->x_min = cxt - 1;
else
c->x_min = 0;
if (cxt + 29 <= (int16_t)c->level->width - 1)
c->x_max = cxt + 29;
else if (cxt + 28 <= (int16_t)c->level->width - 1)
c->x_max = cxt + 28;
else
c->x_max = cxt + 27;
// Initialize VRAM extents for Y
if (cyt - 2 >= 0)
c->y_min = cyt - 2;
else if (cyt - 1 >= 0)
c->y_min = cyt - 1;
else
c->y_min = 0;
if (cyt + 29 <= (int16_t)c->level->height - 1)
c->y_max = cyt + 29;
else if (cyt + 28 <= (int16_t)c->level->height - 1)
c->y_max = cyt + 28;
else
c->y_max = cyt + 27;
// We never call ClearVram() because it takes too long and causes screen tearing
// glitches, so we need to ensure the non-drawn parts of VRAM are initialized to
// something other than RAM tiles, because if you jump above or below the screen
// and overlap with a RAM tile that is being used by another sprite, it will show
if ((c->x_max - c->x_min) != 0x1f) {
uint8_t tx = ((c->x / TILE_WIDTH) + !!(c->x % TILE_WIDTH) + 28) % VRAM_TILES_H;
for (uint8_t y = 0; y < VRAM_TILES_V; ++y)
for (uint8_t x = tx; x < 4 + tx; ++x)
SetTile(x % VRAM_TILES_H, y % VRAM_TILES_V, TILE_PURE_SKY);
}
if ((c->y_max - c->y_min) != 0x1f) {
uint8_t ty = ((c->y / TILE_HEIGHT) + !!(c->y % TILE_HEIGHT) + 28) % VRAM_TILES_V;
for (uint8_t x = 0; x < VRAM_TILES_H; ++x)
for (uint8_t y = ty; y < 4 + ty; ++y)
SetTile(x % VRAM_TILES_H, y % VRAM_TILES_V, TILE_PURE_SKY);
}
// Fill VRAM, based on the current camera position
for (uint8_t i = 0; i < VRAM_TILES_H - 2; ++i)
Level_drawColumn(c->level, cxt + i, cyt, cxt + i);
Level_drawColumn(c->level, cxt + 30, cyt, cxt - 2);
Level_drawColumn(c->level, cxt + 31, cyt, cxt - 1);
#if (DEBUG_MIN_MAX_VALID_VRAM == 1)
UZEMCHR = 'x'; UZEMCHR = '_'; UZEMCHR = 'm'; UZEMCHR = 'i'; UZEMCHR = 'n'; UZEMCHR = ' '; UZEMCHR = '='; UZEMCHR = ' '; UZEMHEX = c->x_min; UZEMCHR = ' ';
UZEMCHR = 'x'; UZEMCHR = '_'; UZEMCHR = 'm'; UZEMCHR = 'a'; UZEMCHR = 'x'; UZEMCHR = ' '; UZEMCHR = '='; UZEMCHR = ' '; UZEMHEX = c->x_max; UZEMCHR = ' ';
UZEMCHR = 'y'; UZEMCHR = '_'; UZEMCHR = 'm'; UZEMCHR = 'i'; UZEMCHR = 'n'; UZEMCHR = ' '; UZEMCHR = '='; UZEMCHR = ' '; UZEMHEX = c->y_min; UZEMCHR = ' ';
UZEMCHR = 'y'; UZEMCHR = '_'; UZEMCHR = 'm'; UZEMCHR = 'a'; UZEMCHR = 'x'; UZEMCHR = ' '; UZEMCHR = '='; UZEMCHR = ' '; UZEMHEX = c->y_max; UZEMCHR = '\n';
#endif
}
void Camera_update(CAMERA* const c, PLAYER* const p)
{
#define X_CAMERA_WINDOW (16)
#define Y_CAMERA_WINDOW (28)
int16_t cxt;
int16_t cyt;
#if (DEBUG_MIN_MAX_VALID_VRAM == 1)
uint8_t prev_x_min = c->x_min;
uint8_t prev_x_max = c->x_max;
uint8_t prev_y_min = c->y_min;
uint8_t prev_y_max = c->y_max;
#endif
int16_t targetX = NEAREST_SCREEN_PIXEL(p->x) - ((SCREEN_TILES_H * TILE_WIDTH) / 2) + TILE_WIDTH / 2;
// Set the X dead zone to a reasonable number
if (targetX >= c->x + X_CAMERA_WINDOW)
Camera_moveToX(c, targetX - X_CAMERA_WINDOW);
else if (targetX <= c->x - X_CAMERA_WINDOW)
Camera_moveToX(c, targetX + X_CAMERA_WINDOW);
uint8_t prevX = Screen.scrollX;
Screen.scrollX = (uint8_t)c->x;
cxt = c->x / TILE_WIDTH;
cyt = c->y / TILE_HEIGHT;
// Have we scrolled past a tile boundary along X?
if ((prevX & ~(TILE_WIDTH - 1)) != (Screen.scrollX & ~(TILE_WIDTH - 1))) {
if ((uint8_t)(Screen.scrollX - prevX) < (uint8_t)(prevX - Screen.scrollX)) {
// Update VRAM min X extent
if (cxt - 3 >= 0)
c->x_min = cxt - 2;
// Load a new column of level data
Level_drawColumn(c->level, cxt + 29, cyt, cxt + 29);
// Update VRAM max X extent
if (cxt + 29 <= (int16_t)c->level->width - 1)
c->x_max = cxt + 29;
} else {
// Update VRAM max X extent
if (cxt + 30 <= (int16_t)c->level->width - 1)
c->x_max = cxt + 29;
// Load a new column of level data
Level_drawColumn(c->level, cxt + 30, cyt, cxt - 2);
// Update VRAM min X extent
if (cxt - 2 >= 0)
c->x_min = cxt - 2;
}
c->is_new_col_frame = true;
} else
c->is_new_col_frame = false;
int16_t targetY = NEAREST_SCREEN_PIXEL(p->y) - ((SCREEN_TILES_V * TILE_HEIGHT) / 2) + TILE_HEIGHT / 2;
// Make the Y dead zone at least as big as the jump height to avoid a bouncy camera
if (targetY >= c->y + Y_CAMERA_WINDOW)
Camera_moveToY(c, targetY - Y_CAMERA_WINDOW);
else if (targetY <= c->y - Y_CAMERA_WINDOW)
Camera_moveToY(c, targetY + Y_CAMERA_WINDOW);
uint8_t prevY = Screen.scrollY;
Screen.scrollY = (uint8_t)c->y;
cxt = c->x / TILE_WIDTH;
cyt = c->y / TILE_HEIGHT;
// Have we scrolled past a tile boundary along Y?
if ((prevY & ~(TILE_HEIGHT - 1)) != (Screen.scrollY & ~(TILE_HEIGHT - 1))) {
if ((uint8_t)(Screen.scrollY - prevY) < (uint8_t)(prevY - Screen.scrollY)) {
// Update VRAM min Y extent
if (cyt - 3 >= 0)
c->y_min = cyt - 2;
// Load a new row of level data
Level_drawRow(c->level, cxt, cyt + 29, cyt + 29);
// Update VRAM max Y extent
if (cyt + 29 <= (int16_t)c->level->height - 1)
c->y_max = cyt + 29;
} else {
// Update VRAM max Y extent
if (cyt + 30 <= (int16_t)c->level->height - 1)
c->y_max = cyt + 29;
// Load a new row of level data
Level_drawRow(c->level, cxt, cyt + 30, cyt - 2);
// Update VRAM min Y extent
if (cyt - 2 >= 0)
c->y_min = cyt - 2;
}
c->is_new_row_frame = true;
} else
c->is_new_row_frame = false;
#if (DEBUG_MIN_MAX_VALID_VRAM == 1)
if (prev_x_min != c->x_min || prev_x_max != c->x_max || prev_y_min != c->y_min || prev_y_max != c->y_max) {
UZEMCHR = 'x'; UZEMCHR = '_'; UZEMCHR = 'm'; UZEMCHR = 'i'; UZEMCHR = 'n'; UZEMCHR = ' '; UZEMCHR = '='; UZEMCHR = ' '; UZEMHEX = c->x_min; UZEMCHR = ' ';
UZEMCHR = 'x'; UZEMCHR = '_'; UZEMCHR = 'm'; UZEMCHR = 'a'; UZEMCHR = 'x'; UZEMCHR = ' '; UZEMCHR = '='; UZEMCHR = ' '; UZEMHEX = c->x_max; UZEMCHR = ' ';
UZEMCHR = 'y'; UZEMCHR = '_'; UZEMCHR = 'm'; UZEMCHR = 'i'; UZEMCHR = 'n'; UZEMCHR = ' '; UZEMCHR = '='; UZEMCHR = ' '; UZEMHEX = c->y_min; UZEMCHR = ' ';
UZEMCHR = 'y'; UZEMCHR = '_'; UZEMCHR = 'm'; UZEMCHR = 'a'; UZEMCHR = 'x'; UZEMCHR = ' '; UZEMCHR = '='; UZEMCHR = ' '; UZEMHEX = c->y_max; UZEMCHR = '\n';
}
#endif
}
////////////////////////////////////////////////////////////////////////////////
// End Camera Operations
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// Begin Dissolving Platform Operations
////////////////////////////////////////////////////////////////////////////////
#define MAX_ACTIVE_DISSOLVING_ANIMATED_TILES (26) // For best results, make this at least big enough so the player can't outrun the tiles fully dissolving
#define DISSOLVING_TILE_STANDARD_EXPIRATION (12)
#define DISSOLVING_TILE_RESET_EXPIRATION (128)
// Dissolving tile bottom (platform) definitions
#define TILE_START_CLOUD_PLATFORM (TILE_START_ONE_WAY + 2) /* The solid cloud one-way tile start */
#define TILE_END_CLOUD_PLATFORM (TILE_START_CLOUD_PLATFORM + 1) /* The solid cloud one-way tile end */
#define TILE_START_CLOUD_PLATFORM_DISSOLVING (TILE_END_CLOUD_PLATFORM + 1) /* The first translucent cloud one-way tile */
#define TILE_END_CLOUD_PLATFORM_DISSOLVING (TILE_START_CLOUD_PLATFORM_DISSOLVING + 9) /* The last translucent cloud one-way tile */
// Dissolving tile top (sky) definitions
#define TILE_START_CLOUD_PLATFORM_TOP (24) /* The solid cloud one-way tile top start (sky decoration) */
#define TILE_END_CLOUD_PLATFORM_TOP (TILE_START_CLOUD_PLATFORM_TOP + 2) /* The solid cloud one-way tile top start (sky decoration) */
#define TILE_START_CLOUD_PLATFORM_TOP_DISSOLVING (TILE_END_CLOUD_PLATFORM_TOP + 1) /* The first tranlucent cloud tile top (sky decoration) */
#define TILE_END_CLOUD_PLATFORM_TOP_DISSOLVING (TILE_START_CLOUD_PLATFORM_TOP_DISSOLVING + 14) /* The last translucent cloud tile top (sky decoration) */
struct DISSOLVING_TILE {
uint16_t tile[MAX_ACTIVE_DISSOLVING_ANIMATED_TILES];
uint8_t expiration[MAX_ACTIVE_DISSOLVING_ANIMATED_TILES];
uint8_t numActive;
uint8_t counter;
} __attribute__ ((packed));
void DissolvingTile_init(DISSOLVING_TILE* const dt)
{
dt->numActive = 0;
}
#define TILE_CLOUD_PLATFORM_NEXT_OFFSET (TILE_START_CLOUD_PLATFORM)
const uint8_t tileCloudPlatformNext[] PROGMEM =
{
TILE_CLOUD_PLATFORM_NEXT_OFFSET + 2,
TILE_CLOUD_PLATFORM_NEXT_OFFSET + 3,
TILE_CLOUD_PLATFORM_NEXT_OFFSET + 4,
TILE_CLOUD_PLATFORM_NEXT_OFFSET + 5,
TILE_CLOUD_PLATFORM_NEXT_OFFSET + 6,
TILE_CLOUD_PLATFORM_NEXT_OFFSET + 7,
TILE_CLOUD_PLATFORM_NEXT_OFFSET + 8,
TILE_CLOUD_PLATFORM_NEXT_OFFSET + 9,
TILE_CLOUD_PLATFORM_NEXT_OFFSET + 10, // TILE_PURE_SKY,
TILE_CLOUD_PLATFORM_NEXT_OFFSET + 11, // TILE_PURE_SKY,
TILE_START_CLOUD_PLATFORM,
TILE_START_CLOUD_PLATFORM + 1,
};
#define TILE_CLOUD_PLATFORM_TOP_NEXT_OFFSET (TILE_START_CLOUD_PLATFORM_TOP)
const uint8_t tileCloudPlatformTopNext[] PROGMEM =
{
TILE_CLOUD_PLATFORM_TOP_NEXT_OFFSET + 3,
TILE_CLOUD_PLATFORM_TOP_NEXT_OFFSET + 4,
TILE_CLOUD_PLATFORM_TOP_NEXT_OFFSET + 5,
TILE_CLOUD_PLATFORM_TOP_NEXT_OFFSET + 6,
TILE_CLOUD_PLATFORM_TOP_NEXT_OFFSET + 7,
TILE_CLOUD_PLATFORM_TOP_NEXT_OFFSET + 8,
TILE_CLOUD_PLATFORM_TOP_NEXT_OFFSET + 9,
TILE_CLOUD_PLATFORM_TOP_NEXT_OFFSET + 10,
TILE_CLOUD_PLATFORM_TOP_NEXT_OFFSET + 11,
TILE_CLOUD_PLATFORM_TOP_NEXT_OFFSET + 12,
TILE_CLOUD_PLATFORM_TOP_NEXT_OFFSET + 13,
TILE_CLOUD_PLATFORM_TOP_NEXT_OFFSET + 14,
TILE_CLOUD_PLATFORM_TOP_NEXT_OFFSET + 15,
TILE_CLOUD_PLATFORM_TOP_NEXT_OFFSET + 16,
TILE_CLOUD_PLATFORM_TOP_NEXT_OFFSET + 17,
TILE_START_CLOUD_PLATFORM_TOP,
TILE_START_CLOUD_PLATFORM_TOP + 1,
TILE_START_CLOUD_PLATFORM_TOP + 2,
};
uint8_t DissolvingTile_getNextTile(uint8_t tile)
{
if ((tile >= TILE_START_CLOUD_PLATFORM) && (tile <= TILE_END_CLOUD_PLATFORM_DISSOLVING))
return pgm_read_byte(&tileCloudPlatformNext[tile - TILE_CLOUD_PLATFORM_NEXT_OFFSET]);
else if ((tile >= TILE_START_CLOUD_PLATFORM_TOP) && (tile <= TILE_END_CLOUD_PLATFORM_TOP_DISSOLVING))
return pgm_read_byte(&tileCloudPlatformTopNext[tile - TILE_CLOUD_PLATFORM_TOP_NEXT_OFFSET]);
return tile;
}
void DissolvingTile_remove(DISSOLVING_TILE* const dt, uint8_t index)
{
#if (DEBUG_DISSOLVING_TILE_ANIMATIONS > 1)
uint16_t xy_index = dt->tile[index];
#endif
dt->tile[index] = dt->tile[dt->numActive - 1];
dt->expiration[index] = dt->expiration[dt->numActive - 1];
dt->numActive--;
#if (DEBUG_DISSOLVING_TILE_ANIMATIONS > 1)
UZEMCHR = 'D'; UZEMCHR = 'T'; UZEMCHR = '-';
UZEMHEX = HI8(xy_index); UZEMHEX = LO8(xy_index); UZEMCHR = '\n';
UZEMCHR = 'N'; UZEMHEX = dt->numActive; UZEMCHR = '\n';
#endif
}
void DissolvingTile_reclaimSpace(DISSOLVING_TILE* const dt)
{
uint8_t tilesToReclaim = 1;
uint8_t minIndex[2] = { 0, 0 };
uint8_t minValue = 0;
uint8_t maxTileNoDiv2 = 0;
// Loop over all the active tiles in the list, and read the tile number in VRAM associated with that xy_index.
// The bottom dissolving platform tiles come in pairs, and we want to treat each separate tile in the pair as
// equal, so we use that tile number divided by two (accounting for the pair starting on both an even or odd
// tile number, in case the tileset later changes). The tile we want to reclaim is the tile that is the most
// dissolved, so we are looking for the maximum tile number with the closest expiration time. However, due to
// the possibility that the player may land on two tiles at the same time (across a tile boundary), there may
// be two tiles in the list with the same expiration time that satisfy those conditions. If we only reclaim
// one of them, there may be a visual glitch (hole left) since the left tile always gets added first, so if
// the conditions match, also look for a second tile with the same expiration time, and reclaim that as well.
// The reclaiming process involves fast-forwarding to the last tile in the dissolving tile sequence, and
// removing it from the list. This algorithm works for cases where the tile is both in the reset period, and
// when it hasn't fully dissolved yet, so it is now possible to set MAX_ACTIVE_DISSOLVING_ANIMATED_TILES
// to any number greater than one and it will do the correct thing.
for (uint8_t i = 0; i < dt->numActive; ++i) {
uint16_t xy_index = dt->tile[i];
uint8_t tx = LO8(xy_index) % VRAM_TILES_H;
uint8_t ty = HI8(xy_index) % VRAM_TILES_V;
// Divide the tile number by 2, because they are in pairs of two, and we want to treat the tiles in the pair as
// equal. Also account for the pair starting on both even and odd tile numbers (in case the tileset is changed)
uint8_t tileNoDiv2 = (GetTile(tx, ty) + (TILE_START_CLOUD_PLATFORM_DISSOLVING & 1)) / 2;
if (tileNoDiv2 > maxTileNoDiv2) {
maxTileNoDiv2 = tileNoDiv2;
minValue = dt->expiration[i] - dt->counter;
minIndex[0] = i;
tilesToReclaim = 1;
#if (DEBUG_DISSOLVING_TILE_ANIMATIONS > 1)
UZEMCHR = 'I'; UZEMHEX = i; UZEMCHR = '\n';
#endif
} else if (tileNoDiv2 == maxTileNoDiv2) {
uint8_t theValue = dt->expiration[i] - dt->counter;
if (theValue < minValue) {
minValue = theValue;
minIndex[0] = i;
tilesToReclaim = 1;
#if (DEBUG_DISSOLVING_TILE_ANIMATIONS > 1)
UZEMCHR = 'I'; UZEMHEX = i; UZEMCHR = '\n';
#endif
} else if (theValue == minValue) {
minIndex[1] = i;
tilesToReclaim = 2;
#if (DEBUG_DISSOLVING_TILE_ANIMATIONS > 1)
UZEMCHR = 'H'; UZEMCHR = '\n';
UZEMCHR = 'i'; UZEMHEX = i; UZEMCHR = '\n';
#endif
}
}
}
#if (DEBUG_DISSOLVING_TILE_ANIMATIONS > 1)
UZEMCHR = 'm'; UZEMHEX = minIndex[0]; UZEMCHR = '\n';
if (tilesToReclaim > 1) {
UZEMCHR = 'm'; UZEMHEX = minIndex[1]; UZEMCHR = '\n';
}
UZEMCHR = 'v'; UZEMHEX = minValue; UZEMCHR = '\n';
#endif
for (uint8_t j = 0; j < tilesToReclaim; ++j) {
// DissolvingTile_add() is only called from within Player_update(), and since Player_update()
// always runs before Camera_update() and DissolvingTile_update(), it follows that the previous
// DissolvingTile_update() has been run before this call to DissolvingTile_add(), so any offscreen
// tiles on the list would have been removed, so we don't need to check that its xy_index is still
// in VRAM, because it should definitely be.
uint16_t xy_index = dt->tile[minIndex[j]];
uint8_t tx = LO8(xy_index) % VRAM_TILES_H;
uint8_t ty = HI8(xy_index) % VRAM_TILES_V;
// This could be an infinite loop, but in case something goes awry, limit it to at least the number of dissolving steps
for (uint8_t i = 0; i < 8; ++i) {
uint8_t newTile = DissolvingTile_getNextTile(GetTile(tx, ty));
SetTile(tx, ty, newTile);
SetTile(tx, (ty - 1) % VRAM_TILES_V, DissolvingTile_getNextTile(GetTile(tx, (ty - 1) % VRAM_TILES_V)));
if (newTile < TILE_START_CLOUD_PLATFORM_DISSOLVING || newTile > TILE_END_CLOUD_PLATFORM_DISSOLVING)
break;
}
DissolvingTile_remove(dt, minIndex[j]);
}
}
void DissolvingTile_add(DISSOLVING_TILE* const dt, uint16_t index, uint8_t expiration)
{
try_again:
if (dt->numActive < MAX_ACTIVE_DISSOLVING_ANIMATED_TILES) {
dt->tile[dt->numActive] = index;
dt->expiration[dt->numActive] = dt->counter + expiration;
dt->numActive++;
#if (DEBUG_DISSOLVING_TILE_ANIMATIONS > 1)
UZEMCHR = 'D'; UZEMCHR = 'T'; UZEMCHR = '+';
UZEMHEX = HI8(index); UZEMHEX = LO8(index); UZEMCHR = '\n';
UZEMCHR = 'N'; UZEMHEX = dt->numActive; UZEMCHR = '\n';
#endif
} else { // We have too many active dissolving tiles, so reclaiming the next one to expire
#if (DEBUG_DISSOLVING_TILE_ANIMATIONS > 1)
UZEMCHR = 'D'; UZEMCHR = 'T'; UZEMCHR = 'R'; UZEMCHR = 'E'; UZEMCHR = 'C';
UZEMCHR = 'L'; UZEMCHR = 'A'; UZEMCHR = 'I'; UZEMCHR = 'M'; UZEMCHR = '\n';
#endif
DissolvingTile_reclaimSpace(dt);
goto try_again;
}
}
//__attribute__((optimize("O3")))
void DissolvingTile_update(DISSOLVING_TILE* const dt, const CAMERA* const c)
{
//UZEMCHR = 'B';
for (uint8_t i = 0; i < dt->numActive; ++i) {
// Retrieve the xy_index of each active tile from the tile list
uint16_t xy_index = dt->tile[i];
// Break that down into world tile coordinates and VRAM tile coordinates
uint8_t wtx = LO8(xy_index);
uint8_t wty = HI8(xy_index);
uint8_t tx = LO8(xy_index) % VRAM_TILES_H;
uint8_t ty = HI8(xy_index) % VRAM_TILES_V;
// We only need to run the glitch protection on frames that we loaded a new row and/or column
// of level data, otherwise we can skip right to the part where we check for the expiration time.
// This requires that Camera_update be called before DissolvingTile_update
if (c->is_new_col_frame || c->is_new_row_frame) {
if ((wtx < c->x_min) || (wtx > c->x_max) || (wty < c->y_min)) {
// Remove the tile from the list if its world coordinates are to the left, right, or above
// the world coordinates currently loaded into our VRAM, as per the Camera object's VRAM
// extent tracking system. The bottom direction is a special case, as we may need to do
// extra work if the world coordinate is a single tile below the VRAM extent, meaning the
// bottom half of the cloud got scrolled out of VRAM, but the top half is still in VRAM.
DissolvingTile_remove(dt, i--);
#if (DEBUG_DISSOLVING_TILE_ANIMATIONS > 0)
UZEMCHR = 'G'; UZEMCHR = 'P'; UZEMCHR = '0'; UZEMCHR = '\n';
#if (DEBUG_DISSOLVING_TILE_ANIMATIONS > 1)
UZEMCHR = 'N'; UZEMHEX = dt->numActive; UZEMCHR = '\n';
#endif
#endif
continue;
} else if (wty > c->y_max) {
// Remove the tile from the list if its world coordinates are below the world coordinates
// currently loaded into our VRAM, as per the Camera object's VRAM extent tracking system.
DissolvingTile_remove(dt, i--);
if (wty == c->y_max + 1) {
// We might have panned up, but only scrolled the bottom half of the cloud off screen,
// so check to see if we need to immediately set the top half to solid so it matches the
// bottom half that got reloaded as solid from the level data.
uint8_t tileAbove = GetTile(tx, (ty - 1) % VRAM_TILES_V);
if (tileAbove >= TILE_START_CLOUD_PLATFORM_TOP_DISSOLVING && tileAbove <= TILE_END_CLOUD_PLATFORM_TOP_DISSOLVING) { // this check optional, but good for sanity?
SetTile(tx, (ty - 1) % VRAM_TILES_V, TILE_START_CLOUD_PLATFORM_TOP + (tileAbove - TILE_START_CLOUD_PLATFORM_TOP) % 3); // 3 = how many tops in a row
#if (DEBUG_DISSOLVING_TILE_ANIMATIONS > 0)
UZEMCHR = 'G'; UZEMCHR = 'P'; UZEMCHR = '1'; UZEMCHR = '\n';
#if (DEBUG_DISSOLVING_TILE_ANIMATIONS > 1)
UZEMCHR = 'N'; UZEMHEX = dt->numActive; UZEMCHR = '\n';
#endif
#endif
}
}
continue;
} else if (wty == c->y_min) {
// We might have panned down, but only scrolled the top half of a cloud off screen, so
// check to see if we need to set the bottom half to solid to match the top half that got
// reloaded as solid from the level data. Similar to the check above, but panning down
// instead of up. We don't need these checks for horizontal scrolling because their top
// and bottom parts should always be loaded at the same time.
DissolvingTile_remove(dt, i--);
uint8_t tile = GetTile(tx, ty);
if (tile >= TILE_START_CLOUD_PLATFORM_DISSOLVING && tile <= TILE_END_CLOUD_PLATFORM_DISSOLVING) { // this check optional, but good for sanity?
SetTile(tx, ty, TILE_START_CLOUD_PLATFORM + (tile - TILE_START_CLOUD_PLATFORM) % 2); // 2 = how many bottoms in a row
#if (DEBUG_DISSOLVING_TILE_ANIMATIONS > 0)
UZEMCHR = 'G'; UZEMCHR = 'P'; UZEMCHR = '2'; UZEMCHR = '\n';
#if (DEBUG_DISSOLVING_TILE_ANIMATIONS > 1)
UZEMCHR = 'N'; UZEMHEX = dt->numActive; UZEMCHR = '\n';
#endif
#endif
}
continue;
}
}
// Check the expiration time, after we fixed any potential glitches caused by scrolling
//UZEMHEX = dt->counter;
if (dt->counter != dt->expiration[i])
continue;
// Get the next animated tiles based on a lookup table
uint8_t newTile = DissolvingTile_getNextTile(GetTile(tx, ty));
// Set the bottom half tile (the one whose index was stored in the list)
SetTile(tx, ty, newTile);
// Set the top half tile (its index is not stored in the list)
SetTile(tx, (ty - 1) % VRAM_TILES_V, DissolvingTile_getNextTile(GetTile(tx, (ty - 1) % VRAM_TILES_V)));
// If the tile we transitioned to is not an animated tile, then we are done with it
if (newTile < TILE_START_CLOUD_PLATFORM_DISSOLVING || newTile > TILE_END_CLOUD_PLATFORM_DISSOLVING) {
// Remove this tile animation from the list
DissolvingTile_remove(dt, i--);
continue;
}
// Reset the expiration time since we transitioned (make the fallthrough "pure sky" lookalikes last longer)
if ((newTile == TILE_END_CLOUD_PLATFORM_DISSOLVING - 1) || (newTile == TILE_END_CLOUD_PLATFORM_DISSOLVING))
dt->expiration[i] = dt->counter + DISSOLVING_TILE_RESET_EXPIRATION;
else
dt->expiration[i] = dt->counter + DISSOLVING_TILE_STANDARD_EXPIRATION;
}
//UZEMCHR = 'E'; UZEMCHR = '\n'; UZEMCHR = '\n';
}
////////////////////////////////////////////////////////////////////////////////
// End Dissolving Platform Operations
////////////////////////////////////////////////////////////////////////////////
#define HORIZONTAL_PLATFORM_TILE_WIDTH (4)
#define HORIZONTAL_PLATFORM_TOP_OFFSET_SUBPIXELS (2 << FP_SHIFT)
#define TILE_START_HORIZONTAL_PLATFORM (TILE_END_CLOUD_PLATFORM_TOP_DISSOLVING + 1)
#define TILE_END_HORIZONTAL_PLATFORM (TILE_START_HORIZONTAL_PLATFORM + 18)
// The below define for the start of the vertical platform is for the vertical platform
// only pieces. Go through the LUT defined below to encompass all of the VMP pieces
// including pieces shared with HMPs.
#define TILE_START_VERTICAL_PLATFORM (TILE_END_HORIZONTAL_PLATFORM + 1)
#define TILE_END_VERTICAL_PLATFORM (TILE_START_VERTICAL_PLATFORM + 27)
// Since VMPs reuse some of the HMP tiles, a LUT is used to make things more sane
const uint8_t verticalMovingPlatformMapTop[] PROGMEM =
{
TILE_START_VERTICAL_PLATFORM - 19, // offset 0, left piece
TILE_START_VERTICAL_PLATFORM - 10, // offset 0, middle piece
TILE_START_VERTICAL_PLATFORM - 1, // offset 0, right piece
TILE_START_VERTICAL_PLATFORM + 0, // offset 1, left piece
TILE_START_VERTICAL_PLATFORM + 1, // offset 1, middle piece
TILE_START_VERTICAL_PLATFORM + 2, // offset 1, right piece
TILE_START_VERTICAL_PLATFORM + 3, // offset 2, left piece
TILE_START_VERTICAL_PLATFORM + 4, // offset 2, middle piece
TILE_START_VERTICAL_PLATFORM + 5, // offset 2, right piece
TILE_START_VERTICAL_PLATFORM + 6, // offset 3, left piece top
TILE_START_VERTICAL_PLATFORM + 7, // offset 3, middle piece top
TILE_START_VERTICAL_PLATFORM + 8, // offset 3, right piece top
TILE_START_VERTICAL_PLATFORM + 10, // offset 4, left piece top
TILE_START_VERTICAL_PLATFORM + 11, // offset 4, middle piece top
TILE_START_VERTICAL_PLATFORM + 12, // offset 4, right piece top
TILE_START_VERTICAL_PLATFORM + 14, // offset 5, left piece top
TILE_START_VERTICAL_PLATFORM + 15, // offset 5, middle piece top
TILE_START_VERTICAL_PLATFORM + 16, // offset 5, right piece top
TILE_START_VERTICAL_PLATFORM + 18, // offset 6, left piece top
TILE_START_VERTICAL_PLATFORM + 19, // offset 6, middle piece top
TILE_START_VERTICAL_PLATFORM + 20, // offset 6, right piece top
TILE_START_VERTICAL_PLATFORM + 22, // offset 7, left piece top
TILE_START_VERTICAL_PLATFORM + 23, // offset 7, middle piece top
TILE_START_VERTICAL_PLATFORM + 24, // offset 7, right piece top
};
const uint8_t verticalMovingPlatformMapBottom[] PROGMEM =
{
TILE_PURE_SKY, // offset 0, left piece (no need to draw)
TILE_PURE_SKY, // offset 0, middle piece (no need to draw)
TILE_PURE_SKY, // offset 0, right piece (no need to draw)
TILE_PURE_SKY, // offset 1, left piece (no need to draw)
TILE_PURE_SKY, // offset 1, middle piece (no need to draw)
TILE_PURE_SKY, // offset 1, right piece (no need to draw)
TILE_PURE_SKY, // offset 2, left piece (no need to draw)
TILE_PURE_SKY, // offset 2, middle piece (no need to draw)
TILE_PURE_SKY, // offset 2, right piece (no need to draw)
TILE_START_VERTICAL_PLATFORM + 9, // offset 3, left piece bottom
TILE_START_VERTICAL_PLATFORM + 9, // offset 3, middle piece bottom
TILE_START_VERTICAL_PLATFORM + 9, // offset 3, right piece bottom
TILE_START_VERTICAL_PLATFORM + 13, // offset 4, left piece bottom
TILE_START_VERTICAL_PLATFORM + 13, // offset 4, middle piece bottom
TILE_START_VERTICAL_PLATFORM + 13, // offset 4, right piece bottom
TILE_START_VERTICAL_PLATFORM + 17, // offset 5, left piece bottom
TILE_START_VERTICAL_PLATFORM + 17, // offset 5, middle piece bottom
TILE_START_VERTICAL_PLATFORM + 17, // offset 5, right piece bottom
TILE_START_VERTICAL_PLATFORM + 21, // offset 6, left piece bottom
TILE_START_VERTICAL_PLATFORM + 21, // offset 6, middle piece bottom
TILE_START_VERTICAL_PLATFORM + 21, // offset 6, right piece bottom
TILE_START_VERTICAL_PLATFORM + 25, // offset 7, left piece bottom
TILE_START_VERTICAL_PLATFORM + 26, // offset 7, middle piece bottom
TILE_START_VERTICAL_PLATFORM + 27, // offset 7, right piece bottom
};
////////////////////////////////////////////////////////////////////////////////
// Begin Player Operations
////////////////////////////////////////////////////////////////////////////////
#define PLAYER_OFFSCREEN_ARROW_SPRITE (5)
#define PLAYER_ANIMATION_FRAME_SKIP (8)
#define PLAYER_ATTACK_ANIMATION_FRAME_SKIP (4)
#define PLAYER_ATTACK_COOLDOWN (12)
#define PLAYER_ATTACK_BUTTON BTN_X
const uint8_t playerAnimation[] PROGMEM = { 0, 1, 2, 3 };
const uint8_t playerAttackAnimation[] PROGMEM = { 0, 1, 0, 1 };
const uint8_t playerLadderAnimation[] PROGMEM = { 3, 2, 1, 0, 1, 2 };
const int8_t* const walk_maps[] PROGMEM = { blitmap_walk1, blitmap_walk2, blitmap_walk0, blitmap_walk3 };
const int8_t* const run_maps[] PROGMEM = { blitmap_run0, blitmap_run1, blitmap_run2, blitmap_run3 };
const int8_t* const attack_maps[] PROGMEM = { blitmap_melee0, blitmap_melee1, blitmap_melee0, blitmap_melee1 };
const int8_t* const climb_maps[] PROGMEM = { blitmap_ladder_climb0, blitmap_ladder_climb1, blitmap_ladder_climb2, blitmap_ladder_climb3 };
void Player_update(PLAYER* const p, CAMERA* const c, DISSOLVING_TILE* const dt, LEVEL* const l);
void Player_render(PLAYER* const p, CAMERA* const c);
void Player_init(PLAYER* const p, LEVEL* const l)
{
memset(p, 0, sizeof(PLAYER) - sizeof(BUTTON_INFO));
p->update = Player_update;
p->level = l;
p->map = blitmap_walk0;
p->x = HT2P(4);//HT2P(57);
p->y = VT2P(4);//VT2P(52);
p->impulse = WORLD_JUMP;
p->visible = true;
p->jumpReleased = true;
p->right = true;
p->falling = true;
p->attacking = false;
p->attackReleased = true;
p->framesFalling = WORLD_FALLING_GRACE_FRAMES + 1; // don't allow a jump if you start in the air
p->attackCounter = PLAYER_ATTACK_COOLDOWN;
p->gulped = false;
p->ddrc_num = 0xFF;
p->onMovingPlatform = false;
}
__attribute__(( always_inline ))
static inline bool IsSolid(const uint8_t t)
{
return ((t) >= TILE_START_SOLID && (t) < TILE_START_HAZARD);
}
__attribute__(( always_inline ))
static inline bool IsOneWay(const uint8_t t)
{
return (((t) >= TILE_START_ONE_WAY) && ((t) <= TILE_END_ONE_WAY));
}
__attribute__(( always_inline ))
static inline bool IsLadder(const uint8_t t)
{
return ((((t) >= TILE_START_LADDER_SKY) && ((t) <= TILE_END_LADDER_SKY)) ||
(((t) >= TILE_START_LADDER_SOLID) && ((t) <= TILE_END_LADDER_SOLID)));
}
__attribute__(( always_inline ))
static inline bool IsTreasure(const uint8_t t)
{
return ((t) <= TILE_LOLLIPOP_TOP_END + 5); // lollipop tops + main sticks
}
__attribute__(( always_inline ))
static inline bool IsHazard(const uint8_t t)
{
return ((t) >= TILE_START_HAZARD/* && (t) <= TILE_START_HAZARD + 5*/);
}
void Player_input(PLAYER* const p)
{
// Maybe farting on an enemy's projectile can send it back to the enemy?
// Maybe the cooldown gets shorter with each lollipop you collect?
p->buttons.prev = p->buttons.held;
p->buttons.held = ReadJoypad(0);
p->buttons.pressed = p->buttons.held & (p->buttons.held ^ p->buttons.prev);
p->buttons.released = p->buttons.prev & (p->buttons.held ^ p->buttons.prev);
if (p->buttons.held & BTN_SELECT) {
if (p->buttons.pressed & BTN_Y || p->buttons.pressed & BTN_B) {
if (p->buttons.pressed & BTN_Y)
DDRC = --p->ddrc_num;
else if (p->buttons.pressed & BTN_B)
DDRC = ++p->ddrc_num;
UZEMHEX = p->ddrc_num; UZEMCHR = '\n';
}
}
if (!p->dead && !p->attacking && p->attackReleased && (p->buttons.held & PLAYER_ATTACK_BUTTON) && p->attackCounter == PLAYER_ATTACK_COOLDOWN) {
p->attacking = true;
p->attackingLeft = !(p->flags & SPRITE_FLIP_X);
p->attackCounter = 0;
TriggerFx(FX_FART, 128, true);
p->attackReleased = false;
p->animationFrameCounter = 0;
}
// Attack/Jump logic
if (p->attacking) {
p->attackCounter++;
// Make sure the attack doesn't last longer than the attack animation
if (p->attackCounter == (PLAYER_ATTACK_ANIMATION_FRAME_SKIP * NELEMS(playerAttackAnimation) - 1)) {
p->attacking = false;
p->attackCounter = 0; // reuse for cooldown
}
} else if (p->attackCounter < PLAYER_ATTACK_COOLDOWN)
p->attackCounter++;
if ((p->buttons.released & PLAYER_ATTACK_BUTTON))
p->attackReleased = true;
p->left = (bool)(p->buttons.held & BTN_LEFT);
p->right = (bool)(p->buttons.held & BTN_RIGHT);
p->up = (bool)(p->buttons.held & BTN_UP);
p->down = (bool)(p->buttons.held & BTN_DOWN);
// Allow players to jump by holding the jump button before landing,
// but require them to release it before they can jump again
if (p->jumpReleased) {
p->jump = (bool)(p->buttons.held & BTN_A);
if (p->jump) {
// Look at the tile(s) above the player's head. If the jump would not
// be allowed, then don't set jump to true until they can actually jump
// This allows the player to hold the jump button early while
// jump-restricted, but still make the jump as soon as it is allowed
int16_t roundedX = NEAREST_SCREEN_PIXEL(p->x) << FP_SHIFT; // ignore subpixels for this calculation
uint8_t tx = P2HT(roundedX);
uint8_t ty = P2VT(p->y - 1);
if ( IsSolid(GetTile(tx % VRAM_TILES_H, ty % VRAM_TILES_V)) || // cellup
((bool)NH(roundedX) && IsSolid(GetTile((tx + PLAYER_TILE_WIDTH) % VRAM_TILES_H, ty % VRAM_TILES_V)))) // cellupdiag
if (p->update == Player_update)
p->jump = false;
}
} else {
p->jump = false;
if (p->buttons.released & BTN_A)
p->jumpReleased = true;
}
}
static void ClearTreasure(LEVEL* const l, uint8_t tx, uint8_t ty)
{
SetTile((tx ) % VRAM_TILES_H, (ty ) % VRAM_TILES_V, TILE_PURE_SKY);
SetTile((tx + 1) % VRAM_TILES_H, (ty ) % VRAM_TILES_V, TILE_PURE_SKY);
SetTile((tx ) % VRAM_TILES_H, (ty + 1) % VRAM_TILES_V,
pgm_read_byte(&lollipop_tile_to_sky_tile[GetTile((tx ) % VRAM_TILES_H, (ty + 1) % VRAM_TILES_V)]));
SetTile((tx + 1) % VRAM_TILES_H, (ty + 1) % VRAM_TILES_V,
pgm_read_byte(&lollipop_tile_to_sky_tile[GetTile((tx + 1) % VRAM_TILES_H, (ty + 1) % VRAM_TILES_V)]));
uint16_t xy_index = MAKEWORD(tx, ty);
Collectable_performAction(l, COLLECTABLE_ACTION_CLEAR, xy_index); // maybe only do this if you aren't dead if keeping score
}
static void KillPlayer(PLAYER* const p)
{
#if (DEBUG_PRINT_KILLED_BY == 1)
UZEMCHR = 'K'; UZEMCHR = 'i'; UZEMCHR = 'l'; UZEMCHR = 'l'; UZEMCHR = 'e'; UZEMCHR ='d'; UZEMCHR = ' ';
UZEMCHR = 'P'; UZEMCHR = 'l'; UZEMCHR = 'a'; UZEMCHR = 'y'; UZEMCHR = 'e'; UZEMCHR = 'r'; UZEMCHR = '\n';
#endif
if (p->invincible || p->dead)
return;
TriggerFx(FX_DEATH, 128, true);
p->dead = true;
p->update = Player_update;
if (IsSongPlaying())
StopSong();
}
static void DetectCollectablesAndHazards(PLAYER* const p, CAMERA* const c)
{
#define LOLLIPOP_Y_OFFSET (-2) // Hack so ceiling hazards feel more fair
int16_t roundedX = NEAREST_SCREEN_PIXEL(p->x) << FP_SHIFT;
uint8_t tx = P2HT(roundedX);
bool nx = (bool)NH(roundedX); // true if entity overlaps right
int16_t roundedY = (NEAREST_SCREEN_PIXEL(p->y) + LOLLIPOP_Y_OFFSET) << FP_SHIFT;
uint8_t ty = P2VT(roundedY);
#if (DEBUG_COLLECTABLES_AND_HAZARDS == 1)
static uint8_t prevTy;
if (prevTy != ty) {
UZEMCHR = 'H'; UZEMHEX = ty; UZEMCHR = '\n';
}
prevTy = ty;
#endif
// Don't do detection on things that are outside of valid VRAM if we fall into a pit
if (ty > c->y_max) {
#if (DEBUG_COLLECTABLES_AND_HAZARDS == 1)
UZEMCHR = 'R'; UZEMCHR = '\n';
#endif
return;
}
const bool ny = true; // 2 high, so always overlaps 1 below
bool nny = (bool)NV(roundedY); // true if entity overlaps 2 below
if ((roundedY - (LOLLIPOP_Y_OFFSET << FP_SHIFT)) < 0/* || p->dead*/) // offscreen or dead, except can't do dead check because if you die then land on
return; // a cloud with lollipop, the dissolving tile relies on the lollipop being collected
bool collected = false;
bool hazard = false;
uint8_t tile = GetTile(tx % VRAM_TILES_H, ty % VRAM_TILES_V);
if (IsTreasure(tile)) {
if (tile > TILE_LOLLIPOP_TOP_END)
ClearTreasure(p->level, (tx) - 1, (ty) - 1);
else
ClearTreasure(p->level, tx, ty);
collected = true;
} //else if (IsHazard(tile))
//hazard = true; // commented out so ceiling hazards aren't unfair
if (nx) {
uint8_t tile = GetTile((tx + 1) % VRAM_TILES_H, ty % VRAM_TILES_V);
if (IsTreasure(tile)) {
if (tile > TILE_LOLLIPOP_TOP_END)
ClearTreasure(p->level, ((tx + 1)) - 1, (ty) - 1);
else
ClearTreasure(p->level, (tx + 1), ty);
collected = true;
} //else if (IsHazard(tile))
//hazard = true; // commented out so ceiling hazards aren't unfair
}
if (ny && ((ty + 1) <= c->y_max)) {
uint8_t tile = GetTile(tx % VRAM_TILES_H, (ty + 1) % VRAM_TILES_V);
if (IsTreasure(tile)) {
if (tile > TILE_LOLLIPOP_TOP_END)
ClearTreasure(p->level, (tx) - 1, ((ty + 1)) - 1);
else
ClearTreasure(p->level, tx, (ty + 1));
collected = true;
} else if (IsHazard(tile))
hazard = true;
}
#if (DEBUG_COLLECTABLES_AND_HAZARDS == 1)
else if (ny && ((ty + 1) > c->y_max)) {
UZEMCHR = '1'; UZEMHEX = ty + 1; UZEMCHR = '\n';
}
#endif
if (nx && ny && ((ty + 1) <= c->y_max)) {
uint8_t tile = GetTile((tx + 1) % VRAM_TILES_H, (ty + 1) % VRAM_TILES_V);
if (IsTreasure(tile)) {
if (tile > TILE_LOLLIPOP_TOP_END)
ClearTreasure(p->level, ((tx + 1)) - 1, ((ty + 1)) - 1);
else
ClearTreasure(p->level, (tx + 1), (ty + 1));
collected = true;
} else if (IsHazard(tile))
hazard = true;
}
#if (DEBUG_COLLECTABLES_AND_HAZARDS == 1)
else if (nx && ny && ((ty + 1) > c->y_max)) {
UZEMCHR = '1'; UZEMHEX = ty + 1; UZEMCHR = '\n';
}
#endif
if (nny && ((ty + 2) <= c->y_max)) {
uint8_t tile = GetTile(tx % VRAM_TILES_H, (ty + 2) % VRAM_TILES_V);
if (IsTreasure(tile)) {
if (tile > TILE_LOLLIPOP_TOP_END)
ClearTreasure(p->level, (tx) - 1, ((ty + 2)) - 1);
else
ClearTreasure(p->level, tx, (ty + 2));
collected = true;
} else if (IsHazard(tile))
hazard = true;
}
#if (DEBUG_COLLECTABLES_AND_HAZARDS == 1)
else if (nny && ((ty + 2) > c->y_max)) {
UZEMCHR = '2'; UZEMHEX = ty + 2; UZEMCHR = '\n';
}
#endif
if (nx && nny && ((ty + 2) <= c->y_max)) {
uint8_t tile = GetTile((tx + 1) % VRAM_TILES_H, (ty + 2) % VRAM_TILES_V);
if (IsTreasure(tile)) {
if (tile > TILE_LOLLIPOP_TOP_END)
ClearTreasure(p->level, ((tx + 1)) - 1, ((ty + 2)) - 1);
else
ClearTreasure(p->level, (tx + 1), (ty + 2));
collected = true;
} else if (IsHazard(tile))
hazard = true;
}
#if (DEBUG_COLLECTABLES_AND_HAZARDS == 1)
else if (nx && nny && ((ty + 2) > c->y_max)) {
UZEMCHR = '2'; UZEMHEX = ty + 2; UZEMCHR = '\n';
}
#endif
if (collected && !p->dead) {
TriggerFx(FX_GULP, 128, true);
p->gulped = true;
p->gulpedAtExpires = GetVsyncCounter() + 8;
}
if (hazard && !p->dead) {
#if (DEBUG_PRINT_KILLED_BY == 1)
UZEMCHR = 'H'; UZEMCHR = 'a'; UZEMCHR = 'z'; UZEMCHR = 'a'; UZEMCHR = 'r'; UZEMCHR = 'd'; UZEMCHR = ' ';
#endif
KillPlayer(p);
}
}
__attribute__(( always_inline ))
static inline void ServicePlayerRequestedClopSoundFx(PLAYER* const p)
{
if (p->clopsound) {
if (p->frames_since_clop > 4) {
TriggerFx(FX_CLOP, p->clopvol, true);
p->frames_since_clop = 0;
}
p->clopsound = 0;
}
}
__attribute__(( always_inline ))
static inline void PlayerJustHitGroundFx(PLAYER* const p)
{
if (p->falling || p->jumping) {
if (p->dead) {
TriggerFx(FX_CLOP, 192, true);
p->frames_since_clop = 0;
} else {
if (p->frames_since_clop > 4) {
TriggerFx(FX_CLOP, 128, true);
p->frames_since_clop = 0;
}
}
} else
ServicePlayerRequestedClopSoundFx(p);
}
//__attribute__((optimize("O3")))
void Player_update_ladder(PLAYER* const p, CAMERA* const c, DISSOLVING_TILE* const dt, LEVEL* const l)
{
(void)c;
(void)dt;
(void)l;
bool wasLeft = (p->dx < 0);
bool wasRight = (p->dx > 0);
int16_t ddx = 0;
// When on a ladder, L and R movement has less acceleration, a lower top speed, and more friction.
// When also moving U or D, the L/R acceleration and L/R top speed is reduced even more. This makes
// it easier to remain on the ladder when using the SNES gamepad, where it is often difficult to
// press only U or D without accidentally pressing L or R at the same time.
if (p->left) {
if (p->up || p->down)
ddx -= WORLD_ACCEL / 2; // entity wants to go left
else
ddx -= WORLD_ACCEL * 2 / 3; // entity wants to go left
} else if (wasLeft)
ddx += WORLD_FRICTION * 2; // entity was going left, but not anymore
if (p->right) {
if (p->up || p->down)
ddx += WORLD_ACCEL / 2; // entity wants to go right
else
ddx += WORLD_ACCEL * 2 / 3; // entity wants to go right
} else if (wasRight)
ddx -= WORLD_FRICTION * 2; // entity was going right, but not anymore
if (p->left || p->right || wasLeft || wasRight) { // smaller and faster than 'if (ddx)'
// Integrate the X forces to calculate the new position (x,y) and the new velocity (dx,dy)
p->x += (p->dx / WORLD_FPS);
p->dx += (ddx / WORLD_FPS);
if (p->up || p->down) {
if (p->dx < -WORLD_MAXDX / 3)
p->dx = -WORLD_MAXDX / 3;
else if (p->dx > WORLD_MAXDX / 3)
p->dx = WORLD_MAXDX / 3;
} else {
if (p->dx < -WORLD_MAXDX / 2)
p->dx = -WORLD_MAXDX / 2;
else if (p->dx > WORLD_MAXDX / 2)
p->dx = WORLD_MAXDX / 2;
}
// Clamp horizontal velocity to zero if we detect that the entities direction has changed
if ((wasLeft && (p->dx > 0)) || (wasRight && (p->dx < 0)))
p->dx = 0; // clamp at zero to prevent friction from making the entity jiggle side to side
// TODO: Clamp X to within screen bounds if ladder could be at edge of screen (or just don't put ladders there!)
}
bool wasUp = (p->dy < 0);
bool wasDown = (p->dy > 0);
int16_t ddy = 0;
if (p->up)
ddy -= WORLD_ACCEL * 3 / 2; // entity wants to go up
else if (wasUp)
ddy += WORLD_FRICTION * 2; // entity was going up, but not anymore
if (p->down)
ddy += WORLD_ACCEL * 3 / 2; // entity wants to go down
else if (wasDown)
ddy -= WORLD_FRICTION * 2; // entity was going down, but not anymore
if (p->up || p->down || wasUp || wasDown) { // smaller and faster than 'if (ddy)'
// Integrate the Y forces to calculate the new position (x,y) and the new velocity (dx,dy)
p->y += (p->dy / WORLD_FPS);
p->dy += (ddy / WORLD_FPS);
if (p->dy < -WORLD_MAXDX)
p->dy = -WORLD_MAXDX;
else if (p->dy > WORLD_MAXDX)
p->dy = WORLD_MAXDX;
// Clamp vertical velocity to zero if we detect that the entities direction has changed
if ((wasUp && (p->dy > 0)) || (wasDown && (p->dy < 0)))
p->dy = 0; // clamp at zero to prevent friction from making the entity jiggle up and down
// TODO: Clamp Y to within screen bounds if ladder could be at edge of screen (or just don't put ladders there!)
}
DetectCollectablesAndHazards(p, c);
ServicePlayerRequestedClopSoundFx(p);
if (p->frames_since_clop <= 4)
p->frames_since_clop++;
// Collision detection for ladders
int16_t roundedX = NEAREST_SCREEN_PIXEL(p->x) << FP_SHIFT; // ignore subpixels for this calculation
uint8_t tx = P2HT(roundedX);
uint8_t ty = P2VT(p->y);
bool nx = (bool)NH(roundedX); // true if entity overlaps right
#if (PLAYER_TILE_HEIGHT == 1)
bool ny = (bool)NV(p->y); // true if entity overlaps below
#endif
#if (PLAYER_TILE_HEIGHT == 2)
bool ny = true;
bool nyy= (bool)NV(p->y); // true if entity overlaps below
#endif
// Check to see if the entity has left the ladder
if (!( IsLadder(GetTile((tx ) % VRAM_TILES_H, (ty ) % VRAM_TILES_V)) ||
(nx && IsLadder(GetTile((tx + 1) % VRAM_TILES_H, (ty ) % VRAM_TILES_V))) ||
(ny && IsLadder(GetTile((tx ) % VRAM_TILES_H, (ty + 1) % VRAM_TILES_V))) ||
(nx && ny && IsLadder(GetTile((tx + 1) % VRAM_TILES_H, (ty + 1) % VRAM_TILES_V)))
#if (PLAYER_TILE_HEIGHT == 2)
|| (nyy && IsLadder(GetTile((tx ) % VRAM_TILES_H, (ty + 2) % VRAM_TILES_V))) ||
(nx && nyy && IsLadder(GetTile((tx + 1) % VRAM_TILES_H, (ty + 2) % VRAM_TILES_V)))
#endif
)) {
p->update = Player_update;
p->animationFrameCounter = 0;
p->framesFalling = 0; // reset the counter so a grace jump is allowed if moving off the ladder causes the entity to fall
p->jumpReleased = true; // set this flag so attempting to jump off a ladder while moving up or down happens as soon as the entity leaves the ladder
p->clopsound = 0;
}
// Allow jumping off ladder if the entity is not moving up or down,
// but don't allow jumping if up or down is being held, because the
// entity would immediately rejoin the ladder
if (p->jump && (!(p->up || p->down || p->left || p->right))) { //
p->jumping = p->falling = false; // ensure jump happens when calling player_update
p->jump = true;
p->update = Player_update;
p->animationFrameCounter = 0;
}
}
__attribute__(( always_inline ))
static inline bool IsSolidForEntity(const uint8_t tx, const uint8_t ty, const int16_t y, const int16_t prevY, const bool down)
{
// One-way tiles are only solid for Y collisions where your previous Y
// puts your feet above the tile, and you're not currently pressing down
uint8_t t = GetTile(tx, ty);
return (IsSolid(t) || (IsOneWay(t) && !down && ((prevY - 1) < VT2P(P2VT(y)))));
}
//__attribute__((optimize("O3")))
void Player_update(PLAYER* const p, CAMERA* const c, DISSOLVING_TILE* const dt, LEVEL* const l)
{
bool wasLeft = (p->dx < 0);
bool wasRight = (p->dx > 0);
int16_t ddx = 0;
if (p->dead) {
p->jumpReleased = true;
p->attacking = false;
p->attackReleased = true;
p->left = 0;
p->right = 0;
p->up = 0;
p->down = 0;
p->jump = 0;
p->clopsound = 0;
} else if (p->gulped) {
uint8_t now = GetVsyncCounter();
if (p->gulpedAtExpires <= now)
p->gulped = false;
}
if (p->left) {
if (p->jumping || p->falling)
ddx -= WORLD_ACCEL * 5 / 5; // make fraction < 1 for in air inertia
else
ddx -= WORLD_ACCEL; // entity wants to go left
} else if (wasLeft) {
if (p->jumping || p->falling)
ddx += WORLD_FRICTION * 5 / 5;
else
ddx += WORLD_FRICTION; // entity was going left, but not anymore
}
if (p->right) {
if (p->jumping || p->falling)
ddx += WORLD_ACCEL * 5 / 5;
else
ddx += WORLD_ACCEL; // entity wants to go right
} else if (wasRight) {
if (p->jumping || p->falling)
ddx -= WORLD_FRICTION * 5 / 5;
else
ddx -= WORLD_FRICTION; // entity was going right, but not anymore
}
BUILD_BUG_IF(NOT_POWER_OF_TWO(WORLD_FPS));
// Integrate the X forces to calculate the new position (x,y) and the new velocity (dx,dy)
p->x += (p->dx / WORLD_FPS);
p->dx += (ddx / WORLD_FPS);
if (p->dx < -WORLD_MAXDX)
p->dx = -WORLD_MAXDX;
else if (p->dx > WORLD_MAXDX)
p->dx = WORLD_MAXDX;
// Don't allow X to be within 2 tiles of the x min and x max level bounds
#define LEVEL_BOUNDARY_TILES_H (2)
// We can't clamp to screen bounds in mode 3 scrolling, but we should clamp within LEVEL_BOUNDARY_TILES_H tiles of the level min and max
if (p->x > HT2P((int16_t)p->level->width - (LEVEL_BOUNDARY_TILES_H + PLAYER_TILE_WIDTH))) {
p->x = HT2P((int16_t)p->level->width - (LEVEL_BOUNDARY_TILES_H + PLAYER_TILE_WIDTH));
p->dx = 0;
} else if (p->x < HT2P(LEVEL_BOUNDARY_TILES_H)) {
p->x = HT2P(LEVEL_BOUNDARY_TILES_H);
p->dx = 0;
}
// Clamp horizontal velocity to zero if we detect that the direction has changed
if ((wasLeft && (p->dx > 0)) || (wasRight && (p->dx < 0))) {
p->dx = 0; // clamp at zero to prevent friction from making the entity jiggle side to side
p->animationFrameCounter = 0; // xxx added this 3
}
uint8_t tx;
bool nx;
uint8_t ty;
bool ny;
bool cell;
bool cellright;
bool celldown;
bool celldiag;
// If we jump off the top of the screen, don't test for X collisions
if (p->y >= -VT2P(PLAYER_TILE_HEIGHT)) {
// Collision Detection for X
tx = P2HT(p->x);
nx = (bool)NH(p->x); // true if entity overlaps right
ty = P2VT(p->y);
ny = (bool)NV(p->y); // true if entity overlaps below
cell = IsSolid(GetTile(tx % VRAM_TILES_H, ty % VRAM_TILES_V)) && (p->y >= 0);
#if PLAYER_TILE_HEIGHT == 2
cell |= IsSolid(GetTile(tx % VRAM_TILES_H, (ty + /* extra tile */ 1) % VRAM_TILES_V)) && (p->y >= -VT2P(1)); // because we are 2 tiles tall
#endif
cellright = IsSolid(GetTile((tx + PLAYER_TILE_WIDTH) % VRAM_TILES_H, ty % VRAM_TILES_V)) && (p->y >= 0);
#if PLAYER_TILE_HEIGHT == 2
cellright |= IsSolid(GetTile((tx + PLAYER_TILE_WIDTH) % VRAM_TILES_H, (ty + /* extra tile */ 1) % VRAM_TILES_V)) && (p->y >= -VT2P(1)); // because we are 2 tiles tall
#endif
celldown = IsSolid(GetTile(tx % VRAM_TILES_H, (ty + PLAYER_TILE_HEIGHT) % VRAM_TILES_V));
celldiag = IsSolid(GetTile((tx + PLAYER_TILE_WIDTH) % VRAM_TILES_H, (ty + PLAYER_TILE_HEIGHT) % VRAM_TILES_V));
//if (p->dx >= 0) { // Can't do this check with HMPs
if ((nx && cellright && !cell) || // nx check avoids potential glitch when moving off ladder
(ny && celldiag && !celldown)) {
p->x = HT2P(P2HT(p->x)); // clamp the x position to avoid moving into the platform just hit
p->dx = 0; // stop horizontal velocity
}
//}
//if (p->dx <= 0) { // Can't do this check with HMPs
if ((nx && cell && !cellright) || // nx check avoids potential glitch when moving off ladder
(ny && celldown && !celldiag)) {
p->x = HT2P(P2HT(p->x) + 1); // clamp the x position to avoid moving into the platform just hit
p->dx = 0; // stop horizontal velocity
}
//}
}
int16_t ddy = WORLD_GRAVITY;
if (p->jump && !p->jumping && !(p->falling ? (p->framesFalling > WORLD_FALLING_GRACE_FRAMES) : false)) {
TriggerFx(FX_JUMP, 56, true);
if (p->dy > 0)
p->dy = 0; // if falling down, reset vertical velocity so jumps during grace period are consistent with jumps from ground
ddy -= WORLD_JUMP; // apply an instantaneous (large) vertical impulse
p->jumping = true;
p->jumpReleased = false;
p->jumpCounter = 0;
#if (DEBUG_MIN_JUMP_HEIGHT == 1)
p->debugJumpCounter = 0;
#endif
}
#if (DEBUG_MIN_JUMP_HEIGHT == 1)
// useful to see how long jumps are being held for
if (p->jumping && !p->jumpReleased && p->debugJumpCounter < 255 && p->dy <= 0) {
p->debugJumpCounter++;
UZEMHEX = p->debugJumpCounter;
}
#endif
if (p->jumping && p->jumpCounter < 255)
p->jumpCounter++;
// Variable height jumping (with a jumpCounter enforced minimum jump height)
if (p->jumpCounter > 3 && (p->jumping && p->jumpReleased && (p->dy < -WORLD_CUT_JUMP_SPEED_LIMIT)))
p->dy = -WORLD_CUT_JUMP_SPEED_LIMIT;
// Integrate the Y forces to calculate the new position (x,y) and the new velocity (dx,dy)
int16_t prevY = p->y; // cache previous Y value for one-way tiles
p->y += (p->dy / WORLD_FPS);
p->dy += (ddy / WORLD_FPS);
if (p->dy < -WORLD_MAXDY)
p->dy = -WORLD_MAXDY;
else if (p->dy > WORLD_MAXDY)
p->dy = WORLD_MAXDY;
// We can't clamp to screen bounds in mode 3 scrolling, so either skip collision detection or kill player
if (p->y < 0)
return;
else if (p->y >= (int16_t)VT2P(p->level->height + 1)) { // the + 1 allows the sprite to fall all the way off screen
p->y = (int16_t)VT2P(p->level->height + 1); // at the expense of calling DetectCollectablesAndHazards
p->dy = 0; // when we don't need to, but it will bail early.
if (!p->dead) {
#if (DEBUG_PRINT_KILLED_BY == 1)
UZEMCHR = 'P'; UZEMCHR = 'i'; UZEMCHR = 't'; UZEMCHR = ' ';
#endif
KillPlayer(p);
}
return;
}
DetectCollectablesAndHazards(p, c);
// Collision Detection for Y (uses rounded X so if it looks like the entity should fall through a one-tile-wide hole, it will)
int16_t roundedX = NEAREST_SCREEN_PIXEL(p->x) << FP_SHIFT;
tx = P2HT(roundedX);
nx = (bool)NH(roundedX); // true if entity overlaps right
ty = P2VT(p->y);
// For entities more than one tile high, we want to compare the bottom-most tile in the sprite with the one-way platform
if (p->dy >= 0) { // Entity going down (use bottom-most tile)
cell = IsSolidForEntity(tx % VRAM_TILES_H,
(ty + PLAYER_TILE_HEIGHT - 1) % VRAM_TILES_V,
p->y + VT2P(PLAYER_TILE_HEIGHT - 1),
prevY + VT2P(PLAYER_TILE_HEIGHT - 1), p->down);
cellright = IsSolidForEntity((tx + PLAYER_TILE_WIDTH) % VRAM_TILES_H,
(ty + PLAYER_TILE_HEIGHT - 1) % VRAM_TILES_V,
p->y + VT2P(PLAYER_TILE_HEIGHT - 1),
prevY + VT2P(PLAYER_TILE_HEIGHT - 1), p->down);
celldown = IsSolidForEntity(tx % VRAM_TILES_H,
(ty + PLAYER_TILE_HEIGHT) % VRAM_TILES_V,
p->y + VT2P(PLAYER_TILE_HEIGHT),
prevY + VT2P(PLAYER_TILE_HEIGHT), p->down);
celldiag = IsSolidForEntity((tx + PLAYER_TILE_WIDTH) % VRAM_TILES_H,
(ty + PLAYER_TILE_HEIGHT) % VRAM_TILES_V,
p->y + VT2P(PLAYER_TILE_HEIGHT),
prevY + VT2P(PLAYER_TILE_HEIGHT), p->down);
} else if (p->dy < 0) { // Entity going up (use top-most tile)
cell = IsSolid(GetTile(tx % VRAM_TILES_H, ty % VRAM_TILES_V));
cellright = IsSolid(GetTile((tx + PLAYER_TILE_WIDTH) % VRAM_TILES_H, ty % VRAM_TILES_V));
celldown = IsSolid(GetTile(tx % VRAM_TILES_H, (ty + PLAYER_TILE_HEIGHT) % VRAM_TILES_V));
celldiag = IsSolid(GetTile((tx + PLAYER_TILE_WIDTH) % VRAM_TILES_H, (ty + PLAYER_TILE_HEIGHT) % VRAM_TILES_V));
}
if (p->dy > 0) {
if (( celldown && !cell) ||
(nx && celldiag && !cellright)) {
p->y = VT2P(P2VT(p->y)); // clamp the y position to avoid falling into platform below
#if (PLAYER_TILE_HEIGHT == 2)
// extra check to avoid clipping into wall if exiting ladder
if (IsSolidForEntity(tx % VRAM_TILES_H, (P2VT(p->y) + 1) % VRAM_TILES_V, p->y, prevY, p->down))
p->y += VT2P(PLAYER_TILE_HEIGHT - 1);
#endif
p->dy = 0; // stop downward velocity
// We just hit the ground
PlayerJustHitGroundFx(p);
////////////////////////////////////////////////////////////////////////////////
// Handle Dissolving platforms
// TODO: Call DissolvingTile_add in the opposite order if the player is facing left
if (celldown) {
uint8_t tile = GetTile(tx % VRAM_TILES_H, (ty + PLAYER_TILE_HEIGHT) % VRAM_TILES_V);
// XXX: It would be easy to detect standing on hazards here and below
if ((tile >= TILE_START_CLOUD_PLATFORM) && (tile <= TILE_END_CLOUD_PLATFORM)) {
#if (DEBUG_DISSOLVING_TILE_ANIMATIONS > 2)
UZEMCHR = 'T'; UZEMHEX = tile; UZEMCHR = '\n';
#endif
uint16_t xy_index = MAKEWORD(tx, (ty + PLAYER_TILE_HEIGHT));
DissolvingTile_add(dt, xy_index, DISSOLVING_TILE_STANDARD_EXPIRATION);
SetTile(tx % VRAM_TILES_H, (ty + PLAYER_TILE_HEIGHT) % VRAM_TILES_V, DissolvingTile_getNextTile(tile));
uint8_t tileabove = GetTile(tx % VRAM_TILES_H, (ty + PLAYER_TILE_HEIGHT - 1) % VRAM_TILES_V);
SetTile(tx % VRAM_TILES_H, (ty + PLAYER_TILE_HEIGHT - 1) % VRAM_TILES_V, DissolvingTile_getNextTile(tileabove));
}
}
if (nx && celldiag) {
uint8_t tile = GetTile((tx + PLAYER_TILE_WIDTH) % VRAM_TILES_H, (ty + PLAYER_TILE_HEIGHT) % VRAM_TILES_V);
// XXX: It would be easy to detect standing on hazards here and above
if ((tile >= TILE_START_CLOUD_PLATFORM) && (tile <= TILE_END_CLOUD_PLATFORM)) {
#if (DEBUG_DISSOLVING_TILE_ANIMATIONS > 2)
UZEMCHR = 'T'; UZEMHEX = tile; UZEMCHR = '\n';
#endif
uint16_t xy_index = MAKEWORD((tx + PLAYER_TILE_WIDTH), (ty + PLAYER_TILE_HEIGHT));
DissolvingTile_add(dt, xy_index, DISSOLVING_TILE_STANDARD_EXPIRATION);
SetTile((tx + PLAYER_TILE_WIDTH) % VRAM_TILES_H, (ty + PLAYER_TILE_HEIGHT) % VRAM_TILES_V, DissolvingTile_getNextTile(tile));
uint8_t tileabove = GetTile((tx + PLAYER_TILE_WIDTH) % VRAM_TILES_H, (ty + PLAYER_TILE_HEIGHT - 1) % VRAM_TILES_V);
SetTile((tx + PLAYER_TILE_WIDTH) % VRAM_TILES_H, (ty + PLAYER_TILE_HEIGHT - 1) % VRAM_TILES_V, DissolvingTile_getNextTile(tileabove));
}
}
////////////////////////////////////////////////////////////////////////////////
p->jumping = false; // no longer jumping
p->framesFalling = 0;
}
} else if (p->dy < 0) {
if (( cell && !celldown) ||
(nx && cellright && !celldiag)) {
p->y = VT2P(P2VT(p->y) + 1); // clamp the y position to avoid jumping into platform above
p->dy = 0; // stop upward velocity
}
}
////////////////////////////////////////////////////////////////////////////////
// Collision Detection with Moving Platforms (considered sky tiles)
bool wasOnMovingPlatform = p->onMovingPlatform;
uint8_t wasPlatformId = (wasOnMovingPlatform) ? p->platformId : 0xFF;
p->onMovingPlatform = false;
if (p->dy >= 0) { // Going down
for (uint8_t i = 0; i < l->numSpawned; ++i) {
uint8_t spawn_type = pgm_read_byte(&ISD_spawn_type[l->entity[i].id]);
if (spawn_type < ST_HORIZONTAL_PLATFORM)
continue;
if (spawn_type == ST_HORIZONTAL_PLATFORM) {
if (NEAREST_SCREEN_PIXEL(p->x) > NEAREST_SCREEN_PIXEL(l->entity[i].x - HT2P(PLAYER_TILE_WIDTH)) &&
NEAREST_SCREEN_PIXEL(p->x) < NEAREST_SCREEN_PIXEL(l->entity[i].x + HT2P(HORIZONTAL_PLATFORM_TILE_WIDTH)) &&
prevY <= l->entity[i].y - VT2P(PLAYER_TILE_HEIGHT) + HORIZONTAL_PLATFORM_TOP_OFFSET_SUBPIXELS &&
p->y >= l->entity[i].y - VT2P(PLAYER_TILE_HEIGHT) + HORIZONTAL_PLATFORM_TOP_OFFSET_SUBPIXELS &&
!p->down) {
p->y = l->entity[i].y - VT2P(PLAYER_TILE_HEIGHT) + HORIZONTAL_PLATFORM_TOP_OFFSET_SUBPIXELS;
p->dy = 0; // stop downward velocity
// We just hit the ground
PlayerJustHitGroundFx(p);
p->jumping = false; // no longer jumping
p->falling = false;
p->framesFalling = 0;
p->onMovingPlatform = true;
p->platformId = l->entity[i].id;
/* if (p->buttons.held & BTN_SL) { */
/* int16_t diff = p->x - l->entity[i].x; */
/* UZEMHEX = HI8(diff); UZEMHEX = LO8(diff); UZEMCHR = '\n'; */
/* } */
break;
}
} else /*if (spawn_type == ST_VERTICAL_PLATFORM)*/{
#if 0
UZEMCHR = '-'; UZEMCHR = '-'; UZEMCHR = '-'; UZEMCHR = '-'; UZEMCHR = '\n';
UZEMCHR = 'p'; UZEMHEX = HI8(prevY); UZEMHEX = LO8(prevY); UZEMCHR = '\n';
UZEMCHR = 'e'; UZEMHEX = HI8(l->entity[i].y - VT2P(PLAYER_TILE_HEIGHT) + HORIZONTAL_PLATFORM_TOP_OFFSET_SUBPIXELS); UZEMHEX = LO8(l->entity[i].y - VT2P(PLAYER_TILE_HEIGHT) + HORIZONTAL_PLATFORM_TOP_OFFSET_SUBPIXELS); UZEMCHR = '\n';
UZEMCHR = 'y'; UZEMHEX = HI8(p->y); UZEMHEX = LO8(p->y); UZEMCHR = '\n';
#endif
if ((wasOnMovingPlatform &&
(wasPlatformId == l->entity[i].id) &&
(!p->down) &&
(NEAREST_SCREEN_PIXEL(p->x) > NEAREST_SCREEN_PIXEL(l->entity[i].x - HT2P(PLAYER_TILE_WIDTH)) &&
(NEAREST_SCREEN_PIXEL(p->x) < NEAREST_SCREEN_PIXEL(l->entity[i].x + HT2P(HORIZONTAL_PLATFORM_TILE_WIDTH))))) ||
(NEAREST_SCREEN_PIXEL(p->x) > NEAREST_SCREEN_PIXEL(l->entity[i].x - HT2P(PLAYER_TILE_WIDTH)) &&
(NEAREST_SCREEN_PIXEL(p->x) < NEAREST_SCREEN_PIXEL(l->entity[i].x + HT2P(HORIZONTAL_PLATFORM_TILE_WIDTH))) &&
(prevY <= l->entity[i].dx /* dx contains the VMP's previous Y value */ - VT2P(PLAYER_TILE_HEIGHT) + HORIZONTAL_PLATFORM_TOP_OFFSET_SUBPIXELS) &&
(p->y >= l->entity[i].y - VT2P(PLAYER_TILE_HEIGHT) + HORIZONTAL_PLATFORM_TOP_OFFSET_SUBPIXELS) &&
(!p->down))) {
p->y = l->entity[i].y - VT2P(PLAYER_TILE_HEIGHT) + HORIZONTAL_PLATFORM_TOP_OFFSET_SUBPIXELS;
p->dy = 0; // stop downward velocity
// We just hit the ground
PlayerJustHitGroundFx(p);
p->jumping = false; // no longer jumping
p->falling = false;
p->framesFalling = 0;
p->onMovingPlatform = true;
p->platformId = l->entity[i].id;
break;
} else if (p->down)
p->onMovingPlatform = false;
}
}
if (!p->onMovingPlatform)
p->falling = !(celldown || (nx && celldiag)) && !p->jumping; // detect if we're now falling or not
}
////////////////////////////////////////////////////////////////////////////////
if (p->frames_since_clop <= 4)
p->frames_since_clop++;
if (p->falling && p->framesFalling <= WORLD_FALLING_GRACE_FRAMES)
p->framesFalling++;
// Collision detection with ladders
if (p->up || p->down) {
if (p->down) {
ty = P2VT(p->y + 1 + VT2P(PLAYER_TILE_HEIGHT - 1)); // the tile one sub-sub-pixel below current position
ny = (bool)NV(p->y + 1); // true if player overlaps below
} else { // p->up
ty = P2VT(p->y - 1); // the tile one sub-sub-pixel above current position
ny = (bool)NV(p->y - 1); // true if entity overlaps above
}
if (( IsLadder(GetTile((tx ) % VRAM_TILES_H, (ty ) % VRAM_TILES_V))) || // cell
(nx && IsLadder(GetTile((tx + 1) % VRAM_TILES_H, (ty ) % VRAM_TILES_V))) || // cellright
(ny && IsLadder(GetTile((tx ) % VRAM_TILES_H, (ty + 1) % VRAM_TILES_V))) || // celldown
(nx && ny && IsLadder(GetTile((tx + 1) % VRAM_TILES_H, (ty + 1) % VRAM_TILES_V)))) { // celldiag
if (p->down)
p->y++; // allow entity to join a ladder directly below them
else // p->up
p->y--; // allow entity to join a ladder directly above them
p->update = Player_update_ladder;
p->animationFrameCounter = 0;
p->jumping = false;
p->falling = false;
p->dx = p->dy = 0;
}
}
}
void Player_render(PLAYER* const p, CAMERA* const c)
{
const int8_t *map;
const int8_t *prevmap = p->map;
if (p->dead) {
map = blitmap_run1;
if (p->y >= (int16_t)VT2P(p->level->height + 1))
return;
BlitMyBlitMap(p->flags | SPRITE_FLIP_Y | SPRITE_BANK0,
map, BLITMAP_HEAD_NORMAL, (p->gulped) ? BLITMAP_HEAD_GULP : BLITMAP_HEAD_NORMAL,
NEAREST_SCREEN_PIXEL(p->x) - c->x,
NEAREST_SCREEN_PIXEL(p->y) - c->y + 4);
return;
}
if (p->attacking) {
map = pgm_read_ptr_near(&attack_maps[pgm_read_byte(&playerAttackAnimation[p->animationFrameCounter / PLAYER_ATTACK_ANIMATION_FRAME_SKIP])]);
BUILD_BUG_IF(NOT_POWER_OF_TWO(PLAYER_ATTACK_ANIMATION_FRAME_SKIP * NELEMS(playerAttackAnimation)));
p->animationFrameCounter = (p->animationFrameCounter + 1) & (PLAYER_ATTACK_ANIMATION_FRAME_SKIP * NELEMS(playerAttackAnimation) - 1);
if (p->attackingLeft)
p->flags = 0;
else
p->flags = SPRITE_FLIP_X;
} else if (p->update == Player_update) {
if (p->jumping || p->falling /*|| e->update == entity_update_flying*/) {
if (p->dy > 100)
map = blitmap_run2;
else if (p->dy < -100)
map = blitmap_run0;
else
map = blitmap_run1;
p->animationFrameCounter = 0; // xxx added this 2
} else {
if (!p->left && !p->right && p->dx == 0) {
map = blitmap_walk0;
p->animationFrameCounter = 0; // xxx added this
} else {
// Rather than always using the run maps when turbo is true, base it off the actual speed
if ((p->dx >= WORLD_MAXDX) || (p->dx <= -WORLD_MAXDX)) {
map = pgm_read_ptr_near(&run_maps[pgm_read_byte(&playerAnimation[p->animationFrameCounter / PLAYER_ANIMATION_FRAME_SKIP])]);
if ((map == blitmap_run0 && prevmap != blitmap_run0) || (map == blitmap_run2 && prevmap != blitmap_run2)) {
// Schedule the clop sound to play during Player_update, so the louder hitting the ground
// sound from a jump/fall has a higher priority and to enforce the minimum time between when
// the clop sound is allowed to play so it doesn't sound weird.
p->clopvol = 80;
p->clopsound = 1;
}
} else {
map = pgm_read_ptr_near(&walk_maps[pgm_read_byte(&playerAnimation[p->animationFrameCounter / PLAYER_ANIMATION_FRAME_SKIP])]);
if (map == blitmap_walk2 && prevmap != blitmap_walk2) {
// Schedule the clop sound to play during Player_update, so the louder hitting the ground
// sound from a jump/fall has a higher priority and to enforce the minimum time between when
// the clop sound is allowed to play so it doesn't sound weird.
p->clopvol = 48;
p->clopsound = 1;
}
}
BUILD_BUG_IF(NOT_POWER_OF_TWO(PLAYER_ANIMATION_FRAME_SKIP * NELEMS(playerAnimation)));
p->animationFrameCounter = (p->animationFrameCounter + 1) & (PLAYER_ANIMATION_FRAME_SKIP * NELEMS(playerAnimation) - 1);
}
}
if (p->left)
p->flags = 0;
else if (p->right)
p->flags = SPRITE_FLIP_X;
} else /* if (p->update == Player_update_ladder)*/ {
if (!p->up && !p->down && !p->left && !p->right && p->dx == 0 && p->dy == 0) {
map = blitmap_ladder_idle;
p->animationFrameCounter = 0;
} else {
map = pgm_read_ptr_near(&climb_maps[pgm_read_byte(&playerLadderAnimation[p->animationFrameCounter / PLAYER_ANIMATION_FRAME_SKIP])]);
if (map != prevmap) {
p->clopvol = 48;
p->clopsound = 1;
}
if (++p->animationFrameCounter == PLAYER_ANIMATION_FRAME_SKIP * NELEMS(playerLadderAnimation))
p->animationFrameCounter = 0;
}
if (p->left)
p->flags = 0;
else if (p->right)
p->flags = SPRITE_FLIP_X;
}
if (p->y + VT2P(PLAYER_TILE_HEIGHT) <= 0) { // Draw an arrow pointing to the unicorn if it is above the screen
p->visible = false;
MyBlitSprite(0,
PLAYER_OFFSCREEN_ARROW_SPRITE,
NEAREST_SCREEN_PIXEL(p->x) - c->x,
0);
} else {
p->visible = true;
// Round x and y to the nearest whole pixel for rendering purposes only
BlitMyBlitMap(p->flags | SPRITE_BANK0 | SPRITE_OFF,
map, BLITMAP_HEAD_NORMAL, (p->gulped) ? BLITMAP_HEAD_GULP : BLITMAP_HEAD_NORMAL,
NEAREST_SCREEN_PIXEL(p->x) - c->x,
NEAREST_SCREEN_PIXEL(p->y) - c->y + (!!(p->attacking) * !!(p->update == Player_update_ladder)));
p->map = map;
}
}
////////////////////////////////////////////////////////////////////////////////
// End Player Operations
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// Begin Entity Operations
////////////////////////////////////////////////////////////////////////////////
#define ENTITY_WIDTH (1)
#define ENTITY_HEIGHT (1)
static inline bool overlap(const uint16_t x1, const uint16_t y1, const uint8_t w1, const uint8_t h1,
const uint16_t x2, const uint16_t y2, const uint8_t w2, const uint8_t h2);
const uint8_t undulate[] PROGMEM = {
0x10,0x11,0x12,0x12,0x13,0x14,0x15,0x15,0x16,0x17,0x18,0x18,0x19,0x1a,0x1a,0x1b,
0x1b,0x1c,0x1c,0x1d,0x1d,0x1e,0x1e,0x1e,0x1f,0x1f,0x1f,0x20,0x20,0x20,0x20,0x20,
0x20,0x20,0x20,0x20,0x20,0x20,0x1f,0x1f,0x1f,0x1e,0x1e,0x1e,0x1d,0x1d,0x1c,0x1c,
0x1b,0x1b,0x1a,0x1a,0x19,0x18,0x18,0x17,0x16,0x15,0x15,0x14,0x13,0x12,0x12,0x11,
0x10,0x0f,0x0e,0x0e,0x0d,0x0c,0x0b,0x0b,0x0a,0x09,0x08,0x08,0x07,0x06,0x06,0x05,
0x05,0x04,0x04,0x03,0x03,0x02,0x02,0x02,0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x02,0x02,0x02,0x03,0x03,0x04,0x04,
0x05,0x05,0x06,0x06,0x07,0x08,0x08,0x09,0x0a,0x0b,0x0b,0x0c,0x0d,0x0e,0x0e,0x0f,
};
void ai_fly_horizontal(ENTITY* const e)
{
if (e->left) {
int16_t xBound = (int16_t)pgm_read_word(&ISD_x[e->id]) + HT2P((int8_t)pgm_read_byte(&ISD_lo_bound[e->id]));
if (e->x <= xBound) {
e->left = false;
e->right = true;
}
} else if (e->right) {
int16_t xBound = (int16_t)pgm_read_word(&ISD_x[e->id]) + HT2P((int8_t)pgm_read_byte(&ISD_hi_bound[e->id]));
if (e->x >= xBound) {
e->right = false;
e->left = true;
}
}
}
void ai_fly_horizontal_undulate(ENTITY* const e)
{
ai_fly_horizontal(e);
e->y = (int16_t)pgm_read_word(&ISD_y[e->id]) + pgm_read_byte(&undulate[e->counter % NELEMS(undulate)]) - 16;
e->counter += 4;
}
void ai_fly_horizontal_erratic(ENTITY* const e)
{
ai_fly_horizontal(e);
//__asm__ __volatile__ ("wdr");
// Try to set the phase to something unique to this entity
int16_t x = (int16_t)pgm_read_word(&ISD_x[e->id]);
int16_t y = (int16_t)pgm_read_word(&ISD_y[e->id]);
uint8_t phase = (uint8_t)(e->id + x + y) * 16;
e->y = y +
(uint8_t)(pgm_read_byte(&undulate[(uint8_t)(phase + e->counter) % NELEMS(undulate)]) << 1) - 32 +
(uint8_t)(pgm_read_byte(&undulate[(uint8_t)(phase + 64 + (e->counter << 2)) % NELEMS(undulate)])) - 16 +
(uint8_t)(pgm_read_byte(&undulate[(uint8_t)(phase + 8 + (e->counter << 3)) % NELEMS(undulate)]) >> 1) - 8;
e->counter++;
//__asm__ __volatile__ ("wdr");
}
const int8_t sine_table[] PROGMEM =
{
0, 3, 6, 9, 12, 15, 18, 21, 25, 28, 31, 34, 37, 40, 43, 46, 49, 52, 54, 57, 60, 63, 66, 68, 71, 73, 76, 79, 81, 83, 86, 88, 90, 92, 95, 97, 99, 101, 103, 104, 106, 108, 110, 111, 113, 114, 115, 117, 118, 119, 120, 121, 122, 123, 124, 125, 125, 126, 126, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 126, 126, 125, 125, 124, 123, 123, 122, 121, 120, 119, 117, 116, 115, 113, 112, 110, 109, 107, 105, 104, 102, 100, 98, 96, 94, 91, 89, 87, 85, 82, 80, 77, 75, 72, 70, 67, 64, 61, 59, 56, 53, 50, 47, 44, 41, 38, 35, 32, 29, 26, 23, 20, 17, 14, 11, 7, 4, 1, -1, -4, -7, -11, -14, -17, -20, -23, -26, -29, -32, -35, -38, -41, -44, -47, -50, -53, -56, -59, -61, -64, -67, -70, -72, -75, -77, -80, -82, -85, -87, -89, -91, -94, -96, -98, -100, -102, -104, -105, -107, -109, -110, -112, -113, -115, -116, -117, -119, -120, -121, -122, -123, -123, -124, -125, -125, -126, -126, -127, -127, -127, -127, -127, -127, -127, -127, -127, -127, -126, -126, -125, -125, -124, -123, -122, -121, -120, -119, -118, -117, -115, -114, -113, -111, -110, -108, -106, -104, -103, -101, -99, -97, -95, -92, -90, -88, -86, -83, -81, -79, -76, -73, -71, -68, -66, -63, -60, -57, -54, -52, -49, -46, -43, -40, -37, -34, -31, -28, -25, -21, -18, -15, -12, -9, -6, -3, 0,
};
// smooth but a bit slower
void ai_fly_horizontal_erraticS(ENTITY* const e)
{
ai_fly_horizontal(e);
//__asm__ __volatile__ ("wdr");
// Try to set the phase to something unique to this entity
int16_t x = (int16_t)pgm_read_word(&ISD_x[e->id]);
int16_t y = (int16_t)pgm_read_word(&ISD_y[e->id]);
uint8_t phase = (uint8_t)(e->id + x + y) * 16;
e->y = y + (((((int8_t)pgm_read_byte(&sine_table[(uint8_t)(phase + (e->counter )) % NELEMS(sine_table)])) +
((int8_t)pgm_read_byte(&sine_table[(uint8_t)(phase + 128 + (e->counter << 2)) % NELEMS(sine_table)]) >> 1) +
((int8_t)pgm_read_byte(&sine_table[(uint8_t)(phase + 16 + (e->counter << 3)) % NELEMS(sine_table)]) >> 2))) >> 2 /* change this to 1 or 2*/);
//UZEMHEX = HI8(e->y); UZEMHEX = LO8(e->y); UZEMCHR = '\n';
e->counter += 2 /* change this to 1 or 2*/;
//__asm__ __volatile__ ("wdr");
}
void ai_fly_vertical(ENTITY* const e)
{
if (e->up) {
int16_t yBound = (int16_t)pgm_read_word(&ISD_y[e->id]) + VT2P((int8_t)pgm_read_byte(&ISD_lo_bound[e->id]));
if (e->y <= yBound) {
e->up = false;
e->down = true;
}
} else if (e->down) {
int16_t yBound = (int16_t)pgm_read_word(&ISD_y[e->id]) + VT2P((int8_t)pgm_read_byte(&ISD_hi_bound[e->id]));
if (e->y >= yBound) {
e->down = false;
e->up = true;
}
}
}
void ai_fly_vertical_undulate(ENTITY* const e)
{
ai_fly_vertical(e);
e->x = (int16_t)pgm_read_word(&ISD_x[e->id]) + (pgm_read_byte(&undulate[e->counter % NELEMS(undulate)]) >> 1) - 8;
e->counter += 4;
}
void ai_fly_vertical_erratic(ENTITY* const e)
{
ai_fly_vertical(e);
// Try to set the phase to something unique to this entity
int16_t x = (int16_t)pgm_read_word(&ISD_x[e->id]);
int16_t y = (int16_t)pgm_read_word(&ISD_y[e->id]);
uint8_t phase = (e->id + x + y) * 16;
e->x = x + (pgm_read_byte(&undulate[(phase + e->counter) % NELEMS(undulate)])) - 16 +
(pgm_read_byte(&undulate[(phase + 64 + ((e->counter << 1) << 1)) % NELEMS(undulate)]) >> 1) - 8 +
(pgm_read_byte(&undulate[(phase + 8 + ((e->counter << 2) << 1)) % NELEMS(undulate)]) >> 2) - 4;
e->counter++;
}
void ai_fly_circle(ENTITY* const e, const int8_t direction, const uint8_t counter)
{
// You can set the phase based on the counter you pass in, the counter should be incremented externally
uint8_t radius = pgm_read_byte(&ISD_hi_bound[e->id]);
int16_t x = (int16_t)pgm_read_word(&ISD_x[e->id]);
int16_t y = (int16_t)pgm_read_word(&ISD_y[e->id]);
e->x = x + ((int16_t)radius * (int8_t)pgm_read_byte(&sine_table[(uint8_t)(counter + 64 * direction)])) / 128;
e->y = y + ((int16_t)radius * (int8_t)pgm_read_byte(&sine_table[(uint8_t)(counter )])) / 128;
// The following code block is for rendering purposes only, and it assumes the entity's update function is null_update, or ignores left/right
if (direction > 0) {
e->left = true;
e->right = false;
} else if (direction < 0) {
e->left = false;
e->right = true;
}
}
void ai_fly_circle_cw(ENTITY* e, const uint8_t counter)
{
ai_fly_circle(e, 1, counter);
}
void ai_fly_circle_ccw(ENTITY* e, const uint8_t counter)
{
ai_fly_circle(e, -1, counter);
}
#define GENERIC_FLYING_ANIMATION_FRAME_SKIP (4)
const uint8_t genericFlyingAnimation[] PROGMEM = { 0, 1, 2, 3, 2, 1 };
#define GENERIC_ROTATING_ANIMATION_FRAME_SKIP (8)
const uint8_t genericRotatingAnimation[] PROGMEM = { 0, 1, 2, 3 };
static void generic_rotating_render(uint8_t* tileIndex, const uint8_t animationStart)
{
uint8_t counter = GetVsyncCounter();
*tileIndex = animationStart +
pgm_read_byte(&genericRotatingAnimation[(counter / GENERIC_ROTATING_ANIMATION_FRAME_SKIP) % NELEMS(genericRotatingAnimation)]);
}
static void generic_flying_render(ENTITY* const e, uint8_t* tileIndex, const uint8_t animationStart)
{
if (e->dead) {
*tileIndex = animationStart + pgm_read_byte(&genericFlyingAnimation[e->animationFrameCounter / GENERIC_FLYING_ANIMATION_FRAME_SKIP]);
} else {
*tileIndex = animationStart + pgm_read_byte(&genericFlyingAnimation[e->animationFrameCounter / GENERIC_FLYING_ANIMATION_FRAME_SKIP]);
if (++e->animationFrameCounter == GENERIC_FLYING_ANIMATION_FRAME_SKIP * NELEMS(genericFlyingAnimation)) // not a power of 2
e->animationFrameCounter = 0;
}
}
#define SPRITE_BUTTERFLY_ANIMATION_START (6)
#define SPRITE_FALLING_SPIKE_START (10)
#define SPRITE_TURTLE_ANIMATION_START (12)
#define SPRITE_FIREBALL_ANIMATION_START (16)
void butterfly_render(ENTITY* const e, uint8_t* tileIndex)
{
generic_flying_render(e, tileIndex, SPRITE_BUTTERFLY_ANIMATION_START);
}
void turtle_render(ENTITY* const e, uint8_t* tileIndex)
{
generic_flying_render(e, tileIndex, SPRITE_TURTLE_ANIMATION_START);
}
#define FALL_STATE_RESET 0
#define FALL_STATE_WIGGLING 1
#define FALL_STATE_FALLING 2
#define FALL_STATE_FELL 3
void falling_spike_render(ENTITY* const e, uint8_t* tileIndex)
{
// We don't need to wrap all of this in an "if (e->visible) {}" check because
// the render function will not be called when e->visible == false
if (e->y == (int16_t)pgm_read_word(&ISD_y[e->id]))
*tileIndex = SPRITE_FALLING_SPIKE_START;
else
*tileIndex = SPRITE_FALLING_SPIKE_START + 1;
}
void fireball_render(ENTITY* const e, uint8_t* tileIndex)
{
(void)e;
generic_rotating_render(tileIndex, SPRITE_FIREBALL_ANIMATION_START);
}
////////////////////////////////////////////////////////////////////////////////
// This does not work while in the middle of sprite flickering, because it assumes only forward iteration!
void Entity_despawn(LEVEL* const l, uint8_t index) // index is the position in the l->entity[] array, not the id
{
#if (DEBUG_SPAWNS == 1)
UZEMCHR = 's'; UZEMHEX = l->entity[index].id; UZEMCHR = '-'; UZEMCHR = '\n';
#endif
BitArray_clearBit(l->entity_is_spawned, l->entity[index].id);
l->entity[index] = l->entity[l->numSpawned - 1];
l->numSpawned--;
}
// This should only be called from within Entity_spawn, because it does not handle reverse list iteration.
// If everything spawned is a NEVER DESPAWN, it might not have reclaimed any space, so it should return false
// so the caller doesn't infinite loop thinking space would be reclaimed.
bool Entity_reclaimSpace(LEVEL* const l)
{
// Loop over all entities, and keep track of the entity that has the max x and y distance away
// from the player as long as the entity's spawn point is outside of VRAM. Then despawn it.
uint16_t maxDistance = 0;
int8_t maxIndex = 0;
for (uint8_t i = 0; i < l->numSpawned; ++i) {
// First make sure the entity is not a NEVER DESPAWN
uint8_t initial_flags = pgm_read_byte(&ISD_initial_flags[l->entity[i].id]);
if (initial_flags & IFLAG_NEVER_DESPAWN)
continue;
// Next make sure its spawn point is not within VRAM (+ 1), so it will come back automatically on its own
int16_t spawn_x = (int16_t)pgm_read_word(&ISD_x[l->entity[i].id]);
int16_t spawn_y = (int16_t)pgm_read_word(&ISD_y[l->entity[i].id]);
uint8_t wtx = P2HT(spawn_x);
uint8_t wty = P2VT(spawn_y);
if ((wtx + 1 >= l->camera->x_min) && (wtx <= l->camera->x_max + 1) && (wty + 1 >= l->camera->y_min) && (wty <= l->camera->y_max + 1))
continue;
// Calculate abs() distance in x and/or y between player's (x,y) and entity's current (x,y) and
// store the index associated with the max distance away for despawning outside this loop
uint16_t dx = abs(l->entity[i].x - l->player->x);
uint16_t dy = abs(l->entity[i].y - l->player->y);
if (dx > maxDistance || dy > maxDistance) {
maxDistance = (dx > dy) ? dx : dy;
maxIndex = i;
}
}
#if (DEBUG_SPAWNS == 1)
UZEMCHR = 'D'; UZEMCHR = ':'; UZEMHEX = l->entity[maxIndex].id; UZEMCHR = ':'; UZEMHEX = pgm_read_byte(&ISD_spawn_type[l->entity[maxIndex].id]); UZEMCHR = ':'; UZEMHEX = HI8(maxDistance); UZEMHEX = LO8(maxDistance); UZEMCHR = '\n';
#endif
if (maxDistance > 0) { // if we found one
Entity_despawn(l, maxIndex);
return true;
}
return false;
}
void Entity_spawn(LEVEL* const l, uint16_t xy_index, uint8_t id) // id is the double-digit number stamped on the tile
{
try_again:
// Has it already been spawned
if (BitArray_readBit(l->entity_is_spawned, id)) {
#if (DEBUG_SPAWNS == 1)
UZEMCHR = 's'; UZEMHEX = HI8(xy_index); UZEMHEX = LO8(xy_index); UZEMCHR = '='; UZEMHEX = id; UZEMCHR = '\n';
#else
(void)xy_index;
#endif
} else if (l->numSpawned < MAX_ACTIVE_SPAWNS) {
if (BitArray_readBit(l->entity_is_dead, id)) {
#if (DEBUG_SPAWNS == 1)
UZEMCHR = 's'; UZEMHEX = HI8(xy_index); UZEMHEX = LO8(xy_index); UZEMCHR = 'x'; UZEMHEX = id; UZEMCHR = '\n';
#endif
} else {
#if (DEBUG_SPAWNS == 1)
UZEMCHR = 's'; UZEMHEX = HI8(xy_index); UZEMHEX = LO8(xy_index); UZEMCHR = '+'; UZEMHEX = id; UZEMCHR = '\n';
#endif
// Record that we spawned it (assuming we can add it to the list)
BitArray_setBit(l->entity_is_spawned, id);
l->entity[l->numSpawned].id = id;
l->entity[l->numSpawned].x = (int16_t)pgm_read_word(&ISD_x[id]);
l->entity[l->numSpawned].y = (int16_t)pgm_read_word(&ISD_y[id]);
l->entity[l->numSpawned].dx = 0;
l->entity[l->numSpawned].dy = 0;
l->entity[l->numSpawned].counter = 0;
l->entity[l->numSpawned].animationFrameCounter = 0;
l->entity[l->numSpawned].dead = false;
uint8_t initial_flags = pgm_read_byte(&ISD_initial_flags[id]);
l->entity[l->numSpawned].left = (bool)(initial_flags & IFLAG_LEFT);
l->entity[l->numSpawned].right = (bool)(initial_flags & IFLAG_RIGHT);
l->entity[l->numSpawned].up = (bool)(initial_flags & IFLAG_UP);
l->entity[l->numSpawned].down= (bool)(initial_flags & IFLAG_DOWN);
l->entity[l->numSpawned].interacts = (bool)!(initial_flags & IFLAG_NOINTERACT);
l->entity[l->numSpawned].visible = (bool)!(initial_flags & IFLAG_NOT_VISIBLE);
l->numSpawned++;
}
}
else {
if (Entity_reclaimSpace(l)) {
goto try_again;
} else {
#if (DEBUG_PRINT_FATAL_ERRORS == 1)
UZEMCHR = 'S'; UZEMCHR = 'P'; UZEMCHR = 'N'; UZEMCHR = 'F'; UZEMCHR = 'U';
UZEMCHR = 'L'; UZEMCHR = 'L'; UZEMCHR = '\n';
#endif
}
}
}
void Entity_update_null(ENTITY* const e, LEVEL* const l)
{
(void)e;
(void)l;
}
void Entity_update_dying(ENTITY* const e)
{
// Integrate the X forces to calculate the new position (x,y) and the new velocity (dx,dy)
e->x += (e->dx / WORLD_FPS);
// Decelerate any motion along the X axis
if (e->dx < 0)
e->dx += WORLD_FRICTION / WORLD_FPS;
else if (e->dx > 0)
e->dx -= WORLD_FRICTION / WORLD_FPS;
// Integrate the Y forces to calculate the new position (x,y) and the new velocity (dx,dy)
e->y += (e->dy / WORLD_FPS);
// Obey gravity (it's the law)
e->dy += WORLD_GRAVITY / WORLD_FPS;
if (e->dy < -WORLD_MAXDY)
e->dy = -WORLD_MAXDY;
else if (e->dy > WORLD_MAXDY)
e->dy = WORLD_MAXDY;
}
void Entity_update_flying_horizontal(ENTITY* const e)
{
bool wasLeft = (e->dx < 0);
bool wasRight = (e->dx > 0);
int16_t ddx = 0;
if (e->left)
ddx -= WORLD_ACCEL; // entity wants to go left
else if (wasLeft)
ddx += WORLD_FRICTION; // entity was going left, but not anymore
if (e->right)
ddx += WORLD_ACCEL; // entity wants to go right
else if (wasRight)
ddx -= WORLD_FRICTION; // entity was going right, but not anymore
if (e->left || e->right || wasLeft || wasRight) { // smaller and faster than 'if (ddx)'
// Integrate the X forces to calculate the new position (x) and the new velocity (dx)
e->x += (e->dx / WORLD_FPS);
e->dx += (ddx / WORLD_FPS);
int16_t maxdx = pgm_read_word(&ISD_maxdx[e->id]);
if (e->dx < -maxdx)
e->dx = -maxdx;
else if (e->dx > maxdx)
e->dx = maxdx;
// Clamp horizontal velocity to zero if we detect that the entities direction has changed
if ((wasLeft && (e->dx > 0)) || (wasRight && (e->dx < 0)))
e->dx = 0; // clamp at zero to prevent friction from making the entity jiggle side to side
}
}
void Entity_update_flying_vertical(ENTITY* const e)
{
bool wasUp = (e->dy < 0);
bool wasDown = (e->dy > 0);
int16_t ddy = 0;
if (e->up)
ddy -= WORLD_ACCEL; // entity wants to go up
else if (wasUp)
ddy += WORLD_FRICTION; // entity was going up, but not anymore
if (e->down)
ddy += WORLD_ACCEL; // entity wants to go down
else if (wasDown)
ddy -= WORLD_FRICTION; // entity was going down, but not anymore
if (e->up || e->down || wasUp || wasDown) { // smaller and faster than 'if (ddy)'
// Integrate the Y forces to calculate the new position (y) and the new velocity (dy)
e->y += (e->dy / WORLD_FPS);
e->dy += (ddy / WORLD_FPS);
int16_t maxdy = pgm_read_word(&ISD_maxdx[e->id]); // reuse maxdx
if (e->dy < -maxdy)
e->dy = -maxdy;
else if (e->dy > maxdy)
e->dy = maxdy;
// Clamp vertical velocity to zero if we detect that the entities direction has changed
if ((wasUp && (e->dy > 0)) || (wasDown && (e->dy < 0)))
e->dy = 0; // clamp at zero to prevent friction from making the entity jiggle up and down
}
}
void Entity_update_charging_horizontal(ENTITY* const e, PLAYER* const p)
{
#define PAUSE_BEFORE_CHARGING_FRAMES 16
#define CHARGE_IF_PLAYER_WITHIN_TILES 6
// Save the old direction, so we can tell if we changed directions
bool wasLeft = e->left;
bool wasRight = e->right;
ai_fly_horizontal(e);
Entity_update_flying_horizontal(e);
bool nowLeft = e->left;
bool nowRight = e->right;
if (e->counter == PAUSE_BEFORE_CHARGING_FRAMES) { // if we are in the charging state
if ((wasLeft && nowRight) || (wasRight && nowLeft)) { // and if we changed directions
e->counter = 0; // go back to the normal walking state
} else { // if we didn't change directions
if (e->dx < 0) { // and if charging left, keep charging left (even if player no longer in view)
int16_t maxdx = pgm_read_word(&ISD_maxdx[e->id]); // read the max speed for the entity
e->dx = -maxdx * 3; // override the maximum speed that was set by the above call to Entity_update_flying_horizontal
} else if (e->dx > 0) { // otherwise if charging right, keep charging right (even if player no longer in view)
int16_t maxdx = pgm_read_word(&ISD_maxdx[e->id]); // read the max speed for the entity
e->dx = maxdx * 3; // override the maximum speed that was set by the above call to Entity_update_flying_horizontal
}
}
}
if (!p->dead) {
if (e->left) {
if ((e->counter > 0) ||
((NEAREST_SCREEN_PIXEL(p->x) < NEAREST_SCREEN_PIXEL(e->x)) &&
(NEAREST_SCREEN_PIXEL(e->x) - NEAREST_SCREEN_PIXEL(p->x) < CHARGE_IF_PLAYER_WITHIN_TILES * TILE_WIDTH) &&
(NEAREST_SCREEN_PIXEL(p->y) + (PLAYER_TILE_HEIGHT - 1) * TILE_HEIGHT >= NEAREST_SCREEN_PIXEL(e->y)) &&
(NEAREST_SCREEN_PIXEL(p->y) + 3 < NEAREST_SCREEN_PIXEL(e->y) + (PLAYER_TILE_HEIGHT - 1) * TILE_HEIGHT) )) {
// the + 3 is because the overlap function has a grace distance where the sprite can
// overlap the player without killing it, and we only want it to charge if it can kill
if (e->counter < PAUSE_BEFORE_CHARGING_FRAMES) {
if (e->counter == 0)
TriggerFx(FX_ALERT, 96, true);
e->counter++;
e->dx = -35; // don't pause completely
} else if (e->dx < 0) {
int16_t maxdx = pgm_read_word(&ISD_maxdx[e->id]);
e->dx = -maxdx * 3;
}
}
} else if (e->right) {
if ((e->counter > 0) ||
((NEAREST_SCREEN_PIXEL(p->x) > NEAREST_SCREEN_PIXEL(e->x)) &&
(NEAREST_SCREEN_PIXEL(p->x) - NEAREST_SCREEN_PIXEL(e->x) < CHARGE_IF_PLAYER_WITHIN_TILES * TILE_WIDTH) &&
(NEAREST_SCREEN_PIXEL(p->y) + (PLAYER_TILE_HEIGHT - 1) * TILE_HEIGHT >= NEAREST_SCREEN_PIXEL(e->y)) &&
(NEAREST_SCREEN_PIXEL(p->y) + 3 < NEAREST_SCREEN_PIXEL(e->y) + (PLAYER_TILE_HEIGHT - 1) * TILE_HEIGHT) )) {
// the + 3 is because the overlap function has a grace distance where the sprite can
// overlap the player without killing it, and we only want it to charge if it can kill
if (e->counter < PAUSE_BEFORE_CHARGING_FRAMES) {
if (e->counter == 0)
TriggerFx(FX_ALERT, 96, true);
e->counter++;
e->dx = 35; // don't pause completely
} else if (e->dx > 0) {
int16_t maxdx = pgm_read_word(&ISD_maxdx[e->id]);
e->dx = maxdx * 3;
}
}
}
} else { // If the player is dead, step over its dead body, so the z-ordering doesn't look uncanny
if (overlap(NEAREST_SCREEN_PIXEL(p->x), NEAREST_SCREEN_PIXEL(p->y), (PLAYER_TILE_WIDTH * TILE_WIDTH), (PLAYER_TILE_HEIGHT * TILE_HEIGHT),
NEAREST_SCREEN_PIXEL(e->x) + 1, NEAREST_SCREEN_PIXEL(e->y) + 3, (ENTITY_WIDTH * TILE_WIDTH) - 2, (ENTITY_HEIGHT * TILE_HEIGHT) - 6))
e->y = (int16_t)pgm_read_word(&ISD_y[e->id]) - 8;
else
e->y = (int16_t)pgm_read_word(&ISD_y[e->id]);
}
}
__attribute__(( always_inline ))
static inline bool GetTileIfInVRAM(CAMERA* const c, uint8_t wtx, uint8_t wty, uint8_t* tile)
{
bool inVRAM = ((wtx >= c->x_min) && (wtx <= c->x_max) && (wty >= c->y_min) && (wty <= c->y_max));
if (inVRAM) {
*tile = GetTile(wtx % VRAM_TILES_H, wty % VRAM_TILES_V);
return true;
}
return false;
}
__attribute__(( always_inline ))
static inline void SetTileIfInVRAM(CAMERA* const c, uint8_t wtx, uint8_t wty, uint8_t tile)
{
bool inVRAM = ((wtx >= c->x_min) && (wtx <= c->x_max) && (wty >= c->y_min) && (wty <= c->y_max));
if (inVRAM)
SetTile(wtx % VRAM_TILES_H, wty % VRAM_TILES_V, tile);
}
__attribute__((optimize("O3")))
void Entity_update_horizontal_platform(ENTITY* const e, PLAYER* const p, CAMERA* const c)
{
int16_t prevRoundedX = NEAREST_SCREEN_PIXEL(e->x) << FP_SHIFT;
Entity_update_flying_horizontal(e);
int16_t init_x = (int16_t)pgm_read_word(&ISD_x[e->id]);
int16_t lo = init_x + HT2P((int8_t)pgm_read_byte(&ISD_lo_bound[e->id]));
int16_t hi = init_x + HT2P((int8_t)pgm_read_byte(&ISD_hi_bound[e->id]));
if (e->x < lo) {
e->x = lo;
e->dx = 0;
e->left = false;
e->right = true;
} else if (e->x > hi) {
e->x = hi;
e->dx = 0;
e->right = false;
e->left = true;
}
int16_t roundedX = NEAREST_SCREEN_PIXEL(e->x) << FP_SHIFT;
if (p->onMovingPlatform && p->platformId == e->id)
p->x += roundedX - prevRoundedX;
uint8_t wtx = P2HT(roundedX);
uint8_t wty = P2VT((int16_t)pgm_read_word(&ISD_y[e->id]));
uint8_t ty = wty % VRAM_TILES_V;
uint8_t overlap = NH(roundedX) >> FP_SHIFT;
// Remove any leftover tile gunk
uint8_t tile;
if (GetTileIfInVRAM(c, wtx - 1, wty, &tile) && (tile >= TILE_START_HORIZONTAL_PLATFORM && tile <= TILE_END_HORIZONTAL_PLATFORM))
SetTile((wtx - 1) % VRAM_TILES_H, ty, TILE_PURE_SKY);
if (GetTileIfInVRAM(c, wtx + 4, wty, &tile) && (tile >= TILE_START_HORIZONTAL_PLATFORM && tile <= TILE_END_HORIZONTAL_PLATFORM))
SetTile((wtx + 4) % VRAM_TILES_H, ty, TILE_PURE_SKY);
if (GetTileIfInVRAM(c, wtx + 5, wty, &tile) && (tile >= TILE_START_HORIZONTAL_PLATFORM && tile <= TILE_END_HORIZONTAL_PLATFORM))
SetTile((wtx + 5) % VRAM_TILES_H, ty, TILE_PURE_SKY);
// Draw platform in its new position if it is in VRAM
if (overlap == 0) {
SetTileIfInVRAM(c, wtx + 0, wty, TILE_START_HORIZONTAL_PLATFORM);
SetTileIfInVRAM(c, wtx + 1, wty, TILE_START_HORIZONTAL_PLATFORM + 9);
SetTileIfInVRAM(c, wtx + 2, wty, TILE_START_HORIZONTAL_PLATFORM + 9);
SetTileIfInVRAM(c, wtx + 3, wty, TILE_END_HORIZONTAL_PLATFORM);
} else {
SetTileIfInVRAM(c, wtx + 0, wty, TILE_START_HORIZONTAL_PLATFORM + overlap);
if (overlap != 7)
SetTileIfInVRAM(c, wtx + 1, wty, TILE_START_HORIZONTAL_PLATFORM + 9);
else
SetTileIfInVRAM(c, wtx + 1, wty, TILE_START_HORIZONTAL_PLATFORM + 8);
SetTileIfInVRAM(c, wtx + 2, wty, TILE_START_HORIZONTAL_PLATFORM + 9);
if (overlap != 1)
SetTileIfInVRAM(c, wtx + 3, wty, TILE_START_HORIZONTAL_PLATFORM + 9);
else
SetTileIfInVRAM(c, wtx + 3, wty, TILE_START_HORIZONTAL_PLATFORM + 10);
SetTileIfInVRAM(c, wtx + 4, wty, TILE_START_HORIZONTAL_PLATFORM + 10 + overlap);
}
}
__attribute__((optimize("O3")))
void Entity_update_vertical_platform(ENTITY* const e, PLAYER* const p, CAMERA* const c)
{
// In a vertically moving platform, e->dx is not used, so use it to store the previous e->y position
// where Player_update can later read it to make sure a VMP moving up doesn't pass through a player
// moving down.
e->dx = e->y;
int16_t prevRoundedY = NEAREST_SCREEN_PIXEL(e->y) << FP_SHIFT;
Entity_update_flying_vertical(e);
int16_t init_y = (int16_t)pgm_read_word(&ISD_y[e->id]);
int16_t lo = init_y + VT2P((int8_t)pgm_read_byte(&ISD_lo_bound[e->id]));
int16_t hi = (2 << FP_SHIFT) + init_y + VT2P((int8_t)pgm_read_byte(&ISD_hi_bound[e->id]));
if (e->y < lo) {
e->y = lo;
e->dy = 0;
e->up = false;
e->down = true;
} else if (e->y > hi) {
e->y = hi;
e->dy = 0;
e->down = false;
e->up = true;
}
int16_t roundedY = NEAREST_SCREEN_PIXEL(e->y) << FP_SHIFT;
if (p->onMovingPlatform && p->platformId == e->id) {
if (!p->down)
p->y += roundedY - prevRoundedY;
else { // Attempt to drop through the VMP, but if the VMP is moving down fast enough, the player might have to hold the down button for longer
p->y += roundedY - prevRoundedY + 1;
p->onMovingPlatform = false;
}
}
uint8_t wty = P2VT(roundedY);
uint8_t wtx = P2HT((int16_t)pgm_read_word(&ISD_x[e->id]));
uint8_t overlap = NV(roundedY) >> FP_SHIFT;
uint8_t ty;
// Remove any leftover tile gunk
uint8_t tile;
ty = (wty - 1) % VRAM_TILES_V;
if (GetTileIfInVRAM(c, wtx + 0, wty - 1, &tile) && (tile >= TILE_START_HORIZONTAL_PLATFORM && tile <= TILE_END_VERTICAL_PLATFORM)) // not a typo
SetTile((wtx + 0) % VRAM_TILES_H, ty, TILE_PURE_SKY);
if (GetTileIfInVRAM(c, wtx + 1, wty - 1, &tile) && (tile >= TILE_START_HORIZONTAL_PLATFORM && tile <= TILE_END_VERTICAL_PLATFORM))
SetTile((wtx + 1) % VRAM_TILES_H, ty, TILE_PURE_SKY);
if (GetTileIfInVRAM(c, wtx + 2, wty - 1, &tile) && (tile >= TILE_START_HORIZONTAL_PLATFORM && tile <= TILE_END_VERTICAL_PLATFORM))
SetTile((wtx + 2) % VRAM_TILES_H, ty, TILE_PURE_SKY);
if (GetTileIfInVRAM(c, wtx + 3, wty - 1, &tile) && (tile >= TILE_START_HORIZONTAL_PLATFORM && tile <= TILE_END_VERTICAL_PLATFORM))
SetTile((wtx + 3) % VRAM_TILES_H, ty, TILE_PURE_SKY);
ty = (wty + 1) % VRAM_TILES_V;
if (GetTileIfInVRAM(c, wtx + 0, wty + 1, &tile) && (tile >= TILE_START_HORIZONTAL_PLATFORM && tile <= TILE_END_VERTICAL_PLATFORM)) // not a typo
SetTile((wtx + 0) % VRAM_TILES_H, ty, TILE_PURE_SKY);
if (GetTileIfInVRAM(c, wtx + 1, wty + 1, &tile) && (tile >= TILE_START_HORIZONTAL_PLATFORM && tile <= TILE_END_VERTICAL_PLATFORM))
SetTile((wtx + 1) % VRAM_TILES_H, ty, TILE_PURE_SKY);
if (GetTileIfInVRAM(c, wtx + 2, wty + 1, &tile) && (tile >= TILE_START_HORIZONTAL_PLATFORM && tile <= TILE_END_VERTICAL_PLATFORM))
SetTile((wtx + 2) % VRAM_TILES_H, ty, TILE_PURE_SKY);
if (GetTileIfInVRAM(c, wtx + 3, wty + 1, &tile) && (tile >= TILE_START_HORIZONTAL_PLATFORM && tile <= TILE_END_VERTICAL_PLATFORM))
SetTile((wtx + 3) % VRAM_TILES_H, ty, TILE_PURE_SKY);
// Draw platform in its new position if it is in VRAM
SetTileIfInVRAM(c, wtx + 0, wty, pgm_read_byte(&verticalMovingPlatformMapTop[3 * overlap + 0]));
SetTileIfInVRAM(c, wtx + 1, wty, pgm_read_byte(&verticalMovingPlatformMapTop[3 * overlap + 1]));
SetTileIfInVRAM(c, wtx + 2, wty, pgm_read_byte(&verticalMovingPlatformMapTop[3 * overlap + 1]));
SetTileIfInVRAM(c, wtx + 3, wty, pgm_read_byte(&verticalMovingPlatformMapTop[3 * overlap + 2]));
if (overlap > 2) {
SetTileIfInVRAM(c, wtx + 0, wty + 1, pgm_read_byte(&verticalMovingPlatformMapBottom[3 * overlap + 0]));
SetTileIfInVRAM(c, wtx + 1, wty + 1, pgm_read_byte(&verticalMovingPlatformMapBottom[3 * overlap + 1]));
SetTileIfInVRAM(c, wtx + 2, wty + 1, pgm_read_byte(&verticalMovingPlatformMapBottom[3 * overlap + 1]));
SetTileIfInVRAM(c, wtx + 3, wty + 1, pgm_read_byte(&verticalMovingPlatformMapBottom[3 * overlap + 2]));
}
}
void Entity_update_falling_hazard(ENTITY* const e, LEVEL* const l, PLAYER* const p)
{
#define FRAMES_BEFORE_FALLING (27)
// Check to see if the player is within the trigger zone (below the object, within x - lo_bound and x + lo_bound)
int16_t y = (int16_t)pgm_read_word(&ISD_y[e->id]);
if ((e->counter == FALL_STATE_RESET) && (!p->dead) &&
(NEAREST_SCREEN_PIXEL(p->y) > NEAREST_SCREEN_PIXEL(y) + TILE_HEIGHT) &&
(NEAREST_SCREEN_PIXEL(p->y) <= NEAREST_SCREEN_PIXEL(y) + TILE_HEIGHT * (int8_t)pgm_read_byte(&ISD_hi_bound[e->id])) &&
(NEAREST_SCREEN_PIXEL(p->x) > NEAREST_SCREEN_PIXEL(e->x) - TILE_WIDTH * (int8_t)pgm_read_byte(&ISD_lo_bound[e->id])) &&
(NEAREST_SCREEN_PIXEL(p->x) < NEAREST_SCREEN_PIXEL(e->x) + TILE_WIDTH * (int8_t)pgm_read_byte(&ISD_lo_bound[e->id]))) {
TriggerFx(FX_SHAKE, 160, true);
e->counter = FALL_STATE_WIGGLING; // move to the next state
e->animationFrameCounter = 0;
}
// If we are wiggling, increment a counter, so we know when to fall
if (e->counter == FALL_STATE_WIGGLING) {
e->animationFrameCounter++;
if (e->animationFrameCounter == FRAMES_BEFORE_FALLING) {
TriggerFx(FX_FALLING, 64, true);
e->x = (int16_t)pgm_read_word(&ISD_x[e->id]); // stop the wiggle (reset X)
e->animationFrameCounter = 0;
e->counter = FALL_STATE_FALLING; // start the actual fall
}
}
// Check the state again, because it may have been modified above
if (e->counter == FALL_STATE_RESET) {
return; // bail early in the most common case
} else if (e->counter == FALL_STATE_WIGGLING) {
int16_t x = (int16_t)pgm_read_word(&ISD_x[e->id]);
e->x = x + (e->animationFrameCounter % 8) - 4; // Make it wiggle side-to-side a bit
if (e->animationFrameCounter % 4 == 0) // synchronized with a sound effect
TriggerFx(FX_SHAKE, 160, true);
} else if (e->counter == FALL_STATE_FELL) {
if (p->dead && e->interacts) // If the player is dead when we are still interactive, early return to keep it on the screen forever
return;
e->animationFrameCounter++; // Increment the multi-stage timer
if (e->animationFrameCounter == 1) {
e->interacts = false; // After hitting the ground, it can not kill the player
} else if (e->animationFrameCounter == 127) {
e->y = y; // Return the sprite back to its initial position, but don't allow it to fall again yet
e->interacts = true;
} else if (e->animationFrameCounter == 255) {
e->counter = FALL_STATE_RESET; // If enough time has passed, allow it to fall again
} else {
e->visible = !((e->animationFrameCounter >= 127 - 90 && e->animationFrameCounter < 127 - 80) ||
(e->animationFrameCounter >= 127 - 70 && e->animationFrameCounter < 127 - 60) ||
(e->animationFrameCounter >= 127 - 50 && e->animationFrameCounter < 127 - 40) ||
(e->animationFrameCounter >= 127 - 30 && e->animationFrameCounter < 127 - 20));
}
} else /*if (e->counter == FALL_STATE_FALLING)*/ {
// Integrate the Y forces to calculate the new position (y) and the new velocity (dy)
e->y += (e->dy / WORLD_FPS);
// Obey gravity (it's the law)
e->dy += WORLD_GRAVITY / WORLD_FPS;
// Limit the maximum falling speed to what is configured for this particular entity (typically WORLD_MAXDY)
int16_t maxdy = (int16_t)pgm_read_word(&ISD_maxdx[e->id]);
if (e->dy > maxdy)
e->dy = maxdy;
// If it has fallen to the hi_bound, stop it and play a sound
int16_t stopHeight = y + VT2P((int8_t)pgm_read_byte(&ISD_hi_bound[e->id]) + 1);
if (e->y >= stopHeight) {
e->y = stopHeight;
e->dy = 0;
TriggerFx(FX_THUD, 255, true);
e->counter = FALL_STATE_FELL; // Transition to the next state
}
// Clamp Y to avoid falling too far below the screen (untested thus far)
if (e->y >= (int16_t)VT2P(l->height + 1)) { // the + 1 allows the sprite to fall all the way off screen
e->y = (int16_t)VT2P(l->height + 1);
e->dy = 0;
e->counter = FALL_STATE_FELL;
}
}
}
// By messing with the visible property, and calling SetTile in every state (in case you scroll it off and
// then back on the screen again, we can make this entity totally tile-based when in the reset state.
void Entity_update_falling_spike(ENTITY* const e, LEVEL* const l, PLAYER* const p, CAMERA* const c)
{
Entity_update_falling_hazard(e, l, p);
uint8_t wtx = P2HT((int16_t)pgm_read_word(&ISD_x[e->id]));
uint8_t wty = P2VT((int16_t)pgm_read_word(&ISD_y[e->id]));
uint8_t tx = wtx % VRAM_TILES_H;
uint8_t ty = wty % VRAM_TILES_V;
bool inVRAM = ((wtx >= c->x_min) && (wtx <= c->x_max) && (wty >= c->y_min) && (wty <= c->y_max));
if (e->counter == FALL_STATE_RESET) {
e->visible = false;
if (inVRAM)
SetTile(tx, ty, pgm_read_byte(&ISD_backing_tile[e->id]));
} else if (e->counter == FALL_STATE_WIGGLING) {
e->visible = true;
if (inVRAM)
SetTile(tx, ty, TILE_PURE_SKY);
} else if (e->counter == FALL_STATE_FALLING) {
if (inVRAM)
SetTile(tx, ty, TILE_PURE_SKY);
} else if (e->counter == FALL_STATE_FELL) {
if (e->y == (int16_t)pgm_read_word(&ISD_y[e->id])) {
e->visible = false;
if (inVRAM)
SetTile(tx, ty, pgm_read_byte(&ISD_backing_tile[e->id]));
} else if (inVRAM)
SetTile(tx, ty, TILE_PURE_SKY);
}
}
void Entity_updateAll(LEVEL* const l, CAMERA* const c, PLAYER* const p)
{
for (uint8_t i = 0; i < l->numSpawned; ++i) {
if (l->entity[i].dead) {
Entity_update_dying(&l->entity[i]);
} else {
switch (pgm_read_byte(&ISD_spawn_type[l->entity[i].id])) {
case ST_BUTTERFLY:
ai_fly_horizontal_erratic(&l->entity[i]);
Entity_update_flying_horizontal(&l->entity[i]);
break;
case ST_FALLING_SPIKE:
Entity_update_falling_spike(&l->entity[i], l, p, c);
break;
case ST_CHARGING_TURTLE:
Entity_update_charging_horizontal(&l->entity[i], p);
break;
case ST_FIREBALL:
ai_fly_circle(&l->entity[i], (pgm_read_byte(&ISD_initial_flags[l->entity[i].id]) & IFLAG_LEFT) ? 1 : -1, GetVsyncCounter());
break;
case ST_HORIZONTAL_PLATFORM:
//__asm__ __volatile__ ("wdr");
Entity_update_horizontal_platform(&l->entity[i], p, c);
//__asm__ __volatile__ ("wdr");
break;
case ST_VERTICAL_PLATFORM:
//__asm__ __volatile__ ("wdr");
Entity_update_vertical_platform(&l->entity[i], p, c);
//__asm__ __volatile__ ("wdr");
break;
}
}
}
}
void Entity_collideAndRenderVisible(LEVEL* const l, CAMERA* const c, PLAYER* const p, const bool usedAllRamTilesLastFrame)
{
#if (OPTION_USE_SPRITE_FLICKERING == 1)
static bool sprite_flicker_traverse_list_backwards = false;
int8_t start;
int8_t end;
int8_t step;
if (sprite_flicker_traverse_list_backwards) {
start = l->numSpawned - 1;
end = -1;
step = -1;
} else {
start = 0;
end = l->numSpawned;
step = 1;
}
// Loop over all spawned entities
for (int8_t i = start; i != end; i += step) {
#else // OPTION_USE_SPRITE_FLICKERING
(void)usedAllRamTilesLastFrame;
for (uint8_t i = 0; i < l->numSpawned; ++i) {
#endif // OPTION_USE_SPRITE_FLICKERING
int16_t ex = l->entity[i].x;
int16_t ey = l->entity[i].y;
// If the enemy is within the camera's viewport
// TODO: Change ENTITY_WIDTH and ENTITY_HEIGHT to reflect the actual enemy size (from a lookup based on its type?)
// Or from PROGMEM by reading the enemies immutable stats.
if (NEAREST_SCREEN_PIXEL(ex) > c->x - (ENTITY_WIDTH * TILE_WIDTH) && NEAREST_SCREEN_PIXEL(ex) < c->x + (SCREEN_TILES_H * TILE_WIDTH) &&
NEAREST_SCREEN_PIXEL(ey) > c->y - (ENTITY_HEIGHT * TILE_HEIGHT) && NEAREST_SCREEN_PIXEL(ey) < c->y + (SCREEN_TILES_V * TILE_HEIGHT)) {
// Check to see if the player is attacking
if (p->attacking) { // by not adding "&& !l->entity[i].dead" here, the unicorn can "fart juggle" a dead corpse :)
int8_t x_offset = (p->flags & SPRITE_FLIP_X) ? 8 : -14; // calculated based on the attack hitbox
int8_t y_offset = (p->update == Player_update_ladder) ? 1 : 0; // If attacking from ladder, the sprite is rendered at a different location
// Check to see if the player killed the sprite
if (overlap(NEAREST_SCREEN_PIXEL(p->x) + x_offset, NEAREST_SCREEN_PIXEL(p->y) + y_offset, (14), (PLAYER_TILE_HEIGHT * TILE_HEIGHT),
NEAREST_SCREEN_PIXEL(ex), NEAREST_SCREEN_PIXEL(ey), (ENTITY_WIDTH * TILE_WIDTH), (ENTITY_HEIGHT * TILE_HEIGHT)) &&
((pgm_read_byte(&ISD_initial_flags[l->entity[i].id]) & IFLAG_INVINCIBLE) == 0) ) {
BitArray_setBit(l->entity_is_dead, l->entity[i].id);
l->entity[i].dead = true;
l->entity[i].dy = -135; // Give it a little flip
l->entity[i].left = false;
l->entity[i].right = false;
l->entity[i].up = false;
l->entity[i].down = false;
}
}
// Check to see if the sprite killed the player
if (!l->entity[i].dead && l->entity[i].interacts) {
if (overlap(NEAREST_SCREEN_PIXEL(p->x), NEAREST_SCREEN_PIXEL(p->y), (PLAYER_TILE_WIDTH * TILE_WIDTH), (PLAYER_TILE_HEIGHT * TILE_HEIGHT),
NEAREST_SCREEN_PIXEL(ex) + 0/*1*/, NEAREST_SCREEN_PIXEL(ey) + 3, (ENTITY_WIDTH * TILE_WIDTH) - 0/*2*/, (ENTITY_HEIGHT * TILE_HEIGHT) - 6)) {
if (!p->dead) {
#if (DEBUG_PRINT_KILLED_BY == 1)
UZEMCHR = 'S'; UZEMCHR = 'p'; UZEMCHR = 'r'; UZEMCHR = 'i'; UZEMCHR = 't'; UZEMCHR = 'e'; UZEMCHR = ' ';
#endif
KillPlayer(p);
}
}
}
// Only do rendering stuff inside the render functions, because they are not always called!
if (l->entity[i].visible) {
uint8_t tileIndex = SPRITE_BLANK;
switch (pgm_read_byte(&ISD_spawn_type[l->entity[i].id])) {
case ST_BUTTERFLY:
butterfly_render(&l->entity[i], &tileIndex);
break;
case ST_FALLING_SPIKE:
falling_spike_render(&l->entity[i], &tileIndex);
break;
case ST_CHARGING_TURTLE:
turtle_render(&l->entity[i], &tileIndex);
break;
case ST_FIREBALL:
fireball_render(&l->entity[i], &tileIndex);
break;
}
#if (DEBUG_SHOW_SPRITE_OPTIMIZATIONS == 1)
// Show when sprites could be turned into tiles for entities
uint8_t nsp_x = NEAREST_SCREEN_PIXEL(ex);
uint8_t nsp_y = NEAREST_SCREEN_PIXEL(ey);
if ((!!(nsp_x % TILE_WIDTH) + !!(nsp_y % TILE_HEIGHT)) == 0) {
uint8_t tx = (nsp_x / TILE_WIDTH) % VRAM_TILES_H;
uint8_t ty = (nsp_y / TILE_HEIGHT) % VRAM_TILES_V;
uint8_t tile = GetTile(tx, ty);
if (tile == TILE_PURE_SKY || tile == 80) {
UZEMCHR = '$';
SetTile(tx, ty, 80);
tileIndex = SPRITE_BLANK; // Skip the blit to see if it would help reduce sprite flickering
}
}
#endif
#if (DEBUG_PRINT_FATAL_ERRORS == 1)
if (tileIndex == SPRITE_BLANK) {
UZEMCHR = 'T'; UZEMCHR = 'I'; UZEMCHR = 'N'; UZEMCHR = 'S'; UZEMHEX = l->entity[i].id; UZEMCHR = '\n';
} else
#endif
MyBlitSprite((l->entity[i].left ? 0 : SPRITE_FLIP_X) | (l->entity[i].dead ? SPRITE_FLIP_Y : 0),
tileIndex,
NEAREST_SCREEN_PIXEL(ex) - c->x,
NEAREST_SCREEN_PIXEL(ey) - c->y);
}
} else { // If the enemy is NOT within the camera's viewport
// If (ex, ey) is far enough away from the player's (x, y), or if an ENTITY is dead and offscreen then despawn it
// TODO: Maybe read the distance through an immutable property of the entity (so falling spikes and control objects can despawn sooner)
if (((pgm_read_byte(&ISD_initial_flags[l->entity[i].id]) & IFLAG_NEVER_DESPAWN) == 0) &&
(abs(ex - p->x) > (/*384*/384 << FP_SHIFT) || abs(ey - p->y) > (/*260*/384 << FP_SHIFT) || l->entity[i].dead)) {
#if (OPTION_USE_SPRITE_FLICKERING == 1)
#if (DEBUG_SPAWNS == 1)
UZEMCHR = 's'; UZEMHEX = l->entity[i].id; UZEMCHR = '-'; UZEMCHR = '\n';
#endif
// Fancy despawn, because we can be iterating forward or reverse
BitArray_clearBit(l->entity_is_spawned, l->entity[i].id);
if (sprite_flicker_traverse_list_backwards) {
l->entity[i] = l->entity[end + 1]; // swap with the last one we haven't processed
l->entity[end + 1] = l->entity[l->numSpawned - 1]; // now put one we already processed in its place
l->numSpawned--;
i++;
end++; // and modify the end stop so we don't process it again
} else {
l->entity[i] = l->entity[end - 1];
l->numSpawned--;
i--;
end--;
}
#else
Entity_despawn(l, i--); // Note: This will lose the dead status stored in the ENTITY struct, so set its bit in entity_is_dead[] if it needs to stay dead
#endif // OPTION_USE_SPRITE_FLICKERING
continue;
}
}
} // end loop over all spawned entities
#if (OPTION_USE_SPRITE_FLICKERING == 1)
//UZEMCHR = 'T'; UZEMCHR = 'R'; UZEMCHR = 'T'; UZEMHEX = ram_tiles_used_for_entities; UZEMCHR = '\n';
if (usedAllRamTilesLastFrame) {
sprite_flicker_traverse_list_backwards = !sprite_flicker_traverse_list_backwards;
#if (DEBUG_PRINT_FATAL_ERRORS == 1)
UZEMCHR = '@';
#endif
// Swizzle the entity array around
#if (OPTION_SF_CIRCULAR_SWIZZLE == 1)
// One big circular rotation
if (l->numSpawned > 2) {
ENTITY tmp = l->entity[0];
memmove(&l->entity[0], &l->entity[1], sizeof(ENTITY) * l->numSpawned - 1);
l->entity[l->numSpawned - 1] = tmp;
}
#endif
#if (OPTION_SF_DOUBLE_MIXER_SWIZZLE == 1)
// Double mixer technique
if (l->numSpawned >= 4) {
ENTITY tmp;
uint8_t llo = 0;
uint8_t lhi = (l->numSpawned - 1) / 2;
tmp = l->entity[lhi];
memmove(&l->entity[llo + 1], &l->entity[llo], sizeof(ENTITY) * (lhi - llo));
l->entity[llo] = tmp;
uint8_t rlo = lhi + 1;
uint8_t rhi = l->numSpawned - 1;
tmp = l->entity[rlo];
memmove(&l->entity[rlo], &l->entity[rlo + 1], sizeof(ENTITY) * (rhi - rlo));
l->entity[rhi] = tmp;
}
#endif
}
#endif // OPTION_USE_SPRITE_FLICKERING
}
////////////////////////////////////////////////////////////////////////////////
// End Entity Operations
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// Begin RamFont Operations
////////////////////////////////////////////////////////////////////////////////
#define GAME_USER_RAM_TILES_COUNT (0)
#define RF_BG (GAME_USER_RAM_TILES_COUNT)
#define RF_A (GAME_USER_RAM_TILES_COUNT + 1)
#define RF_C (GAME_USER_RAM_TILES_COUNT + 2)
#define RF_E (GAME_USER_RAM_TILES_COUNT + 3)
#define RF_I (GAME_USER_RAM_TILES_COUNT + 4)
#define RF_L (GAME_USER_RAM_TILES_COUNT + 5)
#define RF_M (GAME_USER_RAM_TILES_COUNT + 6)
#define RF_R (GAME_USER_RAM_TILES_COUNT + 7)
#define RF_S (GAME_USER_RAM_TILES_COUNT + 8)
#define RF_T (GAME_USER_RAM_TILES_COUNT + 9)
#define RF_U (GAME_USER_RAM_TILES_COUNT + 10)
#define RF_V (GAME_USER_RAM_TILES_COUNT + 11)
#define RF_ASTERISK (GAME_USER_RAM_TILES_COUNT + 12)
#define RF_B_TL (GAME_USER_RAM_TILES_COUNT + 13)
#define RF_B_T (GAME_USER_RAM_TILES_COUNT + 14)
#define RF_B_TR (GAME_USER_RAM_TILES_COUNT + 15)
#define RF_B_L (GAME_USER_RAM_TILES_COUNT + 16)
#define RF_B_R (GAME_USER_RAM_TILES_COUNT + 17)
#define RF_B_BL (GAME_USER_RAM_TILES_COUNT + 18)
#define RF_B_B (GAME_USER_RAM_TILES_COUNT + 19)
#define RF_B_BR (GAME_USER_RAM_TILES_COUNT + 20)
// Run ramfont/main ramfont-popup.png to generate
const uint8_t rf_popup[] PROGMEM =
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Background
0x30, 0x78, 0xec, 0xe4, 0xfe, 0xc2, 0xc2, 0x00, // A
//0x3e, 0x62, 0x32, 0x7e, 0xe2, 0xf2, 0x7e, 0x00, // B
0x7c, 0xc6, 0x02, 0x02, 0xc6, 0xfe, 0x7c, 0x00, // C
//0x3c, 0x62, 0xc2, 0xc2, 0xe2, 0xfe, 0x7e, 0x00, // D
0x7c, 0xc6, 0x02, 0x7e, 0x02, 0xfe, 0xfc, 0x00, // E
//0x7c, 0xc6, 0x02, 0x7e, 0x06, 0x06, 0x06, 0x00, // F
//0x7c, 0xc6, 0x02, 0x02, 0xf2, 0xe6, 0xbc, 0x00, // G
//0x42, 0xc2, 0xc2, 0xfe, 0xc2, 0xc6, 0xc6, 0x00, // H
0x10, 0x30, 0x30, 0x30, 0x38, 0x38, 0x38, 0x00, // I
//0x60, 0xc0, 0xc0, 0xc0, 0xe2, 0xfe, 0x7c, 0x00, // J
//0x64, 0x36, 0x16, 0x3e, 0x76, 0xe6, 0xe6, 0x00, // K
0x04, 0x06, 0x02, 0x02, 0x82, 0xfe, 0x7c, 0x00, // L
0x62, 0xf6, 0xde, 0xca, 0xc2, 0xc6, 0x46, 0x00, // M
//0x46, 0xce, 0xda, 0xf2, 0xe2, 0xc6, 0x46, 0x00, // N
//0x70, 0xcc, 0xc2, 0xc2, 0xe2, 0xfe, 0x7c, 0x00, // O
//0x7c, 0xc6, 0xe2, 0x7e, 0x06, 0x06, 0x04, 0x00, // P
//0x7c, 0xe2, 0xc2, 0xc2, 0x7a, 0xe6, 0xdc, 0x00, // Q
0x7c, 0xc6, 0xc2, 0x7e, 0x1a, 0xf2, 0xe2, 0x00, // R
0x3c, 0x62, 0x02, 0x7c, 0xc0, 0xe6, 0x7c, 0x00, // S
0x7c, 0xfe, 0x12, 0x10, 0x18, 0x18, 0x18, 0x00, // T
0x40, 0xc2, 0xc2, 0xc2, 0xe6, 0x7e, 0x3c, 0x00, // U
0x40, 0xc2, 0xc2, 0xc4, 0x64, 0x38, 0x18, 0x00, // V
//0x40, 0xc2, 0xd2, 0xda, 0xda, 0xfe, 0x6c, 0x00, // W
//0x80, 0xc6, 0x6e, 0x38, 0x38, 0xec, 0xc6, 0x00, // X
//0x80, 0x86, 0xcc, 0x78, 0x30, 0x1c, 0x0c, 0x00, // Y
//0x7c, 0xc0, 0x60, 0x10, 0x0c, 0xfe, 0x7c, 0x00, // Z
0x00, 0x00, 0x18, 0x3c, 0x3c, 0x18, 0x00, 0x00, // *
};
#define RF_POPUP_LEN (sizeof(rf_popup) / 8)
// Compressed ram font data for popup border
// Run ramfont/main ramfont-popup-border.png to generate
const uint8_t rf_popup_border[] PROGMEM =
{
0xff, 0xff, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, // Border Top Left
0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Border Top
0xff, 0x7f, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, // Border Top Right
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, // Border Left
0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, // Border Right
0x03, 0x03, 0x03, 0x03, 0x03, 0xff, 0xff, 0x01, // Border Bottom Left
0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, // Border Bottom
0x60, 0x60, 0x60, 0x60, 0x60, 0x7f, 0x7f, 0x00, // Border Bottom Right
};
#define RF_POPUP_BORDER_LEN (sizeof(rf_popup_border) / 8)
const uint8_t RF_RESUME[] PROGMEM = { RF_R, RF_E, RF_S, RF_U, RF_M, RF_E };
const uint8_t RF_RESTART_LEVEL[] PROGMEM = { RF_R, RF_E, RF_S, RF_T, RF_A, RF_R, RF_T, RF_BG, RF_L, RF_E, RF_V, RF_E, RF_L };
const uint8_t RF_MUSIC[] PROGMEM = { RF_M, RF_U, RF_S, RF_I, RF_C };
// Loads 'len' compressed 'ramfont' tiles into user ram tiles starting at 'user_ram_tile_start' using 'fg_color' and 'bg_color'
void RamFont_Load(const uint8_t* ramfont, uint8_t user_ram_tile_start, uint8_t len, uint8_t fg_color, uint8_t bg_color)
{
for (uint8_t tile = 0; tile < len; ++tile) {
uint8_t* ramTile = GetUserRamTile(user_ram_tile_start + tile);
for (uint8_t row = 0; row < 8; ++row) {
uint8_t rowstart = row * 8;
uint8_t data = (uint8_t)pgm_read_byte(&ramfont[tile * 8 + row]);
for (uint8_t bit = 0, mask = 0x1; mask != 0; ++bit, mask <<= 1)
if (data & mask)
ramTile[rowstart + bit] = fg_color;
else
ramTile[rowstart + bit] = bg_color;
}
}
}
void RamFont_Print(uint8_t x, uint8_t y, const uint8_t* message, uint8_t len)
{
for (uint8_t i = 0; i < len; ++i) {
uint8_t tileno = pgm_read_byte(&message[i]);
SetRamTile((x + i) % VRAM_TILES_H, y % VRAM_TILES_V, tileno);
}
}
////////////////////////////////////////////////////////////////////////////////
// End RamFont Operations
////////////////////////////////////////////////////////////////////////////////
__attribute__(( always_inline ))
static inline bool overlap(const uint16_t x1, const uint16_t y1, const uint8_t w1, const uint8_t h1,
const uint16_t x2, const uint16_t y2, const uint8_t w2, const uint8_t h2)
{
return !(((x1 + w1 - 1) < x2) ||
((x2 + w2 - 1) < x1) ||
((y1 + h1 - 1) < y2) ||
((y2 + h2 - 1) < y1));
}
__attribute__(( always_inline ))
static inline void AnimateCollectables(const CAMERA* const c, const LEVEL* const l)
{
// Since animation is a bit expensive, it might be possible to animate different things
// on different frames. To keep them on alternate frames, maybe it's possible to use:
// if ((atn + {1, 2, 3, 4, 5, 6, 7}) % ANIM_EVERY == 0)
// for the check, and (atn + {1, 2, 3, 4, 5, 6, 7} inside the loop?
static uint8_t atn;
#define ANIM_EVERY (8)
if (atn % ANIM_EVERY == 0) {
if (c->is_new_col_frame || c->is_new_row_frame)
return;
for (uint8_t i = 0; i < sizeof(l->collectable_exists); ++i) {
uint8_t theByte = l->collectable_exists[i];
for (uint8_t j = 0, mask = 0x1; j < 8; ++j, mask <<= 1) {
if (theByte & mask) {
uint16_t xy_index = (uint16_t)pgm_read_word(&sorted_treasure_coords[i * 8 + j]);
uint8_t tx = LO8(xy_index);
uint8_t ty = HI8(xy_index);
if (tx >= c->x_min && tx <= c->x_max && ty >= c->y_min && ty <= c->y_max)
SetTile(tx % VRAM_TILES_H, ty % VRAM_TILES_V, ((uint8_t)pgm_read_byte(&sorted_treasure_tiles[i * 8 + j]) + atn / ANIM_EVERY) % 6);
}
}
}
}
if (++atn == (TILE_LOLLIPOP_TOP_END - TILE_LOLLIPOP_TOP_START + 1) * ANIM_EVERY)
atn = 0;
}
#if 0
// Saving this for later, when streaming level data from the SD card, also read in bitmap
// of the lollipops that are in VRAM for the given camera position, and this will be faster
__attribute__(( always_inline ))
static inline void AnimateCollectablesTracked(const CAMERA* const c, const LEVEL* const l)
{
__asm__ __volatile__ ("wdr");
static uint8_t atn;
#define ANIM_EVERY (8)
if (atn % ANIM_EVERY == 0) {
if (c->is_new_col_frame || c->is_new_row_frame)
return;
for (uint8_t i = 0; i < sizeof(l->collectable_exists); ++i) {
uint8_t theByte = l->collectable_exists[i];
uint8_t theByte2 = l->tracked_animated_treasure_tiles[i];
for (uint8_t j = 0, mask = 0x1; j < 8; ++j, mask <<= 1) {
if ((theByte & mask) && (theByte2 & mask)) {
uint16_t xy_index = (uint16_t)pgm_read_word(&sorted_treasure_coords[i * 8 + j]);
uint8_t tx = LO8(xy_index);
uint8_t ty = HI8(xy_index);
//if (tx >= c->x_min && tx <= c->x_max && ty >= c->y_min && ty <= c->y_max)
SetTile(tx % VRAM_TILES_H, ty % VRAM_TILES_V, ((uint8_t)pgm_read_byte(&sorted_treasure_tiles[i * 8 + j]) + atn / ANIM_EVERY) % 6);
}
}
}
}
if (++atn == (TILE_LOLLIPOP_TOP_END - TILE_LOLLIPOP_TOP_START + 1) * ANIM_EVERY)
atn = 0;
__asm__ __volatile__ ("wdr");
}
#endif
// Call this after calling RestoreBackground, but before calling BlitSprite
static inline void ResetUserRamTilesCountAndFreeTileIndex(void) {
extern uint8_t user_ram_tiles_c;
extern uint8_t user_ram_tiles_c_tmp;
extern uint8_t free_tile_index;
user_ram_tiles_c = user_ram_tiles_c_tmp;
free_tile_index = user_ram_tiles_c;
}
// To sum all numbers in a file (for WDR tracking over time):
// awk '{ sum += $1 } END { print sum }' file
//
// To find the maximum in a file (for WDR tracking over time):
// awk 'BEGIN {max = 0} {if ($1>max) max=$1} END {print max}' file
//
// To track WDR timings:
// uzem -f -c gamefile.uze | grep 'WDR measured' | cut -d' ' -f 3 > wdr.txt
// then use -l to play it back
int main()
{
extern uint8_t free_tile_index;
SetTileTable(tileset);
SetSpritesTileBank(0, spriteset);
InitMusicPlayer(patches);
SetUserRamTilesCount(GAME_USER_RAM_TILES_COUNT);
CAMERA c;
PLAYER p;
LEVEL l;
DISSOLVING_TILE dt;
bool music_flag = true;
memset(&p.buttons, 0, sizeof(BUTTON_INFO));
btn_level:
ClearVsyncCounter();
RestoreBackground();
if (music_flag && !IsSongPlaying())
StartSong(midisong);
Level_initFromMap(&l, map_level, &c, &p);
DissolvingTile_init(&dt);
Player_init(&p, &l);
Camera_init(&c, &l);
Camera_update(&c, &p);
Camera_fillVram(&c);
for (;;) {
// Uncomment and tweak to see how much CPU is left before we don't have enough CPU to complete
// before the next frame and things don't get drawn.
//__builtin_avr_delay_cycles(25000);
// Uncomment below to see CPU time each frame
__asm__ __volatile__ ("wdr");
#if (DEBUG_PRINT_MIN_STACK_COUNT == 1)
PrintMinStackCountIfChanged();
#endif
WaitVsync(1); __asm__ __volatile__ ("wdr");
dt.counter = GetVsyncCounter(); // depending on when the vsync interrupt happens, this can skip and affect the dissolving tiles
bool usedAllRamTilesLastFrame = (free_tile_index == RAM_TILES_COUNT);
RestoreBackground();
ReadControllers();
Entity_updateAll(&l, &c, &p);
Player_input(&p);
p.update(&p, &c, &dt, &l);
Camera_update(&c, &p);
DissolvingTile_update(&dt, &c);
AnimateCollectables(&c, &l);
// BLITTING BEGINS NOW (No more VRAM updates until the next call to RestoreBackground!)
ResetUserRamTilesCountAndFreeTileIndex();
bool wasDead = (p.dead);
Player_render(&p, &c); // either blits immediately if we are dead, or copies the background tiles into reserves ram tiles otherwise
Entity_collideAndRenderVisible(&l, &c, &p, usedAllRamTilesLastFrame);
if (!wasDead && p.visible)
BlitMyBlitMap(p.flags | SPRITE_BANK0,
p.map, BLITMAP_HEAD_NORMAL, (p.gulped) ? BLITMAP_HEAD_GULP : BLITMAP_HEAD_NORMAL,
NEAREST_SCREEN_PIXEL(p.x) - c.x,
NEAREST_SCREEN_PIXEL(p.y) - c.y + (!!(p.attacking) * !!(p.update == Player_update_ladder)));
#if 1
/* Turn to night when all lollipops are gone */ {
static uint16_t tick = 0;
bool present = false;
for (uint8_t i = 0; i < sizeof(l.collectable_exists); ++i)
if (l.collectable_exists[i])
present |= true;
if (!present) {
if (tick == 10U * 6)
DDRC = 0xBF;
else if (tick == 20U * 6)
DDRC = 0x7F;
else if (tick == 30U * 6)
DDRC = 0xDF;
else if (tick == 40U * 6)
DDRC = 0x9F;
else if (tick == 50U * 6)
DDRC = 0x5F;
else if (tick == 60U * 6)
DDRC = 0x1E;
else if (tick == 70U * 6)
DDRC = 0x1D;
tick += 10;
if (tick > 80U * 6)
tick = 80U * 6;
} else {
tick = 0;
DDRC = p.ddrc_num;
}
}
#endif
// Proof of concept for smooth day/night transitions by setting a bitmask on the video out port
#if 0
// FF -> BF -> 7F -> 3F -> 1F
static uint16_t tick = 660U * 6;
if (tick == 10U * 6)
DDRC = 0xBF;
else if (tick == 20U * 6)
DDRC = 0x7F;
else if (tick == 30U * 6)
DDRC = 0xDF;
else if (tick == 40U * 6)
DDRC = 0x9F;
else if (tick == 50U * 6)
DDRC = 0x5F;
else if (tick == 60U * 6)
DDRC = 0x1E;
else if (tick == 70U * 6)
DDRC = 0x1D;
else if (tick == 600U * 6)
DDRC = 0x1E;
else if (tick == 610U * 6)
DDRC = 0x5F;
else if (tick == 620U * 6)
DDRC = 0x9F;
else if (tick == 630U * 6)
DDRC = 0xDF;
else if (tick == 640U * 6)
DDRC = 0x7F;
else if (tick == 650U * 6)
DDRC = 0xBF;
else if (tick == 660U * 6)
DDRC = 0xFF;
else if (tick == 1180U * 6)
tick = 0;
tick += 10;
#endif
////////////////////////////////////////////////////////////////////////////////
// Begin Process Popup Menu
////////////////////////////////////////////////////////////////////////////////
if (p.buttons.pressed & BTN_START) {
if (p.dead)
goto btn_level;
#define MENU_WIDTH (19)
#define MENU_HEIGHT (5)
#define MENU_ITEMS (3)
#define MENU_SELECTION_RESTART_LEVEL (1)
#define MENU_SELECTION_MUSIC (2)
#define MENU_START_X ((SCREEN_TILES_H - MENU_WIDTH) / 2)
#define MENU_START_Y ((SCREEN_TILES_V - MENU_HEIGHT) / 2)
#define SPRITE_SLIDER_ON_START (1)
#define RAM_TILE_SLIDER_ON_START (GAME_USER_RAM_TILES_COUNT + RF_POPUP_LEN + RF_POPUP_BORDER_LEN)
#define RAM_TILE_SLIDER_ON_END (RAM_TILE_SLIDER_ON_START + 1)
#define RAM_TILE_SLIDER_OFF_START (RAM_TILE_SLIDER_ON_START + 2)
#define RAM_TILE_SLIDER_OFF_END (RAM_TILE_SLIDER_ON_START + 3)
RestoreBackground();
// Create a backup of the current state of the buttons and VsyncCounter
BUTTON_INFO backupBI;
memcpy(&backupBI, &p.buttons, sizeof(BUTTON_INFO));
uint16_t backupVsyncCounter = GetVsyncCounter();
// Figure out where to draw the popup menu
uint8_t menu_tx = ((c.x / TILE_WIDTH) + !!(c.x % TILE_WIDTH) + MENU_START_X) % VRAM_TILES_H;
uint8_t menu_ty = ((c.y / TILE_HEIGHT) + !!(c.y % TILE_HEIGHT) + MENU_START_Y) % VRAM_TILES_V;
TriggerFx(FX_MENU, 48, true);
// Save what is behind the popup menu
uint8_t menu_backing[MENU_HEIGHT][MENU_WIDTH];
for (uint8_t y = 0; y < MENU_HEIGHT; ++y)
for (uint8_t x = 0; x < MENU_WIDTH; ++x)
menu_backing[y][x] = GetTile((menu_tx + x) % VRAM_TILES_H, (menu_ty + y) % VRAM_TILES_V);
// Ensure the sprites have a chance to hide before we reuse their ram tiles, avoiding glitches
WaitVsync(1);
// Allow the popup menu to use all the ram tiles
SetUserRamTilesCount(RAM_TILES_COUNT);
// Load the popup menu background/text into ram tiles starting at 0
RamFont_Load(rf_popup, GAME_USER_RAM_TILES_COUNT, RF_POPUP_LEN, 0xFF, 0x00);
// Load the popup menu border after that (starting at the user ram tile: RF_POPUP_LEN) with a different fg color
RamFont_Load(rf_popup_border, GAME_USER_RAM_TILES_COUNT + RF_POPUP_LEN, RF_POPUP_BORDER_LEN, 0xF6, 0x00);
// Make the top right and bottom left pixels of the border "transparent"
uint8_t bgTile = GetTile((menu_tx + MENU_WIDTH - 1) % VRAM_TILES_H, menu_ty);
char bgTilePixel = pgm_read_byte(tileset + bgTile * 64 + 7); // 7 is top right pixel
uint8_t* ramTile = GetUserRamTile(RF_B_TR); // top right corner in rf_popup
ramTile[7] = bgTilePixel; // top right pixel of ramTile
bgTile = GetTile(menu_tx, (menu_ty + MENU_HEIGHT - 1) % VRAM_TILES_V);
bgTilePixel = pgm_read_byte(tileset + bgTile * 64 + 56); // 56 is bottom left pixel
ramTile = GetUserRamTile(RF_B_BL); // bottom left corner in rf_popup
ramTile[56] = bgTilePixel; // bottom left pixel of ramTile
// Copy the on/off sliders into user ram tiles (temporarily swapping tile/sprite pointers to use CopyFlashTile)
SetTileTable(spriteset);
for (uint8_t i = 0; i < 4; ++i)
CopyFlashTile(SPRITE_SLIDER_ON_START + i, RAM_TILE_SLIDER_ON_START + i);
SetTileTable(tileset);
// Manually draw the menu background/borders because Fill() does not work across the edges of VRAM when scrolled
for (uint8_t y = 1; y < MENU_HEIGHT - 1; ++y)
for (uint8_t x = 1; x < MENU_WIDTH - 1; ++x)
SetRamTile((menu_tx + x) % VRAM_TILES_H, (menu_ty + y) % VRAM_TILES_V, RF_BG);
for (uint8_t x = 1; x < MENU_WIDTH - 1; ++x) {
SetRamTile((menu_tx + x) % VRAM_TILES_H, menu_ty, RF_B_T);
SetRamTile((menu_tx + x) % VRAM_TILES_H, (menu_ty + MENU_HEIGHT - 1) % VRAM_TILES_V, RF_B_B);
}
for (uint8_t y = 1; y < MENU_HEIGHT - 1; ++y) {
SetRamTile(menu_tx, (menu_ty + y) % VRAM_TILES_V, RF_B_L);
SetRamTile((menu_tx + MENU_WIDTH - 1) % VRAM_TILES_H, (menu_ty + y) % VRAM_TILES_V, RF_B_R);
}
SetRamTile(menu_tx, menu_ty, RF_B_TL);
SetRamTile((menu_tx + MENU_WIDTH - 1) % VRAM_TILES_H, menu_ty, RF_B_TR);
SetRamTile(menu_tx, (menu_ty + MENU_HEIGHT - 1) % VRAM_TILES_V, RF_B_BL);
SetRamTile((menu_tx + MENU_WIDTH - 1) % VRAM_TILES_H, (menu_ty + MENU_HEIGHT - 1) % VRAM_TILES_V, RF_B_BR);
// Print the menu text
RamFont_Print(menu_tx + 5, menu_ty + 1, RF_RESUME, sizeof(RF_RESUME));
RamFont_Print(menu_tx + 5, menu_ty + 2, RF_RESTART_LEVEL, sizeof(RF_RESTART_LEVEL));
RamFont_Print(menu_tx + 5, menu_ty + 3, RF_MUSIC, sizeof(RF_MUSIC));
// Draw the initial position of the slider based on whether or not music is currently playing
uint8_t slider_tile_start = IsSongPlaying() ? RAM_TILE_SLIDER_ON_START : RAM_TILE_SLIDER_OFF_START;
for (uint8_t x = 0; x < 2; ++x)
SetRamTile((menu_tx + 5 + sizeof(RF_MUSIC) + 1 + x) % VRAM_TILES_H, (menu_ty + 3) % VRAM_TILES_V, slider_tile_start + x);
int8_t prev_selection;
int8_t selection = 0;
bool confirmed = false;
// The popup menu has its own run loop
for (;;) {
WaitVsync(1);
ReadControllers();
p.buttons.prev = p.buttons.held;
p.buttons.held = ReadJoypad(0);
p.buttons.pressed = p.buttons.held & (p.buttons.held ^ p.buttons.prev);
p.buttons.released = p.buttons.prev & (p.buttons.held ^ p.buttons.prev);
SetRamTile((menu_tx + 2) % VRAM_TILES_H, (menu_ty + 1 + selection) % VRAM_TILES_V, RF_ASTERISK);
prev_selection = selection;
if (p.buttons.pressed & BTN_START) {
confirmed = true;
break;
}
if (p.buttons.pressed & BTN_UP) {
if (selection > 0)
selection--;
} else if (p.buttons.pressed & BTN_DOWN) {
if (selection < MENU_ITEMS - 1)
selection++;
}
if (selection != prev_selection) {
TriggerFx(FX_MENU, 48, true);
SetRamTile((menu_tx + 2) % VRAM_TILES_H, (menu_ty + 1 + prev_selection) % VRAM_TILES_V, RF_BG);
SetRamTile((menu_tx + 2) % VRAM_TILES_H, (menu_ty + 1 + selection) % VRAM_TILES_V, RF_ASTERISK);
prev_selection = selection;
}
if (selection == MENU_SELECTION_MUSIC) {
if (IsSongPlaying()) {
SetRamTile((menu_tx + 3) % VRAM_TILES_H, (menu_ty + 1 + selection) % VRAM_TILES_V, RF_BG);
if (p.buttons.pressed & BTN_LEFT) {
TriggerFx(FX_MENU, 48, true);
SetRamTile((menu_tx + 5 + sizeof(RF_MUSIC) + 1) % VRAM_TILES_H, (menu_ty + 3) % VRAM_TILES_V, RAM_TILE_SLIDER_OFF_START);
SetRamTile((menu_tx + 5 + sizeof(RF_MUSIC) + 2) % VRAM_TILES_H, (menu_ty + 3) % VRAM_TILES_V, RAM_TILE_SLIDER_OFF_END);
StopSong();
music_flag = false;
}
} else {
SetRamTile((menu_tx + 1) % VRAM_TILES_H, (menu_ty + 1 + selection) % VRAM_TILES_V, RF_BG);
if (p.buttons.pressed & BTN_RIGHT) {
TriggerFx(FX_MENU, 48, true);
SetRamTile((menu_tx + 5 + sizeof(RF_MUSIC) + 1) % VRAM_TILES_H, (menu_ty + 3) % VRAM_TILES_V, RAM_TILE_SLIDER_ON_START);
SetRamTile((menu_tx + 5 + sizeof(RF_MUSIC) + 2) % VRAM_TILES_H, (menu_ty + 3) % VRAM_TILES_V, RAM_TILE_SLIDER_ON_END);
ResumeSong();
music_flag = true;
}
}
}
} // end popup menu run loop
// Restore the user ram tiles count to what it normally is
SetUserRamTilesCount(GAME_USER_RAM_TILES_COUNT);
// Restore what was behind the popup menu
for (uint8_t y = 0; y < MENU_HEIGHT; ++y)
for (uint8_t x = 0; x < MENU_WIDTH; ++x)
SetTile((menu_tx + x) % VRAM_TILES_H, (menu_ty + y) % VRAM_TILES_V, menu_backing[y][x]);
TriggerFx(FX_MENU, 48, true);
// Restore the backed up button state
memcpy(&p.buttons, &backupBI, sizeof(BUTTON_INFO));
if (confirmed && selection == MENU_SELECTION_RESTART_LEVEL) {
#if (DEBUG_PRINT_KILLED_BY == 1)
UZEMCHR = 'R'; UZEMCHR = 'e'; UZEMCHR = 's'; UZEMCHR = 't'; UZEMCHR = 'a'; UZEMCHR = 'r'; UZEMCHR = 't'; UZEMCHR = '\n';
#endif
goto btn_level;
}
// Restore the backed up VsyncCounter so time doesn't advance for the entities while the game is paused
SetVsyncCounter(backupVsyncCounter);
}
////////////////////////////////////////////////////////////////////////////////
// End Process Popup Menu
////////////////////////////////////////////////////////////////////////////////
} // end main run loop
}