Animations are refactored to let me spawn in an 'attack animation' but I think the data model is wrong. Rather than spawning in an animation every time I can probably just make one, reposition it, then tell it to play. I'll have to try it.

This commit is contained in:
Zed A. Shaw 2025-09-12 11:56:11 -04:00
parent 8384b11993
commit ad3e580495
15 changed files with 109 additions and 56 deletions

View file

@ -70,18 +70,17 @@ namespace animation {
static AnimationManager MGR;
static bool initialized = false;
bool apply(Animation& anim, SpriteTexture& target) {
auto size = target.texture->getSize();
anim.frame_width = int(size.x) / (unsigned int)anim.frames;
sf::IntRect rect{{0,0}, {anim.frame_width, int(size.y)}};
bool apply(Animation& anim, sf::Sprite& sprite) {
sf::IntRect rect{{0,0}, {anim.frame_width, anim.frame_height}};
sf::Vector2f scale{1.0, 1.0};
sf::Vector2f pos{0, 0};
anim.step(scale, pos, rect);
target.sprite->setTextureRect(rect);
target.sprite->setPosition(pos);
target.sprite->setScale(scale);
sprite.setTextureRect(rect);
sprite.setPosition(pos);
sprite.setScale(scale);
return anim.playing;
}
@ -99,11 +98,27 @@ namespace animation {
void init() {
if(!initialized) {
Config config("assets/animations.json");
Config animations("assets/animations.json");
Config config("assets/config.json");
auto& sprites = config["sprites"];
for(auto& [name, data] : config.json().items()) {
auto anim = components::convert<Animation>(data);
MGR.animations.insert_or_assign(name, anim);
for(auto& [name, data] : animations.json().items()) {
try {
auto anim = components::convert<Animation>(data);
auto& sprite_config = sprites[name];
anim.frame_width = sprite_config["frame_width"];
anim.frame_height = sprite_config["frame_height"];
dbc::check(anim.frame_width > 0 && anim.frame_height > 0,
fmt::format("invalid frame width/height for animation: {}",
name));
MGR.animations.insert_or_assign(name, anim);
} catch(...) {
dbc::log(fmt::format("error in sprite config: {}", name));
throw;
}
}
initialized = true;

View file

@ -4,14 +4,14 @@
#include "easings.hpp"
#include "dinkyecs.hpp"
#include <SFML/Graphics/Rect.hpp>
#include <SFML/Graphics/Sprite.hpp>
namespace animation {
struct AnimationManager {
std::unordered_map<std::string, components::Animation> animations;
};
bool apply(components::Animation& anim, textures::SpriteTexture& target);
bool apply(components::Animation& anim, sf::Sprite& target);
void rotate(sf::Sprite& target, float degrees);
void center(sf::Sprite& target, sf::Vector2f pos);

View file

@ -1,5 +1,5 @@
{
"burning_effect": {
"burning_animation": {
"_type": "Animation",
"easing": 0,
"ease_rate": 0.5,
@ -9,7 +9,17 @@
"speed": 0.1,
"stationary": false
},
"ritual_blanket": {
"lightning_animation": {
"_type": "Animation",
"easing": 0,
"ease_rate": 0.5,
"scale": 1.0,
"simple": false,
"frames": 5,
"speed": 0.5,
"stationary": false
},
"ritual_crafting_area": {
"_type": "Animation",
"easing": 0,
"ease_rate": 0.5,

View file

@ -30,8 +30,13 @@
"hp_status_00": "assets/sounds/hp_status_00.ogg"
},
"sprites": {
"burning_effect":
{"path": "assets/sprites/burning_effect.png",
"burning_animation":
{"path": "assets/sprites/burning_animation.png",
"frame_width": 256,
"frame_height": 256
},
"lightning_animation":
{"path": "assets/sprites/lightning_animation.png",
"frame_width": 256,
"frame_height": 256
},

View file

@ -12,21 +12,6 @@
{"_type": "LightSource", "strength": 35, "radius": 2.0}
]
},
"FLAME_BLOB": {
"components": [
{"_type": "Tile", "display": 10899,
"foreground": "enemies/fg:gold_savior",
"background": "color:transparent"
},
{"_type": "Combat", "hp": 20, "max_hp": 20, "damage": 1, "dead": false},
{"_type": "Collision", "has": true},
{"_type": "Motion", "dx": 0, "dy": 0, "random": false},
{"_type": "EnemyConfig", "ai_script": "Enemy::actions", "ai_start_name": "Enemy::initial_state", "ai_goal_name": "Enemy::final_state"},
{"_type": "Personality", "hearing_distance": 5, "tough": true},
{"_type": "Sprite", "name": "burning_effect", "width": 256, "height": 256, "width": 256, "height": 256, "scale": 1.0},
{"_type": "Sound", "attack": "fireball_01", "death": "fireball_01"}
]
},
"GOLD_SAVIOR": {
"components": [
{"_type": "Tile", "display": 42586,

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -26,6 +26,10 @@ namespace components {
std::shared_ptr<sf::Shader> effect;
};
struct Temporary {
bool is = true;
};
struct Collision {
bool has = true;
};
@ -124,7 +128,8 @@ namespace components {
float subframe = 0.0f;
bool looped = false;
// BUG: this is weirdly not used in most animations but also named wrong should be frame_width
int frame_width = TEXTURE_WIDTH;
int frame_width = -1;
int frame_height = -1;
void play();
float twitching();

View file

@ -412,6 +412,7 @@ namespace gui {
draw_gui();
}
System::clear_attack();
$window.display();
}

View file

@ -45,7 +45,7 @@ namespace gui {
$ritual_ui = textures::get_sprite("ritual_crafting_area");
$ritual_ui.sprite->setPosition($gui.get_position());
$ritual_ui.sprite->setTextureRect($ritual_closed_rect);
$ritual_anim = animation::load("ritual_blanket");
$ritual_anim = animation::load("ritual_crafting_area");
auto open_close_toggle = $gui.entity("ritual_ui");
$gui.set<Clickable>(open_close_toggle, {
@ -84,7 +84,6 @@ namespace gui {
}
}
void UI::CLOSED(Event ev) {
if(ev == Event::TOGGLE) {
$ritual_anim.play();
@ -95,7 +94,7 @@ namespace gui {
void UI::OPENING(Event ev) {
if(ev == Event::TICK) {
if(!animation::apply($ritual_anim, $ritual_ui)) {
if(!animation::apply($ritual_anim, *$ritual_ui.sprite)) {
state(State::OPENED);
}
}

View file

@ -108,7 +108,6 @@ void Raycaster::sprite_casting(sf::RenderTarget &target) {
auto world = $level.world;
$level.collision->distance_sorted($sprite_order, {(size_t)$pos_x, (size_t)$pos_y}, RENDER_DISTANCE);
// after sorting the sprites, do the projection
for(auto& rec : $sprite_order) {
if(!$sprites.contains(rec.entity)) continue;
@ -184,6 +183,10 @@ void Raycaster::sprite_casting(sf::RenderTarget &target) {
int d = y * texture_height - $height * half_height + sprite_height * half_height;
int tex_y = ((d * texture_height) / sprite_height) / texture_height;
// BUG: this data could be put into the world
// as frame data, then just have a system that
// constantly applies this to any sprite that
// has an animation and is visible
sf::Vector2f origin{texture_width / 2.0f, texture_height / 2.0f};
sf::Vector2f scale{sprite_scale_w, sprite_scale_h};
sf::Vector2f position{x + origin.x * scale.x, y + origin.y * scale.y};

View file

@ -25,9 +25,9 @@ function Build-Images {
}
}
Build-Images -Source "Textures" -pixel_count 12
Build-Images -Source "Sprites" -pixel_count 6
Build-Images -Source "Items" -pixel_count 2
# Build-Images -Source "Textures" -pixel_count 12
# Build-Images -Source "Sprites" -pixel_count 6
# Build-Images -Source "Items" -pixel_count 2
Build-Images -Source "Animations" -pixel_count 6
cp -recurse -force C:\Users\lcthw\Pictures\Games\Renders\Raycaster\UI assets\ui

View file

@ -27,8 +27,6 @@ using namespace DinkyECS;
using lighting::LightSource;
void System::set_position(World& world, SpatialMap& collision, Entity entity, Position pos) {
dbc::check(world.has<Tile>(entity), "entity doesn't have tile");
world.set<Position>(entity, pos);
bool has_collision = world.has<Collision>(entity);
collision.insert(pos.location, entity, has_collision);
@ -238,11 +236,6 @@ void System::combat(int attack_id) {
auto& level = GameDB::current_level();
auto& collider = *level.collision;
auto& world = *level.world;
auto& the_belt = world.get_the<ritual::Belt>();
if(!the_belt.has(attack_id)) return;
auto& ritual = the_belt.get(attack_id);
const auto& player_pos = GameDB::player_position();
auto& player_combat = world.get<Combat>(level.player);
@ -272,13 +265,7 @@ void System::combat(int attack_id) {
};
if(result.player_did > 0) {
using enum ritual::Element;
if(ritual.element == FIRE || ritual.element == LIGHTNING) {
auto effect = shaders::get(
ritual.element == FIRE ? "flame" : "lightning");
world.set<SpriteEffect>(enemy.entity, {100, effect});
}
spawn_attack(world, attack_id);
}
if(enemy_action == combat::BattleAction::ATTACK) {
@ -656,3 +643,43 @@ gui::Event System::shortest_rotate(Point player_at, Point aiming_at, Point targe
return normalized < 180.0 ? gui::Event::ROTATE_LEFT : gui::Event::ROTATE_RIGHT;
}
void System::clear_attack() {
auto world = GameDB::current_world();
std::vector<Entity> dead_anim;
world->query<Animation, Temporary>([&](auto ent, auto& anim, auto&) {
if(!anim.playing) dead_anim.push_back(ent);
});
for(auto ent : dead_anim) {
world->remove<Sprite>(ent);
world->remove<Animation>(ent);
world->remove<SpriteEffect>(ent);
remove_from_world(ent);
}
}
void System::spawn_attack(World& world, int attack_id) {
using enum ritual::Element; // for FIRE vs LIGHTNING
auto& the_belt = world.get_the<ritual::Belt>();
dbc::check(the_belt.has(attack_id), "STOP passing invalid attack IDs to the system.");
auto& ritual = the_belt.get(attack_id);
auto effect = ritual.element == FIRE ? "burning_animation" : "lightning_animation";
auto effect_id = world.entity();
world.set<Sprite>(effect_id, {effect, 256, 256, 1.0});
world.set<Temporary>(effect_id, {true});
auto shader = shaders::get(ritual.element == FIRE ? "flame" : "lightning");
world.set<SpriteEffect>(effect_id, {100, shader});
auto anim = animation::load(effect);
anim.play();
world.set<Animation>(effect_id, anim);
drop_item(effect_id);
}

View file

@ -71,4 +71,7 @@ namespace System {
paths.compute_paths(walls);
}
void clear_attack();
void spawn_attack(World& world, int attack_id);
}

View file

@ -43,11 +43,11 @@ TEST_CASE("animation utility API", "[animation]") {
animation::init();
auto blanket = textures::get_sprite("ritual_crafting_area");
auto anim = animation::load("ritual_blanket");
auto anim = animation::load("ritual_crafting_area");
anim.play();
while(animation::apply(anim, blanket)) {
while(animation::apply(anim, *blanket.sprite)) {
fmt::println("animation: {}", anim.subframe);
}
}