376 lines
11 KiB
C
376 lines
11 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 "render.h"
|
|
#include "game.h"
|
|
#include "anim.h"
|
|
|
|
#include <gint/display.h>
|
|
#include <gint/defs/util.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 void render_map_layer(map_t const *m, camera_t const *c, int layer)
|
|
{
|
|
/* Render floor and walls */
|
|
for(int row = -2; row < m->height + 2; row++)
|
|
for(int col = -1; col < m->width + 1; col++) {
|
|
tile_t *t = map_tile(m, col, row);
|
|
vec2 tile_pos = { fix(col), fix(row) };
|
|
ivec2 p = camera_map2screen(c, tile_pos);
|
|
|
|
if(!t && layer == CEILING) {
|
|
drect(p.x, p.y, p.x+15, p.y+15, C_BLACK);
|
|
continue;
|
|
}
|
|
if(t->plane != layer)
|
|
continue;
|
|
|
|
dsubimage(p.x, p.y, m->tileset,
|
|
TILE_WIDTH * (t->base % 8), TILE_HEIGHT * (t->base / 8),
|
|
TILE_WIDTH, TILE_HEIGHT, DIMAGE_NOCLIP);
|
|
if(t->decor) dsubimage(p.x, p.y, m->tileset,
|
|
TILE_WIDTH * (t->decor % 8), TILE_HEIGHT * (t->decor / 8),
|
|
TILE_WIDTH, TILE_HEIGHT, DIMAGE_NOCLIP);
|
|
}
|
|
}
|
|
|
|
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, 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);
|
|
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);
|
|
}
|
|
|
|
void render_game(game_t const *g, bool show_hitboxes)
|
|
{
|
|
camera_t const *camera = &g->camera;
|
|
|
|
/* Render map floor and floor entities */
|
|
render_map_layer(g->map, camera, HORIZONTAL);
|
|
render_entities(g, camera, floor_depth_measure, 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, VERTICAL);
|
|
render_entities(g, camera, wall_depth_measure, show_hitboxes);
|
|
|
|
/* Render ceiling tiles (including out of bounds) and ceiling entities */
|
|
render_map_layer(g->map, camera, CEILING);
|
|
render_entities(g, camera, ceiling_depth_measure, show_hitboxes);
|
|
|
|
extern font_t font_rogue;
|
|
font_t const *old_font = dfont(&font_rogue);
|
|
|
|
/* Render wave progress bar */
|
|
level_wave_t const *wave = game_current_wave(g);
|
|
int wave_enemies = 0;
|
|
for(int i = 0; i < wave->entry_count; i++)
|
|
wave_enemies += wave->entries[i].amount;
|
|
dprint_opt(DWIDTH/2, 2, C_WHITE, C_NONE, DTEXT_CENTER, DTEXT_TOP,
|
|
"Wave %d: Spawned %d/%d, time %d/%d+%d s",
|
|
g->wave, g->wave_spawned, wave_enemies,
|
|
fround(g->time_wave), wave->duration, wave->delay_after);
|
|
|
|
/* Render HUD */
|
|
extern bopti_image_t img_hud;
|
|
dimage(0, DHEIGHT - img_hud.height, &img_hud);
|
|
|
|
fighter_t *player_f = getcomp(g->player, fighter);
|
|
mechanical_t *player_m = getcomp(g->player, mechanical);
|
|
|
|
/* 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, DHEIGHT - 5 - fill_height, &img_hud_life,
|
|
0, img_hud_life.height - fill_height, img_hud_life.width, fill_height,
|
|
DIMAGE_NONE);
|
|
|
|
/* 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 < 6; i++) {
|
|
/* Activity and cooldown */
|
|
fixed_t cooldown_total=0, cooldown_remaining=0;
|
|
if(i == 0 && player_m->dash < 0) {
|
|
cooldown_total = player_m->limits->dash_cooldown;
|
|
cooldown_remaining = -player_m->dash;
|
|
}
|
|
else if(i > 0) {
|
|
cooldown_total = player_f->actions_cooldown_total[i-1];
|
|
cooldown_remaining = player_f->actions_cooldown[i-1];
|
|
}
|
|
|
|
int x = 31 + 48*i + 64*(i>=3);
|
|
int y = DHEIGHT - 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;
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
dfont(old_font);
|
|
}
|
|
|
|
void render_pfg_all2one(pfg_all2one_t const *paths, camera_t const *c)
|
|
{
|
|
for(int row = 0; row < paths->map->height; row++)
|
|
for(int col = 0; col < paths->map->width; col++) {
|
|
tile_t *tile = map_tile(paths->map, col, row);
|
|
if(!tile || tile->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));
|
|
}
|
|
}
|
|
}
|