diff --git a/res/e3a/races.xml b/res/e3a/races.xml index 46427ba92..164a599b7 100644 --- a/res/e3a/races.xml +++ b/res/e3a/races.xml @@ -883,7 +883,7 @@ - + diff --git a/res/eressea/races.xml b/res/eressea/races.xml index 98f00f726..c6db51eae 100644 --- a/res/eressea/races.xml +++ b/res/eressea/races.xml @@ -1172,7 +1172,7 @@ - + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e30cd56d9..049afeeeb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -199,6 +199,7 @@ set(TESTS_SRC laws.test.c magic.test.c market.test.c + monsters.test.c move.test.c piracy.test.c prefix.test.c diff --git a/src/kernel/race.h b/src/kernel/race.h index c994746d9..a9d8a4ff0 100644 --- a/src/kernel/race.h +++ b/src/kernel/race.h @@ -214,6 +214,7 @@ extern "C" { #define RCF_SHIPSPEED (1<<26) /* race gets +1 on shipspeed */ #define RCF_STONEGOLEM (1<<27) /* race gets stonegolem properties */ #define RCF_IRONGOLEM (1<<28) /* race gets irongolem properties */ +#define RCF_ATTACK_MOVED (1<<29) /* may attack if it has moved */ /* Economic flags */ #define ECF_KEEP_ITEM (1<<1) /* gibt Gegenstände weg */ diff --git a/src/kernel/unit.c b/src/kernel/unit.c index 64db18193..16f874b80 100644 --- a/src/kernel/unit.c +++ b/src/kernel/unit.c @@ -28,6 +28,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #include "curse.h" #include "item.h" #include "move.h" +#include "monster.h" #include "order.h" #include "plane.h" #include "race.h" @@ -56,6 +57,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #include #include #include +#include #include #include #include @@ -1125,10 +1127,11 @@ void set_number(unit * u, int count) u->number = (unsigned short)count; } -bool learn_skill(unit * u, skill_t sk, double chance) +bool learn_skill(unit * u, skill_t sk, double learn_chance) { skill *sv = u->skills; - if (chance < 1.0 && rng_int() % 10000 >= chance * 10000) + if (learn_chance < 1.0 && rng_int() % 10000 >= learn_chance * 10000) + if (!chance(learn_chance)) return false; while (sv != u->skills + u->skill_size) { assert(sv->weeks > 0); @@ -1891,7 +1894,7 @@ static double produceexp_chance(void) { void produceexp_ex(struct unit *u, skill_t sk, int n, bool (*learn)(unit *, skill_t, double)) { - if (n != 0 && playerrace(u_race(u))) { + if (n != 0 && (is_monsters(u->faction) || playerrace(u_race(u)))) { double chance = produceexp_chance(); if (chance > 0.0F) { learn(u, sk, (n * chance) / u->number); diff --git a/src/kernel/xmlreader.c b/src/kernel/xmlreader.c index 28a351a72..c5af75d04 100644 --- a/src/kernel/xmlreader.c +++ b/src/kernel/xmlreader.c @@ -1604,6 +1604,8 @@ static void parse_ai(race * rc, xmlNodePtr node) rc->flags |= RCF_MOVERANDOM; if (xml_bvalue(node, "learn", false)) rc->flags |= RCF_LEARN; + if (xml_bvalue(node, "moveattack", false)) + rc->flags |= RCF_ATTACK_MOVED; } static int parse_races(xmlDocPtr doc) diff --git a/src/laws.test.c b/src/laws.test.c index 655699895..12c4e93f4 100644 --- a/src/laws.test.c +++ b/src/laws.test.c @@ -657,7 +657,7 @@ static void test_unarmed_races_can_guard(CuTest *tc) { setup_guard(&fix, false); rc = rc_get_or_create(fix.u->_race->_name); - rc->flags |= RCF_UNARMEDGUARD; + fset(rc, RCF_UNARMEDGUARD); CuAssertIntEquals(tc, E_GUARD_OK, can_start_guarding(fix.u)); update_guards(); CuAssertTrue(tc, fval(fix.u, UFL_GUARD)); diff --git a/src/monster.c b/src/monster.c index 101377ef0..03faa1830 100644 --- a/src/monster.c +++ b/src/monster.c @@ -69,7 +69,8 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. bool monster_is_waiting(const unit * u) { - if (fval(u, UFL_ISNEW | UFL_MOVED)) + int test = fval(u_race(u), RCF_ATTACK_MOVED) ? UFL_ISNEW : UFL_ISNEW | UFL_MOVED; + if (fval(u, test)) return true; return false; } diff --git a/src/monsters.c b/src/monsters.c index 607994b69..988045a9d 100644 --- a/src/monsters.c +++ b/src/monsters.c @@ -71,7 +71,7 @@ #include #include -#define MOVECHANCE 25 /* chance fuer bewegung */ +#define MOVECHANCE .25 /* chance fuer bewegung */ #define DRAGON_RANGE 20 /* Max. Distanz zum nächsten Drachenziel */ #define MAXILLUSION_TEXTS 3 @@ -83,6 +83,10 @@ static void give_peasants(unit *u, const item_type *itype, int reduce) { unit_addorder(u, parse_order(buf, u->faction->locale)); } +static double random_move_chance(void) { + return config_get_flt("rules.monsters.random_move_chance", MOVECHANCE); +} + static void reduce_weight(unit * u) { int capacity, weight = 0; @@ -155,9 +159,30 @@ static order *monster_attack(unit * u, const unit * target) return create_order(K_ATTACK, u->faction->locale, "%i", target->no); } +int monster_attacks(unit * monster, bool respect_buildings, bool rich_only) +{ + region *r = monster->region; + unit *u2; + int money = 0; + + for (u2 = r->units; u2; u2 = u2->next) { + if (u2->faction != monster->faction && cansee(monster->faction, r, u2, 0) && !in_safe_building(u2, monster)) { + int m = get_money(u2); + if (!rich_only || m > 0) { + order *ord = monster_attack(monster, u2); + if (ord) { + addlist(&monster->orders, ord); + money += m; + } + } + } + } + return money; +} + static order *get_money_for_dragon(region * r, unit * udragon, int wanted) { - int n; + int money; bool attacks = attack_chance > 0.0; /* falls genug geld in der region ist, treiben wir steuern ein. */ @@ -171,26 +196,14 @@ static order *get_money_for_dragon(region * r, unit * udragon, int wanted) /* falls der drache launisch ist, oder das regionssilber knapp, greift er alle an * und holt sich Silber von Einheiten, vorausgesetzt er bewacht bereits */ - n = 0; + money = 0; if (attacks && is_guard(udragon, GUARD_TAX)) { - unit *u; - for (u = r->units; u; u = u->next) { - if (u->faction != udragon->faction && cansee(udragon->faction, r, u, 0) && !in_safe_building(u, udragon)) { - int m = get_money(u); - if (m != 0) { - order *ord = monster_attack(udragon, u); - if (ord) { - addlist(&udragon->orders, ord); - n += m; - } - } - } - } + money += monster_attacks(udragon, true, true); } /* falls die einnahmen erreicht werden, bleibt das monster noch eine */ /* runde hier. */ - if (n + rmoney(r) >= wanted) { + if (money + rmoney(r) >= wanted) { return create_order(K_LOOT, default_locale, NULL); } @@ -397,10 +410,10 @@ static int dragon_affinity_value(region * r, unit * u) int m = all_money(r, u->faction); if (u_race(u) == get_race(RC_FIREDRAGON)) { - return (int)(normalvariate(m, m / 2)); + return dice(4, m / 2); } else { - return (int)(normalvariate(m, m / 4)); + return dice(6, m / 3); } } @@ -536,21 +549,6 @@ static order *monster_seeks_target(region * r, unit * u) } #endif -static void monster_attacks(unit * monster) -{ - region *r = monster->region; - unit *u; - - for (u = r->units; u; u = u->next) { - if (u->faction != monster->faction && cansee(monster->faction, r, u, 0) && !in_safe_building(u, monster)) { - order *ord = monster_attack(monster, u); - if (ord) { - addlist(&monster->orders, ord); - } - } - } -} - static const char *random_growl(void) { switch (rng_int() % 5) { @@ -742,9 +740,10 @@ static order *plan_dragon(unit * u) } } if (long_order == NULL) { + int attempts = 0; skill_t sk = SK_PERCEPTION; /* study perception (or a random useful skill) */ - while (!skill_enabled(sk) || u_race(u)->bonus[sk] < -5) { + while ((!skill_enabled(sk) || (attempts < MAXSKILLS && u_race(u)->bonus[sk] < (++attempts < 10?1:-5 )))) { sk = (skill_t)(rng_int() % MAXSKILLS); } long_order = create_order(K_STUDY, u->faction->locale, "'%s'", @@ -763,7 +762,7 @@ void plan_monsters(faction * f) for (r = regions; r; r = r->next) { unit *u; - bool attacking = false; + bool attacking = chance(attack_chance); for (u = r->units; u; u = u->next) { attrib *ta; @@ -777,20 +776,17 @@ void plan_monsters(faction * f) free_orders(&u->orders); if (skill_enabled(SK_PERCEPTION)) { /* Monster bekommen jede Runde ein paar Tage Wahrnehmung dazu */ - /* TODO: this only works for playerrace */ produceexp(u, SK_PERCEPTION, u->number); } - if (!attacking) { - if (chance(attack_chance)) attacking = true; - } if (u->status > ST_BEHIND) { setstatus(u, ST_FIGHT); /* all monsters fight */ } - if (attacking && is_guard(u, GUARD_TAX)) { - monster_attacks(u); + if (attacking && (!r->land || is_guard(u, GUARD_TAX))) { + monster_attacks(u, true, false); } + /* units with a plan to kill get ATTACK orders: */ ta = a_find(u->attribs, &at_hate); if (ta && !monster_is_waiting(u)) { @@ -825,7 +821,7 @@ void plan_monsters(faction * f) } } else if (u_race(u)->flags & RCF_MOVERANDOM) { - if (rng_int() % 100 < MOVECHANCE || check_overpopulated(u)) { + if (chance(random_move_chance()) || check_overpopulated(u)) { long_order = monster_move(r, u); } } diff --git a/src/monsters.test.c b/src/monsters.test.c new file mode 100644 index 000000000..89978bdcb --- /dev/null +++ b/src/monsters.test.c @@ -0,0 +1,273 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "monster.h" +#include "guard.h" +#include "skill.h" + +#include +#include + +#include +#include +#include +#include +#include + +extern void plan_monsters(struct faction *f); +extern int monster_attacks(unit * monster, bool respect_buildings, bool rich_only); + +static void init_language(void) +{ + locale* lang; + int i; + + lang = get_or_create_locale("de"); + locale_setstring(lang, "skill::unarmed", "Waffenloser Kampf"); + locale_setstring(lang, "keyword::attack", "ATTACKIERE"); + locale_setstring(lang, "keyword::study", "LERNE"); + locale_setstring(lang, "keyword::tax", "TREIBE"); + locale_setstring(lang, "keyword::loot", "PLUENDERE"); + locale_setstring(lang, "keyword::guard", "BEWACHE"); + locale_setstring(lang, "keyword::move", "NACH"); + locale_setstring(lang, "keyword::message", "BOTSCHAFT"); + locale_setstring(lang, "REGION", "REGION"); + locale_setstring(lang, "east", "O"); + + for (i = 0; i < MAXKEYWORDS; ++i) { + if (!locale_getstring(lang, mkname("keyword", keywords[i]))) + locale_setstring(lang, mkname("keyword", keywords[i]), keywords[i]); + } + for (i = 0; i < MAXSKILLS; ++i) { + if (!locale_getstring(lang, mkname("skill", skillnames[i]))) + locale_setstring(lang, mkname("skill", skillnames[i]), skillnames[i]); + } + init_keywords(lang); + init_skills(lang); +} + +static order *find_order(const char *expected, const unit *unit) +{ + char cmd[32]; + order *order; + for (order = unit->orders; order; order = order->next) { + if (strcmp(expected, get_command(order, cmd, sizeof(cmd))) == 0) { + return order; + } + } + return NULL; +} + +static void create_monsters(faction **player, faction **monsters, region **r, unit **u, unit **m) { + race* rc; + + test_cleanup(); + + init_language(); + + test_create_world(); + *player = test_create_faction(NULL); + *monsters = get_or_create_monsters(); + assert(rc_find((*monsters)->race->_name)); + rc = rc_get_or_create((*monsters)->race->_name); + fset(rc, RCF_UNARMEDGUARD); + fset(rc, RCF_NPC); + fset(*monsters, FFL_NOIDLEOUT); + assert(fval(*monsters, FFL_NPC) && fval((*monsters)->race, RCF_UNARMEDGUARD) && fval((*monsters)->race, RCF_NPC) && fval(*monsters, FFL_NOIDLEOUT)); + (*monsters)->locale = default_locale; + + *r = findregion(0, 0); + + *u = test_create_unit(*player, *r); + unit_setid(*u, 1); + *m = test_create_unit(*monsters, *r); +} + +static void test_monsters_attack(CuTest * tc) +{ + faction *f, *f2; + region *r; + unit *u, *m; + + create_monsters(&f, &f2, &r, &u, &m); + + guard(m, GUARD_TAX); + + config_set("rules.monsters.attack_chance", "1"); + + plan_monsters(f2); + + CuAssertPtrNotNull(tc, find_order("ATTACKIERE 1", m)); + test_cleanup(); +} + +static void test_monsters_attack_ocean(CuTest * tc) +{ + faction *f, *f2; + region *r; + unit *u, *m; + + create_monsters(&f, &f2, &r, &u, &m); + r = findregion(-1, 0); + u = test_create_unit(u->faction, r); + unit_setid(u, 2); + m = test_create_unit(m->faction, r); + assert(!m->region->land); + + config_set("rules.monsters.attack_chance", "1"); + + plan_monsters(f2); + + CuAssertPtrNotNull(tc, find_order("ATTACKIERE 2", m)); + test_cleanup(); +} + +static void test_monsters_waiting(CuTest * tc) +{ + faction *f, *f2; + region *r; + unit *u, *m; + + create_monsters(&f, &f2, &r, &u, &m); + guard(m, GUARD_TAX); + fset(m, UFL_ISNEW); + monster_attacks(m, false, false); + CuAssertPtrEquals(tc, 0, find_order("ATTACKIERE 1", m)); + test_cleanup(); +} + +static void test_seaserpent_attack(CuTest * tc) +{ + faction *f, *f2; + region *r; + unit *u, *m; + race *rc; + + create_monsters(&f, &f2, &r, &u, &m); + r = findregion(-1, 0); + u = test_create_unit(u->faction, r); + unit_setid(u, 2); + m = test_create_unit(m->faction, r); + u_setrace(m, rc = test_create_race("seaserpent")); + assert(!m->region->land); + fset(m, UFL_MOVED); + fset(rc, RCF_ATTACK_MOVED); + + config_set("rules.monsters.attack_chance", "1"); + + plan_monsters(f2); + + CuAssertPtrNotNull(tc, find_order("ATTACKIERE 2", m)); + test_cleanup(); +} + +static void test_monsters_attack_not(CuTest * tc) +{ + faction *f, *f2; + region *r; + unit *u, *m; + + create_monsters(&f, &f2, &r, &u, &m); + + guard(m, GUARD_TAX); + guard(u, GUARD_TAX); + + config_set("rules.monsters.attack_chance", "0"); + + plan_monsters(f2); + + CuAssertPtrEquals(tc, 0, find_order("ATTACKIERE 1", m)); + test_cleanup(); +} + +static void test_dragon_attacks_the_rich(CuTest * tc) +{ + faction *f, *f2; + region *r; + unit *u, *m; + const item_type *i_silver; + + init_language(); + create_monsters(&f, &f2, &r, &u, &m); + + guard(m, GUARD_TAX); + set_level(m, SK_WEAPONLESS, 10); + + rsetmoney(r, 1); + rsetmoney(findregion(1, 0), 0); + i_silver = it_find("money"); + assert(i_silver); + i_change(&u->items, i_silver, 5000); + + config_set("rules.monsters.attack_chance", "0.00001"); + + plan_monsters(f2); + + CuAssertPtrNotNull(tc, find_order("ATTACKIERE 1", m)); + CuAssertPtrNotNull(tc, find_order("PLUENDERE", m)); + test_cleanup(); +} + +static void test_dragon_moves(CuTest * tc) +{ + faction *f, *f2; + region *r; + unit *u, *m; + + create_monsters(&f, &f2, &r, &u, &m); + rsetpeasants(r, 0); + rsetmoney(r, 0); + rsetmoney(findregion(1, 0), 1000); + + set_level(m, SK_WEAPONLESS, 10); + config_set("rules.monsters.attack_chance", ".0"); + plan_monsters(f2); + + CuAssertPtrNotNull(tc, find_order("NACH O", m)); + test_cleanup(); +} + +static void test_monsters_learn_exp(CuTest * tc) +{ + faction *f, *f2; + region *r; + unit *u, *m; + skill* sk; + + create_monsters(&f, &f2, &r, &u, &m); + config_set("study.from_use", "1"); + + u_setrace(u, u_race(m)); + produceexp(u, SK_MELEE, u->number); + sk = unit_skill(u, SK_MELEE); + CuAssertTrue(tc, !sk); + + produceexp(m, SK_MELEE, u->number); + sk = unit_skill(m, SK_MELEE); + CuAssertTrue(tc, sk && (sk->level > 0 || (sk->level == 0 && sk->weeks > 0))); + + test_cleanup(); +} + +CuSuite *get_monsters_suite(void) +{ + CuSuite *suite = CuSuiteNew(); + SUITE_ADD_TEST(suite, test_monsters_attack); + SUITE_ADD_TEST(suite, test_monsters_attack_ocean); + SUITE_ADD_TEST(suite, test_seaserpent_attack); + SUITE_ADD_TEST(suite, test_monsters_waiting); + SUITE_ADD_TEST(suite, test_monsters_attack_not); + SUITE_ADD_TEST(suite, test_dragon_attacks_the_rich); + SUITE_ADD_TEST(suite, test_dragon_moves); + SUITE_ADD_TEST(suite, test_monsters_learn_exp); + return suite; +} diff --git a/src/piracy.c b/src/piracy.c index 8b215d718..312af6c4b 100644 --- a/src/piracy.c +++ b/src/piracy.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -65,6 +66,8 @@ static attrib *mk_piracy(const faction * pirate, const faction * target, } static bool validate_pirate(unit *u, order *ord) { + if (fval(u_race(u), RCF_SWIM | RCF_FLY)) + return true; if (!u->ship) { cmistake(u, ord, 144, MSG_MOVE); return false; diff --git a/src/piracy.test.c b/src/piracy.test.c index 4ed735c45..188ea1fed 100644 --- a/src/piracy.test.c +++ b/src/piracy.test.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -66,6 +67,7 @@ static void test_piracy_cmd(CuTest * tc) { } static void test_piracy_cmd_errors(CuTest * tc) { + race *r; faction *f; unit *u, *u2; order *ord; @@ -73,7 +75,8 @@ static void test_piracy_cmd_errors(CuTest * tc) { setup_piracy(); st_boat = st_get_or_create("boat"); - u = test_create_unit(f = test_create_faction(0), test_create_region(0, 0, get_or_create_terrain("ocean"))); + r = test_create_race("pirates"); + u = test_create_unit(f = test_create_faction(r), test_create_region(0, 0, get_or_create_terrain("ocean"))); f->locale = get_or_create_locale("de"); ord = create_order(K_PIRACY, f->locale, ""); assert(u && ord); @@ -81,6 +84,17 @@ static void test_piracy_cmd_errors(CuTest * tc) { piracy_cmd(u, ord); CuAssertPtrNotNullMsg(tc, "must be on a ship for PIRACY", test_find_messagetype(f->msgs, "error144")); + test_clear_messages(f); + fset(r, RCF_SWIM); + piracy_cmd(u, ord); + CuAssertPtrEquals_Msg(tc, "swimmers are pirates", 0, test_find_messagetype(f->msgs, "error144")); + CuAssertPtrEquals_Msg(tc, "swimmers are pirates", 0, test_find_messagetype(f->msgs, "error146")); + freset(r, RCF_SWIM); + fset(r, RCF_FLY); + CuAssertPtrEquals_Msg(tc, "flyers are pirates", 0, test_find_messagetype(f->msgs, "error144")); + freset(r, RCF_FLY); + test_clear_messages(f); + u_set_ship(u, test_create_ship(u->region, st_boat)); u2 = test_create_unit(u->faction, u->region); diff --git a/src/test_eressea.c b/src/test_eressea.c index 8f5c1faa0..f3d4b71ae 100644 --- a/src/test_eressea.c +++ b/src/test_eressea.c @@ -118,6 +118,7 @@ int RunAllTests(int argc, char *argv[]) ADD_SUITE(give); ADD_SUITE(laws); ADD_SUITE(market); + ADD_SUITE(monsters); ADD_SUITE(move); ADD_SUITE(piracy); ADD_SUITE(stealth); diff --git a/src/tests.c b/src/tests.c index d553255de..03d387914 100644 --- a/src/tests.c +++ b/src/tests.c @@ -236,10 +236,10 @@ void test_create_world(void) test_create_itemtype("iron"); test_create_itemtype("stone"); - t_plain = test_create_terrain("plain", LAND_REGION | FOREST_REGION | WALK_INTO | CAVALRY_REGION); + t_plain = test_create_terrain("plain", LAND_REGION | FOREST_REGION | WALK_INTO | CAVALRY_REGION | FLY_INTO); t_plain->size = 1000; t_plain->max_road = 100; - t_ocean = test_create_terrain("ocean", SEA_REGION | SAIL_INTO | SWIM_INTO); + t_ocean = test_create_terrain("ocean", SEA_REGION | SAIL_INTO | SWIM_INTO | FLY_INTO); t_ocean->size = 0; island[0] = test_create_region(0, 0, t_plain);