#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 "anim.h" #include "enemies.h" #include "game.h" #include "item.h" #include "render.h" #include "skills.h" #include #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(2)/16; } 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) { if(p->plane != direction) return -1; if(!p->bound_to_entity) return p->y; physical_t *bound_p = getcomp(p->bound_entity, physical); return bound_p ? bound_p->y : -1; } 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 = { 0, 0 }; fixed_t z = 0; if(pt && pt->bound_to_entity) { physical_t *bound_p = getcomp(pt->bound_entity, physical); if(bound_p) xy = (vec2) { bound_p->x, bound_p->y }; z = pt->z; } else 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 && 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, elevated_y, pt); } } free(rendering_order); } static int render_info_delay(int x, int y, level_event_t const *event) { (void)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) { anim_t const *anim = item_anim(event->item); if(anim) { anim_frame_render(x+5, y+5, anim->start[0]); return anim->start[0]->w; } else { 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; /* Determine maximum length of message that can be rendered */ int intro_w, frozen_w; char str[32]; snprintf(str, 32, "Wave %d", level_wave_count(g->level)); dsize(str, NULL, &intro_w, NULL); dsize("Frozen", NULL, &frozen_w, NULL); intro_w = max(intro_w, frozen_w); int overlay_color = -1; if(g->freeze_time > 0) { strcpy(str, "Frozen"); overlay_color = 0x5555; } else { snprintf(str, 32, "Wave %d", g->wave_number); if(g->hud_wave_number_timer) overlay_color = C_RGB(fround(g->hud_wave_number_timer * 31), 0, 0); } if(overlay_color >= 0) { dtext(x+8, y-2, overlay_color, str); dtext(x+8, y, overlay_color, str); dtext(x+7, y-1, overlay_color, str); dtext(x+9, y-1, overlay_color, str); } 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; } } static void render_panel(int x, int y, bool hflip) { extern bopti_image_t img_hud_panel; int w = 130; int sx = hflip ? 0 : img_hud_panel.width - w; int sy = 0; dsubimage(x, y, &img_hud_panel, sx, sy, w, 30, DIMAGE_NONE); y += 30; sy += 30; for(int i = 0; i < 10; i++) { dsubimage(x, y, &img_hud_panel, sx, sy, w, 10, DIMAGE_NONE); y += 10; } sy += 10; dsubimage(x, y, &img_hud_panel, sx, sy, w, 30, DIMAGE_NONE); } void render_full_panel(int x, int y, int w, int h) { extern bopti_image_t img_hud_panel; w = max(w, 100); h = max(h, 60); /* Top row */ dsubimage(x, y, &img_hud_panel, 0, 0, 50, 30, DIMAGE_NONE); for(int sx = 50; sx < w-50;) { int frame_w = min(w-50-sx, 44); dsubimage(x+sx, y, &img_hud_panel, 50, 0, frame_w, 30, DIMAGE_NONE); sx += frame_w; } dsubimage(x+w-50, y, &img_hud_panel, 94, 0, 50, 30, DIMAGE_NONE); /* Middle row */ for(int sy = 30; sy < h-30;) { int frame_h = min(h-30-sy, 10); dsubimage(x, y+sy, &img_hud_panel, 0, 30, 50, 10, DIMAGE_NONE); dsubimage(x+w-50, y+sy, &img_hud_panel, 94, 30, 50, 10, DIMAGE_NONE); sy += frame_h; } if(w > 100 && h > 60) drect(x+50, y+30, x+w-51, y+h-31, RGB24(0x202828)); /* Bottom row */ dsubimage(x, y+h-30, &img_hud_panel, 0, 40, 50, 30, DIMAGE_NONE); for(int sx = 50; sx < w-50;) { int frame_w = min(w-50-sx, 44); dsubimage(x+sx, y+h-30, &img_hud_panel, 50, 40, frame_w, 30, DIMAGE_NONE); sx += frame_w; } dsubimage(x+w-50, y+h-30, &img_hud_panel, 94, 40, 50, 30, DIMAGE_NONE); } uint32_t time_render_map = 0; uint32_t time_render_hud = 0; static void subimage_outline(int x, int y, image_t const *img, int left, int top, size_t w, size_t h, int color) { int alpha = image_alpha(img->format); for(int iy = -1; iy < (int)h+1; iy++) for(int ix = -1; ix < (int)w+1; ix++) { bool inb = ((size_t)ix < w && (size_t)iy < h); int pixel = inb ? image_get_pixel(img, left+ix, top+iy) : alpha; if(pixel != alpha) { dpixel(x+ix, y+iy, image_decode_pixel(img, pixel)); continue; } bool linked = false; for(int i = 0; i < 4; i++) { int dy = (i == 0) - (i == 1); int dx = (i == 2) - (i == 3); if((size_t)(ix+dx) < w && (size_t)(iy+dy) < h) linked |= image_get_pixel(img, left+ix+dx, top+iy+dy) != alpha; } if(linked) dpixel(x+ix, y+iy, color); } } static void anim_frame_render_outline(int x, int y, anim_frame_t const *frame, int color) { if(!frame) return; subimage_outline(x - frame->cx, y - frame->cy, frame->sheet, frame->x, frame->y, frame->w, frame->h, color); } int render_small_text(int x, int y, int color, char const *text, int size) { extern bopti_image_t img_hud_small; (void)color; if(size < 0) size = strlen(text); static char const *texts[] = { "SHIFT", "HP", "ATK", "MAG", "DEF", "PLAY", "HIGH SCORE", "OTHER" }; static int const widths[] = { 27, 12, 17, 19, 18, 22, 51, 28 }; for(size_t i = 0; i < sizeof texts / sizeof texts[0]; i++) { if(!strncmp(text, texts[i], size)) { dsubimage(x-1, y+3, &img_hud_small, 0, 8*i, widths[i], 8, DIMAGE_NONE); return widths[i] - 1; } } return 0; } static int render_arcade_char_size(int c, int font_size) { if(c < '0' || c > '9') return 0; if(font_size == 1) return 7 - (c == '1' || c == '7') + (c == '4'); if(font_size == 2) return 8 + (c == '0' || c == '4'); return 0; } int render_arcade_dsize(int value, int font_size) { char str[16]; sprintf(str, "%d", value); int pixels = 0; for(int i = 0; str[i]; i++) pixels += render_arcade_char_size(str[i], font_size); return pixels; } void render_arcade(int x, int y, int halign, int value, int color_style, int font_size) { extern bopti_image_t img_hud_arcade_font; extern bopti_image_t img_hud_arcade_font2; int w = render_arcade_dsize(value, font_size); if(halign == DTEXT_RIGHT) x -= w; else if(halign == DTEXT_CENTER) x -= (w >> 1); /* Auto style based on the number of digits */ if(color_style < 0) color_style = (value >= 10) + (value >= 30); char str[16]; sprintf(str, "%d", value); for(int i = 0; str[i]; i++) { int w = render_arcade_char_size(str[i], font_size); if(font_size == 1) dsubimage(x, y, &img_hud_arcade_font, 1 + 9 * (str[i] - '0'), 1 + 9 * color_style, w, 8, DIMAGE_NONE); if(font_size == 2) dsubimage(x, y, &img_hud_arcade_font2, 1 + 10 * (str[i] - '0'), 1 + 12 * color_style, w, 12, DIMAGE_NONE); x += w; } } static void print_stat_opt(int x, int y, int stat, int reference, char const *format, ...) { va_list args; va_start(args, format); char str[32]; vsnprintf(str, 32, format, args); va_end(args); int color = C_WHITE; if(stat < reference) color = RGB24(0xc05458); if(stat > reference) color = RGB24(0x21c24f); dtext(x, y, color, str); } static void print_stat(int x, int y, int current, int g_vis, int g_base) { dprint(x, y, C_WHITE, "%d", current); int color = C_RGB(10, 10, 10); if(g_vis < g_base) color = RGB24(0xc05458); if(g_vis > g_base) color = RGB24(0x21c24f); dprint(x+30, y, color, "+%d/Lv", g_vis); } static void dtext_multi(int x0, int y, int color, char const *str) { int x = x0; while(*str) { if(!strncmp(str, "HP", 2)) { x += render_small_text(x, y, color, str, 2) + 1; str += 2; } else if(!strncmp(str, "ATK", 3) || !strncmp(str, "MAG", 3) || !strncmp(str, "DEF", 3)) { x += render_small_text(x, y, color, str, 3) + 1; str += 3; } else if(*str == '\n') { str++; y += 13; x = x0; } else { int w; dnsize(str, 1, NULL, &w, NULL); dtext_opt(x, y, color, C_NONE, DTEXT_LEFT, DTEXT_TOP, str, 1); x += w + 1; str++; } } } static int skill_x(int i) { return 23 + 44*i + 103*(i>=3); } 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 score box */ extern bopti_image_t img_hud_top; dimage(6, HEADER_Y - 2, &img_hud_top); int score = game_compute_score(g); render_arcade(42, HEADER_Y, DTEXT_LEFT, score, 0 + (score >= 100) + (score >= 1000), 2); /* Render wave information */ render_info(90, HEADER_Y, g); /* Render current message */ if(g->message) { dtext_opt(DWIDTH - 8, HEADER_Y + 16, RGB24(0x15171a), C_NONE, DTEXT_RIGHT, DTEXT_TOP, g->message, -1); dtext_opt(DWIDTH - 8, HEADER_Y + 15, RGB24(0xabb1ba), C_NONE, DTEXT_RIGHT, DTEXT_TOP, g->message, -1); } /* 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(167, HUD_Y - 5, RGB24(0x15171a), "%d", player_data->xp_level); dprint(167, HUD_Y - 6, RGB24(0xabb1ba), "%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(158, 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(158, HUD_Y-32, g->hud_xp_anim.frame, 0, XP_FULL - fill_height, -1, fill_height); } /* Render skill icons */ static const int skill_box_size = 27; for(int i = 0; i < 5; 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 skill = player_f->skills[i+1]; int x = skill_x(i); int y = HUD_Y - 33; int bg = (cooldown_remaining != 0) ? 2 : 1; skill_render(x+2, y+2, skill, bg, C_WHITE); /* Whiten 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); } } } } /* Render backpack icon */ anim_frame_render(skill_x(5)+2, HUD_Y-33+2, g->hud_backpack_anim.frame); /* Render combo score */ extern bopti_image_t img_hud_combo; fill_height = fround(img_hud_combo.height * g->combo_health); dsubimage(224, HUD_Y - 9 - fill_height, &img_hud_combo, 0, img_hud_combo.height - fill_height, img_hud_combo.width, fill_height, DIMAGE_NONE); render_arcade(234, HUD_Y-25, DTEXT_CENTER, g->combo, -1, 1); if(g->menu_time > 0) { int x1 = cubic(-130, 0, g->menu_time, fix(1)); int x2 = DWIDTH - 130 - x1; fighter_t *player_f = getcomp(g->player, fighter); player_data_t *player_data = player_f->player; render_panel(x1, 22, false); render_panel(x2, 22, true); extern font_t font_rogue; font_t const *old_font = dfont(&font_rogue); dprint_opt(x1+61, 33, C_WHITE, C_NONE, DTEXT_CENTER, DTEXT_TOP, "Inventory"); dprint_opt(x2+72, 33, C_WHITE, C_NONE, DTEXT_CENTER, DTEXT_TOP, "Status"); dprint_opt(x2+72, 44, C_WHITE, C_NONE, DTEXT_CENTER, DTEXT_TOP, "Lv.%d (%d/%d)", player_data->xp_level, player_data->xp_current, player_data->xp_to_next_level); int selected_item = player_data->inventory[g->menu_cursor]; extern bopti_image_t img_hud_itemslots; for(int y = 0; y < 2; y++) for(int x = 0; x < 4; x++) { int sx = g->menu_cursor == 4*y + x ? 21 : 0; int x2 = x1 + 15 + 24*x; int y2 = 48 + 24*y; dsubimage(x2, y2, &img_hud_itemslots, sx, 0, 21, 21, DIMAGE_NONE); int item = player_data->inventory[4*y + x]; int slot = item_equipment_slot(item); if(item > 0) { anim_t const *anim = item_anim(item); if(anim) { if(slot >= 0 && player_data->equipment[slot] == 4*y + x) anim_frame_render_outline(x2+10, y2+10, anim->start[0], RGB24(0x979515)); else anim_frame_render(x2+10, y2+10, anim->start[0]); } } } if(selected_item >= 0) { int w, h; dsize(item_name(selected_item), NULL, &w, &h); drect(x1+5, 104, x1+w+8, 104+h+3, RGB24(0x3d5050)); dtext(x1+7, 105, C_WHITE, item_name(selected_item)); char const *desc = item_description(selected_item); if(desc) dtext_multi(x1+7, 123, C_WHITE, desc); int dx = render_small_text(x1+7, 156, -1, "SHIFT", -1); char const *use_str = "Use"; if(item_is_equip(selected_item)) { use_str = "Equip"; int slot = item_equipment_slot(selected_item); if(slot >= 0 && player_data->equipment[slot] == g->menu_cursor) use_str = "Unequip"; } dtext(x1+7+dx+4, 156, C_WHITE, use_str); } /* What the growth is with the current equips */ fighter_stats_t growth_base; growth_base = player_compute_growth(g->player, player_data->equipment); /* What the stats would be if the selected item is equipped */ fighter_stats_t growth_equip; int switched_equipment[3]; memcpy(switched_equipment, player_data->equipment, 3*sizeof(int)); int selected_slot = item_equipment_slot(selected_item); if(selected_item >= 0 && selected_slot >= 0) switched_equipment[selected_slot] = g->menu_cursor; growth_equip = player_compute_growth(g->player, switched_equipment); render_small_text(x2+14, 100, C_WHITE, "HP", -1); print_stat_opt(x2+44, 100, growth_equip.HP, growth_base.HP, "%d/%d", player_f->HP, player_f->HP_max); render_small_text(x2+14, 114, C_WHITE, "ATK", -1); print_stat(x2+44, 114, player_f->ATK, growth_equip.ATK, growth_base.ATK); render_small_text(x2+14, 128, C_WHITE, "MAG", -1); print_stat(x2+44, 128, player_f->MAG, growth_equip.MAG, growth_base.MAG); render_small_text(x2+14, 142, C_WHITE, "DEF", -1); print_stat(x2+44, 142, player_f->DEF, growth_equip.DEF, growth_base.DEF); dfont(old_font); /* What the skills would be if the selected item is equipped */ int switched_skills[6] = { -1, -1, -1, -1, -1, -1 }; player_compute_skills(g->player, switched_equipment, switched_skills); for(int i = 0; i < 5; i++) { int s1 = player_f->skills[i+1], s2 = switched_skills[i+1]; if(s1 == s2) continue; int x = skill_x(i); int y = HUD_Y - 33; skill_render(x+2, y+2, s2, 3, C_WHITE); } } /* Render final panel */ if(g->final_screen_time >= 0 && g->victory) { int PANEL_Y = cubic(-(DHEIGHT-30), 30, g->final_screen_time, fix(2.0)); render_full_panel(30, PANEL_Y, DWIDTH-30*2, DHEIGHT-30-40); dtext_opt(DWIDTH/2, PANEL_Y+12, C_WHITE, C_NONE, DTEXT_CENTER, DTEXT_TOP, "Victory!", -1); dprint(45, PANEL_Y+30, 0x5555, "Score"); dprint(300, PANEL_Y+30, 0x5555, "%d", game_compute_score(g)); dprint(45, PANEL_Y+45, C_WHITE, "Time survived"); dprint(300, PANEL_Y+45, C_WHITE, "%d", g->score.waves_survived); dprint(45, PANEL_Y+60, C_WHITE, "Kills (%d one-shot)", g->score.one_shot_kills); dprint(300, PANEL_Y+60, C_WHITE, "%d", g->score.kill_number); dprint(45, PANEL_Y+75, C_WHITE, "Combos (longest: %d)", g->score.longest_combo_chain); dprint(300, PANEL_Y+75, C_WHITE, "%d", g->score.combo_chains); dprint(45, PANEL_Y+90, C_WHITE, "Simult. kills (largest: %d)", g->score.largest_simult_kill); dprint(300, PANEL_Y+90, C_WHITE, "%d", g->score.simult_kills); dtext_opt(DWIDTH/2, PANEL_Y+120, C_WHITE, C_NONE, DTEXT_CENTER, DTEXT_TOP, "EXIT: Back to menu", -1); } if(g->final_screen_time >= 0 && !g->victory) { int PANEL_Y = cubic(-60, DHEIGHT/2-30, g->final_screen_time, fix(2.0)); render_full_panel(DWIDTH/2-90, PANEL_Y, 180, 60); dtext_opt(DWIDTH/2, PANEL_Y+15, C_RED, C_NONE, DTEXT_CENTER, DTEXT_TOP, "Defeat!", -1); dtext_opt(DWIDTH/2, PANEL_Y+30, C_WHITE, C_NONE, DTEXT_CENTER, DTEXT_TOP, "EXIT: Back to menu", -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); } } }