RogueLife/src/render.c

594 lines
18 KiB
C

#include "comp/entity.h"
#include "comp/physical.h"
#include "comp/visible.h"
#include "comp/mechanical.h"
#include "comp/fighter.h"
#include "comp/particle.h"
#include "enemies.h"
#include "render.h"
#include "skills.h"
#include "game.h"
#include "anim.h"
#include <gint/display.h>
#include <gint/defs/util.h>
#include <stdio.h>
#include <libprof.h>
//---
// Camera management
//---
void camera_init(camera_t *c, map_t const *m)
{
c->zoom = 1;
c->limits.x_min = fix(-CAMERA_BORDER);
c->limits.x_max = fix(m->width + CAMERA_BORDER);
c->limits.y_min = fix(-CAMERA_BORDER);
c->limits.y_max = fix(m->height + CAMERA_BORDER);
/* Fullscreen */
c->viewport.x_min = 0;
c->viewport.x_max = DWIDTH;
c->viewport.y_min = 0;
c->viewport.y_max = DHEIGHT;
c->width = fix(c->viewport.x_max - c->viewport.x_min) / TILE_WIDTH;
c->height = fix(c->viewport.y_max - c->viewport.y_min) / TILE_HEIGHT;
c->x = (c->limits.x_min + c->limits.x_max) / 2;
c->y = (c->limits.y_min + c->limits.y_max) / 2;
/* Vertical adjustment to add space for the HUD */
c->y -= fix(0);
}
ivec2 camera_map2screen(camera_t const *c, vec2 p)
{
return (ivec2){
.x = DWIDTH / 2 + fround((p.x - c->x) * TILE_WIDTH * c->zoom),
.y = DHEIGHT / 2 + fround((p.y - c->y) * TILE_HEIGHT * c->zoom),
};
}
/* Translate screen coordinates to map coordinates */
vec2 camera_screen2map(camera_t const *c, ivec2 p)
{
return (vec2){
.x = c->x + fix(p.x - DWIDTH / 2) / TILE_WIDTH / c->zoom,
.y = c->y + fix(p.y - DHEIGHT / 2) / TILE_HEIGHT / c->zoom,
};
}
/* Lock the camera at the center if set by settings. */
static bool camera_lock(camera_t *c)
{
if(c->zoom == 1 && CAMERA_LOCK_AT_x1)
{
c->x = (c->limits.x_min + c->limits.x_max) / 2;
c->y = (c->limits.y_min + c->limits.y_max) / 2;
return true;
}
return false;
}
/* Bound the camera to the map limits */
static void camera_bound(camera_t *c)
{
/* Project top left and bottom-right corners of viewport onto the map */
ivec2 v_tl = { c->viewport.x_min, c->viewport.y_min };
ivec2 v_br = { c->viewport.x_max, c->viewport.y_max };
vec2 m_tl = camera_screen2map(c, v_tl);
vec2 m_br = camera_screen2map(c, v_br);
/* Bound viewport to map limits */
if(m_tl.x < c->limits.x_min)
c->x += (c->limits.x_min - m_tl.x);
else if(m_br.x > c->limits.x_max)
c->x += (c->limits.x_max - m_br.x);
if(m_tl.y < c->limits.y_min)
c->y += (c->limits.y_min - m_tl.y);
else if(m_br.y > c->limits.y_max)
c->y += (c->limits.y_max - m_br.y);
}
void camera_move(camera_t *c, map_coord_t dx, map_coord_t dy)
{
if(camera_lock(c)) return;
c->x += dx;
c->y += dy;
camera_bound(c);
}
void camera_zoom(camera_t *c, int zoom)
{
if(zoom < ZOOM_MIN) zoom = ZOOM_MIN;
if(zoom > ZOOM_MAX) zoom = ZOOM_MAX;
c->zoom = zoom;
if(camera_lock(c)) return;
camera_bound(c);
}
fixed_t camera_ppu(camera_t const *c)
{
/* Since this assumes isotropic space we can use TILE_WIDTH or TILE_HEIGHT
indifferently (they're assumed equal) */
return fix(1) * c->zoom * TILE_WIDTH;
}
//---
// Rendering
//---
static inline void render_tile(int x, int y, tileset_t const *tileset,
int tile_id, int time_ms, int flags)
{
/* If the tile is animated, find the position in the cycle */
if(tileset->tiles[tile_id].anim_length > 0) {
int start = tileset->tiles[tile_id].anim_start;
tile_animation_frame_t *frames = &tileset->anim[start];
int cycle_duration_ms = 0;
for(int i = 0; i < tileset->tiles[tile_id].anim_length; i++)
cycle_duration_ms += frames[i].duration_ms;
time_ms %= cycle_duration_ms;
int i = 0;
while(time_ms >= 0) {
time_ms -= frames[i].duration_ms;
i++;
}
tile_id = frames[i-1].tile_id;
}
dsubimage(x, y, tileset->sheet,
TILE_WIDTH * (tile_id % tileset->width),
TILE_HEIGHT * (tile_id / tileset->width),
TILE_WIDTH, TILE_HEIGHT, flags);
}
void render_map_layer(map_t const *map, camera_t const *c, int ss_x, int ss_y,
int layer, uint16_t *map_anim, int flags)
{
/* Render floor and walls */
for(int row = -2; row < map->height + 2; row++)
for(int col = -1; col < map->width + 1; col++) {
map_cell_t *cell = map_cell(map, col, row);
vec2 tile_pos = { fix(col), fix(row) };
ivec2 p = camera_map2screen(c, tile_pos);
p.x += ss_x;
p.y += ss_y;
if(!cell) {
if(layer == CEILING)
drect(p.x, p.y, p.x+15, p.y+15, C_BLACK);
continue;
}
int time_ms = map_anim ? map_anim[map->width * row + col] : 0;
if(map->tileset->tiles[cell->base].plane == layer)
render_tile(p.x, p.y, map->tileset, cell->base, time_ms, flags);
if(cell->decor && map->tileset->tiles[cell->decor].plane == layer)
render_tile(p.x, p.y, map->tileset, cell->decor, time_ms, flags);
}
}
static int depth_measure(entity_t const *e, int direction)
{
particle_t *p = getcomp(e, particle);
if(p) {
return (p->plane != direction) ? -1 : p->y;
}
visible_t *v = getcomp(e, visible);
if(v) {
return (v->sprite_plane != direction) ? -1 : getcomp(e, physical)->y;
}
return -1;
}
static int floor_depth_measure(entity_t const *e)
{
return depth_measure(e, HORIZONTAL);
}
static int wall_depth_measure(entity_t const *e)
{
return depth_measure(e, VERTICAL);
}
static int ceiling_depth_measure(entity_t const *e)
{
return depth_measure(e, CEILING);
}
static void render_shadow(int cx, int cy, int shadow_size)
{
if(shadow_size < 1 || shadow_size > 4)
return;
static const int hw_array[] = { 2, 3, 5, 7 };
static const int hh_array[] = { 1, 1, 2, 3 };
/* TODO: render_shadow(): Encode shadow shapes properly x) */
static const uint8_t profile_1[] = { 1, 0, 1 };
static const uint8_t profile_2[] = { 1, 0, 1 };
static const uint8_t profile_3[] = { 3, 1, 0, 1, 3 };
static const uint8_t profile_4[] = { 4, 2, 1, 0, 1, 2, 4 };
static const uint8_t *profile_array[] = {
profile_1, profile_2, profile_3, profile_4,
};
int hw = hw_array[shadow_size - 1];
int hh = hh_array[shadow_size - 1];
uint8_t const *profile = profile_array[shadow_size - 1] + hh;
int xmin = max(-hw, -cx);
int xmax = min(hw, DWIDTH-1 - cx);
int ymin = max(-hh, -cy);
int ymax = min(hh, DHEIGHT-1 - cy);
for(int y = ymin; y <= ymax; y++)
for(int x = xmin; x <= xmax; x++) {
if(hw - abs(x) < profile[y])
continue;
int i = DWIDTH * (cy+y) + (cx+x);
gint_vram[i] = (gint_vram[i] & 0xf7de) >> 1;
}
}
static void render_entities(game_t const *g, camera_t const *camera,
entity_measure_t *measure, int ss_x, int ss_y, bool show_hitboxes)
{
uint16_t *rendering_order;
int count = game_sort_entities(g, measure, &rendering_order);
for(int i = 0; i < count; i++) {
entity_t *e = g->entities[rendering_order[i]];
physical_t *p = getcomp(e, physical);
visible_t *v = getcomp(e, visible);
particle_t *pt = getcomp(e, particle);
vec2 xy;
fixed_t z;
if(pt) {
xy = (vec2) { pt->x, pt->y };
z = pt->z;
}
else {
xy = physical_pos(e);
z = v->z;
}
ivec2 scr = camera_map2screen(camera, xy);
scr.x += ss_x;
scr.y += ss_y;
int elevated_y = scr.y - fround(16 * z);
/* Show shadow */
if(v && v->shadow_size) {
render_shadow(scr.x, scr.y, v->shadow_size);
}
/* Show entity sprite */
if(v && v->anim.frame) {
anim_frame_render(scr.x, elevated_y, v->anim.frame);
}
/* Show entity hitbox in the map coordinate system */
if(v && (!v->anim.frame || show_hitboxes)) {
rect r = p->hitbox;
r = rect_scale(r, camera_ppu(camera));
r = rect_translate(r, vec_i2f(scr));
rect_draw(r, C_BLUE);
/* Show entity center */
dline(scr.x-1, scr.y, scr.x+1, scr.y, C_BLUE);
dline(scr.x, scr.y-1, scr.x, scr.y+1, C_BLUE);
}
/* Show particle */
if(pt) {
particle_render(scr.x, scr.y, pt);
}
}
free(rendering_order);
}
static int render_info_delay(int x, int y, level_event_t const *event)
{
extern bopti_image_t img_hud_delay;
dimage(x, y, &img_hud_delay);
return img_hud_delay.width;
}
static int render_info_wave(int x, int y, level_event_t const *event,
uint8_t *wave_left)
{
level_wave_t const *wave = event->wave;
int x0 = x;
y += 4;
for(int i = 0; i < wave->entry_count; i++) {
enemy_t const *enemy = enemy_data(wave->entries[i].identity);
int amount_total = wave->entries[i].amount;
int amount = wave_left ? wave_left[i] : amount_total;
int text_w;
font_damage_size(amount_total, &text_w, NULL);
anim_frame_t *frame = enemy->anim_idle->start[0];
dsubimage(x, y - frame->h / 2, frame->sheet,
frame->x, frame->y, frame->w, frame->h, DIMAGE_NONE);
font_damage_print(x + frame->w - 4, y + frame->h / 2 - 4, C_WHITE,
DTEXT_LEFT, DTEXT_TOP, amount);
x += frame->w - 6 + text_w;
}
return x-x0;
}
static int render_info_item(int x, int y, level_event_t const *event)
{
int w;
dsize("Item", NULL, &w, NULL);
dprint(x, y, C_WHITE, "Item");
return w;
}
static void render_info(int x, int y, game_t const *g)
{
if(!g->level)
return;
int intro_w;
char str[32];
snprintf(str, 32, "Wave %d", level_wave_count(g->level));
dsize(str, NULL, &intro_w, NULL);
snprintf(str, 32, "Wave %d", g->wave_number);
dtext(x+8, y-1, C_WHITE, str);
x += intro_w + 20;
int sep_w;
dsize(">", NULL, &sep_w, NULL);
for(int i = g->event; i <= g->level->event_count && x <= DWIDTH; i++) {
if(i == g->level->event_count) {
extern bopti_image_t img_hud_flag;
dimage(x+1, y, &img_hud_flag);
// TODO: If victory reached, write "Victory!"
break;
}
level_event_t const *event = &g->level->events[i];
if(event->type == LEVEL_EVENT_DELAY)
x += render_info_delay(x, y, event);
if(event->type == LEVEL_EVENT_WAVE) {
uint8_t *wave_left = (i == g->event) ? g->wave_left : NULL;
x += render_info_wave(x, y, event, wave_left);
}
if(event->type == LEVEL_EVENT_ITEM)
x += render_info_item(x, y, event);
x += 12;
dprint(x, y, C_WHITE, ">");
x += sep_w + 12;
}
}
void render_window(int x, int y, int w, int h)
{
extern bopti_image_t img_hud_window;
w = max(w, 40);
h = max(h, 22);
int slices = w / 20 - 2;
w = (slices + 2) * 20;
/* Shaded background */
int x0=x, x1=x+w-2;
x0 = (x0 + (x0 & 1)) >> 1;
x1 = (x1 + (x1 & 1)) >> 1;
uint32_t *vram = (void *)gint_vram + (DWIDTH*2) * (y+9);
for(int vy = 0; vy <= h-18; vy++) {
for(int vx = x0; vx <= x1; vx++) {
vram[vx] = (vram[vx] & 0xf7def7de) >> 1;
vram[vx] = (vram[vx] & 0xf7def7de) >> 1;
}
vram += DWIDTH / 2;
}
/* Top section */
dsubimage(x, y, &img_hud_window, 0, 0, 20, 11, DIMAGE_NONE);
for(int i = 0; i < slices; i++)
dsubimage(x+20*(i+1), y, &img_hud_window, 20, 0, 20, 11, DIMAGE_NONE);
dsubimage(x+w-20, y, &img_hud_window, 40, 0, 20, 11, DIMAGE_NONE);
/* Vertical bars */
int color = RGB24(0x66686f);
dline(x, y+10, x, y+h-11, color);
dline(x+w-1, y+10, x+w-1, y+h-11, color);
/* Bottom section */
dsubimage(x, y+h-11, &img_hud_window, 0, 11, 20, 11, DIMAGE_NONE);
for(int i = 0; i < slices; i++)
dsubimage(x+20*(i+1), y+h-11, &img_hud_window, 20, 11, 20, 11,
DIMAGE_NONE);
dsubimage(x+w-20, y+h-11, &img_hud_window, 40, 11, 20, 11, DIMAGE_NONE);
}
uint32_t time_render_map = 0;
uint32_t time_render_hud = 0;
void render_game(game_t const *g, bool show_hitboxes)
{
camera_t const *camera = &g->camera;
/* Screenshake displacement */
int ss_x=0, ss_y=0;
if(g->screenshake_duration > 0) {
int amp = g->screenshake_amplitude;
ss_x = rand() % amp - (amp/2);
ss_y = rand() % amp - (amp/2);
}
prof_t ctx = prof_make();
prof_enter(ctx);
/* Render map floor and floor entities */
render_map_layer(g->map, camera, ss_x, ss_y, HORIZONTAL, g->map_anim,
DIMAGE_NOCLIP);
render_entities(g, camera, floor_depth_measure, ss_x, ss_y, show_hitboxes);
/* Render map walls and vertical entities
TODO ECS: Sort walls and wall entities together for proper ordering!*/
render_map_layer(g->map, camera, ss_x, ss_y, VERTICAL, g->map_anim,
DIMAGE_NOCLIP);
render_entities(g, camera, wall_depth_measure, ss_x, ss_y, show_hitboxes);
/* Render ceiling tiles (including out of bounds) and ceiling entities */
render_map_layer(g->map, camera, ss_x, ss_y, CEILING, g->map_anim,
DIMAGE_NOCLIP);
render_entities(g, camera, ceiling_depth_measure, ss_x,ss_y,show_hitboxes);
prof_leave(ctx);
time_render_map = prof_time(ctx);
extern font_t font_rogue;
font_t const *old_font = dfont(&font_rogue);
ctx = prof_make();
prof_enter(ctx);
/* GUI positioning variables used during level entry */
fixed_t MAX_GUI_TIME = fix(0.75);
fixed_t gui_time = MAX_GUI_TIME;
if(g->time_total < MAX_GUI_TIME)
gui_time = g->time_total;
int HUD_Y = cubic(DHEIGHT+30, DHEIGHT, gui_time, MAX_GUI_TIME);
int HEADER_Y = cubic(-15, 2, gui_time, MAX_GUI_TIME);
/* Render wave information */
render_info(0, HEADER_Y, g);
/* Render HUD */
extern bopti_image_t img_hud;
dimage(0, HUD_Y - img_hud.height, &img_hud);
fighter_t *player_f = getcomp(g->player, fighter);
extern font_t font_hud;
player_data_t *player_data = player_f->player;
dfont(&font_hud);
dprint_opt(349, HUD_Y - 5, RGB24(0x15171a), C_NONE, DTEXT_CENTER,
DTEXT_TOP, "%d", player_data->xp_level);
dprint_opt(349, HUD_Y - 6, RGB24(0xabb1ba), C_NONE, DTEXT_CENTER,
DTEXT_TOP, "%d", player_data->xp_level);
dfont(&font_rogue);
/* Render life bar */
extern bopti_image_t img_hud_life;
int fill_height = (img_hud_life.height * player_f->HP) / player_f->HP_max;
dsubimage(184, HUD_Y - 5 - fill_height, &img_hud_life,
0, img_hud_life.height - fill_height, img_hud_life.width, fill_height,
DIMAGE_NONE);
/* Render XP bar. The following values indicate the geometry of the XP bar
so that we can show a partially-filled version of it */
if(anim_in(g->hud_xp_anim.frame, &anims_hud_xp_Explode, 0)) {
anim_frame_render(343, HUD_Y-32, g->hud_xp_anim.frame);
}
else {
static int const XP_FULL=22;
int xp_current = player_data->xp_current;
int xp_total = max(player_data->xp_to_next_level, 1);
int fill_height = XP_FULL * xp_current / xp_total;
anim_frame_subrender(343, HUD_Y-32, g->hud_xp_anim.frame,
0, XP_FULL - fill_height, -1, fill_height);
}
/* Render skill icons */
extern bopti_image_t img_skillicons;
static const int skill_size = 23;
static const int skill_box_size = 27;
for(int i = 0; i < 4; i++) {
/* Activity and cooldown */
fixed_t cooldown_total = skill_cooldown(player_f->skills[i+1]);
fixed_t cooldown_remaining = player_f->actions_cooldown[i+1];
int x = 31 + 48*i + 64*(i>=3);
int y = HUD_Y - 33;
int bg = (cooldown_remaining != 0);
dsubimage(x+2, y+2, &img_skillicons, skill_size * bg, 0, skill_size,
skill_size, DIMAGE_NONE);
dsubimage(x+2, y+2, &img_skillicons, skill_size * (i+2), 0, skill_size,
skill_size, DIMAGE_NONE);
/* Darken the area representing remaining cooldown */
if(cooldown_total != 0) {
int height = (cooldown_remaining*skill_box_size) / cooldown_total;
int ymin = y + skill_box_size - height;
int ymax = y + skill_box_size;
if(ymin >= DHEIGHT) ymin = DHEIGHT;
if(ymax >= DHEIGHT) ymax = DHEIGHT;
for(int y1 = ymin; y1 < ymax; y1++) {
for(int x1 = x; x1 < x + skill_box_size; x1++) {
int i = DWIDTH * y1 + x1;
gint_vram[i] = ~((~gint_vram[i] & 0xf7de) >> 1);
}
}
}
}
prof_leave(ctx);
time_render_hud = prof_time(ctx);
dfont(old_font);
}
void render_pfg_all2one(pfg_all2one_t const *paths, camera_t const *c,
uint8_t *occupation)
{
for(int row = 0; row < paths->map->height; row++)
for(int col = 0; col < paths->map->width; col++) {
map_cell_t *cell = map_cell(paths->map, col, row);
if(!cell)
continue;
if(paths->map->tileset->tiles[cell->base].solid)
continue;
if(cell->decor && paths->map->tileset->tiles[cell->decor].solid)
continue;
vec2 fp = vec_i2f_center((ivec2){ col, row });
ivec2 p = camera_map2screen(c, fp);
int dir = paths->direction[row * paths->map->width + col];
if(dir == -1) {
drect(p.x-1, p.y-1, p.x+1, p.y+1, C_RGB(31, 0, 31));
}
else {
ivec2 v = vec_f2i(fdir(dir));
int x2 = p.x + 8*v.x;
int y2 = p.y + 8*v.y;
int dx = 3*v.x, dy = 3*v.y;
dline(p.x, p.y, x2, y2, C_RGB(31, 0, 31));
dline(x2, y2, x2-dx-dy, y2-dy-dx, C_RGB(31, 0, 31));
dline(x2, y2, x2-dx+dy, y2-dy+dx, C_RGB(31, 0, 31));
}
int occ = occupation ? occupation[paths->map->width * row + col] : -1;
if(occ > 0) {
dprint_opt(p.x, p.y, C_WHITE, C_NONE, DTEXT_CENTER, DTEXT_MIDDLE,
"%d", occ);
}
}
}