render: use tileset atlas for all entity and tile rendering; anims
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Idb42cff72368e26d8d44db79ba9c413a6a6a6964
This commit is contained in:
parent
2f5c959500
commit
5b640dcefd
7 changed files with 556 additions and 90 deletions
66
src/enemy.c
66
src/enemy.c
|
|
@ -5,6 +5,7 @@
|
||||||
#include "movement.h"
|
#include "movement.h"
|
||||||
#include "rng/rng.h"
|
#include "rng/rng.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
|
#include "tileset/tileset.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
// Forward declaration
|
// Forward declaration
|
||||||
|
|
@ -25,6 +26,12 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
|
||||||
if (floor >= 4)
|
if (floor >= 4)
|
||||||
max_type = 3;
|
max_type = 3;
|
||||||
|
|
||||||
|
// Get the player's starting room (first room) to exclude from enemy spawn
|
||||||
|
Room *start_room = NULL;
|
||||||
|
if (map->room_count > 0) {
|
||||||
|
start_room = &map->rooms[0];
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < num_enemies; i++) {
|
for (int i = 0; i < num_enemies; i++) {
|
||||||
// Find random floor position
|
// Find random floor position
|
||||||
int ex, ey;
|
int ex, ey;
|
||||||
|
|
@ -35,6 +42,14 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't spawn in the starting room
|
||||||
|
if (start_room != NULL) {
|
||||||
|
if (ex >= start_room->x && ex < start_room->x + start_room->w && ey >= start_room->y &&
|
||||||
|
ey < start_room->y + start_room->h) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Don't spawn on other enemies
|
// Don't spawn on other enemies
|
||||||
if (is_enemy_at(enemies, *count, ex, ey)) {
|
if (is_enemy_at(enemies, *count, ex, ey)) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -125,6 +140,27 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
|
||||||
}
|
}
|
||||||
e.cooldown = e.speed;
|
e.cooldown = e.speed;
|
||||||
|
|
||||||
|
// Initialize animation state
|
||||||
|
e.anim_state = ENEMY_ANIM_IDLE;
|
||||||
|
e.anim_frame = 0;
|
||||||
|
e.anim_timer = 0;
|
||||||
|
e.facing_right = (e.position.x < p->position.x) ? 1 : 0;
|
||||||
|
// Set sprite tile ID based on enemy type
|
||||||
|
switch (e.type) {
|
||||||
|
case ENEMY_GOBLIN:
|
||||||
|
e.sprite_tile_id = SPRITE_ENEMY_GOBLIN;
|
||||||
|
break;
|
||||||
|
case ENEMY_SKELETON:
|
||||||
|
e.sprite_tile_id = SPRITE_ENEMY_SKELETON;
|
||||||
|
break;
|
||||||
|
case ENEMY_ORC:
|
||||||
|
e.sprite_tile_id = SPRITE_ENEMY_ORC;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
e.sprite_tile_id = SPRITE_ENEMY_GOBLIN;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
enemies[i] = e;
|
enemies[i] = e;
|
||||||
(*count)++;
|
(*count)++;
|
||||||
}
|
}
|
||||||
|
|
@ -275,6 +311,9 @@ void enemy_act(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_coun
|
||||||
|
|
||||||
// Attack if adjacent to player
|
// Attack if adjacent to player
|
||||||
if (can_see && can_see_entity(map, e->position.x, e->position.y, p->position.x, p->position.y, 1)) {
|
if (can_see && can_see_entity(map, e->position.x, e->position.y, p->position.x, p->position.y, 1)) {
|
||||||
|
e->anim_state = ENEMY_ANIM_ATTACK;
|
||||||
|
e->anim_timer = 12;
|
||||||
|
e->facing_right = (e->position.x < p->position.x) ? 1 : 0;
|
||||||
combat_enemy_attack(e, p);
|
combat_enemy_attack(e, p);
|
||||||
propagate_alert(e, all_enemies, enemy_count);
|
propagate_alert(e, all_enemies, enemy_count);
|
||||||
return;
|
return;
|
||||||
|
|
@ -282,14 +321,30 @@ void enemy_act(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_coun
|
||||||
|
|
||||||
// Move toward player if visible
|
// Move toward player if visible
|
||||||
if (can_see) {
|
if (can_see) {
|
||||||
|
int old_x = e->position.x;
|
||||||
|
int old_y = e->position.y;
|
||||||
enemy_move_toward_player(e, p, map, all_enemies, enemy_count);
|
enemy_move_toward_player(e, p, map, all_enemies, enemy_count);
|
||||||
|
if (e->position.x != old_x || e->position.y != old_y) {
|
||||||
|
e->anim_state = ENEMY_ANIM_WALK;
|
||||||
|
e->anim_timer = 8;
|
||||||
|
e->facing_right = (e->position.x < p->position.x) ? 1 : 0;
|
||||||
|
}
|
||||||
propagate_alert(e, all_enemies, enemy_count);
|
propagate_alert(e, all_enemies, enemy_count);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If alert but can't see player, move toward last known position
|
// If alert but can't see player, move toward last known position
|
||||||
if (e->alert) {
|
if (e->alert) {
|
||||||
|
int old_x = e->position.x;
|
||||||
|
int old_y = e->position.y;
|
||||||
enemy_move_to_last_known(e, map, all_enemies, enemy_count);
|
enemy_move_to_last_known(e, map, all_enemies, enemy_count);
|
||||||
|
if (e->position.x != old_x || e->position.y != old_y) {
|
||||||
|
e->anim_state = ENEMY_ANIM_WALK;
|
||||||
|
e->anim_timer = 8;
|
||||||
|
if (e->position.x != old_x) {
|
||||||
|
e->facing_right = (e->position.x < old_x) ? 0 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -304,6 +359,17 @@ void enemy_update_all(Enemy enemies[], int count, Player *p, Map *map) {
|
||||||
if (!e->alive)
|
if (!e->alive)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// Update animation timer
|
||||||
|
if (e->anim_timer > 0) {
|
||||||
|
e->anim_timer--;
|
||||||
|
if (e->anim_timer <= 0) {
|
||||||
|
e->anim_state = ENEMY_ANIM_IDLE;
|
||||||
|
e->anim_frame = 0;
|
||||||
|
} else if (e->anim_state == ENEMY_ANIM_WALK) {
|
||||||
|
e->anim_frame = (e->anim_timer / 4) % 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
e->cooldown -= e->speed;
|
e->cooldown -= e->speed;
|
||||||
if (e->cooldown <= 0) {
|
if (e->cooldown <= 0) {
|
||||||
enemy_act(e, p, map, enemies, count);
|
enemy_act(e, p, map, enemies, count);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#define GAME_STATE_H
|
#define GAME_STATE_H
|
||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
#include "tileset/tileset.h"
|
||||||
#include <raylib.h>
|
#include <raylib.h>
|
||||||
|
|
||||||
// Floating damage text
|
// Floating damage text
|
||||||
|
|
@ -65,6 +66,12 @@ typedef struct {
|
||||||
int final_score;
|
int final_score;
|
||||||
// Seed for this run
|
// Seed for this run
|
||||||
unsigned int run_seed;
|
unsigned int run_seed;
|
||||||
|
// Tileset atlas for rendering
|
||||||
|
Tileset tileset;
|
||||||
|
// Slash effect timer for attack animations
|
||||||
|
int slash_timer; // frames remaining for slash effect
|
||||||
|
int slash_x, slash_y; // position of slash effect
|
||||||
|
DamageClass slash_dmg_class; // damage type for slash visual
|
||||||
} GameState;
|
} GameState;
|
||||||
|
|
||||||
#endif // GAME_STATE_H
|
#endif // GAME_STATE_H
|
||||||
|
|
|
||||||
17
src/items.c
17
src/items.c
|
|
@ -2,6 +2,7 @@
|
||||||
#include "map/map.h"
|
#include "map/map.h"
|
||||||
#include "rng/rng.h"
|
#include "rng/rng.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
|
#include "tileset/tileset.h"
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
@ -77,6 +78,22 @@ void item_spawn(Item items[], int *count, Map *map, int floor) {
|
||||||
item.power = 1 + rng_int(0, floor / 2);
|
item.power = 1 + rng_int(0, floor / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set sprite tile ID based on item type
|
||||||
|
switch (item.type) {
|
||||||
|
case ITEM_POTION:
|
||||||
|
item.sprite_tile_id = SPRITE_ITEM_POTION;
|
||||||
|
break;
|
||||||
|
case ITEM_WEAPON:
|
||||||
|
item.sprite_tile_id = SPRITE_ITEM_WEAPON;
|
||||||
|
break;
|
||||||
|
case ITEM_ARMOR:
|
||||||
|
item.sprite_tile_id = SPRITE_ITEM_ARMOR;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
item.sprite_tile_id = SPRITE_ITEM_POTION;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
items[*count] = item;
|
items[*count] = item;
|
||||||
(*count)++;
|
(*count)++;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
107
src/main.c
107
src/main.c
|
|
@ -4,11 +4,14 @@
|
||||||
#include "enemy.h"
|
#include "enemy.h"
|
||||||
#include "items.h"
|
#include "items.h"
|
||||||
#include "map/map.h"
|
#include "map/map.h"
|
||||||
|
#include "map/utils.h"
|
||||||
#include "movement.h"
|
#include "movement.h"
|
||||||
#include "player.h"
|
#include "player.h"
|
||||||
#include "render.h"
|
#include "render.h"
|
||||||
#include "rng/rng.h"
|
#include "rng/rng.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
|
#include "tileset/tileset.h"
|
||||||
|
#include "tileset/tileset_paint.h"
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
@ -182,19 +185,18 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
|
||||||
if (gs->game_over)
|
if (gs->game_over)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Check if stepped on stairs
|
|
||||||
if (gs->map.tiles[gs->player.position.y][gs->player.position.x] == TILE_STAIRS) {
|
|
||||||
gs->awaiting_descend = 1;
|
|
||||||
gs->last_message = "Descend to next floor? (Y/N)";
|
|
||||||
gs->message_timer = 120;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// combat feedback - player attacked an enemy this turn
|
// combat feedback - player attacked an enemy this turn
|
||||||
if (attacked_enemy != NULL) {
|
if (attacked_enemy != NULL) {
|
||||||
int ex = attacked_enemy->position.x * TILE_SIZE + 8;
|
int ex = attacked_enemy->position.x * TILE_SIZE + 8;
|
||||||
int ey = attacked_enemy->position.y * TILE_SIZE;
|
int ey = attacked_enemy->position.y * TILE_SIZE;
|
||||||
|
|
||||||
|
// Trigger slash effect
|
||||||
|
gs->slash_timer = 8;
|
||||||
|
gs->slash_x = attacked_enemy->position.x;
|
||||||
|
gs->slash_y = attacked_enemy->position.y;
|
||||||
|
// Use player's equipped weapon damage class, or default to slash
|
||||||
|
gs->slash_dmg_class = gs->player.has_weapon ? gs->player.equipped_weapon.dmg_class : DMG_SLASH;
|
||||||
|
|
||||||
if (combat_was_dodged()) {
|
if (combat_was_dodged()) {
|
||||||
spawn_floating_label(gs, ex, ey, LABEL_DODGE, EFFECT_NONE);
|
spawn_floating_label(gs, ex, ey, LABEL_DODGE, EFFECT_NONE);
|
||||||
audio_play_dodge(gs);
|
audio_play_dodge(gs);
|
||||||
|
|
@ -237,6 +239,7 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
|
||||||
gs->screen_shake = SHAKE_PLAYER_DAMAGE_DURATION;
|
gs->screen_shake = SHAKE_PLAYER_DAMAGE_DURATION;
|
||||||
gs->damage_taken += combat_get_last_damage();
|
gs->damage_taken += combat_get_last_damage();
|
||||||
gs->times_hit++;
|
gs->times_hit++;
|
||||||
|
gs->player.flash_timer = 4; // Trigger damage flash
|
||||||
spawn_floating_text(gs, gs->player.position.x * TILE_SIZE + 8, gs->player.position.y * TILE_SIZE,
|
spawn_floating_text(gs, gs->player.position.x * TILE_SIZE + 8, gs->player.position.y * TILE_SIZE,
|
||||||
combat_get_last_damage(), combat_was_critical());
|
combat_get_last_damage(), combat_was_critical());
|
||||||
}
|
}
|
||||||
|
|
@ -247,6 +250,13 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
|
||||||
|
|
||||||
if (gs->player.hp <= 0)
|
if (gs->player.hp <= 0)
|
||||||
gs->game_over = 1;
|
gs->game_over = 1;
|
||||||
|
|
||||||
|
// Check if stepped on stairs AFTER enemy turns
|
||||||
|
if (gs->map.tiles[gs->player.position.y][gs->player.position.x] == TILE_STAIRS) {
|
||||||
|
gs->awaiting_descend = 1;
|
||||||
|
gs->last_message = "Descend to next floor? (Y/N)";
|
||||||
|
gs->message_timer = 120;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If player is stunned, wait for any key then consume the turn
|
// If player is stunned, wait for any key then consume the turn
|
||||||
|
|
@ -457,11 +467,27 @@ static int handle_movement_input(GameState *gs) {
|
||||||
try_move_entity(&gs->player.position, direction, &gs->map, &gs->player, gs->enemies, gs->enemy_count, true);
|
try_move_entity(&gs->player.position, direction, &gs->map, &gs->player, gs->enemies, gs->enemy_count, true);
|
||||||
if (result == MOVE_RESULT_MOVED) {
|
if (result == MOVE_RESULT_MOVED) {
|
||||||
player_on_move(&gs->player);
|
player_on_move(&gs->player);
|
||||||
|
// Set walk animation
|
||||||
|
gs->player.anim_state = PLAYER_ANIM_WALK;
|
||||||
|
gs->player.anim_frame = 0;
|
||||||
|
gs->player.anim_timer = 8; // frames to show each walk frame
|
||||||
|
// Update facing direction
|
||||||
|
if (direction.x != 0)
|
||||||
|
gs->player.facing_right = (direction.x > 0);
|
||||||
action = 1;
|
action = 1;
|
||||||
} else if (result == MOVE_RESULT_BLOCKED_ENEMY) {
|
} else if (result == MOVE_RESULT_BLOCKED_ENEMY) {
|
||||||
target = player_find_enemy_at(gs->enemies, gs->enemy_count, new_x, new_y);
|
target = player_find_enemy_at(gs->enemies, gs->enemy_count, new_x, new_y);
|
||||||
if (target != NULL) {
|
if (target != NULL) {
|
||||||
player_attack(&gs->player, target);
|
player_attack(&gs->player, target);
|
||||||
|
// Set attack animation
|
||||||
|
gs->player.anim_state = PLAYER_ANIM_ATTACK;
|
||||||
|
gs->player.anim_frame = 0;
|
||||||
|
gs->player.anim_timer = 12; // frames to show attack
|
||||||
|
// Face the enemy
|
||||||
|
if (target->position.x > gs->player.position.x)
|
||||||
|
gs->player.facing_right = 1;
|
||||||
|
else if (target->position.x < gs->player.position.x)
|
||||||
|
gs->player.facing_right = 0;
|
||||||
action = 1;
|
action = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -526,13 +552,35 @@ static void game_loop(unsigned int run_seed, FontManager *fm) {
|
||||||
load_audio_assets(&gs);
|
load_audio_assets(&gs);
|
||||||
// font
|
// font
|
||||||
init_fonts(fm);
|
init_fonts(fm);
|
||||||
|
// Initialize tileset atlas
|
||||||
|
if (!tileset_init(&gs.tileset, TILE_SIZE, TILE_SIZE)) {
|
||||||
|
fprintf(stderr, "Failed to initialize tileset\n");
|
||||||
|
destroy_fonts(fm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!tileset_paint_all(&gs.tileset)) {
|
||||||
|
fprintf(stderr, "Failed to paint tiles\n");
|
||||||
|
tileset_destroy(&gs.tileset);
|
||||||
|
destroy_fonts(fm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!tileset_finalize(&gs.tileset)) {
|
||||||
|
fprintf(stderr, "Failed to finalize tileset\n");
|
||||||
|
tileset_destroy(&gs.tileset);
|
||||||
|
destroy_fonts(fm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize first floor
|
// Initialize first floor
|
||||||
init_floor(&gs, 1);
|
init_floor(&gs, 1);
|
||||||
|
|
||||||
// Disable esc to exit
|
// Disable esc to exit
|
||||||
SetExitKey(0);
|
SetExitKey(0);
|
||||||
|
|
||||||
|
int frame_counter = 0;
|
||||||
while (!WindowShouldClose()) {
|
while (!WindowShouldClose()) {
|
||||||
|
frame_counter++;
|
||||||
|
|
||||||
// Handle input
|
// Handle input
|
||||||
if (!gs.game_over) {
|
if (!gs.game_over) {
|
||||||
// Tick status effects at the start of each frame where input is checked
|
// Tick status effects at the start of each frame where input is checked
|
||||||
|
|
@ -552,6 +600,12 @@ static void game_loop(unsigned int run_seed, FontManager *fm) {
|
||||||
gs.game_won = 0;
|
gs.game_won = 0;
|
||||||
load_audio_assets(&gs);
|
load_audio_assets(&gs);
|
||||||
init_fonts(fm);
|
init_fonts(fm);
|
||||||
|
// Re-initialize tileset for new run
|
||||||
|
if (!tileset_init(&gs.tileset, TILE_SIZE, TILE_SIZE) || !tileset_paint_all(&gs.tileset) ||
|
||||||
|
!tileset_finalize(&gs.tileset)) {
|
||||||
|
fprintf(stderr, "Failed to re-initialize tileset\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
init_floor(&gs, 1);
|
init_floor(&gs, 1);
|
||||||
// Update window title with new seed
|
// Update window title with new seed
|
||||||
char title[128];
|
char title[128];
|
||||||
|
|
@ -567,6 +621,27 @@ static void game_loop(unsigned int run_seed, FontManager *fm) {
|
||||||
// Update effects
|
// Update effects
|
||||||
update_effects(&gs);
|
update_effects(&gs);
|
||||||
|
|
||||||
|
// Update slash effect timer
|
||||||
|
if (gs.slash_timer > 0)
|
||||||
|
gs.slash_timer--;
|
||||||
|
|
||||||
|
// Update player animation
|
||||||
|
if (gs.player.anim_timer > 0) {
|
||||||
|
gs.player.anim_timer--;
|
||||||
|
if (gs.player.anim_timer <= 0) {
|
||||||
|
// Animation finished, return to idle
|
||||||
|
gs.player.anim_state = PLAYER_ANIM_IDLE;
|
||||||
|
gs.player.anim_frame = 0;
|
||||||
|
} else if (gs.player.anim_state == PLAYER_ANIM_WALK) {
|
||||||
|
// Toggle walk frame every 4 frames
|
||||||
|
gs.player.anim_frame = (gs.player.anim_timer / 4) % 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update player damage flash
|
||||||
|
if (gs.player.flash_timer > 0)
|
||||||
|
gs.player.flash_timer--;
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
BeginDrawing();
|
BeginDrawing();
|
||||||
ClearBackground(BLACK);
|
ClearBackground(BLACK);
|
||||||
|
|
@ -576,15 +651,19 @@ static void game_loop(unsigned int run_seed, FontManager *fm) {
|
||||||
cam.zoom = 1.0f;
|
cam.zoom = 1.0f;
|
||||||
cam.offset = (Vector2){(float)gs.shake_x, (float)gs.shake_y};
|
cam.offset = (Vector2){(float)gs.shake_x, (float)gs.shake_y};
|
||||||
BeginMode2D(cam);
|
BeginMode2D(cam);
|
||||||
render_map(&gs.map);
|
render_map(&gs.map, &gs.tileset);
|
||||||
render_items(gs.items, gs.item_count, gs.map.visible);
|
render_items(gs.items, gs.item_count, gs.map.visible, &gs.tileset);
|
||||||
render_enemies(gs.enemies, gs.enemy_count, gs.map.visible);
|
render_enemies(gs.enemies, gs.enemy_count, gs.map.visible, &gs.tileset, frame_counter);
|
||||||
render_player(&gs.player);
|
render_player(&gs.player, &gs.tileset, frame_counter);
|
||||||
|
// Draw slash effect on top of entities
|
||||||
|
if (gs.slash_timer > 0) {
|
||||||
|
render_slash_effect(gs.slash_x, gs.slash_y, gs.slash_dmg_class, gs.slash_timer);
|
||||||
|
}
|
||||||
EndMode2D();
|
EndMode2D();
|
||||||
|
|
||||||
// Floating texts follow world shake
|
// Floating texts follow world shake
|
||||||
render_floating_texts(gs.floating_texts, gs.floating_count, gs.shake_x, gs.shake_y);
|
render_floating_texts(gs.floating_texts, gs.floating_count, gs.shake_x, gs.shake_y, fm);
|
||||||
render_ui(&gs.player, fm);
|
render_ui(&gs.player, &gs.tileset, fm);
|
||||||
|
|
||||||
// Draw action log
|
// Draw action log
|
||||||
render_action_log(gs.action_log, gs.log_count, gs.log_head, fm);
|
render_action_log(gs.action_log, gs.log_count, gs.log_head, fm);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "items.h"
|
#include "items.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
|
#include "tileset/tileset.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
void player_init(Player *p, int x, int y) {
|
void player_init(Player *p, int x, int y) {
|
||||||
|
|
@ -28,6 +29,13 @@ void player_init(Player *p, int x, int y) {
|
||||||
p->effect_count = 0;
|
p->effect_count = 0;
|
||||||
memset(p->effects, 0, sizeof(p->effects));
|
memset(p->effects, 0, sizeof(p->effects));
|
||||||
|
|
||||||
|
// Initialize animation state
|
||||||
|
p->anim_state = PLAYER_ANIM_IDLE;
|
||||||
|
p->anim_frame = 0;
|
||||||
|
p->anim_timer = 0;
|
||||||
|
p->facing_right = 1;
|
||||||
|
p->sprite_tile_id = SPRITE_PLAYER;
|
||||||
|
|
||||||
// Initialize inventory to empty
|
// Initialize inventory to empty
|
||||||
for (int i = 0; i < MAX_INVENTORY; i++) {
|
for (int i = 0; i < MAX_INVENTORY; i++) {
|
||||||
p->inventory[i].picked_up = 1; // mark as invalid
|
p->inventory[i].picked_up = 1; // mark as invalid
|
||||||
|
|
|
||||||
412
src/render.c
412
src/render.c
|
|
@ -1,6 +1,8 @@
|
||||||
#include "render.h"
|
#include "render.h"
|
||||||
#include "items.h"
|
#include "items.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
|
#include "map/utils.h"
|
||||||
|
#include <math.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
@ -63,78 +65,292 @@ static void draw_text_body(Font f, const char *text, float x, float y, int size,
|
||||||
DrawTextEx(f, text, (Vector2){x, y}, (float)size, spacing, c);
|
DrawTextEx(f, text, (Vector2){x, y}, (float)size, spacing, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
void render_map(const Map *map) {
|
void render_map(const Map *map, const Tileset *tileset) {
|
||||||
for (int y = 0; y < MAP_HEIGHT; y++) {
|
for (int y = 0; y < MAP_HEIGHT; y++) {
|
||||||
for (int x = 0; x < MAP_WIDTH; x++) {
|
for (int x = 0; x < MAP_WIDTH; x++) {
|
||||||
Rectangle rect = {(float)(x * TILE_SIZE), (float)(y * TILE_SIZE), (float)TILE_SIZE, (float)TILE_SIZE};
|
Rectangle dst = {(float)(x * TILE_SIZE), (float)(y * TILE_SIZE), (float)TILE_SIZE, (float)TILE_SIZE};
|
||||||
int visible = map->visible[y][x];
|
int visible = map->visible[y][x];
|
||||||
int remembered = map->remembered[y][x];
|
int remembered = map->remembered[y][x];
|
||||||
|
|
||||||
if (!visible && !remembered) {
|
if (!visible && !remembered) {
|
||||||
DrawRectangleRec(rect, (Color){5, 5, 10, 255});
|
DrawRectangleRec(dst, (Color){5, 5, 10, 255});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int tile_id = -1;
|
||||||
|
switch (map->tiles[y][x]) {
|
||||||
|
case TILE_WALL:
|
||||||
|
tile_id = TILE_WALL_0 + ((x * 7 + y * 13) % 2);
|
||||||
|
break;
|
||||||
|
case TILE_FLOOR:
|
||||||
|
tile_id = TILE_FLOOR_0 + ((x * 7 + y * 13) % 4);
|
||||||
|
break;
|
||||||
|
case TILE_STAIRS:
|
||||||
|
tile_id = TILE_STAIRS_SPRITE;
|
||||||
|
break;
|
||||||
|
case TILE_DOOR_CLOSED:
|
||||||
|
tile_id = TILE_DOOR_CLOSED_SPRITE;
|
||||||
|
break;
|
||||||
|
case TILE_DOOR_OPEN:
|
||||||
|
tile_id = TILE_DOOR_OPEN_SPRITE;
|
||||||
|
break;
|
||||||
|
case TILE_DOOR_RUINED:
|
||||||
|
tile_id = TILE_DOOR_OPEN_SPRITE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tile_id >= 0 && tileset != NULL && tileset->finalized) {
|
||||||
|
Rectangle src = tileset_get_region(tileset, tile_id);
|
||||||
|
if (src.width > 0) {
|
||||||
|
Color tint = WHITE;
|
||||||
|
if (!visible) {
|
||||||
|
// Dim remembered tiles
|
||||||
|
tint = (Color){128, 128, 128, 255};
|
||||||
|
}
|
||||||
|
DrawTexturePro(tileset->atlas, src, dst, (Vector2){0, 0}, 0.0f, tint);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to solid colors if tileset not available
|
||||||
Color wall_color = visible ? DARKGRAY : (Color){25, 25, 30, 255};
|
Color wall_color = visible ? DARKGRAY : (Color){25, 25, 30, 255};
|
||||||
Color floor_color = visible ? BLACK : (Color){15, 15, 20, 255};
|
Color floor_color = visible ? BLACK : (Color){15, 15, 20, 255};
|
||||||
Color stairs_color = visible ? (Color){100, 100, 100, 255} : (Color){40, 40, 45, 255};
|
Color stairs_color = visible ? (Color){180, 160, 100, 255} : (Color){60, 55, 50, 255};
|
||||||
|
Color door_color = visible ? (Color){139, 119, 89, 255} : (Color){60, 55, 50, 255};
|
||||||
|
|
||||||
switch (map->tiles[y][x]) {
|
switch (map->tiles[y][x]) {
|
||||||
case TILE_WALL:
|
case TILE_WALL:
|
||||||
DrawRectangleRec(rect, wall_color);
|
DrawRectangleRec(dst, wall_color);
|
||||||
break;
|
break;
|
||||||
case TILE_FLOOR:
|
case TILE_FLOOR:
|
||||||
DrawRectangleRec(rect, floor_color);
|
DrawRectangleRec(dst, floor_color);
|
||||||
|
// Torch flicker: warm tint on floor tiles adjacent to stairs
|
||||||
|
{
|
||||||
|
int is_adjacent_to_stairs = 0;
|
||||||
|
for (int dy = -1; dy <= 1 && !is_adjacent_to_stairs; dy++) {
|
||||||
|
for (int dx = -1; dx <= 1 && !is_adjacent_to_stairs; dx++) {
|
||||||
|
int nx = x + dx;
|
||||||
|
int ny = y + dy;
|
||||||
|
if (in_bounds(nx, ny, MAP_WIDTH, MAP_HEIGHT) && map->tiles[ny][nx] == TILE_STAIRS) {
|
||||||
|
is_adjacent_to_stairs = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (is_adjacent_to_stairs && visible) {
|
||||||
|
int flicker = (int)(sinf(GetTime() * 5.0f) * 15.0f);
|
||||||
|
DrawRectangleRec(dst, (Color){40 + flicker, 25, 10, 60});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Grid lines
|
||||||
|
if (DRAW_GRID_LINES && visible) {
|
||||||
|
DrawRectangleLines((int)dst.x, (int)dst.y, (int)dst.width, (int)dst.height, (Color){20, 20, 20, 80});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case TILE_STAIRS:
|
case TILE_STAIRS:
|
||||||
DrawRectangleRec(rect, stairs_color);
|
DrawRectangleRec(dst, stairs_color);
|
||||||
if (visible)
|
// Make stairs very visible with bright symbol and bounce
|
||||||
DrawText(">", x * TILE_SIZE + 4, y * TILE_SIZE + 2, NORM_FONT, WHITE);
|
{
|
||||||
else
|
int bounce = (int)(sinf(GetTime() * 3.0f) * 1.5f);
|
||||||
DrawText(">", x * TILE_SIZE + 4, y * TILE_SIZE + 2, NORM_FONT, (Color){60, 60, 65, 255});
|
if (visible)
|
||||||
|
DrawText(">", x * TILE_SIZE + 4, y * TILE_SIZE + 1 + bounce, NORM_FONT + 2, (Color){255, 255, 200, 255});
|
||||||
|
else
|
||||||
|
DrawText(">", x * TILE_SIZE + 4, y * TILE_SIZE + 1 + bounce, NORM_FONT + 2, (Color){100, 90, 70, 255});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TILE_DOOR_CLOSED:
|
||||||
|
DrawRectangleRec(dst, door_color);
|
||||||
|
if (visible) {
|
||||||
|
DrawRectangle(x * TILE_SIZE + 2, y * TILE_SIZE + 2, TILE_SIZE - 4, TILE_SIZE - 4, (Color){100, 80, 60, 255});
|
||||||
|
DrawRectangleLines(x * TILE_SIZE + 2, y * TILE_SIZE + 2, TILE_SIZE - 4, TILE_SIZE - 4,
|
||||||
|
(Color){60, 50, 40, 255});
|
||||||
|
DrawText("+", x * TILE_SIZE + 5, y * TILE_SIZE + 1, NORM_FONT, WHITE);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TILE_DOOR_OPEN:
|
||||||
|
DrawRectangleRec(dst, floor_color);
|
||||||
|
if (visible) {
|
||||||
|
DrawRectangle(x * TILE_SIZE + 2, y * TILE_SIZE + 2, TILE_SIZE - 4, TILE_SIZE - 4, (Color){80, 70, 50, 180});
|
||||||
|
DrawRectangleLines(x * TILE_SIZE + 2, y * TILE_SIZE + 2, TILE_SIZE - 4, TILE_SIZE - 4,
|
||||||
|
(Color){60, 50, 40, 200});
|
||||||
|
DrawText("'", x * TILE_SIZE + 6, y * TILE_SIZE + 2, NORM_FONT, (Color){150, 140, 120, 255});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TILE_DOOR_RUINED:
|
||||||
|
DrawRectangleRec(dst, (Color){60, 45, 30, 255});
|
||||||
|
if (visible) {
|
||||||
|
DrawRectangle(x * TILE_SIZE + 1, y * TILE_SIZE + 1, TILE_SIZE - 2, TILE_SIZE - 2, (Color){80, 60, 40, 200});
|
||||||
|
DrawLine(x * TILE_SIZE + 2, y * TILE_SIZE + 2, x * TILE_SIZE + TILE_SIZE - 2, y * TILE_SIZE + TILE_SIZE - 2,
|
||||||
|
(Color){120, 90, 60, 255});
|
||||||
|
DrawLine(x * TILE_SIZE + TILE_SIZE - 2, y * TILE_SIZE + 2, x * TILE_SIZE + 2, y * TILE_SIZE + TILE_SIZE - 2,
|
||||||
|
(Color){120, 90, 60, 255});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void render_player(const Player *p) {
|
void render_player(const Player *p, const Tileset *tileset, int frame_counter) {
|
||||||
Rectangle rect = {(float)(p->position.x * TILE_SIZE), (float)(p->position.y * TILE_SIZE), (float)TILE_SIZE,
|
Rectangle dst = {(float)(p->position.x * TILE_SIZE), (float)(p->position.y * TILE_SIZE), (float)TILE_SIZE,
|
||||||
(float)TILE_SIZE};
|
(float)TILE_SIZE};
|
||||||
DrawRectangleRec(rect, BLUE);
|
|
||||||
|
if (tileset != NULL && tileset->finalized) {
|
||||||
|
int tile_id = p->sprite_tile_id;
|
||||||
|
switch (p->anim_state) {
|
||||||
|
case PLAYER_ANIM_WALK:
|
||||||
|
tile_id = (p->anim_frame == 0) ? SPRITE_PLAYER_WALK_0 : SPRITE_PLAYER_WALK_1;
|
||||||
|
break;
|
||||||
|
case PLAYER_ANIM_ATTACK:
|
||||||
|
tile_id = SPRITE_PLAYER_ATTACK;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Idle breathing: subtle bob every 60 frames
|
||||||
|
if ((frame_counter / 30) % 2 == 0) {
|
||||||
|
dst.y -= 1;
|
||||||
|
}
|
||||||
|
tile_id = p->sprite_tile_id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle src = tileset_get_region(tileset, tile_id);
|
||||||
|
if (src.width > 0) {
|
||||||
|
// Flip horizontally if facing left
|
||||||
|
if (!p->facing_right) {
|
||||||
|
src.width = -src.width;
|
||||||
|
}
|
||||||
|
DrawTexturePro(tileset->atlas, src, dst, (Vector2){0, 0}, 0.0f, WHITE);
|
||||||
|
|
||||||
|
// Draw status effect overlays
|
||||||
|
for (int e = 0; e < p->effect_count && e < MAX_EFFECTS; e++) {
|
||||||
|
if (p->effects[e].duration > 0) {
|
||||||
|
int effect_tile = -1;
|
||||||
|
switch (p->effects[e].type) {
|
||||||
|
case EFFECT_BURN:
|
||||||
|
effect_tile = SPRITE_EFFECT_BURN;
|
||||||
|
break;
|
||||||
|
case EFFECT_POISON:
|
||||||
|
effect_tile = SPRITE_EFFECT_POISON;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (effect_tile >= 0) {
|
||||||
|
Rectangle eff_src = tileset_get_region(tileset, effect_tile);
|
||||||
|
if (eff_src.width > 0) {
|
||||||
|
Rectangle eff_dst = {dst.x + 8, dst.y + 8, 8, 8};
|
||||||
|
DrawTexturePro(tileset->atlas, eff_src, eff_dst, (Vector2){0, 0}, 0.0f, (Color){255, 255, 255, 180});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Damage flash overlay
|
||||||
|
if (p->flash_timer > 0) {
|
||||||
|
DrawRectangleRec(dst, (Color){255, 0, 0, 128});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to solid color
|
||||||
|
DrawRectangleRec(dst, BLUE);
|
||||||
|
if (p->flash_timer > 0) {
|
||||||
|
DrawRectangleRec(dst, (Color){255, 0, 0, 128});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void render_enemies(const Enemy *enemies, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH]) {
|
void render_enemies(const Enemy *enemies, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH],
|
||||||
|
const Tileset *tileset, int frame_counter) {
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
if (!enemies[i].alive)
|
if (!enemies[i].alive)
|
||||||
continue;
|
continue;
|
||||||
if (!visible[enemies[i].position.y][enemies[i].position.x])
|
if (!visible[enemies[i].position.y][enemies[i].position.x])
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
Rectangle rect = {(float)(enemies[i].position.x * TILE_SIZE), (float)(enemies[i].position.y * TILE_SIZE),
|
Rectangle dst = {(float)(enemies[i].position.x * TILE_SIZE), (float)(enemies[i].position.y * TILE_SIZE),
|
||||||
(float)TILE_SIZE, (float)TILE_SIZE};
|
(float)TILE_SIZE, (float)TILE_SIZE};
|
||||||
|
|
||||||
// Different colors based on enemy type
|
// Select animation frame based on sprite_tile_id base
|
||||||
Color enemy_color;
|
int base_tile = enemies[i].sprite_tile_id;
|
||||||
switch (enemies[i].type) {
|
int tile_id;
|
||||||
case ENEMY_GOBLIN:
|
if (enemies[i].anim_state == ENEMY_ANIM_WALK) {
|
||||||
enemy_color = COLOR_ENEMY_GOBLIN; // dark red
|
tile_id = (enemies[i].anim_frame == 0) ? base_tile + 1 : base_tile + 2;
|
||||||
break;
|
} else if (enemies[i].anim_state == ENEMY_ANIM_ATTACK) {
|
||||||
case ENEMY_SKELETON:
|
tile_id = base_tile + 3;
|
||||||
enemy_color = COLOR_ENEMY_SKELETON; // light gray
|
} else if (enemies[i].anim_state == ENEMY_ANIM_IDLE) {
|
||||||
break;
|
// Idle breathing: subtle bob every 60 frames
|
||||||
case ENEMY_ORC:
|
if ((frame_counter / 30) % 2 == 0) {
|
||||||
enemy_color = COLOR_ENEMY_ORC; // dark green
|
dst.y -= 1;
|
||||||
break;
|
}
|
||||||
default:
|
tile_id = base_tile;
|
||||||
enemy_color = RED;
|
} else {
|
||||||
break;
|
tile_id = base_tile;
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawRectangleRec(rect, enemy_color);
|
if (tile_id >= 0 && tileset != NULL && tileset->finalized) {
|
||||||
|
Rectangle src = tileset_get_region(tileset, tile_id);
|
||||||
|
if (src.width > 0) {
|
||||||
|
// Flip horizontally if facing left
|
||||||
|
if (!enemies[i].facing_right) {
|
||||||
|
src.width = -src.width;
|
||||||
|
}
|
||||||
|
DrawTexturePro(tileset->atlas, src, dst, (Vector2){0, 0}, 0.0f, WHITE);
|
||||||
|
|
||||||
|
// Draw status effect overlays
|
||||||
|
for (int e = 0; e < enemies[i].effect_count && e < MAX_EFFECTS; e++) {
|
||||||
|
if (enemies[i].effects[e].duration > 0) {
|
||||||
|
int effect_tile = -1;
|
||||||
|
switch (enemies[i].effects[e].type) {
|
||||||
|
case EFFECT_BURN:
|
||||||
|
effect_tile = SPRITE_EFFECT_BURN;
|
||||||
|
break;
|
||||||
|
case EFFECT_POISON:
|
||||||
|
effect_tile = SPRITE_EFFECT_POISON;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (effect_tile >= 0) {
|
||||||
|
Rectangle eff_src = tileset_get_region(tileset, effect_tile);
|
||||||
|
if (eff_src.width > 0) {
|
||||||
|
Rectangle eff_dst = {dst.x + 8, dst.y + 8, 8, 8};
|
||||||
|
DrawTexturePro(tileset->atlas, eff_src, eff_dst, (Vector2){0, 0}, 0.0f, (Color){255, 255, 255, 180});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enemy alert overlay (yellow tint when alert)
|
||||||
|
if (enemies[i].alert) {
|
||||||
|
DrawRectangleRec(dst, (Color){255, 255, 0, 30});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to solid colors
|
||||||
|
Color enemy_color;
|
||||||
|
switch (enemies[i].type) {
|
||||||
|
case ENEMY_GOBLIN:
|
||||||
|
enemy_color = COLOR_ENEMY_GOBLIN;
|
||||||
|
break;
|
||||||
|
case ENEMY_SKELETON:
|
||||||
|
enemy_color = COLOR_ENEMY_SKELETON;
|
||||||
|
break;
|
||||||
|
case ENEMY_ORC:
|
||||||
|
enemy_color = COLOR_ENEMY_ORC;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
enemy_color = RED;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
DrawRectangleRec(dst, enemy_color);
|
||||||
|
if (enemies[i].alert) {
|
||||||
|
DrawRectangleRec(dst, (Color){255, 255, 0, 30});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw hp bar above enemy, color-coded by health remaining
|
// Draw hp bar above enemy, color-coded by health remaining
|
||||||
int hp_pixels = (enemies[i].hp * TILE_SIZE) / enemies[i].max_hp;
|
int hp_pixels = (enemies[i].max_hp > 0) ? (enemies[i].hp * TILE_SIZE) / enemies[i].max_hp : 0;
|
||||||
if (hp_pixels > 0) {
|
if (hp_pixels > 0) {
|
||||||
float hp_ratio = (float)enemies[i].hp / (float)enemies[i].max_hp;
|
float hp_ratio = (float)enemies[i].hp / (float)enemies[i].max_hp;
|
||||||
Color bar_color;
|
Color bar_color;
|
||||||
|
|
@ -151,38 +367,47 @@ void render_enemies(const Enemy *enemies, int count, const unsigned char visible
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void render_items(const Item *items, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH]) {
|
void render_items(const Item *items, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH],
|
||||||
|
const Tileset *tileset) {
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
if (items[i].picked_up)
|
if (items[i].picked_up)
|
||||||
continue;
|
continue;
|
||||||
if (!visible[items[i].y][items[i].x])
|
if (!visible[items[i].y][items[i].x])
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
Rectangle rect = {(float)(items[i].x * TILE_SIZE), (float)(items[i].y * TILE_SIZE), (float)TILE_SIZE,
|
Rectangle dst = {(float)(items[i].x * TILE_SIZE), (float)(items[i].y * TILE_SIZE), (float)TILE_SIZE,
|
||||||
(float)TILE_SIZE};
|
(float)TILE_SIZE};
|
||||||
|
|
||||||
// Different colors based on item type
|
int tile_id = items[i].sprite_tile_id;
|
||||||
Color item_color;
|
|
||||||
switch (items[i].type) {
|
if (tile_id >= 0 && tileset != NULL && tileset->finalized) {
|
||||||
case ITEM_POTION:
|
Rectangle src = tileset_get_region(tileset, tile_id);
|
||||||
item_color = COLOR_ITEM_POTION; // red/pink
|
if (src.width > 0) {
|
||||||
break;
|
DrawTexturePro(tileset->atlas, src, dst, (Vector2){0, 0}, 0.0f, WHITE);
|
||||||
case ITEM_WEAPON:
|
}
|
||||||
item_color = COLOR_ITEM_WEAPON; // yellow
|
} else {
|
||||||
break;
|
// Fallback to solid colors
|
||||||
case ITEM_ARMOR:
|
Color item_color;
|
||||||
item_color = COLOR_ITEM_ARMOR; // blue
|
switch (items[i].type) {
|
||||||
break;
|
case ITEM_POTION:
|
||||||
default:
|
item_color = COLOR_ITEM_POTION;
|
||||||
item_color = GREEN;
|
break;
|
||||||
break;
|
case ITEM_WEAPON:
|
||||||
|
item_color = COLOR_ITEM_WEAPON;
|
||||||
|
break;
|
||||||
|
case ITEM_ARMOR:
|
||||||
|
item_color = COLOR_ITEM_ARMOR;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
item_color = GREEN;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
DrawRectangleRec(dst, item_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawRectangleRec(rect, item_color);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void render_ui(const Player *p, const FontManager *fm) {
|
void render_ui(const Player *p, const Tileset *tileset, const FontManager *fm) {
|
||||||
// HUD Panel
|
// HUD Panel
|
||||||
const int hud_y = MAP_HEIGHT * TILE_SIZE;
|
const int hud_y = MAP_HEIGHT * TILE_SIZE;
|
||||||
const int hud_height = 60;
|
const int hud_height = 60;
|
||||||
|
|
@ -213,10 +438,18 @@ void render_ui(const Player *p, const FontManager *fm) {
|
||||||
int portrait_y = hud_y + 8;
|
int portrait_y = hud_y + 8;
|
||||||
int portrait_size = 44;
|
int portrait_size = 44;
|
||||||
|
|
||||||
// FIXME: for now this is just a blue square indicating the player. Once we
|
// Draw player sprite in portrait
|
||||||
// model the player, add classes, sprites, etc. this will need to be revisited.
|
if (tileset != NULL && tileset->finalized) {
|
||||||
DrawRectangle(portrait_x, portrait_y, portrait_size, portrait_size, (Color){30, 30, 45, 255});
|
Rectangle src = tileset_get_region(tileset, SPRITE_PLAYER);
|
||||||
DrawRectangle(portrait_x + 2, portrait_y + 2, portrait_size - 4, portrait_size - 4, BLUE);
|
if (src.width > 0) {
|
||||||
|
Rectangle dst = {(float)portrait_x, (float)portrait_y, (float)portrait_size, (float)portrait_size};
|
||||||
|
DrawTexturePro(tileset->atlas, src, dst, (Vector2){0, 0}, 0.0f, WHITE);
|
||||||
|
} else {
|
||||||
|
DrawRectangle(portrait_x, portrait_y, portrait_size, portrait_size, BLUE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DrawRectangle(portrait_x, portrait_y, portrait_size, portrait_size, BLUE);
|
||||||
|
}
|
||||||
DrawRectangleLines(portrait_x, portrait_y, portrait_size, portrait_size, (Color){139, 119, 89, 255});
|
DrawRectangleLines(portrait_x, portrait_y, portrait_size, portrait_size, (Color){139, 119, 89, 255});
|
||||||
|
|
||||||
// HP Bar, to the right of portrait
|
// HP Bar, to the right of portrait
|
||||||
|
|
@ -552,7 +785,54 @@ static int label_font_size(FloatingLabel label) {
|
||||||
return (label == LABEL_CRIT) ? FONT_SIZE_FLOAT_CRIT : FONT_SIZE_FLOAT_LABEL;
|
return (label == LABEL_CRIT) ? FONT_SIZE_FLOAT_CRIT : FONT_SIZE_FLOAT_LABEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
void render_floating_texts(FloatingText *texts, int count, int shake_x, int shake_y) {
|
void render_slash_effect(int x, int y, DamageClass dmg_class, int timer) {
|
||||||
|
if (timer <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float alpha = (float)timer / 8.0f;
|
||||||
|
if (alpha > 1.0f)
|
||||||
|
alpha = 1.0f;
|
||||||
|
int a = (int)(255 * alpha);
|
||||||
|
int px = x * TILE_SIZE;
|
||||||
|
int py = y * TILE_SIZE;
|
||||||
|
|
||||||
|
switch (dmg_class) {
|
||||||
|
case DMG_SLASH:
|
||||||
|
// Red diagonal slash
|
||||||
|
DrawLine(px + 2, py + 2, px + TILE_SIZE - 2, py + TILE_SIZE - 2, (Color){255, 80, 80, a});
|
||||||
|
DrawLine(px + 4, py + 2, px + TILE_SIZE - 2, py + TILE_SIZE - 4, (Color){255, 120, 120, a});
|
||||||
|
break;
|
||||||
|
case DMG_IMPACT:
|
||||||
|
// Orange burst (star pattern)
|
||||||
|
DrawLine(px + TILE_SIZE / 2, py + 2, px + TILE_SIZE / 2, py + TILE_SIZE - 2, (Color){255, 180, 60, a});
|
||||||
|
DrawLine(px + 2, py + TILE_SIZE / 2, px + TILE_SIZE - 2, py + TILE_SIZE / 2, (Color){255, 180, 60, a});
|
||||||
|
DrawLine(px + 4, py + 4, px + TILE_SIZE - 4, py + TILE_SIZE - 4, (Color){255, 200, 100, a});
|
||||||
|
DrawLine(px + 4, py + TILE_SIZE - 4, px + TILE_SIZE - 4, py + 4, (Color){255, 200, 100, a});
|
||||||
|
break;
|
||||||
|
case DMG_PIERCE:
|
||||||
|
// Yellow horizontal streak
|
||||||
|
DrawLine(px + 2, py + TILE_SIZE / 2, px + TILE_SIZE - 2, py + TILE_SIZE / 2, (Color){255, 255, 100, a});
|
||||||
|
DrawLine(px + 2, py + TILE_SIZE / 2 - 2, px + TILE_SIZE - 2, py + TILE_SIZE / 2 - 2, (Color){255, 255, 150, a});
|
||||||
|
DrawLine(px + 2, py + TILE_SIZE / 2 + 2, px + TILE_SIZE - 2, py + TILE_SIZE / 2 + 2, (Color){255, 255, 150, a});
|
||||||
|
break;
|
||||||
|
case DMG_FIRE:
|
||||||
|
// Red-orange flame burst
|
||||||
|
DrawLine(px + TILE_SIZE / 2, py + TILE_SIZE - 2, px + TILE_SIZE / 2, py + 4, (Color){255, 100, 30, a});
|
||||||
|
DrawLine(px + TILE_SIZE / 2 - 3, py + TILE_SIZE - 4, px + TILE_SIZE / 2 - 1, py + 6, (Color){255, 150, 50, a});
|
||||||
|
DrawLine(px + TILE_SIZE / 2 + 3, py + TILE_SIZE - 4, px + TILE_SIZE / 2 + 1, py + 6, (Color){255, 150, 50, a});
|
||||||
|
break;
|
||||||
|
case DMG_POISON:
|
||||||
|
// Green splash
|
||||||
|
DrawLine(px + 4, py + 4, px + TILE_SIZE - 4, py + TILE_SIZE - 4, (Color){50, 255, 100, a});
|
||||||
|
DrawLine(px + TILE_SIZE - 4, py + 4, px + 4, py + TILE_SIZE - 4, (Color){80, 255, 120, a});
|
||||||
|
DrawCircle(px + TILE_SIZE / 2, py + TILE_SIZE / 2, 3.0f, (Color){100, 255, 150, a / 2});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void render_floating_texts(FloatingText *texts, int count, int shake_x, int shake_y, const FontManager *fm) {
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
if (texts[i].lifetime <= 0)
|
if (texts[i].lifetime <= 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -568,15 +848,17 @@ void render_floating_texts(FloatingText *texts, int count, int shake_x, int shak
|
||||||
int font_size = label_font_size(texts[i].label);
|
int font_size = label_font_size(texts[i].label);
|
||||||
Color color = label_color(&texts[i], a);
|
Color color = label_color(&texts[i], a);
|
||||||
const char *text = label_text(texts[i].label);
|
const char *text = label_text(texts[i].label);
|
||||||
int text_w = MeasureText(text, font_size);
|
Vector2 text_size = MeasureTextEx(fm->body_font, text, (float)font_size, NORM_CHAR_SPACE);
|
||||||
DrawText(text, x - text_w / 2, y, font_size, color);
|
draw_text_body(fm->body_font, text, (float)(x - (int)text_size.x / 2), (float)y, font_size, NORM_CHAR_SPACE,
|
||||||
|
color);
|
||||||
} else {
|
} else {
|
||||||
// Numeric damage
|
// Numeric damage
|
||||||
Color color = texts[i].is_critical ? (Color){255, 200, 50, a} : (Color){255, 100, 100, a};
|
Color color = texts[i].is_critical ? (Color){255, 200, 50, a} : (Color){255, 100, 100, a};
|
||||||
char text[16];
|
char text[16];
|
||||||
snprintf(text, sizeof(text), "%d", texts[i].value);
|
snprintf(text, sizeof(text), "%d", texts[i].value);
|
||||||
int text_w = MeasureText(text, FONT_SIZE_FLOAT_DMG);
|
Vector2 text_size = MeasureTextEx(fm->body_font, text, (float)FONT_SIZE_FLOAT_DMG, NORM_CHAR_SPACE);
|
||||||
DrawText(text, x - text_w / 2, y, FONT_SIZE_FLOAT_DMG, color);
|
draw_text_body(fm->body_font, text, (float)(x - (int)text_size.x / 2), (float)y, FONT_SIZE_FLOAT_DMG,
|
||||||
|
NORM_CHAR_SPACE, color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
src/render.h
27
src/render.h
|
|
@ -99,20 +99,24 @@ int init_fonts(FontManager *fm);
|
||||||
// Unload all fonts held by a FontManager
|
// Unload all fonts held by a FontManager
|
||||||
void destroy_fonts(FontManager *fm);
|
void destroy_fonts(FontManager *fm);
|
||||||
|
|
||||||
// Render the map tiles
|
// Render the map tiles using tileset atlas
|
||||||
void render_map(const Map *map);
|
void render_map(const Map *map, const Tileset *tileset);
|
||||||
|
|
||||||
// Render the player
|
// Render the player using tileset atlas
|
||||||
void render_player(const Player *p);
|
// frame_counter is used for idle breathing animation
|
||||||
|
void render_player(const Player *p, const Tileset *tileset, int frame_counter);
|
||||||
|
|
||||||
// Render all enemies
|
// Render all enemies using tileset atlas
|
||||||
void render_enemies(const Enemy *enemies, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH]);
|
// frame_counter is used for idle breathing animation
|
||||||
|
void render_enemies(const Enemy *enemies, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH],
|
||||||
|
const Tileset *tileset, int frame_counter);
|
||||||
|
|
||||||
// Render all items
|
// Render all items using tileset atlas
|
||||||
void render_items(const Item *items, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH]);
|
void render_items(const Item *items, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH],
|
||||||
|
const Tileset *tileset);
|
||||||
|
|
||||||
// Render UI overlay
|
// Render UI overlay
|
||||||
void render_ui(const Player *p, const FontManager *fm);
|
void render_ui(const Player *p, const Tileset *tileset, const FontManager *fm);
|
||||||
|
|
||||||
// Render action log (bottom left corner)
|
// Render action log (bottom left corner)
|
||||||
void render_action_log(const char log[5][128], int count, int head, const FontManager *fm);
|
void render_action_log(const char log[5][128], int count, int head, const FontManager *fm);
|
||||||
|
|
@ -121,7 +125,10 @@ void render_action_log(const char log[5][128], int count, int head, const FontMa
|
||||||
void render_inventory_overlay(const Player *p, int selected, const FontManager *fm);
|
void render_inventory_overlay(const Player *p, int selected, const FontManager *fm);
|
||||||
|
|
||||||
// Render floating damage text
|
// Render floating damage text
|
||||||
void render_floating_texts(FloatingText *texts, int count, int shake_x, int shake_y);
|
void render_floating_texts(FloatingText *texts, int count, int shake_x, int shake_y, const FontManager *fm);
|
||||||
|
|
||||||
|
// Render slash effect during attacks
|
||||||
|
void render_slash_effect(int x, int y, DamageClass dmg_class, int timer);
|
||||||
|
|
||||||
// Render end screen (victory or death) with stats breakdown
|
// Render end screen (victory or death) with stats breakdown
|
||||||
void render_end_screen(int is_victory, int kills, int items, int damage_dealt, int damage_taken, int crits,
|
void render_end_screen(int is_victory, int kills, int items, int damage_dealt, int damage_taken, int crits,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue