1 // Copyright 2014-2018 the openage authors. See copying.md for legal info.
2
3 #include <algorithm>
4 #include <cmath>
5
6 #include "../engine.h"
7 #include "../pathfinding/a_star.h"
8 #include "../pathfinding/heuristics.h"
9 #include "../terrain/terrain.h"
10 #include "../terrain/terrain_search.h"
11 #include "action.h"
12 #include "command.h"
13 #include "producer.h"
14 #include "research.h"
15 #include "unit_texture.h"
16
17 namespace openage {
18
IntervalTimer(unsigned int interval)19 IntervalTimer::IntervalTimer(unsigned int interval)
20 :
21 IntervalTimer{interval, -1} {
22 }
23
IntervalTimer(unsigned int interval,int max_triggers)24 IntervalTimer::IntervalTimer(unsigned int interval, int max_triggers)
25 :
26 interval{interval},
27 max_triggers{max_triggers},
28 time_left{interval},
29 triggers{0} {
30 }
31
skip_to_trigger()32 void IntervalTimer::skip_to_trigger() {
33 this->time_left = 0;
34 }
35
update(unsigned int time)36 bool IntervalTimer::update(unsigned int time) {
37 if (this->triggers == this->max_triggers) {
38 return false;
39 } else if (this->time_left > time) {
40 this->time_left -= time;
41 return false;
42 } else {
43 this->time_left += this->interval - time;
44 this->triggers += 1;
45 return true;
46 }
47 }
48
get_time_left() const49 unsigned int IntervalTimer::get_time_left() const {
50 return this->time_left;
51 }
52
get_progress() const53 float IntervalTimer::get_progress() const {
54 return 1.0f - (this->time_left * 1.0f / this->interval);
55 }
56
has_triggers() const57 bool IntervalTimer::has_triggers() const {
58 return this->triggers > 0;
59 }
60
finished() const61 bool IntervalTimer::finished() const {
62 return this->triggers == this->max_triggers;
63 }
64
65 bool UnitAction::show_debug = false;
66
adjacent_range(Unit * u)67 coord::phys_t UnitAction::adjacent_range(Unit *u) {
68 return path::path_grid_size * 3 + (u->location->min_axis() / 2L);
69 }
70
get_attack_range(Unit * u)71 coord::phys_t UnitAction::get_attack_range(Unit *u) {
72 coord::phys_t range = adjacent_range(u);
73 if (u->has_attribute(attr_type::attack)) {
74 auto &attack = u->get_attribute<attr_type::attack>();
75 range += attack.max_range;
76 }
77 return range;
78 }
79
get_heal_range(Unit * u)80 coord::phys_t UnitAction::get_heal_range(Unit *u) {
81 coord::phys_t range = adjacent_range(u);
82 if (u->has_attribute(attr_type::heal)) {
83 auto &heal = u->get_attribute<attr_type::heal>();
84 range += heal.range;
85 }
86 return range;
87 }
88
UnitAction(Unit * u,graphic_type initial_gt)89 UnitAction::UnitAction(Unit *u, graphic_type initial_gt)
90 :
91 entity{u},
92 graphic{initial_gt},
93 frame{.0f},
94 frame_rate{.0f} {
95
96 auto &g_set = this->current_graphics();
97 if (g_set.count(initial_gt) > 0) {
98 auto utex = g_set.at(initial_gt);
99 if (utex) {
100 this->frame_rate = utex->frame_rate;
101 }
102 else {
103 this->entity->log(MSG(dbg) << "Broken graphic (null)");
104 }
105 }
106 else {
107 this->entity->log(MSG(dbg) << "Broken graphic (not available)");
108 }
109
110 if (this->frame_rate == 0) {
111
112 // a random starting point for static graphics
113 // this creates variations in trees / houses etc
114 // this value is also deterministic to match across clients
115 this->frame = (u->id * u->id * 19249) & 0xff;
116 }
117 }
118
type() const119 graphic_type UnitAction::type() const {
120 return this->graphic;
121 }
122
current_frame() const123 float UnitAction::current_frame() const {
124 return this->frame;
125 }
126
current_graphics() const127 const graphic_set &UnitAction::current_graphics() const {
128
129 // return the default graphic
130 return this->entity->unit_type->graphics;
131 }
132
draw_debug(const Engine & engine)133 void UnitAction::draw_debug(const Engine &engine) {
134 // draw debug content if available
135 if (show_debug && this->debug_draw_action) {
136 this->debug_draw_action(engine);
137 }
138 }
139
face_towards(const coord::phys3 pos)140 void UnitAction::face_towards(const coord::phys3 pos) {
141 if (this->entity->has_attribute(attr_type::direction)) {
142 auto &d_attr = this->entity->get_attribute<attr_type::direction>();
143 d_attr.unit_dir = pos - this->entity->location->pos.draw;
144 }
145 }
146
damage_unit(Unit & target)147 bool UnitAction::damage_unit(Unit &target) {
148 bool killed = false;
149
150 if (target.has_attribute(attr_type::damaged)) {
151 auto &dm = target.get_attribute<attr_type::damaged>();
152
153 // this is the damage calculation system
154
155 if (dm.hp == 0) {
156 // already killed, do nothing
157 }
158 else if (target.has_attribute(attr_type::armor) && this->entity->has_attribute(attr_type::attack)) {
159 auto &armor = target.get_attribute<attr_type::armor>().armor;
160 auto &damage = this->entity->get_attribute<attr_type::attack>().damage;
161
162 unsigned int actual_damage = 0;
163 for (const auto &pair : armor) {
164 auto search = damage.find(pair.first);
165 if (search != damage.end()) {
166 if (pair.second < search->second) {
167 actual_damage += search->second - pair.second;
168 }
169 }
170 }
171 // TODO add elevation modifier here
172 if (actual_damage < 1) {
173 actual_damage = 1;
174 }
175
176 if (dm.hp > actual_damage) {
177 dm.hp -= actual_damage;
178 }
179 else {
180 dm.hp = 0;
181 killed = true;
182 }
183 }
184 else {
185 // TODO remove (keep for testing)
186 unsigned int dmg = 1;
187 if (dm.hp > dmg) {
188 dm.hp -= dmg;
189 }
190 else {
191 dm.hp = 0;
192 killed = true;
193 }
194 }
195 }
196
197 if (killed) {
198 // if killed, give credit to a player
199 if (this->entity->has_attribute(attr_type::owner)) {
200 auto &owner = this->entity->get_attribute<attr_type::owner>().player;
201 owner.killed_unit(target);
202 }
203 }
204
205 return killed;
206 }
207
move_to(Unit & target,bool use_range)208 void UnitAction::move_to(Unit &target, bool use_range) {
209 auto &player = this->entity->get_attribute<attr_type::owner>().player;
210 Command cmd(player, &target);
211 cmd.set_ability(ability_type::move);
212 if (use_range) {
213 cmd.add_flag(command_flag::use_range);
214 }
215 this->entity->queue_cmd(cmd);
216 }
217
TargetAction(Unit * u,graphic_type gt,UnitReference r,coord::phys_t rad)218 TargetAction::TargetAction(Unit *u, graphic_type gt, UnitReference r, coord::phys_t rad)
219 :
220 UnitAction(u, gt),
221 target{r},
222 target_type_id{0},
223 repath_attempts{10},
224 end_action{false},
225 radius{rad} {
226
227 // update type
228 if (this->target.is_valid()) {
229 auto target_ptr = this->target.get();
230 this->target_type_id = target_ptr->unit_type->id();
231 }
232
233 // initial value for distance
234 this->update_distance();
235 }
236
TargetAction(Unit * u,graphic_type gt,UnitReference r)237 TargetAction::TargetAction(Unit *u, graphic_type gt, UnitReference r)
238 :
239 TargetAction(u, gt, r, adjacent_range(u)) {
240 }
241
update(unsigned int time)242 void TargetAction::update(unsigned int time) {
243 auto target_ptr = this->update_distance();
244 if (!target_ptr) {
245 return; // target has become invalid
246 }
247
248 // this update moves a unit within radius of the target
249 // once within the radius the update gets passed to the class
250 // derived from TargetAction
251
252 // set direction unit should face
253 this->face_towards(target_ptr->location->pos.draw);
254
255 // move to within the set radius
256 if (this->dist_to_target <= this->radius) {
257
258 // the derived class controls what to
259 // do when in range of the target
260 this->update_in_range(time, target_ptr);
261 this->repath_attempts = 10;
262 }
263 else if (this->repath_attempts) {
264
265 // out of range so try move towards
266 // if this unit has a move ability
267 this->move_to(*target_ptr);
268 this->repath_attempts -= 1;
269 }
270 else {
271
272 // unit is stuck
273 this->end_action = true;
274 }
275 }
276
on_completion()277 void TargetAction::on_completion() {
278 // do not retask if action is forced to end
279 if (this->end_action ||
280 !this->entity->location) {
281 return;
282 }
283
284 // retask units on nearby objects
285 // such as gathers targeting a new resource
286 // when the current target expires
287 this->on_completion_in_range(this->target_type_id);
288 }
289
completed() const290 bool TargetAction::completed() const {
291 if (this->end_action ||
292 !this->target.is_valid() ||
293 !this->target.get()->location) {
294 return true;
295 }
296 return this->completed_in_range(this->target.get());
297 }
298
distance_to_target()299 coord::phys_t TargetAction::distance_to_target() {
300 return this->dist_to_target;
301 }
302
update_distance()303 Unit *TargetAction::update_distance() {
304 if (!this->target.is_valid()) {
305 return nullptr;
306 }
307
308 // make sure object is not garrisoned
309 auto target_ptr = this->target.get();
310 if (!target_ptr->location) {
311 return nullptr;
312 }
313
314 // update distance
315 this->dist_to_target = target_ptr->location->from_edge(this->entity->location->pos.draw);
316
317 // return the targeted unit
318 return target_ptr;
319 }
320
get_target() const321 UnitReference TargetAction::get_target() const {
322 return this->target;
323 }
324
get_target_type_id() const325 int TargetAction::get_target_type_id() const {
326 return this->target_type_id;
327 }
328
set_target(UnitReference new_target)329 void TargetAction::set_target(UnitReference new_target) {
330 if (new_target.is_valid()) {
331 this->target = new_target;
332 this->update_distance();
333 }
334 else {
335 this->end_action = true;
336 }
337 }
338
DecayAction(Unit * e)339 DecayAction::DecayAction(Unit *e)
340 :
341 UnitAction(e, graphic_type::standing),
342 end_frame{.0f} {
343
344 auto &g_set = this->current_graphics();
345 if (g_set.count(this->graphic) > 0) {
346 this->end_frame = g_set.at(this->graphic)->frame_count - 1;
347 }
348 }
349
update(unsigned int time)350 void DecayAction::update(unsigned int time) {
351 this->frame += time * this->frame_rate / 10000.0f;
352 }
353
on_completion()354 void DecayAction::on_completion() {}
355
completed() const356 bool DecayAction::completed() const {
357 return this->frame > this->end_frame;
358 }
359
DeadAction(Unit * e,std::function<void ()> on_complete)360 DeadAction::DeadAction(Unit *e, std::function<void()> on_complete)
361 :
362 UnitAction(e, graphic_type::dying),
363 end_frame{.0f},
364 on_complete_func{on_complete} {
365
366 auto &g_set = this->current_graphics();
367 if (g_set.count(graphic) > 0) {
368 this->end_frame = g_set.at(graphic)->frame_count - 1;
369 }
370 }
371
update(unsigned int time)372 void DeadAction::update(unsigned int time) {
373 if (this->entity->has_attribute(attr_type::damaged)) {
374 auto &dm = this->entity->get_attribute<attr_type::damaged>();
375 dm.hp = 0;
376 }
377
378 // decay resources
379 if (this->entity->has_attribute(attr_type::resource)) {
380 auto &resource = this->entity->get_attribute<attr_type::resource>();
381 if (resource.decay > 0) {
382 resource.amount -= resource.decay;
383 }
384 }
385
386 // inc frame but do not pass the end frame
387 // the end frame will remain if the object carries resources
388 if (this->frame < this->end_frame) {
389 this->frame += 0.001 + time * this->frame_rate / 3.0f;
390 }
391 else {
392 this->frame = this->end_frame;
393 }
394 }
395
on_completion()396 void DeadAction::on_completion() {
397 if (this->entity->has_attribute(attr_type::owner)) {
398 auto &owner = this->entity->get_attribute<attr_type::owner>().player;
399 owner.active_unit_removed(this->entity); // TODO move before the start of dead action?
400 }
401
402 this->on_complete_func();
403 }
404
completed() const405 bool DeadAction::completed() const {
406
407 // check resource, trees/huntables with resource are not removed but not workers
408 if (this->entity->has_attribute(attr_type::resource) && !this->entity->has_attribute(attr_type::worker)) {
409 auto &res_attr = this->entity->get_attribute<attr_type::resource>();
410 return res_attr.amount <= 0; // cannot complete when resource remains
411 }
412 return this->frame > this->end_frame;
413 }
414
FoundationAction(Unit * e,bool add_destruction)415 FoundationAction::FoundationAction(Unit *e, bool add_destruction)
416 :
417 UnitAction(e, graphic_type::construct),
418 add_destruct_effect{add_destruction},
419 cancel{false} {
420 }
421
update(unsigned int)422 void FoundationAction::update(unsigned int) {
423 if (!this->entity->location) {
424 this->cancel = true;
425 }
426 }
427
on_completion()428 void FoundationAction::on_completion() {
429
430 // do nothing if construction is cancelled
431 if (this->cancel) {
432 return;
433 }
434
435 if (this->entity->has_attribute(attr_type::owner)) {
436 auto &owner = this->entity->get_attribute<attr_type::owner>().player;
437 owner.active_unit_added(this->entity, true);
438 }
439
440 // add destruction effect when available
441 if (this->add_destruct_effect) {
442 this->entity->push_action(std::make_unique<DeadAction>(this->entity), true);
443 }
444 this->entity->push_action(std::make_unique<IdleAction>(this->entity), true);
445 }
446
completed() const447 bool FoundationAction::completed() const {
448 return this->cancel ||
449 (this->entity->has_attribute(attr_type::building) &&
450 (this->entity->get_attribute<attr_type::building>().completed >= 1.0f));
451 }
452
IdleAction(Unit * e)453 IdleAction::IdleAction(Unit *e)
454 :
455 UnitAction(e, graphic_type::standing) {
456 auto terrain = this->entity->location->get_terrain();
457 auto current_tile = this->entity->location->pos.draw.to_tile3().to_tile();
458 this->search = std::make_shared<TerrainSearch>(terrain, current_tile, 5.0f);
459
460 // currently allow attack and heal automatically
461 this->auto_abilities = UnitAbility::set_from_list({ability_type::attack, ability_type::heal});
462 }
463
update(unsigned int time)464 void IdleAction::update(unsigned int time) {
465
466 // auto task searching
467 if (this->entity->location &&
468 this->entity->has_attribute(attr_type::owner) &&
469 this->entity->has_attribute(attr_type::attack) &&
470 this->entity->has_attribute(attr_type::formation) &&
471 this->entity->get_attribute<attr_type::formation>().stance != attack_stance::do_nothing) {
472
473 // restart search from new tile when moved
474 auto terrain = this->entity->location->get_terrain();
475 auto current_tile = this->entity->location->pos.draw.to_tile3().to_tile();
476 if (!(current_tile == this->search->start_tile())) {
477 this->search = std::make_shared<TerrainSearch>(terrain, current_tile, 5.0f);
478 }
479
480 // search one tile per update
481 // next tile will always be valid
482 coord::tile tile = this->search->next_tile();
483 auto tile_data = terrain->get_data(tile);
484 auto &player = this->entity->get_attribute<attr_type::owner>().player;
485
486 // find and actions which can be invoked
487 for (auto object_location : tile_data->obj) {
488 Command to_object(player, &object_location->unit);
489
490 // only allow abilities in the set of auto ability types
491 to_object.set_ability_set(auto_abilities);
492 if (this->entity->queue_cmd(to_object)) {
493 break;
494 }
495 }
496 }
497
498 // generate resources
499 // TODO move elsewhere
500 if (this->entity->has_attribute(attr_type::resource_generator) &&
501 this->entity->has_attribute(attr_type::owner)) {
502
503 auto &player = this->entity->get_attribute<attr_type::owner>().player;
504 auto &resource_generator = this->entity->get_attribute<attr_type::resource_generator>();
505
506 ResourceBundle resources = resource_generator.resources.clone();
507 if (resource_generator.rate == 0) {
508 resources *= time;
509 } else {
510 // TODO add in intervals and not continuously
511 resources *= time * resource_generator.rate;
512 }
513
514 player.receive(resources);
515 }
516
517 // unit carrying ressources take the carrying sprite when idle
518 // we're not updating frames because the carying sprite is walking
519 if (entity->has_attribute(attr_type::worker)) {
520 auto &worker_resource = this->entity->get_attribute<attr_type::resource>();
521 if (worker_resource.amount > 0) {
522 this->graphic = graphic_type::carrying;
523 } else {
524 this->graphic = graphic_type::standing;
525 this->frame += time * this->frame_rate / 20.0f;
526 }
527 }
528 else {
529 // inc frame
530 this->frame += time * this->frame_rate / 20.0f;
531 }
532 }
533
on_completion()534 void IdleAction::on_completion() {}
535
completed() const536 bool IdleAction::completed() const {
537 if (this->entity->has_attribute(attr_type::damaged)) {
538 auto &dm = this->entity->get_attribute<attr_type::damaged>();
539 return dm.hp == 0;
540 }
541 else if (this->entity->has_attribute(attr_type::resource)) {
542 auto &res_attr = this->entity->get_attribute<attr_type::resource>();
543 return res_attr.amount <= 0.0;
544 }
545 return false;
546 }
547
MoveAction(Unit * e,coord::phys3 tar,bool repath)548 MoveAction::MoveAction(Unit *e, coord::phys3 tar, bool repath)
549 :
550 UnitAction{e, graphic_type::walking},
551 unit_target{},
552 target(tar),
553 radius{path::path_grid_size},
554 allow_repath{repath},
555 end_action{false} {
556 this->initialise();
557 }
558
MoveAction(Unit * e,UnitReference tar,coord::phys_t within_range)559 MoveAction::MoveAction(Unit *e, UnitReference tar, coord::phys_t within_range)
560 :
561 UnitAction{e, graphic_type::walking},
562 unit_target{tar},
563 target(tar.get()->location->pos.draw),
564 radius{within_range},
565 allow_repath{false},
566 end_action{false} {
567 this->initialise();
568 }
569
initialise()570 void MoveAction::initialise() {
571 // switch workers to the carrying graphic
572 if (this->entity->has_attribute(attr_type::worker)) {
573 auto &worker_resource = this->entity->get_attribute<attr_type::resource>();
574 if (worker_resource.amount > 0) {
575 this->graphic = graphic_type::carrying;
576 }
577 }
578
579 // set initial distance
580 this->set_distance();
581
582 // set an initial path
583 this->set_path();
584 this->debug_draw_action = [&](const Engine &engine) {
585 this->path.draw_path(engine.coord);
586 };
587 }
588
~MoveAction()589 MoveAction::~MoveAction() {}
590
update(unsigned int time)591 void MoveAction::update(unsigned int time) {
592 if (this->unit_target.is_valid()) {
593 // a unit is targeted, which may move
594 auto &target_object = this->unit_target.get()->location;
595
596 // check for garrisoning objects
597 if (!target_object) {
598 this->end_action = true;
599 return;
600 }
601 coord::phys3 &target_pos = target_object->pos.draw;
602 coord::phys3 &unit_pos = this->entity->location->pos.draw;
603
604 // repath if target changes tiles by a threshold
605 // this repathing is more frequent when the unit is
606 // close to its target
607 coord::phys_t tdx = target_pos.ne - this->target.ne;
608 coord::phys_t tdy = target_pos.se - this->target.se;
609 coord::phys_t udx = unit_pos.ne - this->target.ne;
610 coord::phys_t udy = unit_pos.se - this->target.se;
611 if (this->path.waypoints.empty() || std::hypot(tdx, tdy) > std::hypot(udx, udy)) {
612 this->target = target_pos;
613 this->set_path();
614 }
615 }
616
617 // path not found
618 if (this->path.waypoints.empty()) {
619 if (!this->allow_repath) {
620 this->entity->log(MSG(dbg) << "Path not found -- drop action");
621 this->end_action = true;
622 }
623 return;
624 }
625
626 // find distance to move in this update
627 auto &sp_attr = this->entity->get_attribute<attr_type::speed>();
628 double distance_to_move = sp_attr.unit_speed.to_double() * time;
629
630 // current position and direction
631 coord::phys3 new_position = this->entity->location->pos.draw;
632 auto &d_attr = this->entity->get_attribute<attr_type::direction>();
633 coord::phys3_delta new_direction = d_attr.unit_dir;
634
635 while (distance_to_move > 0) {
636 if (this->path.waypoints.empty()) {
637 break;
638 }
639
640 // find a point to move directly towards
641 coord::phys3 waypoint = this->next_waypoint();
642 coord::phys3_delta move_dir = waypoint - new_position;
643
644 // normalise dir
645 double distance_to_waypoint = std::hypot(move_dir.ne, move_dir.se);
646
647 if (distance_to_waypoint <= distance_to_move) {
648 distance_to_move -= distance_to_waypoint;
649
650 // change entity position and direction
651 new_position = waypoint;
652 new_direction = move_dir;
653
654 // remove the waypoint
655 this->path.waypoints.pop_back();
656 }
657 else {
658 // change entity position and direction
659 new_position += move_dir * (distance_to_move / distance_to_waypoint);
660 new_direction = move_dir;
661 break;
662 }
663 }
664
665 // check move collisions
666 bool move_completed = this->entity->location->move(new_position);
667 if (move_completed) {
668 d_attr.unit_dir = new_direction;
669 this->set_distance();
670 }
671 else {
672 // cases for modifying path when blocked
673 if (this->allow_repath) {
674 this->entity->log(MSG(dbg) << "Path blocked -- finding new path");
675 this->set_path();
676 }
677 else {
678 this->entity->log(MSG(dbg) << "Path blocked -- drop action");
679 this->end_action = true;
680 }
681 }
682
683 // inc frame
684 this->frame += time * this->frame_rate / 5.0f;
685 }
686
on_completion()687 void MoveAction::on_completion() {}
688
completed() const689 bool MoveAction::completed() const {
690
691 // no more waypoints to a static location
692 if (this->end_action ||
693 (!this->unit_target.is_valid() &&
694 this->path.waypoints.empty())) {
695 return true;
696 }
697
698 //close enough to end action
699 if (this->distance_to_target < this->radius) {
700 return true;
701 }
702 return false;
703 }
704
705
next_waypoint() const706 coord::phys3 MoveAction::next_waypoint() const {
707 if (this->path.waypoints.size() > 0) {
708 return this->path.waypoints.back().position;
709 } else {
710 throw Error{MSG(err) << "No next waypoint available!"};
711 }
712 }
713
714
set_path()715 void MoveAction::set_path() {
716 if (this->unit_target.is_valid()) {
717 this->path = path::to_object(this->entity->location.get(), this->unit_target.get()->location.get(), this->radius);
718 }
719 else {
720 coord::phys3 start = this->entity->location->pos.draw;
721 coord::phys3 end = this->target;
722 this->path = path::to_point(start, end, this->entity->location->passable);
723 }
724 }
725
set_distance()726 void MoveAction::set_distance() {
727 if (this->unit_target.is_valid()) {
728 auto &target_object = this->unit_target.get()->location;
729 coord::phys3 &unit_pos = this->entity->location->pos.draw;
730 this->distance_to_target = target_object->from_edge(unit_pos);
731 }
732 else {
733 coord::phys3_delta move_dir = this->target - this->entity->location->pos.draw;
734 this->distance_to_target = static_cast<coord::phys_t>(std::hypot(move_dir.ne, move_dir.se));
735 }
736 }
737
GarrisonAction(Unit * e,UnitReference build)738 GarrisonAction::GarrisonAction(Unit *e, UnitReference build)
739 :
740 TargetAction{e, graphic_type::standing, build},
741 complete{false} {
742 }
743
update_in_range(unsigned int,Unit * target_unit)744 void GarrisonAction::update_in_range(unsigned int, Unit *target_unit) {
745 auto &garrison_attr = target_unit->get_attribute<attr_type::garrison>();
746 garrison_attr.content.push_back(this->entity->get_ref());
747
748 if (this->entity->location) {
749 this->entity->location->remove();
750 this->entity->location = nullptr;
751 }
752 this->complete = true;
753 }
754
UngarrisonAction(Unit * e,const coord::phys3 & pos)755 UngarrisonAction::UngarrisonAction(Unit *e, const coord::phys3 &pos)
756 :
757 UnitAction{e, graphic_type::standing},
758 position(pos),
759 complete{false} {
760 }
761
update(unsigned int)762 void UngarrisonAction::update(unsigned int) {
763 auto &garrison_attr = this->entity->get_attribute<attr_type::garrison>();
764
765 // try unload all objects currently garrisoned
766 auto position_it = std::remove_if(
767 std::begin(garrison_attr.content),
768 std::end(garrison_attr.content),
769 [this](UnitReference &u) {
770 if (u.is_valid()) {
771
772 // ptr to unit being ungarrisoned
773 Unit *unit_ptr = u.get();
774
775 // make sure it was placed outside
776 if (unit_ptr->unit_type->place_beside(unit_ptr, this->entity->location.get())) {
777
778 // task unit to move to position
779 auto &player = this->entity->get_attribute<attr_type::owner>().player;
780 Command cmd(player, this->position);
781 cmd.set_ability(ability_type::move);
782 unit_ptr->queue_cmd(cmd);
783 return true;
784 }
785 }
786 return false;
787 });
788
789 // remove elements which were ungarrisoned
790 garrison_attr.content.erase(position_it, std::end(garrison_attr.content));
791
792 // completed when no units are remaining
793 this->complete = garrison_attr.content.empty();
794 }
795
on_completion()796 void UngarrisonAction::on_completion() {}
797
TrainAction(Unit * e,UnitType * pp)798 TrainAction::TrainAction(Unit *e, UnitType *pp)
799 :
800 UnitAction{e, graphic_type::standing},
801 trained{pp},
802 timer{10000, 1}, // TODO get the training time from unit type
803 started{false},
804 complete{false} {
805 // TODO deduct resources
806 }
807
update(unsigned int time)808 void TrainAction::update(unsigned int time) {
809
810 if (!this->started) {
811 // check if there is enough population capacity
812 if (!this->trained->default_attributes.has(attr_type::population)) {
813 this->started = true;
814 }
815 else {
816 auto &player = this->entity->get_attribute<attr_type::owner>().player;
817 auto &population_demand = this->trained->default_attributes.get<attr_type::population>().demand;
818 bool can_start = population_demand == 0 || population_demand <= player.population.get_space();
819 // TODO trigger not enough population capacity message
820 this->started = can_start;
821 }
822 }
823
824 if (this->started) {
825 // place unit when ready
826 if (this->timer.finished() || this->timer.update(time)) {
827
828 // create using the producer
829 UnitContainer *container = this->entity->get_container();
830 auto &player = this->entity->get_attribute<attr_type::owner>().player;
831 auto uref = container->new_unit(*this->trained, player, this->entity->location.get());
832
833 // make sure unit got placed
834 // try again next update if cannot place
835 if (uref.is_valid()) {
836 if (this->entity->has_attribute(attr_type::building)) {
837
838 // use a move command to the gather point
839 auto &build_attr = this->entity->get_attribute<attr_type::building>();
840 Command cmd(player, build_attr.gather_point);
841 cmd.set_ability(ability_type::move);
842 uref.get()->queue_cmd(cmd);
843 }
844 this->complete = true;
845 }
846 }
847 }
848 }
849
on_completion()850 void TrainAction::on_completion() {
851 if (!this->complete) {
852 // TODO give back the resources
853 }
854 }
855
ResearchAction(Unit * e,Research * research)856 ResearchAction::ResearchAction(Unit *e, Research *research)
857 :
858 UnitAction{e, graphic_type::standing},
859 research{research},
860 timer{research->type->get_research_time(), 1},
861 complete{false} {
862 this->research->started();
863 }
864
update(unsigned int time)865 void ResearchAction::update(unsigned int time) {
866 if (timer.update(time)) {
867 this->complete = true;
868 this->research->apply();
869 this->research->completed();
870 }
871 }
872
on_completion()873 void ResearchAction::on_completion() {
874 if (!this->complete) {
875 this->research->stopped();
876 }
877 }
878
BuildAction(Unit * e,UnitReference foundation)879 BuildAction::BuildAction(Unit *e, UnitReference foundation)
880 :
881 TargetAction{e, graphic_type::work, foundation},
882 complete{.0f},
883 build_rate{.0001f} {
884
885 // update the units type
886 if (this->entity->has_attribute(attr_type::multitype)) {
887 this->entity->get_attribute<attr_type::multitype>().switchType(gamedata::unit_classes::BUILDING, this->entity);
888 }
889 }
890
update_in_range(unsigned int time,Unit * target_unit)891 void BuildAction::update_in_range(unsigned int time, Unit *target_unit) {
892 if (target_unit->has_attribute(attr_type::building)) {
893 auto &build = target_unit->get_attribute<attr_type::building>();
894
895 // upgrade floating outlines
896 auto target_location = target_unit->location.get();
897 if (target_location->is_floating()) {
898
899 // try to place the object
900 if (target_location->place(object_state::placed)) {
901
902 // modify ground terrain
903 if (build.foundation_terrain > 0) {
904 target_location->set_ground(build.foundation_terrain, 0);
905 }
906 }
907 else {
908
909 // failed to start construction
910 this->complete = 1.0f;
911 return;
912 }
913 }
914
915 // increment building completion
916 build.completed += build_rate * time;
917 this->complete = build.completed;
918
919 if (this->complete >= 1.0f) {
920 this->complete = build.completed = 1.0f;
921 target_location->place(build.completion_state);
922 }
923 }
924 else {
925 this->complete = 1.0f;
926 }
927
928 // inc frame
929 this->frame += time * this->frame_rate / 2.5f;
930 }
931
on_completion()932 void BuildAction::on_completion() {
933 if (this->get_target().is_valid() && this->get_target().get()->get_attribute<attr_type::building>().completed < 1.0f) {
934 // The BuildAction was just aborted and we shouldn't look for new buildings
935 return;
936 }
937 this->entity->log(MSG(dbg) << "Done building, searching for new building");
938 auto valid = [this](const TerrainObject &obj) {
939 if (!this->entity->get_attribute<attr_type::owner>().player.owns(obj.unit) ||
940 !obj.unit.has_attribute(attr_type::building) ||
941 obj.unit.get_attribute<attr_type::building>().completed >= 1.0f) {
942 return false;
943 }
944 this->entity->log(MSG(dbg) << "Found unit " << obj.unit.logsource_name());
945 return true;
946 };
947
948 TerrainObject *new_target = find_in_radius(*this->entity->location, valid, BuildAction::search_tile_distance);
949 if (new_target != nullptr) {
950 this->entity->log(MSG(dbg) << "Found new building, queueing command");
951 Command cmd(this->entity->get_attribute<attr_type::owner>().player, &new_target->unit);
952 this->entity->queue_cmd(cmd);
953 } else {
954 this->entity->log(MSG(dbg) << "Didn't find new building");
955 }
956 }
957
RepairAction(Unit * e,UnitReference tar)958 RepairAction::RepairAction(Unit *e, UnitReference tar)
959 :
960 TargetAction{e, graphic_type::work, tar},
961 timer{80},
962 complete{false} {
963
964 if (!tar.is_valid()) {
965 // the target no longer exists
966 complete = true;
967 }
968 else {
969
970 Unit *target = tar.get();
971
972 if (!target->has_attribute(attr_type::building)) {
973 this->timer.set_interval(this->timer.get_interval() * 4);
974 }
975
976 // cost formula: 0.5 * (target cost) / (target max hp)
977 auto &hp = target->get_attribute<attr_type::hitpoints>();
978 auto &owner = this->entity->get_attribute<attr_type::owner>();
979
980 // get the target unit's cost
981 this->cost += target->unit_type->cost.get(owner.player);
982 this->cost *= 0.5 / hp.hp;
983
984 if (!owner.player.deduct(this->cost)) {
985 // no resources to start
986 this->complete = true;
987 }
988 }
989 }
990
update_in_range(unsigned int time,Unit * target_unit)991 void RepairAction::update_in_range(unsigned int time, Unit *target_unit) {
992
993 auto &hp = target_unit->get_attribute<attr_type::hitpoints>();
994 auto &dm = target_unit->get_attribute<attr_type::damaged>();
995
996 if (dm.hp >= hp.hp) {
997 // repaired by something else
998 this->complete = true;
999 }
1000 else if (this->timer.update(time)) {
1001 dm.hp += 1;
1002
1003 if (dm.hp >= hp.hp) {
1004 this->complete = true;
1005 }
1006
1007 if (!this->complete) {
1008 auto &owner = this->entity->get_attribute<attr_type::owner>();
1009 if (!owner.player.deduct(this->cost)) {
1010 // no resources to continue
1011 this->complete = true;
1012 }
1013 }
1014 }
1015
1016 // inc frame
1017 this->frame += time * this->frame_rate / 2.5f;
1018 }
1019
GatherAction(Unit * e,UnitReference tar)1020 GatherAction::GatherAction(Unit *e, UnitReference tar)
1021 :
1022 TargetAction{e, graphic_type::work, tar},
1023 complete{false},
1024 target_resource{true},
1025 target{tar} {
1026
1027 Unit *target = this->target.get();
1028 this->resource_class = target->unit_type->unit_class;
1029
1030 // handle unit type changes based on resource class
1031 if (this->entity->has_attribute(attr_type::multitype)) {
1032 this->entity->get_attribute<attr_type::multitype>().switchType(this->resource_class, this->entity);
1033 }
1034
1035 // set the type of gatherer
1036 auto &worker_resource = this->entity->get_attribute<attr_type::resource>();
1037 if (target->has_attribute(attr_type::resource)) {
1038 auto &resource_attr = target->get_attribute<attr_type::resource>();
1039 if (worker_resource.resource_type != resource_attr.resource_type) {
1040 worker_resource.amount = 0;
1041 }
1042 worker_resource.resource_type = resource_attr.resource_type;
1043 } else {
1044 throw std::invalid_argument("Unit reference has no resource attribute");
1045 }
1046 }
1047
~GatherAction()1048 GatherAction::~GatherAction() {}
1049
update_in_range(unsigned int time,Unit * targeted_resource)1050 void GatherAction::update_in_range(unsigned int time, Unit *targeted_resource) {
1051 auto &worker = this->entity->get_attribute<attr_type::worker>();
1052 auto &worker_resource = this->entity->get_attribute<attr_type::resource>();
1053 if (this->target_resource) {
1054
1055 // the targets attributes
1056 if (!targeted_resource->has_attribute(attr_type::resource)) {
1057 complete = true;
1058 return;
1059 }
1060
1061 // attack objects which have hitpoints (trees, hunt, sheep)
1062 if (this->entity->has_attribute(attr_type::owner) &&
1063 targeted_resource->has_attribute(attr_type::damaged)) {
1064 auto &pl = this->entity->get_attribute<attr_type::owner>();
1065 auto &dm = targeted_resource->get_attribute<attr_type::damaged>();
1066
1067 // only attack if hitpoints remain
1068 if (dm.hp > 0) {
1069 Command cmd(pl.player, targeted_resource);
1070 cmd.set_ability(ability_type::attack);
1071 cmd.add_flag(command_flag::attack_res);
1072 this->entity->queue_cmd(cmd);
1073 return;
1074 }
1075 }
1076
1077 // need to return to dropsite
1078 if (worker_resource.amount > worker.capacity) {
1079
1080 // move to dropsite location
1081 this->target_resource = false;
1082 this->set_target(this->nearest_dropsite(worker_resource.resource_type));
1083 }
1084 else {
1085
1086 auto &resource_attr = targeted_resource->get_attribute<attr_type::resource>();
1087 if (resource_attr.amount <= 0.0) {
1088
1089 // when the resource runs out
1090 if (worker_resource.amount > 0.0) {
1091 this->target_resource = false;
1092 this->set_target(this->nearest_dropsite(worker_resource.resource_type));
1093 }
1094 else {
1095 this->complete = true;
1096 }
1097 }
1098 else {
1099
1100 // transfer using gather rate
1101 double amount = worker.gather_rate[worker_resource.resource_type]
1102 * resource_attr.gather_rate * time;
1103 worker_resource.amount += amount;
1104 resource_attr.amount -= amount;
1105 }
1106 }
1107 }
1108 else {
1109
1110 // dropsite has been reached
1111 // add value to player stockpile
1112 auto &player = this->entity->get_attribute<attr_type::owner>().player;
1113 player.receive(worker_resource.resource_type, worker_resource.amount);
1114 worker_resource.amount = 0.0;
1115
1116 // make sure the resource stil exists
1117 if (this->target.is_valid() &&
1118 this->target.get()->get_attribute<attr_type::resource>().amount > 0.0) {
1119
1120 // return to resouce collection
1121 this->target_resource = true;
1122 this->set_target(this->target);
1123 }
1124 else {
1125
1126 // resource depleted
1127 this->complete = true;
1128 }
1129 }
1130
1131 // inc frame
1132 this->frame += time * this->frame_rate / 3.0f;
1133 }
1134
on_completion_in_range(int target_type)1135 void GatherAction::on_completion_in_range(int target_type) {
1136
1137 // find a different target with same type
1138 TerrainObject *new_target = nullptr;
1139 new_target = find_near(*this->entity->location,
1140 [target_type](const TerrainObject &obj) {
1141 return obj.unit.unit_type->id() == target_type &&
1142 !obj.unit.has_attribute(attr_type::worker) &&
1143 obj.unit.has_attribute(attr_type::resource) &&
1144 obj.unit.get_attribute<attr_type::resource>().amount > 0.0;
1145 });
1146
1147 if (new_target) {
1148 this->entity->log(MSG(dbg) << "auto retasking");
1149 auto &pl_attr = this->entity->get_attribute<attr_type::owner>();
1150 Command cmd(pl_attr.player, &new_target->unit);
1151 this->entity->queue_cmd(cmd);
1152 }
1153 }
1154
nearest_dropsite(game_resource res_type)1155 UnitReference GatherAction::nearest_dropsite(game_resource res_type) {
1156
1157 // find nearest dropsite from the targeted resource
1158 auto ds = find_near(*this->target.get()->location,
1159 [=](const TerrainObject &obj) {
1160
1161 if (not obj.unit.has_attribute(attr_type::building) or &obj.unit == this->entity or &obj.unit == this->target.get()) {
1162 return false;
1163 }
1164
1165 return obj.unit.get_attribute<attr_type::building>().completed >= 1.0f &&
1166 obj.unit.has_attribute(attr_type::owner) &&
1167 obj.unit.get_attribute<attr_type::owner>().player.owns(*this->entity) &&
1168 obj.unit.has_attribute(attr_type::dropsite) &&
1169 obj.unit.get_attribute<attr_type::dropsite>().accepting_resource(res_type);
1170 });
1171
1172 if (ds) {
1173 return ds->unit.get_ref();
1174 }
1175 else {
1176 this->entity->log(MSG(dbg) << "no dropsite found");
1177 return UnitReference();
1178 }
1179 }
1180
AttackAction(Unit * e,UnitReference tar)1181 AttackAction::AttackAction(Unit *e, UnitReference tar)
1182 :
1183 TargetAction{e, graphic_type::attack, tar, get_attack_range(e)},
1184 timer{500} { // TODO get fire rate from unit type
1185
1186 // check if attacking a non resource unit
1187 if (this->entity->has_attribute(attr_type::worker) &&
1188 (!tar.get()->has_attribute(attr_type::resource) || tar.get()->has_attribute(attr_type::worker))) {
1189 // switch to default villager graphics
1190 if (this->entity->has_attribute(attr_type::multitype)) {
1191 this->entity->get_attribute<attr_type::multitype>().switchType(gamedata::unit_classes::CIVILIAN, this->entity);
1192 }
1193 }
1194
1195 // TODO rivit logic, a start inside the animation should be provided
1196 this->timer.skip_to_trigger();
1197 }
1198
~AttackAction()1199 AttackAction::~AttackAction() {}
1200
update_in_range(unsigned int time,Unit * target_ptr)1201 void AttackAction::update_in_range(unsigned int time, Unit *target_ptr) {
1202 if (this->timer.update(time)) {
1203 this->attack(*target_ptr);
1204 }
1205
1206 // inc frame
1207 this->frame += time * this->current_graphics().at(graphic)->frame_count * 1.0f / this->timer.get_interval();
1208 }
1209
completed_in_range(Unit * target_ptr) const1210 bool AttackAction::completed_in_range(Unit *target_ptr) const {
1211 auto &dm = target_ptr->get_attribute<attr_type::damaged>();
1212 return dm.hp < 1; // is unit still alive?
1213 }
1214
attack(Unit & target)1215 void AttackAction::attack(Unit &target) {
1216 auto &attack = this->entity->get_attribute<attr_type::attack>();
1217 if (attack.ptype) {
1218
1219 // add projectile to the game
1220 this->fire_projectile(attack, target.location->pos.draw);
1221 }
1222 else {
1223 this->damage_unit(target);
1224 }
1225 }
1226
fire_projectile(const Attribute<attr_type::attack> & att,const coord::phys3 & target)1227 void AttackAction::fire_projectile(const Attribute<attr_type::attack> &att, const coord::phys3 &target) {
1228
1229 // container terrain and initial position
1230 UnitContainer *container = this->entity->get_container();
1231 coord::phys3 current_pos = this->entity->location->pos.draw;
1232 current_pos.up = att.init_height;
1233
1234 // create using the producer
1235 auto &player = this->entity->get_attribute<attr_type::owner>().player;
1236 auto projectile_ref = container->new_unit(*att.ptype, player, current_pos);
1237
1238 // send towards target using a projectile ability (creates projectile motion action)
1239 if (projectile_ref.is_valid()) {
1240 auto projectile = projectile_ref.get();
1241 auto &projectile_attr = projectile->get_attribute<attr_type::projectile>();
1242 projectile_attr.launcher = this->entity->get_ref();
1243 projectile_attr.launched = true;
1244 projectile->push_action(std::make_unique<ProjectileAction>(projectile, target), true);
1245 }
1246 else {
1247 this->entity->log(MSG(dbg) << "projectile launch failed");
1248 }
1249 }
1250
1251
HealAction(Unit * e,UnitReference tar)1252 HealAction::HealAction(Unit *e, UnitReference tar)
1253 :
1254 TargetAction{e, graphic_type::heal, tar, get_attack_range(e)},
1255 timer{this->entity->get_attribute<attr_type::heal>().rate} {
1256
1257 }
1258
~HealAction()1259 HealAction::~HealAction() {}
1260
update_in_range(unsigned int time,Unit * target_ptr)1261 void HealAction::update_in_range(unsigned int time, Unit *target_ptr) {
1262 if (this->timer.update(time)) {
1263 this->heal(*target_ptr);
1264 }
1265
1266 // inc frame
1267 this->frame += time * this->current_graphics().at(graphic)->frame_count * 1.0f / this->timer.get_interval();
1268 }
1269
completed_in_range(Unit * target_ptr) const1270 bool HealAction::completed_in_range(Unit *target_ptr) const {
1271 auto &hp = target_ptr->get_attribute<attr_type::hitpoints>();
1272 auto &dm = target_ptr->get_attribute<attr_type::damaged>();
1273 return dm.hp >= hp.hp; // is unit at full hitpoints?
1274 }
1275
heal(Unit & target)1276 void HealAction::heal(Unit &target) {
1277 auto &heal = this->entity->get_attribute<attr_type::heal>();
1278
1279 // TODO move to seperate function heal_unit (like damage_unit)?
1280 // heal object
1281 if (target.has_attribute(attr_type::hitpoints) && target.has_attribute(attr_type::damaged)) {
1282 auto &hp = target.get_attribute<attr_type::hitpoints>();
1283 auto &dm = target.get_attribute<attr_type::damaged>();
1284 if ((dm.hp + heal.life) < hp.hp) {
1285 dm.hp += heal.life;
1286 }
1287 else {
1288 dm.hp = hp.hp;
1289 }
1290 }
1291
1292 }
1293
1294
ConvertAction(Unit * e,UnitReference tar)1295 ConvertAction::ConvertAction(Unit *e, UnitReference tar)
1296 :
1297 TargetAction{e, graphic_type::attack, tar},
1298 complete{.0f} {
1299 }
1300
update_in_range(unsigned int,Unit *)1301 void ConvertAction::update_in_range(unsigned int, Unit *) {}
1302
ProjectileAction(Unit * e,coord::phys3 target)1303 ProjectileAction::ProjectileAction(Unit *e, coord::phys3 target)
1304 :
1305 UnitAction{e, graphic_type::standing},
1306 has_hit{false} {
1307
1308 // find speed to move
1309 auto &sp_attr = this->entity->get_attribute<attr_type::speed>();
1310 double projectile_speed = sp_attr.unit_speed.to_double();
1311
1312 // arc of projectile
1313 auto &pr_attr = this->entity->get_attribute<attr_type::projectile>();
1314 float projectile_arc = pr_attr.projectile_arc;
1315
1316 // distance and time to target
1317 coord::phys3_delta d = target - this->entity->location->pos.draw;
1318 double distance_to_target = d.length();
1319 double flight_time = distance_to_target / projectile_speed;
1320
1321
1322 if (projectile_arc < 0) {
1323 // TODO negative values probably indicate something
1324 projectile_arc += 0.2;
1325 }
1326
1327 // now figure gravity from arc parameter
1328 // TODO projectile arc is the ratio between horizontal and
1329 // vertical components of the initial direction
1330 this->grav = 0.01f * (exp(pow(projectile_arc, 0.5f)) - 1) * projectile_speed;
1331
1332 // inital launch direction
1333 auto &d_attr = this->entity->get_attribute<attr_type::direction>();
1334 d_attr.unit_dir = d * (projectile_speed / distance_to_target);
1335
1336 // account for initial height
1337 coord::phys_t initial_height = this->entity->location->pos.draw.up;
1338 d_attr.unit_dir.up = coord::phys_t((grav * flight_time) / 2) - (initial_height * (1 / flight_time));
1339 }
1340
~ProjectileAction()1341 ProjectileAction::~ProjectileAction() {}
1342
update(unsigned int time)1343 void ProjectileAction::update(unsigned int time) {
1344 auto &d_attr = this->entity->get_attribute<attr_type::direction>();
1345
1346 // apply gravity
1347 d_attr.unit_dir.up -= this->grav * time;
1348
1349 coord::phys3 new_position = this->entity->location->pos.draw + d_attr.unit_dir * time;
1350 if (!this->entity->location->move(new_position)) {
1351
1352 // TODO implement friendly_fire (now friendly_fire is always on), attack_attribute.friendly_fire
1353
1354 // find object which was hit
1355 auto terrain = this->entity->location->get_terrain();
1356 TileContent *tc = terrain->get_data(new_position.to_tile3().to_tile());
1357 if (tc && !tc->obj.empty()) {
1358 for (auto obj_location : tc->obj) {
1359 if (this->entity->location.get() != obj_location &&
1360 obj_location->check_collisions()) {
1361 this->damage_unit(obj_location->unit);
1362 break;
1363 }
1364 }
1365 }
1366
1367 // TODO implement area of effect, attack_attribute.area_of_effect
1368
1369 has_hit = true;
1370 }
1371
1372 // inc frame
1373 this->frame += time * this->frame_rate;
1374 }
1375
on_completion()1376 void ProjectileAction::on_completion() {}
1377
completed() const1378 bool ProjectileAction::completed() const {
1379 return (has_hit || this->entity->location->pos.draw.up <= 0);
1380 }
1381
1382 } // namespace openage
1383