1 /*
2 Copyright © 2011-2012 Clint Bellanger
3 Copyright © 2012 Igor Paliychuk
4 Copyright © 2012 Stefan Beller
5 Copyright © 2013 Henrik Andersson
6 Copyright © 2012-2016 Justin Jacobs
7
8 This file is part of FLARE.
9
10 FLARE is free software: you can redistribute it and/or modify it under the terms
11 of the GNU General Public License as published by the Free Software Foundation,
12 either version 3 of the License, or (at your option) any later version.
13
14 FLARE is distributed in the hope that it will be useful, but WITHOUT ANY
15 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
16 PARTICULAR PURPOSE. See the GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License along with
19 FLARE. If not, see http://www.gnu.org/licenses/
20 */
21
22 /**
23 * class StatBlock
24 *
25 * Character stats and calculations
26 */
27
28 #include "Avatar.h"
29 #include "CampaignManager.h"
30 #include "CombatText.h"
31 #include "Entity.h"
32 #include "EntityManager.h"
33 #include "EngineSettings.h"
34 #include "FileParser.h"
35 #include "Hazard.h"
36 #include "LootManager.h"
37 #include "MapCollision.h"
38 #include "MapRenderer.h"
39 #include "MenuPowers.h"
40 #include "MessageEngine.h"
41 #include "PowerManager.h"
42 #include "Settings.h"
43 #include "SharedGameResources.h"
44 #include "SharedResources.h"
45 #include "StatBlock.h"
46 #include "UtilsMath.h"
47 #include "UtilsParsing.h"
48
49 #include <limits>
50
51 #include <math.h>
52 #ifndef M_SQRT2
53 #define M_SQRT2 sqrt(2.0)
54 #endif
55
56 const float StatBlock::DIRECTION_DELTA_X[8] = {-1, -1, -1, 0, 1, 1, 1, 0};
57 const float StatBlock::DIRECTION_DELTA_Y[8] = { 1, 0, -1, -1, -1, 0, 1, 1};
58 const float StatBlock::SPEED_MULTIPLIER[8] = { static_cast<float>(1.0/M_SQRT2), 1.0f, static_cast<float>(1.0/M_SQRT2), 1.0f, static_cast<float>(1.0/M_SQRT2), 1.0f, static_cast<float>(1.0/M_SQRT2), 1.0f};
59
StatBlock()60 StatBlock::StatBlock()
61 : statsLoaded(false)
62 , alive(true)
63 , corpse(false)
64 , corpse_timer()
65 , hero(false)
66 , hero_ally(false)
67 , enemy_ally(false)
68 , npc(false)
69 , humanoid(false)
70 , lifeform(true)
71 , permadeath(false)
72 , transformed(false)
73 , refresh_stats(false)
74 , converted(false)
75 , summoned(false)
76 , summoned_power_index(0)
77 , encountered(false)
78 , target_corpse(NULL) // hero only
79 , target_nearest(NULL) // hero only
80 , target_nearest_corpse(NULL) // hero only
81 , target_nearest_dist(0) // hero only
82 , target_nearest_corpse_dist(0) // hero only
83 , block_power(0)
84 , movement_type(MapCollision::MOVE_NORMAL)
85 , flying(false)
86 , intangible(false)
87 , facing(true)
88 , name("")
89 , level(0)
90 , xp(0)
91 , level_up(false)
92 , check_title(false)
93 , stat_points_per_level(1)
94 , power_points_per_level(1)
95 , starting(Stats::COUNT + eset->damage_types.count, 0)
96 , base(Stats::COUNT + eset->damage_types.count, 0)
97 , current(Stats::COUNT + eset->damage_types.count, 0)
98 , per_level(Stats::COUNT + eset->damage_types.count, 0)
99 , character_class("")
100 , character_subclass("")
101 , hp(0)
102 , hp_f(0)
103 , mp(0)
104 , mp_f(0)
105 , speed_default(0.1f)
106 , dmg_min_add(eset->damage_types.list.size(), 0)
107 , dmg_max_add(eset->damage_types.list.size(), 0)
108 , absorb_min_add(0)
109 , absorb_max_add(0)
110 , speed(0.1f)
111 , charge_speed(0.0f)
112 , vulnerable(eset->elements.list.size(), 100)
113 , vulnerable_base(eset->elements.list.size(), 100)
114 , transform_duration(0)
115 , transform_duration_total(0)
116 , manual_untransform(false)
117 , transform_with_equipment(false)
118 , untransform_on_hit(false)
119 , effects()
120 , blocking(false) // hero only
121 , pos()
122 , knockback_speed()
123 , knockback_srcpos()
124 , knockback_destpos()
125 , direction(0)
126 , cooldown_hit()
127 , cooldown_hit_enabled(false)
128 , cur_state(ENTITY_STANCE)
129 , state_timer()
130 , hold_state(false)
131 , prevent_interrupt(false)
132 , waypoints() // enemy only
133 , waypoint_timer(settings->max_frames_per_sec) // enemy only
134 , wander(false) // enemy only
135 , wander_area() // enemy only
136 , chance_pursue(0)
137 , chance_flee(0) // enemy only
138 , powers_list() // hero only
139 , powers_list_items() // hero only
140 , powers_passive()
141 , powers_ai() // enemy only
142 , melee_range(1.0f) //both
143 , threat_range(0) // enemy
144 , threat_range_far(0) // enemy
145 , flee_range(0) // enemy
146 , combat_style(COMBAT_DEFAULT)//enemy
147 , hero_stealth(0)
148 , turn_delay(0)
149 , in_combat(false) //enemy only
150 , join_combat(false)
151 , cooldown()
152 , activated_power(NULL) // enemy only
153 , half_dead_power(false) // enemy only
154 , suppress_hp(false)
155 , flee_timer(settings->max_frames_per_sec) // enemy only
156 , flee_cooldown_timer(settings->max_frames_per_sec) // enemy only
157 , perfect_accuracy(false)
158 , teleportation(false)
159 , teleport_destination()
160 , currency(0)
161 , death_penalty(false)
162 , defeat_status(0) // enemy only
163 , convert_status(0) // enemy only
164 , quest_loot_requires_status(0) // enemy only
165 , quest_loot_requires_not_status(0) // enemy only
166 , quest_loot_id(0) // enemy only
167 , first_defeat_loot(0) // enemy only
168 , gfx_base("male")
169 , gfx_head("head_short")
170 , gfx_portrait("")
171 , transform_type("")
172 , animations("")
173 , sfx_attack()
174 , sfx_step("")
175 , sfx_hit()
176 , sfx_die()
177 , sfx_critdie()
178 , sfx_block()
179 , sfx_levelup("")
180 , sfx_lowhp("")
181 , sfx_lowhp_loop(false)
182 , max_spendable_stat_points(0)
183 , max_points_per_stat(0)
184 , prev_maxhp(0)
185 , prev_maxmp(0)
186 , prev_hp(0)
187 , prev_mp(0)
188 , summons()
189 , summoner(NULL)
190 , abort_npc_interact(false)
191 {
192 primary.resize(eset->primary_stats.list.size(), 0);
193 primary_starting.resize(eset->primary_stats.list.size(), 0);
194 primary_additional.resize(eset->primary_stats.list.size(), 0);
195 per_primary.resize(eset->primary_stats.list.size());
196
197 for (size_t i = 0; i < per_primary.size(); ++i) {
198 per_primary[i].resize(Stats::COUNT + eset->damage_types.count, 0);
199 }
200
201 cooldown.reset(Timer::END);
202 }
203
~StatBlock()204 StatBlock::~StatBlock() {
205 removeFromSummons();
206 if (loot)
207 loot->removeFromEnemiesDroppingLoot(this);
208 }
209
loadCoreStat(FileParser * infile)210 bool StatBlock::loadCoreStat(FileParser *infile) {
211 // @CLASS StatBlock: Core stats|Description of engine/stats.txt and enemies in enemies/
212
213 if (infile->key == "speed") {
214 // @ATTR speed|float|Movement speed
215 float fvalue = Parse::toFloat(infile->val, 0);
216 speed = speed_default = fvalue / settings->max_frames_per_sec;
217 return true;
218 }
219 else if (infile->key == "cooldown") {
220 // @ATTR cooldown|int|Cooldown between attacks in 'ms' or 's'.
221 cooldown.setDuration(Parse::toDuration(infile->val));
222 return true;
223 }
224 else if (infile->key == "cooldown_hit") {
225 // @ATTR cooldown_hit|duration|Duration of cooldown after being hit in 'ms' or 's'.
226 cooldown_hit.setDuration(Parse::toDuration(infile->val));
227 cooldown_hit_enabled = true;
228 return true;
229 }
230 else if (infile->key == "stat") {
231 // @ATTR stat|string, int : Stat name, Value|The starting value for this stat.
232 std::string stat = Parse::popFirstString(infile->val);
233 int value = Parse::popFirstInt(infile->val);
234
235 for (int i=0; i<Stats::COUNT; ++i) {
236 if (Stats::KEY[i] == stat) {
237 starting[i] = value;
238 return true;
239 }
240 }
241
242 for (size_t i = 0; i < eset->damage_types.list.size(); ++i) {
243 if (eset->damage_types.list[i].min == stat) {
244 starting[Stats::COUNT + (i*2)] = value;
245 return true;
246 }
247 else if (eset->damage_types.list[i].max == stat) {
248 starting[Stats::COUNT + (i*2) + 1] = value;
249 return true;
250 }
251 }
252 }
253 else if (infile->key == "stat_per_level") {
254 // @ATTR stat_per_level|predefined_string, int : Stat name, Value|The value for this stat added per level.
255 std::string stat = Parse::popFirstString(infile->val);
256 int value = Parse::popFirstInt(infile->val);
257
258 for (int i=0; i<Stats::COUNT; i++) {
259 if (Stats::KEY[i] == stat) {
260 per_level[i] = value;
261 return true;
262 }
263 }
264
265 for (size_t i = 0; i < eset->damage_types.list.size(); ++i) {
266 if (eset->damage_types.list[i].min == stat) {
267 per_level[Stats::COUNT + (i*2)] = value;
268 return true;
269 }
270 else if (eset->damage_types.list[i].max == stat) {
271 per_level[Stats::COUNT + (i*2) + 1] = value;
272 return true;
273 }
274 }
275 }
276 else if (infile->key == "stat_per_primary") {
277 // @ATTR stat_per_primary|predefined_string, predefined_string, int : Primary Stat, Stat name, Value|The value for this stat added for every point allocated to this primary stat.
278 std::string prim_stat = Parse::popFirstString(infile->val);
279 size_t prim_stat_index = eset->primary_stats.getIndexByID(prim_stat);
280 if (prim_stat_index == eset->primary_stats.list.size()) {
281 infile->error("StatBlock: '%s' is not a valid primary stat.", prim_stat.c_str());
282 return true;
283 }
284
285 std::string stat = Parse::popFirstString(infile->val);
286 int value = Parse::popFirstInt(infile->val);
287
288 for (int i=0; i<Stats::COUNT; i++) {
289 if (Stats::KEY[i] == stat) {
290 per_primary[prim_stat_index][i] = value;
291 return true;
292 }
293 }
294
295 for (size_t i = 0; i < eset->damage_types.list.size(); ++i) {
296 if (eset->damage_types.list[i].min == stat) {
297 per_primary[prim_stat_index][Stats::COUNT + (i*2)] = value;
298 return true;
299 }
300 else if (eset->damage_types.list[i].max == stat) {
301 per_primary[prim_stat_index][Stats::COUNT + (i*2) + 1] = value;
302 return true;
303 }
304 }
305 }
306 else if (infile->key == "vulnerable") {
307 // @ATTR vulnerable|predefined_string, int : Element, Value|Percentage weakness to this element.
308 std::string element = Parse::popFirstString(infile->val);
309 int value = Parse::popFirstInt(infile->val);
310
311 for (unsigned int i=0; i<eset->elements.list.size(); i++) {
312 if (element == eset->elements.list[i].id) {
313 vulnerable[i] = vulnerable_base[i] = value;
314 return true;
315 }
316 }
317 }
318 else if (infile->key == "power_filter") {
319 // @ATTR power_filter|list(power_id)|Only these powers are allowed to hit this entity.
320 std::string power_id = Parse::popFirstString(infile->val);
321 while (!power_id.empty()) {
322 power_filter.push_back(Parse::toPowerID(power_id));
323 power_id = Parse::popFirstString(infile->val);
324 }
325 return true;
326 }
327 else if (infile->key == "categories") {
328 // @ATTR categories|list(string)|Categories that this entity belongs to.
329 categories.clear();
330 std::string cat;
331 while ((cat = Parse::popFirstString(infile->val)) != "") {
332 categories.push_back(cat);
333 }
334 return true;
335 }
336 else if (infile->key == "melee_range") {
337 // @ATTR melee_range|float|Determines the distance from the caster that some powers will be placed. For AI entities, it also means the minimum distance from target required to use melee powers.
338 melee_range = Parse::toFloat(infile->val);
339 return true;
340 }
341
342 return false;
343 }
344
345 /**
346 * Set paths for sound effects
347 */
loadSfxStat(FileParser * infile)348 bool StatBlock::loadSfxStat(FileParser *infile) {
349 // @CLASS StatBlock: Sound effects|Description of heroes in engine/avatar/ and enemies in enemies/
350
351 if (infile->new_section) {
352 sfx_attack.clear();
353 sfx_hit.clear();
354 sfx_die.clear();
355 sfx_critdie.clear();
356 sfx_block.clear();
357 }
358
359 if (infile->key == "sfx_attack") {
360 // @ATTR sfx_attack|repeatable(predefined_string, filename) : Animation name, Sound file|Filename of sound effect for the specified attack animation.
361 std::string anim_name = Parse::popFirstString(infile->val);
362 std::string filename = Parse::popFirstString(infile->val);
363
364 size_t found_index = sfx_attack.size();
365 for (size_t i = 0; i < sfx_attack.size(); ++i) {
366 if (anim_name == sfx_attack[i].first) {
367 found_index = i;
368 break;
369 }
370 }
371
372 if (found_index == sfx_attack.size()) {
373 sfx_attack.push_back(std::pair<std::string, std::vector<std::string> >());
374 sfx_attack.back().first = anim_name;
375 sfx_attack.back().second.push_back(filename);
376 }
377 else {
378 if (std::find(sfx_attack[found_index].second.begin(), sfx_attack[found_index].second.end(), filename) == sfx_attack[found_index].second.end()) {
379 sfx_attack[found_index].second.push_back(filename);
380 }
381 }
382 }
383 else if (infile->key == "sfx_hit") {
384 // @ATTR sfx_hit|repeatable(filename)|Filename of sound effect for being hit.
385 if (std::find(sfx_hit.begin(), sfx_hit.end(), infile->val) == sfx_hit.end()) {
386 sfx_hit.push_back(infile->val);
387 }
388 }
389 else if (infile->key == "sfx_die") {
390 // @ATTR sfx_die|repeatable(filename)|Filename of sound effect for dying.
391 if (std::find(sfx_die.begin(), sfx_die.end(), infile->val) == sfx_die.end()) {
392 sfx_die.push_back(infile->val);
393 }
394 }
395 else if (infile->key == "sfx_critdie") {
396 // @ATTR sfx_critdie|repeatable(filename)|Filename of sound effect for dying to a critical hit.
397 if (std::find(sfx_critdie.begin(), sfx_critdie.end(), infile->val) == sfx_critdie.end()) {
398 sfx_critdie.push_back(infile->val);
399 }
400 }
401 else if (infile->key == "sfx_block") {
402 // @ATTR sfx_block|repeatable(filename)|Filename of sound effect for blocking an incoming hit.
403 if (std::find(sfx_block.begin(), sfx_block.end(), infile->val) == sfx_block.end()) {
404 sfx_block.push_back(infile->val);
405 }
406 }
407 else if (infile->key == "sfx_levelup") {
408 // @ATTR sfx_levelup|filename|Filename of sound effect for leveling up.
409 sfx_levelup = infile->val;
410 }
411 else if (infile->key == "sfx_lowhp") {
412 // @ATTR sfx_lowhp|filename, bool: Sound file, loop|Filename of sound effect for low health warning. Optionally, it can be looped.
413 sfx_lowhp = Parse::popFirstString(infile->val);
414 if (infile->val != "") sfx_lowhp_loop = Parse::toBool(infile->val);
415 }
416 else {
417 return false;
418 }
419
420 return true;
421 }
422
isNPCStat(FileParser * infile)423 bool StatBlock::isNPCStat(FileParser *infile) {
424 if (infile->section == "npc") return true;
425 else if (infile->section == "dialog") return true;
426
427 if (infile->key == "gfx") {
428 infile->error("StatBlock: Warning! 'gfx' is deprecated. Use 'animations' instead.");
429 animations = infile->val;
430 return true;
431 }
432 else if (infile->key == "direction") return true;
433 else if (infile->key == "talker") return true;
434 else if (infile->key == "portrait") return true;
435 else if (infile->key == "vendor") return true;
436 else if (infile->key == "vendor_requires_status") return true;
437 else if (infile->key == "vendor_requires_not_status") return true;
438 else if (infile->key == "constant_stock") return true;
439 else if (infile->key == "status_stock") return true;
440 else if (infile->key == "random_stock") return true;
441 else if (infile->key == "random_stock_count") return true;
442 else if (infile->key == "vox_intro") return true;
443
444 return false;
445 }
446
447 /**
448 * load a statblock, typically for an enemy definition
449 */
load(const std::string & filename)450 void StatBlock::load(const std::string& filename) {
451 // @CLASS StatBlock: Enemies|Description of enemies in enemies/
452 FileParser infile;
453 if (!infile.open(filename, FileParser::MOD_FILE, FileParser::ERROR_NORMAL))
454 return;
455
456 bool clear_loot = true;
457 bool flee_range_defined = false;
458
459 while (infile.next()) {
460 if (infile.new_section) {
461 // APPENDed file
462 clear_loot = true;
463 }
464
465 int num = Parse::toInt(infile.val);
466 float fnum = Parse::toFloat(infile.val);
467 bool valid = loadCoreStat(&infile) || loadSfxStat(&infile) || isNPCStat(&infile);
468
469 // @ATTR name|string|Name
470 if (infile.key == "name") name = msg->get(infile.val);
471 // @ATTR humanoid|bool|This creature gives human traits when transformed into, such as the ability to talk with NPCs.
472 else if (infile.key == "humanoid") humanoid = Parse::toBool(infile.val);
473 // @ATTR lifeform|bool|Determines whether or not this entity is referred to as a living thing, such as displaying "Dead" vs "Destroyed" when their HP is 0.
474 else if (infile.key == "lifeform") lifeform = Parse::toBool(infile.val);
475
476 // @ATTR level|int|Level
477 else if (infile.key == "level") level = num;
478
479 // enemy death rewards and events
480 // @ATTR xp|int|XP awarded upon death.
481 else if (infile.key == "xp") xp = num;
482 else if (infile.key == "loot") {
483 // @ATTR loot|repeatable(loot)|Possible loot that can be dropped on death.
484
485 // loot entries format:
486 // loot=[id],[percent_chance]
487 // optionally allow range:
488 // loot=[id],[percent_chance],[count_min],[count_max]
489
490 if (clear_loot) {
491 loot_table.clear();
492 clear_loot = false;
493 }
494
495 loot_table.push_back(EventComponent());
496 loot->parseLoot(infile.val, &loot_table.back(), &loot_table);
497 }
498 else if (infile.key == "loot_count") {
499 // @ATTR loot_count|int, int : Min, Max|Sets the minimum (and optionally, the maximum) amount of loot this creature can drop. Overrides the global drop_max setting.
500 loot_count.x = Parse::popFirstInt(infile.val);
501 loot_count.y = Parse::popFirstInt(infile.val);
502 if (loot_count.x != 0 || loot_count.y != 0) {
503 loot_count.x = std::max(loot_count.x, 1);
504 loot_count.y = std::max(loot_count.y, loot_count.x);
505 }
506 }
507 // @ATTR defeat_status|string|Campaign status to set upon death.
508 else if (infile.key == "defeat_status") defeat_status = camp->registerStatus(infile.val);
509 // @ATTR convert_status|string|Campaign status to set upon being converted to a player ally.
510 else if (infile.key == "convert_status") convert_status = camp->registerStatus(infile.val);
511 // @ATTR first_defeat_loot|item_id|Drops this item upon first death.
512 else if (infile.key == "first_defeat_loot") first_defeat_loot = Parse::toItemID(infile.val);
513 // @ATTR quest_loot|string, string, item_id : Required status, Required not status, Item|Drops this item when campaign status is met.
514 else if (infile.key == "quest_loot") {
515 quest_loot_requires_status = camp->registerStatus(Parse::popFirstString(infile.val));
516 quest_loot_requires_not_status = camp->registerStatus(Parse::popFirstString(infile.val));
517 quest_loot_id = Parse::toItemID(Parse::popFirstString(infile.val));
518 }
519
520 // behavior stats
521 // @ATTR flying|bool|Creature can move over gaps/water.
522 else if (infile.key == "flying") flying = Parse::toBool(infile.val);
523 // @ATTR intangible|bool|Creature can move through walls.
524 else if (infile.key == "intangible") intangible = Parse::toBool(infile.val);
525 // @ATTR facing|bool|Creature can turn to face their target.
526 else if (infile.key == "facing") facing = Parse::toBool(infile.val);
527
528 // @ATTR waypoint_pause|duration|Duration to wait at each waypoint in 'ms' or 's'.
529 else if (infile.key == "waypoint_pause") waypoint_timer.setDuration(Parse::toDuration(infile.val));
530
531 // @ATTR turn_delay|duration|Duration it takes for this creature to turn and face their target in 'ms' or 's'.
532 else if (infile.key == "turn_delay") turn_delay = Parse::toDuration(infile.val);
533 // @ATTR chance_pursue|int|Percentage change that the creature will chase their target.
534 else if (infile.key == "chance_pursue") chance_pursue = num;
535 // @ATTR chance_flee|int|Percentage chance that the creature will run away from their target.
536 else if (infile.key == "chance_flee") chance_flee = num;
537
538 else if (infile.key == "power") {
539 // @ATTR power|["melee", "ranged", "beacon", "on_hit", "on_death", "on_half_dead", "on_join_combat", "on_debuff"], power_id, int : State, Power, Chance|A power that has a chance of being triggered in a certain state.
540 AIPower ai_power;
541
542 std::string ai_type = Parse::popFirstString(infile.val);
543
544 ai_power.id = powers->verifyID(Parse::toPowerID(Parse::popFirstString(infile.val)), &infile, !PowerManager::ALLOW_ZERO_ID);
545 if (ai_power.id == 0)
546 continue; // verifyID() will print our error message
547
548 ai_power.chance = Parse::popFirstInt(infile.val);
549
550 if (ai_type == "melee") ai_power.type = AI_POWER_MELEE;
551 else if (ai_type == "ranged") ai_power.type = AI_POWER_RANGED;
552 else if (ai_type == "beacon") ai_power.type = AI_POWER_BEACON;
553 else if (ai_type == "on_hit") ai_power.type = AI_POWER_HIT;
554 else if (ai_type == "on_death") ai_power.type = AI_POWER_DEATH;
555 else if (ai_type == "on_half_dead") ai_power.type = AI_POWER_HALF_DEAD;
556 else if (ai_type == "on_join_combat") ai_power.type = AI_POWER_JOIN_COMBAT;
557 else if (ai_type == "on_debuff") ai_power.type = AI_POWER_DEBUFF;
558 else {
559 infile.error("StatBlock: '%s' is not a valid enemy power type.", ai_type.c_str());
560 continue;
561 }
562
563 if (ai_power.type == AI_POWER_HALF_DEAD)
564 half_dead_power = true;
565
566 powers_ai.push_back(ai_power);
567 }
568
569 else if (infile.key == "passive_powers") {
570 // @ATTR passive_powers|list(power_id)|A list of passive powers this creature has.
571 powers_passive.clear();
572 std::string p = Parse::popFirstString(infile.val);
573 while (p != "") {
574 powers_passive.push_back(Parse::toPowerID(p));
575 p = Parse::popFirstString(infile.val);
576
577 // if a passive power has a post power, add it to the AI power list so we can track its cooldown
578 PowerID post_power = powers->powers[powers_passive.back()].post_power;
579 if (post_power > 0) {
580 AIPower passive_post_power;
581 passive_post_power.type = AI_POWER_PASSIVE_POST;
582 passive_post_power.id = post_power;
583 passive_post_power.chance = 0; // post_power chance is used instead
584 powers_ai.push_back(passive_post_power);
585 }
586 }
587 }
588
589 // @ATTR threat_range|float, float: Engage distance, Stop distance|The first value is the radius of the area this creature will be able to start chasing the hero. The second, optional, value is the radius at which this creature will stop pursuing their target and defaults to double the first value.
590 else if (infile.key == "threat_range") {
591 threat_range = Parse::toFloat(Parse::popFirstString(infile.val));
592
593 std::string tr_far = Parse::popFirstString(infile.val);
594 if (!tr_far.empty())
595 threat_range_far = Parse::toFloat(tr_far);
596 else
597 threat_range_far = threat_range * 2;
598 }
599 // @ATTR flee_range|float|The radius at which this creature will start moving to a safe distance. Defaults to half of the threat_range.
600 else if (infile.key == "flee_range") {
601 flee_range = fnum;
602 flee_range_defined = true;
603 }
604 // @ATTR combat_style|["default", "aggressive", "passive"]|How the creature will enter combat. Default is within range of the hero; Aggressive is always in combat; Passive must be attacked to enter combat.
605 else if (infile.key == "combat_style") {
606 if (infile.val == "default") combat_style = COMBAT_DEFAULT;
607 else if (infile.val == "aggressive") combat_style = COMBAT_AGGRESSIVE;
608 else if (infile.val == "passive") combat_style = COMBAT_PASSIVE;
609 else infile.error("StatBlock: Unknown combat style '%s'", infile.val.c_str());
610 }
611
612 // @ATTR animations|filename|Filename of an animation definition.
613 else if (infile.key == "animations") animations = infile.val;
614
615 // @ATTR suppress_hp|bool|Hides the enemy HP bar for this creature.
616 else if (infile.key == "suppress_hp") suppress_hp = Parse::toBool(infile.val);
617
618 // @ATTR flee_duration|duration|The minimum amount of time that this creature will flee. They may flee longer than the specified time.
619 else if (infile.key == "flee_duration") flee_timer.setDuration(Parse::toDuration(infile.val));
620 // @ATTR flee_cooldown|duration|The amount of time this creature must wait before they can start fleeing again.
621 else if (infile.key == "flee_cooldown") flee_cooldown_timer.setDuration(Parse::toDuration(infile.val));
622
623 // this is only used for EnemyGroupManager
624 // we check for them here so that we don't get an error saying they are invalid
625 else if (infile.key == "rarity") ; // but do nothing
626
627 else if (!valid) {
628 infile.error("StatBlock: '%s' is not a valid key.", infile.key.c_str());
629 }
630 }
631 infile.close();
632
633 hp = starting[Stats::HP_MAX];
634 mp = starting[Stats::MP_MAX];
635
636 if (!flee_range_defined)
637 flee_range = threat_range / 2;
638
639 applyEffects();
640 }
641
642 /**
643 * Reduce temphp first, then hp
644 */
takeDamage(int dmg,bool crit,int source_type)645 void StatBlock::takeDamage(int dmg, bool crit, int source_type) {
646 hp -= effects.damageShields(dmg);
647 if (hp <= 0) {
648 hp = 0;
649
650 effects.triggered_death = true;
651 // TODO should effects.clearEffects() be here as well?
652 // what about other things that happen in the "dead" entity states?
653
654 if (hero) {
655 cur_state = StatBlock::ENTITY_DEAD;
656 }
657 else {
658 // enemy died; do rewards
659 if (!hero_ally || converted) {
660 // some creatures create special loot if we're on a quest
661 if (quest_loot_requires_status != 0) {
662 // the loot manager will check quest_loot_id
663 // if set (not zero), the loot manager will 100% generate that loot.
664 if (!(camp->checkStatus(quest_loot_requires_status) && !camp->checkStatus(quest_loot_requires_not_status))) {
665 quest_loot_id = 0;
666 }
667 }
668
669 // some creatures drop special loot the first time they are defeated
670 // this must be done in conjunction with defeat status
671 if (first_defeat_loot > 0) {
672 if (!camp->checkStatus(defeat_status)) {
673 quest_loot_id = first_defeat_loot;
674 }
675 }
676
677 // defeating some creatures (e.g. bosses) affects the story
678 if (defeat_status != 0) {
679 camp->setStatus(defeat_status);
680 }
681
682 // reward XP; adjust for party exp if necessary
683 float xp_multiplier = 1;
684 if (source_type == Power::SOURCE_TYPE_ALLY)
685 xp_multiplier = static_cast<float>(eset->misc.party_exp_percentage) / 100.0f;
686
687 camp->rewardXP(static_cast<int>((static_cast<float>(xp) * xp_multiplier)), !CampaignManager::XP_SHOW_MSG);
688
689 // drop loot
690 loot->addEnemyLoot(this);
691 }
692
693 if (crit)
694 cur_state = StatBlock::ENTITY_CRITDEAD;
695 else
696 cur_state = StatBlock::ENTITY_DEAD;
697
698 mapr->collider.unblock(pos.x, pos.y);
699 }
700
701 }
702 }
703
704 /**
705 * Recalc level and stats
706 * Refill HP/MP
707 * Creatures might skip these formulas.
708 */
recalc()709 void StatBlock::recalc() {
710
711 if (hero) {
712 if (!statsLoaded) loadHeroStats();
713
714 refresh_stats = true;
715
716 unsigned long xp_max = eset->xp.getLevelXP(eset->xp.getMaxLevel());
717 xp = std::min(xp, xp_max);
718
719 level = eset->xp.getLevelFromXP(xp);
720 if (level != 0)
721 check_title = true;
722 }
723
724 if (level < 1)
725 level = 1;
726
727 applyEffects();
728
729 hp = get(Stats::HP_MAX);
730 mp = get(Stats::MP_MAX);
731 }
732
733 /**
734 * Base damage and absorb is 0
735 * Plus an optional bonus_per_[base stat]
736 */
calcBase()737 void StatBlock::calcBase() {
738 // bonuses are skipped for the default level 1 of a stat
739 const int lev0 = std::max(level - 1, 0);
740
741 if (per_primary.empty()) {
742 for (size_t i = 0; i < Stats::COUNT + eset->damage_types.count; ++i) {
743 base[i] = starting[i] + (lev0 * per_level[i]);
744 }
745 }
746 else {
747 for (size_t j = 0; j < per_primary.size(); ++j) {
748 const int current_primary = std::max(get_primary(j) - 1, 0);
749 const std::vector<int>& per_primary_vec = per_primary[j];
750 for (size_t i = 0; i < Stats::COUNT + eset->damage_types.count; ++i) {
751 if (j==0)
752 base[i] = starting[i] + (lev0 * per_level[i]);
753 base[i] += (current_primary * per_primary_vec[i]);
754 }
755 }
756 }
757
758 // add damage from equipment and increase to minimum amounts
759 for (size_t i = 0; i < eset->damage_types.list.size(); ++i) {
760 base[Stats::COUNT + (i*2)] += dmg_min_add[i];
761 base[Stats::COUNT + (i*2) + 1] += dmg_max_add[i];
762 base[Stats::COUNT + (i*2)] = std::max(base[Stats::COUNT + (i*2)], 0);
763 base[Stats::COUNT + (i*2) + 1] = std::max(base[Stats::COUNT + (i*2) + 1], base[Stats::COUNT + (i*2)]);
764 }
765
766 // add absorb from equipment and increase to minimum amounts
767 base[Stats::ABS_MIN] += absorb_min_add;
768 base[Stats::ABS_MAX] += absorb_max_add;
769 base[Stats::ABS_MIN] = std::max(base[Stats::ABS_MIN], 0);
770 base[Stats::ABS_MAX] = std::max(base[Stats::ABS_MAX], base[Stats::ABS_MIN]);
771 }
772
773 /**
774 * Recalc derived stats from base stats + effect bonuses
775 */
applyEffects()776 void StatBlock::applyEffects() {
777 // preserve hp/mp states
778 // max HP and MP can't drop below 1
779 prev_maxhp = std::max(get(Stats::HP_MAX), 1);
780 prev_maxmp = std::max(get(Stats::MP_MAX), 1);
781 prev_hp = hp;
782 prev_mp = mp;
783
784 // calculate primary stats
785 // refresh the character menu if there has been a change
786 for (size_t i = 0; i < primary.size(); ++i) {
787 if (get_primary(i) != primary[i] + effects.bonus_primary[i])
788 refresh_stats = true;
789
790 primary_additional[i] = effects.bonus_primary[i];
791 }
792
793 calcBase();
794
795 for (size_t i = 0; i < Stats::COUNT + eset->damage_types.count; ++i) {
796 current[i] = base[i] + effects.bonus[i];
797 }
798
799 for (size_t i = 0; i < effects.bonus_resist.size(); ++i) {
800 vulnerable[i] = vulnerable_base[i] - effects.bonus_resist[i];
801 }
802
803 current[Stats::HP_MAX] += (current[Stats::HP_MAX] * current[Stats::HP_PERCENT]) / 100;
804 current[Stats::MP_MAX] += (current[Stats::MP_MAX] * current[Stats::MP_PERCENT]) / 100;
805
806 // max HP and MP can't drop below 1
807 current[Stats::HP_MAX] = std::max(get(Stats::HP_MAX), 1);
808 current[Stats::MP_MAX] = std::max(get(Stats::MP_MAX), 1);
809
810 if (hp > get(Stats::HP_MAX)) hp = get(Stats::HP_MAX);
811 if (mp > get(Stats::MP_MAX)) mp = get(Stats::MP_MAX);
812
813 speed = speed_default;
814 }
815
816 /**
817 * Process per-frame actions
818 */
logic()819 void StatBlock::logic() {
820 alive = !(hp <= 0 && !effects.triggered_death && !effects.revive);
821
822 // handle party buffs
823 if (entitym && powers) {
824 while (!party_buffs.empty()) {
825 PowerID power_index = party_buffs.front();
826 party_buffs.pop();
827 Power *buff_power = &powers->powers[power_index];
828
829 for (size_t i=0; i < entitym->entities.size(); ++i) {
830 Entity* party_member = entitym->entities[i];
831 if(party_member->stats.hp > 0 &&
832 ((party_member->stats.hero_ally && hero) || (party_member->stats.enemy_ally && party_member->stats.summoner == this)) &&
833 (buff_power->buff_party_power_id == 0 || buff_power->buff_party_power_id == party_member->stats.summoned_power_index)
834 ) {
835 powers->effect(&(party_member->stats), this, power_index, (hero ? Power::SOURCE_TYPE_HERO : Power::SOURCE_TYPE_ENEMY));
836 }
837 }
838 }
839 }
840
841 // handle effect timers
842 effects.logic();
843
844 // apply bonuses from items/effects to base stats
845 applyEffects();
846
847 if (hero && effects.refresh_stats) {
848 refresh_stats = true;
849 effects.refresh_stats = false;
850 }
851
852 // preserve ratio on maxmp and maxhp changes
853 float ratio;
854 if (prev_maxhp != get(Stats::HP_MAX)) {
855 ratio = static_cast<float>(prev_hp) / static_cast<float>(prev_maxhp);
856 hp = static_cast<int>(ratio * static_cast<float>(get(Stats::HP_MAX)));
857 }
858 if (prev_maxmp != get(Stats::MP_MAX)) {
859 ratio = static_cast<float>(prev_mp) / static_cast<float>(prev_maxmp);
860 mp = static_cast<int>(ratio * static_cast<float>(get(Stats::MP_MAX)));
861 }
862
863 // handle cooldowns
864 cooldown.tick(); // global cooldown
865
866 for (size_t i=0; i<powers_ai.size(); ++i) { // NPC/enemy powerslot cooldown
867 powers_ai[i].cooldown.tick();
868 }
869
870 // sync hp/mp with their floating point counterparts
871 if (static_cast<int>(hp_f) != hp)
872 hp_f = static_cast<float>(hp);
873 if (static_cast<int>(mp_f) != mp)
874 mp_f = static_cast<float>(mp);
875
876 // HP regen
877 if (hp < get(Stats::HP_MAX) && hp > 0) {
878 float hp_regen_per_frame;
879 if (!in_combat && !hero_ally && !hero && pc->stats.alive) {
880 // enemies heal rapidly (full heal in 5 seconds) while not in combat
881 hp_regen_per_frame = static_cast<float>(get(Stats::HP_MAX)) / 5.f / settings->max_frames_per_sec;
882 }
883 else {
884 hp_regen_per_frame = static_cast<float>(get(Stats::HP_REGEN)) / 60.f / settings->max_frames_per_sec;
885 }
886 hp_f += hp_regen_per_frame;
887 hp = std::min(static_cast<int>(hp_f), get(Stats::HP_MAX));
888 }
889
890 // MP regen
891 if (mp < get(Stats::MP_MAX) && hp > 0) {
892 float mp_regen_per_frame = static_cast<float>(get(Stats::MP_REGEN)) / 60.f / settings->max_frames_per_sec;
893 mp_f += mp_regen_per_frame;
894 mp = std::min(static_cast<int>(mp_f), get(Stats::MP_MAX));
895 }
896
897 // handle buff/debuff durations
898 if (transform_duration > 0)
899 transform_duration--;
900
901 // apply bleed
902 if (effects.damage > 0 && hp > 0) {
903 takeDamage(effects.damage, !StatBlock::TAKE_DMG_CRIT, effects.getDamageSourceType(Effect::DAMAGE));
904 comb->addInt(effects.damage, pos, CombatText::MSG_TAKEDMG);
905 }
906 if (effects.damage_percent > 0 && hp > 0) {
907 int damage = (get(Stats::HP_MAX)*effects.damage_percent)/100;
908 takeDamage(damage, !StatBlock::TAKE_DMG_CRIT, effects.getDamageSourceType(Effect::DAMAGE_PERCENT));
909 comb->addInt(damage, pos, CombatText::MSG_TAKEDMG);
910 }
911
912 if(effects.death_sentence)
913 takeDamage(get(Stats::HP_MAX), !StatBlock::TAKE_DMG_CRIT, Power::SOURCE_TYPE_NEUTRAL);
914
915 cooldown_hit.tick();
916
917 if (effects.stun) {
918 // stun stops charge attacks
919 state_timer.reset(Timer::END);
920 charge_speed = 0;
921 }
922
923 state_timer.tick();
924
925 // apply healing over time
926 if (effects.hpot > 0) {
927 comb->addString(msg->get("+%d HP",effects.hpot), pos, CombatText::MSG_BUFF);
928 hp += effects.hpot;
929 if (hp > get(Stats::HP_MAX)) hp = get(Stats::HP_MAX);
930 }
931 if (effects.hpot_percent > 0) {
932 int hpot = (get(Stats::HP_MAX)*effects.hpot_percent)/100;
933 comb->addString(msg->get("+%d HP",hpot), pos, CombatText::MSG_BUFF);
934 hp += hpot;
935 if (hp > get(Stats::HP_MAX)) hp = get(Stats::HP_MAX);
936 }
937 if (effects.mpot > 0) {
938 comb->addString(msg->get("+%d MP",effects.mpot), pos, CombatText::MSG_BUFF);
939 mp += effects.mpot;
940 if (mp > get(Stats::MP_MAX)) mp = get(Stats::MP_MAX);
941 }
942 if (effects.mpot_percent > 0) {
943 int mpot = (get(Stats::MP_MAX)*effects.mpot_percent)/100;
944 comb->addString(msg->get("+%d MP",mpot), pos, CombatText::MSG_BUFF);
945 mp += mpot;
946 if (mp > get(Stats::MP_MAX)) mp = get(Stats::MP_MAX);
947 }
948
949 // set movement type
950 // some creatures may shift between movement types
951 if (intangible) movement_type = MapCollision::MOVE_INTANGIBLE;
952 else if (flying) movement_type = MapCollision::MOVE_FLYING;
953 else movement_type = MapCollision::MOVE_NORMAL;
954
955 if (hp == 0)
956 removeSummons();
957
958 if (effects.knockback_speed != 0) {
959 float theta = Utils::calcTheta(knockback_srcpos.x, knockback_srcpos.y, knockback_destpos.x, knockback_destpos.y);
960 knockback_speed.x = effects.knockback_speed * cosf(theta);
961 knockback_speed.y = effects.knockback_speed * sinf(theta);
962
963 mapr->collider.unblock(pos.x, pos.y);
964 mapr->collider.move(pos.x, pos.y, knockback_speed.x, knockback_speed.y, movement_type, mapr->collider.getCollideType(hero));
965 mapr->collider.block(pos.x, pos.y, hero_ally);
966 }
967 else if (charge_speed != 0.0f) {
968 float tmp_speed = charge_speed * SPEED_MULTIPLIER[direction];
969 float dx = tmp_speed * DIRECTION_DELTA_X[direction];
970 float dy = tmp_speed * DIRECTION_DELTA_Y[direction];
971
972 mapr->collider.unblock(pos.x, pos.y);
973 mapr->collider.move(pos.x, pos.y, dx, dy, movement_type, mapr->collider.getCollideType(hero));
974 mapr->collider.block(pos.x, pos.y, hero_ally);
975 }
976
977 waypoint_timer.tick();
978
979 // check for revive
980 if (hp <= 0 && effects.revive) {
981 hp = get(Stats::HP_MAX);
982 alive = true;
983 corpse = false;
984 cur_state = ENTITY_STANCE;
985 }
986
987 // non-hero entities can have their disposition reversed
988 if (!hero && effects.convert != converted) {
989 converted = !converted;
990 hero_ally = !hero_ally;
991 if (convert_status != 0) {
992 camp->setStatus(convert_status);
993 }
994 }
995 }
996
canUsePower(PowerID powerid,bool allow_passive) const997 bool StatBlock::canUsePower(PowerID powerid, bool allow_passive) const {
998 const Power& power = powers->powers[powerid];
999
1000 if (!alive) {
1001 // can't use powers when dead
1002 return false;
1003 }
1004 else if (!hero) {
1005 // AI can always use their powers
1006 return true;
1007 }
1008 else if (transformed) {
1009 // needed to unlock shapeshifter powers
1010 return mp >= power.requires_mp;
1011 }
1012 else {
1013 return (
1014 mp >= power.requires_mp
1015 && (!power.passive || allow_passive)
1016 && !power.meta_power
1017 && (!effects.stun || (allow_passive && power.passive))
1018 && (power.sacrifice || hp > power.requires_hp)
1019 && powers->checkRequiredMaxHPMP(power, this)
1020 && (!power.requires_corpse || (target_corpse && !target_corpse->corpse_timer.isEnd()) || (target_nearest_corpse && powers->checkNearestTargeting(power, this, true) && !target_nearest_corpse->corpse_timer.isEnd()))
1021 && (checkRequiredSpawns(power.requires_spawns))
1022 && (menu_powers && menu_powers->meetsUsageStats(powerid))
1023 && (power.type == Power::TYPE_SPAWN ? !summonLimitReached(powerid) : true)
1024 && !(power.spawn_type == "untransform" && !transformed)
1025 && std::includes(equip_flags.begin(), equip_flags.end(), power.requires_flags.begin(), power.requires_flags.end())
1026 && (!power.buff_party || (power.buff_party && entitym && entitym->checkPartyMembers()))
1027 && powers->checkRequiredItems(power, this)
1028 );
1029 }
1030
1031 }
1032
loadHeroStats()1033 void StatBlock::loadHeroStats() {
1034 // set the default global cooldown
1035 cooldown.setDuration(Parse::toDuration("66ms"));
1036
1037 // Redefine numbers from config file if present
1038 FileParser infile;
1039 // @CLASS StatBlock: Hero stats|Description of engine/stats.txt
1040 if (infile.open("engine/stats.txt", FileParser::MOD_FILE, FileParser::ERROR_NORMAL)) {
1041 while (infile.next()) {
1042 int value = Parse::toInt(infile.val);
1043
1044 bool valid = loadCoreStat(&infile);
1045
1046 if (infile.key == "max_points_per_stat") {
1047 // @ATTR max_points_per_stat|int|Maximum points for each primary stat.
1048 max_points_per_stat = value;
1049 }
1050 else if (infile.key == "sfx_step") {
1051 // @ATTR sfx_step|string|An id for a set of step sound effects. See items/step_sounds.txt.
1052 sfx_step = infile.val;
1053 }
1054 else if (infile.key == "stat_points_per_level") {
1055 // @ATTR stat_points_per_level|int|The amount of stat points awarded each level.
1056 stat_points_per_level = value;
1057 }
1058 else if (infile.key == "power_points_per_level") {
1059 // @ATTR power_points_per_level|int|The amount of power points awarded each level.
1060 power_points_per_level = value;
1061 }
1062 else if (!valid) {
1063 infile.error("StatBlock: '%s' is not a valid key.", infile.key.c_str());
1064 }
1065 }
1066 infile.close();
1067 }
1068
1069 if (max_points_per_stat == 0) max_points_per_stat = max_spendable_stat_points / 4 + 1;
1070 statsLoaded = true;
1071
1072 max_spendable_stat_points = eset->xp.getMaxLevel() * stat_points_per_level;
1073 }
1074
loadHeroSFX()1075 void StatBlock::loadHeroSFX() {
1076 // load the paths to base sound effects
1077 FileParser infile;
1078 if (infile.open("engine/avatar/"+gfx_base+".txt", FileParser::MOD_FILE, FileParser::ERROR_NONE)) {
1079 while(infile.next()) {
1080 loadSfxStat(&infile);
1081 }
1082 infile.close();
1083 }
1084 }
1085
1086 /**
1087 * Recursivly kill all summoned creatures
1088 */
removeSummons()1089 void StatBlock::removeSummons() {
1090 for (std::vector<StatBlock*>::iterator it = summons.begin(); it != summons.end(); ++it) {
1091 (*it)->takeDamage((*it)->get(Stats::HP_MAX), !StatBlock::TAKE_DMG_CRIT, Power::SOURCE_TYPE_NEUTRAL);
1092 (*it)->removeSummons();
1093 (*it)->summoner = NULL;
1094 }
1095
1096 summons.clear();
1097 }
1098
removeFromSummons()1099 void StatBlock::removeFromSummons() {
1100
1101 if(summoner != NULL && !summoner->summons.empty()) {
1102 std::vector<StatBlock*>::iterator parent_ref = std::find(summoner->summons.begin(), summoner->summons.end(), this);
1103
1104 if(parent_ref != summoner->summons.end())
1105 summoner->summons.erase(parent_ref);
1106
1107 summoner = NULL;
1108 }
1109
1110 removeSummons();
1111 }
1112
summonLimitReached(PowerID power_id) const1113 bool StatBlock::summonLimitReached(PowerID power_id) const {
1114
1115 //find the limit
1116 Power *spawn_power = &powers->powers[power_id];
1117
1118 int max_summons = 0;
1119
1120 if(spawn_power->spawn_limit_mode == Power::SPAWN_LIMIT_MODE_FIXED)
1121 max_summons = spawn_power->spawn_limit_qty;
1122 else if(spawn_power->spawn_limit_mode == Power::SPAWN_LIMIT_MODE_STAT) {
1123 int stat_val = 1;
1124 for (size_t i = 0; i < eset->primary_stats.list.size(); ++i) {
1125 if (spawn_power->spawn_limit_stat == i) {
1126 stat_val = get_primary(i);
1127 break;
1128 }
1129 }
1130 max_summons = (stat_val / (spawn_power->spawn_limit_every == 0 ? 1 : spawn_power->spawn_limit_every)) * spawn_power->spawn_limit_qty;
1131 }
1132 else
1133 return false;//unlimited or unknown mode
1134
1135 //if the power is available, there should be at least 1 allowed summon
1136 if(max_summons < 1) max_summons = 1;
1137
1138
1139 //find out how many there are currently
1140 int qty_summons = 0;
1141
1142 for (unsigned int i=0; i < summons.size(); i++) {
1143 if(!summons[i]->corpse && summons[i]->summoned_power_index == power_id
1144 && summons[i]->cur_state != ENTITY_SPAWN
1145 && summons[i]->cur_state != ENTITY_DEAD
1146 && summons[i]->cur_state != ENTITY_CRITDEAD) {
1147 qty_summons++;
1148 }
1149 }
1150
1151 return qty_summons >= max_summons;
1152 }
1153
setWanderArea(int r)1154 void StatBlock::setWanderArea(int r) {
1155 wander_area.x = int(floorf(pos.x)) - r;
1156 wander_area.y = int(floorf(pos.y)) - r;
1157 wander_area.w = wander_area.h = (r*2) + 1;
1158 }
1159
1160 /**
1161 * Returns the short version of the class string
1162 * For the sake of consistency with previous versions,
1163 * this means returning the generated subclass
1164 */
getShortClass()1165 std::string StatBlock::getShortClass() {
1166 if (character_subclass == "")
1167 return msg->get(character_class);
1168 else
1169 return msg->get(character_subclass);
1170 }
1171
1172 /**
1173 * Returns the long version of the class string
1174 * It contains both the base class and the generated subclass
1175 */
getLongClass()1176 std::string StatBlock::getLongClass() {
1177 if (character_subclass == "" || character_class == character_subclass)
1178 return msg->get(character_class);
1179 else
1180 return msg->get(character_class) + " / " + msg->get(character_subclass);
1181 }
1182
addXP(int amount)1183 void StatBlock::addXP(int amount) {
1184 xp += amount;
1185
1186 unsigned long xp_max = eset->xp.getLevelXP(eset->xp.getMaxLevel());
1187 xp = std::min(xp, xp_max);
1188 }
1189
getAIPower(int ai_type)1190 StatBlock::AIPower* StatBlock::getAIPower(int ai_type) {
1191 std::vector<size_t> possible_ids;
1192 int chance = rand() % 100;
1193
1194 for (size_t i=0; i<powers_ai.size(); ++i) {
1195 if (ai_type != powers_ai[i].type)
1196 continue;
1197
1198 if (chance > powers_ai[i].chance)
1199 continue;
1200
1201 if (!powers_ai[i].cooldown.isEnd())
1202 continue;
1203
1204 if (powers->powers[powers_ai[i].id].type == Power::TYPE_SPAWN) {
1205 if (summonLimitReached(powers_ai[i].id))
1206 continue;
1207 }
1208
1209 if (!checkRequiredSpawns(powers->powers[powers_ai[i].id].requires_spawns))
1210 continue;
1211
1212 possible_ids.push_back(i);
1213 }
1214
1215 if (!possible_ids.empty()) {
1216 size_t index = static_cast<size_t>(rand()) % possible_ids.size();
1217 return &powers_ai[possible_ids[index]];
1218 }
1219
1220 return NULL;
1221 }
1222
checkRequiredSpawns(int req_amount) const1223 bool StatBlock::checkRequiredSpawns(int req_amount) const {
1224 if (req_amount <= 0)
1225 return true;
1226
1227 int live_summon_count = 0;
1228 for (size_t j=0; j<summons.size(); ++j) {
1229 if (summons[j]->hp > 0) {
1230 ++live_summon_count;
1231 }
1232 }
1233
1234 if (live_summon_count < req_amount)
1235 return false;
1236
1237 return true;
1238 }
1239
getPowerCooldown(PowerID power_id)1240 int StatBlock::getPowerCooldown(PowerID power_id) {
1241 if (hero) {
1242 return pc->power_cooldown_timers[power_id].getDuration();
1243 }
1244 else {
1245 for (size_t i = 0; i < powers_ai.size(); ++i) {
1246 if (power_id == powers_ai[i].id)
1247 return powers_ai[i].cooldown.getDuration();
1248 }
1249 }
1250
1251 return 0;
1252 }
1253
setPowerCooldown(PowerID power_id,int power_cooldown)1254 void StatBlock::setPowerCooldown(PowerID power_id, int power_cooldown) {
1255 if (hero) {
1256 pc->power_cooldown_timers[power_id].setDuration(power_cooldown);
1257 }
1258 else {
1259 for (size_t i = 0; i < powers_ai.size(); ++i) {
1260 if (power_id == powers_ai[i].id) {
1261 powers_ai[i].cooldown.setDuration(power_cooldown);
1262 break;
1263 }
1264 }
1265 }
1266 }
1267