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 "map_gui.h"
19 #include "view_object.h"
20 #include "map_layout.h"
21 #include "view_index.h"
22 #include "tile.h"
23 #include "window_view.h"
24 #include "renderer.h"
25 #include "clock.h"
26 #include "view_id.h"
27 #include "level.h"
28 #include "creature_view.h"
29 #include "options.h"
30 #include "drag_and_drop.h"
31 #include "game_info.h"
32 
33 using SDL::SDL_Keysym;
34 using SDL::SDL_Keycode;
35 
MapGui(Callbacks call,Clock * c,Options * o,GuiFactory * f)36 MapGui::MapGui(Callbacks call, Clock* c, Options* o, GuiFactory* f) : objects(Level::getMaxBounds()), callbacks(call),
37     clock(c), options(o), fogOfWar(Level::getMaxBounds(), false), extraBorderPos(Level::getMaxBounds(), {}),
38     lastSquareUpdate(Level::getMaxBounds()), connectionMap(Level::getMaxBounds()), guiFactory(f) {
39   clearCenter();
40 }
41 
42 static int fireVar = 50;
43 
getFireColor()44 static Color getFireColor() {
45   return Color(200 + Random.get(-fireVar, fireVar), Random.get(fireVar), Random.get(fireVar), 150);
46 }
47 
setButtonViewId(ViewId id)48 void MapGui::setButtonViewId(ViewId id) {
49   buttonViewId = id;
50 }
51 
clearButtonViewId()52 void MapGui::clearButtonViewId() {
53   buttonViewId = none;
54 }
55 
getLastHighlighted()56 const MapGui::HighlightedInfo& MapGui::getLastHighlighted() {
57   return lastHighlighted;
58 }
59 
highlightTeam(const vector<UniqueEntity<Creature>::Id> & ids)60 void MapGui::highlightTeam(const vector<UniqueEntity<Creature>::Id>& ids) {
61   for (auto& id : ids)
62     ++teamHighlight.getOrInit(id);
63 }
64 
unhighlightTeam(const vector<UniqueEntity<Creature>::Id> & ids)65 void MapGui::unhighlightTeam(const vector<UniqueEntity<Creature>::Id>& ids) {
66   for (auto& id : ids)
67     CHECK(--teamHighlight.getOrFail(id) >= 0);
68 }
69 
getScreenPos() const70 Vec2 MapGui::getScreenPos() const {
71   return Vec2(
72       (int) (min<double>(levelBounds.right(), max(0.0, center.x - mouseOffset.x)) * layout->getSquareSize().x),
73       (int) (min<double>(levelBounds.bottom(), max(0.0, center.y - mouseOffset.y)) * layout->getSquareSize().y));
74 }
75 
setSpriteMode(bool s)76 void MapGui::setSpriteMode(bool s) {
77   spriteMode = s;
78 }
79 
addAnimation(PAnimation animation,Vec2 pos)80 void MapGui::addAnimation(PAnimation animation, Vec2 pos) {
81   animation->setBegin(clock->getRealMillis());
82   animations.push_back({std::move(animation), pos});
83 }
84 
getMousePos()85 optional<Vec2> MapGui::getMousePos() {
86   if (lastMouseMove && lastMouseMove->inRectangle(getBounds()))
87     return lastMouseMove;
88   else
89     return none;
90 }
91 
projectOnMap(Vec2 screenCoord)92 optional<Vec2> MapGui::projectOnMap(Vec2 screenCoord) {
93   if (screenCoord.inRectangle(getBounds()))
94     return layout->projectOnMap(getBounds(), getScreenPos(), screenCoord);
95   else
96     return none;
97 }
98 
getHighlightedTile(Renderer & renderer)99 optional<Vec2> MapGui::getHighlightedTile(Renderer& renderer) {
100   if (auto pos = getMousePos())
101     return layout->projectOnMap(getBounds(), getScreenPos(), *pos);
102   else
103     return none;
104 }
105 
getHighlightColor(const ViewIndex & index,HighlightType type)106 Color MapGui::getHighlightColor(const ViewIndex& index, HighlightType type) {
107   double amount = index.getHighlight(type);
108   switch (type) {
109     case HighlightType::RECT_DESELECTION: return Color::RED.transparency(90);
110     case HighlightType::DIG: return Color::YELLOW.transparency(120);
111     case HighlightType::FETCH_ITEMS: //return Color(Color::YELLOW).transparency(50);
112     case HighlightType::CUT_TREE: return Color::YELLOW.transparency(100);
113     case HighlightType::PERMANENT_FETCH_ITEMS: return Color::ORANGE.transparency(50);
114     case HighlightType::STORAGE_EQUIPMENT: return Color::BLUE.transparency(buttonViewId ? 120 : 50);
115     case HighlightType::STORAGE_RESOURCES: return Color::GREEN.transparency(buttonViewId ? 120 : 50);
116     case HighlightType::RECT_SELECTION: return Color::YELLOW.transparency(90);
117     case HighlightType::FOG: return Color::WHITE.transparency(120 * amount);
118     case HighlightType::POISON_GAS: return Color(0, min<Uint8>(255., amount * 500), 0, (Uint8)(amount * 140));
119     case HighlightType::MEMORY: return Color::BLACK.transparency(80);
120     case HighlightType::NIGHT: return Color::NIGHT_BLUE.transparency(amount * 160);
121     case HighlightType::EFFICIENCY: return Color(255, 0, 0, 120 * (1 - amount));
122     case HighlightType::PRIORITY_TASK: return Color(0, 255, 0, 120);
123     case HighlightType::CREATURE_DROP:
124       if (index.hasObject(ViewLayer::FLOOR) && getHighlightedFurniture() == index.getObject(ViewLayer::FLOOR).id())
125         return Color(0, 255, 0);
126       else
127         return Color(0, 255, 0, 120);
128     case HighlightType::CLICKABLE_FURNITURE: return Color(255, 255, 0, 120);
129     case HighlightType::CLICKED_FURNITURE: return Color(255, 255, 0);
130     case HighlightType::FORBIDDEN_ZONE: return Color(255, 0, 0, 120);
131     case HighlightType::UNAVAILABLE: return Color(0, 0, 0, 120);
132   }
133 }
134 
getConnectionId(ViewId id)135 static ViewId getConnectionId(ViewId id) {
136   switch (id) {
137     case ViewId::BLACK_WALL:
138     case ViewId::WOOD_WALL:
139     case ViewId::CASTLE_WALL:
140     case ViewId::MUD_WALL:
141     case ViewId::MOUNTAIN:
142     case ViewId::DUNGEON_WALL:
143     case ViewId::GOLD_ORE:
144     case ViewId::IRON_ORE:
145     case ViewId::STONE:
146     case ViewId::WALL: return ViewId::WALL;
147     default: return id;
148   }
149 }
150 
getConnectionSet(Vec2 tilePos,ViewId id)151 DirSet MapGui::getConnectionSet(Vec2 tilePos, ViewId id) {
152   DirSet ret;
153   int cnt = 0;
154   for (Vec2 dir : Vec2::directions8()) {
155     Vec2 pos = tilePos + dir;
156     if (pos.inRectangle(levelBounds) && connectionMap[pos].contains(getConnectionId(id)))
157       ret.insert((Dir) cnt);
158     ++cnt;
159   }
160   return ret;
161 }
162 
setSoftCenter(Vec2 pos)163 void MapGui::setSoftCenter(Vec2 pos) {
164   setSoftCenter(pos.x, pos.y);
165 }
166 
setSoftCenter(double x,double y)167 void MapGui::setSoftCenter(double x, double y) {
168   Coords coords {x, y};
169   if (softCenter != coords) {
170     softCenter = coords;
171     lastScrollUpdate = clock->getRealMillis();
172   }
173 }
174 
softScroll(double x,double y)175 void MapGui::softScroll(double x, double y) {
176   if (!softCenter)
177     softCenter = center;
178   softCenter->x = max(0.0, min<double>(softCenter->x + x, levelBounds.right()));
179   softCenter->y = max(0.0, min<double>(softCenter->y + y, levelBounds.bottom()));
180   lastScrollUpdate = clock->getRealMillis();
181 }
182 
onKeyPressed2(SDL_Keysym key)183 bool MapGui::onKeyPressed2(SDL_Keysym key) {
184   const double scrollDist = 9 * 32 / layout->getSquareSize().x;
185   if (!keyScrolling)
186     return false;
187   switch (key.sym) {
188     case SDL::SDLK_w:
189       if (!options->getBoolValue(OptionId::WASD_SCROLLING) || GuiFactory::isAlt(key))
190         break;
191       FALLTHROUGH;
192     case SDL::SDLK_UP:
193     case SDL::SDLK_KP_8:
194       softScroll(0, -scrollDist);
195       break;
196     case SDL::SDLK_KP_9:
197       softScroll(scrollDist, -scrollDist);
198       break;
199     case SDL::SDLK_d:
200       if (!options->getBoolValue(OptionId::WASD_SCROLLING) || GuiFactory::isAlt(key))
201         break;
202       FALLTHROUGH;
203     case SDL::SDLK_RIGHT:
204     case SDL::SDLK_KP_6:
205       softScroll(scrollDist, 0);
206       break;
207     case SDL::SDLK_KP_3:
208       softScroll(scrollDist, scrollDist);
209       break;
210     case SDL::SDLK_s:
211       if (!options->getBoolValue(OptionId::WASD_SCROLLING) || GuiFactory::isAlt(key))
212         break;
213       FALLTHROUGH;
214     case SDL::SDLK_DOWN:
215     case SDL::SDLK_KP_2:
216       softScroll(0, scrollDist);
217       break;
218     case SDL::SDLK_KP_1:
219       softScroll(-scrollDist, scrollDist);
220       break;
221     case SDL::SDLK_a:
222       if (!options->getBoolValue(OptionId::WASD_SCROLLING) || GuiFactory::isAlt(key))
223         break;
224       FALLTHROUGH;
225     case SDL::SDLK_LEFT:
226     case SDL::SDLK_KP_4:
227       softScroll(-scrollDist, 0);
228       break;
229     case SDL::SDLK_KP_7:
230       softScroll(-scrollDist, -scrollDist);
231       break;
232     default: break;
233   }
234   return false;
235 }
236 
onLeftClick(Vec2 v)237 bool MapGui::onLeftClick(Vec2 v) {
238   if (v.inRectangle(getBounds())) {
239     mouseHeldPos = v;
240     mouseOffset.x = mouseOffset.y = 0;
241     if (auto c = getCreature(v))
242       draggedCandidate = c;
243     return true;
244   }
245   return false;
246 }
247 
onRightClick(Vec2 pos)248 bool MapGui::onRightClick(Vec2 pos) {
249   if (lastRightClick && clock->getRealMillis() - *lastRightClick < milliseconds{200}) {
250     lockedView = true;
251     lastRightClick = none;
252     return true;
253   }
254   lastRightClick = clock->getRealMillis();
255   if (pos.inRectangle(getBounds())) {
256     lastMousePos = pos;
257     isScrollingNow = true;
258     mouseOffset.x = mouseOffset.y = 0;
259     lockedView = false;
260     return true;
261   }
262   return false;
263 }
264 
onMouseGone()265 void MapGui::onMouseGone() {
266   lastMouseMove = none;
267 }
268 
considerContinuousLeftClick(Vec2 mousePos)269 void MapGui::considerContinuousLeftClick(Vec2 mousePos) {
270   Vec2 pos = layout->projectOnMap(getBounds(), getScreenPos(), mousePos);
271   if (!lastMapLeftClick || lastMapLeftClick != pos) {
272     callbacks.continuousLeftClickFun(pos);
273     lastMapLeftClick = pos;
274   }
275 }
276 
onMouseMove(Vec2 v)277 bool MapGui::onMouseMove(Vec2 v) {
278   lastMouseMove = v;
279   auto draggedCreature = getDraggedCreature();
280   if (v.inRectangle(getBounds()) && mouseHeldPos && !draggedCreature)
281     considerContinuousLeftClick(v);
282   if (!draggedCreature && draggedCandidate && mouseHeldPos && mouseHeldPos->distD(v) > 30) {
283     callbacks.creatureDragFun(draggedCandidate->id, draggedCandidate->viewId, v);
284     setDraggedCreature(draggedCandidate->id, draggedCandidate->viewId, v);
285   }
286   if (isScrollingNow) {
287     mouseOffset.x = double(v.x - lastMousePos.x) / layout->getSquareSize().x;
288     mouseOffset.y = double(v.y - lastMousePos.y) / layout->getSquareSize().y;
289     callbacks.refreshFun();
290   }
291   return false;
292 }
293 
getCreature(Vec2 mousePos)294 optional<MapGui::CreatureInfo> MapGui::getCreature(Vec2 mousePos) {
295   auto info = getHighlightedInfo(layout->getSquareSize(), clock->getRealMillis());
296   if (info.creaturePos && info.object && info.object->getCreatureId())
297     return CreatureInfo {*info.object->getCreatureId(), info.object->id()};
298   else
299     return none;
300 }
301 
onMouseRelease(Vec2 v)302 void MapGui::onMouseRelease(Vec2 v) {
303   if (isScrollingNow) {
304     if (fabs(mouseOffset.x) + fabs(mouseOffset.y) < 1)
305       callbacks.rightClickFun(layout->projectOnMap(getBounds(), getScreenPos(), lastMousePos));
306     else {
307       center.x = min<double>(levelBounds.right(), max(0.0, center.x - mouseOffset.x));
308       center.y = min<double>(levelBounds.bottom(), max(0.0, center.y - mouseOffset.y));
309     }
310     isScrollingNow = false;
311     callbacks.refreshFun();
312     mouseOffset.x = mouseOffset.y = 0;
313   }
314   auto draggedCreature = getDraggedCreature();
315   if (auto& draggedElem = guiFactory->getDragContainer().getElement())
316     if (v.inRectangle(getBounds()) && guiFactory->getDragContainer().getOrigin().distD(v) > 10) {
317       switch (draggedElem->getId()) {
318         case DragContentId::CREATURE:
319           callbacks.creatureDroppedFun(draggedElem->get<UniqueEntity<Creature>::Id>(),
320               layout->projectOnMap(getBounds(), getScreenPos(), v));
321           break;
322         case DragContentId::TEAM:
323           callbacks.teamDroppedFun(draggedElem->get<TeamId>(),
324               layout->projectOnMap(getBounds(), getScreenPos(), v));
325           break;
326         default:
327           break;
328       }
329     }
330   if (mouseHeldPos) {
331     if (mouseHeldPos->distD(v) > 10) {
332       if (!draggedCreature)
333         considerContinuousLeftClick(v);
334     } else {
335       if (auto c = getCreature(*mouseHeldPos))
336         callbacks.creatureClickFun(c->id);
337       else {
338         callbacks.leftClickFun(layout->projectOnMap(getBounds(), getScreenPos(), v));
339         considerContinuousLeftClick(v);
340       }
341     }
342   }
343   mouseHeldPos = none;
344   lastMapLeftClick = none;
345   draggedCandidate = none;
346 }
347 
348 /*void MapGui::drawFloorBorders(Renderer& renderer, DirSet borders, int x, int y) {
349   for (const Dir& dir : borders) {
350     int coord;
351     switch (dir) {
352       case Dir::N: coord = 0; break;
353       case Dir::E: coord = 1; break;
354       case Dir::S: coord = 2; break;
355       case Dir::W: coord = 3; break;
356       default: continue;
357     }
358     renderer.drawTile(x, y, {Vec2(coord, 18), 1});
359   }
360 }*/
361 
getMoraleColor(double morale)362 static Color getMoraleColor(double morale) {
363   if (morale < 0)
364     return Color(255, 0, 0, -morale * 150);
365   else
366     return Color(0, 255, 0, morale * 150);
367 }
368 
getAttachmentOffset(Dir dir,Vec2 size)369 static Vec2 getAttachmentOffset(Dir dir, Vec2 size) {
370   switch (dir) {
371     case Dir::N: return Vec2(0, -size.y * 2 / 3);
372     case Dir::S: return Vec2(0, size.y / 4);
373     case Dir::E:
374     case Dir::W: return Vec2(dir) * size.x / 2;
375     default: FATAL << "Bad attachment dir " << int(dir);
376   }
377   return Vec2();
378 }
379 
getJumpOffset(const ViewObject & object,double state)380 static double getJumpOffset(const ViewObject& object, double state) {
381   if (object.hasModifier(ViewObjectModifier::NO_UP_MOVEMENT))
382     return 0;
383   if (state > 0.5)
384     state -= 0.5;
385   state *= 2;
386   const double maxH = 0.09;
387   return maxH * (1.0 - (2.0 * state - 1) * (2.0 * state - 1));
388 }
389 
getMovementOffset(const ViewObject & object,Vec2 size,double time,milliseconds curTimeReal,bool verticalMovement)390 Vec2 MapGui::getMovementOffset(const ViewObject& object, Vec2 size, double time, milliseconds curTimeReal,
391     bool verticalMovement) {
392   if (auto dir = object.getAttachmentDir())
393     return getAttachmentOffset(*dir, size);
394   if (!object.hasAnyMovementInfo())
395     return Vec2(0, 0);
396   double state;
397   Vec2 dir;
398   if (screenMovement &&
399       curTimeReal >= screenMovement->startTimeReal &&
400       curTimeReal <= screenMovement->endTimeReal) {
401     state = (double) (curTimeReal - screenMovement->startTimeReal).count() /
402           (double) (screenMovement->endTimeReal - screenMovement->startTimeReal).count();
403     dir = object.getMovementInfo(screenMovement->startTimeGame);
404   }
405   else if (!screenMovement) {
406     MovementInfo info = object.getLastMovementInfo();
407     dir = info.direction;
408 /*    if (info.direction.length8() == 0 || time >= info.tEnd + 0.001 || time <= info.tBegin - 0.001)
409       return Vec2(0, 0);*/
410     state = (time - info.tBegin) / (info.tEnd - info.tBegin);
411     double minStopTime = 0.2;
412     state = min(1.0, max(0.0, (state - minStopTime) / (1.0 - 2 * minStopTime)));
413   } else
414     return Vec2(0, 0);
415   double vertical = verticalMovement ? getJumpOffset(object, state) : 0;
416   if (object.getLastMovementInfo().type == MovementInfo::ATTACK)
417     if (dir.length8() == 1) {
418       if (verticalMovement)
419         return Vec2(0.8 * (state < 0.5 ? state : 1 - state) * dir.x * size.x,
420             (0.8 * (state < 0.5 ? state : 1 - state)* dir.y - vertical) * size.y);
421       else
422         return Vec2(0, 0);
423     }
424   return Vec2((state - 1) * dir.x * size.x, ((state - 1)* dir.y - vertical) * size.y);
425 }
426 
drawCreatureHighlights(Renderer & renderer,const ViewObject & object,Vec2 pos,Vec2 sz,milliseconds curTime)427 void MapGui::drawCreatureHighlights(Renderer& renderer, const ViewObject& object, Vec2 pos, Vec2 sz,
428     milliseconds curTime) {
429   auto getHighlight = [](Color id) { return Color(id).transparency(200); };
430   if (object.hasModifier(ViewObject::Modifier::HOSTILE) && highlightEnemies)
431     drawCreatureHighlight(renderer, pos, sz, getHighlight(Color::PURPLE));
432   if (object.hasModifier(ViewObject::Modifier::DRAW_MORALE) && highlightMorale)
433     if (auto morale = object.getAttribute(ViewObject::Attribute::MORALE))
434       drawCreatureHighlight(renderer, pos, sz, getMoraleColor(*morale));
435   if (object.hasModifier(ViewObject::Modifier::PLAYER)) {
436     if ((curTime.count() / 500) % 2 == 0)
437       drawCreatureHighlight(renderer, pos, sz, getHighlight(Color::YELLOW));
438   } else
439   if (object.hasModifier(ViewObject::Modifier::TEAM_HIGHLIGHT))
440     drawCreatureHighlight(renderer, pos, sz, getHighlight(Color::YELLOW));
441   if (auto id = object.getCreatureId())
442     if (isCreatureHighlighted(*id))
443       drawCreatureHighlight(renderer, pos, sz, getHighlight(Color::YELLOW));
444 }
445 
isCreatureHighlighted(UniqueEntity<Creature>::Id creature)446 bool MapGui::isCreatureHighlighted(UniqueEntity<Creature>::Id creature) {
447   return teamHighlight.getMaybe(creature).value_or(0) > 0;
448 }
449 
mirrorSprite(ViewId id)450 static bool mirrorSprite(ViewId id) {
451   switch (id) {
452     case ViewId::GRASS:
453     case ViewId::HILL:
454       return true;
455     default:
456       return false;
457   }
458 }
459 
drawHealthBar(Renderer & renderer,Vec2 pos,Vec2 size,double health)460 void MapGui::drawHealthBar(Renderer& renderer, Vec2 pos, Vec2 size, double health) {
461   if (hideFullHealthBars && health == 1)
462     return;
463   pos.y -= size.y * 0.2;
464   double barWidth = 0.12;
465   double barLength = 0.8;
466   auto getBar = [&](double state) {
467     return Rectangle((int) (pos.x + size.x * (1 - barLength) / 2), pos.y,
468         (int) (pos.x + size.x * state * (1 + barLength) / 2), (int) (pos.y + size.y * barWidth));
469   };
470   auto color = Color::f(min(1.0, 2 - health * 2), min(1.0, 2 * health), 0);
471   auto fullRect = getBar(1);
472   renderer.drawFilledRectangle(fullRect.minusMargin(-1), Color::TRANSPARENT, Color::BLACK.transparency(100));
473   renderer.drawFilledRectangle(fullRect, color.transparency(100));
474   if (health > 0)
475     renderer.drawFilledRectangle(getBar(health), color.transparency(200));
476   Rectangle shadowRect(fullRect.bottomLeft() - Vec2(0, 1), fullRect.bottomRight());
477   renderer.drawFilledRectangle(shadowRect, Color::BLACK.transparency(100));
478 }
479 
480 
481 
drawObjectAbs(Renderer & renderer,Vec2 pos,const ViewObject & object,Vec2 size,Vec2 movement,Vec2 tilePos,milliseconds curTimeReal)482 void MapGui::drawObjectAbs(Renderer& renderer, Vec2 pos, const ViewObject& object, Vec2 size, Vec2 movement,
483     Vec2 tilePos, milliseconds curTimeReal) {
484   auto id = object.id();
485   const Tile& tile = Tile::getTile(id, spriteMode);
486   Color color = colorWoundedRed ? Renderer::getBleedingColor(object) : Color::WHITE;
487   if (object.hasModifier(ViewObject::Modifier::INVISIBLE) || object.hasModifier(ViewObject::Modifier::HIDDEN))
488     color = color.transparency(70);
489   else
490     if (tile.translucent > 0)
491       color = color.transparency(255 * (1 - tile.translucent));
492     else if (object.hasModifier(ViewObject::Modifier::ILLUSION))
493       color = color.transparency(150);
494   if (object.hasModifier(ViewObject::Modifier::PLANNED))
495     color = color.transparency(100);
496   if (auto waterDepth = object.getAttribute(ViewObject::Attribute::WATER_DEPTH))
497     if (*waterDepth > 0) {
498       Uint8 val = max(0.0, 255.0 - min(2.0f, *waterDepth) * 60);
499       color = Color(val, val, val);
500     }
501   if (spriteMode && tile.hasSpriteCoord()) {
502     DirSet dirs;
503     if (tile.hasAnyConnections() || tile.hasAnyCorners())
504       dirs = getConnectionSet(tilePos, id);
505     Vec2 move;
506     drawCreatureHighlights(renderer, object, pos + movement, size, curTimeReal);
507     if (object.layer() == ViewLayer::CREATURE || tile.roundShadow) {
508       static auto coord = renderer.getTileCoord("round_shadow");
509       renderer.drawTile(pos + movement, coord, size, Color(255, 255, 255, 160));
510       move.y = -4* size.y / renderer.getNominalSize().y;
511     }
512     if (auto background = tile.getBackgroundCoord())
513       renderer.drawTile(pos, *background, size, color);
514     move += movement;
515     if (mirrorSprite(id))
516       renderer.drawTile(pos + move, tile.getSpriteCoord(dirs), size, color,
517           Renderer::SpriteOrientation((bool) (tilePos.getHash() % 2), (bool) (tilePos.getHash() % 4 > 1)));
518     else
519       renderer.drawTile(pos + move, tile.getSpriteCoord(dirs), size, color);
520     if (tile.hasAnyCorners()) {
521       for (auto coord : tile.getCornerCoords(dirs))
522         renderer.drawTile(pos + move, coord, size, color);
523     }
524 /*    if (tile.floorBorders) {
525       drawFloorBorders(renderer, borderDirs, x, y);
526     }*/
527     static auto shortShadow = renderer.getTileCoord("short_shadow");
528     if (object.layer() == ViewLayer::FLOOR_BACKGROUND && shadowed.count(tilePos))
529       renderer.drawTile(pos, shortShadow, size, Color(255, 255, 255, 170));
530     if (auto burningVal = object.getAttribute(ViewObject::Attribute::BURNING))
531       if (*burningVal > 0) {
532         static auto fire1 = renderer.getTileCoord("fire1");
533         static auto fire2 = renderer.getTileCoord("fire2");
534         renderer.drawTile(pos, (curTimeReal.count() + pos.getHash()) % 500 < 250 ? fire1 : fire2, size);
535       }
536     if (displayAllHealthBars || lastHighlighted.creaturePos == pos + movement)
537       if (auto wounded = object.getAttribute(ViewObject::Attribute::WOUNDED))
538         drawHealthBar(renderer, pos + move, size, 1 - *wounded);
539   } else {
540     Vec2 movement = getMovementOffset(object, size, currentTimeGame, curTimeReal, true);
541     Vec2 tilePos = pos + movement + Vec2(size.x / 2, -3);
542     drawCreatureHighlights(renderer, object, pos, size, curTimeReal);
543     renderer.drawText(tile.symFont ? Renderer::SYMBOL_FONT : Renderer::TILE_FONT, size.y, Tile::getColor(object),
544         tilePos.x, tilePos.y, tile.text, Renderer::HOR);
545     if (auto burningVal = object.getAttribute(ViewObject::Attribute::BURNING))
546       if (*burningVal > 0) {
547         renderer.drawText(Renderer::SYMBOL_FONT, size.y, getFireColor(), pos.x + size.x / 2, pos.y - 3, u8"ѡ",
548             Renderer::HOR);
549         if (*burningVal > 0.5)
550           renderer.drawText(Renderer::SYMBOL_FONT, size.y, getFireColor(), pos.x + size.x / 2, pos.y - 3, u8"Ѡ",
551               Renderer::HOR);
552       }
553   }
554 }
555 
resetScrolling()556 void MapGui::resetScrolling() {
557   lockedView = true;
558 }
559 
clearCenter()560 void MapGui::clearCenter() {
561   center = mouseOffset = {0.0, 0.0};
562   softCenter = none;
563   screenMovement = none;
564 }
565 
isCentered() const566 bool MapGui::isCentered() const {
567   return center.x != 0 || center.y != 0;
568 }
569 
setCenter(double x,double y)570 void MapGui::setCenter(double x, double y) {
571   center = {x, y};
572   center.x = max(0.0, min<double>(center.x, levelBounds.right()));
573   center.y = max(0.0, min<double>(center.y, levelBounds.bottom()));
574   softCenter = none;
575 }
576 
setCenter(Vec2 v)577 void MapGui::setCenter(Vec2 v) {
578   setCenter(v.x, v.y);
579 }
580 
drawFoWSprite(Renderer & renderer,Vec2 pos,Vec2 size,DirSet dirs)581 void MapGui::drawFoWSprite(Renderer& renderer, Vec2 pos, Vec2 size, DirSet dirs) {
582   const Tile& tile = Tile::getTile(ViewId::FOG_OF_WAR, true);
583   const Tile& tile2 = Tile::getTile(ViewId::FOG_OF_WAR_CORNER, true);
584   static DirSet fourDirs = DirSet({Dir::N, Dir::S, Dir::E, Dir::W});
585   auto coord = tile.getSpriteCoord(dirs & fourDirs);
586   renderer.drawTile(pos, coord, size);
587   for (Dir dir : dirs.intersection(fourDirs.complement())) {
588     static DirSet ne({Dir::N, Dir::E});
589     static DirSet se({Dir::S, Dir::E});
590     static DirSet nw({Dir::N, Dir::W});
591     static DirSet sw({Dir::S, Dir::W});
592     switch (dir) {
593       case Dir::NE: if (!dirs.contains(ne)) continue;
594         FALLTHROUGH;
595       case Dir::SE: if (!dirs.contains(se)) continue;
596         FALLTHROUGH;
597       case Dir::NW: if (!dirs.contains(nw)) continue;
598         FALLTHROUGH;
599       case Dir::SW: if (!dirs.contains(sw)) continue;
600         FALLTHROUGH;
601       default: break;
602     }
603     renderer.drawTile(pos, tile2.getSpriteCoord(DirSet::oneElement(dir)), size);
604   }
605 }
606 
isFoW(Vec2 pos) const607 bool MapGui::isFoW(Vec2 pos) const {
608   return !pos.inRectangle(Level::getMaxBounds()) || fogOfWar.getValue(pos);
609 }
610 
renderExtraBorders(Renderer & renderer,milliseconds currentTimeReal)611 void MapGui::renderExtraBorders(Renderer& renderer, milliseconds currentTimeReal) {
612   extraBorderPos.clear();
613   for (Vec2 wpos : layout->getAllTiles(getBounds(), levelBounds, getScreenPos()))
614     if (objects[wpos] && objects[wpos]->hasObject(ViewLayer::FLOOR_BACKGROUND)) {
615       ViewId viewId = objects[wpos]->getObject(ViewLayer::FLOOR_BACKGROUND).id();
616       if (Tile::getTile(viewId, true).hasExtraBorders())
617         for (Vec2 v : wpos.neighbors4())
618           if (v.inRectangle(extraBorderPos.getBounds())) {
619             if (extraBorderPos.isDirty(v))
620               extraBorderPos.getDirtyValue(v).push_back(viewId);
621             else
622               extraBorderPos.setValue(v, {viewId});
623           }
624     }
625   for (Vec2 wpos : layout->getAllTiles(getBounds(), levelBounds, getScreenPos()))
626     for (ViewId id : extraBorderPos.getValue(wpos)) {
627       const Tile& tile = Tile::getTile(id, true);
628       for (ViewId underId : tile.getExtraBorderIds())
629         if (connectionMap[wpos].contains(underId)) {
630           DirSet dirs = 0;
631           for (Vec2 v : Vec2::directions4())
632             if ((wpos + v).inRectangle(levelBounds) && connectionMap[wpos + v].contains(id))
633               dirs.insert(v.getCardinalDir());
634           if (auto coord = tile.getExtraBorderCoord(dirs)) {
635             Vec2 pos = projectOnScreen(wpos);
636             renderer.drawTile(pos, *coord, layout->getSquareSize());
637           }
638         }
639     }
640 }
641 
projectOnScreen(Vec2 wpos)642 Vec2 MapGui::projectOnScreen(Vec2 wpos) {
643   double x = wpos.x;
644   double y = wpos.y;
645   /*if (screenMovement) {
646     if (curTime >= screenMovement->startTimeReal && curTime <= screenMovement->endTimeReal) {
647       double state = (double)(curTime - screenMovement->startTimeReal).count() /
648           (double) (screenMovement->endTimeReal - screenMovement->startTimeReal).count();
649       x += (1 - state) * (screenMovement->to.x - screenMovement->from.x);
650       y += (1 - state) * (screenMovement->to.y - screenMovement->from.y);
651     }
652   }*/
653   return layout->projectOnScreen(getBounds(), getScreenPos(), x, y);
654 }
655 
getHighlightedFurniture()656 optional<ViewId> MapGui::getHighlightedFurniture() {
657   if (auto mousePos = getMousePos()) {
658     Vec2 curPos = layout->projectOnMap(getBounds(), getScreenPos(), *mousePos);
659     if (curPos.inRectangle(objects.getBounds()) &&
660         objects[curPos] &&
661         objects[curPos]->hasObject(ViewLayer::FLOOR) &&
662         (objects[curPos]->getHighlight(HighlightType::CLICKABLE_FURNITURE) > 0 ||
663          (objects[curPos]->getHighlight(HighlightType::CREATURE_DROP) > 0 && !!getDraggedCreature())))
664       return objects[curPos]->getObject(ViewLayer::FLOOR).id();
665   }
666   return none;
667 }
668 
isRenderedHighlight(const ViewIndex & index,HighlightType type)669 bool MapGui::isRenderedHighlight(const ViewIndex& index, HighlightType type) {
670   if (index.getHighlight(type) > 0)
671     switch (type) {
672       case HighlightType::CLICKABLE_FURNITURE:
673         return
674             index.hasObject(ViewLayer::FLOOR) &&
675             getHighlightedFurniture() == index.getObject(ViewLayer::FLOOR).id() &&
676             !getDraggedCreature() &&
677             !buttonViewId;
678       case HighlightType::CREATURE_DROP:
679         return !!getDraggedCreature();
680       default: return true;
681     }
682   else
683     return false;
684 }
685 
isRenderedHighlightLow(const ViewIndex & index,HighlightType type)686 bool MapGui::isRenderedHighlightLow(const ViewIndex& index, HighlightType type) {
687   switch (type) {
688     case HighlightType::PRIORITY_TASK:
689       return index.getHighlight(HighlightType::DIG) == 0;
690     case HighlightType::CLICKABLE_FURNITURE:
691     case HighlightType::CREATURE_DROP:
692     case HighlightType::FORBIDDEN_ZONE:
693     case HighlightType::FETCH_ITEMS:
694     case HighlightType::PERMANENT_FETCH_ITEMS:
695     case HighlightType::STORAGE_EQUIPMENT:
696     case HighlightType::STORAGE_RESOURCES:
697     case HighlightType::CLICKED_FURNITURE:
698     case HighlightType::CUT_TREE:
699       return true;
700     default: return false;
701   }
702 }
703 
renderTexturedHighlight(Renderer & renderer,Vec2 pos,Vec2 size,Color color)704 void MapGui::renderTexturedHighlight(Renderer& renderer, Vec2 pos, Vec2 size, Color color) {
705   if (spriteMode)
706     renderer.drawTile(pos, Tile::getTile(ViewId::DIG_MARK, true).getSpriteCoord(), size, color);
707   else
708     renderer.addQuad(Rectangle(pos, pos + size), color);
709 }
710 
renderHighlight(Renderer & renderer,Vec2 pos,Vec2 size,const ViewIndex & index,HighlightType highlight)711 void MapGui::renderHighlight(Renderer& renderer, Vec2 pos, Vec2 size, const ViewIndex& index, HighlightType highlight) {
712   auto color = getHighlightColor(index, highlight);
713   switch (highlight) {
714     case HighlightType::MEMORY:
715     case HighlightType::POISON_GAS:
716     case HighlightType::NIGHT:
717       renderer.addQuad(Rectangle(pos, pos + size), color);
718       break;
719 /*    case HighlightType::CUT_TREE:
720       if (spriteMode && index.hasObject(ViewLayer::FLOOR))
721         break;
722       FALLTHROUGH;*/
723     default:
724       renderTexturedHighlight(renderer, pos, size, color);
725       break;
726   }
727 }
728 
renderHighlights(Renderer & renderer,Vec2 size,milliseconds currentTimeReal,bool lowHighlights)729 void MapGui::renderHighlights(Renderer& renderer, Vec2 size, milliseconds currentTimeReal, bool lowHighlights) {
730   Rectangle allTiles = layout->getAllTiles(getBounds(), levelBounds, getScreenPos());
731   Vec2 topLeftCorner = projectOnScreen(allTiles.topLeft());
732   for (Vec2 wpos : allTiles)
733     if (auto& index = objects[wpos])
734       if (index->hasAnyHighlight()) {
735         Vec2 pos = topLeftCorner + (wpos - allTiles.topLeft()).mult(size);
736         for (HighlightType highlight : ENUM_ALL(HighlightType))
737           if (isRenderedHighlight(*index, highlight) && isRenderedHighlightLow(*index, highlight) == lowHighlights)
738             renderHighlight(renderer, pos, size, *index, highlight);
739       }
740   for (Vec2 wpos : lowHighlights ? tutorialHighlightLow : tutorialHighlightHigh) {
741     Vec2 pos = topLeftCorner + (wpos - allTiles.topLeft()).mult(size);
742     if ((currentTimeReal.count() / 1000) % 2 == 0)
743       renderTexturedHighlight(renderer, pos, size, Color(255, 255, 0, lowHighlights ? 120 : 40));
744   }
745 }
746 
renderAnimations(Renderer & renderer,milliseconds currentTimeReal)747 void MapGui::renderAnimations(Renderer& renderer, milliseconds currentTimeReal) {
748   animations = std::move(animations).filter([=](const AnimationInfo& elem)
749       { return !elem.animation->isDone(currentTimeReal);});
750   for (auto& elem : animations)
751     elem.animation->render(
752         renderer,
753         getBounds(),
754         projectOnScreen(elem.position),
755         currentTimeReal);
756 }
757 
getHighlightedInfo(Vec2 size,milliseconds currentTimeReal)758 MapGui::HighlightedInfo MapGui::getHighlightedInfo(Vec2 size, milliseconds currentTimeReal) {
759   HighlightedInfo ret {};
760   Rectangle allTiles = layout->getAllTiles(getBounds(), levelBounds, getScreenPos());
761   Vec2 topLeftCorner = projectOnScreen(allTiles.topLeft());
762   if (auto mousePos = getMousePos())
763     if (mouseUI) {
764       ret.tilePos = layout->projectOnMap(getBounds(), getScreenPos(), *mousePos);
765       if (!buttonViewId && ret.tilePos->inRectangle(objects.getBounds()))
766         for (Vec2 wpos : Rectangle(*ret.tilePos - Vec2(2, 2), *ret.tilePos + Vec2(2, 2))
767             .intersection(objects.getBounds())) {
768           Vec2 pos = topLeftCorner + (wpos - allTiles.topLeft()).mult(size);
769           if (objects[wpos] && objects[wpos]->hasObject(ViewLayer::CREATURE)) {
770             const ViewObject& object = objects[wpos]->getObject(ViewLayer::CREATURE);
771             Vec2 movement = getMovementOffset(object, size, currentTimeGame, currentTimeReal, true);
772             if (mousePos->inRectangle(Rectangle(pos + movement, pos + movement + size))) {
773               ret.tilePos = wpos;
774               ret.object = object;
775               ret.creaturePos = pos + movement;
776               break;
777             }
778           }
779         }
780     }
781   return ret;
782 }
783 
renderMapObjects(Renderer & renderer,Vec2 size,milliseconds currentTimeReal)784 void MapGui::renderMapObjects(Renderer& renderer, Vec2 size, milliseconds currentTimeReal) {
785   Rectangle allTiles = layout->getAllTiles(getBounds(), levelBounds, getScreenPos());
786   Vec2 topLeftCorner = projectOnScreen(allTiles.topLeft());
787   fogOfWar.clear();
788   for (ViewLayer layer : layout->getLayers()) {
789     for (Vec2 wpos : allTiles) {
790       Vec2 pos = topLeftCorner + (wpos - allTiles.topLeft()).mult(size);
791       if (!objects[wpos] || objects[wpos]->noObjects()) {
792         if (layer == layout->getLayers().back()) {
793           if (wpos.inRectangle(levelBounds))
794             renderer.addQuad(Rectangle(pos, pos + size), Color::BLACK);
795         }
796         fogOfWar.setValue(wpos, true);
797         continue;
798       }
799       const ViewIndex& index = *objects[wpos];
800       const ViewObject* object = nullptr;
801       if (spriteMode) {
802         if (index.hasObject(layer))
803           object = &index.getObject(layer);
804       } else
805         object = index.getTopObject(layout->getLayers());
806       if (object) {
807         Vec2 movement = getMovementOffset(*object, size, currentTimeGame, currentTimeReal, true);
808         drawObjectAbs(renderer, pos, *object, size, movement, wpos, currentTimeReal);
809         if (lastHighlighted.tilePos == wpos && !lastHighlighted.creaturePos && object->layer() != ViewLayer::CREATURE)
810           lastHighlighted.object = *object;
811       }
812       if (spriteMode && layer == layout->getLayers().back())
813         if (!isFoW(wpos))
814           drawFoWSprite(renderer, pos, size, DirSet(
815               !isFoW(wpos + Vec2(Dir::N)),
816               !isFoW(wpos + Vec2(Dir::S)),
817               !isFoW(wpos + Vec2(Dir::E)),
818               !isFoW(wpos + Vec2(Dir::W)),
819               isFoW(wpos + Vec2(Dir::NE)),
820               isFoW(wpos + Vec2(Dir::NW)),
821               isFoW(wpos + Vec2(Dir::SE)),
822               isFoW(wpos + Vec2(Dir::SW))));
823     }
824     if (layer == ViewLayer::FLOOR || !spriteMode) {
825       if (!buttonViewId && lastHighlighted.creaturePos)
826         drawCreatureHighlight(renderer, *lastHighlighted.creaturePos, size, Color::ALMOST_WHITE);
827       else if (lastHighlighted.tilePos && (!getHighlightedFurniture() || !!buttonViewId))
828         drawSquareHighlight(renderer, topLeftCorner + (*lastHighlighted.tilePos - allTiles.topLeft()).mult(size),
829             size);
830     }
831     if (layer == ViewLayer::FLOOR_BACKGROUND)
832       renderHighlights(renderer, size, currentTimeReal, true);
833     if (!spriteMode)
834       break;
835     if (layer == ViewLayer::FLOOR_BACKGROUND)
836       renderExtraBorders(renderer, currentTimeReal);
837   }
838   renderHighlights(renderer, size, currentTimeReal, false);
839 }
840 
drawCreatureHighlight(Renderer & renderer,Vec2 pos,Vec2 size,Color color)841 void MapGui::drawCreatureHighlight(Renderer& renderer, Vec2 pos, Vec2 size, Color color) {
842   if (spriteMode)
843     renderer.drawViewObject(pos + Vec2(0, size.y / 5), ViewId::CREATURE_HIGHLIGHT, true, size, color);
844   else
845     renderer.drawFilledRectangle(Rectangle(pos, pos + size), Color::TRANSPARENT, color);
846 }
847 
drawSquareHighlight(Renderer & renderer,Vec2 pos,Vec2 size)848 void MapGui::drawSquareHighlight(Renderer& renderer, Vec2 pos, Vec2 size) {
849   if (spriteMode)
850     renderer.drawViewObject(pos, ViewId::SQUARE_HIGHLIGHT, true, size, Color::ALMOST_WHITE);
851   else
852     renderer.drawFilledRectangle(Rectangle(pos, pos + size), Color::TRANSPARENT, Color::LIGHT_GRAY);
853 }
854 
considerRedrawingSquareHighlight(Renderer & renderer,milliseconds currentTimeReal,Vec2 pos,Vec2 size)855 void MapGui::considerRedrawingSquareHighlight(Renderer& renderer, milliseconds currentTimeReal, Vec2 pos, Vec2 size) {
856   Rectangle allTiles = layout->getAllTiles(getBounds(), levelBounds, getScreenPos());
857   Vec2 topLeftCorner = projectOnScreen(allTiles.topLeft());
858   for (Vec2 v : concat({pos}, pos.neighbors8()))
859     if (v.inRectangle(objects.getBounds()) && (!objects[v] || objects[v]->noObjects())) {
860       drawSquareHighlight(renderer, topLeftCorner + (pos - allTiles.topLeft()).mult(size), size);
861       break;
862     }
863 }
864 
processScrolling(milliseconds time)865 void MapGui::processScrolling(milliseconds time) {
866   if (!!softCenter && !!lastScrollUpdate) {
867     double offsetx = softCenter->x - center.x;
868     double offsety = softCenter->y - center.y;
869     double offset = sqrt(offsetx * offsetx + offsety * offsety);
870     if (offset < 0.1)
871       softCenter = none;
872     else {
873       double timeDiff = (time - *lastScrollUpdate).count();
874       double moveDist = min(offset, max(offset, 4.0) * 10 * timeDiff / 1000);
875       offsetx /= offset;
876       offsety /= offset;
877       center.x += offsetx * moveDist;
878       center.y += offsety * moveDist;
879     }
880     lastScrollUpdate = time;
881   }
882 }
883 
getDraggedCreature() const884 optional<UniqueEntity<Creature>::Id> MapGui::getDraggedCreature() const {
885   if (auto draggedContent = guiFactory->getDragContainer().getElement())
886     switch (draggedContent->getId()) {
887       case DragContentId::CREATURE:
888         return draggedContent->get<UniqueEntity<Creature>::Id>();
889       default:
890         break;
891     }
892   return none;
893 }
894 
setDraggedCreature(UniqueEntity<Creature>::Id id,ViewId viewId,Vec2 origin)895 void MapGui::setDraggedCreature(UniqueEntity<Creature>::Id id, ViewId viewId, Vec2 origin) {
896   guiFactory->getDragContainer().put({DragContentId::CREATURE, id}, guiFactory->viewObject(viewId), origin);
897 }
898 
considerScrollingToCreature()899 void MapGui::considerScrollingToCreature() {
900   if (auto& info = centeredCreaturePosition) {
901     Vec2 size = layout->getSquareSize();
902     Vec2 offset;
903     if (auto index = objects[info->pos])
904       if (index->hasObject(ViewLayer::CREATURE))
905         offset = getMovementOffset(index->getObject(ViewLayer::CREATURE), size, 0, clock->getRealMillis(), false);
906     double targetx = info->pos.x + (double)offset.x / size.x;
907     double targety = info->pos.y + (double)offset.y / size.y;
908     if (info->softScroll)
909       setSoftCenter(targetx, targety);
910     else
911       setCenter(targetx, targety);
912     // soft scrolling is done once when the creature is first controlled, so if we are centered then turn it off
913     if (fabs(center.x - targetx) + fabs(center.y - targety) < 0.01)
914       info->softScroll = false;
915   }
916 }
917 
render(Renderer & renderer)918 void MapGui::render(Renderer& renderer) {
919   considerScrollingToCreature();
920   Vec2 size = layout->getSquareSize();
921   auto currentTimeReal = clock->getRealMillis();
922   lastHighlighted = getHighlightedInfo(size, currentTimeReal);
923   renderMapObjects(renderer, size, currentTimeReal);
924   renderAnimations(renderer, currentTimeReal);
925   if (lastHighlighted.tilePos)
926     considerRedrawingSquareHighlight(renderer, currentTimeReal, *lastHighlighted.tilePos, size);
927   if (spriteMode && buttonViewId && renderer.getMousePos().inRectangle(getBounds()))
928     renderer.drawViewObject(renderer.getMousePos() + Vec2(15, 15), *buttonViewId, spriteMode, size);
929   processScrolling(currentTimeReal);
930 }
931 
updateObject(Vec2 pos,CreatureView * view,milliseconds currentTime)932 void MapGui::updateObject(Vec2 pos, CreatureView* view, milliseconds currentTime) {
933   WLevel level = view->getLevel();
934   objects[pos].emplace();
935   auto& index = *objects[pos];
936   view->getViewIndex(pos, index);
937   level->setNeedsRenderUpdate(pos, false);
938   if (index.hasObject(ViewLayer::FLOOR) || index.hasObject(ViewLayer::FLOOR_BACKGROUND))
939     index.setHighlight(HighlightType::NIGHT, 1.0 - view->getLevel()->getLight(pos));
940   lastSquareUpdate[pos] = currentTime;
941   connectionMap[pos].clear();
942   shadowed.erase(pos + Vec2(0, 1));
943   if (index.hasObject(ViewLayer::FLOOR)) {
944     auto& object = index.getObject(ViewLayer::FLOOR);
945     auto& tile = Tile::getTile(object.id());
946     if (tile.wallShadow) {
947       shadowed.insert(pos + Vec2(0, 1));
948     }
949     if (tile.hasAnyConnections() || tile.hasExtraBorders() || tile.hasAnyCorners())
950       connectionMap[pos].insert(getConnectionId(object.id()));
951   }
952   if (index.hasObject(ViewLayer::FLOOR_BACKGROUND)) {
953     auto& object = index.getObject(ViewLayer::FLOOR_BACKGROUND);
954     auto& tile = Tile::getTile(object.id());
955     if (tile.hasAnyConnections() || tile.hasExtraBorders() || tile.hasAnyCorners())
956       connectionMap[pos].insert(getConnectionId(object.id()));
957   }
958   if (auto viewId = index.getHiddenId()) {
959     auto& tile = Tile::getTile(*viewId);
960     if (tile.hasAnyConnections() || tile.hasExtraBorders() || tile.hasAnyCorners())
961       connectionMap[pos].insert(getConnectionId(*viewId));
962   }
963 }
964 
getDistanceToEdgeRatio(Vec2 pos)965 double MapGui::getDistanceToEdgeRatio(Vec2 pos) {
966   Vec2 v = projectOnScreen(pos);
967   double ret = 100000;
968   auto bounds = getBounds();
969   ret = min(ret, fabs((double) v.x - bounds.left()) / bounds.width());
970   ret = min(ret, fabs((double) v.x - bounds.right()) / bounds.width());
971   ret = min(ret, fabs((double) v.y - bounds.top()) / bounds.height());
972   ret = min(ret, fabs((double) v.y - bounds.bottom()) / bounds.height());
973   if (!v.inRectangle(bounds))
974     ret = -ret;
975   return ret;
976 }
977 
updateObjects(CreatureView * view,MapLayout * mapLayout,bool smoothMovement,bool ui,const optional<TutorialInfo> & tutorial)978 void MapGui::updateObjects(CreatureView* view, MapLayout* mapLayout, bool smoothMovement, bool ui,
979     const optional<TutorialInfo>& tutorial) {
980   if (tutorial) {
981     tutorialHighlightLow = tutorial->highlightedSquaresLow;
982     tutorialHighlightHigh = tutorial->highlightedSquaresHigh;
983   } else {
984     tutorialHighlightLow.clear();
985     tutorialHighlightHigh.clear();
986   }
987   WLevel level = view->getLevel();
988   levelBounds = view->getLevel()->getBounds();
989   mouseUI = ui;
990   layout = mapLayout;
991   auto currentTimeReal = clock->getRealMillis();
992   if (view != previousView || level != previousLevel)
993     for (Vec2 pos : level->getBounds())
994       level->setNeedsRenderUpdate(pos, true);
995   else
996     for (Vec2 pos : mapLayout->getAllTiles(getBounds(), Level::getMaxBounds(), getScreenPos()))
997       if (level->needsRenderUpdate(pos) || lastSquareUpdate[pos] < currentTimeReal - milliseconds{1000})
998         updateObject(pos, view, currentTimeReal);
999   previousView = view;
1000   if (previousLevel != level) {
1001     screenMovement = none;
1002     clearCenter();
1003     setCenter(view->getPosition());
1004     previousLevel = level;
1005     mouseOffset = {0, 0};
1006   }
1007   keyScrolling = view->getCenterType() == CreatureView::CenterType::NONE;
1008   bool newTurn = false;
1009   {
1010     double newCurrentTimeGame = smoothMovement ? view->getLocalTime() : 1000000000;
1011     if (currentTimeGame != newCurrentTimeGame) {
1012       lastEndTimeGame = currentTimeGame;
1013       currentTimeGame = newCurrentTimeGame;
1014       newTurn = true;
1015     }
1016   }
1017   if (smoothMovement && view->getCenterType() != CreatureView::CenterType::NONE) {
1018     if (!screenMovement || newTurn) {
1019       screenMovement = ScreenMovement {
1020         clock->getRealMillis(),
1021         clock->getRealMillis() + milliseconds{100},
1022         lastEndTimeGame
1023       };
1024     }
1025   } else
1026     screenMovement = none;
1027   if (view->getCenterType() == CreatureView::CenterType::FOLLOW) {
1028     if (centeredCreaturePosition) {
1029       centeredCreaturePosition->pos = view->getPosition();
1030       if (newTurn)
1031         centeredCreaturePosition->softScroll = false;
1032     } else
1033       centeredCreaturePosition = CenteredCreatureInfo { view->getPosition(), true };
1034   } else {
1035     centeredCreaturePosition = none;
1036     if (!isCentered() ||
1037         (view->getCenterType() == CreatureView::CenterType::STAY_ON_SCREEN && getDistanceToEdgeRatio(view->getPosition()) < 0.33)) {
1038       setSoftCenter(view->getPosition());
1039     }
1040   }
1041 }
1042 
1043