1 // Copyright 2014-2018 the openage authors. See copying.md for legal info.
2
3 #include <initializer_list>
4
5 #include "../engine.h"
6 #include "../log/log.h"
7 #include "../gamedata/unit.gen.h"
8 #include "../terrain/terrain.h"
9 #include "../terrain/terrain_object.h"
10 #include "../terrain/terrain_outline.h"
11 #include "../util/strings.h"
12 #include "ability.h"
13 #include "action.h"
14 #include "producer.h"
15 #include "unit.h"
16 #include "unit_texture.h"
17
18 /** @file
19 * Many values in this file are hardcoded, due to limited understanding of how the original
20 * game files work -- as more becomes known these will be removed.
21 *
22 * It is likely the conversion from gamedata to openage units will be done by the nyan
23 * system in future
24 */
25
26 namespace openage {
27
allowed_terrains(const gamedata::ground_type & restriction)28 std::unordered_set<terrain_t> allowed_terrains(const gamedata::ground_type &restriction) {
29 std::unordered_set<terrain_t> result;
30
31 // 1, 14, and 15 are water, 2 is shore
32 if (restriction == gamedata::ground_type::WATER ||
33 restriction == gamedata::ground_type::WATER_0x0D ||
34 restriction == gamedata::ground_type::WATER_SHIP_0x03 ||
35 restriction == gamedata::ground_type::WATER_SHIP_0x0F) {
36 result.insert(1);
37 result.insert(2);
38 result.insert(14);
39 result.insert(15);
40 }
41 else {
42 for (int i = 0; i < 32; ++i) {
43 if (i != 1 && i != 14 && i != 15) {
44 result.insert(i);
45 }
46 }
47 }
48
49 return result;
50 }
51
create_resource_cost(game_resource resource,int amount)52 ResourceBundle create_resource_cost(game_resource resource, int amount) {
53 ResourceBundle resources = ResourceBundle();
54 resources[resource] = amount;
55 return resources;
56 }
57
ObjectProducer(const Player & owner,const GameSpec & spec,const gamedata::unit_object * ud)58 ObjectProducer::ObjectProducer(const Player &owner, const GameSpec &spec, const gamedata::unit_object *ud)
59 :
60 UnitType(owner),
61 dataspec(spec),
62 unit_data(*ud),
63 terrain_outline{nullptr},
64 default_tex{spec.get_unit_texture(ud->graphic_standing0)},
65 dead_unit_id{ud->dead_unit_id} {
66
67 // copy the class type
68 this->unit_class = this->unit_data.unit_class;
69
70 // for now just look for type names ending with "_D"
71 this->decay = unit_data.name.substr(unit_data.name.length() - 2) == "_D";
72
73 // find suitable sounds
74 int creation_sound = this->unit_data.train_sound;
75 int dying_sound = this->unit_data.sound_dying;
76 if (creation_sound == -1) {
77 creation_sound = this->unit_data.damage_sound;
78 }
79 if (creation_sound == -1) {
80 creation_sound = this->unit_data.sound_selection;
81 }
82 if (dying_sound == -1) {
83 dying_sound = 323; //generic explosion sound
84 }
85 on_create = spec.get_sound(creation_sound);
86 on_destroy = spec.get_sound(dying_sound);
87
88 // convert the float to the discrete foundation size...
89 this->foundation_size = {
90 static_cast<int>(this->unit_data.radius_x * 2),
91 static_cast<int>(this->unit_data.radius_y * 2),
92 };
93
94 // shape of the outline
95 if (this->unit_data.selection_shape > 1) {
96 this->terrain_outline = radial_outline(this->unit_data.radius_x);
97 }
98 else {
99 this->terrain_outline = square_outline(this->foundation_size);
100 }
101
102 // graphic set
103 auto standing = spec.get_unit_texture(this->unit_data.graphic_standing0);
104 if (!standing) {
105
106 // indicates problems with data converion
107 throw Error(MSG(err) << "Unit id " << this->unit_data.id0
108 << " has invalid graphic data, try reconverting the data");
109 }
110 this->graphics[graphic_type::standing] = standing;
111 auto dying_tex = spec.get_unit_texture(this->unit_data.dying_graphic);
112 if (dying_tex) {
113 this->graphics[graphic_type::dying] = dying_tex;
114 }
115
116 // default extra graphics
117 this->graphics[graphic_type::attack] = this->graphics[graphic_type::standing];
118 this->graphics[graphic_type::work] = this->graphics[graphic_type::standing];
119
120 // pull extra graphics from unit commands
121 auto cmds = spec.get_command_data(this->unit_data.id0);
122 for (auto cmd : cmds) {
123
124 // same attack / work graphic
125 if (cmd->work_sprite_id == -1 && cmd->proceed_sprite_id > 0) {
126 auto task = spec.get_unit_texture(cmd->proceed_sprite_id);
127 if (task) {
128 this->graphics[graphic_type::work] = task;
129 this->graphics[graphic_type::attack] = task;
130 }
131 }
132
133 // seperate work and attack graphics
134 if (cmd->work_sprite_id > 0 && cmd->proceed_sprite_id > 0 ) {
135 auto attack = spec.get_unit_texture(cmd->proceed_sprite_id);
136 auto work = spec.get_unit_texture(cmd->work_sprite_id);
137 if (attack) {
138 this->graphics[graphic_type::attack] = attack;
139 }
140 if (work) {
141 this->graphics[graphic_type::work] = work;
142 }
143 }
144
145 // villager carrying resources graphics
146 if (cmd->carry_sprite_id > 0) {
147 auto carry = spec.get_unit_texture(cmd->carry_sprite_id);
148 this->graphics[graphic_type::carrying] = carry;
149 }
150 }
151
152 // TODO get cost, temp fixed cost of 50 food
153 this->cost.set(cost_type::constant, create_resource_cost(game_resource::food, 50));
154
155 }
156
~ObjectProducer()157 ObjectProducer::~ObjectProducer() {}
158
id() const159 int ObjectProducer::id() const {
160 return this->unit_data.id0;
161 }
162
parent_id() const163 int ObjectProducer::parent_id() const {
164 int uid = this->unit_data.id0;
165
166 // male types
167 if (uid == 156 || uid == 120 || uid == 592 || uid == 123 || uid == 579 || uid == 124) {
168 return 83;
169 }
170
171 // female types
172 else if (uid == 222 || uid == 354 || uid == 590 || uid == 218 || uid == 581 || uid == 220) {
173 return 293;
174 }
175 return uid;
176 }
177
name() const178 std::string ObjectProducer::name() const {
179 return this->unit_data.name;
180 }
181
initialise(Unit * unit,Player & player)182 void ObjectProducer::initialise(Unit *unit, Player &player) {
183 ENSURE(this->owner == player, "unit init from a UnitType of a wrong player which breaks tech levels");
184
185 // log attributes
186 unit->log(MSG(dbg) << "setting unit type " <<
187 this->unit_data.id0 << " " <<
188 this->unit_data.name);
189
190 // reset existing attributes
191 unit->reset();
192
193 // initialise unit
194 unit->unit_type = this;
195
196 // colour
197 unit->add_attribute(std::make_shared<Attribute<attr_type::owner>>(player));
198
199 // hitpoints if available
200 if (this->unit_data.hit_points > 0) {
201 unit->add_attribute(std::make_shared<Attribute<attr_type::hitpoints>>(this->unit_data.hit_points));
202 unit->add_attribute(std::make_shared<Attribute<attr_type::damaged>>(this->unit_data.hit_points));
203 }
204
205 // collectable resources
206 if (this->unit_data.unit_class == gamedata::unit_classes::TREES) {
207 unit->add_attribute(std::make_shared<Attribute<attr_type::resource>>(game_resource::wood, 125));
208 }
209 else if (this->unit_data.unit_class == gamedata::unit_classes::BERRY_BUSH) {
210 unit->add_attribute(std::make_shared<Attribute<attr_type::resource>>(game_resource::food, 100));
211 }
212 else if (this->unit_data.unit_class == gamedata::unit_classes::SEA_FISH) {
213 unit->add_attribute(std::make_shared<Attribute<attr_type::resource>>(game_resource::food, 200));
214 }
215 else if (this->unit_data.unit_class == gamedata::unit_classes::PREY_ANIMAL) {
216 unit->add_attribute(std::make_shared<Attribute<attr_type::resource>>(game_resource::food, 140));
217 }
218 else if (this->unit_data.unit_class == gamedata::unit_classes::SHEEP) {
219 unit->add_attribute(std::make_shared<Attribute<attr_type::resource>>(game_resource::food, 100, 0.1));
220 }
221 else if (this->unit_data.unit_class == gamedata::unit_classes::GOLD_MINE) {
222 unit->add_attribute(std::make_shared<Attribute<attr_type::resource>>(game_resource::gold, 800));
223 }
224 else if (this->unit_data.unit_class == gamedata::unit_classes::STONE_MINE) {
225 unit->add_attribute(std::make_shared<Attribute<attr_type::resource>>(game_resource::stone, 350));
226 }
227
228 // decaying units have a timed lifespan
229 if (decay) {
230 unit->push_action(std::make_unique<DecayAction>(unit), true);
231 }
232 else {
233 // if destruction graphic is available
234 if (this->dead_unit_id) {
235 unit->push_action(
236 std::make_unique<DeadAction>(
237 unit,
238 [this, unit, &player]() {
239
240 // modify unit to have dead type
241 UnitType *t = player.get_type(this->dead_unit_id);
242 if (t) {
243 t->initialise(unit, player);
244 }
245 }
246 ),
247 true);
248 }
249 else if (this->graphics.count(graphic_type::dying) > 0) {
250 unit->push_action(std::make_unique<DeadAction>(unit), true);
251 }
252
253 // the default action
254 unit->push_action(std::make_unique<IdleAction>(unit), true);
255 }
256
257 // give required abilitys
258 for (auto &a : this->type_abilities) {
259 unit->give_ability(a);
260 }
261 }
262
place(Unit * u,std::shared_ptr<Terrain> terrain,coord::phys3 init_pos) const263 TerrainObject *ObjectProducer::place(Unit *u, std::shared_ptr<Terrain> terrain, coord::phys3 init_pos) const {
264
265 // create new object with correct base shape
266 if (this->unit_data.selection_shape > 1) {
267 u->make_location<RadialObject>(this->unit_data.radius_x, this->terrain_outline);
268 }
269 else {
270 u->make_location<SquareObject>(this->foundation_size, this->terrain_outline);
271 }
272
273 // find set of allowed terrains
274 std::unordered_set<terrain_t> terrains = allowed_terrains(this->unit_data.terrain_restriction);
275
276 /*
277 * decide what terrain is passable using this lambda
278 * currently unit avoids water and tiles with another unit
279 * this function should be true if pos is a valid position of the object
280 */
281 TerrainObject *obj_ptr = u->location.get();
282 std::weak_ptr<Terrain> terrain_ptr = terrain;
283 u->location->passable = [obj_ptr, terrain_ptr, terrains](const coord::phys3 &pos) -> bool {
284
285 // if location is deleted, then so is this lambda (deleting terrain implies location is deleted)
286 // so locking objects here will not return null
287 auto terrain = terrain_ptr.lock();
288
289 // look at all tiles in the bases range
290 for (coord::tile check_pos : tile_list(obj_ptr->get_range(pos, *terrain))) {
291 TileContent *tc = terrain->get_data(check_pos);
292
293 // invalid tile types
294 if (!tc || terrains.count(tc->terrain_id) == 0) {
295 return false;
296 }
297
298 // compare with objects intersecting the units tile
299 // ensure no intersections with other objects
300 for (auto obj_cmp : tc->obj) {
301 if (obj_ptr != obj_cmp &&
302 obj_cmp->check_collisions() &&
303 obj_ptr->intersects(*obj_cmp, pos)) {
304 return false;
305 }
306 }
307 }
308 return true;
309 };
310
311 u->location->draw = [u, obj_ptr](const Engine &e) {
312 if (u->selected) {
313 obj_ptr->draw_outline(e.coord);
314 }
315 u->draw(e);
316 };
317
318 // try to place the obj, it knows best whether it will fit.
319 auto state = this->decay? object_state::placed_no_collision : object_state::placed;
320 if (u->location->place(terrain, init_pos, state)) {
321 if (this->on_create) {
322 this->on_create->play();
323 }
324 return u->location.get();
325 }
326
327 // placing at the given position failed
328 u->log(MSG(dbg) << "failed to place object");
329 return nullptr;
330 }
331
MovableProducer(const Player & owner,const GameSpec & spec,const gamedata::projectile_unit * um)332 MovableProducer::MovableProducer(const Player &owner, const GameSpec &spec, const gamedata::projectile_unit *um)
333 :
334 ObjectProducer(owner, spec, um),
335 unit_data(*um),
336 on_move{spec.get_sound(this->unit_data.command_sound_id)},
337 on_attack{spec.get_sound(this->unit_data.command_sound_id)},
338 projectile{this->unit_data.missile_unit_id} {
339
340 // extra graphics if available
341 // villagers have invalid attack and walk graphics
342 // it seems these come from the command data instead
343 auto walk = spec.get_unit_texture(this->unit_data.walking_graphics0);
344 if (!walk) {
345
346 // use standing instead
347 walk = this->graphics[graphic_type::standing];
348 }
349 this->graphics[graphic_type::walking] = walk;
350
351 // reuse as carry graphic if not already set
352 if (this->graphics.count(graphic_type::carrying) == 0) {
353 this->graphics[graphic_type::carrying] = walk;
354 }
355
356 auto attack = spec.get_unit_texture(this->unit_data.fight_sprite_id);
357 if (attack && attack->is_valid()) {
358 this->graphics[graphic_type::attack] = attack;
359 }
360
361 // extra abilities
362 this->type_abilities.emplace_back(std::make_shared<MoveAbility>(this->on_move));
363 this->type_abilities.emplace_back(std::make_shared<AttackAbility>(this->on_attack));
364 }
365
~MovableProducer()366 MovableProducer::~MovableProducer() {}
367
initialise(Unit * unit,Player & player)368 void MovableProducer::initialise(Unit *unit, Player &player) {
369
370 /*
371 * call base function
372 */
373 ObjectProducer::initialise(unit, player);
374
375 /*
376 * basic attributes
377 */
378 if (!unit->has_attribute(attr_type::direction)) {
379 unit->add_attribute(std::make_shared<Attribute<attr_type::direction>>(coord::phys3_delta{ 1, 0, 0 }));
380 }
381
382 /*
383 * distance per millisecond -- consider original game speed
384 * where 1.5 in game seconds pass in 1 real second
385 */
386 coord::phys_t sp = this->unit_data.speed / 666;
387 unit->add_attribute(std::make_shared<Attribute<attr_type::speed>>(sp));
388
389 // projectile of melee attacks
390 UnitType *proj_type = this->owner.get_type(this->projectile);
391 if (this->unit_data.missile_unit_id > 0 && proj_type) {
392
393 // calculate requirements for ranged attacks
394 coord::phys_t range_phys = this->unit_data.weapon_range_max;
395 unit->add_attribute(std::make_shared<Attribute<attr_type::attack>>(proj_type, range_phys, 48000, 1));
396 }
397 else {
398 unit->add_attribute(std::make_shared<Attribute<attr_type::attack>>(nullptr, 0, 0, 1));
399 }
400 unit->add_attribute(std::make_shared<Attribute<attr_type::formation>>());
401 }
402
place(Unit * unit,std::shared_ptr<Terrain> terrain,coord::phys3 init_pos) const403 TerrainObject *MovableProducer::place(Unit *unit, std::shared_ptr<Terrain> terrain, coord::phys3 init_pos) const {
404 return ObjectProducer::place(unit, terrain, init_pos);
405 }
406
LivingProducer(const Player & owner,const GameSpec & spec,const gamedata::living_unit * ud)407 LivingProducer::LivingProducer(const Player &owner, const GameSpec &spec, const gamedata::living_unit *ud)
408 :
409 MovableProducer(owner, spec, ud),
410 unit_data(*ud) {
411
412 // extra abilities
413 this->type_abilities.emplace_back(std::make_shared<GarrisonAbility>(this->on_move));
414 }
415
~LivingProducer()416 LivingProducer::~LivingProducer() {}
417
initialise(Unit * unit,Player & player)418 void LivingProducer::initialise(Unit *unit, Player &player) {
419
420 /*
421 * call base function
422 */
423 MovableProducer::initialise(unit, player);
424
425 // population of 1 for all movable units
426 if (this->unit_data.unit_class != gamedata::unit_classes::SHEEP) {
427 unit->add_attribute(std::make_shared<Attribute<attr_type::population>>(1, 0));
428 }
429
430 // add worker attributes
431 if (this->unit_data.unit_class == gamedata::unit_classes::CIVILIAN) {
432 unit->add_attribute(std::make_shared<Attribute<attr_type::worker>>());
433 unit->add_attribute(std::make_shared<Attribute<attr_type::resource>>());
434 unit->add_attribute(std::make_shared<Attribute<attr_type::multitype>>());
435
436 // add graphic ids for resource actions
437 auto &worker_attr = unit->get_attribute<attr_type::worker>();
438 worker_attr.capacity = 10.0;
439 worker_attr.gather_rate[game_resource::wood] = 0.002;
440 worker_attr.gather_rate[game_resource::food] = 0.002;
441 worker_attr.gather_rate[game_resource::gold] = 0.002;
442 worker_attr.gather_rate[game_resource::stone] = 0.002;
443
444 auto &multitype_attr = unit->get_attribute<attr_type::multitype>();
445 // currently not sure where the game data keeps these values
446 // todo PREY_ANIMAL SEA_FISH
447 if (this->parent_id() == 83) {
448
449 // male graphics
450 multitype_attr.types[gamedata::unit_classes::CIVILIAN] = this->parent_type(); // get default villager
451 multitype_attr.types[gamedata::unit_classes::BUILDING] = this->owner.get_type(156); // builder 118
452 multitype_attr.types[gamedata::unit_classes::BERRY_BUSH] = this->owner.get_type(120); // forager
453 multitype_attr.types[gamedata::unit_classes::SHEEP] = this->owner.get_type(592); // sheperd
454 multitype_attr.types[gamedata::unit_classes::TREES] = this->owner.get_type(123); // woodcutter
455 multitype_attr.types[gamedata::unit_classes::GOLD_MINE] = this->owner.get_type(579); // gold miner
456 multitype_attr.types[gamedata::unit_classes::STONE_MINE] = this->owner.get_type(124); // stone miner
457
458 }
459 else {
460
461 // female graphics
462 multitype_attr.types[gamedata::unit_classes::CIVILIAN] = this->parent_type(); // get default villager
463 multitype_attr.types[gamedata::unit_classes::BUILDING] = this->owner.get_type(222); // builder 212
464 multitype_attr.types[gamedata::unit_classes::BERRY_BUSH] = this->owner.get_type(354); // forager
465 multitype_attr.types[gamedata::unit_classes::SHEEP] = this->owner.get_type(590); // sheperd
466 multitype_attr.types[gamedata::unit_classes::TREES] = this->owner.get_type(218); // woodcutter
467 multitype_attr.types[gamedata::unit_classes::GOLD_MINE] = this->owner.get_type(581); // gold miner
468 multitype_attr.types[gamedata::unit_classes::STONE_MINE] = this->owner.get_type(220); // stone miner
469 }
470 unit->give_ability(std::make_shared<GatherAbility>(this->on_attack));
471 unit->give_ability(std::make_shared<BuildAbility>(this->on_attack));
472 unit->give_ability(std::make_shared<RepairAbility>(this->on_attack));
473 }
474 else if (this->unit_data.unit_class == gamedata::unit_classes::FISHING_BOAT) {
475 unit->add_attribute(std::make_shared<Attribute<attr_type::worker>>());
476 unit->add_attribute(std::make_shared<Attribute<attr_type::resource>>());
477
478 // add fishing abilites
479 auto &worker_attr = unit->get_attribute<attr_type::worker>();
480 worker_attr.capacity = 15.0;
481 worker_attr.gather_rate[game_resource::food] = 0.002;
482
483 unit->give_ability(std::make_shared<GatherAbility>(this->on_attack));
484 }
485 }
486
place(Unit * unit,std::shared_ptr<Terrain> terrain,coord::phys3 init_pos) const487 TerrainObject *LivingProducer::place(Unit *unit, std::shared_ptr<Terrain> terrain, coord::phys3 init_pos) const {
488 return MovableProducer::place(unit, terrain, init_pos);
489 }
490
BuildingProducer(const Player & owner,const GameSpec & spec,const gamedata::building_unit * ud)491 BuildingProducer::BuildingProducer(const Player &owner, const GameSpec &spec, const gamedata::building_unit *ud)
492 :
493 UnitType(owner),
494 unit_data{*ud},
495 texture{spec.get_unit_texture(ud->graphic_standing0)},
496 destroyed{spec.get_unit_texture(ud->dying_graphic)},
497 projectile{this->unit_data.missile_unit_id},
498 foundation_terrain{ud->foundation_terrain_id},
499 enable_collisions{this->unit_data.id0 != 109} { // 109 = town center
500
501 // copy the class type
502 this->unit_class = this->unit_data.unit_class;
503
504 // find suitable sounds
505 int creation_sound = this->unit_data.train_sound;
506 int dying_sound = this->unit_data.sound_dying;
507 if (creation_sound == -1) {
508 creation_sound = this->unit_data.damage_sound;
509 }
510 if (creation_sound == -1) {
511 creation_sound = this->unit_data.sound_selection;
512 }
513 if (dying_sound == -1) {
514 dying_sound = 323; //generic explosion sound
515 }
516 on_create = spec.get_sound(creation_sound);
517 on_destroy = spec.get_sound(dying_sound);
518
519 // convert the float to the discrete foundation size...
520 this->foundation_size = {
521 static_cast<int>(this->unit_data.radius_x * 2),
522 static_cast<int>(this->unit_data.radius_y * 2),
523 };
524
525 // graphic set
526 this->graphics[graphic_type::construct] = spec.get_unit_texture(ud->construction_graphic_id);
527 this->graphics[graphic_type::standing] = spec.get_unit_texture(ud->graphic_standing0);
528 this->graphics[graphic_type::attack] = spec.get_unit_texture(ud->graphic_standing0);
529 auto dying_tex = spec.get_unit_texture(ud->dying_graphic);
530 if (dying_tex) {
531 this->graphics[graphic_type::dying] = dying_tex;
532 }
533
534 this->terrain_outline = square_outline(this->foundation_size);
535
536 // TODO get cost, temp fixed cost of 100 wood
537 this->cost.set(cost_type::constant, create_resource_cost(game_resource::wood, 100));
538 }
539
~BuildingProducer()540 BuildingProducer::~BuildingProducer() {}
541
id() const542 int BuildingProducer::id() const {
543 return this->unit_data.id0;
544 }
545
parent_id() const546 int BuildingProducer::parent_id() const {
547 return this->unit_data.id0;
548 }
549
name() const550 std::string BuildingProducer::name() const {
551 return this->unit_data.name;
552 }
553
initialise(Unit * unit,Player & player)554 void BuildingProducer::initialise(Unit *unit, Player &player) {
555 ENSURE(this->owner == player, "unit init from a UnitType of a wrong player which breaks tech levels");
556
557 // log type
558 unit->log(MSG(dbg) << "setting unit type " <<
559 this->unit_data.id0 << " " <<
560 this->unit_data.name);
561
562 // initialize graphic set
563 unit->unit_type = this;
564
565 auto player_attr = std::make_shared<Attribute<attr_type::owner>>(player);
566 unit->add_attribute(player_attr);
567
568 // building specific attribute
569 auto build_attr = std::make_shared<Attribute<attr_type::building>>(
570 this->foundation_terrain,
571 this->owner.get_type(293), // fem_villager, male is 83
572 unit->location->pos.draw
573 );
574 build_attr->completion_state = this->enable_collisions ? object_state::placed : object_state::placed_no_collision;
575 unit->add_attribute(build_attr);
576
577 // garrison and hp for all buildings
578 unit->add_attribute(std::make_shared<Attribute<attr_type::garrison>>());
579 unit->add_attribute(std::make_shared<Attribute<attr_type::hitpoints>>(this->unit_data.hit_points));
580 unit->add_attribute(std::make_shared<Attribute<attr_type::damaged>>(this->unit_data.hit_points));
581
582 // population
583 if (this->id() == 109 || this->id() == 70) { // Town center, House
584 unit->add_attribute(std::make_shared<Attribute<attr_type::population>>(0, 5));
585 }
586 else if (this->id() == 82) { // Castle
587 unit->add_attribute(std::make_shared<Attribute<attr_type::population>>(0, 20));
588 }
589
590 // limits
591 if (this->id() == 109 || this->id() == 276) { // Town center, Wonder
592 this->have_limit = 2; // TODO change to 1, 2 is for testing
593 }
594
595 bool has_destruct_graphic = this->destroyed != nullptr;
596 unit->push_action(std::make_unique<FoundationAction>(unit, has_destruct_graphic), true);
597
598 UnitType *proj_type = this->owner.get_type(this->projectile);
599 if (this->unit_data.missile_unit_id > 0 && proj_type) {
600 coord::phys_t range_phys = this->unit_data.weapon_range_max;
601 unit->add_attribute(std::make_shared<Attribute<attr_type::attack>>(proj_type, range_phys, 350000, 1));
602 // formation is used only for the attack_stance
603 unit->add_attribute(std::make_shared<Attribute<attr_type::formation>>(attack_stance::aggressive));
604 unit->give_ability(std::make_shared<AttackAbility>());
605 }
606
607 // dropsite attribute
608 std::vector<game_resource> accepted_resources = this->get_accepted_resources();
609 if (accepted_resources.size() != 0) {
610 unit->add_attribute(std::make_shared<Attribute<attr_type::dropsite>>(accepted_resources));
611 }
612
613 // building can train new units and ungarrison
614 unit->give_ability(std::make_shared<SetPointAbility>());
615 unit->give_ability(std::make_shared<TrainAbility>());
616 unit->give_ability(std::make_shared<UngarrisonAbility>());
617 }
618
get_accepted_resources()619 std::vector<game_resource> BuildingProducer::get_accepted_resources() {
620 //TODO use a more general approach instead of hard coded ids
621
622 auto id_in = [=](std::initializer_list<int> ids) {
623 return std::any_of(ids.begin(), ids.end(), [=](int n) { return n == this->id(); });
624 };
625
626 if (this->id() == 109) { // Town center
627 return std::vector<game_resource>{
628 game_resource::wood,
629 game_resource::food,
630 game_resource::gold,
631 game_resource::stone
632 };
633 } else if (id_in({584, 585, 586, 587})) { // Mine
634 return std::vector<game_resource>{
635 game_resource::gold,
636 game_resource::stone
637 };
638 } else if (id_in({68, 129, 130, 131})) { // Mill
639 return std::vector<game_resource>{
640 game_resource::food
641 };
642 } else if (id_in({562, 563, 564, 565})) { // Lumberjack camp
643 return std::vector<game_resource>{
644 game_resource::wood
645 };
646 }
647
648 return std::vector<game_resource>();
649 }
650
place(Unit * u,std::shared_ptr<Terrain> terrain,coord::phys3 init_pos) const651 TerrainObject *BuildingProducer::place(Unit *u, std::shared_ptr<Terrain> terrain, coord::phys3 init_pos) const {
652
653 // buildings have a square base
654 u->make_location<SquareObject>(this->foundation_size, this->terrain_outline);
655
656 /*
657 * decide what terrain is passable using this lambda
658 * currently unit avoids water and tiles with another unit
659 * this function should be true if pos is a valid position of the object
660 */
661 TerrainObject *obj_ptr = u->location.get();
662 std::weak_ptr<Terrain> terrain_ptr = terrain;
663 u->location->passable = [obj_ptr, terrain_ptr](const coord::phys3 &pos) -> bool {
664 auto terrain = terrain_ptr.lock();
665
666 // look at all tiles in the bases range
667 for (coord::tile check_pos : tile_list(obj_ptr->get_range(pos, *terrain))) {
668 TileContent *tc = terrain->get_data(check_pos);
669 if (!tc) {
670 return false;
671 }
672 for (auto tobj : tc->obj) {
673 if (tobj->check_collisions()) return false;
674 }
675 }
676 return true;
677 };
678
679 // drawing function
680 bool draw_outline = this->enable_collisions;
681 u->location->draw = [u, obj_ptr, draw_outline](const Engine &e) {
682 if (u->selected && draw_outline) {
683 obj_ptr->draw_outline(e.coord);
684 }
685 u->draw(e);
686 };
687
688 // try to place the obj, it knows best whether it will fit.
689 auto state = object_state::floating;
690 if (!u->location->place(terrain, init_pos, state)) {
691 return nullptr;
692 }
693
694 // annex objects
695 for (unsigned i = 0; i < 4; ++i) {
696 const gamedata::building_annex &annex = this->unit_data.building_annex.data[i];
697 if (annex.unit_id > 0) {
698
699 // make objects for annex
700 coord::phys3 a_pos = u->location->pos.draw;
701 a_pos.ne += annex.misplaced0;
702 a_pos.se += annex.misplaced1;
703 this->make_annex(*u, terrain, annex.unit_id, a_pos, i == 0);
704 }
705 }
706
707 // TODO: play sound once built
708 if (this->on_create) {
709 this->on_create->play();
710 }
711 return u->location.get();
712 }
713
make_annex(Unit & u,std::shared_ptr<Terrain> t,int annex_id,coord::phys3 annex_pos,bool c) const714 TerrainObject *BuildingProducer::make_annex(Unit &u, std::shared_ptr<Terrain> t,
715 int annex_id, coord::phys3 annex_pos, bool c) const {
716
717 // for use in lambda drawing functions
718 auto annex_type = this->owner.get_type(annex_id);
719 if (!annex_type) {
720 u.log(MSG(warn) << "Invalid annex type id " << annex_id);
721 return nullptr;
722 }
723
724 // foundation size
725 coord::tile_delta annex_foundation = annex_type->foundation_size;
726
727 // producers place by the nw tile
728 coord::phys3 start_tile = annex_pos;
729 start_tile.ne -= annex_foundation.ne / 2.0;
730 start_tile.se -= annex_foundation.se / 2.0;
731
732 // create and place on terrain
733 TerrainObject *annex_loc = u.location->make_annex<SquareObject>(annex_foundation);
734 object_state state = c? object_state::placed : object_state::placed_no_collision;
735 annex_loc->place(t, start_tile, state);
736
737 // create special drawing functions for annexes,
738 annex_loc->draw = [annex_loc, annex_type, &u, c](const Engine &e) {
739
740 // hack which draws the outline in the right order
741 // removable once rendering system is improved
742 if (c && u.selected) {
743 annex_loc->get_parent()->draw_outline(e.coord);
744 }
745
746 // only draw if building is completed
747 if (u.has_attribute(attr_type::building) &&
748 u.get_attribute<attr_type::building>().completed >= 1.0f) {
749 u.draw(annex_loc, annex_type->graphics, e);
750 }
751 };
752 return annex_loc;
753 }
754
ProjectileProducer(const Player & owner,const GameSpec & spec,const gamedata::missile_unit * pd)755 ProjectileProducer::ProjectileProducer(const Player &owner, const GameSpec &spec, const gamedata::missile_unit *pd)
756 :
757 UnitType(owner),
758 unit_data{*pd},
759 tex{spec.get_unit_texture(this->unit_data.graphic_standing0)},
760 sh{spec.get_unit_texture(3379)}, // 3379 = general arrow shadow
761 destroyed{spec.get_unit_texture(this->unit_data.dying_graphic)} {
762
763 // copy the class type
764 this->unit_class = this->unit_data.unit_class;
765
766 // graphic set
767 this->graphics[graphic_type::standing] = this->tex;
768 this->graphics[graphic_type::shadow] = this->sh;
769 if (destroyed) {
770 this->graphics[graphic_type::dying] = destroyed;
771 }
772
773 // outline
774 terrain_outline = radial_outline(pd->radius_y);
775 }
776
~ProjectileProducer()777 ProjectileProducer::~ProjectileProducer() {}
778
id() const779 int ProjectileProducer::id() const {
780 return this->unit_data.id0;
781 }
782
parent_id() const783 int ProjectileProducer::parent_id() const {
784 return this->unit_data.id0;
785 }
786
name() const787 std::string ProjectileProducer::name() const {
788 return this->unit_data.name;
789 }
790
initialise(Unit * unit,Player & player)791 void ProjectileProducer::initialise(Unit *unit, Player &player) {
792 ENSURE(this->owner == player, "unit init from a UnitType of a wrong player which breaks tech levels");
793
794 // initialize graphic set
795 unit->unit_type = this;
796
797 auto player_attr = std::make_shared<Attribute<attr_type::owner>>(player);
798 unit->add_attribute(player_attr);
799
800 // projectile speed
801 coord::phys_t sp = this->unit_data.speed / 666;
802 unit->add_attribute(std::make_shared<Attribute<attr_type::speed>>(sp));
803 unit->add_attribute(std::make_shared<Attribute<attr_type::projectile>>(this->unit_data.projectile_arc));
804 unit->add_attribute(std::make_shared<Attribute<attr_type::direction>>(coord::phys3_delta{ 1, 0, 0 }));
805
806 // if destruction graphic is available
807 if (this->destroyed) {
808 unit->push_action(std::make_unique<DeadAction>(unit), true);
809 }
810 }
811
place(Unit * u,std::shared_ptr<Terrain> terrain,coord::phys3 init_pos) const812 TerrainObject *ProjectileProducer::place(Unit *u, std::shared_ptr<Terrain> terrain, coord::phys3 init_pos) const {
813 /*
814 * radial base shape without collision checking
815 */
816 u->make_location<RadialObject>(this->unit_data.radius_y, this->terrain_outline);
817
818 TerrainObject *obj_ptr = u->location.get();
819 std::weak_ptr<Terrain> terrain_ptr = terrain;
820 u->location->passable = [obj_ptr, u, terrain_ptr](const coord::phys3 &pos) -> bool {
821 if (pos.up > 64000) {
822 return true;
823 }
824
825 // avoid intersections with launcher
826 Unit *launcher = nullptr;
827 auto terrain = terrain_ptr.lock();
828 if (u->has_attribute(attr_type::projectile)) {
829 auto &pr_attr = u->get_attribute<attr_type::projectile>();
830 if (pr_attr.launched && pr_attr.launcher.is_valid()) {
831 launcher = pr_attr.launcher.get();
832 }
833 else {
834 return true;
835 }
836 }
837 else {
838 return true;
839 }
840
841 // look at all tiles in the bases range
842 for (coord::tile check_pos : tile_list(obj_ptr->get_range(pos, *terrain))) {
843 TileContent *tc = terrain->get_data(check_pos);
844 if (!tc) return false;
845
846 // ensure no intersections with other objects
847 for (auto obj_cmp : tc->obj) {
848 if (obj_ptr != obj_cmp &&
849 &obj_cmp->unit != launcher &&
850 obj_cmp->check_collisions() &&
851 obj_ptr->intersects(*obj_cmp, pos)) {
852 return false;
853 }
854 }
855 }
856 return true;
857 };
858
859 u->location->draw = [u](const Engine &e) {
860 u->draw(e);
861 };
862
863 // try to place the obj, it knows best whether it will fit.
864 if (u->location->place(terrain, init_pos, object_state::placed_no_collision)) {
865 return u->location.get();
866 }
867 return nullptr;
868 }
869
870 } // namespace openage
871