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