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