From 41bcd52c36bb3483e09bb9c8c99a81af504ec546 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Sun, 2 Mar 2014 13:19:16 +0400 Subject: [PATCH 1/8] Make siege engine debug markings toggleable via lua. Also check that the tile actually exists to avoid crashes. --- plugins/siege-engine.cpp | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/plugins/siege-engine.cpp b/plugins/siege-engine.cpp index 2d6f5fcdd..7e842683e 100644 --- a/plugins/siege-engine.cpp +++ b/plugins/siege-engine.cpp @@ -77,6 +77,21 @@ DFHACK_PLUGIN("siege-engine"); * Misc. utils */ +static bool debug_mode = false; + +static void setDebug(bool on) +{ + debug_mode = on; +} + +static void set_arrow_color(df::coord pos, int color) +{ + auto tile = Maps::getTileOccupancy(pos); + + if (tile) + tile->bits.arrow_color = color; +} + typedef std::pair coord_range; static void set_range(coord_range *target, df::coord p1, df::coord p2) @@ -1359,7 +1374,8 @@ struct projectile_hook : df::proj_itemst { target_pos = path.target; // Debug - Maps::getTileOccupancy(path.goal)->bits.arrow_color = COLOR_LIGHTMAGENTA; + if (debug_mode) + set_arrow_color(path.goal, COLOR_LIGHTMAGENTA); PathMetrics raytrace(path); @@ -1395,7 +1411,8 @@ struct projectile_hook : df::proj_itemst { orient_engine(engine->bld, path.goal); // Debug - Maps::getTileOccupancy(path.goal)->bits.arrow_color = COLOR_LIGHTRED; + if (debug_mode) + set_arrow_color(path.goal, COLOR_LIGHTRED); // Dabbling always hit in 7x7 area if (skill < skill_rating::Novice) @@ -1736,6 +1753,7 @@ IMPLEMENT_VMETHOD_INTERPOSE(building_hook, updateAction); */ DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(setDebug), DFHACK_LUA_FUNCTION(clearTargetArea), DFHACK_LUA_FUNCTION(setTargetArea), DFHACK_LUA_FUNCTION(isLinkedToPile), From 983685de8de2909f50fbb2a1a14f00103ef53f3e Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Sun, 2 Mar 2014 13:40:53 +0400 Subject: [PATCH 2/8] Squash uninitialized variable warnings. --- plugins/siege-engine.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/siege-engine.cpp b/plugins/siege-engine.cpp index 7e842683e..93ab68228 100644 --- a/plugins/siege-engine.cpp +++ b/plugins/siege-engine.cpp @@ -868,6 +868,7 @@ struct PathMetrics { void compute(const ProjectilePath &path) { + hit_type = Impassable; collision_step = goal_step = goal_z_step = 1000000; collision_z_step = 0; @@ -1023,7 +1024,7 @@ static void paintAimScreen(df::building_siegeenginest *bld, df::coord view, df:: if (!cur_tile.valid()) continue; - int color; + int color = COLOR_YELLOW; switch (calcTileStatus(engine, tile_pos)) { From 9827184ef793a517e1de62e1cf1304daebab95e9 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Sun, 2 Mar 2014 13:42:32 +0400 Subject: [PATCH 3/8] Switch to the new random number generator class. --- plugins/siege-engine.cpp | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/plugins/siege-engine.cpp b/plugins/siege-engine.cpp index 93ab68228..39e0ba477 100644 --- a/plugins/siege-engine.cpp +++ b/plugins/siege-engine.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -159,21 +160,13 @@ inline void normalize(float &x, float &y, float &z) x /= dist; y /= dist; z /= dist; } +static Random::MersenneRNG rng; + static void random_direction(float &x, float &y, float &z) { - float a, b, d; - for (;;) { - a = (rand() + 0.5f)*2.0f/RAND_MAX - 1.0f; - b = (rand() + 0.5f)*2.0f/RAND_MAX - 1.0f; - d = a*a + b*b; - if (d < 1.0f) - break; - } - - float sq = sqrtf(1-d); - x = 2.0f*a*sq; - y = 2.0f*b*sq; - z = 1.0f - 2.0f*d; + float vec[3]; + rng.unitvector(vec, 3); + x = vec[0]; y = vec[1]; z = vec[2]; } static const int WEAR_TICKS = 806400; @@ -190,8 +183,8 @@ static bool apply_impact_damage(df::item *item, int minv, int maxv) auto &strength = info.material->strength; // Use random strain type excluding COMPRESSIVE (conveniently last) - int type = random_int(strain_type::COMPRESSIVE); - int power = minv + random_int(maxv-minv+1); + int type = rng.random(strain_type::COMPRESSIVE); + int power = minv + rng.random(maxv-minv+1); // High elasticity materials just bend if (strength.strain_at_yield[type] >= 5000) @@ -1418,8 +1411,8 @@ struct projectile_hook : df::proj_itemst { // Dabbling always hit in 7x7 area if (skill < skill_rating::Novice) { - fail_target.x += random_int(7)-3; - fail_target.y += random_int(7)-3; + fail_target.x += rng.random(7)-3; + fail_target.y += rng.random(7)-3; aimAtPoint(engine, ProjectilePath(path.origin, fail_target)); return; } @@ -1427,7 +1420,7 @@ struct projectile_hook : df::proj_itemst { // Exact hit chance float hit_chance = 1.04f - powf(0.8f, skill); - if (float(rand())/RAND_MAX < hit_chance) + if (rng.drandom() < hit_chance) { aimAtPoint(engine, path); return; @@ -1437,13 +1430,13 @@ struct projectile_hook : df::proj_itemst { if (skill <= skill_rating::Proficient) { // 5x5 - fail_target.x += random_int(5)-2; - fail_target.y += random_int(5)-2; + fail_target.x += rng.random(5)-2; + fail_target.y += rng.random(5)-2; } else { // 3x3 - int idx = random_int(8); + int idx = rng.random(8); fail_target.x += offsets[idx][0]; fail_target.y += offsets[idx][1]; } @@ -1462,7 +1455,7 @@ struct projectile_hook : df::proj_itemst { for (int i = 0; i < 50; i++) { target = tbase + df::coord( - random_int(tsize.x), random_int(tsize.y), random_int(tsize.z) + rng.random(tsize.x), rng.random(tsize.y), rng.random(tsize.z) ); if (adjustToTarget(engine, &target)) @@ -1876,6 +1869,8 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { + rng.init(); + if (Core::getInstance().isMapLoaded()) plugin_onstatechange(out, SC_MAP_LOADED); From ef93f2ea4cf5ecb64aa70268c4b3cc3cee5f9413 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Sun, 2 Mar 2014 14:17:07 +0400 Subject: [PATCH 4/8] Compute quality of siege engines and use it to extend firing range. Since parts aren't there until it is built, it requires checking that. --- plugins/siege-engine.cpp | 36 +++++++++++++++++++++++++++++++----- scripts/gui/siege-engine.lua | 3 +++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/plugins/siege-engine.cpp b/plugins/siege-engine.cpp index 39e0ba477..8a8c8482d 100644 --- a/plugins/siege-engine.cpp +++ b/plugins/siege-engine.cpp @@ -125,12 +125,12 @@ static bool is_in_range(const coord_range &target, df::coord pos) target.first.z <= pos.z && pos.z <= target.second.z; } -static std::pair get_engine_range(df::building_siegeenginest *bld) +static std::pair get_engine_range(df::building_siegeenginest *bld, float quality) { if (bld->type == siegeengine_type::Ballista) - return std::make_pair(1, 200); + return std::make_pair(1, 200 + int(10 * quality)); else - return std::make_pair(30, 100); + return std::make_pair(30 - int(quality), 100 + int(5 * quality)); } static void orient_engine(df::building_siegeenginest *bld, df::coord target) @@ -148,6 +148,27 @@ static void orient_engine(df::building_siegeenginest *bld, df::coord target) df::building_siegeenginest::Up; } +static bool is_build_complete(df::building *bld) +{ + return bld->getBuildStage() >= bld->getMaxBuildStage(); +} + +static float average_quality(df::building_actual *bld) +{ + float quality = 0; + int count = 0; + + for (size_t i = 0; i < bld->contained_items.size(); i++) + { + if (bld->contained_items[i]->use_mode != 2) + continue; + count++; + quality += bld->contained_items[i]->item->getQuality(); + } + + return count > 0 ? quality/count : 0; +} + static int point_distance(df::coord speed) { return std::max(abs(speed.x), std::max(abs(speed.y), abs(speed.z))); @@ -239,6 +260,7 @@ struct EngineInfo { df::coord center; coord_range building_rect; + float quality; bool is_catapult; int proj_speed, hit_delay; std::pair fire_range; @@ -280,7 +302,7 @@ static EngineInfo *find_engine(df::building *bld, bool create = false) return obj; } - if (!create) + if (!create || !is_build_complete(bld)) return NULL; obj = new EngineInfo(); @@ -292,10 +314,11 @@ static EngineInfo *find_engine(df::building *bld, bool create = false) df::coord(bld->x1, bld->y1, bld->z), df::coord(bld->x2, bld->y2, bld->z) ); + obj->quality = average_quality(ebld); obj->is_catapult = (ebld->type == siegeengine_type::Catapult); obj->proj_speed = 2; obj->hit_delay = obj->is_catapult ? 2 : -1; - obj->fire_range = get_engine_range(ebld); + obj->fire_range = get_engine_range(ebld, obj->quality); obj->ammo_vector_id = job_item_vector_id::BOULDER; obj->ammo_item_type = item_type::BOULDER; @@ -444,6 +467,7 @@ static bool setTargetArea(df::building_siegeenginest *bld, df::coord target_min, { CHECK_NULL_POINTER(bld); CHECK_INVALID_ARGUMENT(target_min.isValid() && target_max.isValid()); + CHECK_INVALID_ARGUMENT(is_build_complete(bld)); if (!enable_plugin()) return false; @@ -577,6 +601,7 @@ static bool addStockpileLink(df::building_siegeenginest *bld, df::building_stock { CHECK_NULL_POINTER(bld); CHECK_NULL_POINTER(pile); + CHECK_INVALID_ARGUMENT(is_build_complete(bld)); if (!enable_plugin()) return false; @@ -612,6 +637,7 @@ static bool removeStockpileLink(df::building_siegeenginest *bld, df::building_st static df::workshop_profile *saveWorkshopProfile(df::building_siegeenginest *bld) { CHECK_NULL_POINTER(bld); + CHECK_INVALID_ARGUMENT(is_build_complete(bld)); if (!enable_plugin()) return NULL; diff --git a/scripts/gui/siege-engine.lua b/scripts/gui/siege-engine.lua index 9a5d76066..1e5200875 100644 --- a/scripts/gui/siege-engine.lua +++ b/scripts/gui/siege-engine.lua @@ -491,6 +491,9 @@ local building = df.global.world.selected_building if not df.building_siegeenginest:is_instance(building) then qerror("A siege engine must be selected") end +if building:getBuildStage() < building:getMaxBuildStage() then + qerror("This engine is not completely built yet") +end local list = SiegeEngine{ building = building } list:show() From df22f25a69ea0be41fa4e7401cb87670a1c69fe4 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Sun, 2 Mar 2014 15:46:04 +0400 Subject: [PATCH 5/8] Rewrite aiming error code to account for distance and engine quality. Now the error is simulated by using an approximate normal distribution. --- plugins/siege-engine.cpp | 98 ++++++++++++++++++++++++++++++++-------- 1 file changed, 78 insertions(+), 20 deletions(-) diff --git a/plugins/siege-engine.cpp b/plugins/siege-engine.cpp index 8a8c8482d..a65b6a21c 100644 --- a/plugins/siege-engine.cpp +++ b/plugins/siege-engine.cpp @@ -74,6 +74,62 @@ using Screen::Pen; DFHACK_PLUGIN("siege-engine"); +/* + Aiming is simulated by using a normal distribution to perturb X and Y. + + The chance a normal distribution is within n*sigma of median is + erf(n/sqrt(2)). For direct hit, it must be within 0.5 tiles of + center, so it is erf(0.5/sigma/sqrt(2)) = erf(1/sigma/sqrt(8)). + Since it must hit in both X and Y, it must be squared, so final + is erf(1/sigma/sqrt(8))^2 = erf(skill*coeff/(distance*sqrt(8)))^2. + + The chance to hit a RxR area is erf(skill*coeff*R/(distance*sqrt(8)))^2. + + Catapults can fire between 30 and 100, and the projectile is supposed + to travel in an arc, and is thus harder to aim; yet they require a direct + hit unless using the feature for firing bins. + + The coefficient of 30 gives the following probabilities: + + | | Direct Hit | 3x3 Area Hit | + | | 30 50 100 | 30 50 100 | + | Novice | 15% 5% <5% | 75% 40% 10% | + | Adequate | 45% 20% 5% | 100% 85% 40% | + | Competent | 75% 40% 10% | 100% 100% 70% | + | Proficient | 100% 75% 30% | 100% 100% 95% | + | Professional | 100% 100% 80% | 100% 100% 100% | + | Legendary | 100% 100% 95% | 100% 100% 100% | + + Original data: + + * http://www.wolframalpha.com/input/?i=erf%2830*x%2Fsqrt%288%29%2F30%29^2%2C+erf%2830*x%2Fsqrt%288%29%2F50%29^2%2C+erf%2830*x%2Fsqrt%288%29%2F100%29^2%2C+x+from+0+to+15 + * http://www.wolframalpha.com/input/?i=erf%2830*x*3%2Fsqrt%288%29%2F30%29^2%2C+erf%2830*x*3%2Fsqrt%288%29%2F50%29^2%2C+erf%2830*x*3%2Fsqrt%288%29%2F100%29^2%2C+x+from+0+to+6 + + Ballista can fire up to 200 tiles away, and can't use the bin method + to compensate for inaccuracy. On the other hand, it shoots straight + and should be easier to aim. It also damages everything in its path, + so the hit probability may be estimated using an 1D projection. + + The coefficient of 48 gives the following probabilities: + + | | Direct Hit | 1D Hit | + | | 30 50 100 200 | 30 50 100 200 | + | Novice | 25% 10% 5% <5% | 55% 35% 20% 10% | + | Adequate | 80% 45% 15% 5% | 90% 65% 40% 20% | + | Competent | 95% 70% 30% 8% | 100% 85% 50% 30% | + | Proficient | 100% 95% 65% 20% | 100% 100% 75% 45% | + | Accomplished | 100% 100% 95% 60% | 100% 100% 100% 75% | + | Legendary | 100% 100% 100% 85% | 100% 100% 100% 90% | + + Original data: + + * http://www.wolframalpha.com/input/?i=erf%2848*x%2Fsqrt%288%29%2F30%29^2%2C+erf%2848*x%2Fsqrt%288%29%2F50%29^2%2C+erf%2848*x%2Fsqrt%288%29%2F100%29^2%2C+erf%2848*x%2Fsqrt%288%29%2F200%29^2%2C+x+from+0+to+15 + * http://www.wolframalpha.com/input/?i=erf%2848*x%2Fsqrt%288%29%2F30%29%2C+erf%2848*x%2Fsqrt%288%29%2F50%29%2C+erf%2848*x%2Fsqrt%288%29%2F100%29%2C+erf%2848*x%2Fsqrt%288%29%2F200%29%2C+x+from+0+to+15 + + Quality can increase range of both engines by 25% max, so it + also boosts aiming by 1.06x every step, up to 33.8% gain. +*/ + /* * Misc. utils */ @@ -190,6 +246,12 @@ static void random_direction(float &x, float &y, float &z) x = vec[0]; y = vec[1]; z = vec[2]; } +static double random_error() +{ + // Irwin-Hall approximation to normal distribution with n = 3; varies in (-3,3) + return (rng.drandom0() + rng.drandom0() + rng.drandom0()) * 2.0 - 3.0; +} + static const int WEAR_TICKS = 806400; static bool apply_impact_damage(df::item *item, int minv, int maxv) @@ -265,6 +327,8 @@ struct EngineInfo { int proj_speed, hit_delay; std::pair fire_range; + double sigma_coeff; + coord_range target; df::job_item_vector_id ammo_vector_id; @@ -320,6 +384,9 @@ static EngineInfo *find_engine(df::building *bld, bool create = false) obj->hit_delay = obj->is_catapult ? 2 : -1; obj->fire_range = get_engine_range(ebld, obj->quality); + // Base coefficients per engine type, plus 6% exponential bonus per quality level + obj->sigma_coeff = (obj->is_catapult ? 30.0 : 48.0) * pow(1.06, obj->quality); + obj->ammo_vector_id = job_item_vector_id::BOULDER; obj->ammo_item_type = item_type::BOULDER; @@ -1434,38 +1501,29 @@ struct projectile_hook : df::proj_itemst { if (debug_mode) set_arrow_color(path.goal, COLOR_LIGHTRED); - // Dabbling always hit in 7x7 area + // Dabbling always hit in 11x11 area if (skill < skill_rating::Novice) { - fail_target.x += rng.random(7)-3; - fail_target.y += rng.random(7)-3; + fail_target.x += rng.random(11)-5; + fail_target.y += rng.random(11)-5; aimAtPoint(engine, ProjectilePath(path.origin, fail_target)); return; } - // Exact hit chance - float hit_chance = 1.04f - powf(0.8f, skill); + // Otherwise use a normal distribution to simulate errors + double sigma = point_distance(path.origin - path.goal) / (engine->sigma_coeff * skill); - if (rng.drandom() < hit_chance) + int dx = (int)round(random_error() * sigma); + int dy = (int)round(random_error() * sigma); + + if (dx == 0 && dy == 0) { aimAtPoint(engine, path); return; } - // Otherwise perturb - if (skill <= skill_rating::Proficient) - { - // 5x5 - fail_target.x += rng.random(5)-2; - fail_target.y += rng.random(5)-2; - } - else - { - // 3x3 - int idx = rng.random(8); - fail_target.x += offsets[idx][0]; - fail_target.y += offsets[idx][1]; - } + fail_target.x += dx; + fail_target.y += dy; ProjectilePath fail(path.origin, fail_target, path.fudge_delta, path.fudge_factor); aimAtPoint(engine, fail); From 97ae857c38f7a4b580d67a3ee675d2ca531d9517 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Fri, 7 Mar 2014 17:42:49 +0400 Subject: [PATCH 6/8] Always shatter glass items within bins thrown by a catapult. The glass materials have impact strength copied from compression strength, so the items had 1/5 chance to survive without damage. Since glass is infinite, it makes sense to make it single-use. --- plugins/siege-engine.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/siege-engine.cpp b/plugins/siege-engine.cpp index a65b6a21c..37ddc0603 100644 --- a/plugins/siege-engine.cpp +++ b/plugins/siege-engine.cpp @@ -275,7 +275,7 @@ static bool apply_impact_damage(df::item *item, int minv, int maxv) // Instant fracture? int fracture = strength.fracture[type]; - if (fracture <= power) + if (fracture <= power || info.material->flags.is_set(material_flags::IS_GLASS)) { item->setWear(3); return false; From 8963a49414d04bc91d631013658dc1d8ed911d00 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Mon, 24 Mar 2014 16:05:12 +0400 Subject: [PATCH 7/8] Forbid the items shot by siege engines if the forbid ammo order is set. This only applies when targeting the engine, so you could still make cheaty infinite ammo training units by using stock aiming mode. --- plugins/siege-engine.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/siege-engine.cpp b/plugins/siege-engine.cpp index 37ddc0603..2c79cc5e3 100644 --- a/plugins/siege-engine.cpp +++ b/plugins/siege-engine.cpp @@ -1619,6 +1619,10 @@ struct projectile_hook : df::proj_itemst { aimAtArea(engine, skill); } + bool forbid_ammo = DF_GLOBAL_VALUE(standing_orders_forbid_used_ammo, false); + if (forbid_ammo) + item->flags.bits.forbid = true; + switch (item->getType()) { case item_type::CAGE: From 14afa61b8b024ed748448eda9c1dd3dc7c559e1f Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Mon, 24 Mar 2014 19:33:19 +0400 Subject: [PATCH 8/8] Update NEWS. --- NEWS | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS b/NEWS index b43ced4f0..28dca6098 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,7 @@ DFHack future Misc improvements: - digfort: improved csv parsing, add start() comment handling - exterminate: allow specifying a caste (exterminate gob:male) + - siege-engine: engine quality and distance to target now affect accuracy. DFHack v0.34.11-r4