- Add EntityBehavior struct with 11 behavior types: IDLE, CUSTOM, NOISE4/8, PATH, WAYPOINT, PATROL, LOOP, SLEEP, SEEK, FLEE. Each returns BehaviorOutput (MOVED/DONE/BLOCKED/NO_ACTION) without modifying entity position directly (closes #300) - Add grid.step(n=1, turn_order=None) turn manager: groups entities by turn_order, executes behaviors, fires triggers (TARGET/DONE/BLOCKED), updates cell_position and spatial hash. Snapshot-based iteration for callback safety (closes #301) - Entity properties: behavior_type (read-only), turn_order, move_speed, target_label, sight_radius. Method: set_behavior(type, waypoints, turns, path) - Update ColorLayer::updatePerspective to use cell_position Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
321 lines
11 KiB
C++
321 lines
11 KiB
C++
#include "EntityBehavior.h"
|
|
#include "UIEntity.h"
|
|
#include "UIGrid.h"
|
|
#include "UIGridPathfinding.h"
|
|
#include <random>
|
|
#include <algorithm>
|
|
|
|
// Thread-local random engine for behavior randomness
|
|
static thread_local std::mt19937 rng{std::random_device{}()};
|
|
|
|
// =============================================================================
|
|
// Per-behavior execution functions
|
|
// =============================================================================
|
|
|
|
static BehaviorOutput executeIdle(UIEntity& entity, UIGrid& grid) {
|
|
return {BehaviorResult::NO_ACTION, {}};
|
|
}
|
|
|
|
static BehaviorOutput executeCustom(UIEntity& entity, UIGrid& grid) {
|
|
// CUSTOM does nothing built-in — step callback handles everything
|
|
return {BehaviorResult::NO_ACTION, {}};
|
|
}
|
|
|
|
static bool isCellWalkable(UIGrid& grid, int x, int y) {
|
|
if (x < 0 || x >= grid.grid_w || y < 0 || y >= grid.grid_h) return false;
|
|
return grid.at(x, y).walkable;
|
|
}
|
|
|
|
static BehaviorOutput executeNoise(UIEntity& entity, UIGrid& grid, bool include_diagonals) {
|
|
int cx = entity.cell_position.x;
|
|
int cy = entity.cell_position.y;
|
|
|
|
// Build candidate moves
|
|
std::vector<sf::Vector2i> candidates;
|
|
// Cardinal directions
|
|
sf::Vector2i dirs4[] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
|
|
sf::Vector2i dirs8[] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0},
|
|
{-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
|
|
|
|
auto* dirs = include_diagonals ? dirs8 : dirs4;
|
|
int count = include_diagonals ? 8 : 4;
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
int nx = cx + dirs[i].x;
|
|
int ny = cy + dirs[i].y;
|
|
if (isCellWalkable(grid, nx, ny)) {
|
|
candidates.push_back({nx, ny});
|
|
}
|
|
}
|
|
|
|
if (candidates.empty()) {
|
|
return {BehaviorResult::NO_ACTION, {}};
|
|
}
|
|
|
|
std::uniform_int_distribution<int> dist(0, candidates.size() - 1);
|
|
auto target = candidates[dist(rng)];
|
|
return {BehaviorResult::MOVED, target};
|
|
}
|
|
|
|
static BehaviorOutput executePath(UIEntity& entity, UIGrid& grid) {
|
|
auto& behavior = entity.behavior;
|
|
|
|
if (behavior.path_step_index >= static_cast<int>(behavior.current_path.size())) {
|
|
return {BehaviorResult::DONE, {}};
|
|
}
|
|
|
|
auto target = behavior.current_path[behavior.path_step_index];
|
|
behavior.path_step_index++;
|
|
|
|
if (!isCellWalkable(grid, target.x, target.y)) {
|
|
return {BehaviorResult::BLOCKED, target};
|
|
}
|
|
|
|
return {BehaviorResult::MOVED, target};
|
|
}
|
|
|
|
static BehaviorOutput executeWaypoint(UIEntity& entity, UIGrid& grid) {
|
|
auto& behavior = entity.behavior;
|
|
|
|
if (behavior.waypoints.empty()) {
|
|
return {BehaviorResult::DONE, {}};
|
|
}
|
|
|
|
// If we've reached the current waypoint, advance to next
|
|
auto& wp = behavior.waypoints[behavior.current_waypoint_index];
|
|
if (entity.cell_position.x == wp.x && entity.cell_position.y == wp.y) {
|
|
behavior.current_waypoint_index++;
|
|
if (behavior.current_waypoint_index >= static_cast<int>(behavior.waypoints.size())) {
|
|
return {BehaviorResult::DONE, {}};
|
|
}
|
|
// Clear path to recompute for new waypoint
|
|
behavior.current_path.clear();
|
|
behavior.path_step_index = 0;
|
|
}
|
|
|
|
// Compute path to current waypoint if needed
|
|
if (behavior.current_path.empty() || behavior.path_step_index >= static_cast<int>(behavior.current_path.size())) {
|
|
auto& target_wp = behavior.waypoints[behavior.current_waypoint_index];
|
|
// Use grid pathfinding (A*)
|
|
TCODPath path(grid.getTCODMap());
|
|
path.compute(entity.cell_position.x, entity.cell_position.y, target_wp.x, target_wp.y);
|
|
|
|
behavior.current_path.clear();
|
|
behavior.path_step_index = 0;
|
|
int px, py;
|
|
while (path.walk(&px, &py, true)) {
|
|
behavior.current_path.push_back({px, py});
|
|
}
|
|
|
|
if (behavior.current_path.empty()) {
|
|
return {BehaviorResult::BLOCKED, target_wp};
|
|
}
|
|
}
|
|
|
|
// Follow the path
|
|
auto target = behavior.current_path[behavior.path_step_index];
|
|
behavior.path_step_index++;
|
|
|
|
if (!isCellWalkable(grid, target.x, target.y)) {
|
|
return {BehaviorResult::BLOCKED, target};
|
|
}
|
|
|
|
return {BehaviorResult::MOVED, target};
|
|
}
|
|
|
|
static BehaviorOutput executePatrol(UIEntity& entity, UIGrid& grid) {
|
|
auto& behavior = entity.behavior;
|
|
|
|
if (behavior.waypoints.empty()) {
|
|
return {BehaviorResult::NO_ACTION, {}};
|
|
}
|
|
|
|
// Check if at current waypoint
|
|
auto& wp = behavior.waypoints[behavior.current_waypoint_index];
|
|
if (entity.cell_position.x == wp.x && entity.cell_position.y == wp.y) {
|
|
int next = behavior.current_waypoint_index + behavior.patrol_direction;
|
|
if (next < 0 || next >= static_cast<int>(behavior.waypoints.size())) {
|
|
behavior.patrol_direction *= -1;
|
|
next = behavior.current_waypoint_index + behavior.patrol_direction;
|
|
}
|
|
behavior.current_waypoint_index = next;
|
|
behavior.current_path.clear();
|
|
behavior.path_step_index = 0;
|
|
}
|
|
|
|
// Same path-following logic as waypoint
|
|
if (behavior.current_path.empty() || behavior.path_step_index >= static_cast<int>(behavior.current_path.size())) {
|
|
auto& target_wp = behavior.waypoints[behavior.current_waypoint_index];
|
|
TCODPath path(grid.getTCODMap());
|
|
path.compute(entity.cell_position.x, entity.cell_position.y, target_wp.x, target_wp.y);
|
|
|
|
behavior.current_path.clear();
|
|
behavior.path_step_index = 0;
|
|
int px, py;
|
|
while (path.walk(&px, &py, true)) {
|
|
behavior.current_path.push_back({px, py});
|
|
}
|
|
|
|
if (behavior.current_path.empty()) {
|
|
return {BehaviorResult::BLOCKED, target_wp};
|
|
}
|
|
}
|
|
|
|
auto target = behavior.current_path[behavior.path_step_index];
|
|
behavior.path_step_index++;
|
|
|
|
if (!isCellWalkable(grid, target.x, target.y)) {
|
|
return {BehaviorResult::BLOCKED, target};
|
|
}
|
|
|
|
return {BehaviorResult::MOVED, target};
|
|
}
|
|
|
|
static BehaviorOutput executeLoop(UIEntity& entity, UIGrid& grid) {
|
|
auto& behavior = entity.behavior;
|
|
|
|
if (behavior.waypoints.empty()) {
|
|
return {BehaviorResult::NO_ACTION, {}};
|
|
}
|
|
|
|
// Check if at current waypoint
|
|
auto& wp = behavior.waypoints[behavior.current_waypoint_index];
|
|
if (entity.cell_position.x == wp.x && entity.cell_position.y == wp.y) {
|
|
behavior.current_waypoint_index = (behavior.current_waypoint_index + 1) % behavior.waypoints.size();
|
|
behavior.current_path.clear();
|
|
behavior.path_step_index = 0;
|
|
}
|
|
|
|
// Same path-following logic
|
|
if (behavior.current_path.empty() || behavior.path_step_index >= static_cast<int>(behavior.current_path.size())) {
|
|
auto& target_wp = behavior.waypoints[behavior.current_waypoint_index];
|
|
TCODPath path(grid.getTCODMap());
|
|
path.compute(entity.cell_position.x, entity.cell_position.y, target_wp.x, target_wp.y);
|
|
|
|
behavior.current_path.clear();
|
|
behavior.path_step_index = 0;
|
|
int px, py;
|
|
while (path.walk(&px, &py, true)) {
|
|
behavior.current_path.push_back({px, py});
|
|
}
|
|
|
|
if (behavior.current_path.empty()) {
|
|
return {BehaviorResult::BLOCKED, target_wp};
|
|
}
|
|
}
|
|
|
|
auto target = behavior.current_path[behavior.path_step_index];
|
|
behavior.path_step_index++;
|
|
|
|
if (!isCellWalkable(grid, target.x, target.y)) {
|
|
return {BehaviorResult::BLOCKED, target};
|
|
}
|
|
|
|
return {BehaviorResult::MOVED, target};
|
|
}
|
|
|
|
static BehaviorOutput executeSleep(UIEntity& entity, UIGrid& grid) {
|
|
auto& behavior = entity.behavior;
|
|
|
|
if (behavior.sleep_turns_remaining > 0) {
|
|
behavior.sleep_turns_remaining--;
|
|
if (behavior.sleep_turns_remaining == 0) {
|
|
return {BehaviorResult::DONE, {}};
|
|
}
|
|
}
|
|
return {BehaviorResult::NO_ACTION, {}};
|
|
}
|
|
|
|
static BehaviorOutput executeSeek(UIEntity& entity, UIGrid& grid) {
|
|
auto& behavior = entity.behavior;
|
|
|
|
if (!behavior.dijkstra_map) {
|
|
return {BehaviorResult::NO_ACTION, {}};
|
|
}
|
|
|
|
// Use Dijkstra map to find the lowest-distance neighbor (moving toward target)
|
|
int cx = entity.cell_position.x;
|
|
int cy = entity.cell_position.y;
|
|
float best_dist = std::numeric_limits<float>::max();
|
|
sf::Vector2i best_cell = {cx, cy};
|
|
bool found = false;
|
|
|
|
sf::Vector2i dirs[] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0},
|
|
{-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
|
|
|
|
for (auto& dir : dirs) {
|
|
int nx = cx + dir.x;
|
|
int ny = cy + dir.y;
|
|
if (!isCellWalkable(grid, nx, ny)) continue;
|
|
|
|
float dist = behavior.dijkstra_map->getDistance(nx, ny);
|
|
if (dist >= 0 && dist < best_dist) {
|
|
best_dist = dist;
|
|
best_cell = {nx, ny};
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
if (!found || (best_cell.x == cx && best_cell.y == cy)) {
|
|
return {BehaviorResult::BLOCKED, {cx, cy}};
|
|
}
|
|
|
|
return {BehaviorResult::MOVED, best_cell};
|
|
}
|
|
|
|
static BehaviorOutput executeFlee(UIEntity& entity, UIGrid& grid) {
|
|
auto& behavior = entity.behavior;
|
|
|
|
if (!behavior.dijkstra_map) {
|
|
return {BehaviorResult::NO_ACTION, {}};
|
|
}
|
|
|
|
// Use Dijkstra map to find the highest-distance neighbor (fleeing from target)
|
|
int cx = entity.cell_position.x;
|
|
int cy = entity.cell_position.y;
|
|
float best_dist = -1.0f;
|
|
sf::Vector2i best_cell = {cx, cy};
|
|
bool found = false;
|
|
|
|
sf::Vector2i dirs[] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0},
|
|
{-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
|
|
|
|
for (auto& dir : dirs) {
|
|
int nx = cx + dir.x;
|
|
int ny = cy + dir.y;
|
|
if (!isCellWalkable(grid, nx, ny)) continue;
|
|
|
|
float dist = behavior.dijkstra_map->getDistance(nx, ny);
|
|
if (dist >= 0 && dist > best_dist) {
|
|
best_dist = dist;
|
|
best_cell = {nx, ny};
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
if (!found || (best_cell.x == cx && best_cell.y == cy)) {
|
|
return {BehaviorResult::BLOCKED, {cx, cy}};
|
|
}
|
|
|
|
return {BehaviorResult::MOVED, best_cell};
|
|
}
|
|
|
|
// =============================================================================
|
|
// Main dispatch
|
|
// =============================================================================
|
|
BehaviorOutput executeBehavior(UIEntity& entity, UIGrid& grid) {
|
|
switch (entity.behavior.type) {
|
|
case BehaviorType::IDLE: return executeIdle(entity, grid);
|
|
case BehaviorType::CUSTOM: return executeCustom(entity, grid);
|
|
case BehaviorType::NOISE4: return executeNoise(entity, grid, false);
|
|
case BehaviorType::NOISE8: return executeNoise(entity, grid, true);
|
|
case BehaviorType::PATH: return executePath(entity, grid);
|
|
case BehaviorType::WAYPOINT: return executeWaypoint(entity, grid);
|
|
case BehaviorType::PATROL: return executePatrol(entity, grid);
|
|
case BehaviorType::LOOP: return executeLoop(entity, grid);
|
|
case BehaviorType::SLEEP: return executeSleep(entity, grid);
|
|
case BehaviorType::SEEK: return executeSeek(entity, grid);
|
|
case BehaviorType::FLEE: return executeFlee(entity, grid);
|
|
}
|
|
return {BehaviorResult::NO_ACTION, {}};
|
|
}
|