diff --git a/meson.build b/meson.build index 8ba66af..af4a500 100644 --- a/meson.build +++ b/meson.build @@ -60,6 +60,7 @@ elif build_machine.system() == 'darwin' endif catch2 = subproject('catch2').get_variable('catch2_with_main_dep') +fuc2 = subproject('fuc2').get_variable('fuc2_dep') fmt = subproject('fmt').get_variable('fmt_dep') json = subproject('nlohmann_json').get_variable('nlohmann_json_dep') freetype2 = subproject('freetype2').get_variable('freetype_dep') @@ -114,6 +115,13 @@ executable('runtests', sources + tests, override_options: exe_defaults, dependencies: dependencies + [catch2]) +executable('fuc2it', sources + fuc2_tests, + cpp_args: cpp_args, + link_args: link_args, + include_directories: inc_dirs, + override_options: exe_defaults, + dependencies: dependencies + [fuc2]) + executable('zedcaster', [ 'src/main.cpp' ], cpp_args: cpp_args, diff --git a/src/graphics/raycaster.cpp b/src/graphics/raycaster.cpp index a2803f2..58ae542 100644 --- a/src/graphics/raycaster.cpp +++ b/src/graphics/raycaster.cpp @@ -38,15 +38,17 @@ union ColorConv { * and I guess the compiler can handle it better than shifting * bits around. */ -inline RGBA lighting_calc(RGBA pixel, float dist, int level) { +inline float intensity_calc(float dist, int level) { + return (float(level) * PERCENT) / (dist + 1) * LIGHT_MULTIPLIER; +} + +inline RGBA lighting_calc(RGBA pixel, float intensity) { ColorConv conv{.as_int=pixel}; if(conv.as_color.b < GLOW_LIMIT && conv.as_color.r < GLOW_LIMIT && conv.as_color.g < GLOW_LIMIT) { - float intensity = (float(level) * PERCENT) / (dist + 1) * LIGHT_MULTIPLIER; - conv.as_color.r *= intensity; conv.as_color.g *= intensity; conv.as_color.b *= intensity; @@ -55,6 +57,7 @@ inline RGBA lighting_calc(RGBA pixel, float dist, int level) { return conv.as_int; } + Raycaster::Raycaster(int x, int y, int width, int height) : $view_texture(sf::Vector2u{(unsigned int)width, (unsigned int)height}), $view_sprite($view_texture), @@ -336,12 +339,14 @@ void Raycaster::cast_rays() { // Starting texture coordinate double tex_pos = (draw_start - $pitch - $height / 2 + line_height / 2) * step; + int light_level = lights[map_y][map_x]; + float intensity = intensity_calc(perp_wall_dist, light_level); + for(int y = draw_start; y < draw_end; y++) { int tex_y = (int)tex_pos & (texture_height - 1); tex_pos += step; RGBA pixel = texture[texture_height * tex_y + tex_x]; - int light_level = lights[map_y][map_x]; - $pixels[pixcoord(x, y)] = lighting_calc(pixel, perp_wall_dist, light_level); + $pixels[pixcoord(x, y)] = lighting_calc(pixel, intensity); } // SET THE ZBUFFER FOR THE SPRITE CASTING @@ -422,14 +427,15 @@ void Raycaster::draw_ceiling_floor() { } // NOTE: use map_x/y to get the floor, ceiling texture. + float intensity = intensity_calc(row_distance, light_level); // FLOOR color = floor_texture[texture_width * ty + tx]; - $pixels[pixcoord(x, y)] = lighting_calc(color, row_distance, light_level); + $pixels[pixcoord(x, y)] = lighting_calc(color, intensity); // CEILING color = ceiling_texture[texture_width * ty + tx]; - $pixels[pixcoord(x, $height - y - 1)] = lighting_calc(color, row_distance, light_level); + $pixels[pixcoord(x, $height - y - 1)] = lighting_calc(color, intensity); } } } diff --git a/tests/ai.cpp b/tests/ai.cpp index d6b3385..cdc82eb 100644 --- a/tests/ai.cpp +++ b/tests/ai.cpp @@ -1,209 +1,223 @@ -#include #include "dbc.hpp" #include "ai/ai.hpp" #include "ai/ai_debug.hpp" #include #include "game/rituals.hpp" +#include using namespace dbc; using namespace nlohmann; +using namespace fuc2; -TEST_CASE("state and actions work", "[ai]") { - enum StateNames { - ENEMY_IN_RANGE, - ENEMY_DEAD - }; +namespace ai_tests { + void test_state_and_actions() { + enum StateNames { + ENEMY_IN_RANGE, + ENEMY_DEAD + }; - ai::State goal; - ai::State start; - std::vector actions; + ai::State goal; + ai::State start; + std::vector actions; - // start off enemy not dead and not in range - start[ENEMY_DEAD] = false; - start[ENEMY_IN_RANGE] = false; + // start off enemy not dead and not in range + start[ENEMY_DEAD] = false; + start[ENEMY_IN_RANGE] = false; - // end goal is enemy is dead - goal[ENEMY_DEAD] = true; + // end goal is enemy is dead + goal[ENEMY_DEAD] = true; - ai::Action move_closer("move_closer", 10); - move_closer.needs(ENEMY_IN_RANGE, false); - move_closer.effect(ENEMY_IN_RANGE, true); + ai::Action move_closer("move_closer", 10); + move_closer.needs(ENEMY_IN_RANGE, false); + move_closer.effect(ENEMY_IN_RANGE, true); - REQUIRE(move_closer.can_effect(start)); - auto after_move_state = move_closer.apply_effect(start); - REQUIRE(start[ENEMY_IN_RANGE] == false); - REQUIRE(after_move_state[ENEMY_IN_RANGE] == true); - REQUIRE(after_move_state[ENEMY_DEAD] == false); - // start is clean but after move is dirty - REQUIRE(move_closer.can_effect(start)); - REQUIRE(!move_closer.can_effect(after_move_state)); - REQUIRE(ai::distance_to_goal(start, after_move_state) == 1); + CHECK(move_closer.can_effect(start)); + auto after_move_state = move_closer.apply_effect(start); + CHECK(start[ENEMY_IN_RANGE] == false); + CHECK(after_move_state[ENEMY_IN_RANGE] == true); + CHECK(after_move_state[ENEMY_DEAD] == false); + // start is clean but after move is dirty + CHECK(move_closer.can_effect(start)); + CHECK(!move_closer.can_effect(after_move_state)); + CHECK(ai::distance_to_goal(start, after_move_state) == 1); - ai::Action kill_it("kill_it", 10); - kill_it.needs(ENEMY_IN_RANGE, true); - kill_it.needs(ENEMY_DEAD, false); - kill_it.effect(ENEMY_DEAD, true); + ai::Action kill_it("kill_it", 10); + kill_it.needs(ENEMY_IN_RANGE, true); + kill_it.needs(ENEMY_DEAD, false); + kill_it.effect(ENEMY_DEAD, true); - REQUIRE(!kill_it.can_effect(start)); - REQUIRE(kill_it.can_effect(after_move_state)); + CHECK(!kill_it.can_effect(start)); + CHECK(kill_it.can_effect(after_move_state)); - auto after_kill_state = kill_it.apply_effect(after_move_state); - REQUIRE(!kill_it.can_effect(after_kill_state)); - REQUIRE(ai::distance_to_goal(after_move_state, after_kill_state) == 1); + auto after_kill_state = kill_it.apply_effect(after_move_state); + CHECK(!kill_it.can_effect(after_kill_state)); + CHECK(ai::distance_to_goal(after_move_state, after_kill_state) == 1); - kill_it.ignore(ENEMY_IN_RANGE); - REQUIRE(kill_it.can_effect(after_move_state)); + kill_it.ignore(ENEMY_IN_RANGE); + CHECK(kill_it.can_effect(after_move_state)); - actions.push_back(kill_it); - actions.push_back(move_closer); + actions.push_back(kill_it); + actions.push_back(move_closer); - REQUIRE(start != goal); -} - -TEST_CASE("basic feature tests", "[ai]") { - enum StateNames { - ENEMY_IN_RANGE, - ENEMY_DEAD - }; - - ai::State goal; - ai::State start; - std::vector actions; - - // start off enemy not dead and not in range - start[ENEMY_DEAD] = false; - start[ENEMY_IN_RANGE] = false; - - // end goal is enemy is dead - goal[ENEMY_DEAD] = true; - - ai::Action move_closer("move_closer", 10); - move_closer.needs(ENEMY_IN_RANGE, false); - move_closer.effect(ENEMY_IN_RANGE, true); - - ai::Action kill_it("kill_it", 10); - kill_it.needs(ENEMY_IN_RANGE, true); - // this is duplicated on purpose to confirm that setting - // a positive then a negative properly cancels out - kill_it.needs(ENEMY_DEAD, true); - kill_it.needs(ENEMY_DEAD, false); - - // same thing with effects - kill_it.effect(ENEMY_DEAD, false); - kill_it.effect(ENEMY_DEAD, true); - - // order seems to matter which is wrong - actions.push_back(kill_it); - actions.push_back(move_closer); - - auto result = ai::plan_actions(actions, start, goal); - REQUIRE(result.complete); - - auto state = start; - - for(auto& action : result.script) { - state = action.apply_effect(state); + CHECK(start != goal); } - REQUIRE(state[ENEMY_DEAD]); -} + void test_basic_features() { + enum StateNames { + ENEMY_IN_RANGE, + ENEMY_DEAD + }; + ai::State goal; + ai::State start; + std::vector actions; -TEST_CASE("ai as a module like sound/sprites", "[ai]") { - ai::reset(); - ai::init("tests/ai_fixture.json"); + // start off enemy not dead and not in range + start[ENEMY_DEAD] = false; + start[ENEMY_IN_RANGE] = false; - auto start = ai::load_state("test_start"); - auto goal = ai::load_state("test_goal"); + // end goal is enemy is dead + goal[ENEMY_DEAD] = true; - auto a_plan = ai::plan("test1", start, goal); - REQUIRE(a_plan.complete); + ai::Action move_closer("move_closer", 10); + move_closer.needs(ENEMY_IN_RANGE, false); + move_closer.effect(ENEMY_IN_RANGE, true); - auto state = start; - for(auto& action : a_plan.script) { - fmt::println("ACTION: {}", action.name); - state = action.apply_effect(state); + ai::Action kill_it("kill_it", 10); + kill_it.needs(ENEMY_IN_RANGE, true); + // this is duplicated on purpose to confirm that setting + // a positive then a negative properly cancels out + kill_it.needs(ENEMY_DEAD, true); + kill_it.needs(ENEMY_DEAD, false); + + // same thing with effects + kill_it.effect(ENEMY_DEAD, false); + kill_it.effect(ENEMY_DEAD, true); + + // order seems to matter which is wrong + actions.push_back(kill_it); + actions.push_back(move_closer); + + auto result = ai::plan_actions(actions, start, goal); + CHECK(result.complete); + + auto state = start; + + for(auto& action : result.script) { + state = action.apply_effect(state); + } + + CHECK(state[ENEMY_DEAD]); } - REQUIRE(ai::test(state, "target_dead")); -} - -TEST_CASE("ai autowalker ai test", "[ai]") { - ai::reset(); - ai::init("ai"); - auto start = ai::load_state("Host::initial_state"); - auto goal = ai::load_state("Host::final_state"); - int enemy_count = 5; - - ai::set(start, "no_more_enemies", enemy_count == 0); - - // find an enemy and kill them - auto a_plan = ai::plan("Host::actions", start, goal); - REQUIRE(!a_plan.complete); - - auto result = ai::dump_script("\n\nWALKER KILL STUFF", start, a_plan.script); - REQUIRE(ai::test(result, "enemy_found")); - REQUIRE(!ai::test(result, "no_more_enemies")); - - // health is low, go heal - ai::set(result, "health_good", false); - ai::set(result, "in_combat", false); - ai::set(result, "enemy_found", false); - ai::set(result, "have_healing", true); - ai::set(result, "have_item", true); - REQUIRE(!ai::test(result, "health_good")); - - auto health_plan = ai::plan("Host::actions", result, goal); - result = ai::dump_script("\n\nWALKER NEED HEALTH", result, health_plan.script); - REQUIRE(!health_plan.complete); - REQUIRE(ai::test(result, "health_good")); - - // health is good, enemies dead, go get stuff - ai::set(result, "no_more_enemies", true); - REQUIRE(ai::test(result, "no_more_enemies")); - - auto new_plan = ai::plan("Host::actions", result, goal); - result = ai::dump_script("\n\nWALKER COLLECT ITEMS", result, new_plan.script); - REQUIRE(ai::test(result, "no_more_items")); - REQUIRE(ai::test(result, "no_more_enemies")); -} - -TEST_CASE("Confirm EntityAI behaves as expected", "[ai]") { - ai::reset(); - ai::init("ai"); - auto ai_start = ai::load_state("Enemy::initial_state"); - auto ai_goal = ai::load_state("Enemy::final_state"); - - ai::EntityAI enemy("Enemy::actions", ai_start, ai_goal); - - enemy.set_state("detect_enemy", true); - enemy.update(); - REQUIRE(enemy.wants_to("find_enemy")); - - enemy.set_state("enemy_found", true); - enemy.set_state("in_combat", true); - enemy.update(); - REQUIRE(enemy.wants_to("kill_enemy")); - - enemy.set_state("have_item", true); - enemy.set_state("have_healing", true); - enemy.set_state("in_combat", false); - enemy.set_state("health_good", false); - enemy.update(); - REQUIRE(enemy.wants_to("use_healing")); - - enemy.set_state("have_healing", false); - enemy.set_state("tough_personality", true); - enemy.set_state("in_combat", true); - enemy.set_state("health_good", true); - enemy.update(); - REQUIRE(enemy.wants_to("kill_enemy")); - - fmt::println("\n\n\n\n=============================\n\n\n\n"); - enemy.set_state("have_healing", false); - enemy.set_state("tough_personality", false); - enemy.set_state("in_combat", true); - enemy.set_state("health_good", false); - enemy.update(); - REQUIRE(enemy.wants_to("run_away")); + + void test_ai_module_like_sound() { + ai::reset(); + ai::init("tests/ai_fixture.json"); + + auto start = ai::load_state("test_start"); + auto goal = ai::load_state("test_goal"); + + auto a_plan = ai::plan("test1", start, goal); + CHECK(a_plan.complete); + + auto state = start; + for(auto& action : a_plan.script) { + fmt::println("ACTION: {}", action.name); + state = action.apply_effect(state); + } + + CHECK(ai::test(state, "target_dead")); + } + + void test_ai_autowalker() { + ai::reset(); + ai::init("ai"); + auto start = ai::load_state("Host::initial_state"); + auto goal = ai::load_state("Host::final_state"); + int enemy_count = 5; + + ai::set(start, "no_more_enemies", enemy_count == 0); + + // find an enemy and kill them + auto a_plan = ai::plan("Host::actions", start, goal); + CHECK(!a_plan.complete); + + auto result = ai::dump_script("\n\nWALKER KILL STUFF", start, a_plan.script); + CHECK(ai::test(result, "enemy_found")); + CHECK(!ai::test(result, "no_more_enemies")); + + // health is low, go heal + ai::set(result, "health_good", false); + ai::set(result, "in_combat", false); + ai::set(result, "enemy_found", false); + ai::set(result, "have_healing", true); + ai::set(result, "have_item", true); + CHECK(!ai::test(result, "health_good")); + + auto health_plan = ai::plan("Host::actions", result, goal); + result = ai::dump_script("\n\nWALKER NEED HEALTH", result, health_plan.script); + CHECK(!health_plan.complete); + CHECK(ai::test(result, "health_good")); + + // health is good, enemies dead, go get stuff + ai::set(result, "no_more_enemies", true); + CHECK(ai::test(result, "no_more_enemies")); + + auto new_plan = ai::plan("Host::actions", result, goal); + result = ai::dump_script("\n\nWALKER COLLECT ITEMS", result, new_plan.script); + CHECK(ai::test(result, "no_more_items")); + CHECK(ai::test(result, "no_more_enemies")); + } + + void test_confirm_EntityAI_behaves() { + ai::reset(); + ai::init("ai"); + auto ai_start = ai::load_state("Enemy::initial_state"); + auto ai_goal = ai::load_state("Enemy::final_state"); + + ai::EntityAI enemy("Enemy::actions", ai_start, ai_goal); + + enemy.set_state("detect_enemy", true); + enemy.update(); + CHECK(enemy.wants_to("find_enemy")); + + enemy.set_state("enemy_found", true); + enemy.set_state("in_combat", true); + enemy.update(); + CHECK(enemy.wants_to("kill_enemy")); + + enemy.set_state("have_item", true); + enemy.set_state("have_healing", true); + enemy.set_state("in_combat", false); + enemy.set_state("health_good", false); + enemy.update(); + CHECK(enemy.wants_to("use_healing")); + + enemy.set_state("have_healing", false); + enemy.set_state("tough_personality", true); + enemy.set_state("in_combat", true); + enemy.set_state("health_good", true); + enemy.update(); + CHECK(enemy.wants_to("kill_enemy")); + + fmt::println("\n\n\n\n=============================\n\n\n\n"); + enemy.set_state("have_healing", false); + enemy.set_state("tough_personality", false); + enemy.set_state("in_combat", true); + enemy.set_state("health_good", false); + enemy.update(); + CHECK(enemy.wants_to("run_away")); + } + + fuc2::Set TESTS{ + .name="ai functionality", + .tests={ + TEST(test_state_and_actions), + TEST(test_basic_features), + TEST(test_ai_module_like_sound), + TEST(test_ai_autowalker), + TEST(test_confirm_EntityAI_behaves), + } + }; } diff --git a/tests/meson.build b/tests/meson.build index 2a40bbd..35a3d43 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1,5 +1,4 @@ tests = files( - 'ai.cpp', 'animation.cpp', 'base.cpp', 'battle.cpp', @@ -26,3 +25,8 @@ tests = files( 'systems.cpp', 'textures.cpp', ) + +fuc2_tests = files( + 'ai.cpp', + 'main.cpp' +) diff --git a/wraps/fuc2.wrap b/wraps/fuc2.wrap new file mode 100644 index 0000000..50fe0df --- /dev/null +++ b/wraps/fuc2.wrap @@ -0,0 +1,9 @@ +[wrap-git] +directory=fuc2-0.1.0 +url=https://lcthw.dev/cpp/fuc2.git +revision=HEAD +depth=1 +method=meson + +[provide] +fuc2 = fuc2_dep