feat(enemy): els enemics poden disparar bales via tick d'IA
This commit is contained in:
@@ -54,10 +54,29 @@ void Bullet::init() {
|
||||
body_.clearAccumulators();
|
||||
}
|
||||
|
||||
void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed) {
|
||||
void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed, const BulletConfig* cfg) {
|
||||
is_active_ = true;
|
||||
owner_id_ = owner_id;
|
||||
|
||||
// Si no es passa cfg, restaurem al config per defecte (BulletRegistry::get):
|
||||
// els slots són reutilitzables i una bala que abans ha estat disparada amb
|
||||
// una variant (p.ex. bullet_long d'enemic) ha de tornar al bullet.shp del
|
||||
// player quan aquest la reutilitza.
|
||||
const BulletConfig* effective = (cfg != nullptr) ? cfg : &BulletRegistry::get();
|
||||
if (effective != config_) {
|
||||
config_ = effective;
|
||||
shape_ = Graphics::ShapeLoader::load(config_->shape.path);
|
||||
if (!shape_ || !shape_->isValid()) {
|
||||
std::cerr << "[Bullet] Error: no s'ha pogut carregar " << config_->shape.path << '\n';
|
||||
}
|
||||
const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F;
|
||||
collision_radius_ = BOUNDING * config_->shape.scale * config_->shape.collision_factor;
|
||||
body_.setMass(config_->physics.mass);
|
||||
body_.restitution = config_->physics.restitution;
|
||||
body_.linear_damping = config_->physics.linear_damping;
|
||||
body_.angular_damping = config_->physics.angular_damping;
|
||||
}
|
||||
|
||||
center_ = position;
|
||||
prev_position_ = position; // spawn: swept degenera a punt-cercle
|
||||
angle_ = angle;
|
||||
|
||||
@@ -19,7 +19,11 @@ class Bullet : public Entities::Entity {
|
||||
explicit Bullet(Rendering::Renderer* renderer);
|
||||
|
||||
void init() override;
|
||||
void fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed);
|
||||
// cfg = nullptr → manté el config actual (per defecte: BulletRegistry::get()).
|
||||
// cfg != nullptr → substitueix el config per a aquesta bala (recarrega
|
||||
// shape, recalcula collision_radius_, mass, etc.). Útil per a bales
|
||||
// d'enemic amb shape pròpia.
|
||||
void fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed, const BulletConfig* cfg = nullptr);
|
||||
void update(float delta_time) override;
|
||||
void postUpdate(float delta_time) override;
|
||||
void draw() const override;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// bullet_registry.cpp - Implementació del registre de bala
|
||||
// bullet_registry.cpp - Implementació del registre de bales
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "game/entities/bullet_registry.hpp"
|
||||
@@ -8,23 +8,34 @@
|
||||
|
||||
#include "core/entities/entity_loader.hpp"
|
||||
|
||||
BulletConfig BulletRegistry::config;
|
||||
std::unordered_map<std::string, BulletConfig> BulletRegistry::configs;
|
||||
bool BulletRegistry::loaded = false;
|
||||
|
||||
namespace {
|
||||
// Tria comú: carrega el YAML d'un name, parseja a BulletConfig i el guarda
|
||||
// al map. Retorna punter al config inserit, o nullptr si falla.
|
||||
auto loadInto(std::unordered_map<std::string, BulletConfig>& configs, const std::string& name) -> const BulletConfig* {
|
||||
auto yaml = Entities::EntityLoader::load(name);
|
||||
if (!yaml) {
|
||||
std::cerr << "[BulletRegistry] Error: no s'ha pogut carregar " << name << ".yaml\n";
|
||||
return nullptr;
|
||||
}
|
||||
auto cfg = BulletConfig::fromYaml(*yaml);
|
||||
if (!cfg) {
|
||||
std::cerr << "[BulletRegistry] Error: format invàlid a " << name << ".yaml\n";
|
||||
return nullptr;
|
||||
}
|
||||
auto [it, _] = configs.insert_or_assign(name, *cfg);
|
||||
return &it->second;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
auto BulletRegistry::load() -> bool {
|
||||
auto yaml = Entities::EntityLoader::load("bullet");
|
||||
if (!yaml) {
|
||||
std::cerr << "[BulletRegistry] Error: no s'ha pogut carregar bullet.yaml\n";
|
||||
if (loadInto(configs, "bullet") == nullptr) {
|
||||
return false;
|
||||
}
|
||||
auto cfg = BulletConfig::fromYaml(*yaml);
|
||||
if (!cfg) {
|
||||
std::cerr << "[BulletRegistry] Error: format invàlid a bullet.yaml\n";
|
||||
return false;
|
||||
}
|
||||
config = *cfg;
|
||||
loaded = true;
|
||||
std::cout << "[BulletRegistry] Configuració de bala carregada.\n";
|
||||
std::cout << "[BulletRegistry] Configuració de bala 'bullet' carregada.\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -33,5 +44,13 @@ auto BulletRegistry::get() -> const BulletConfig& {
|
||||
std::cerr << "[BulletRegistry] FATAL: get() abans de load()\n";
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
return config;
|
||||
return configs.at("bullet");
|
||||
}
|
||||
|
||||
auto BulletRegistry::get(const std::string& name) -> const BulletConfig* {
|
||||
auto it = configs.find(name);
|
||||
if (it != configs.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
return loadInto(configs, name);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
// bullet_registry.hpp - Registre estàtic de la configuració de la bala
|
||||
// bullet_registry.hpp - Registre estàtic de configuracions de bala
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Una única instància per a tota la sessió. Es manté el patró registry
|
||||
// (paral·lel a EnemyRegistry) tot i ser una sola entitat: si el dia de demà
|
||||
// hi ha més tipus de bala (laser/plasma/etc.) només cal estendre-ho.
|
||||
// Diverses configuracions per nom (data/entities/<name>/<name>.yaml). El
|
||||
// config "bullet" es manté com a default històric (player) i es carrega via
|
||||
// load(). Els altres (ex. "bullet_long" per a bales d'enemic) es carreguen
|
||||
// peresosament la primera vegada que algú els demana per nom.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "game/entities/bullet_config.hpp"
|
||||
|
||||
class BulletRegistry {
|
||||
public:
|
||||
BulletRegistry() = delete;
|
||||
|
||||
// Carrega data/entities/bullet/bullet.yaml. Retorna false si falla.
|
||||
// Carrega data/entities/bullet/bullet.yaml com a default. Retorna false si falla.
|
||||
static auto load() -> bool;
|
||||
|
||||
// Accés a la configuració. Avorta amb log fatal si load() no s'ha cridat
|
||||
// o ha fallat.
|
||||
// Accés a la configuració per defecte ("bullet"). Avorta amb log fatal si
|
||||
// load() no s'ha cridat o ha fallat. Mantingut per a backwards compat del Bullet ctor.
|
||||
static auto get() -> const BulletConfig&;
|
||||
|
||||
// Accés a una configuració per nom. Lazy-load: si no està al map, intenta
|
||||
// carregar data/entities/<name>/<name>.yaml. Retorna nullptr si no es pot.
|
||||
static auto get(const std::string& name) -> const BulletConfig*;
|
||||
|
||||
private:
|
||||
static BulletConfig config;
|
||||
static std::unordered_map<std::string, BulletConfig> configs;
|
||||
static bool loaded;
|
||||
};
|
||||
|
||||
@@ -57,6 +57,13 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
||||
ai_state_ = EnemyAiState{};
|
||||
ai_state_.tracking_strength = cfg.ai.movement.tracking_strength;
|
||||
|
||||
// Timers paral·lels a tick: random [0, interval) per evitar que tots els
|
||||
// enemics del mateix tipus disparin sincronitzats al spawn.
|
||||
ai_tick_timers_.resize(cfg.ai.tick.size());
|
||||
for (std::size_t i = 0; i < cfg.ai.tick.size(); ++i) {
|
||||
ai_tick_timers_[i] = randFloat01() * cfg.ai.tick[i].interval;
|
||||
}
|
||||
|
||||
shape_ = Graphics::ShapeLoader::load(cfg.shape.path);
|
||||
if (!shape_ || !shape_->isValid()) {
|
||||
std::cerr << "[Enemy] Error: no se ha podido cargar " << cfg.shape.path << '\n';
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "core/entities/entity.hpp"
|
||||
#include "core/types.hpp"
|
||||
@@ -81,6 +82,7 @@ class Enemy : public Entities::Entity {
|
||||
|
||||
// Accessors per al sistema d'IA (Systems::EnemyAi).
|
||||
[[nodiscard]] auto getAiState() -> EnemyAiState& { return ai_state_; }
|
||||
[[nodiscard]] auto getAiTickTimers() -> std::vector<float>& { return ai_tick_timers_; }
|
||||
[[nodiscard]] auto getRotationBase() const -> float { return animation_.rotation_delta_base; }
|
||||
void setRotationDelta(float rot) { rotation_delta_ = rot; }
|
||||
// Public: el sistema d'IA reorienta la velocitat des d'un angle.
|
||||
@@ -136,6 +138,11 @@ class Enemy : public Entities::Entity {
|
||||
// Estat per-instància que la primitiva de moviment manté entre frames.
|
||||
EnemyAiState ai_state_;
|
||||
|
||||
// Timers paral·lels a config_->ai.tick: timers_[i] és el temps restant
|
||||
// (en segons) fins a la pròxima execució de l'acció i. Re-dimensionat a
|
||||
// init() segons la mida de config_->ai.tick.
|
||||
std::vector<float> ai_tick_timers_;
|
||||
|
||||
// Referències als 2 ships per a AI de tracking/proximity/chase/flee.
|
||||
std::array<const Ship*, 2> ships_{nullptr, nullptr};
|
||||
|
||||
|
||||
@@ -65,7 +65,8 @@ struct AiTickAction {
|
||||
float interval{1.0F};
|
||||
AimMode aim_mode{AimMode::RANDOM};
|
||||
float jitter_rad{0.0F};
|
||||
std::string bullet_config_name; // referit per nom (Fase C); buit a Fase A/B
|
||||
float bullet_speed{200.0F}; // px/s; la magnitud la decideix l'enemic, no el bullet config
|
||||
std::string bullet_config_name; // bullet config a usar (lazy-load des de BulletRegistry)
|
||||
};
|
||||
|
||||
struct EnemyAiConfig {
|
||||
|
||||
@@ -308,6 +308,9 @@ namespace {
|
||||
if (item.contains("jitter_rad")) {
|
||||
action.jitter_rad = item["jitter_rad"].get_value<float>();
|
||||
}
|
||||
if (item.contains("bullet_speed")) {
|
||||
action.bullet_speed = item["bullet_speed"].get_value<float>();
|
||||
}
|
||||
if (item.contains("bullet")) {
|
||||
action.bullet_config_name = item["bullet"].get_value<std::string>();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user