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:
- 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

@ -11,6 +11,7 @@
#include <modules/Units.h>
#include <modules/Job.h>
#include <modules/Materials.h>
#include <modules/Random.h>
#include <LuaTools.h>
#include <TileTypes.h>
#include <vector>
@ -73,10 +74,81 @@ 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
*/
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;
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;
}
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)
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)
@ -132,6 +204,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)));
@ -144,21 +237,19 @@ 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 vec[3];
rng.unitvector(vec, 3);
x = vec[0]; y = vec[1]; z = vec[2];
}
float sq = sqrtf(1-d);
x = 2.0f*a*sq;
y = 2.0f*b*sq;
z = 1.0f - 2.0f*d;
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;
@ -175,8 +266,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)
@ -184,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;
@ -231,10 +322,13 @@ struct EngineInfo {
df::coord center;
coord_range building_rect;
float quality;
bool is_catapult;
int proj_speed, hit_delay;
std::pair<int, int> fire_range;
double sigma_coeff;
coord_range target;
df::job_item_vector_id ammo_vector_id;
@ -272,7 +366,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();
@ -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->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);
// 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;
@ -436,6 +534,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;
@ -569,6 +668,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;
@ -604,6 +704,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;
@ -853,6 +954,7 @@ struct PathMetrics {
void compute(const ProjectilePath &path)
{
hit_type = Impassable;
collision_step = goal_step = goal_z_step = 1000000;
collision_z_step = 0;
@ -1008,7 +1110,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))
{
@ -1359,7 +1461,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,40 +1498,32 @@ 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
// Dabbling always hit in 11x11 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(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 (float(rand())/RAND_MAX < 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 += 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];
}
fail_target.x += dx;
fail_target.y += dy;
ProjectilePath fail(path.origin, fail_target, path.fudge_delta, path.fudge_factor);
aimAtPoint(engine, fail);
@ -1444,7 +1539,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))
@ -1524,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:
@ -1736,6 +1835,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),
@ -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)
{
rng.init();
if (Core::getInstance().isMapLoaded())
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
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()