commit c95fd413
Add monster attacks
Changed files
diff --git a/src/SagoImGui.cpp b/src/SagoImGui.cpp
index f76bf22..df3d404 100644
--- a/src/SagoImGui.cpp
+++ b/src/SagoImGui.cpp
@@ -29,7 +29,8 @@ void InitImGui(SDL_Window* window, SDL_Renderer* renderer, int width, int height
// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
- ImGuiIO& io = ImGui::GetIO(); (void)io;
+ ImGuiIO& io = ImGui::GetIO();
+ (void)io;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
io.DisplaySize.x = static_cast<float>(width);
diff --git a/src/editor/SagoTextureSelector.cpp b/src/editor/SagoTextureSelector.cpp
index 5cfd8ce..0f0e059 100644
--- a/src/editor/SagoTextureSelector.cpp
+++ b/src/editor/SagoTextureSelector.cpp
@@ -31,8 +31,7 @@ https://github.com/sago007/saland
#include "../global.hpp"
-class ChangeSDLColor
-{
+class ChangeSDLColor {
public:
ChangeSDLColor(SDL_Renderer* renderer, Uint8 r, Uint8 g, Uint8 b, Uint8 a) : renderer(renderer) {
SDL_GetRenderDrawColor(renderer, &oldr, &oldg, &oldb, &olda);
@@ -226,7 +225,7 @@ bool SagoTextureSelector::IsActive() {
return isActive;
}
-void SagoTextureSelector::ProcessInput(const SDL_Event& event, bool &processed) {
+void SagoTextureSelector::ProcessInput(const SDL_Event& event, bool& processed) {
ImGui_ImplSDL2_ProcessEvent(&event);
}
diff --git a/src/editor/SagoTextureSelector.hpp b/src/editor/SagoTextureSelector.hpp
index dbc3a82..1987b1a 100644
--- a/src/editor/SagoTextureSelector.hpp
+++ b/src/editor/SagoTextureSelector.hpp
@@ -36,9 +36,9 @@ public:
SagoTextureSelector();
SagoTextureSelector(const SagoTextureSelector& orig) = delete;
virtual ~SagoTextureSelector();
-
+
bool IsActive() override;
- void ProcessInput(const SDL_Event& event, bool &processed) override;
+ void ProcessInput(const SDL_Event& event, bool& processed) override;
void Draw(SDL_Renderer* target) override;
void Update() override;
void Init();
diff --git a/src/editor/editor_loader.cpp b/src/editor/editor_loader.cpp
index 643c38f..27f3566 100644
--- a/src/editor/editor_loader.cpp
+++ b/src/editor/editor_loader.cpp
@@ -43,10 +43,10 @@ void from_json(const nlohmann::json& j, SagoSprite& p) {
if (j.contains("frame_time")) {
j.at("frame_time").get_to(p.frame_time);
}
- if(j.contains("originx")) {
+ if (j.contains("originx")) {
j.at("originx").get_to(p.originx);
}
- if(j.contains("originy")) {
+ if (j.contains("originy")) {
j.at("originy").get_to(p.originy);
}
}
diff --git a/src/saland/Game.cpp b/src/saland/Game.cpp
index 7863a88..ab4f482 100644
--- a/src/saland/Game.cpp
+++ b/src/saland/Game.cpp
@@ -443,7 +443,7 @@ void Game::Draw(SDL_Renderer* target) {
// Draw black bars where globalData.logicalResize suggests that the physical area ends.
// This covers the letterboxing/pillarboxing margins to ensure only the game area is visible.
- #if 1
+#if 1
int topMargin = globalData.logicalResize.GetTopMargin();
int leftMargin = globalData.logicalResize.GetLeftMargin();
@@ -468,7 +468,7 @@ void Game::Draw(SDL_Renderer* target) {
SDL_SetRenderDrawColor(target, 0, 0, 0, 255);
SDL_RenderFillRect(target, &rightBar);
}
- #endif
+#endif
if (data->consoleActive && data->console) {
data->console->Draw(target);
@@ -641,7 +641,12 @@ void Game::Update() {
}
Monster* monster = dynamic_cast<Monster*> (entity.get());
if (monster) {
- UpdateMonster(monster, deltaTime, data->human->X, data->human->Y);
+ UpdateMonster(monster, deltaTime, data->human.get());
+ // Check if monster just attacked
+ if (monster->attack.animationTime == monster->attack.animationDuration) {
+ // Attack just started, apply damage to player
+ MonsterAttackPlayer(monster, data->human.get());
+ }
}
MiscItem* item = dynamic_cast<MiscItem*> (entity.get());
if (item) {
diff --git a/src/saland/GameDraw.cpp b/src/saland/GameDraw.cpp
index 4b982ed..1acde25 100644
--- a/src/saland/GameDraw.cpp
+++ b/src/saland/GameDraw.cpp
@@ -267,6 +267,47 @@ void DrawMonster(SDL_Renderer* target, sago::SagoSpriteHolder* sHolder, const Mo
DrawCollision(target, entity, offsetX, offsetY, drawCollision, resize);
const sago::SagoSprite& mySprite = sHolder->GetSprite(entity->race + "_" + std::string(1, entity->direction));
mySprite.Draw(target, time, std::round(entity->X) - offsetX, std::round(entity->Y) - offsetY, resize);
+
+ // Draw attack animation if active
+ if (entity->attack.animationTime > 0.0f) {
+ // Draw a slash effect in front of the monster
+ int slashOffsetX = 0;
+ int slashOffsetY = 0;
+
+ // Position slash based on monster direction
+ switch (entity->direction) {
+ case 'N':
+ slashOffsetY = -20;
+ break;
+ case 'S':
+ slashOffsetY = 20;
+ break;
+ case 'E':
+ slashOffsetX = 20;
+ break;
+ case 'W':
+ slashOffsetX = -20;
+ break;
+ }
+
+ int slashX = std::round(entity->X) + slashOffsetX - offsetX;
+ int slashY = std::round(entity->Y) + slashOffsetY - offsetY;
+
+ // Draw the slash sprite (reuse male_slash animation)
+ {
+ // If slash sprite doesn't exist, draw a simple rectangle
+ SDL_Rect slashRect;
+ slashRect.x = slashX;
+ slashRect.y = slashY;
+ slashRect.w = 16;
+ slashRect.h = 16;
+ if (resize) {
+ resize->LogicalToPhysical(slashRect);
+ }
+ SDL_SetRenderDrawColor(target, 255, 100, 100, 200);
+ SDL_RenderFillRect(target, &slashRect);
+ }
+ }
}
@@ -367,10 +408,10 @@ void UpdateDamageNumbers(Placeable* entity) {
// Remove expired damage numbers
entity->damageNumbers.erase(
- std::remove_if(entity->damageNumbers.begin(), entity->damageNumbers.end(),
- [currentTime](const DamageNumber& dmg) {
- return (currentTime - dmg.createdAt) >= dmg.duration;
- }),
- entity->damageNumbers.end()
+ std::remove_if(entity->damageNumbers.begin(), entity->damageNumbers.end(),
+ [currentTime](const DamageNumber& dmg) {
+ return (currentTime - dmg.createdAt) >= dmg.duration;
+ }),
+ entity->damageNumbers.end()
);
}
\ No newline at end of file
diff --git a/src/saland/GameItems.cpp b/src/saland/GameItems.cpp
index 9da8f7b..656a7d3 100644
--- a/src/saland/GameItems.cpp
+++ b/src/saland/GameItems.cpp
@@ -53,7 +53,7 @@ static void ReadItemFile(const std::string& filename) {
}
for (const auto& item : items.GetArray()) {
if (item.IsObject()) {
- ItemDef new_item = itemDef;
+ ItemDef new_item = itemDef;
for (const auto& member : item.GetObject()) {
if (member.name == "itemid") {
new_item.itemid = member.value.GetString();
@@ -95,7 +95,7 @@ static void ReadItemFile(const std::string& filename) {
static void initItems() {
std::vector<std::string> file_list = sago::GetFileList(item_dir);
- for(const std::string& file : file_list) {
+ for (const std::string& file : file_list) {
std::string filename = std::format("{}{}", item_dir, file);
printf("Item file: %s\n",filename.c_str());
ReadItemFile(filename);
diff --git a/src/saland/GameSpellState.cpp b/src/saland/GameSpellState.cpp
index 18cc2d1..654a7c8 100644
--- a/src/saland/GameSpellState.cpp
+++ b/src/saland/GameSpellState.cpp
@@ -102,16 +102,16 @@ bool GameSpellState::IsSpellSelectActive() {
}
void GameSpellState::ProcessInput(const SDL_Event& event, bool& processed) {
- UpdateMouseCoordinates(event, globalData.mousex, globalData.mousey);
- if (event.type == SDL_KEYDOWN) {
- if (event.key.keysym.sym == SDLK_ESCAPE) {
- data->spellSelectActive = !data->spellSelectActive;
- processed = true;
- }
- }
+ UpdateMouseCoordinates(event, globalData.mousex, globalData.mousey);
+ if (event.type == SDL_KEYDOWN) {
+ if (event.key.keysym.sym == SDLK_ESCAPE) {
+ data->spellSelectActive = !data->spellSelectActive;
+ processed = true;
+ }
+ }
if (!data->spellSelectActive) {
return;
- }
+ }
if (event.type == SDL_MOUSEBUTTONDOWN) {
if (event.button.button == SDL_BUTTON_LEFT) {
// Convert physical mouse coordinates to logical coordinates
diff --git a/src/saland/GameUpdates.cpp b/src/saland/GameUpdates.cpp
index d8947f0..bfbb899 100644
--- a/src/saland/GameUpdates.cpp
+++ b/src/saland/GameUpdates.cpp
@@ -82,65 +82,102 @@ void UpdateHuman(Human* entity, float fDeltaTime) {
entity->Y = place.y*pixel2unit;
}
-static void MonsterThink(Monster* entity, float playerX, float playerY) {
+static void MonsterThink(Monster* entity, Human* player) {
+ if (!player) {
+ return;
+ }
+ float playerX = player->X;
+ float playerY = player->Y;
+
switch (entity->aiState) {
- case Monster::State::Roaming:
- // Random wandering behavior
+ case Monster::State::Roaming:
+ // Random wandering behavior
+ if (rand()%2==0) {
+ //1 in 2 chance of changing direction
if (rand()%2==0) {
- //1 in 2 chance of changing direction
- if (rand()%2==0) {
- entity->moveX = 0.1f;
- }
- else {
- entity->moveX = -0.1f;
- }
- if (rand()%2 == 0) {
- entity->moveY = 0.1f;
- }
- else {
- entity->moveY = -0.1f;
- }
+ entity->moveX = 0.1f;
}
- break;
- case Monster::State::Aggressive:
- // Move towards player
- {
- float dirX = playerX - entity->X;
- float dirY = playerY - entity->Y;
- float length = std::sqrt(dirX * dirX + dirY * dirY);
- if (length > 0.01f) {
- entity->moveX = (dirX / length) * entity->speed;
- entity->moveY = (dirY / length) * entity->speed;
- }
+ else {
+ entity->moveX = -0.1f;
}
- break;
- case Monster::State::Fleeing:
- // Move away from player
- {
- float dirX = entity->X - playerX;
- float dirY = entity->Y - playerY;
- float length = std::sqrt(dirX * dirX + dirY * dirY);
- if (length > 0.01f) {
- entity->moveX = (dirX / length) * entity->speed * 1.2f; // Flee slightly faster
- entity->moveY = (dirY / length) * entity->speed * 1.2f;
- }
+ if (rand()%2 == 0) {
+ entity->moveY = 0.1f;
}
- break;
+ else {
+ entity->moveY = -0.1f;
+ }
+ }
+ break;
+ case Monster::State::Aggressive:
+ // Move towards player
+ {
+ float dirX = playerX - entity->X;
+ float dirY = playerY - entity->Y;
+ float length = std::sqrt(dirX * dirX + dirY * dirY);
+ if (length > 0.01f) {
+ entity->moveX = (dirX / length) * entity->speed;
+ entity->moveY = (dirY / length) * entity->speed;
+ }
+ }
+ break;
+ case Monster::State::Fleeing:
+ // Move away from player
+ {
+ float dirX = entity->X - playerX;
+ float dirY = entity->Y - playerY;
+ float length = std::sqrt(dirX * dirX + dirY * dirY);
+ if (length > 0.01f) {
+ entity->moveX = (dirX / length) * entity->speed * 1.2f; // Flee slightly faster
+ entity->moveY = (dirY / length) * entity->speed * 1.2f;
+ }
+ }
+ break;
}
}
-void UpdateMonster(Monster* entity, float fDeltaTime, float playerX, float playerY) {
+void UpdateMonster(Monster* entity, float fDeltaTime, Human* player) {
SetCreatureMovementEntity(entity, entity->moveX, entity->moveY);
b2Vec2 place = entity->body->GetPosition();
entity->X = place.x*pixel2unit;
entity->Y = place.y*pixel2unit;
- if (entity->aiNextThink > 0.0) {
- entity->aiNextThink -= fDeltaTime;
+
+ // Update attack cooldown
+ if (entity->attack.cooldown > 0.0f) {
+ entity->attack.cooldown -= fDeltaTime;
}
- else {
- entity->aiNextThink = 2000.0f;
- MonsterThink(entity, playerX, playerY);
+
+ // Update attack animation timer
+ if (entity->attack.animationTime > 0.0f) {
+ entity->attack.animationTime -= fDeltaTime;
}
+
+ // Check if player is alive before attacking
+ if (player && player->diedAt == 0.0f) {
+ float playerX = player->X;
+ float playerY = player->Y;
+
+ // Check if in aggressive state and within attack range
+ if (entity->aiState == Monster::State::Aggressive && entity->attack.cooldown <= 0.0f) {
+ float distX = playerX - entity->X;
+ float distY = playerY - entity->Y;
+ float distance = std::sqrt(distX * distX + distY * distY);
+
+ if (distance <= entity->attack.range) {
+ // Attack the player
+ entity->attack.cooldown = entity->attack.cooldownDuration;
+ entity->attack.animationTime = entity->attack.animationDuration;
+ }
+ }
+
+ if (entity->aiNextThink > 0.0) {
+ entity->aiNextThink -= fDeltaTime;
+ }
+ else {
+ entity->aiNextThink = 2000.0f;
+ MonsterThink(entity, player);
+ }
+ }
+
if (entity->health <= 0.0) {
entity->removeMe = true;
}
@@ -175,4 +212,21 @@ void ProjectileHit(Projectile* p, Placeable* target) {
// Potentially change monster state to aggressive
monster->aiState = Monster::State::Aggressive;
}
+}
+
+void MonsterAttackPlayer(Monster* monster, Human* player) {
+ if (!player || !monster) {
+ return;
+ }
+
+ // Apply damage to player
+ player->health -= monster->attack.damage;
+
+ // Create damage number on player
+ DamageNumber dmg;
+ dmg.X = player->X;
+ dmg.Y = player->Y - player->Radius;
+ dmg.damage = monster->attack.damage;
+ dmg.createdAt = SDL_GetTicks();
+ player->damageNumbers.push_back(dmg);
}
\ No newline at end of file
diff --git a/src/saland/GameUpdates.hpp b/src/saland/GameUpdates.hpp
index 8995f36..5989256 100644
--- a/src/saland/GameUpdates.hpp
+++ b/src/saland/GameUpdates.hpp
@@ -31,8 +31,9 @@ https://github.com/sago007/saland
void ProjectileHit(Projectile* p, Placeable* target);
void UpdateHuman(Human* entity, float fDeltaTime);
-void UpdateMonster(Monster* entity, float fDeltaTime, float playerX, float playerY);
+void UpdateMonster(Monster* entity, float fDeltaTime, Human* player);
void UpdateProjectile(Projectile* entity, float fDeltaTime);
+void MonsterAttackPlayer(Monster* monster, Human* player);
#endif /* GAMEUPDATES_HPP */
diff --git a/src/saland/console/Console.cpp b/src/saland/console/Console.cpp
index 0688ecc..1ea53a6 100644
--- a/src/saland/console/Console.cpp
+++ b/src/saland/console/Console.cpp
@@ -139,7 +139,8 @@ void Console::Draw(SDL_Renderer* target) {
if (commands.find(currentInput) != commands.end()) {
// Show help message for the complete command
ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f), "Help: %s", commands[currentInput]->helpMessage().c_str());
- } else {
+ }
+ else {
// Show matching commands
std::vector<std::string> matches;
for (const auto& cmd : commands) {
@@ -150,7 +151,9 @@ void Console::Draw(SDL_Renderer* target) {
if (matches.size()) {
std::string matchText = "Matches: ";
for (size_t i = 0; i < matches.size(); ++i) {
- if (i > 0) matchText += ", ";
+ if (i > 0) {
+ matchText += ", ";
+ }
matchText += matches[i];
}
ImGui::TextColored(ImVec4(0.7f, 0.7f, 1.0f, 1.0f), "%s", matchText.c_str());
@@ -219,69 +222,70 @@ int Console::TextEditCallbackStub(ImGuiInputTextCallbackData* data) {
int Console::TextEditCallback(ImGuiInputTextCallbackData* data) {
switch (data->EventFlag) {
- case ImGuiInputTextFlags_CallbackHistory: {
- // Clear completion state when using history
- completionCandidates.clear();
- completionIndex = -1;
-
- const int prev_history_pos = historyPos;
- if (data->EventKey == ImGuiKey_UpArrow) {
- if (historyPos == -1) {
- historyPos = commandHistory.size() - 1;
- }
- else if (historyPos > 0) {
- historyPos--;
- }
+ case ImGuiInputTextFlags_CallbackHistory: {
+ // Clear completion state when using history
+ completionCandidates.clear();
+ completionIndex = -1;
+
+ const int prev_history_pos = historyPos;
+ if (data->EventKey == ImGuiKey_UpArrow) {
+ if (historyPos == -1) {
+ historyPos = commandHistory.size() - 1;
}
- else if (data->EventKey == ImGuiKey_DownArrow) {
- if (historyPos != -1) {
- historyPos++;
- if (historyPos >= (int)commandHistory.size()) {
- historyPos = -1;
- }
+ else if (historyPos > 0) {
+ historyPos--;
+ }
+ }
+ else if (data->EventKey == ImGuiKey_DownArrow) {
+ if (historyPos != -1) {
+ historyPos++;
+ if (historyPos >= (int)commandHistory.size()) {
+ historyPos = -1;
}
}
+ }
- // Apply history
- if (prev_history_pos != historyPos) {
- const char* history_str = (historyPos >= 0) ? commandHistory[historyPos].c_str() : "";
- data->DeleteChars(0, data->BufTextLen);
- data->InsertChars(0, history_str);
- data->BufDirty = true;
- data->CursorPos = data->BufTextLen;
- data->SelectionStart = data->SelectionEnd = data->CursorPos;
- }
- break;
+ // Apply history
+ if (prev_history_pos != historyPos) {
+ const char* history_str = (historyPos >= 0) ? commandHistory[historyPos].c_str() : "";
+ data->DeleteChars(0, data->BufTextLen);
+ data->InsertChars(0, history_str);
+ data->BufDirty = true;
+ data->CursorPos = data->BufTextLen;
+ data->SelectionStart = data->SelectionEnd = data->CursorPos;
}
- case ImGuiInputTextFlags_CallbackCompletion: {
- // Tab completion
- std::string currentInput(data->Buf, data->BufTextLen);
-
- // Build completion candidates on first tab press
- if (completionCandidates.empty()) {
- for (const auto& cmd : commands) {
- if (cmd.first.find(currentInput) == 0) {
- completionCandidates.push_back(cmd.first);
- }
+ break;
+ }
+ case ImGuiInputTextFlags_CallbackCompletion: {
+ // Tab completion
+ std::string currentInput(data->Buf, data->BufTextLen);
+
+ // Build completion candidates on first tab press
+ if (completionCandidates.empty()) {
+ for (const auto& cmd : commands) {
+ if (cmd.first.find(currentInput) == 0) {
+ completionCandidates.push_back(cmd.first);
}
- completionIndex = 0;
- } else {
- // Cycle through candidates
- completionIndex = (completionIndex + 1) % completionCandidates.size();
- }
-
- if (!completionCandidates.empty()) {
- data->DeleteChars(0, data->BufTextLen);
- data->InsertChars(0, completionCandidates[completionIndex].c_str());
- data->BufDirty = true; // Tell ImGui the buffer was modified
- // Move cursor to end of inserted text
- data->CursorPos = data->BufTextLen;
- data->SelectionStart = data->SelectionEnd = data->CursorPos;
}
- break;
+ completionIndex = 0;
}
+ else {
+ // Cycle through candidates
+ completionIndex = (completionIndex + 1) % completionCandidates.size();
+ }
+
+ if (!completionCandidates.empty()) {
+ data->DeleteChars(0, data->BufTextLen);
+ data->InsertChars(0, completionCandidates[completionIndex].c_str());
+ data->BufDirty = true; // Tell ImGui the buffer was modified
+ // Move cursor to end of inserted text
+ data->CursorPos = data->BufTextLen;
+ data->SelectionStart = data->SelectionEnd = data->CursorPos;
+ }
+ break;
}
-
+ }
+
return 0;
}
diff --git a/src/saland/model/placeables.hpp b/src/saland/model/placeables.hpp
index b665c1c..52a4239 100644
--- a/src/saland/model/placeables.hpp
+++ b/src/saland/model/placeables.hpp
@@ -104,6 +104,16 @@ public:
float castTime = 400; //Number of milliseconds it will take to complete the cast
};
+struct MonsterAttack {
+ float cooldown = 0.0f;
+ float cooldownDuration = 1500.0f; // milliseconds between attacks
+ float range = 40.0f; // pixels
+ float damage = 5.0f;
+ std::string animation = "slash"; // Animation to show when attacking
+ float animationTime = 0.0f; // Time remaining for attack animation
+ float animationDuration = 300.0f; // milliseconds
+};
+
class Monster : public Creature {
public:
enum class State {
@@ -118,6 +128,8 @@ public:
State aiState = State::Roaming;
float targetX = 0.0f; // Used for fleeing direction
float targetY = 0.0f;
+ // Attack logic:
+ MonsterAttack attack;
};
diff --git a/src/saland/model/spells.cpp b/src/saland/model/spells.cpp
index 1459939..256f452 100644
--- a/src/saland/model/spells.cpp
+++ b/src/saland/model/spells.cpp
@@ -103,7 +103,7 @@ void SpellHolder::init() {
clearTileSpell.type = SpellCursorType::tile;
const char* spellDir = "saland/spells/";
std::vector<std::string> file_list = sago::GetFileList(spellDir);
- for(const std::string& file : file_list) {
+ for (const std::string& file : file_list) {
std::string filename = std::format("{}{}", spellDir, file);
printf("Spells: file: %s\n",filename.c_str());
ReadSpellFile(filename);