// SPDX-License-Identifier: MPL-2.0 #include "sequa/core/render/scene_projection.hpp" #include "sequa/core/sim/simulation.hpp" #include #include #include #include #include using namespace sequa::core::sim; using sequa::core::render::SceneProjector; using namespace sequa::three_d; namespace { constexpr ObjectId ground_id = 1; constexpr ObjectId player_id = 2; constexpr ObjectId coin_id_base = 1000; // Monde de test deterministe : aucun corpus aleatoire a traiter, tout est pose a la main. WorldState world_with_three_coins() { WorldState world; world.next_coin_id = 3; world.coins.push_back(Coin{0, Fixed::from_int(2), Fixed::from_ratio(1, 2)}); world.coins.push_back(Coin{1, Fixed::from_int(-4), Fixed::from_int(1)}); world.coins.push_back(Coin{2, Fixed::from_int(10), Fixed::from_int(2)}); return world; } // Session scriptee : courses, pauses, demi-tours et sauts periodiques suffisent a faire // bouger joueur, pieces et camera. Copie locale des sessions des tests sim (meme idee). InputFrame scripted_input(Tick tick) { InputFrame input = neutral_input(tick); const Tick phase = tick % 180; if (phase < 90) { input.move_x = 32767; } else if (phase < 120) { input.move_x = 0; } else { input.move_x = -12000; } if (tick % 60 < 4) { input.buttons = button::jump; } return input; } template [[nodiscard]] std::size_t count_commands(const SceneDelta& delta) { std::size_t count = 0; for (const SceneCommand& command : delta.commands) { if (std::holds_alternative(command)) { ++count; } } return count; } template [[nodiscard]] const Command* find_command(const SceneDelta& delta, ObjectId id) { for (const SceneCommand& command : delta.commands) { if (const Command* c = std::get_if(&command); c != nullptr && c->id == id) { return c; } } return nullptr; } void check_no_set_visual(const SceneDelta& delta) { CHECK(count_commands(delta) == 0); } } // namespace TEST_CASE("SceneProjector : le premier appel renvoie la scene complete") { SceneProjector projector(ArenaParams{}); const SceneDelta delta = projector.update(world_with_three_coins(), 5); check_no_set_visual(delta); CHECK(delta.world_version == 5); REQUIRE(delta.commands.size() == 6); // sol, joueur, 3 pieces, camera // Les creations arrivent d'abord, par identifiant croissant. std::vector created; for (const SceneCommand& command : delta.commands) { if (const CreateObject* c = std::get_if(&command)) { created.push_back(c->id); } } CHECK(created == std::vector{ground_id, player_id, 1000, 1001, 1002}); CHECK(std::holds_alternative(delta.commands.back())); const CreateObject* ground = find_command(delta, ground_id); REQUIRE(ground != nullptr); CHECK(ground->visual.primitive == Primitive::plane); CHECK(ground->visual.size == Vec3{40, 1, 4}); // 2 * half_width par defaut CHECK(ground->transform.position == Vec3{0, 0, 0}); CHECK(ground->visual.color.r < 0.5f); // vert sombre : dominante verte CHECK(ground->visual.color.g > ground->visual.color.r); CHECK(ground->visual.color.g > ground->visual.color.b); const CreateObject* player = find_command(delta, player_id); REQUIRE(player != nullptr); CHECK(player->visual.primitive == Primitive::capsule); CHECK(player->visual.size == Vec3{0.8f, 1.8f, 0.8f}); // Body.y est l'altitude des pieds : le centre de la capsule est une demi-hauteur plus // haut, donc a y = 0.9 au sol. CHECK(player->transform.position == Vec3{0, 0.9f, 0}); const CreateObject* coin = find_command(delta, coin_id_base); REQUIRE(coin != nullptr); CHECK(coin->visual.primitive == Primitive::sphere); CHECK(coin->visual.size == Vec3{0.5f, 0.5f, 0.5f}); // premiere piece posee a (2, 0.5) CHECK(coin->transform.position == Vec3{2, 0.5f, 0}); // SetCamera n'a pas d'identifiant : la camera ferme la scene complete. const SetCamera* camera_command = std::get_if(&delta.commands.back()); REQUIRE(camera_command != nullptr); CHECK(camera_command->camera.transform.position == Vec3{0, 3, 12}); CHECK(camera_command->camera.transform.rotation == Quat{0, 0, 0, 1}); CHECK(camera_command->camera.vertical_fov_deg == 50.0f); CHECK(camera_command->camera.near_plane == 0.1f); CHECK(camera_command->camera.far_plane == 200.0f); } TEST_CASE("SceneProjector : rien n'a bouge, delta vide") { SceneProjector projector(ArenaParams{}); const WorldState world = world_with_three_coins(); REQUIRE(projector.update(world, 1).commands.size() == 6); const SceneDelta delta = projector.update(world, 2); CHECK(delta.world_version == 2); CHECK(delta.commands.empty()); } TEST_CASE("SceneProjector : seul le joueur bouge, exactement ce qui a change") { SceneProjector projector(ArenaParams{}); WorldState world = world_with_three_coins(); REQUIRE(projector.update(world, 1).commands.size() == 6); SUBCASE("deplacement horizontal : transform et camera") { world.player.x = Fixed::from_int(1); const SceneDelta delta = projector.update(world, 2); check_no_set_visual(delta); CHECK(count_commands(delta) == 1); const SetTransform* transform = find_command(delta, player_id); REQUIRE(transform != nullptr); CHECK(transform->transform.position == Vec3{1, 0.9f, 0}); CHECK(count_commands(delta) == 1); CHECK(count_commands(delta) == 0); CHECK(count_commands(delta) == 0); } SUBCASE("montee verticale seule : transform sans camera") { world.player.x = Fixed::from_int(1); REQUIRE(projector.update(world, 2).commands.size() == 2); world.player.y = Fixed::from_ratio(1, 2); const SceneDelta delta = projector.update(world, 3); check_no_set_visual(delta); CHECK(count_commands(delta) == 1); const SetTransform* transform = find_command(delta, player_id); REQUIRE(transform != nullptr); CHECK(transform->transform.position == Vec3{1, 1.4f, 0}); // pieds a y = 0.5 CHECK(count_commands(delta) == 0); } } TEST_CASE("SceneProjector : une piece ramassee devient un DestroyObject") { SceneProjector projector(ArenaParams{}); WorldState world = world_with_three_coins(); REQUIRE(projector.update(world, 1).commands.size() == 6); // La piece d'identifiant 1 a ete ramassee : elle quitte le monde. world.coins.erase(world.coins.begin() + 1); const SceneDelta delta = projector.update(world, 2); check_no_set_visual(delta); REQUIRE(delta.commands.size() == 1); const DestroyObject destroyed = std::get(delta.commands.front()); CHECK(destroyed.id == 1001); } TEST_CASE("SceneProjector : une piece apparue devient un CreateObject") { SceneProjector projector(ArenaParams{}); WorldState world = world_with_three_coins(); REQUIRE(projector.update(world, 1).commands.size() == 6); // La piece ramassee reapparait ailleurs sous un nouvel identifiant. world.coins.push_back(Coin{5, Fixed::from_int(6), Fixed::from_int(1)}); const SceneDelta delta = projector.update(world, 2); check_no_set_visual(delta); REQUIRE(delta.commands.size() == 1); const CreateObject created = std::get(delta.commands.front()); CHECK(created.id == 1005); CHECK(created.visual.primitive == Primitive::sphere); CHECK(created.transform.position == Vec3{6, 1, 0}); } TEST_CASE("SceneProjector : reset() renvoie a la scene complete") { SceneProjector projector(ArenaParams{}); WorldState world = world_with_three_coins(); REQUIRE(projector.update(world, 1).commands.size() == 6); world.player.x = Fixed::from_int(1); REQUIRE(projector.update(world, 2).commands.size() == 2); projector.reset(); const SceneDelta delta = projector.update(world, 3); check_no_set_visual(delta); // Toute la scene est repoussee, transform du joueur compris. CHECK(delta.commands.size() == 6); CHECK(count_commands(delta) == 0); CHECK(count_commands(delta) == 1); } TEST_CASE("SceneProjector : une vraie session passee pas a pas est acceptee par SceneState") { SimConfig config; config.seed = 1234; Simulation simulation(config); SceneProjector projector(simulation.config().arena); SceneState scene; std::size_t non_empty_deltas = 0; for (Tick tick = 0; tick < 300; ++tick) { const SceneDelta delta = projector.update(simulation.world(), tick + 1); REQUIRE_FALSE(scene.apply(delta).has_value()); if (!delta.commands.empty()) { ++non_empty_deltas; } for (const SceneCommand& command : delta.commands) { CHECK_FALSE(std::holds_alternative(command)); } simulation.step(scripted_input(tick)); } CHECK(scene.world_version() == 300); CHECK(scene.objects().size() == 2 + simulation.config().arena.coin_count); // sol, joueur, pieces CHECK(scene.camera().has_value()); CHECK(non_empty_deltas > 0); // la session a bien fait bouger le joueur } TEST_CASE("SceneProjector : meme suite d'entrees, memes deltas") { SimConfig config; config.seed = 1234; Simulation a(config); Simulation b(config); SceneProjector projector_a(a.config().arena); SceneProjector projector_b(b.config().arena); for (Tick tick = 0; tick < 300; ++tick) { const SceneDelta delta_a = projector_a.update(a.world(), tick + 1); const SceneDelta delta_b = projector_b.update(b.world(), tick + 1); CHECK(delta_a.world_version == delta_b.world_version); CHECK(delta_a.commands == delta_b.commands); const InputFrame input = scripted_input(tick); a.step(input); b.step(input); } }