#include "autowalker.hpp" #include "ai_debug.hpp" #include "gui/ritual_ui.hpp" #include "game_level.hpp" #include "systems.hpp" struct InventoryStats { int healing = 0; int other = 0; }; template int number_left() { int count = 0; auto world = GameDB::current_world(); auto player = GameDB::the_player(); world->query( [&](const auto ent, auto&, auto&) { if(ent != player) { count++; } }); return count; } template Pathing compute_paths() { auto& level = GameDB::current_level(); auto walls_copy = level.map->$walls; Pathing paths{matrix::width(walls_copy), matrix::height(walls_copy)}; System::multi_path(level, paths, walls_copy); return paths; } DinkyECS::Entity Autowalker::camera_aim() { auto& level = GameDB::current_level(); auto player_pos = GameDB::player_position(); // what happens if there's two things at that spot if(level.collision->something_there(player_pos.aiming_at)) { return level.collision->get(player_pos.aiming_at); } else { return DinkyECS::NONE; } } void Autowalker::log(std::wstring msg) { fsm.$map_ui.log(msg); } void Autowalker::status(std::wstring msg) { fsm.$main_ui.$overlay_ui.show_text("bottom", msg); } void Autowalker::close_status() { fsm.$main_ui.$overlay_ui.close_text("bottom"); } Pathing Autowalker::path_to_enemies() { return compute_paths(); } Pathing Autowalker::path_to_items() { return compute_paths(); } void Autowalker::handle_window_events() { fsm.$window.handleEvents( [&](const sf::Event::KeyPressed &) { fsm.autowalking = false; close_status(); log(L"Aborting autowalk."); }, [&](const sf::Event::MouseButtonPressed &) { fsm.autowalking = false; close_status(); log(L"Aborting autowalk."); } ); } void Autowalker::process_combat() { while(fsm.in_state(gui::State::IN_COMBAT) || fsm.in_state(gui::State::ATTACKING)) { if(fsm.in_state(gui::State::ATTACKING)) { send_event(gui::Event::TICK); } else { send_event(gui::Event::ATTACK); } } } void Autowalker::path_fail(const std::string& msg, Matrix& bad_paths, Point pos) { dbc::log(msg); status(L"PATH FAIL"); matrix::dump("MOVE FAIL PATHS", bad_paths, pos.x, pos.y); log(L"Autowalk failed to find a path."); send_event(gui::Event::STAIRS_DOWN); } bool Autowalker::path_player(Pathing& paths, Point& target_out) { auto& level = GameDB::current_level(); auto found = paths.find_path(target_out, PATHING_TOWARD, false); if(found == PathingResult::FAIL) { // failed to find a linear path, try diagonal if(paths.find_path(target_out, PATHING_TOWARD, true) == PathingResult::FAIL) { path_fail("random_walk", paths.$paths, target_out); return false; } } if(!level.map->can_move(target_out)) { path_fail("level_map->can_move", paths.$paths, target_out); return false; } return true; } void Autowalker::rotate_player(Point target) { auto &player = GameDB::player_position(); if(target == player.location) { dbc::log("player stuck at a locatoin"); fsm.autowalking = false; return; } auto dir = System::shortest_rotate(player.location, player.aiming_at, target); for(int i = 0; player.aiming_at != target; i++) { if(i > 10) { dbc::log("HIT OVER ROTATE BUG!"); break; } send_event(dir); while(fsm.in_state(gui::State::ROTATING) || fsm.in_state(gui::State::COMBAT_ROTATE)) { send_event(gui::Event::TICK); } } fsm.autowalking = player.aiming_at == target; } ai::State Autowalker::update_state(ai::State start) { int enemy_count = number_left(); int item_count = number_left(); ai::set(start, "no_more_enemies", enemy_count == 0); ai::set(start, "no_more_items", item_count == 0); // BUG: so isn't this wrong? we "find" an enemy when we are aiming at one ai::set(start, "enemy_found", found_enemy()); ai::set(start, "health_good", player_health_good()); ai::set(start, "in_combat", fsm.in_state(gui::State::IN_COMBAT) || fsm.in_state(gui::State::ATTACKING)); auto inv = player_item_count(); ai::set(start, "have_item", inv.other > 0 || inv.healing > 0); ai::set(start, "have_healing", inv.healing > 0); return start; } void Autowalker::handle_boss_fight() { // skip the boss fight for now if(fsm.in_state(gui::State::NEXT_LEVEL)) { // eventually we'll have AI handle this too send_event(gui::Event::STAIRS_DOWN); } } void Autowalker::handle_player_walk(ai::State& start, ai::State& goal) { start = update_state(start); auto a_plan = ai::plan("Host::actions", start, goal); auto action = a_plan.script.front(); auto level = GameDB::current_level(); // ai::dump_script("AUTOWALK", start, a_plan.script); if(action.name == "find_enemy") { status(L"FINDING ENEMY"); auto paths = path_to_enemies(); process_move(paths, [&](auto target) -> bool { return level.collision->occupied(target); }); face_enemy(); } else if(action.name == "kill_enemy") { status(L"KILLING ENEMY"); if(fsm.in_state(gui::State::IN_COMBAT)) { if(face_enemy()) { process_combat(); } } } else if(action.name == "use_healing") { status(L"USING HEALING"); player_use_healing(); } else if(action.name == "collect_items") { status(L"COLLECTING ITEMS"); auto paths = path_to_items(); bool found_it = process_move(paths, [&](auto target) -> bool { if(!level.collision->something_there(target)) return false; auto entity = level.collision->get(target); return ( level.world->has(entity) || level.world->has(entity)); }); if(found_it) pickup_item(); } else if(action == ai::FINAL_ACTION) { close_status(); log(L"FINAL ACTION! Autowalk done."); fsm.autowalking = false; } else { close_status(); dbc::log(fmt::format("Unknown action: {}", action.name)); } } void Autowalker::craft_weapon() { if(!weapon_crafted) { auto& ritual_ui = fsm.$status_ui.$ritual_ui; fsm.$status_ui.$gui.click_on("ritual_ui"); while(!ritual_ui.in_state(gui::ritual::State::OPENED)) { send_event(gui::Event::TICK); } ritual_ui.$gui.click_on("inv_slot0"); send_event(gui::Event::TICK); ritual_ui.$gui.click_on("inv_slot1"); send_event(gui::Event::TICK); while(!ritual_ui.in_state(gui::ritual::State::CRAFTING)) { send_event(gui::Event::TICK); } ritual_ui.$gui.click_on("result_image", true); send_event(gui::Event::TICK); ritual_ui.$gui.click_on("ritual_ui"); send_event(gui::Event::TICK); weapon_crafted = true; } } void Autowalker::open_map() { if(!map_opened_once) { if(!fsm.$map_open) { send_event(gui::Event::MAP_OPEN); map_opened_once = true; } } } void Autowalker::close_map() { if(fsm.$map_open) { send_event(gui::Event::MAP_OPEN); } } void Autowalker::autowalk() { handle_window_events(); if(!fsm.autowalking) { close_status(); return; } craft_weapon(); open_map(); int move_attempts = 0; auto start = ai::load_state("Host::initial_state"); auto goal = ai::load_state("Host::final_state"); do { handle_window_events(); handle_boss_fight(); handle_player_walk(start, goal); close_map(); move_attempts++; } while(move_attempts < 100 && fsm.autowalking); } bool Autowalker::process_move(Pathing& paths, std::function is_that_it) { // target has to start at the player location then... auto target_out = GameDB::player_position().location; // ... target gets modified as an out parameter to find the path if(!path_player(paths, target_out)) { close_status(); log(L"No paths found, aborting autowalk."); return false; } if(rayview->aiming_at != target_out) rotate_player(target_out); bool found_it = is_that_it(target_out); if(!found_it) { send_event(gui::Event::MOVE_FORWARD); while(fsm.in_state(gui::State::MOVING)) send_event(gui::Event::TICK); } return found_it; } bool Autowalker::found_enemy() { auto& level = GameDB::current_level(); auto player = GameDB::player_position(); for(matrix::compass it{level.map->$walls, player.location.x, player.location.y}; it.next();) { Point aim{it.x, it.y}; auto aimed_ent = level.collision->occupied_by(player.aiming_at); if(aim != player.aiming_at || aimed_ent == DinkyECS::NONE) continue; if(level.world->has(aimed_ent)) return true; } return false; } bool Autowalker::found_item() { auto world = GameDB::current_world(); auto aimed_at = camera_aim(); return aimed_at != DinkyECS::NONE && world->has(aimed_at); } void Autowalker::send_event(gui::Event ev, std::any data) { fsm.event(ev, data); fsm.render(); fsm.handle_world_events(); } bool Autowalker::player_health_good() { auto world = GameDB::current_world(); auto player = GameDB::the_player(); auto combat = world->get(player); return float(combat.hp) / float(combat.max_hp) > 0.5f; } InventoryStats Autowalker::player_item_count() { InventoryStats stats; stats.healing = 0; return stats; } void Autowalker::player_use_healing() { } void Autowalker::start_autowalk() { fsm.autowalking = true; } void Autowalker::face_target(Point target) { if(rayview->aiming_at != target) rotate_player(target); } bool Autowalker::face_enemy() { auto& level = GameDB::current_level(); auto player_at = GameDB::player_position(); auto [found, neighbors] = level.collision->neighbors(player_at.location, true); if(found) { auto enemy_pos = level.world->get(neighbors[0]); face_target(enemy_pos.location); } else { dbc::log("No enemies nearby, moving on."); } return found; } void Autowalker::click_inventory(const std::string& name, guecs::Modifiers mods) { auto& cell = fsm.$status_ui.$gui.cell_for(name); fsm.$status_ui.mouse(cell.mid_x, cell.mid_y, mods); fsm.handle_world_events(); } void Autowalker::pocket_potion(GameDB::Level &level) { auto& inventory = level.world->get(level.player); if(inventory.has("pocket_r") && inventory.has("pocket_l")) { auto gui_id = fsm.$status_ui.$gui.entity("pocket_r"); send_event(gui::Event::USE_ITEM, gui_id); } send_event(gui::Event::AIM_CLICK); if(inventory.has("pocket_r")) { click_inventory("pocket_l", {1 << guecs::ModBit::left}); } else { click_inventory("pocket_r", {1 << guecs::ModBit::left}); } } void Autowalker::pickup_item() { auto& level = GameDB::current_level(); auto& player_pos = GameDB::player_position(); auto collision = level.collision; if(collision->something_there(player_pos.aiming_at)) { auto entity = collision->get(player_pos.aiming_at); fmt::println("AIMING AT entity {} @ {},{}", entity, player_pos.aiming_at.x, player_pos.aiming_at.y); if(level.world->has(entity)) { pocket_potion(level); status(L"A POTION"); } else { send_event(gui::Event::AIM_CLICK); status(L"I DON'T KNOW"); } } }