Compare commits

..

2 commits

Author SHA1 Message Date
Sara 6c0fc17196 feat: added rifleman model 2025-08-27 14:07:08 +02:00
Sara 9b07c70b11 feat: implemented enemies spawning based on current region difficulty 2025-08-27 14:06:59 +02:00
15 changed files with 400 additions and 131 deletions

View file

@ -22,7 +22,7 @@ void EnemyBody::ready() {
void EnemyBody::physics_process(double delta) {
GETSET(velocity, {
velocity = Vector3{ this->movement_direction.x * this->movement_speed, velocity.y, this->movement_direction.y * this->movement_speed };
velocity = Vector3{ this->movement_direction.x * this->movement_speed, velocity.y, this->movement_direction.y * this->movement_speed } + (velocity * delta);
});
if (!this->movement_direction.is_zero_approx()) {
look_at(get_global_position() + Vector3{ this->movement_direction.x, 0.f, this->movement_direction.y });

View file

@ -0,0 +1,111 @@
#include "enemy_spawner.h"
#include "core/io/resource.h"
#include "core/object/object.h"
#include "macros.h"
#include "map_region.h"
#include "npc_unit.h"
#include "patrol_path.h"
#include "scene/resources/packed_scene.h"
void SpawnData::_bind_methods() {
BIND_HPROPERTY(Variant::DICTIONARY, difficulty_spawns, PROPERTY_HINT_DICTIONARY_TYPE, vformat("int;%s/%s:PackedScene", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE));
}
void SpawnData::set_difficulty_spawns(Dictionary dict) {
this->difficulty_spawns.clear();
for (KeyValue<Variant, Variant> const &kvp : dict) {
Ref<PackedScene> scene{ kvp.value };
if (scene.is_null() || !scene.is_valid()) {
continue;
} else if (!kvp.key.is_num()) {
continue;
} else {
this->difficulty_spawns.insert(kvp.key, scene);
}
}
}
Dictionary SpawnData::get_difficulty_spawns() const {
Dictionary r{};
for (KeyValue<int, Ref<PackedScene>> const &kvp : this->difficulty_spawns) {
r.set(kvp.key, kvp.value);
}
return r;
}
void EnemySpawner::_bind_methods() {
BIND_HPROPERTY(Variant::OBJECT, spawn_data, PROPERTY_HINT_RESOURCE_TYPE, "SpawnData");
BIND_HPROPERTY(Variant::OBJECT, patrol_path, PROPERTY_HINT_NODE_TYPE, "PatrolPath");
}
void EnemySpawner::on_phase_change(bool hunt) {
Ref<PackedScene> scene{ Ref<PackedScene>() };
int const idx{ this->region->get_current_difficulty() };
if (this->spawn_data->difficulty_spawns.has(idx)) {
scene = this->spawn_data->difficulty_spawns[idx];
}
if (!scene.is_valid()) {
print_error("EnemySpawner::on_phase_change: Spawn data scene is invalid");
return;
}
Node *instantiated{ scene->instantiate() };
if (instantiated == nullptr) {
print_error("EnemySpawner::on_phase_change: Spawn data instantiated a nullptr node");
return;
}
NpcUnit *unit_3d{ cast_to<NpcUnit>(instantiated) };
if (unit_3d == nullptr) {
instantiated->queue_free();
print_error("EnemySpawner::on_phase_change: Spawn data instantiated an invalid unit that cannot be cast to NpcUnit");
return;
}
unit_3d->set_patrol_path(this->patrol_path);
add_child(unit_3d);
unit_3d->set_global_transform(this->get_global_transform());
}
void EnemySpawner::ready() {
if (MapRegion * region{ cast_to<MapRegion>(this->get_parent()) }) {
this->region = region;
region->connect(MapRegion::sig_phase_changed, callable_mp(this, &self_type::on_phase_change));
} else {
print_error("EnemySpawner::ready: Failed to find region in parent");
}
if (!this->spawn_data.is_valid()) {
this->spawn_data = ResourceLoader::load("res://data/spawn_data/default.tres");
}
if (this->spawn_data.is_valid()) {
on_phase_change(false);
} else {
print_error("EnemySpawner::ready: No spawn data found, default spawn data is invalid, consider setting up a default spawn data");
}
}
void EnemySpawner::_notification(int what) {
if (Engine::get_singleton()->is_editor_hint()) {
return;
}
switch (what) {
default:
return;
case NOTIFICATION_READY:
ready();
return;
}
}
void EnemySpawner::set_spawn_data(Ref<SpawnData> data) {
this->spawn_data = data;
}
Ref<SpawnData> EnemySpawner::get_spawn_data() const {
return this->spawn_data;
}
void EnemySpawner::set_patrol_path(PatrolPath *path) {
this->patrol_path = path;
}
PatrolPath *EnemySpawner::get_patrol_path() const {
return this->patrol_path;
}

View file

@ -0,0 +1,43 @@
#ifndef ENEMY_SPAWNER_H
#define ENEMY_SPAWNER_H
#include "core/io/resource.h"
#include "core/templates/hash_map.h"
#include "scene/3d/node_3d.h"
class MapRegion;
class PatrolPath;
class SpawnData : public Resource {
GDCLASS(SpawnData, Resource);
static void _bind_methods();
public:
void set_difficulty_spawns(Dictionary dict);
Dictionary get_difficulty_spawns() const;
public:
HashMap<int, Ref<PackedScene>> difficulty_spawns{};
};
class EnemySpawner : public Node3D {
GDCLASS(EnemySpawner, Node3D);
static void _bind_methods();
void on_phase_change(bool hunt);
void ready();
protected:
void _notification(int what);
public:
void set_spawn_data(Ref<SpawnData> data);
Ref<SpawnData> get_spawn_data() const;
void set_patrol_path(PatrolPath *path);
PatrolPath *get_patrol_path() const;
private:
Ref<SpawnData> spawn_data{};
PatrolPath *patrol_path{ nullptr };
MapRegion *region{ nullptr };
};
#endif // !ENEMY_SPAWNER_H

View file

@ -1,10 +1,12 @@
#include "map_region.h"
#include "enemy_body.h"
String const MapRegion::sig_phase_changed{ "phase_changed" };
String const MapRegion::sig_difficulty_increased{ "difficulty_increased" };
String const MapRegion::sig_phase_changed{ "hunt_phase" };
void MapRegion::_bind_methods() {
ADD_SIGNAL(MethodInfo(sig_phase_changed, PropertyInfo(Variant::BOOL, "hunt_phase")));
ADD_SIGNAL(MethodInfo(sig_difficulty_increased));
ADD_SIGNAL(MethodInfo(sig_phase_changed, PropertyInfo(Variant::BOOL, "hunt")));
}
void MapRegion::on_node_entered(Node *node) {
@ -43,3 +45,22 @@ void MapRegion::remove_unit(NpcUnit *unit) {
this->units.erase(unit);
}
}
void MapRegion::raise_difficulty(double amount) {
if (this->hunt_phase) {
return;
}
double const new_difficulty{ this->difficulty + amount };
int const new_trunc{ (int)Math::floor(new_difficulty) };
int const old_trunc{ (int)Math::floor(this->difficulty) };
if (new_trunc != old_trunc) {
emit_signal(sig_difficulty_increased);
emit_signal(sig_phase_changed, true);
this->hunt_phase = true;
}
}
int MapRegion::get_current_difficulty() const {
int difficulty{ (int)Math::floor(this->difficulty) };
return difficulty;
}

View file

@ -17,13 +17,16 @@ protected:
public:
void register_unit(NpcUnit *unit);
void remove_unit(NpcUnit *unit);
void raise_difficulty(double amount);
int get_current_difficulty() const;
private:
float awareness{ 0.f };
double difficulty{ 0.f };
bool hunt_phase{ false };
HashSet<NpcUnit *> units{ nullptr };
public:
static String const sig_difficulty_increased;
static String const sig_phase_changed;
};

View file

@ -4,6 +4,7 @@
#include "wave_survival/damage_box.h"
#include "wave_survival/enemies/enemy_wretched.h"
#include "wave_survival/enemy_body.h"
#include "wave_survival/enemy_spawner.h"
#include "wave_survival/heads_up_display.h"
#include "wave_survival/health_status.h"
#include "wave_survival/hitbox.h"
@ -59,6 +60,8 @@ void initialize_wave_survival_module(ModuleInitializationLevel p_level) {
GDREGISTER_CLASS(MuzzleEffect);
GDREGISTER_RUNTIME_CLASS(HeadsUpDisplay);
GDREGISTER_RUNTIME_CLASS(MapRegion);
GDREGISTER_RUNTIME_CLASS(SpawnData);
GDREGISTER_RUNTIME_CLASS(EnemySpawner);
memnew(SoundEventPatchboard);
Engine::get_singleton()->add_singleton(Engine::Singleton(SoundEventPatchboard::get_class_static(), SoundEventPatchboard::get_singleton()));

Binary file not shown.

View file

@ -0,0 +1,59 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://bcy5dxdvkkq4y"
path="res://.godot/imported/rifleman.blend-337a40d56611f3a26c5a0fd4a9f9ba8c.scn"
[deps]
source_file="res://assets/models/enemies/rifleman.blend"
dest_files=["res://.godot/imported/rifleman.blend-337a40d56611f3a26c5a0fd4a9f9ba8c.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
blender/nodes/visible=0
blender/nodes/active_collection_only=false
blender/nodes/punctual_lights=true
blender/nodes/cameras=true
blender/nodes/custom_properties=true
blender/nodes/modifiers=1
blender/meshes/colors=false
blender/meshes/uvs=true
blender/meshes/normals=true
blender/meshes/export_geometry_nodes_instances=false
blender/meshes/tangents=true
blender/meshes/skins=2
blender/meshes/export_bones_deforming_mesh_only=false
blender/materials/unpack_enabled=true
blender/materials/export_materials=1
blender/animation/limit_playback=true
blender/animation/always_sample=true
blender/animation/group_tracks=true
gltf/naming_version=2

Binary file not shown.

View file

@ -0,0 +1,8 @@
[gd_resource type="SpawnData" load_steps=2 format=3 uid="uid://jya2iftfk0f6"]
[ext_resource type="PackedScene" uid="uid://5hg5eirw7v8n" path="res://objects/units/unit_4_wretched.tscn" id="1_0y836"]
[resource]
difficulty_spawns = {
0: ExtResource("1_0y836")
}

File diff suppressed because one or more lines are too long