Merge branch 'siege-quality' into develop

develop
Alexander Gavrilov 2014-03-24 19:33:52 +04:00
commit 6e338d96e9
3 changed files with 150 additions and 44 deletions

@ -10,6 +10,7 @@ DFHack future
Misc improvements: Misc improvements:
- digfort: improved csv parsing, add start() comment handling - digfort: improved csv parsing, add start() comment handling
- exterminate: allow specifying a caste (exterminate gob:male) - exterminate: allow specifying a caste (exterminate gob:male)
- siege-engine: engine quality and distance to target now affect accuracy.
DFHack v0.34.11-r4 DFHack v0.34.11-r4

@ -11,6 +11,7 @@
#include <modules/Units.h> #include <modules/Units.h>
#include <modules/Job.h> #include <modules/Job.h>
#include <modules/Materials.h> #include <modules/Materials.h>
#include <modules/Random.h>
#include <LuaTools.h> #include <LuaTools.h>
#include <TileTypes.h> #include <TileTypes.h>
#include <vector> #include <vector>
@ -73,10 +74,81 @@ using Screen::Pen;
DFHACK_PLUGIN("siege-engine"); 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 * 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<df::coord, df::coord> coord_range; typedef std::pair<df::coord, df::coord> coord_range;
static void set_range(coord_range *target, df::coord p1, df::coord p2) static void set_range(coord_range *target, df::coord p1, df::coord p2)
@ -109,12 +181,12 @@ static bool is_in_range(const coord_range &target, df::coord pos)
target.first.z <= pos.z && pos.z <= target.second.z; target.first.z <= pos.z && pos.z <= target.second.z;
} }
static std::pair<int, int> get_engine_range(df::building_siegeenginest *bld) static std::pair<int, int> get_engine_range(df::building_siegeenginest *bld, float quality)
{ {
if (bld->type == siegeengine_type::Ballista) if (bld->type == siegeengine_type::Ballista)
return std::make_pair(1, 200); return std::make_pair(1, 200 + int(10 * quality));
else 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) static void orient_engine(df::building_siegeenginest *bld, df::coord target)
@ -132,6 +204,27 @@ static void orient_engine(df::building_siegeenginest *bld, df::coord target)
df::building_siegeenginest::Up; 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) static int point_distance(df::coord speed)
{ {
return std::max(abs(speed.x), std::max(abs(speed.y), abs(speed.z))); return std::max(abs(speed.x), std::max(abs(speed.y), abs(speed.z)));
@ -144,21 +237,19 @@ inline void normalize(float &x, float &y, float &z)
x /= dist; y /= dist; z /= dist; x /= dist; y /= dist; z /= dist;
} }
static Random::MersenneRNG rng;
static void random_direction(float &x, float &y, float &z) static void random_direction(float &x, float &y, float &z)
{ {
float a, b, d; float vec[3];
for (;;) { rng.unitvector(vec, 3);
a = (rand() + 0.5f)*2.0f/RAND_MAX - 1.0f; x = vec[0]; y = vec[1]; z = vec[2];
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); static double random_error()
x = 2.0f*a*sq; {
y = 2.0f*b*sq; // Irwin-Hall approximation to normal distribution with n = 3; varies in (-3,3)
z = 1.0f - 2.0f*d; return (rng.drandom0() + rng.drandom0() + rng.drandom0()) * 2.0 - 3.0;
} }
static const int WEAR_TICKS = 806400; static const int WEAR_TICKS = 806400;
@ -175,8 +266,8 @@ static bool apply_impact_damage(df::item *item, int minv, int maxv)
auto &strength = info.material->strength; auto &strength = info.material->strength;
// Use random strain type excluding COMPRESSIVE (conveniently last) // Use random strain type excluding COMPRESSIVE (conveniently last)
int type = random_int(strain_type::COMPRESSIVE); int type = rng.random(strain_type::COMPRESSIVE);
int power = minv + random_int(maxv-minv+1); int power = minv + rng.random(maxv-minv+1);
// High elasticity materials just bend // High elasticity materials just bend
if (strength.strain_at_yield[type] >= 5000) if (strength.strain_at_yield[type] >= 5000)
@ -184,7 +275,7 @@ static bool apply_impact_damage(df::item *item, int minv, int maxv)
// Instant fracture? // Instant fracture?
int fracture = strength.fracture[type]; int fracture = strength.fracture[type];
if (fracture <= power) if (fracture <= power || info.material->flags.is_set(material_flags::IS_GLASS))
{ {
item->setWear(3); item->setWear(3);
return false; return false;
@ -231,10 +322,13 @@ struct EngineInfo {
df::coord center; df::coord center;
coord_range building_rect; coord_range building_rect;
float quality;
bool is_catapult; bool is_catapult;
int proj_speed, hit_delay; int proj_speed, hit_delay;
std::pair<int, int> fire_range; std::pair<int, int> fire_range;
double sigma_coeff;
coord_range target; coord_range target;
df::job_item_vector_id ammo_vector_id; df::job_item_vector_id ammo_vector_id;
@ -272,7 +366,7 @@ static EngineInfo *find_engine(df::building *bld, bool create = false)
return obj; return obj;
} }
if (!create) if (!create || !is_build_complete(bld))
return NULL; return NULL;
obj = new EngineInfo(); obj = new EngineInfo();
@ -284,10 +378,14 @@ static EngineInfo *find_engine(df::building *bld, bool create = false)
df::coord(bld->x1, bld->y1, bld->z), df::coord(bld->x1, bld->y1, bld->z),
df::coord(bld->x2, bld->y2, bld->z) df::coord(bld->x2, bld->y2, bld->z)
); );
obj->quality = average_quality(ebld);
obj->is_catapult = (ebld->type == siegeengine_type::Catapult); obj->is_catapult = (ebld->type == siegeengine_type::Catapult);
obj->proj_speed = 2; obj->proj_speed = 2;
obj->hit_delay = obj->is_catapult ? 2 : -1; obj->hit_delay = obj->is_catapult ? 2 : -1;
obj->fire_range = get_engine_range(ebld); 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_vector_id = job_item_vector_id::BOULDER;
obj->ammo_item_type = item_type::BOULDER; obj->ammo_item_type = item_type::BOULDER;
@ -436,6 +534,7 @@ static bool setTargetArea(df::building_siegeenginest *bld, df::coord target_min,
{ {
CHECK_NULL_POINTER(bld); CHECK_NULL_POINTER(bld);
CHECK_INVALID_ARGUMENT(target_min.isValid() && target_max.isValid()); CHECK_INVALID_ARGUMENT(target_min.isValid() && target_max.isValid());
CHECK_INVALID_ARGUMENT(is_build_complete(bld));
if (!enable_plugin()) if (!enable_plugin())
return false; return false;
@ -569,6 +668,7 @@ static bool addStockpileLink(df::building_siegeenginest *bld, df::building_stock
{ {
CHECK_NULL_POINTER(bld); CHECK_NULL_POINTER(bld);
CHECK_NULL_POINTER(pile); CHECK_NULL_POINTER(pile);
CHECK_INVALID_ARGUMENT(is_build_complete(bld));
if (!enable_plugin()) if (!enable_plugin())
return false; return false;
@ -604,6 +704,7 @@ static bool removeStockpileLink(df::building_siegeenginest *bld, df::building_st
static df::workshop_profile *saveWorkshopProfile(df::building_siegeenginest *bld) static df::workshop_profile *saveWorkshopProfile(df::building_siegeenginest *bld)
{ {
CHECK_NULL_POINTER(bld); CHECK_NULL_POINTER(bld);
CHECK_INVALID_ARGUMENT(is_build_complete(bld));
if (!enable_plugin()) if (!enable_plugin())
return NULL; return NULL;
@ -853,6 +954,7 @@ struct PathMetrics {
void compute(const ProjectilePath &path) void compute(const ProjectilePath &path)
{ {
hit_type = Impassable;
collision_step = goal_step = goal_z_step = 1000000; collision_step = goal_step = goal_z_step = 1000000;
collision_z_step = 0; collision_z_step = 0;
@ -1008,7 +1110,7 @@ static void paintAimScreen(df::building_siegeenginest *bld, df::coord view, df::
if (!cur_tile.valid()) if (!cur_tile.valid())
continue; continue;
int color; int color = COLOR_YELLOW;
switch (calcTileStatus(engine, tile_pos)) switch (calcTileStatus(engine, tile_pos))
{ {
@ -1359,7 +1461,8 @@ struct projectile_hook : df::proj_itemst {
target_pos = path.target; target_pos = path.target;
// Debug // Debug
Maps::getTileOccupancy(path.goal)->bits.arrow_color = COLOR_LIGHTMAGENTA; if (debug_mode)
set_arrow_color(path.goal, COLOR_LIGHTMAGENTA);
PathMetrics raytrace(path); PathMetrics raytrace(path);
@ -1395,40 +1498,32 @@ struct projectile_hook : df::proj_itemst {
orient_engine(engine->bld, path.goal); orient_engine(engine->bld, path.goal);
// Debug // 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 // Dabbling always hit in 11x11 area
if (skill < skill_rating::Novice) if (skill < skill_rating::Novice)
{ {
fail_target.x += random_int(7)-3; fail_target.x += rng.random(11)-5;
fail_target.y += random_int(7)-3; fail_target.y += rng.random(11)-5;
aimAtPoint(engine, ProjectilePath(path.origin, fail_target)); aimAtPoint(engine, ProjectilePath(path.origin, fail_target));
return; return;
} }
// Exact hit chance // Otherwise use a normal distribution to simulate errors
float hit_chance = 1.04f - powf(0.8f, skill); double sigma = point_distance(path.origin - path.goal) / (engine->sigma_coeff * skill);
int dx = (int)round(random_error() * sigma);
int dy = (int)round(random_error() * sigma);
if (float(rand())/RAND_MAX < hit_chance) if (dx == 0 && dy == 0)
{ {
aimAtPoint(engine, path); aimAtPoint(engine, path);
return; return;
} }
// Otherwise perturb fail_target.x += dx;
if (skill <= skill_rating::Proficient) fail_target.y += dy;
{
// 5x5
fail_target.x += random_int(5)-2;
fail_target.y += random_int(5)-2;
}
else
{
// 3x3
int idx = random_int(8);
fail_target.x += offsets[idx][0];
fail_target.y += offsets[idx][1];
}
ProjectilePath fail(path.origin, fail_target, path.fudge_delta, path.fudge_factor); ProjectilePath fail(path.origin, fail_target, path.fudge_delta, path.fudge_factor);
aimAtPoint(engine, fail); aimAtPoint(engine, fail);
@ -1444,7 +1539,7 @@ struct projectile_hook : df::proj_itemst {
for (int i = 0; i < 50; i++) for (int i = 0; i < 50; i++)
{ {
target = tbase + df::coord( 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)) if (adjustToTarget(engine, &target))
@ -1524,6 +1619,10 @@ struct projectile_hook : df::proj_itemst {
aimAtArea(engine, skill); 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()) switch (item->getType())
{ {
case item_type::CAGE: case item_type::CAGE:
@ -1736,6 +1835,7 @@ IMPLEMENT_VMETHOD_INTERPOSE(building_hook, updateAction);
*/ */
DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_PLUGIN_LUA_FUNCTIONS {
DFHACK_LUA_FUNCTION(setDebug),
DFHACK_LUA_FUNCTION(clearTargetArea), DFHACK_LUA_FUNCTION(clearTargetArea),
DFHACK_LUA_FUNCTION(setTargetArea), DFHACK_LUA_FUNCTION(setTargetArea),
DFHACK_LUA_FUNCTION(isLinkedToPile), DFHACK_LUA_FUNCTION(isLinkedToPile),
@ -1857,6 +1957,8 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands) DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{ {
rng.init();
if (Core::getInstance().isMapLoaded()) if (Core::getInstance().isMapLoaded())
plugin_onstatechange(out, SC_MAP_LOADED); plugin_onstatechange(out, SC_MAP_LOADED);

@ -491,6 +491,9 @@ local building = df.global.world.selected_building
if not df.building_siegeenginest:is_instance(building) then if not df.building_siegeenginest:is_instance(building) then
qerror("A siege engine must be selected") qerror("A siege engine must be selected")
end end
if building:getBuildStage() < building:getMaxBuildStage() then
qerror("This engine is not completely built yet")
end
local list = SiegeEngine{ building = building } local list = SiegeEngine{ building = building }
list:show() list:show()