#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 #include #include #include //--- // 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 void render_wave_info(game_t const *g) { level_t const *level = g->level; 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(2, 2, C_WHITE, "Wave %d (%d left)", g->wave, wave_enemies - g->wave_spawned); if(g->wave + 1 > level->wave_count) return; level_wave_t const *next_wave = &level->waves[g->wave+1 - 1]; int x = DWIDTH - 2; int y = 10; for(int i = next_wave->entry_count - 1; i >= 0; i--) { enemy_t const *enemy = enemy_data(next_wave->entries[i].identity); int amount = next_wave->entries[i].amount; int text_w; font_damage_size(amount, &text_w, NULL); anim_frame_t *frame = enemy->anim_idle->start[0]; dsubimage(x - frame->w - text_w + 4, y - frame->h / 2, frame->sheet, frame->x, frame->y, frame->w, frame->h, DIMAGE_NONE); font_damage_print(x - text_w, y + frame->h / 2 - 4, C_WHITE, DTEXT_LEFT, DTEXT_TOP, amount); x = x - (text_w-4) - frame->w - 4; } dtext_opt(x-2, y, C_WHITE, C_NONE, DTEXT_RIGHT, DTEXT_MIDDLE, "Next:"); } 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); /* Render wave information */ render_wave_info(g); /* Render HUD */ extern bopti_image_t img_hud; dimage(0, DHEIGHT - img_hud.height, &img_hud); extern font_t font_hud; dfont(&font_hud); dprint_opt(349, DHEIGHT - 5, RGB24(0x15171a), C_NONE, DTEXT_CENTER, DTEXT_TOP, "%d", g->player_data->xp_level); dprint_opt(349, DHEIGHT - 6, RGB24(0xabb1ba), C_NONE, DTEXT_CENTER, DTEXT_TOP, "%d", g->player_data->xp_level); dfont(&font_rogue); fighter_t *player_f = getcomp(g->player, fighter); /* 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 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, DHEIGHT-32, g->hud_xp_anim.frame); } else { static int const XP_FULL=22; int xp_current = g->player_data->xp_current; int xp_total = max(g->player_data->xp_to_next_level, 1); int fill_height = XP_FULL * xp_current / xp_total; anim_frame_subrender(343, DHEIGHT-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 = 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); } } } } 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); } } }