1 /* Copyright (C) 2013-2014 Michal Brzozowski (rusolis@poczta.fm)
2 
3    This file is part of KeeperRL.
4 
5    KeeperRL is free software; you can redistribute it and/or modify it under the terms of the
6    GNU General Public License as published by the Free Software Foundation; either version 2
7    of the License, or (at your option) any later version.
8 
9    KeeperRL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
10    even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11    GNU General Public License for more details.
12 
13    You should have received a copy of the GNU General Public License along with this program.
14    If not, see http://www.gnu.org/licenses/ . */
15 
16 #include "stdafx.h"
17 
18 #include "player_control.h"
19 #include "level.h"
20 #include "task.h"
21 #include "model.h"
22 #include "statistics.h"
23 #include "options.h"
24 #include "technology.h"
25 #include "village_control.h"
26 #include "item.h"
27 #include "item_factory.h"
28 #include "creature.h"
29 #include "square.h"
30 #include "view_id.h"
31 #include "collective.h"
32 #include "effect.h"
33 #include "music.h"
34 #include "encyclopedia.h"
35 #include "map_memory.h"
36 #include "item_action.h"
37 #include "equipment.h"
38 #include "collective_teams.h"
39 #include "minion_equipment.h"
40 #include "task_map.h"
41 #include "construction_map.h"
42 #include "minion_task_map.h"
43 #include "spell.h"
44 #include "tribe.h"
45 #include "visibility_map.h"
46 #include "creature_name.h"
47 #include "view.h"
48 #include "view_index.h"
49 #include "collective_attack.h"
50 #include "territory.h"
51 #include "sound.h"
52 #include "game.h"
53 #include "collective_name.h"
54 #include "creature_attributes.h"
55 #include "collective_config.h"
56 #include "villain_type.h"
57 #include "workshops.h"
58 #include "attack_trigger.h"
59 #include "view_object.h"
60 #include "body.h"
61 #include "furniture.h"
62 #include "furniture_type.h"
63 #include "furniture_factory.h"
64 #include "known_tiles.h"
65 #include "tile_efficiency.h"
66 #include "zones.h"
67 #include "inventory.h"
68 #include "immigration.h"
69 #include "scroll_position.h"
70 #include "tutorial.h"
71 #include "tutorial_highlight.h"
72 #include "container_range.h"
73 #include "trap_type.h"
74 #include "collective_warning.h"
75 #include "furniture_usage.h"
76 #include "message_generator.h"
77 #include "message_buffer.h"
78 #include "minion_controller.h"
79 #include "build_info.h"
80 #include "vision.h"
81 #include "external_enemies.h"
82 #include "resource_info.h"
83 #include "workshop_item.h"
84 
85 
86 template <class Archive>
serialize(Archive & ar,const unsigned int version)87 void PlayerControl::serialize(Archive& ar, const unsigned int version) {
88   ar& SUBCLASS(CollectiveControl) & SUBCLASS(EventListener);
89   ar(memory, introText, lastControlKeeperQuestion);
90   ar(newAttacks, ransomAttacks, messages, hints, visibleEnemies);
91   ar(visibilityMap);
92   ar(messageHistory, tutorial, controlModeMessages);
93 }
94 
95 SERIALIZABLE(PlayerControl)
96 
97 SERIALIZATION_CONSTRUCTOR_IMPL(PlayerControl)
98 
99 using ResourceId = Collective::ResourceId;
100 
101 const int hintFrequency = 700;
getHints()102 static vector<string> getHints() {
103   return {
104     "Research geology to uncover ores in the mountain.",
105     "Morale affects minion productivity and chances of fleeing from battle.",
106  //   "You can turn these hints off in the settings (F2).",
107 //    "Killing a leader greatly lowers the morale of his tribe and stops immigration.",
108 //    "Your minions' morale is boosted when they are commanded by the Keeper.",
109   };
110 }
111 
PlayerControl(Private,WCollective col)112 PlayerControl::PlayerControl(Private, WCollective col) : CollectiveControl(col), hints(getHints()) {
113   controlModeMessages = make_shared<MessageBuffer>();
114   visibilityMap = make_shared<VisibilityMap>();
115   bool hotkeys[128] = {0};
116   for (auto& info : BuildInfo::get()) {
117     if (info.hotkey) {
118       CHECK(!hotkeys[int(info.hotkey)]);
119       hotkeys[int(info.hotkey)] = true;
120     }
121   }
122   for (TechInfo info : getTechInfo()) {
123     if (info.button.hotkey) {
124       CHECK(!hotkeys[int(info.button.hotkey)]);
125       hotkeys[int(info.button.hotkey)] = true;
126     }
127   }
128   memory.reset(new MapMemory());
129 }
130 
create(WCollective col,vector<string> introText)131 PPlayerControl PlayerControl::create(WCollective col, vector<string> introText) {
132   auto ret = makeOwner<PlayerControl>(Private{}, col);
133   ret->subscribeTo(col->getLevel()->getModel());
134   ret->introText = introText;
135   return ret;
136 }
137 
~PlayerControl()138 PlayerControl::~PlayerControl() {
139 }
140 
getControlled() const141 const vector<WCreature>& PlayerControl::getControlled() const {
142   return getGame()->getPlayerCreatures();
143 }
144 
getCurrentTeam() const145 optional<TeamId> PlayerControl::getCurrentTeam() const {
146   for (TeamId team : getTeams().getAllActive())
147     if (getTeams().getLeader(team)->isPlayer())
148       return team;
149   return none;
150 }
151 
onControlledKilled(WConstCreature victim)152 void PlayerControl::onControlledKilled(WConstCreature victim) {
153   TeamId currentTeam = *getCurrentTeam();
154   if (getTeams().getLeader(currentTeam) == victim) {
155     vector<CreatureInfo> team;
156     for (auto c : getTeams().getMembers(currentTeam))
157       if (c != victim)
158         team.push_back(CreatureInfo(c));
159     if (team.empty())
160       return;
161     optional<Creature::Id> newLeader;
162     if (team.size() == 1)
163       newLeader = team[0].uniqueId;
164     else
165       newLeader = getView()->chooseCreature("Choose new team leader:", team, "Order team back to base");
166     if (newLeader) {
167       if (WCreature c = getCreature(*newLeader)) {
168         getTeams().setLeader(currentTeam, c);
169         if (!c->isPlayer())
170           c->pushController(createMinionController(c));
171         return;
172       }
173     }
174     leaveControl();
175   }
176 }
177 
onSunlightVisibilityChanged()178 void PlayerControl::onSunlightVisibilityChanged() {
179   for (auto pos : getCollective()->getConstructions().getBuiltPositions(FurnitureType::EYEBALL))
180     visibilityMap->updateEyeball(pos);
181 }
182 
setTutorial(STutorial t)183 void PlayerControl::setTutorial(STutorial t) {
184   tutorial = t;
185 }
186 
getTutorial() const187 STutorial PlayerControl::getTutorial() const {
188   return tutorial;
189 }
190 
swapTeam()191 bool PlayerControl::swapTeam() {
192   if (auto teamId = getCurrentTeam())
193     if (getTeams().getMembers(*teamId).size() > 1) {
194       auto controlled = getControlled();
195       if (controlled.size() == 1) {
196         vector<CreatureInfo> team;
197         TeamId currentTeam = *getCurrentTeam();
198         for (auto c : getTeams().getMembers(currentTeam))
199           if (!c->isPlayer())
200             team.push_back(CreatureInfo(c));
201         if (team.empty())
202           return false;
203         if (auto newLeader = getView()->chooseCreature("Choose new team leader:", team, "Cancel"))
204           if (WCreature c = getCreature(*newLeader)) {
205             getTeams().getLeader(*teamId)->popController();
206             getTeams().setLeader(*teamId, c);
207             c->pushController(createMinionController(c));
208           }
209         return true;
210       }
211     }
212   return false;
213 }
214 
leaveControl()215 void PlayerControl::leaveControl() {
216   set<TeamId> allTeams;
217   for (auto controlled : copyOf(getControlled())) {
218     if (controlled == getKeeper())
219       lastControlKeeperQuestion = getCollective()->getGlobalTime();
220     if (!controlled->getPosition().isSameLevel(getLevel()))
221       getView()->setScrollPos(getPosition());
222     controlled->popController();
223     for (TeamId team : getTeams().getActive(controlled))
224       allTeams.insert(team);
225   }
226   for (auto team : allTeams) {
227     for (WCreature c : getTeams().getMembers(team))
228 //      if (getGame()->canTransferCreature(c, getCollective()->getLevel()->getModel()))
229         getGame()->transferCreature(c, getModel());
230     if (!getTeams().isPersistent(team)) {
231       if (getTeams().getMembers(team).size() == 1)
232         getTeams().cancel(team);
233       else
234         getTeams().deactivate(team);
235       break;
236     }
237   }
238   getView()->stopClock();
239 }
240 
render(View * view)241 void PlayerControl::render(View* view) {
242   if (firstRender) {
243     firstRender = false;
244     initialize();
245   }
246   if (getControlled().empty()) {
247     ViewObject::setHallu(false);
248     view->updateView(this, false);
249   }
250   if (!introText.empty() && getGame()->getOptions()->getBoolValue(OptionId::HINTS)) {
251     view->updateView(this, false);
252     for (auto& msg : introText)
253       view->presentText("", msg);
254     introText.clear();
255   }
256 }
257 
isTurnBased()258 bool PlayerControl::isTurnBased() {
259   return !getControlled().empty();
260 }
261 
addConsumableItem(WCreature creature)262 void PlayerControl::addConsumableItem(WCreature creature) {
263   ScrollPosition scrollPos;
264   while (1) {
265     WItem chosenItem = chooseEquipmentItem(creature, {}, [&](WConstItem it) {
266         return !getCollective()->getMinionEquipment().isOwner(it, creature)
267             && !it->canEquip()
268             && getCollective()->getMinionEquipment().needsItem(creature, it, true); }, &scrollPos);
269     if (chosenItem) {
270       CHECK(getCollective()->getMinionEquipment().tryToOwn(creature, chosenItem));
271     } else
272       break;
273   }
274 }
275 
addEquipment(WCreature creature,EquipmentSlot slot)276 void PlayerControl::addEquipment(WCreature creature, EquipmentSlot slot) {
277   vector<WItem> currentItems = creature->getEquipment().getSlotItems(slot);
278   WItem chosenItem = chooseEquipmentItem(creature, currentItems, [&](WConstItem it) {
279       return !getCollective()->getMinionEquipment().isOwner(it, creature)
280       && creature->canEquipIfEmptySlot(it, nullptr) && it->getEquipmentSlot() == slot; });
281   if (chosenItem) {
282     if (auto creatureId = getCollective()->getMinionEquipment().getOwner(chosenItem))
283       if (WCreature c = getCreature(*creatureId))
284         c->removeEffect(LastingEffect::SLEEP);
285     CHECK(getCollective()->getMinionEquipment().tryToOwn(creature, chosenItem));
286   }
287 }
288 
minionEquipmentAction(const EquipmentActionInfo & action)289 void PlayerControl::minionEquipmentAction(const EquipmentActionInfo& action) {
290   WCreature creature = getCreature(action.creature);
291   switch (action.action) {
292     case ItemAction::DROP:
293       for (auto id : action.ids)
294         getCollective()->getMinionEquipment().discard(id);
295       break;
296     case ItemAction::REPLACE:
297       if (action.slot)
298         addEquipment(creature, *action.slot);
299       else
300         addConsumableItem(creature);
301       break;
302     case ItemAction::LOCK:
303       for (auto id : action.ids)
304         getCollective()->getMinionEquipment().setLocked(creature, id, true);
305       break;
306     case ItemAction::UNLOCK:
307       for (auto id : action.ids)
308         getCollective()->getMinionEquipment().setLocked(creature, id, false);
309       break;
310     default:
311       break;
312   }
313 }
314 
minionTaskAction(const TaskActionInfo & action)315 void PlayerControl::minionTaskAction(const TaskActionInfo& action) {
316   if (auto c = getCreature(action.creature)) {
317     if (action.switchTo)
318       getCollective()->setMinionTask(c, *action.switchTo);
319     for (MinionTask task : action.lock)
320       c->getAttributes().getMinionTasks().toggleLock(task);
321   }
322 }
323 
getItemInfo(const vector<WItem> & stack,bool equiped,bool pending,bool locked,optional<ItemInfo::Type> type=none)324 static ItemInfo getItemInfo(const vector<WItem>& stack, bool equiped, bool pending, bool locked,
325     optional<ItemInfo::Type> type = none) {
326   return CONSTRUCT(ItemInfo,
327     c.name = stack[0]->getShortName();
328     c.fullName = stack[0]->getNameAndModifiers(false);
329     c.description = stack[0]->getDescription();
330     c.number = stack.size();
331     if (stack[0]->canEquip())
332       c.slot = stack[0]->getEquipmentSlot();
333     c.viewId = stack[0]->getViewObject().id();
334     for (auto it : stack)
335       c.ids.insert(it->getUniqueId());
336     c.actions = {ItemAction::DROP};
337     c.equiped = equiped;
338     c.locked = locked;
339     if (type)
340       c.type = *type;
341     c.pending = pending;);
342 }
343 
getSlotViewId(EquipmentSlot slot)344 static ViewId getSlotViewId(EquipmentSlot slot) {
345   switch (slot) {
346     case EquipmentSlot::BOOTS: return ViewId::LEATHER_BOOTS;
347     case EquipmentSlot::WEAPON: return ViewId::SWORD;
348     case EquipmentSlot::RINGS: return ViewId::FIRE_RESIST_RING;
349     case EquipmentSlot::HELMET: return ViewId::LEATHER_HELM;
350     case EquipmentSlot::RANGED_WEAPON: return ViewId::BOW;
351     case EquipmentSlot::GLOVES: return ViewId::LEATHER_GLOVES;
352     case EquipmentSlot::BODY_ARMOR: return ViewId::LEATHER_ARMOR;
353     case EquipmentSlot::AMULET: return ViewId::AMULET1;
354   }
355 }
356 
getEmptySlotItem(EquipmentSlot slot)357 static ItemInfo getEmptySlotItem(EquipmentSlot slot) {
358   return CONSTRUCT(ItemInfo,
359     c.name = "";
360     c.fullName = "";
361     c.description = "";
362     c.slot = slot;
363     c.number = 1;
364     c.viewId = getSlotViewId(slot);
365     c.actions = {ItemAction::REPLACE};
366     c.equiped = false;
367     c.pending = false;);
368 }
369 
getTradeItemInfo(const vector<WItem> & stack,int budget)370 static ItemInfo getTradeItemInfo(const vector<WItem>& stack, int budget) {
371   return CONSTRUCT(ItemInfo,
372     c.name = stack[0]->getShortName(nullptr, true);
373     c.price = make_pair(ViewId::GOLD, stack[0]->getPrice());
374     c.fullName = stack[0]->getNameAndModifiers(false);
375     c.description = stack[0]->getDescription();
376     c.number = stack.size();
377     c.viewId = stack[0]->getViewObject().id();
378     for (auto it : stack)
379       c.ids.insert(it->getUniqueId());
380     c.unavailable = c.price->second > budget;);
381 }
382 
fillEquipment(WCreature creature,PlayerInfo & info) const383 void PlayerControl::fillEquipment(WCreature creature, PlayerInfo& info) const {
384   if (!creature->getBody().isHumanoid())
385     return;
386   int index = 0;
387   double scrollPos = 0;
388   vector<EquipmentSlot> slots;
389   for (auto slot : Equipment::slotTitles)
390     slots.push_back(slot.first);
391   vector<WItem> ownedItems = getCollective()->getMinionEquipment().getItemsOwnedBy(creature);
392   vector<WItem> slotItems;
393   vector<EquipmentSlot> slotIndex;
394   for (auto slot : slots) {
395     vector<WItem> items;
396     for (WItem it : ownedItems)
397       if (it->canEquip() && it->getEquipmentSlot() == slot)
398         items.push_back(it);
399     for (int i = creature->getEquipment().getMaxItems(slot); i < items.size(); ++i)
400       // a rare occurence that minion owns too many items of the same slot,
401       //should happen only when an item leaves the fortress and then is braught back
402       if (!getCollective()->getMinionEquipment().isLocked(creature, items[i]->getUniqueId()))
403         getCollective()->getMinionEquipment().discard(items[i]);
404     append(slotItems, items);
405     append(slotIndex, vector<EquipmentSlot>(items.size(), slot));
406     for (WItem item : items) {
407       ownedItems.removeElement(item);
408       bool equiped = creature->getEquipment().isEquipped(item);
409       bool locked = getCollective()->getMinionEquipment().isLocked(creature, item->getUniqueId());
410       info.inventory.push_back(getItemInfo({item}, equiped, !equiped, locked, ItemInfo::EQUIPMENT));
411       info.inventory.back().actions.push_back(locked ? ItemAction::UNLOCK : ItemAction::LOCK);
412     }
413     if (creature->getEquipment().getMaxItems(slot) > items.size()) {
414       info.inventory.push_back(getEmptySlotItem(slot));
415       slotIndex.push_back(slot);
416       slotItems.push_back(nullptr);
417     }
418     if (slot == EquipmentSlot::WEAPON && tutorial &&
419         tutorial->getHighlights(getGame()).contains(TutorialHighlight::EQUIPMENT_SLOT_WEAPON))
420       info.inventory.back().tutorialHighlight = true;
421   }
422   vector<vector<WItem>> consumables = Item::stackItems(ownedItems,
423       [&](WConstItem it) { if (!creature->getEquipment().hasItem(it)) return " (pending)"; else return ""; } );
424   for (auto& stack : consumables)
425     info.inventory.push_back(getItemInfo(stack, false,
426           !creature->getEquipment().hasItem(stack[0]), false, ItemInfo::CONSUMABLE));
427   for (WItem item : creature->getEquipment().getItems())
428     if (!getCollective()->getMinionEquipment().isItemUseful(item))
429       info.inventory.push_back(getItemInfo({item}, false, false, false, ItemInfo::OTHER));
430 }
431 
chooseEquipmentItem(WCreature creature,vector<WItem> currentItems,ItemPredicate predicate,ScrollPosition * scrollPos)432 WItem PlayerControl::chooseEquipmentItem(WCreature creature, vector<WItem> currentItems, ItemPredicate predicate,
433     ScrollPosition* scrollPos) {
434   vector<WItem> availableItems;
435   vector<WItem> usedItems;
436   vector<WItem> allItems = getCollective()->getAllItems(predicate);
437   getCollective()->getMinionEquipment().sortByEquipmentValue(creature, allItems);
438   for (WItem item : allItems)
439     if (!currentItems.contains(item)) {
440       auto owner = getCollective()->getMinionEquipment().getOwner(item);
441       if (owner && getCreature(*owner))
442         usedItems.push_back(item);
443       else
444         availableItems.push_back(item);
445     }
446   if (currentItems.empty() && availableItems.empty() && usedItems.empty())
447     return nullptr;
448   vector<vector<WItem>> usedStacks = Item::stackItems(usedItems,
449       [&](WConstItem it) {
450         WConstCreature c = getCreature(*getCollective()->getMinionEquipment().getOwner(it));
451         return c->getName().bare() + toString<int>(c->getBestAttack().value);});
452   vector<WItem> allStacked;
453   vector<ItemInfo> options;
454   for (WItem it : currentItems)
455     options.push_back(getItemInfo({it}, true, false, false));
456   for (auto& stack : concat(Item::stackItems(availableItems), usedStacks)) {
457     options.emplace_back(getItemInfo(stack, false, false, false));
458     if (auto creatureId = getCollective()->getMinionEquipment().getOwner(stack[0]))
459       if (WConstCreature c = getCreature(*creatureId))
460         options.back().owner = CreatureInfo(c);
461     allStacked.push_back(stack.front());
462   }
463   auto index = getView()->chooseItem(options, scrollPos);
464   if (!index)
465     return nullptr;
466   return concat(currentItems, allStacked)[*index];
467 }
468 
getNumMinions() const469 int PlayerControl::getNumMinions() const {
470   return (int) getCollective()->getCreatures(MinionTrait::FIGHTER).size();
471 }
472 
getMinLibrarySize() const473 int PlayerControl::getMinLibrarySize() const {
474   return (int) getCollective()->getTechnologies().size();
475 }
476 
477 typedef CollectiveInfo::Button Button;
478 
getCostObjWithZero(CostInfo cost)479 static optional<pair<ViewId, int>> getCostObjWithZero(CostInfo cost) {
480   auto& resourceInfo = CollectiveConfig::getResourceInfo(cost.id);
481   if (!resourceInfo.dontDisplay)
482     return make_pair(resourceInfo.viewId, cost.value);
483   else
484     return none;
485 }
486 
getCostObj(CostInfo cost)487 static optional<pair<ViewId, int>> getCostObj(CostInfo cost) {
488   auto& resourceInfo = CollectiveConfig::getResourceInfo(cost.id);
489   if (cost.value > 0 && !resourceInfo.dontDisplay)
490     return make_pair(resourceInfo.viewId, cost.value);
491   else
492     return none;
493 }
494 
getCostObj(const optional<CostInfo> & cost)495 static optional<pair<ViewId, int>> getCostObj(const optional<CostInfo>& cost) {
496   if (cost)
497     return getCostObj(*cost);
498   else
499     return none;
500 }
501 
getMinionName(CreatureId id) const502 string PlayerControl::getMinionName(CreatureId id) const {
503   static map<CreatureId, string> names;
504   if (!names.count(id))
505     names[id] = CreatureFactory::fromId(id, TribeId::getMonster())->getName().bare();
506   return names.at(id);
507 }
508 
getFurnitureViewId(FurnitureType type)509 static ViewId getFurnitureViewId(FurnitureType type) {
510   static EnumMap<FurnitureType, optional<ViewId>> ids;
511   if (!ids[type])
512     ids[type] = FurnitureFactory::get(type, TribeId::getMonster())->getViewObject()->id();
513   return *ids[type];
514 }
515 
fillButtons(const vector<BuildInfo> & buildInfo) const516 vector<Button> PlayerControl::fillButtons(const vector<BuildInfo>& buildInfo) const {
517   vector<Button> buttons;
518   EnumMap<ResourceId, int> numResource([this](ResourceId id) { return getCollective()->numResource(id);});
519   for (auto& button : buildInfo) {
520     switch (button.buildType) {
521       case BuildInfo::FURNITURE: {
522            auto& elem = button.furnitureInfo;
523            ViewId viewId = getFurnitureViewId(elem.types[0]);
524            string description;
525            if (elem.cost.value > 0) {
526              int num = 0;
527              for (auto type : elem.types)
528                num += getCollective()->getConstructions().getBuiltCount(type);
529              if (num > 0)
530                description = "[" + toString(num) + "]";
531            }
532            int availableNow = !elem.cost.value ? 1 : numResource[elem.cost.id] / elem.cost.value;
533            if (CollectiveConfig::getResourceInfo(elem.cost.id).dontDisplay && availableNow)
534              description += " (" + toString(availableNow) + " available)";
535            buttons.push_back({viewId, button.name,
536                getCostObj(elem.cost),
537                description,
538                (elem.noCredit && !availableNow) ?
539                   CollectiveInfo::Button::GRAY_CLICKABLE : CollectiveInfo::Button::ACTIVE });
540            }
541            break;
542       case BuildInfo::DIG:
543            buttons.push_back({ViewId::DIG_ICON, button.name, none, "", CollectiveInfo::Button::ACTIVE});
544            break;
545       case BuildInfo::ZONE:
546            buttons.push_back({button.viewId, button.name, none, "", CollectiveInfo::Button::ACTIVE});
547            break;
548       case BuildInfo::CLAIM_TILE:
549            buttons.push_back({ViewId::KEEPER_FLOOR, button.name, none, "", CollectiveInfo::Button::ACTIVE});
550            break;
551       case BuildInfo::DISPATCH:
552            buttons.push_back({ViewId::IMP, button.name, none, "", CollectiveInfo::Button::ACTIVE});
553            break;
554       case BuildInfo::TRAP: {
555              auto& elem = button.trapInfo;
556              buttons.push_back({elem.viewId, button.name, none});
557            }
558            break;
559       case BuildInfo::DESTROY:
560            buttons.push_back({ViewId::DESTROY_BUTTON, button.name, none, "",
561                    CollectiveInfo::Button::ACTIVE});
562            break;
563       case BuildInfo::FORBID_ZONE:
564            buttons.push_back({ViewId::FORBID_ZONE, button.name, none, "", CollectiveInfo::Button::ACTIVE});
565            break;
566     }
567     vector<string> unmetReqText;
568     for (auto& req : button.requirements)
569       if (!BuildInfo::meetsRequirement(getCollective(), req)) {
570         unmetReqText.push_back("Requires " + BuildInfo::getRequirementText(req) + ".");
571         buttons.back().state = CollectiveInfo::Button::INACTIVE;
572       }
573     if (unmetReqText.empty())
574       buttons.back().help = button.help;
575     else
576       buttons.back().help = combineSentences(concat({button.help}, unmetReqText));
577     buttons.back().hotkey = button.hotkey;
578     buttons.back().groupName = button.groupName;
579     buttons.back().hotkeyOpensGroup = button.hotkeyOpensGroup;
580     buttons.back().tutorialHighlight = button.tutorialHighlight;
581   }
582   return buttons;
583 }
584 
getTechInfo() const585 vector<PlayerControl::TechInfo> PlayerControl::getTechInfo() const {
586   vector<TechInfo> ret;
587   ret.push_back({{ViewId::BOOKCASE_GOLD, "Library", 'l'},
588       [](PlayerControl* c, View* view) { c->setChosenLibrary(!c->chosenLibrary); }});
589   ret.push_back({{ViewId::BOOK, "Keeperopedia"},
590       [](PlayerControl* c, View* view) { Encyclopedia().present(view); }});
591   return ret;
592 }
593 
getTriggerLabel(const AttackTrigger & trigger)594 static string getTriggerLabel(const AttackTrigger& trigger) {
595   switch (trigger.getId()) {
596     case AttackTriggerId::SELF_VICTIMS: return "Killed tribe members";
597     case AttackTriggerId::GOLD: return "Your gold";
598     case AttackTriggerId::STOLEN_ITEMS: return "Item theft";
599     case AttackTriggerId::ROOM_BUILT:
600       switch (trigger.get<FurnitureType>()) {
601         case FurnitureType::THRONE: return "Your throne";
602         case FurnitureType::DEMON_SHRINE: return "Your lack of demon shrines";
603         case FurnitureType::IMPALED_HEAD: return "Impaled heads";
604         default: FATAL << "Unsupported ROOM_BUILT type"; return "";
605       }
606     case AttackTriggerId::POWER: return "Your power";
607     case AttackTriggerId::FINISH_OFF: return "Finishing you off";
608     case AttackTriggerId::ENEMY_POPULATION: return "Dungeon population";
609     case AttackTriggerId::TIMER: return "Your evilness";
610     case AttackTriggerId::NUM_CONQUERED: return "Your aggression";
611     case AttackTriggerId::ENTRY: return "Entry";
612     case AttackTriggerId::PROXIMITY: return "Proximity";
613   }
614 }
615 
getVillageInfo(WConstCollective col) const616 VillageInfo::Village PlayerControl::getVillageInfo(WConstCollective col) const {
617   VillageInfo::Village info;
618   info.name = col->getName()->shortened;
619   info.id = col->getUniqueId();
620   info.tribeName = col->getName()->race;
621   info.triggers.clear();
622   if (col->getModel() == getModel()) {
623     if (!getCollective()->isKnownVillainLocation(col))
624       info.access = VillageInfo::Village::NO_LOCATION;
625     else {
626       info.access = VillageInfo::Village::LOCATION;
627       for (auto& trigger : col->getTriggers(getCollective()))
628         info.triggers.push_back({getTriggerLabel(trigger.trigger), trigger.value});
629     }
630   } else if (!getGame()->isVillainActive(col))
631     info.access = VillageInfo::Village::INACTIVE;
632   else {
633     info.access = VillageInfo::Village::ACTIVE;
634     for (auto& trigger : col->getTriggers(getCollective()))
635       info.triggers.push_back({getTriggerLabel(trigger.trigger), trigger.value});
636   }
637   bool hostile = col->getTribe()->isEnemy(getCollective()->getTribe());
638   if (col->isConquered()) {
639     info.state = info.CONQUERED;
640     info.triggers.clear();
641     if (col->canPillage())
642       info.actions.push_back({VillageAction::PILLAGE, none});
643   } else if (hostile)
644     info.state = info.HOSTILE;
645   else {
646     info.state = info.FRIENDLY;
647     if (getCollective()->isKnownVillainLocation(col)) {
648       if (col->hasTradeItems())
649         info.actions.push_back({VillageAction::TRADE, none});
650     } else if (getGame()->isVillainActive(col)) {
651       if (col->hasTradeItems())
652         info.actions.push_back({VillageAction::TRADE, string("You must discover the location of the ally first.")});
653     }
654   }
655   return info;
656 }
657 
handleTrading(WCollective ally)658 void PlayerControl::handleTrading(WCollective ally) {
659   ScrollPosition scrollPos;
660   const set<Position>& storage = getCollective()->getZones().getPositions(ZoneId::STORAGE_EQUIPMENT);
661   if (storage.empty()) {
662     getView()->presentText("Information", "You need a storage room for equipment in order to trade.");
663     return;
664   }
665   while (1) {
666     vector<WItem> available = ally->getTradeItems();
667     vector<vector<WItem>> items = Item::stackItems(available);
668     if (items.empty())
669       break;
670     int budget = getCollective()->numResource(ResourceId::GOLD);
671     vector<ItemInfo> itemInfo = items.transform(
672         [budget] (const vector<WItem>& it) { return getTradeItemInfo(it, budget); });
673     auto index = getView()->chooseTradeItem("Trade with " + ally->getName()->shortened,
674         {ViewId::GOLD, getCollective()->numResource(ResourceId::GOLD)}, itemInfo, &scrollPos);
675     if (!index)
676       break;
677     for (WItem it : available)
678       if (it->getUniqueId() == *index && it->getPrice() <= budget) {
679         getCollective()->takeResource({ResourceId::GOLD, it->getPrice()});
680         Random.choose(storage).dropItem(ally->buyItem(it));
681       }
682     getView()->updateView(this, true);
683   }
684 }
685 
getPillageItemInfo(const vector<WItem> & stack,bool noStorage)686 static ItemInfo getPillageItemInfo(const vector<WItem>& stack, bool noStorage) {
687   return CONSTRUCT(ItemInfo,
688     c.name = stack[0]->getShortName(nullptr, true);
689     c.fullName = stack[0]->getNameAndModifiers(false);
690     c.description = stack[0]->getDescription();
691     c.number = stack.size();
692     c.viewId = stack[0]->getViewObject().id();
693     for (auto it : stack)
694       c.ids.insert(it->getUniqueId());
695     c.unavailable = noStorage;
696     c.unavailableReason = noStorage ? "No storage is available for this item." : "";
697   );
698 }
699 
retrieveItems(WCollective col,vector<WItem> items)700 static vector<PItem> retrieveItems(WCollective col, vector<WItem> items) {
701   vector<PItem> ret;
702   EntitySet<Item> index(items);
703   for (auto pos : col->getTerritory().getAll()) {
704     for (auto item : copyOf(pos.getInventory().getItems()))
705       if (index.contains(item))
706         ret.push_back(pos.modInventory().removeItem(item));
707   }
708   return ret;
709 }
710 
handlePillage(WCollective col)711 void PlayerControl::handlePillage(WCollective col) {
712   ScrollPosition scrollPos;
713   while (1) {
714     struct PillageOption {
715       vector<WItem> items;
716       set<Position> storage;
717     };
718     vector<PillageOption> options;
719     for (auto& elem : Item::stackItems(col->getAllItems(false)))
720       if (auto storage = getCollective()->getStorageFor(elem.front()))
721         options.push_back({elem, *storage});
722       else
723         options.push_back({elem, getCollective()->getZones().getPositions(ZoneId::STORAGE_EQUIPMENT)});
724     if (options.empty())
725       return;
726     vector<ItemInfo> itemInfo = options.transform([] (const PillageOption& it) {
727             return getPillageItemInfo(it.items, it.storage.empty());});
728     auto index = getView()->choosePillageItem("Pillage " + col->getName()->shortened, itemInfo, &scrollPos);
729     if (!index)
730       break;
731     CHECK(!options[*index].storage.empty());
732     Random.choose(options[*index].storage).dropItems(retrieveItems(col, options[*index].items));
733     getView()->updateView(this, true);
734   }
735 }
736 
handleRansom(bool pay)737 void PlayerControl::handleRansom(bool pay) {
738   if (ransomAttacks.empty())
739     return;
740   auto& ransom = ransomAttacks.front();
741   int amount = *ransom.getRansom();
742   if (pay && getCollective()->hasResource({ResourceId::GOLD, amount})) {
743     getCollective()->takeResource({ResourceId::GOLD, amount});
744     ransom.getAttacker()->onRansomPaid();
745   }
746   ransomAttacks.removeIndex(0);
747 }
748 
getKnownVillains() const749 vector<WCollective> PlayerControl::getKnownVillains() const {
750   auto showAll = getGame()->getOptions()->getBoolValue(OptionId::SHOW_MAP);
751   return getGame()->getCollectives().filter([&](WCollective c) {
752       return showAll || getCollective()->isKnownVillain(c);});
753 }
754 
getMinionsLike(WCreature like) const755 vector<WCreature> PlayerControl::getMinionsLike(WCreature like) const {
756   vector<WCreature> minions;
757   for (WCreature c : getCreatures())
758     if (c->getName().stack() == like->getName().stack())
759       minions.push_back(c);
760   return minions;
761 }
762 
sortMinionsForUI(vector<WCreature> & minions) const763 void PlayerControl::sortMinionsForUI(vector<WCreature>& minions) const {
764   std::sort(minions.begin(), minions.end(), [] (WConstCreature c1, WConstCreature c2) {
765       auto l1 = (int) max(c1->getAttr(AttrType::DAMAGE), c1->getAttr(AttrType::SPELL_DAMAGE));
766       auto l2 = (int) max(c2->getAttr(AttrType::DAMAGE), c2->getAttr(AttrType::SPELL_DAMAGE));
767       return l1 > l2 || (l1 == l2 && c1->getUniqueId() > c2->getUniqueId());
768       });
769 }
770 
getPlayerInfos(vector<WCreature> creatures,UniqueEntity<Creature>::Id chosenId) const771 vector<PlayerInfo> PlayerControl::getPlayerInfos(vector<WCreature> creatures, UniqueEntity<Creature>::Id chosenId) const {
772   sortMinionsForUI(creatures);
773   vector<PlayerInfo> minions;
774   for (WCreature c : creatures) {
775     minions.emplace_back(c);
776     // only fill equipment for the chosen minion to avoid lag
777     if (c->getUniqueId() == chosenId) {
778       for (auto expType : ENUM_ALL(ExperienceType))
779         if (auto requiredDummy = getCollective()->getMissingTrainingFurniture(c, expType))
780           minions.back().levelInfo.warning[expType] =
781               "Requires " + Furniture::getName(*requiredDummy) + " to train further.";
782       for (MinionTask t : ENUM_ALL(MinionTask))
783         if (c->getAttributes().getMinionTasks().isAvailable(getCollective(), c, t, true)) {
784           minions.back().minionTasks.push_back({t,
785               !getCollective()->isMinionTaskPossible(c, t),
786               getCollective()->getMinionTask(c) == t,
787               c->getAttributes().getMinionTasks().isLocked(t)});
788         }
789       if (getCollective()->usesEquipment(c))
790         fillEquipment(c, minions.back());
791       if (!getCollective()->hasTrait(c, MinionTrait::PRISONER)) {
792         minions.back().actions = { PlayerInfo::CONTROL, PlayerInfo::RENAME };
793         if (c != getCollective()->getLeader())
794           minions.back().actions.push_back(PlayerInfo::BANISH);
795       }
796       if (c->getAttributes().getSkills().hasDiscrete(SkillId::CONSUMPTION))
797         minions.back().actions.push_back(PlayerInfo::CONSUME);
798     }
799   }
800   return minions;
801 }
802 
getCreatureGroups(vector<WCreature> v) const803 vector<CollectiveInfo::CreatureGroup> PlayerControl::getCreatureGroups(vector<WCreature> v) const {
804   sortMinionsForUI(v);
805   map<string, CollectiveInfo::CreatureGroup> groups;
806   for (WCreature c : v) {
807     if (!groups.count(c->getName().stack()))
808       groups[c->getName().stack()] = { c->getUniqueId(), c->getName().stack(), c->getViewObject().id(), 0};
809     ++groups[c->getName().stack()].count;
810     if (chosenCreature == c->getUniqueId() && !getChosenTeam())
811       groups[c->getName().stack()].highlight = true;
812   }
813   return getValues(groups);
814 }
815 
getEnemyGroups() const816 vector<CollectiveInfo::CreatureGroup> PlayerControl::getEnemyGroups() const {
817   vector<WCreature> enemies;
818   for (Vec2 v : getVisibleEnemies())
819     if (WCreature c = Position(v, getCollective()->getLevel()).getCreature())
820       enemies.push_back(c);
821   return getCreatureGroups(enemies);
822 }
823 
fillMinions(CollectiveInfo & info) const824 void PlayerControl::fillMinions(CollectiveInfo& info) const {
825   vector<WCreature> minions;
826   for (WCreature c : getCollective()->getCreaturesAnyOf(
827         {MinionTrait::FIGHTER, MinionTrait::PRISONER, MinionTrait::WORKER}))
828     minions.push_back(c);
829   minions.push_back(getCollective()->getLeader());
830   info.minionGroups = getCreatureGroups(minions);
831   info.minions = minions.transform([](WConstCreature c) { return CreatureInfo(c) ;});
832   info.minionCount = getCollective()->getPopulationSize();
833   info.minionLimit = getCollective()->getMaxPopulation();
834 }
835 
getWorkshopItem(const WorkshopItem & option) const836 ItemInfo PlayerControl::getWorkshopItem(const WorkshopItem& option) const {
837   return CONSTRUCT(ItemInfo,
838       c.number = option.number * option.batchSize;
839       c.name = c.number == 1 ? option.name : toString(c.number) + " " + option.pluralName;
840       c.viewId = option.viewId;
841       c.price = getCostObj(option.cost * option.number);
842       if (option.techId && !getCollective()->hasTech(*option.techId)) {
843         c.unavailable = true;
844         c.unavailableReason = "Requires technology: " + Technology::get(*option.techId)->getName();
845       }
846       c.description = option.description;
847       c.productionState = option.state.value_or(0);
848       c.actions = LIST(ItemAction::REMOVE, ItemAction::CHANGE_NUMBER);
849       c.tutorialHighlight = tutorial && option.tutorialHighlight &&
850           tutorial->getHighlights(getGame()).contains(*option.tutorialHighlight);
851     );
852 }
853 
getConstructionObject(FurnitureType type)854 static const ViewObject& getConstructionObject(FurnitureType type) {
855   static EnumMap<FurnitureType, optional<ViewObject>> objects;
856   if (!objects[type]) {
857     objects[type] =  FurnitureFactory::get(type, TribeId::getMonster())->getViewObject();
858     objects[type]->setModifier(ViewObject::Modifier::PLANNED);
859   }
860   return *objects[type];
861 }
862 
acquireTech(int index)863 void PlayerControl::acquireTech(int index) {
864   auto techs = Technology::getNextTechs(getCollective()->getTechnologies()).filter(
865       [](const Technology* tech) { return tech->canResearch(); });
866   if (index < techs.size()) {
867     Technology* tech = techs[index];
868     auto cost = tech->getCost();
869     if (getCollective()->hasResource(cost)) {
870       getCollective()->takeResource(cost);
871       getCollective()->acquireTech(tech);
872     }
873   }
874 }
875 
fillLibraryInfo(CollectiveInfo & collectiveInfo) const876 void PlayerControl::fillLibraryInfo(CollectiveInfo& collectiveInfo) const {
877   if (chosenLibrary) {
878     collectiveInfo.libraryInfo.emplace();
879     auto& info = *collectiveInfo.libraryInfo;
880     int libraryCount = 0;
881     for (auto f : CollectiveConfig::getTrainingFurniture(ExperienceType::SPELL))
882       libraryCount += getCollective()->getConstructions().getBuiltPositions(f).size();
883     if (libraryCount == 0)
884       info.warning = "You need to build a library to start research."_s;
885     else if (libraryCount <= getMinLibrarySize())
886       info.warning = "You need a larger library to continue research."_s;
887     info.resource = *getCostObjWithZero(Technology::getAvailableResource(getCollective()));
888     auto techs = Technology::getNextTechs(getCollective()->getTechnologies()).filter(
889         [](const Technology* tech) { return tech->canResearch(); });
890     for (Technology* tech : techs) {
891       info.available.emplace_back();
892       auto& techInfo = info.available.back();
893       techInfo.name = tech->getName();
894       auto cost = tech->getCost();
895       techInfo.cost = *getCostObj(cost);
896       techInfo.tutorialHighlight = tech->getTutorialHighlight();
897       techInfo.active = !info.warning && getCollective()->hasResource(cost);
898       techInfo.description = tech->getDescription();
899     }
900     for (Technology* tech : getCollective()->getTechnologies()) {
901       info.researched.emplace_back();
902       auto& techInfo = info.researched.back();
903       techInfo.name = tech->getName();
904       techInfo.cost = *getCostObj(tech->getCost());
905       techInfo.description = tech->getDescription();
906     }
907   }
908 }
909 
fillWorkshopInfo(CollectiveInfo & info) const910 void PlayerControl::fillWorkshopInfo(CollectiveInfo& info) const {
911   info.workshopButtons.clear();
912   int index = 0;
913   int i = 0;
914   for (auto workshopType : ENUM_ALL(WorkshopType)) {
915     auto& workshopInfo = CollectiveConfig::getWorkshopInfo(workshopType);
916     bool unavailable = getCollective()->getConstructions().getBuiltPositions(workshopInfo.furniture).empty();
917     info.workshopButtons.push_back({capitalFirst(workshopInfo.taskName),
918         getConstructionObject(workshopInfo.furniture).id(), false, unavailable});
919     if (chosenWorkshop == workshopType) {
920       index = i;
921       info.workshopButtons.back().active = true;
922     }
923     ++i;
924   }
925   if (chosenWorkshop) {
926     auto transFun = [this](const WorkshopItem& item) { return getWorkshopItem(item); };
927     info.chosenWorkshop = CollectiveInfo::ChosenWorkshopInfo {
928         getCollective()->getWorkshops().get(*chosenWorkshop).getOptions().transform(transFun),
929         getCollective()->getWorkshops().get(*chosenWorkshop).getQueued().transform(transFun),
930         index
931     };
932   }
933 }
934 
fillImmigration(CollectiveInfo & info) const935 void PlayerControl::fillImmigration(CollectiveInfo& info) const {
936   info.immigration.clear();
937   auto& immigration = getCollective()->getImmigration();
938   for (auto& elem : immigration.getAvailable()) {
939     const auto& candidate = elem.second.get();
940     const int count = (int) candidate.getCreatures().size();
941     optional<double> timeRemaining;
942     if (auto time = candidate.getEndTime())
943       timeRemaining = *time - getGame()->getGlobalTime();
944     vector<string> infoLines;
945     candidate.getInfo().visitRequirements(makeVisitor(
946         [&](const Pregnancy&) {
947           optional<int> maxT;
948           for (WCreature c : getCollective()->getCreatures())
949             if (c->isAffected(LastingEffect::PREGNANT))
950               if (auto remaining = c->getTimeRemaining(LastingEffect::PREGNANT))
951                 if (!maxT || *remaining > *maxT)
952                   maxT = *remaining;
953           if (maxT && (!timeRemaining || *maxT > *timeRemaining))
954             timeRemaining = *maxT;
955         },
956         [&](const RecruitmentInfo& info) {
957           infoLines.push_back(
958               toString(info.getAvailableRecruits(getGame(), candidate.getInfo().getId(0)).size()) +
959               " recruits available");
960         },
961         [&](const auto&) {}
962     ));
963     WCreature c = candidate.getCreatures()[0];
964     string name = c->getName().multiple(count);
965     if (auto& s = c->getName().stackOnly())
966       name += " (" + *s + ")";
967     info.immigration.push_back(ImmigrantDataInfo {
968         immigration.getMissingRequirements(candidate),
969         infoLines,
970         getCostObj(candidate.getCost()),
971         name,
972         c->getViewObject().id(),
973         AttributeInfo::fromCreature(c),
974         count,
975         timeRemaining,
976         elem.first,
977         none,
978         candidate.getCreatedTime(),
979         candidate.getInfo().getKeybinding(),
980         candidate.getInfo().getTutorialHighlight()
981     });
982   }
983   sort(info.immigration.begin(), info.immigration.end(),
984       [](const ImmigrantDataInfo& i1, const ImmigrantDataInfo& i2) {
985         return (i1.timeLeft && (!i2.timeLeft || *i1.timeLeft > *i2.timeLeft)) ||
986             (!i1.timeLeft && !i2.timeLeft && i1.id > i2.id);
987       });
988 }
989 
fillImmigrationHelp(CollectiveInfo & info) const990 void PlayerControl::fillImmigrationHelp(CollectiveInfo& info) const {
991   info.allImmigration.clear();
992   struct CreatureStats {
993     PCreature creature;
994   };
995   static EnumMap<CreatureId, optional<CreatureStats>> creatureStats;
996   auto getStats = [&](CreatureId id) -> CreatureStats& {
997     if (!creatureStats[id]) {
998       creatureStats[id] = CreatureStats{CreatureFactory::fromId(id, TribeId::getKeeper())};
999     }
1000     return *creatureStats[id];
1001   };
1002   for (auto elem : Iter(getCollective()->getConfig().getImmigrantInfo())) {
1003     if (elem->isHiddenInHelp())
1004       continue;
1005     auto creatureId = elem->getId(0);
1006     WCreature c = getStats(creatureId).creature.get();
1007     optional<pair<ViewId, int>> costObj;
1008     vector<string> requirements;
1009     vector<string> infoLines;
1010     elem->visitRequirements(makeVisitor(
1011         [&](const AttractionInfo& attraction) {
1012           int required = attraction.amountClaimed;
1013           requirements.push_back("Requires " + toString(required) + " " +
1014               combineWithOr(attraction.types.transform(
1015                   [&](const AttractionType& type) { return AttractionInfo::getAttractionName(type, required); })));
1016         },
1017         [&](const TechId& techId) {
1018           requirements.push_back("Requires technology: " + Technology::get(techId)->getName());
1019         },
1020         [&](const SunlightState& state) {
1021           requirements.push_back("Will only join during the "_s + SunlightInfo::getText(state));
1022         },
1023         [&](const FurnitureType& type) {
1024           requirements.push_back("Requires at least one " + Furniture::getName(type));
1025         },
1026         [&](const CostInfo& cost) {
1027           costObj = getCostObj(cost);
1028         },
1029         [&](const ExponentialCost& cost) {
1030           auto& resourceInfo = CollectiveConfig::getResourceInfo(cost.base.id);
1031           costObj = make_pair(resourceInfo.viewId, cost.base.value);
1032           infoLines.push_back("Cost doubles for every " + toString(cost.numToDoubleCost) + " "
1033               + c->getName().plural());
1034           if (cost.numFree > 0)
1035             infoLines.push_back("First " + toString(cost.numFree) + " " + c->getName().plural() + " are free");
1036         },
1037         [&](const Pregnancy&) {
1038           requirements.push_back("Requires a pregnant succubus");
1039         },
1040         [&](const RecruitmentInfo& info) {
1041           if (info.findEnemy(getGame()))
1042             requirements.push_back("Ally must be discovered and have recruits available");
1043           else
1044             requirements.push_back("Recruit is not available in this game");
1045         },
1046         [&](const TutorialRequirement&) {
1047         }
1048     ));
1049     if (auto limit = elem->getLimit())
1050       infoLines.push_back("Limited to " + toString(*limit) + " creatures");
1051     info.allImmigration.push_back(ImmigrantDataInfo {
1052         requirements,
1053         infoLines,
1054         costObj,
1055         c->getName().stack(),
1056         c->getViewObject().id(),
1057         AttributeInfo::fromCreature(c),
1058         0,
1059         none,
1060         elem.index(),
1061         getCollective()->getImmigration().getAutoState(elem.index())
1062     });
1063   }
1064 }
1065 
refreshGameInfo(GameInfo & gameInfo) const1066 void PlayerControl::refreshGameInfo(GameInfo& gameInfo) const {
1067   if (tutorial)
1068     tutorial->refreshInfo(getGame(), gameInfo.tutorial);
1069   gameInfo.singleModel = getGame()->isSingleModel();
1070   gameInfo.villageInfo.villages.clear();
1071   for (WConstCollective col : getKnownVillains())
1072     if (col->getName() && col->isDiscoverable())
1073       gameInfo.villageInfo.villages[col->getVillainType()].push_back(getVillageInfo(col));
1074   SunlightInfo sunlightInfo = getGame()->getSunlightInfo();
1075   gameInfo.sunlightInfo = { sunlightInfo.getText(), (int)sunlightInfo.getTimeRemaining() };
1076   gameInfo.infoType = GameInfo::InfoType::BAND;
1077   gameInfo.playerInfo = CollectiveInfo();
1078   auto& info = *gameInfo.playerInfo.getReferenceMaybe<CollectiveInfo>();
1079   info.buildings = fillButtons(BuildInfo::get());
1080   fillMinions(info);
1081   fillImmigration(info);
1082   fillImmigrationHelp(info);
1083   info.chosenCreature.reset();
1084   if (chosenCreature)
1085     if (WCreature c = getCreature(*chosenCreature)) {
1086       if (!getChosenTeam())
1087         info.chosenCreature = CollectiveInfo::ChosenCreatureInfo {
1088             *chosenCreature, getPlayerInfos(getMinionsLike(c), *chosenCreature)};
1089       else
1090         info.chosenCreature = CollectiveInfo::ChosenCreatureInfo {
1091             *chosenCreature, getPlayerInfos(getTeams().getMembers(*getChosenTeam()), *chosenCreature), *getChosenTeam()};
1092     }
1093   fillWorkshopInfo(info);
1094   fillLibraryInfo(info);
1095   info.monsterHeader = "Minions: " + toString(info.minionCount) + " / " + toString(info.minionLimit);
1096   info.enemyGroups = getEnemyGroups();
1097   info.numResource.clear();
1098   for (auto resourceId : ENUM_ALL(CollectiveResourceId)) {
1099     auto& elem = CollectiveConfig::getResourceInfo(resourceId);
1100     if (!elem.dontDisplay)
1101       info.numResource.push_back(
1102           {elem.viewId, getCollective()->numResourcePlusDebt(resourceId), elem.name, elem.tutorialHighlight});
1103   }
1104   info.warning = "";
1105   gameInfo.time = getCollective()->getGame()->getGlobalTime();
1106   gameInfo.modifiedSquares = gameInfo.totalSquares = 0;
1107   for (WCollective col : getCollective()->getGame()->getCollectives()) {
1108     gameInfo.modifiedSquares += col->getLevel()->getNumGeneratedSquares();
1109     gameInfo.totalSquares += col->getLevel()->getNumTotalSquares();
1110   }
1111   info.teams.clear();
1112   for (int i : All(getTeams().getAll())) {
1113     TeamId team = getTeams().getAll()[i];
1114     info.teams.emplace_back();
1115     for (WCreature c : getTeams().getMembers(team))
1116       info.teams.back().members.push_back(c->getUniqueId());
1117     info.teams.back().active = getTeams().isActive(team);
1118     info.teams.back().id = team;
1119     if (getChosenTeam() == team)
1120       info.teams.back().highlight = true;
1121   }
1122   info.techButtons.clear();
1123   for (TechInfo tech : getTechInfo())
1124     info.techButtons.push_back(tech.button);
1125   gameInfo.messageBuffer = messages;
1126   info.taskMap.clear();
1127   for (WConstTask task : getCollective()->getTaskMap().getAllTasks()) {
1128     optional<UniqueEntity<Creature>::Id> creature;
1129     if (auto c = getCollective()->getTaskMap().getOwner(task))
1130       creature = c->getUniqueId();
1131     info.taskMap.push_back({task->getDescription(), creature, getCollective()->getTaskMap().isPriorityTask(task)});
1132   }
1133   for (auto& elem : ransomAttacks) {
1134     info.ransom = CollectiveInfo::Ransom {make_pair(ViewId::GOLD, *elem.getRansom()), elem.getAttackerName(),
1135         getCollective()->hasResource({ResourceId::GOLD, *elem.getRansom()})};
1136     break;
1137   }
1138   constexpr int maxEnemyCountdown = 500;
1139   if (auto& enemies = getModel()->getExternalEnemies())
1140     if (auto nextWave = enemies->getNextWave()) {
1141       int countDown = (int) (nextWave->attackTime - getLocalTime());
1142       auto index = enemies->getNextWaveIndex();
1143       auto name = nextWave->enemy.name;
1144       auto viewId = nextWave->viewId;
1145       if (index % 6 == 5) {
1146         name = "Unknown";
1147         viewId = ViewId::UNKNOWN_MONSTER;
1148       }
1149       if (!dismissedNextWaves.count(index) && countDown <= maxEnemyCountdown)
1150         info.nextWave = CollectiveInfo::NextWave {
1151           viewId,
1152           name,
1153           countDown
1154         };
1155     }
1156 }
1157 
addMessage(const PlayerMessage & msg)1158 void PlayerControl::addMessage(const PlayerMessage& msg) {
1159   messages.push_back(msg);
1160   messageHistory.push_back(msg);
1161   if (msg.getPriority() == MessagePriority::CRITICAL) {
1162     getView()->stopClock();
1163     for (auto c : getControlled()) {
1164       c->privateMessage(msg);
1165       break;
1166     }
1167   }
1168 }
1169 
initialize()1170 void PlayerControl::initialize() {
1171   for (WCreature c : getCreatures())
1172     updateMinionVisibility(c);
1173 }
1174 
updateMinionVisibility(WConstCreature c)1175 void PlayerControl::updateMinionVisibility(WConstCreature c) {
1176   auto visibleTiles = c->getVisibleTiles();
1177   visibilityMap->update(c, visibleTiles);
1178   for (Position pos : visibleTiles) {
1179     if (getCollective()->addKnownTile(pos))
1180       updateKnownLocations(pos);
1181     addToMemory(pos);
1182   }
1183 }
1184 
onEvent(const GameEvent & event)1185 void PlayerControl::onEvent(const GameEvent& event) {
1186   using namespace EventInfo;
1187   event.visit(
1188       [&](const Projectile& info) {
1189         if (canSee(info.begin) || canSee(info.end))
1190           getView()->animateObject(info.begin.getCoord(), info.end.getCoord(), info.viewId);
1191       },
1192       [&](const CreatureEvent& info) {
1193         if (getCollective()->getCreatures().contains(info.creature))
1194           addMessage(PlayerMessage(info.message).setCreature(info.creature->getUniqueId()));
1195       },
1196       [&](const VisibilityChanged& info) {
1197         visibilityMap->onVisibilityChanged(info.pos);
1198       },
1199       [&](const CreatureMoved& info) {
1200         if (getCreatures().contains(info.creature))
1201           updateMinionVisibility(info.creature);
1202       },
1203       [&](const ItemsEquipped& info) {
1204         if (info.creature->isPlayer() &&
1205             !getCollective()->getMinionEquipment().tryToOwn(info.creature, info.items.getOnlyElement()))
1206           getView()->presentText("", "Item won't be permanently assigned to creature because the equipment slot is locked.");
1207       },
1208       [&](const WonGame&) {
1209         CHECK(!getKeeper()->isDead());
1210         getGame()->conquered(*getKeeper()->getName().first(), getCollective()->getKills().getSize(),
1211             getCollective()->getDangerLevel() + getCollective()->getPoints());
1212         getView()->presentText("", "When you are ready, retire your dungeon and share it online. "
1213           "Other players will be able to invade it as adventurers. To do this, press Escape and choose \'retire\'.");
1214       },
1215       [&](const TechbookRead& info) {
1216         Technology* tech = info.technology;
1217         vector<Technology*> nextTechs = Technology::getNextTechs(getCollective()->getTechnologies());
1218         if (tech == nullptr) {
1219           if (!nextTechs.empty())
1220             tech = Random.choose(nextTechs);
1221           else
1222             tech = Random.choose(Technology::getAll());
1223         }
1224         if (!getCollective()->getTechnologies().contains(tech)) {
1225           if (!nextTechs.contains(tech))
1226             getView()->presentText("Information", "The tome describes the knowledge of " + tech->getName()
1227                 + ", but you do not comprehend it.");
1228           else {
1229             getView()->presentText("Information", "You have acquired the knowledge of " + tech->getName());
1230             getCollective()->acquireTech(tech);
1231           }
1232         } else {
1233           getView()->presentText("Information", "The tome describes the knowledge of " + tech->getName()
1234               + ", which you already possess.");
1235         }
1236       },
1237       [&](const FurnitureDestroyed& info) {
1238         if (info.type == FurnitureType::EYEBALL)
1239           visibilityMap->removeEyeball(info.position);
1240       },
1241       [&](const auto&) {}
1242   );
1243 }
1244 
updateKnownLocations(const Position & pos)1245 void PlayerControl::updateKnownLocations(const Position& pos) {
1246   /*if (pos.getModel() == getModel())
1247     if (const Location* loc = pos.getLocation())
1248       if (!knownLocations.count(loc)) {
1249         knownLocations.insert(loc);
1250         if (auto name = loc->getName())
1251           addMessage(PlayerMessage("Your minions discover the location of " + *name, MessagePriority::HIGH)
1252               .setLocation(loc));
1253         else if (loc->isMarkedAsSurprise())
1254           addMessage(PlayerMessage("Your minions discover a new location.").setLocation(loc));
1255       }*/
1256   if (getGame()) // check in case this method is called before Game is constructed
1257     for (WConstCollective col : getGame()->getCollectives())
1258       if (col != getCollective() && col->getTerritory().contains(pos)) {
1259         getCollective()->addKnownVillain(col);
1260         if (!getCollective()->isKnownVillainLocation(col)) {
1261           getCollective()->addKnownVillainLocation(col);
1262           if (col->isDiscoverable())
1263             if (auto& name = col->getName())
1264               addMessage(PlayerMessage("Your minions discover the location of " + name->full,
1265                   MessagePriority::HIGH).setPosition(pos));
1266         }
1267       }
1268 }
1269 
1270 
getMemory() const1271 const MapMemory& PlayerControl::getMemory() const {
1272   return *memory;
1273 }
1274 
getTrapObject(TrapType type,bool armed)1275 ViewObject PlayerControl::getTrapObject(TrapType type, bool armed) {
1276   for (auto& info : BuildInfo::get())
1277     if (info.buildType == BuildInfo::TRAP && info.trapInfo.type == type) {
1278       if (!armed)
1279         return ViewObject(info.trapInfo.viewId, ViewLayer::FLOOR, "Unarmed " + getTrapName(type) + " trap")
1280           .setModifier(ViewObject::Modifier::PLANNED);
1281       else
1282         return ViewObject(info.trapInfo.viewId, ViewLayer::FLOOR, getTrapName(type) + " trap");
1283     }
1284   FATAL << "trap not found" << int(type);
1285   return ViewObject(ViewId::EMPTY, ViewLayer::FLOOR);
1286 }
1287 
getSquareViewIndex(Position pos,bool canSee,ViewIndex & index) const1288 void PlayerControl::getSquareViewIndex(Position pos, bool canSee, ViewIndex& index) const {
1289   if (canSee)
1290     pos.getViewIndex(index, getCollective()->getLeader()); // use the leader as a generic viewer
1291   else
1292     index.setHiddenId(pos.getViewObject().id());
1293   if (WConstCreature c = pos.getCreature())
1294     if (canSee) {
1295       index.insert(c->getViewObject());
1296       if (isEnemy(c))
1297         index.getObject(ViewLayer::CREATURE).setModifier(ViewObject::Modifier::HOSTILE);
1298     }
1299 }
1300 
showEfficiency(FurnitureType type)1301 static bool showEfficiency(FurnitureType type) {
1302   switch (type) {
1303     case FurnitureType::BOOKCASE_WOOD:
1304     case FurnitureType::BOOKCASE_IRON:
1305     case FurnitureType::BOOKCASE_GOLD:
1306     case FurnitureType::DEMON_SHRINE:
1307     case FurnitureType::WORKSHOP:
1308     case FurnitureType::TRAINING_WOOD:
1309     case FurnitureType::TRAINING_IRON:
1310     case FurnitureType::TRAINING_STEEL:
1311     case FurnitureType::LABORATORY:
1312     case FurnitureType::JEWELER:
1313     case FurnitureType::THRONE:
1314     case FurnitureType::FORGE:
1315     case FurnitureType::ARCHERY_RANGE:
1316     case FurnitureType::STEEL_FURNACE:
1317       return true;
1318     default:
1319       return false;
1320   }
1321 }
1322 
getViewIndex(Vec2 pos,ViewIndex & index) const1323 void PlayerControl::getViewIndex(Vec2 pos, ViewIndex& index) const {
1324   auto collective = getCollective();
1325   Position position(pos, collective->getLevel());
1326   bool canSeePos = canSee(position);
1327   getSquareViewIndex(position, canSeePos, index);
1328   if (!canSeePos)
1329     if (auto memIndex = getMemory().getViewIndex(position))
1330       index.mergeFromMemory(*memIndex);
1331   if (collective->getTerritory().contains(position))
1332     if (auto furniture = position.getFurniture(FurnitureLayer::MIDDLE)) {
1333       if (furniture->getUsageType() == FurnitureUsageType::STUDY || CollectiveConfig::getWorkshopType(furniture->getType()))
1334         index.setHighlight(HighlightType::CLICKABLE_FURNITURE);
1335       if ((chosenWorkshop && chosenWorkshop == CollectiveConfig::getWorkshopType(furniture->getType())) ||
1336           (chosenLibrary && furniture->getUsageType() == FurnitureUsageType::STUDY))
1337         index.setHighlight(HighlightType::CLICKED_FURNITURE);
1338       if (draggedCreature)
1339         if (WCreature c = getCreature(*draggedCreature))
1340           if (auto task = MinionTasks::getTaskFor(collective, c, furniture->getType()))
1341             if (c->getAttributes().getMinionTasks().isAvailable(collective, c, *task))
1342               index.setHighlight(HighlightType::CREATURE_DROP);
1343       if (showEfficiency(furniture->getType()) && index.hasObject(ViewLayer::FLOOR))
1344         index.getObject(ViewLayer::FLOOR).setAttribute(ViewObject::Attribute::EFFICIENCY,
1345             collective->getTileEfficiency().getEfficiency(position));
1346     }
1347   if (collective->isMarked(position))
1348     index.setHighlight(collective->getMarkHighlight(position));
1349   if (collective->hasPriorityTasks(position))
1350     index.setHighlight(HighlightType::PRIORITY_TASK);
1351   if (!index.hasObject(ViewLayer::CREATURE))
1352     for (auto task : collective->getTaskMap().getTasks(position))
1353       if (auto viewId = task->getViewId())
1354           index.insert(ViewObject(*viewId, ViewLayer::CREATURE));
1355   if (position.isTribeForbidden(getTribeId()))
1356     index.setHighlight(HighlightType::FORBIDDEN_ZONE);
1357   collective->getZones().setHighlights(position, index);
1358   if (rectSelection
1359       && pos.inRectangle(Rectangle::boundingBox({rectSelection->corner1, rectSelection->corner2})))
1360     index.setHighlight(rectSelection->deselect ? HighlightType::RECT_DESELECTION : HighlightType::RECT_SELECTION);
1361   const ConstructionMap& constructions = collective->getConstructions();
1362   if (auto& trap = constructions.getTrap(position))
1363     index.insert(getTrapObject(trap->getType(), trap->isArmed()));
1364   for (auto layer : ENUM_ALL(FurnitureLayer))
1365     if (auto f = constructions.getFurniture(position, layer))
1366       if (!f->isBuilt())
1367         index.insert(getConstructionObject(f->getFurnitureType()));
1368   /*if (surprises.count(position) && !collective->getKnownTiles().isKnown(position))
1369     index.insert(ViewObject(ViewId::UNKNOWN_MONSTER, ViewLayer::CREATURE, "Surprise"));*/
1370 }
1371 
getPosition() const1372 Vec2 PlayerControl::getPosition() const {
1373   if (WConstCreature keeper = getKeeper())
1374     if (!keeper->isDead() && keeper->getLevel() == getLevel())
1375       return keeper->getPosition().getCoord();
1376   if (!getCollective()->getTerritory().isEmpty())
1377     return getCollective()->getTerritory().getAll().front().getCoord();
1378   return Vec2(0, 0);
1379 }
1380 
1381 static enum Selection { SELECT, DESELECT, NONE } selection = NONE;
1382 
controlSingle(WCreature c)1383 void PlayerControl::controlSingle(WCreature c) {
1384   CHECK(getCreatures().contains(c));
1385   CHECK(!c->isDead());
1386   commandTeam(getTeams().create({c}));
1387 }
1388 
getCreature(UniqueEntity<Creature>::Id id) const1389 WCreature PlayerControl::getCreature(UniqueEntity<Creature>::Id id) const {
1390   for (WCreature c : getCreatures())
1391     if (c->getUniqueId() == id)
1392       return c;
1393   return nullptr;
1394 }
1395 
getTeam(WConstCreature c)1396 vector<WCreature> PlayerControl::getTeam(WConstCreature c) {
1397   vector<WCreature> ret;
1398   for (auto team : getTeams().getActive(c))
1399     append(ret, getTeams().getMembers(team));
1400   return ret;
1401 }
1402 
commandTeam(TeamId team)1403 void PlayerControl::commandTeam(TeamId team) {
1404   if (!getControlled().empty())
1405     leaveControl();
1406   auto c = getTeams().getLeader(team);
1407   c->pushController(createMinionController(c));
1408   getTeams().activate(team);
1409   getCollective()->freeTeamMembers(team);
1410   getView()->resetCenter();
1411 }
1412 
toggleControlAllTeamMembers()1413 void PlayerControl::toggleControlAllTeamMembers() {
1414   if (auto teamId = getCurrentTeam()) {
1415     auto members = getTeams().getMembers(*teamId);
1416     if (members.size() > 1) {
1417       if (getControlled().size() == 1) {
1418         for (auto c : members)
1419           if (!c->isPlayer())
1420             c->pushController(createMinionController(c));
1421       } else
1422         for (auto c : members)
1423           if (c->isPlayer() && c != getTeams().getLeader(*teamId))
1424             c->popController();
1425     }
1426   }
1427 }
1428 
findMessage(PlayerMessage::Id id)1429 optional<PlayerMessage> PlayerControl::findMessage(PlayerMessage::Id id){
1430   for (auto& elem : messages)
1431     if (elem.getUniqueId() == id)
1432       return elem;
1433   return none;
1434 }
1435 
getTeams()1436 CollectiveTeams& PlayerControl::getTeams() {
1437   return getCollective()->getTeams();
1438 }
1439 
getTeams() const1440 const CollectiveTeams& PlayerControl::getTeams() const {
1441   return getCollective()->getTeams();
1442 }
1443 
setScrollPos(Position pos)1444 void PlayerControl::setScrollPos(Position pos) {
1445   if (pos.isSameLevel(getLevel()))
1446     getView()->setScrollPos(pos.getCoord());
1447   else if (auto stairs = getLevel()->getStairsTo(pos.getLevel()))
1448     getView()->setScrollPos(stairs->getCoord());
1449 }
1450 
scrollToMiddle(const vector<Position> & pos)1451 void PlayerControl::scrollToMiddle(const vector<Position>& pos) {
1452   vector<Vec2> visible;
1453   for (Position v : pos)
1454     if (getCollective()->getKnownTiles().isKnown(v))
1455       visible.push_back(v.getCoord());
1456   CHECK(!visible.empty());
1457   getView()->setScrollPos(Rectangle::boundingBox(visible).middle());
1458 }
1459 
getVillain(UniqueEntity<Collective>::Id id)1460 WCollective PlayerControl::getVillain(UniqueEntity<Collective>::Id id) {
1461   for (auto col : getGame()->getCollectives())
1462     if (col->getUniqueId() == id)
1463       return col;
1464   return nullptr;
1465 }
1466 
getChosenTeam() const1467 optional<TeamId> PlayerControl::getChosenTeam() const {
1468   if (chosenTeam && getTeams().exists(*chosenTeam))
1469     return chosenTeam;
1470   else
1471     return none;
1472 }
1473 
setChosenCreature(optional<UniqueEntity<Creature>::Id> id)1474 void PlayerControl::setChosenCreature(optional<UniqueEntity<Creature>::Id> id) {
1475   clearChosenInfo();
1476   chosenCreature = id;
1477 }
1478 
setChosenTeam(optional<TeamId> team,optional<UniqueEntity<Creature>::Id> creature)1479 void PlayerControl::setChosenTeam(optional<TeamId> team, optional<UniqueEntity<Creature>::Id> creature) {
1480   clearChosenInfo();
1481   chosenTeam = team;
1482   chosenCreature = creature;
1483 }
1484 
clearChosenInfo()1485 void PlayerControl::clearChosenInfo() {
1486   setChosenWorkshop(none);
1487   setChosenLibrary(false);
1488   chosenCreature = none;
1489   chosenTeam = none;
1490 }
1491 
setChosenLibrary(bool state)1492 void PlayerControl::setChosenLibrary(bool state) {
1493   for (auto f : CollectiveConfig::getTrainingFurniture(ExperienceType::SPELL))
1494     for (auto pos : getCollective()->getConstructions().getBuiltPositions(f))
1495       pos.setNeedsRenderUpdate(true);
1496   if (state)
1497     clearChosenInfo();
1498   chosenLibrary = state;
1499 }
1500 
setChosenWorkshop(optional<WorkshopType> type)1501 void PlayerControl::setChosenWorkshop(optional<WorkshopType> type) {
1502   auto refreshHighlights = [&] {
1503     if (chosenWorkshop)
1504       for (auto pos : getCollective()->getConstructions().getBuiltPositions(
1505              CollectiveConfig::getWorkshopInfo(*chosenWorkshop).furniture))
1506         pos.setNeedsRenderUpdate(true);
1507   };
1508   refreshHighlights();
1509   if (type)
1510     clearChosenInfo();
1511   chosenWorkshop = type;
1512   refreshHighlights();
1513 }
1514 
minionDragAndDrop(const CreatureDropInfo & info)1515 void PlayerControl::minionDragAndDrop(const CreatureDropInfo& info) {
1516   Position pos(info.pos, getLevel());
1517   if (WCreature c = getCreature(info.creatureId)) {
1518     c->removeEffect(LastingEffect::TIED_UP);
1519     c->removeEffect(LastingEffect::SLEEP);
1520     if (auto furniture = getCollective()->getConstructions().getFurniture(pos, FurnitureLayer::MIDDLE))
1521       if (auto task = MinionTasks::getTaskFor(getCollective(), c, furniture->getFurnitureType())) {
1522         if (getCollective()->isMinionTaskPossible(c, *task)) {
1523           getCollective()->setMinionTask(c, *task);
1524           getCollective()->setTask(c, Task::goTo(pos));
1525           return;
1526         }
1527       }
1528     PTask task = Task::goToAndWait(pos, 15);
1529     task->setViewId(ViewId::GUARD_POST);
1530     getCollective()->setTask(c, std::move(task));
1531     pos.setNeedsRenderUpdate(true);
1532   }
1533 }
1534 
canSelectRectangle(const BuildInfo & info)1535 bool PlayerControl::canSelectRectangle(const BuildInfo& info) {
1536   switch (info.buildType) {
1537     case BuildInfo::ZONE:
1538     case BuildInfo::FORBID_ZONE:
1539     case BuildInfo::FURNITURE:
1540     case BuildInfo::DIG:
1541     case BuildInfo::DESTROY:
1542     case BuildInfo::DISPATCH:
1543     case BuildInfo::CLAIM_TILE:
1544       return true;
1545     default:
1546       return false;
1547   }
1548 }
1549 
processInput(View * view,UserInput input)1550 void PlayerControl::processInput(View* view, UserInput input) {
1551   switch (input.getId()) {
1552     case UserInputId::MESSAGE_INFO:
1553         if (auto message = findMessage(input.get<PlayerMessage::Id>())) {
1554           if (auto pos = message->getPosition())
1555             setScrollPos(*pos);
1556           else if (auto id = message->getCreature()) {
1557             if (WConstCreature c = getCreature(*id))
1558               setScrollPos(c->getPosition());
1559           }/* else if (auto loc = message->getLocation()) {
1560             if (loc->getMiddle().isSameLevel(getLevel()))
1561               scrollToMiddle(loc->getAllSquares());
1562             else
1563               setScrollPos(loc->getMiddle());
1564           }*/
1565         }
1566         break;
1567     case UserInputId::GO_TO_VILLAGE:
1568         if (WCollective col = getVillain(input.get<Collective::Id>())) {
1569           if (col->getLevel() != getLevel())
1570             setScrollPos(col->getTerritory().getAll()[0]);
1571           else
1572             scrollToMiddle(col->getTerritory().getAll());
1573         }
1574         break;
1575     case UserInputId::CREATE_TEAM:
1576         if (WCreature c = getCreature(input.get<Creature::Id>()))
1577           if (getCollective()->hasTrait(c, MinionTrait::FIGHTER) || c == getCollective()->getLeader())
1578             getTeams().create({c});
1579         break;
1580     case UserInputId::CREATE_TEAM_FROM_GROUP:
1581         if (WCreature creature = getCreature(input.get<Creature::Id>())) {
1582           vector<WCreature> group = getMinionsLike(creature);
1583           optional<TeamId> team;
1584           for (WCreature c : group)
1585             if (getCollective()->hasTrait(c, MinionTrait::FIGHTER) || c == getCollective()->getLeader()) {
1586               if (!team)
1587                 team = getTeams().create({c});
1588               else
1589                 getTeams().add(*team, c);
1590             }
1591         }
1592         break;
1593     case UserInputId::CREATURE_DRAG:
1594         draggedCreature = input.get<Creature::Id>();
1595         for (auto task : ENUM_ALL(MinionTask))
1596           for (auto& pos : MinionTasks::getAllPositions(getCollective(), nullptr, task))
1597             pos.setNeedsRenderUpdate(true);
1598         break;
1599     case UserInputId::CREATURE_DRAG_DROP:
1600         minionDragAndDrop(input.get<CreatureDropInfo>());
1601         draggedCreature = none;
1602         break;
1603     case UserInputId::TEAM_DRAG_DROP: {
1604         auto& info = input.get<TeamDropInfo>();
1605         Position pos = Position(info.pos, getLevel());
1606         if (getTeams().exists(info.teamId))
1607           for (WCreature c : getTeams().getMembers(info.teamId)) {
1608             c->removeEffect(LastingEffect::TIED_UP);
1609             c->removeEffect(LastingEffect::SLEEP);
1610             PTask task = Task::goToAndWait(pos, 15);
1611             task->setViewId(ViewId::GUARD_POST);
1612             getCollective()->setTask(c, std::move(task));
1613             pos.setNeedsRenderUpdate(true);
1614           }
1615         break;
1616     }
1617     case UserInputId::CANCEL_TEAM:
1618         if (getChosenTeam() == input.get<TeamId>()) {
1619           setChosenTeam(none);
1620           chosenCreature = none;
1621         }
1622         getTeams().cancel(input.get<TeamId>());
1623         break;
1624     case UserInputId::SELECT_TEAM: {
1625         auto teamId = input.get<TeamId>();
1626         if (getChosenTeam() == teamId) {
1627           setChosenTeam(none);
1628           chosenCreature = none;
1629         } else
1630           setChosenTeam(teamId, getTeams().getLeader(teamId)->getUniqueId());
1631         break;
1632     }
1633     case UserInputId::ACTIVATE_TEAM:
1634         if (!getTeams().isActive(input.get<TeamId>()))
1635           getTeams().activate(input.get<TeamId>());
1636         else
1637           getTeams().deactivate(input.get<TeamId>());
1638         break;
1639     case UserInputId::TILE_CLICK: {
1640         Vec2 pos = input.get<Vec2>();
1641         if (pos.inRectangle(getLevel()->getBounds()))
1642           onSquareClick(Position(pos, getLevel()));
1643         break;
1644         }
1645 
1646 /*    case UserInputId::MOVE_TO:
1647         if (getCurrentTeam() && getTeams().isActive(*getCurrentTeam()) &&
1648             getCollective()->getKnownTiles().isKnown(Position(input.get<Vec2>(), getLevel()))) {
1649           getCollective()->freeTeamMembers(*getCurrentTeam());
1650           getCollective()->setTask(getTeams().getLeader(*getCurrentTeam()),
1651               Task::goTo(Position(input.get<Vec2>(), getLevel())), true);
1652  //         view->continueClock();
1653         }
1654         break;*/
1655     case UserInputId::DRAW_LEVEL_MAP: view->drawLevelMap(this); break;
1656     case UserInputId::DRAW_WORLD_MAP: getGame()->presentWorldmap(); break;
1657     case UserInputId::TECHNOLOGY: getTechInfo()[input.get<int>()].butFun(this, view); break;
1658     case UserInputId::WORKSHOP: {
1659           int index = input.get<int>();
1660           if (index < 0 || index >= EnumInfo<WorkshopType>::size)
1661             setChosenWorkshop(none);
1662           else {
1663             WorkshopType type = (WorkshopType) index;
1664             if (chosenWorkshop == type)
1665               setChosenWorkshop(none);
1666             else
1667               setChosenWorkshop(type);
1668           }
1669         }
1670         break;
1671     case UserInputId::WORKSHOP_ADD:
1672         if (chosenWorkshop) {
1673           getCollective()->getWorkshops().get(*chosenWorkshop).queue(input.get<int>());
1674           getCollective()->getWorkshops().scheduleItems(getCollective());
1675           getCollective()->updateResourceProduction();
1676         }
1677         break;
1678     case UserInputId::LIBRARY_ADD:
1679         acquireTech(input.get<int>());
1680         break;
1681     case UserInputId::LIBRARY_CLOSE:
1682         setChosenLibrary(false);
1683         break;
1684     case UserInputId::WORKSHOP_ITEM_ACTION: {
1685         auto& info = input.get<WorkshopQueuedActionInfo>();
1686         if (chosenWorkshop) {
1687           auto& workshop = getCollective()->getWorkshops().get(*chosenWorkshop);
1688           if (info.itemIndex < workshop.getQueued().size()) {
1689             switch (info.action) {
1690               case ItemAction::REMOVE:
1691                 workshop.unqueue(info.itemIndex);
1692                 break;
1693               case ItemAction::CHANGE_NUMBER: {
1694                 int batchSize = workshop.getQueued()[info.itemIndex].batchSize;
1695                 if (auto number = getView()->getNumber("Change the number of items:", 0, 50 * batchSize, batchSize)) {
1696                   if (*number > 0)
1697                     workshop.changeNumber(info.itemIndex, *number / batchSize);
1698                   else
1699                     workshop.unqueue(info.itemIndex);
1700                 }
1701                 break;
1702               }
1703               default:
1704                 break;
1705             }
1706             getCollective()->getWorkshops().scheduleItems(getCollective());
1707             getCollective()->updateResourceProduction();
1708           }
1709         }
1710       }
1711       break;
1712     case UserInputId::CREATURE_GROUP_BUTTON:
1713         if (WCreature c = getCreature(input.get<Creature::Id>()))
1714           if (!chosenCreature || getChosenTeam() || !getCreature(*chosenCreature) ||
1715               getCreature(*chosenCreature)->getName().stack() != c->getName().stack()) {
1716             setChosenTeam(none);
1717             setChosenCreature(input.get<Creature::Id>());
1718             break;
1719           }
1720         setChosenTeam(none);
1721         chosenCreature = none;
1722         break;
1723     case UserInputId::CREATURE_BUTTON: {
1724         auto chosenId = input.get<Creature::Id>();
1725         if (WCreature c = getCreature(chosenId)) {
1726           if (!getChosenTeam() || !getTeams().contains(*getChosenTeam(), c))
1727             setChosenCreature(chosenId);
1728           else
1729             setChosenTeam(*chosenTeam, chosenId);
1730         }
1731         else
1732           setChosenTeam(none);
1733       }
1734       break;
1735     case UserInputId::CREATURE_TASK_ACTION:
1736         minionTaskAction(input.get<TaskActionInfo>());
1737         break;
1738     case UserInputId::CREATURE_EQUIPMENT_ACTION:
1739         minionEquipmentAction(input.get<EquipmentActionInfo>());
1740         break;
1741     case UserInputId::CREATURE_CONTROL:
1742         if (WCreature c = getCreature(input.get<Creature::Id>())) {
1743           if (getChosenTeam() && getTeams().exists(*getChosenTeam())) {
1744             getTeams().setLeader(*getChosenTeam(), c);
1745             commandTeam(*getChosenTeam());
1746           } else
1747             controlSingle(c);
1748           chosenCreature = none;
1749           setChosenTeam(none);
1750         }
1751         break;
1752     case UserInputId::CREATURE_RENAME:
1753         if (WCreature c = getCreature(input.get<RenameActionInfo>().creature))
1754           c->getName().setFirst(input.get<RenameActionInfo>().name);
1755         break;
1756     case UserInputId::CREATURE_CONSUME:
1757         if (WCreature c = getCreature(input.get<Creature::Id>())) {
1758           if (auto creatureId = getView()->chooseCreature("Choose minion to absorb",
1759               getCollective()->getConsumptionTargets(c).transform(
1760                   [] (WConstCreature c) { return CreatureInfo(c);}), "cancel"))
1761             if (WCreature consumed = getCreature(*creatureId))
1762               getCollective()->orderConsumption(c, consumed);
1763         }
1764         break;
1765     case UserInputId::CREATURE_BANISH:
1766         if (WCreature c = getCreature(input.get<Creature::Id>()))
1767           if (getView()->yesOrNoPrompt("Do you want to banish " + c->getName().the() + " forever? "
1768               "Banishing has a negative impact on morale of other minions.")) {
1769             vector<WCreature> like = getMinionsLike(c);
1770             sortMinionsForUI(like);
1771             if (like.size() > 1)
1772               for (int i : All(like))
1773                 if (like[i] == c) {
1774                   if (i < like.size() - 1)
1775                     setChosenCreature(like[i + 1]->getUniqueId());
1776                   else
1777                     setChosenCreature(like[like.size() - 2]->getUniqueId());
1778                   break;
1779                 }
1780             getCollective()->banishCreature(c);
1781           }
1782         break;
1783     case UserInputId::GO_TO_ENEMY:
1784         for (Vec2 v : getVisibleEnemies())
1785           if (WCreature c = Position(v, getCollective()->getLevel()).getCreature())
1786             setScrollPos(c->getPosition());
1787         break;
1788     case UserInputId::ADD_GROUP_TO_TEAM: {
1789         auto info = input.get<TeamCreatureInfo>();
1790         if (WCreature creature = getCreature(info.creatureId)) {
1791           vector<WCreature> group = getMinionsLike(creature);
1792           for (WCreature c : group)
1793             if (getTeams().exists(info.team) && !getTeams().contains(info.team, c) &&
1794                 (getCollective()->hasTrait(c, MinionTrait::FIGHTER) || c == getCollective()->getLeader()))
1795               getTeams().add(info.team, c);
1796         }
1797         break; }
1798     case UserInputId::ADD_TO_TEAM: {
1799         auto info = input.get<TeamCreatureInfo>();
1800         if (WCreature c = getCreature(info.creatureId))
1801           if (getTeams().exists(info.team) && !getTeams().contains(info.team, c) &&
1802               (getCollective()->hasTrait(c, MinionTrait::FIGHTER) || c == getCollective()->getLeader()))
1803             getTeams().add(info.team, c);
1804         break; }
1805     case UserInputId::REMOVE_FROM_TEAM: {
1806         auto info = input.get<TeamCreatureInfo>();
1807         if (WCreature c = getCreature(info.creatureId))
1808           if (getTeams().exists(info.team) && getTeams().contains(info.team, c)) {
1809             getTeams().remove(info.team, c);
1810             if (getTeams().exists(info.team)) {
1811               if (chosenCreature == info.creatureId)
1812                 setChosenTeam(info.team, getTeams().getLeader(info.team)->getUniqueId());
1813             } else
1814               chosenCreature = none;
1815           }
1816         break; }
1817     case UserInputId::IMMIGRANT_ACCEPT: {
1818         auto available = getCollective()->getImmigration().getAvailable();
1819         if (auto info = getReferenceMaybe(available, input.get<int>()))
1820           if (auto sound = info->get().getInfo().getSound())
1821             getView()->addSound(*sound);
1822         getCollective()->getImmigration().accept(input.get<int>());
1823         break; }
1824     case UserInputId::IMMIGRANT_REJECT:
1825         getCollective()->getImmigration().rejectIfNonPersistent(input.get<int>());
1826         break;
1827     case UserInputId::IMMIGRANT_AUTO_ACCEPT: {
1828         int id = input.get<int>();
1829         if (!!getCollective()->getImmigration().getAutoState(id))
1830           getCollective()->getImmigration().setAutoState(id, none);
1831         else
1832           getCollective()->getImmigration().setAutoState(id, ImmigrantAutoState::AUTO_ACCEPT);
1833         }
1834         break;
1835     case UserInputId::IMMIGRANT_AUTO_REJECT: {
1836         int id = input.get<int>();
1837         if (!!getCollective()->getImmigration().getAutoState(id))
1838           getCollective()->getImmigration().setAutoState(id, none);
1839         else
1840           getCollective()->getImmigration().setAutoState(id, ImmigrantAutoState::AUTO_REJECT);
1841         }
1842         break;
1843     case UserInputId::RECT_SELECTION: {
1844         auto& info = input.get<BuildingInfo>();
1845         if (canSelectRectangle(BuildInfo::get()[info.building])) {
1846           updateSelectionSquares();
1847           if (rectSelection) {
1848             rectSelection->corner2 = info.pos;
1849           } else
1850             rectSelection = CONSTRUCT(SelectionInfo, c.corner1 = c.corner2 = info.pos;);
1851           updateSelectionSquares();
1852         } else
1853           handleSelection(info.pos, BuildInfo::get()[info.building], false);
1854         break; }
1855     case UserInputId::RECT_DESELECTION:
1856         updateSelectionSquares();
1857         if (rectSelection) {
1858           rectSelection->corner2 = input.get<Vec2>();
1859         } else
1860           rectSelection = CONSTRUCT(SelectionInfo, c.corner1 = c.corner2 = input.get<Vec2>(); c.deselect = true;);
1861         updateSelectionSquares();
1862         break;
1863     case UserInputId::BUILD: {
1864         auto& info = input.get<BuildingInfo>();
1865         handleSelection(info.pos, BuildInfo::get()[info.building], false);
1866         break; }
1867     case UserInputId::VILLAGE_ACTION: {
1868         auto& info = input.get<VillageActionInfo>();
1869         if (WCollective village = getVillain(info.id))
1870           switch (info.action) {
1871             case VillageAction::TRADE:
1872               handleTrading(village);
1873               break;
1874             case VillageAction::PILLAGE:
1875               handlePillage(village);
1876               break;
1877           }
1878         break;
1879     }
1880     case UserInputId::PAY_RANSOM:
1881         handleRansom(true);
1882         break;
1883     case UserInputId::IGNORE_RANSOM:
1884         handleRansom(false);
1885         break;
1886     case UserInputId::SHOW_HISTORY:
1887         PlayerMessage::presentMessages(getView(), messageHistory);
1888         break;
1889     case UserInputId::CHEAT_ATTRIBUTES:
1890         for (auto resource : ENUM_ALL(CollectiveResourceId))
1891           getCollective()->returnResource(CostInfo(resource, 1000));
1892         break;
1893     case UserInputId::TUTORIAL_CONTINUE:
1894         if (tutorial)
1895           tutorial->continueTutorial(getGame());
1896         break;
1897     case UserInputId::TUTORIAL_GO_BACK:
1898         if (tutorial)
1899           tutorial->goBack();
1900         break;
1901     case UserInputId::RECT_CONFIRM:
1902         if (rectSelection) {
1903           selection = rectSelection->deselect ? DESELECT : SELECT;
1904           for (Vec2 v : Rectangle::boundingBox({rectSelection->corner1, rectSelection->corner2}))
1905             handleSelection(v, BuildInfo::get()[input.get<BuildingInfo>().building], true, rectSelection->deselect);
1906         }
1907         FALLTHROUGH;
1908     case UserInputId::RECT_CANCEL:
1909         updateSelectionSquares();
1910         rectSelection = none;
1911         selection = NONE;
1912         break;
1913     case UserInputId::EXIT: getGame()->exitAction(); return;
1914     case UserInputId::IDLE: break;
1915     case UserInputId::DISMISS_NEXT_WAVE:
1916       if (auto& enemies = getModel()->getExternalEnemies())
1917         if (auto nextWave = enemies->getNextWave())
1918           dismissedNextWaves.insert(enemies->getNextWaveIndex());
1919       break;
1920     default: break;
1921   }
1922 }
1923 
updateSelectionSquares()1924 void PlayerControl::updateSelectionSquares() {
1925   if (rectSelection)
1926     for (Vec2 v : Rectangle::boundingBox({rectSelection->corner1, rectSelection->corner2}))
1927       Position(v, getLevel()).setNeedsRenderUpdate(true);
1928 }
1929 
handleSelection(Vec2 pos,const BuildInfo & building,bool rectangle,bool deselectOnly)1930 void PlayerControl::handleSelection(Vec2 pos, const BuildInfo& building, bool rectangle, bool deselectOnly) {
1931   Position position(pos, getLevel());
1932   for (auto& req : building.requirements)
1933     if (!BuildInfo::meetsRequirement(getCollective(), req))
1934       return;
1935   if (position.isUnavailable())
1936     return;
1937   if (!deselectOnly && rectangle && !canSelectRectangle(building))
1938     return;
1939   switch (building.buildType) {
1940     case BuildInfo::TRAP:
1941         if (getCollective()->getConstructions().getTrap(position) && selection != SELECT) {
1942           getCollective()->removeTrap(position);
1943           getView()->addSound(SoundId::DIG_UNMARK);
1944           selection = DESELECT;
1945           // Does this mean I can remove the order if the trap physically exists?
1946         } else
1947         if (position.canEnterEmpty({MovementTrait::WALK}) &&
1948             getCollective()->getTerritory().contains(position) &&
1949             !getCollective()->getConstructions().getTrap(position) &&
1950             selection != DESELECT) {
1951           getCollective()->addTrap(position, building.trapInfo.type);
1952           getView()->addSound(SoundId::ADD_CONSTRUCTION);
1953           selection = SELECT;
1954         }
1955       break;
1956     case BuildInfo::DESTROY:
1957         for (auto layer : building.destroyLayers) {
1958           auto f = getCollective()->getConstructions().getFurniture(position, layer);
1959           if (f && !f->isBuilt()) {
1960             getCollective()->removeFurniture(position, layer);
1961             getView()->addSound(SoundId::DIG_UNMARK);
1962             selection = SELECT;
1963           } else
1964           if (getCollective()->getKnownTiles().isKnown(position) && !position.isBurning()) {
1965             selection = SELECT;
1966             getCollective()->destroySquare(position, layer);
1967             if (auto f = position.getFurniture(layer))
1968               if (f->getType() == FurnitureType::TREE_TRUNK)
1969                 position.removeFurniture(f);
1970             getView()->addSound(SoundId::REMOVE_CONSTRUCTION);
1971             updateSquareMemory(position);
1972           }
1973         }
1974         break;
1975     case BuildInfo::FORBID_ZONE:
1976         if (position.isTribeForbidden(getTribeId()) && selection != SELECT) {
1977           position.allowMovementForTribe(getTribeId());
1978           selection = DESELECT;
1979         }
1980         else if (!position.isTribeForbidden(getTribeId()) && selection != DESELECT) {
1981           position.forbidMovementForTribe(getTribeId());
1982           selection = SELECT;
1983         }
1984         break;
1985     case BuildInfo::DIG: {
1986         bool markedToDig = getCollective()->isMarked(position) &&
1987             (getCollective()->getMarkHighlight(position) == HighlightType::DIG ||
1988              getCollective()->getMarkHighlight(position) == HighlightType::CUT_TREE);
1989         if (markedToDig && selection != SELECT) {
1990           getCollective()->cancelMarkedTask(position);
1991           getView()->addSound(SoundId::DIG_UNMARK);
1992           selection = DESELECT;
1993         } else
1994         if (!markedToDig && selection != DESELECT) {
1995           if (auto furniture = position.getFurniture(FurnitureLayer::MIDDLE))
1996             for (auto type : {DestroyAction::Type::CUT, DestroyAction::Type::DIG})
1997               if (furniture->canDestroy(type)) {
1998                 getCollective()->orderDestruction(position, type);
1999                 getView()->addSound(SoundId::DIG_MARK);
2000                 selection = SELECT;
2001                 break;
2002               }
2003         }
2004         break;
2005         }
2006     case BuildInfo::ZONE:
2007         if (getCollective()->getZones().isZone(position, building.zone) && selection != SELECT) {
2008           getCollective()->getZones().eraseZone(position, building.zone);
2009           selection = DESELECT;
2010         } else if (selection != DESELECT && !getCollective()->getZones().isZone(position, building.zone) &&
2011             getCollective()->getKnownTiles().isKnown(position)) {
2012           getCollective()->getZones().setZone(position, building.zone);
2013           selection = SELECT;
2014         }
2015         break;
2016     case BuildInfo::CLAIM_TILE:
2017         if (getCollective()->canClaimSquare(position))
2018           getCollective()->claimSquare(position);
2019         break;
2020     case BuildInfo::DISPATCH:
2021         getCollective()->setPriorityTasks(position);
2022         break;
2023     case BuildInfo::FURNITURE: {
2024         auto& info = building.furnitureInfo;
2025         auto layer = Furniture::getLayer(info.types[0]);
2026         auto currentPlanned = getCollective()->getConstructions().getFurniture(position, layer);
2027         if (currentPlanned && currentPlanned->isBuilt())
2028           currentPlanned = none;
2029         int nextIndex = 0;
2030         if (currentPlanned) {
2031           if (auto currentIndex = info.types.findElement(currentPlanned->getFurnitureType()))
2032             nextIndex = *currentIndex + 1;
2033           else
2034             break;
2035         }
2036         bool removed = false;
2037         if (!!currentPlanned && selection != SELECT) {
2038           getCollective()->removeFurniture(position, layer);
2039           removed = true;
2040         }
2041         while (nextIndex < info.types.size() && !getCollective()->canAddFurniture(position, info.types[nextIndex]))
2042           ++nextIndex;
2043         int totalCount = 0;
2044         for (auto type : info.types)
2045           totalCount += getCollective()->getConstructions().getTotalCount(type);
2046         if (nextIndex < info.types.size() && selection != DESELECT &&
2047             (!info.maxNumber || *info.maxNumber > totalCount)) {
2048           getCollective()->addFurniture(position, info.types[nextIndex], info.cost, info.noCredit);
2049           getCollective()->updateResourceProduction();
2050           selection = SELECT;
2051           getView()->addSound(SoundId::ADD_CONSTRUCTION);
2052         } else if (removed) {
2053           selection = DESELECT;
2054           getView()->addSound(SoundId::DIG_UNMARK);
2055         }
2056         break;
2057       }
2058   }
2059 }
2060 
onSquareClick(Position pos)2061 void PlayerControl::onSquareClick(Position pos) {
2062   if (getCollective()->getTerritory().contains(pos))
2063     if (auto furniture = pos.getFurniture(FurnitureLayer::MIDDLE)) {
2064       if (furniture->isClickable()) {
2065         furniture->click(pos); // this can remove the furniture
2066         updateSquareMemory(pos);
2067       } else {
2068         if (auto workshopType = CollectiveConfig::getWorkshopType(furniture->getType()))
2069           setChosenWorkshop(*workshopType);
2070         if (furniture->getUsageType() == FurnitureUsageType::STUDY)
2071           setChosenLibrary(!chosenLibrary);
2072       }
2073     }
2074 }
2075 
getLocalTime() const2076 double PlayerControl::getLocalTime() const {
2077   return getModel()->getLocalTime();
2078 }
2079 
getCenterType() const2080 PlayerControl::CenterType PlayerControl::getCenterType() const {
2081   return CenterType::NONE;
2082 }
2083 
getUnknownLocations(WConstLevel) const2084 vector<Vec2> PlayerControl::getUnknownLocations(WConstLevel) const {
2085   vector<Vec2> ret;
2086   for (auto col : getModel()->getCollectives())
2087     if (col->getLevel() == getLevel() && !getCollective()->isKnownVillainLocation(col))
2088       if (auto& pos = col->getTerritory().getCentralPoint())
2089         ret.push_back(pos->getCoord());
2090   return ret;
2091 }
2092 
getKeeper() const2093 WConstCreature PlayerControl::getKeeper() const {
2094   return getCollective()->getLeader();
2095 }
2096 
getKeeper()2097 WCreature PlayerControl::getKeeper() {
2098   return getCollective()->getLeader();
2099 }
2100 
addToMemory(Position pos)2101 void PlayerControl::addToMemory(Position pos) {
2102   if (!pos.needsMemoryUpdate())
2103     return;
2104   pos.setNeedsMemoryUpdate(false);
2105   ViewIndex index;
2106   getSquareViewIndex(pos, true, index);
2107   memory->update(pos, index);
2108 }
2109 
checkKeeperDanger()2110 void PlayerControl::checkKeeperDanger() {
2111   auto controlled = getControlled();
2112   WCreature keeper = getKeeper();
2113   auto prompt = [&] {
2114       return getView()->yesOrNoPrompt("The Keeper is in trouble. Do you want to control " +
2115           keeper->getAttributes().getGender().him() + "?");
2116   };
2117   if (!getKeeper()->isDead() && !controlled.contains(getKeeper())) {
2118     if ((getKeeper()->wasInCombat(5) || getKeeper()->getBody().isWounded())
2119         && lastControlKeeperQuestion < getCollective()->getGlobalTime() - 50) {
2120       lastControlKeeperQuestion = getCollective()->getGlobalTime();
2121       if (prompt()) {
2122         controlSingle(getKeeper());
2123         return;
2124       }
2125     }
2126     if (getKeeper()->isAffected(LastingEffect::POISON)
2127         && lastControlKeeperQuestion < getCollective()->getGlobalTime() - 5) {
2128       lastControlKeeperQuestion = getCollective()->getGlobalTime();
2129       if (prompt()) {
2130         controlSingle(getKeeper());
2131         return;
2132       }
2133     }
2134   }
2135 }
2136 
onNoEnemies()2137 void PlayerControl::onNoEnemies() {
2138   getGame()->setCurrentMusic(MusicType::PEACEFUL, false);
2139 }
2140 
onPositionDiscovered(Position pos)2141 void PlayerControl::onPositionDiscovered(Position pos) {
2142   if (getCollective()->addKnownTile(pos))
2143     updateKnownLocations(pos);
2144   addToMemory(pos);
2145 }
2146 
considerNightfallMessage()2147 void PlayerControl::considerNightfallMessage() {
2148   /*if (getGame()->getSunlightInfo().getState() == SunlightState::NIGHT) {
2149     if (!isNight) {
2150       addMessage(PlayerMessage("Night is falling. Killing enemies in their sleep yields double mana.",
2151             MessagePriority::HIGH));
2152       isNight = true;
2153     }
2154   } else
2155     isNight = false;*/
2156 }
2157 
update(bool currentlyActive)2158 void PlayerControl::update(bool currentlyActive) {
2159   updateVisibleCreatures();
2160   vector<WCreature> addedCreatures;
2161   vector<WLevel> currentLevels {getLevel()};
2162   for (auto c : getControlled())
2163     if (!currentLevels.contains(c->getLevel()))
2164       currentLevels.push_back(c->getLevel());
2165   for (WLevel l : currentLevels)
2166     for (WCreature c : l->getAllCreatures())
2167       if (c->getTribeId() == getTribeId() && canSee(c) && !isEnemy(c)) {
2168         if (c->getAttributes().getSpawnType() && !getCreatures().contains(c) && !getCollective()->wasBanished(c)) {
2169           addedCreatures.push_back(c);
2170           getCollective()->addCreature(c, {MinionTrait::FIGHTER});
2171           for (auto controlled : getControlled())
2172             if ((getCollective()->hasTrait(controlled, MinionTrait::FIGHTER)
2173                   || controlled == getCollective()->getLeader())
2174                 && c->getPosition().isSameLevel(controlled->getPosition())) {
2175               for (auto team : getTeams().getActive(controlled)) {
2176                 getTeams().add(team, c);
2177                 controlled->privateMessage(PlayerMessage(c->getName().a() + " joins your team.",
2178                       MessagePriority::HIGH));
2179                 break;
2180               }
2181               break;
2182             }
2183         } else
2184           if (c->getBody().isMinionFood() && !getCreatures().contains(c))
2185             getCollective()->addCreature(c, {MinionTrait::FARM_ANIMAL, MinionTrait::NO_LIMIT});
2186       }
2187   if (!addedCreatures.empty()) {
2188     getCollective()->addNewCreatureMessage(addedCreatures);
2189   }
2190 }
2191 
isConsideredAttacking(WConstCreature c,WConstCollective enemy)2192 bool PlayerControl::isConsideredAttacking(WConstCreature c, WConstCollective enemy) {
2193   if (enemy && enemy->getModel() == getModel())
2194     return canSee(c) && getCollective()->getTerritory().getStandardExtended().contains(c->getPosition());
2195   else
2196     return canSee(c) && c->getLevel() == getLevel();
2197 }
2198 
2199 const double messageTimeout = 80;
2200 
tick()2201 void PlayerControl::tick() {
2202   for (auto& elem : messages)
2203     elem.setFreshness(max(0.0, elem.getFreshness() - 1.0 / messageTimeout));
2204   messages = messages.filter([&] (const PlayerMessage& msg) {
2205       return msg.getFreshness() > 0; });
2206   considerNightfallMessage();
2207   if (auto msg = getCollective()->getWarnings().getNextWarning(getLocalTime()))
2208     addMessage(PlayerMessage(*msg, MessagePriority::HIGH));
2209   checkKeeperDanger();
2210   for (auto attack : copyOf(ransomAttacks))
2211     for (WConstCreature c : attack.getCreatures())
2212       if (getCollective()->getTerritory().contains(c->getPosition())) {
2213         ransomAttacks.removeElement(attack);
2214         break;
2215       }
2216   for (auto attack : copyOf(newAttacks))
2217     for (WConstCreature c : attack.getCreatures())
2218       if (isConsideredAttacking(c, attack.getAttacker())) {
2219         addMessage(PlayerMessage("You are under attack by " + attack.getAttackerName() + "!",
2220             MessagePriority::CRITICAL).setPosition(c->getPosition()));
2221         getGame()->setCurrentMusic(MusicType::BATTLE, true);
2222         newAttacks.removeElement(attack);
2223         if (auto attacker = attack.getAttacker())
2224           getCollective()->addKnownVillain(attacker);
2225         if (attack.getRansom())
2226           ransomAttacks.push_back(attack);
2227         break;
2228       }
2229   double time = getCollective()->getLocalTime();
2230   if (getGame()->getOptions()->getBoolValue(OptionId::HINTS) && time > hintFrequency) {
2231     int numHint = int(time) / hintFrequency - 1;
2232     if (numHint < hints.size() && !hints[numHint].empty()) {
2233       addMessage(PlayerMessage(hints[numHint], MessagePriority::HIGH));
2234       hints[numHint] = "";
2235     }
2236   }
2237 }
2238 
canSee(WConstCreature c) const2239 bool PlayerControl::canSee(WConstCreature c) const {
2240   return canSee(c->getPosition());
2241 }
2242 
canSee(Position pos) const2243 bool PlayerControl::canSee(Position pos) const {
2244   return getGame()->getOptions()->getBoolValue(OptionId::SHOW_MAP) || visibilityMap->isVisible(pos);
2245 }
2246 
getTribeId() const2247 TribeId PlayerControl::getTribeId() const {
2248   return getCollective()->getTribeId();
2249 }
2250 
isEnemy(WConstCreature c) const2251 bool PlayerControl::isEnemy(WConstCreature c) const {
2252   return getKeeper() && getKeeper()->isEnemy(c);
2253 }
2254 
onMemberKilled(WConstCreature victim,WConstCreature killer)2255 void PlayerControl::onMemberKilled(WConstCreature victim, WConstCreature killer) {
2256   if (victim->isPlayer() && victim != getKeeper())
2257     onControlledKilled(victim);
2258   visibilityMap->remove(victim);
2259   if (victim == getKeeper() && !getGame()->isGameOver()) {
2260     getGame()->gameOver(victim, getCollective()->getKills().getSize(), "enemies",
2261         getCollective()->getDangerLevel() + getCollective()->getPoints());
2262   }
2263 }
2264 
onMemberAdded(WConstCreature c)2265 void PlayerControl::onMemberAdded(WConstCreature c) {
2266   updateMinionVisibility(c);
2267 }
2268 
getLevel() const2269 WLevel PlayerControl::getLevel() const {
2270   return getCollective()->getLevel();
2271 }
2272 
getModel() const2273 WModel PlayerControl::getModel() const {
2274   return getLevel()->getModel();
2275 }
2276 
getGame() const2277 WGame PlayerControl::getGame() const {
2278   return getLevel()->getModel()->getGame();
2279 }
2280 
getView() const2281 View* PlayerControl::getView() const {
2282   return getGame()->getView();
2283 }
2284 
addAttack(const CollectiveAttack & attack)2285 void PlayerControl::addAttack(const CollectiveAttack& attack) {
2286   newAttacks.push_back(attack);
2287 }
2288 
updateSquareMemory(Position pos)2289 void PlayerControl::updateSquareMemory(Position pos) {
2290   ViewIndex index;
2291   pos.getViewIndex(index, getCollective()->getLeader()); // use the leader as a generic viewer
2292   memory->update(pos, index);
2293 }
2294 
onConstructed(Position pos,FurnitureType type)2295 void PlayerControl::onConstructed(Position pos, FurnitureType type) {
2296   if (type == FurnitureType::EYEBALL)
2297     visibilityMap->updateEyeball(pos);
2298 }
2299 
createMinionController(WCreature c)2300 PController PlayerControl::createMinionController(WCreature c) {
2301   return ::getMinionController(c, memory, this, controlModeMessages, visibilityMap, tutorial);
2302 }
2303 
onClaimedSquare(Position position)2304 void PlayerControl::onClaimedSquare(Position position) {
2305   auto ground = position.modFurniture(FurnitureLayer::GROUND);
2306   CHECK(ground) << "No ground found at " << position.getCoord();
2307   ground->getViewObject()->setId(ViewId::KEEPER_FLOOR);
2308   position.setNeedsRenderUpdate(true);
2309   updateSquareMemory(position);
2310 }
2311 
onDestructed(Position pos,FurnitureType type,const DestroyAction & action)2312 void PlayerControl::onDestructed(Position pos, FurnitureType type, const DestroyAction& action) {
2313   if (action.getType() == DestroyAction::Type::DIG) {
2314     Vec2 visRadius(3, 3);
2315     for (Position v : pos.getRectangle(Rectangle(-visRadius, visRadius + Vec2(1, 1)))) {
2316       getCollective()->addKnownTile(v);
2317       updateSquareMemory(v);
2318     }
2319     pos.modFurniture(FurnitureLayer::GROUND)->getViewObject()->setId(ViewId::KEEPER_FLOOR);
2320     pos.setNeedsRenderUpdate(true);
2321   }
2322 }
2323 
updateVisibleCreatures()2324 void PlayerControl::updateVisibleCreatures() {
2325   visibleEnemies.clear();
2326   for (WConstCreature c : getLevel()->getAllCreatures())
2327     if (canSee(c) && isEnemy(c))
2328       visibleEnemies.push_back(c->getPosition().getCoord());
2329 }
2330 
getVisibleEnemies() const2331 vector<Vec2> PlayerControl::getVisibleEnemies() const {
2332   return visibleEnemies;
2333 }
2334 
2335 REGISTER_TYPE(ListenerTemplate<PlayerControl>)
2336