Some checks failed
Linux / Build Linux (push) Has been cancelled
Linux / Build Linux-1 (push) Has been cancelled
macOS / Build macOS (push) Has been cancelled
macOS / Build macOS-1 (push) Has been cancelled
Windows (MinGW) / Build MinGW (push) Has been cancelled
Windows (MinGW) / Build MinGW-1 (push) Has been cancelled
Windows (MSVC) / Build Windows (push) Has been cancelled
Windows (MSVC) / Build Windows-1 (push) Has been cancelled
320 lines
10 KiB
C
320 lines
10 KiB
C
#include "rlc_turn_manager.h"
|
|
#include "rlc_components.h"
|
|
#include "rlc_items.h"
|
|
#include "rlc_log.h"
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
/* Action queue comparison function for qsort */
|
|
static int action_compare(const void *a, const void *b) {
|
|
const QueuedAction *qa = (const QueuedAction *)a;
|
|
const QueuedAction *qb = (const QueuedAction *)b;
|
|
/* Sort by delay ascending, then priority descending */
|
|
if (qa->delay != qb->delay) return qa->delay - qb->delay;
|
|
return qb->priority - qa->priority;
|
|
}
|
|
|
|
/* Event queue comparison function for qsort */
|
|
static int event_compare(const void *a, const void *b) {
|
|
const DelayedEvent *ea = (const DelayedEvent *)a;
|
|
const DelayedEvent *eb = (const DelayedEvent *)b;
|
|
/* Sort by turn_due ascending, then priority descending */
|
|
/* Use comparison operators instead of subtraction to prevent overflow */
|
|
if (ea->turn_due < eb->turn_due) return -1;
|
|
if (ea->turn_due > eb->turn_due) return 1;
|
|
if (eb->priority < ea->priority) return -1;
|
|
if (eb->priority > ea->priority) return 1;
|
|
return 0;
|
|
}
|
|
|
|
/* Action queue management */
|
|
void rlc_action_queue_init(TurnState *ts) {
|
|
ts->action_capacity = 32;
|
|
ts->action_count = 0;
|
|
ts->action_queue = malloc(sizeof(QueuedAction) * ts->action_capacity);
|
|
}
|
|
|
|
void rlc_action_queue_push(TurnState *ts, QueuedAction action) {
|
|
if (ts->action_count >= ts->action_capacity) {
|
|
size_t new_capacity = ts->action_capacity * 2;
|
|
QueuedAction *new_queue = realloc(ts->action_queue,
|
|
sizeof(QueuedAction) * new_capacity);
|
|
if (!new_queue) {
|
|
RLC_LOG_ERROR("Failed to resize action queue (capacity=%zu)", ts->action_capacity);
|
|
return;
|
|
}
|
|
ts->action_queue = new_queue;
|
|
ts->action_capacity = new_capacity;
|
|
}
|
|
ts->action_queue[ts->action_count++] = action;
|
|
}
|
|
|
|
void rlc_action_queue_sort(TurnState *ts) {
|
|
if (ts->action_count > 1) {
|
|
qsort(ts->action_queue, ts->action_count,
|
|
sizeof(QueuedAction), action_compare);
|
|
}
|
|
}
|
|
|
|
void rlc_action_queue_clear(TurnState *ts) {
|
|
ts->action_count = 0;
|
|
}
|
|
|
|
void rlc_action_queue_destroy(TurnState *ts) {
|
|
if (ts->action_queue) {
|
|
free(ts->action_queue);
|
|
ts->action_queue = NULL;
|
|
ts->action_count = 0;
|
|
ts->action_capacity = 0;
|
|
}
|
|
}
|
|
|
|
/* Event queue management */
|
|
void rlc_event_queue_init(TurnState *ts) {
|
|
ts->event_capacity = 16;
|
|
ts->event_count = 0;
|
|
ts->event_queue = malloc(sizeof(DelayedEvent) * ts->event_capacity);
|
|
}
|
|
|
|
void rlc_event_queue_push(TurnState *ts, DelayedEvent event) {
|
|
if (ts->event_count >= ts->event_capacity) {
|
|
size_t new_capacity = ts->event_capacity * 2;
|
|
DelayedEvent *new_queue = realloc(ts->event_queue,
|
|
sizeof(DelayedEvent) * new_capacity);
|
|
if (!new_queue) {
|
|
RLC_LOG_ERROR("Failed to resize event queue (capacity=%zu)", ts->event_capacity);
|
|
return;
|
|
}
|
|
ts->event_queue = new_queue;
|
|
ts->event_capacity = new_capacity;
|
|
}
|
|
ts->event_queue[ts->event_count++] = event;
|
|
}
|
|
|
|
void rlc_event_queue_process(ecs_world_t *world, TurnState *ts) {
|
|
if (ts->event_count == 0) return;
|
|
|
|
/* Sort events by turn_due and priority */
|
|
qsort(ts->event_queue, ts->event_count,
|
|
sizeof(DelayedEvent), event_compare);
|
|
|
|
size_t remaining = 0;
|
|
|
|
for (size_t i = 0; i < ts->event_count; i++) {
|
|
DelayedEvent *event = &ts->event_queue[i];
|
|
|
|
if (event->turn_due <= ts->round_number) {
|
|
/* Execute event */
|
|
if (event->callback) {
|
|
event->callback(world, event->data);
|
|
}
|
|
/* Free data if allocated */
|
|
if (event->data) {
|
|
free(event->data);
|
|
event->data = NULL;
|
|
}
|
|
} else {
|
|
/* Keep event - move to front if needed */
|
|
if (remaining != i) {
|
|
ts->event_queue[remaining] = *event;
|
|
}
|
|
remaining++;
|
|
}
|
|
}
|
|
|
|
ts->event_count = remaining;
|
|
}
|
|
|
|
void rlc_event_queue_clear(TurnState *ts) {
|
|
for (size_t i = 0; i < ts->event_count; i++) {
|
|
if (ts->event_queue[i].data) {
|
|
free(ts->event_queue[i].data);
|
|
ts->event_queue[i].data = NULL;
|
|
}
|
|
}
|
|
ts->event_count = 0;
|
|
}
|
|
|
|
void rlc_event_queue_destroy(TurnState *ts) {
|
|
if (ts->event_queue) {
|
|
rlc_event_queue_clear(ts);
|
|
free(ts->event_queue);
|
|
ts->event_queue = NULL;
|
|
ts->event_capacity = 0;
|
|
}
|
|
}
|
|
|
|
/* Turn manager initialization */
|
|
void rlc_turn_manager_init(ecs_world_t *world) {
|
|
TurnState *ts = ecs_singleton_get_mut(world, TurnState);
|
|
if (ts) {
|
|
memset(ts, 0, sizeof(TurnState));
|
|
ts->phase = PHASE_WAITING_FOR_PLAYER;
|
|
ts->round_number = 1;
|
|
ts->processing = false;
|
|
ts->phase_transitioned = false;
|
|
|
|
rlc_action_queue_init(ts);
|
|
rlc_event_queue_init(ts);
|
|
}
|
|
}
|
|
|
|
/* --- Message Log API Implementation --- */
|
|
|
|
const char* rlc_get_entity_display_name(ecs_world_t *world, ecs_entity_t entity) {
|
|
if (!world || !entity) {
|
|
return "something";
|
|
}
|
|
|
|
/* Check for flecs built-in name first */
|
|
const char *name = ecs_get_name(world, entity);
|
|
if (name && name[0] != '\0') {
|
|
return name;
|
|
}
|
|
|
|
/* Check if it's the player */
|
|
if (ecs_has(world, entity, Player)) {
|
|
return "you";
|
|
}
|
|
|
|
return "something";
|
|
}
|
|
|
|
void rlc_log_message(ecs_world_t *world, int type, const char *fmt, ...) {
|
|
if (!world || !fmt) return;
|
|
|
|
MessageLog *log = ecs_singleton_get_mut(world, MessageLog);
|
|
if (!log) return;
|
|
|
|
const TurnState *ts = ecs_singleton_get(world, TurnState);
|
|
int turn = ts ? ts->round_number : 0;
|
|
|
|
/* Format the message */
|
|
char formatted[128];
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
vsnprintf(formatted, sizeof(formatted), fmt, args);
|
|
va_end(args);
|
|
|
|
/* Check for stacking with the most recent message */
|
|
if (log->message_count > 0) {
|
|
int prev_idx = (log->write_index - 1 + MAX_LOG_MESSAGES) % MAX_LOG_MESSAGES;
|
|
LogMessage *prev = &log->messages[prev_idx];
|
|
|
|
/* Stack if same message on same turn */
|
|
if ((int)prev->type == type &&
|
|
prev->turn_number == turn &&
|
|
strcmp(prev->formatted, formatted) == 0) {
|
|
prev->stack_count++;
|
|
log->updated_this_frame = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Write new message */
|
|
LogMessage *msg = &log->messages[log->write_index];
|
|
strncpy(msg->formatted, formatted, sizeof(msg->formatted) - 1);
|
|
msg->formatted[sizeof(msg->formatted) - 1] = '\0';
|
|
msg->type = (LogMessageType)type;
|
|
msg->turn_number = turn;
|
|
msg->stack_count = 1;
|
|
|
|
/* Advance write index (circular) */
|
|
log->write_index = (log->write_index + 1) % MAX_LOG_MESSAGES;
|
|
if (log->message_count < MAX_LOG_MESSAGES) {
|
|
log->message_count++;
|
|
}
|
|
|
|
log->updated_this_frame = true;
|
|
}
|
|
|
|
void rlc_log_combat(ecs_world_t *world, ecs_entity_t attacker, ecs_entity_t target, int damage, bool critical) {
|
|
if (!world) return;
|
|
|
|
/* Cache names at call time to avoid entity recycling issues */
|
|
const char *attacker_name = rlc_get_entity_display_name(world, attacker);
|
|
const char *target_name = rlc_get_entity_display_name(world, target);
|
|
|
|
/* Copy names to local buffers in case entities are deleted */
|
|
char atk_buf[64], tgt_buf[64];
|
|
snprintf(atk_buf, sizeof(atk_buf), "%s", attacker_name);
|
|
snprintf(tgt_buf, sizeof(tgt_buf), "%s", target_name);
|
|
|
|
/* Capitalize first letter for display */
|
|
if (atk_buf[0] >= 'a' && atk_buf[0] <= 'z') {
|
|
atk_buf[0] = atk_buf[0] - 'a' + 'A';
|
|
}
|
|
|
|
/* Check if target is dead (for kill message) */
|
|
const Health *target_hp = ecs_get(world, target, Health);
|
|
bool is_kill = target_hp && target_hp->current <= 0;
|
|
|
|
if (is_kill) {
|
|
rlc_log_message(world, LOG_MSG_COMBAT_KILL,
|
|
"%s kills %s!", atk_buf, tgt_buf);
|
|
} else if (critical) {
|
|
rlc_log_message(world, LOG_MSG_COMBAT_CRITICAL,
|
|
"%s critically hits %s for %d damage!", atk_buf, tgt_buf, damage);
|
|
} else if (damage > 0) {
|
|
rlc_log_message(world, LOG_MSG_COMBAT_HIT,
|
|
"%s hits %s for %d damage.", atk_buf, tgt_buf, damage);
|
|
} else {
|
|
rlc_log_message(world, LOG_MSG_COMBAT_MISS,
|
|
"%s misses %s.", atk_buf, tgt_buf);
|
|
}
|
|
}
|
|
|
|
void rlc_log_combat_detailed(ecs_world_t *world, ecs_entity_t attacker, ecs_entity_t target,
|
|
int raw_damage, int defense, int final_damage,
|
|
ecs_entity_t weapon_entity) {
|
|
if (!world) return;
|
|
|
|
const char *attacker_name = rlc_get_entity_display_name(world, attacker);
|
|
const char *target_name = rlc_get_entity_display_name(world, target);
|
|
|
|
char atk_buf[64], tgt_buf[64];
|
|
snprintf(atk_buf, sizeof(atk_buf), "%s", attacker_name);
|
|
snprintf(tgt_buf, sizeof(tgt_buf), "%s", target_name);
|
|
|
|
if (atk_buf[0] >= 'a' && atk_buf[0] <= 'z') {
|
|
atk_buf[0] = atk_buf[0] - 'a' + 'A';
|
|
}
|
|
|
|
const Health *target_hp = ecs_get(world, target, Health);
|
|
bool is_kill = target_hp && target_hp->current <= 0;
|
|
|
|
if (is_kill) {
|
|
rlc_log_message(world, LOG_MSG_COMBAT_KILL,
|
|
"%s kills %s!", atk_buf, tgt_buf);
|
|
} else if (final_damage > 0) {
|
|
/* Get weapon name for detailed message */
|
|
const char *wpn_name = NULL;
|
|
if (weapon_entity) {
|
|
const Name *wn = ecs_get(world, weapon_entity, Name);
|
|
if (wn) wpn_name = wn->str;
|
|
}
|
|
|
|
if (wpn_name && defense > 0) {
|
|
rlc_log_message(world, LOG_MSG_COMBAT_HIT,
|
|
"%s hits %s with %s: %d - %d def = %d",
|
|
atk_buf, tgt_buf, wpn_name, raw_damage, defense, final_damage);
|
|
} else if (wpn_name) {
|
|
rlc_log_message(world, LOG_MSG_COMBAT_HIT,
|
|
"%s hits %s with %s for %d damage.",
|
|
atk_buf, tgt_buf, wpn_name, final_damage);
|
|
} else if (defense > 0) {
|
|
rlc_log_message(world, LOG_MSG_COMBAT_HIT,
|
|
"%s hits %s: %d - %d def = %d",
|
|
atk_buf, tgt_buf, raw_damage, defense, final_damage);
|
|
} else {
|
|
rlc_log_message(world, LOG_MSG_COMBAT_HIT,
|
|
"%s hits %s for %d damage.",
|
|
atk_buf, tgt_buf, final_damage);
|
|
}
|
|
} else {
|
|
rlc_log_message(world, LOG_MSG_COMBAT_MISS,
|
|
"%s misses %s.", atk_buf, tgt_buf);
|
|
}
|
|
}
|