1--[[ Copyright (c) 2010 Manuel "Roujin" Wolf
2
3Permission is hereby granted, free of charge, to any person obtaining a copy of
4this software and associated documentation files (the "Software"), to deal in
5the Software without restriction, including without limitation the rights to
6use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7of the Software, and to permit persons to whom the Software is furnished to do
8so, subject to the following conditions:
9
10The above copyright notice and this permission notice shall be included in all
11copies or substantial portions of the Software.
12
13THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19SOFTWARE. --]]
20
21corsixth.require("ui")
22corsixth.require("announcer")
23
24--! Variant of UI for running games
25class "GameUI" (UI)
26
27---@type GameUI
28local GameUI = _G["GameUI"]
29
30local TH = require("TH")
31
32local Announcer = _G["Announcer"]
33
34-- The maximum distance to shake the screen from the origin during an
35-- earthquake with full intensity.
36local shake_screen_max_movement = 50 --pixels
37
38-- 0.002 is about 5 pixels on a 1920 pixel display
39local multigesture_pinch_sensitivity_factor = 0.002
40-- combined with the above, multiplying by 100 means minimum current_momentum.z for any detected pinch
41-- will result in a call to adjustZoom in the onTick method
42local multigesture_pinch_amplification_factor = 100
43
44--! Game UI constructor.
45--!param app (Application) Application object.
46--!param local_hospital Hospital to display
47--!param map_editor (bool) Whether the map is editable.
48function GameUI:GameUI(app, local_hospital, map_editor)
49  self:UI(app)
50  self.app = app
51
52  self.hospital = local_hospital
53  self.tutorial = { chapter = 0, phase = 0 }
54  if map_editor then
55    self.map_editor = UIMapEditor(self)
56    self:addWindow(self.map_editor)
57  else
58    self.adviser = UIAdviser(self)
59    self.bottom_panel = UIBottomPanel(self)
60    self.bottom_panel:addWindow(self.adviser)
61    self:addWindow(self.bottom_panel)
62  end
63
64  -- UI widgets
65  self.menu_bar = UIMenuBar(self, self.map_editor)
66  self:addWindow(self.menu_bar)
67
68  local scr_w = app.config.width
69  local scr_h = app.config.height
70  self.visible_diamond = self:makeVisibleDiamond(scr_w, scr_h)
71  if self.visible_diamond.w <= 0 or self.visible_diamond.h <= 0 then
72    -- For a standard 128x128 map, screen size would have to be in the
73    -- region of 3276x2457 in order to be too large.
74    if not self.map_editor then
75      error("Screen size too large for the map")
76    end
77  end
78  self.screen_offset_x, self.screen_offset_y = app.map:WorldToScreen(
79    app.map.th:getCameraTile(local_hospital:getPlayerIndex()))
80  self.zoom_factor = 1
81  self:scrollMap(-scr_w / 2, 16 - scr_h / 2)
82  self.limit_to_visible_diamond = not self.map_editor
83  self.transparent_walls = false
84  self.do_world_hit_test = true
85
86  self.momentum = app.config.scrolling_momentum
87  self.current_momentum = {x = 0.0, y = 0.0, z = 0.0}
88  self.multigesturemove = {x = 0.0, y = 0.0}
89
90  self.recallpositions = {}
91
92  self.speed_up_key_pressed = false
93
94  -- The currently specified intensity value for earthquakes. To abstract
95  -- the effect from the implementation this value is a number between 0
96  -- and 1.
97  self.shake_screen_intensity = 0
98
99  self.announcer = Announcer(app)
100end
101
102function GameUI:setupGlobalKeyHandlers()
103  UI.setupGlobalKeyHandlers(self)
104
105  -- Set the scrolling keys.
106  self.scroll_keys = {
107     [tostring(self.app.hotkeys["ingame_scroll_up"])] = {x = 0, y = -10},
108     [tostring(self.app.hotkeys["ingame_scroll_down"])] = {x = 0, y = 10},
109     [tostring(self.app.hotkeys["ingame_scroll_left"])] = {x = -10, y = 0},
110     [tostring(self.app.hotkeys["ingame_scroll_right"])] = {x = 10, y = 0},
111  }
112
113  -- This is the long version of the shift speed key.
114  -- i.e. if the "ingame_scroll_shift" key is "ctrl", then it will give us
115  --  "left ctrl" and "right ctrl" for reference against the rawchar in
116  --  "onKeyDown()" and "onKeyUp()"
117  self.shift_scroll_key_long = {}
118  self.shift_scroll_speed_pressed = false
119  local temp_table = {}
120  local shift_scroll_key_index = 1
121  if type(self.app.hotkeys["ingame_scroll_shift"]) == "string" then
122    temp_table = {self.app.hotkeys["ingame_scroll_shift"]}
123  elseif type(self.app.hotkeys["ingame_scroll_shift"]) == "table" then
124    temp_table = shallow_clone(self.app.hotkeys["ingame_scroll_shift"])
125  end
126  -- Go through the "ingame_scroll_shift" key table and see if it has any modifier names.
127  for _, v in pairs (temp_table) do
128    -- If it does then add long name version of them into the long key table.
129    if v == "ctrl" then
130      self.shift_scroll_key_long[shift_scroll_key_index] = "left ctrl"
131      shift_scroll_key_index = shift_scroll_key_index + 1
132      self.shift_scroll_key_long[shift_scroll_key_index] = "right ctrl"
133      shift_scroll_key_index = shift_scroll_key_index + 1
134    elseif v == "alt" then
135      self.shift_scroll_key_long[shift_scroll_key_index] = "left alt"
136      shift_scroll_key_index = shift_scroll_key_index + 1
137      self.shift_scroll_key_long[shift_scroll_key_index] = "right alt"
138      shift_scroll_key_index = shift_scroll_key_index + 1
139    elseif v == "shift" then
140      self.shift_scroll_key_long[shift_scroll_key_index] = "left shift"
141      shift_scroll_key_index = shift_scroll_key_index + 1
142      self.shift_scroll_key_long[shift_scroll_key_index] = "right shift"
143      shift_scroll_key_index = shift_scroll_key_index + 1
144    end
145  end
146
147  self:addKeyHandler("global_window_close", self, self.setEditRoom, false)
148  self:addKeyHandler("ingame_showmenubar", self, self.showMenuBar)
149  self:addKeyHandler("ingame_gamespeed_speedup", self, self.keySpeedUp)
150  self:addKeyHandler("ingame_setTransparent", self, self.keyTransparent)
151  self:addKeyHandler("ingame_toggleAdvisor", self, self.toggleAdviser)
152  self:addKeyHandler("ingame_poopLog", self.app.world, self.app.world.dumpGameLog)
153  self:addKeyHandler("ingame_poopStrings", self.app, self.app.dumpStrings)
154  self:addKeyHandler("ingame_toggleAnnouncements", self, self.togglePlayAnnouncements)
155  self:addKeyHandler("ingame_toggleSounds", self, self.togglePlaySounds)
156  self:addKeyHandler("ingame_toggleMusic", self, self.togglePlayMusic)
157
158  -- scroll to map position
159  for i = 0, 9 do
160    -- set camera view
161    self:addKeyHandler(string.format("ingame_storePosition_%d", i), self, self.setMapRecallPosition, i)
162    -- recall camera view
163    self:addKeyHandler(string.format("ingame_recallPosition_%d", i), self, self.recallMapPosition, i)
164  end
165
166  if self.app.config.debug then
167    self:addKeyHandler("ingame_showCheatWindow", self, self.showCheatsWindow)
168  end
169end
170
171function GameUI:makeVisibleDiamond(scr_w, scr_h)
172  local map_w = self.app.map.width
173  local map_h = self.app.map.height
174  assert(map_w == map_h, "UI limiter requires square map")
175
176  -- The visible diamond is the region which the top-left corner of the screen
177  -- is limited to, and ensures that the map always covers all of the screen.
178  -- Its vertices are at (x + w, y), (x - w, y), (x, y + h), (x, y - h).
179  return {
180    x = - scr_w / 2,
181    y = 16 * map_h - scr_h / 2,
182    w = 32 * map_h - scr_h - scr_w / 2,
183    h = 16 * map_h - scr_h / 2 - scr_w / 4,
184  }
185end
186
187--! Calculate the minimum valid zoom value
188--!
189--! Zooming out too much would cause negative width/height to be returned from
190--! makeVisibleDiamond. This function calculates the minimum zoom_factor that
191--! would be allowed.
192function GameUI:calculateMinimumZoom()
193  local scr_w = self.app.config.width
194  local scr_h = self.app.config.height
195  local map_h = self.app.map.height
196
197  -- Minimum width:  0 = 32 * map_h - (scr_h/factor) - (scr_w/factor) / 2,
198  -- Minimum height: 0 = 16 * map_h - (scr_h/factor) / 2 - (scr_w/factor) / 4
199  -- Both rearrange to:
200  local factor = (scr_w + 2 * scr_h) / (64 * map_h)
201
202  -- Due to precision issues a tolerance is needed otherwise setZoom might fail
203  factor = factor + 0.001
204
205  return factor
206end
207
208function GameUI:setZoom(factor)
209  if factor <= 0 then
210    return false
211  end
212  if not factor or math.abs(factor - 1) < 0.001 then
213    factor = 1
214  end
215
216  local scr_w = self.app.config.width
217  local scr_h = self.app.config.height
218  local new_diamond = self:makeVisibleDiamond(scr_w / factor, scr_h / factor)
219  if new_diamond.w < 0 or new_diamond.h < 0 then
220    return false
221  end
222
223  self.visible_diamond = new_diamond
224  local refx, refy = self.cursor_x or scr_w / 2, self.cursor_y or scr_h / 2
225  local cx, cy = self:ScreenToWorld(refx, refy)
226  self.zoom_factor = factor
227
228  cx, cy = self.app.map:WorldToScreen(cx, cy)
229  cx = cx - self.screen_offset_x - refx / factor
230  cy = cy - self.screen_offset_y - refy / factor
231  self:scrollMap(cx, cy)
232  return true
233end
234
235function GameUI:draw(canvas)
236  local app = self.app
237  local config = app.config
238  if self.map_editor or not self.in_visible_diamond then
239    canvas:fillBlack()
240  end
241  local zoom = self.zoom_factor
242  local dx = self.screen_offset_x +
243      math.floor((0.5 - math.random()) * self.shake_screen_intensity * shake_screen_max_movement * 2)
244  local dy = self.screen_offset_y +
245      math.floor((0.5 - math.random()) * self.shake_screen_intensity * shake_screen_max_movement * 2)
246  if canvas:scale(zoom) then
247    app.map:draw(canvas, dx, dy, math.floor(config.width / zoom), math.floor(config.height / zoom), 0, 0)
248    canvas:scale(1)
249  else
250    self:setZoom(1)
251    app.map:draw(canvas, dx, dy, config.width, config.height, 0, 0)
252  end
253  Window.draw(self, canvas, 0, 0) -- NB: not calling UI.draw on purpose
254  self:drawTooltip(canvas)
255  if self.simulated_cursor then
256    self.simulated_cursor.draw(canvas, self.cursor_x, self.cursor_y)
257  end
258end
259
260function GameUI:onChangeResolution()
261  -- Calculate and enforce minimum zoom
262  local minimum_zoom = self:calculateMinimumZoom()
263  if self.zoom_factor < minimum_zoom then
264    self:setZoom(minimum_zoom)
265  end
266  -- Recalculate scrolling bounds
267  local scr_w = self.app.config.width
268  local scr_h = self.app.config.height
269  self.visible_diamond = self:makeVisibleDiamond(scr_w / self.zoom_factor, scr_h / self.zoom_factor)
270  self:scrollMap(0, 0)
271
272  UI.onChangeResolution(self)
273end
274
275--! Update UI state after the UI has been depersisted
276--! When an UI object is depersisted, its state will reflect how the UI was at
277-- the moment of persistence, which may be different to the keyboard / mouse
278-- state at the moment of depersistence.
279--!param ui (UI) The previously existing UI object, from which values should be
280-- taken.
281function GameUI:resync(ui)
282  if self.drag_mouse_move then
283    -- Check that a window is actually being dragged. If none is found, then
284    -- remove the drag handler.
285    local something_being_dragged = false
286    for _, window in ipairs(self.windows) do
287      if window.dragging then
288        something_being_dragged = true
289        break
290      end
291    end
292    if not something_being_dragged then
293      self.drag_mouse_move = nil
294    end
295  end
296  self.tick_scroll_amount = ui.tick_scroll_amount
297  self.down_count = ui.down_count
298  if ui.limit_to_visible_diamond ~= nil then
299    self.limit_to_visible_diamond = ui.limit_to_visible_diamond
300  end
301
302  self.key_remaps = ui.key_remaps
303  self.key_to_button_remaps = ui.key_to_button_remaps
304end
305
306function GameUI:updateKeyScroll()
307  local dx, dy = 0, 0
308  for key, scr in pairs(self.scroll_keys) do
309    if self.buttons_down[key] then
310      dx = dx + scr.x
311      dy = dy + scr.y
312    end
313  end
314  --If there is any movement on the x or y axis...
315  if dx ~= 0 or dy ~= 0 then
316    --Get the length of the scrolling vector.
317    local mag = (dx^2 + dy^2) ^ 0.5
318    --Then normalize the scrolling vector, after which multiply it by the scroll speed variable used in self.scroll_keys, which is 10 as of 14/10/18.
319    dx = (dx / mag) * 10
320    dy = (dy / mag) * 10
321    -- Set the scroll amount to be used.
322    self.tick_scroll_amount = {x = dx, y = dy}
323    return true
324  else
325    self.tick_scroll_amount = false
326    return false
327  end
328end
329
330function GameUI:keySpeedUp()
331  self.speed_up_key_pressed = true
332  self.app.world:speedUp()
333end
334
335function GameUI:keyTransparent()
336  self:setWallsTransparent(true)
337end
338
339function GameUI:onKeyDown(rawchar, modifiers, is_repeat)
340  if UI.onKeyDown(self, rawchar, modifiers, is_repeat) then
341    -- Key has been handled already
342    return true
343  end
344  local key = rawchar:lower()
345  -- If key is shift speed key...
346  for _, v in pairs(self.shift_scroll_key_long) do
347    if v == key then
348      self.shift_scroll_speed_pressed = true
349    end
350  end
351  if self.scroll_keys[key] then
352    self:updateKeyScroll()
353    return
354  end
355end
356
357function GameUI:onKeyUp(rawchar)
358  if UI.onKeyUp(self, rawchar) then
359    return true
360  end
361
362  local key = rawchar:lower()
363  for _, v in pairs(self.shift_scroll_key_long) do
364    if v == key then
365      self.shift_scroll_speed_pressed = false
366    end
367  end
368  if self.scroll_keys[key] then
369    self:updateKeyScroll()
370    return
371  end
372
373  -- Guess that the "Speed Up" key was released because the
374  -- code parameter can't provide UTF-8 key codes:
375  self.speed_up_key_pressed = false
376  if self.app.world:isCurrentSpeed("Speed Up") then
377    self.app.world:previousSpeed()
378  end
379
380  self:setWallsTransparent(false)
381end
382
383function GameUI:makeDebugFax()
384  local message = {
385    {text = "debug fax"}, -- no translation needed imo
386    choices = {{text = "close debug fax", choice = "close"}},
387  }
388  -- Don't use "strike" type here, as these open a different window and must have an owner
389  local types = {"emergency", "epidemy", "personality", "information", "disease", "report"}
390  self.bottom_panel:queueMessage(types[math.random(1, #types)], message)
391end
392
393function GameUI:ScreenToWorld(x, y)
394  local zoom = self.zoom_factor
395  return self.app.map:ScreenToWorld(self.screen_offset_x + x / zoom, self.screen_offset_y + y / zoom)
396end
397
398function GameUI:WorldToScreen(x, y)
399  local zoom = self.zoom_factor
400  x, y = self.app.map:WorldToScreen(x, y)
401  x = x - self.screen_offset_x
402  y = y - self.screen_offset_y
403  return x * zoom, y * zoom
404end
405
406function GameUI:getScreenOffset()
407  return self.screen_offset_x, self.screen_offset_y
408end
409
410--! Change if the World should be tested for entities under the cursor
411--!param mode (boolean or room) true to enable hit test (normal), false
412--! to disable, room to enable only for non-door objects in given room
413function GameUI:setWorldHitTest(mode)
414  self.do_world_hit_test = mode
415end
416
417function GameUI:onCursorWorldPositionChange()
418  local zoom = self.zoom_factor
419  local x = math.floor(self.screen_offset_x + self.cursor_x / zoom)
420  local y = math.floor(self.screen_offset_y + self.cursor_y / zoom)
421  local entity = nil
422  local overwindow = self:hitTest(self.cursor_x, self.cursor_y)
423  if self.do_world_hit_test and not overwindow then
424    entity = self.app.map.th:hitTestObjects(x, y)
425    if self.do_world_hit_test ~= true then
426      -- limit to non-door objects in room
427      local room = self.do_world_hit_test
428      entity = entity and class.is(entity, Object) and
429          entity:getRoom() == room and entity ~= room.door and entity
430    end
431  end
432  if entity ~= self.cursor_entity then
433    -- Stop displaying hoverable moods for the old entity
434    if self.cursor_entity then
435      self.cursor_entity:setMood(nil)
436    end
437
438    -- Make the entity easily accessible when debugging, and ignore "deselecting" an entity.
439    if entity then
440      self.debug_cursor_entity = entity
441    end
442
443    local epidemic = self.hospital.epidemic
444    local infected_cursor = TheApp.gfx:loadMainCursor("epidemic")
445    local epidemic_cursor = TheApp.gfx:loadMainCursor("epidemic_hover")
446
447    self.cursor_entity = entity
448    if self.cursor ~= self.edit_room_cursor and self.cursor ~= self.waiting_cursor then
449      local cursor = self.default_cursor
450      if self.app.world.user_actions_allowed then
451        --- If the patient is infected show the infected cursor
452        if epidemic and epidemic.coverup_in_progress and
453          entity and entity.infected and not epidemic.timer.closed then
454          cursor = infected_cursor
455          -- In vaccination mode display epidemic hover cursor for all entities
456        elseif epidemic and epidemic.vaccination_mode_active then
457          cursor = epidemic_cursor
458          -- Otherwise just show the normal cursor and hover if appropriate
459        else
460          cursor = entity and entity.hover_cursor or
461          (self.down_count ~= 0 and self.down_cursor or self.default_cursor)
462        end
463      end
464      self:setCursor(cursor)
465    end
466    if self.bottom_panel then
467      self.bottom_panel:setDynamicInfo(nil)
468    end
469  end
470
471  -- Queueing icons over patients
472  local wx, wy = self:ScreenToWorld(self.cursor_x, self.cursor_y)
473  wx = math.floor(wx)
474  wy = math.floor(wy)
475  local room
476  if not overwindow and wx > 0 and wy > 0 and wx < self.app.map.width and wy < self.app.map.height then
477    room = self.app.world:getRoom(wx, wy)
478  end
479  if room ~= self.cursor_room then
480    -- Unset queue mood for patients queueing the old room
481    if self.cursor_room then
482      local queue = self.cursor_room.door.queue
483      if queue then
484        for _, humanoid in ipairs(queue) do
485          humanoid:setMood("queue", "deactivate")
486        end
487      end
488    end
489    -- Set queue mood for patients queueing the new room
490    if room then
491      local queue = room.door.queue
492      if queue then
493        for _, humanoid in ipairs(queue) do
494          humanoid:setMood("queue", "activate")
495        end
496      end
497    end
498    self.cursor_room = room
499  end
500
501  -- Any hoverable mood should be displayed on the new entity
502  if class.is(entity, Humanoid) then
503    for _, value in pairs(entity.active_moods) do
504      if value.on_hover then
505        entity:setMoodInfo(value)
506        break
507      end
508    end
509  end
510  -- Dynamic info
511  if entity and self.bottom_panel then
512    self.bottom_panel:setDynamicInfo(entity:getDynamicInfo())
513  end
514
515  return Window.onCursorWorldPositionChange(self, self.cursor_x, self.cursor_y)
516end
517
518local UpdateCursorPosition = TH.cursor.setPosition
519
520local highlight_x, highlight_y
521
522--! Called when the mouse enters or leaves the game window.
523function GameUI:onWindowActive(gain)
524  if gain == 0 then
525    self.tick_scroll_amount_mouse = false
526  end
527end
528
529-- TODO: try to remove duplication with UI:onMouseMove
530function GameUI:onMouseMove(x, y, dx, dy)
531  if self.mouse_released then
532    return false
533  end
534
535  local repaint = UpdateCursorPosition(self.app.video, x, y)
536  if self.app.moviePlayer.playing then
537    return false
538  end
539
540  self.cursor_x = x
541  self.cursor_y = y
542  if self:onCursorWorldPositionChange() or self.simulated_cursor then
543    repaint = true
544  end
545  if self.buttons_down.mouse_middle then
546    local zoom = self.zoom_factor
547    self.current_momentum.x = -dx/zoom
548    self.current_momentum.y = -dy/zoom
549    -- Stop zooming when the middle mouse button is pressed
550    self.current_momentum.z = 0
551    self:scrollMap(self.current_momentum.x, self.current_momentum.y)
552    repaint = true
553  end
554
555  if self.drag_mouse_move then
556    self.drag_mouse_move(x, y)
557    return true
558  end
559
560  local scroll_region_size
561  if self.app.config.fullscreen then
562    -- As the mouse is locked within the window, a 1px region feels a lot
563    -- larger than it actually is.
564    scroll_region_size = 1
565  else
566    -- In windowed mode, a reasonable size is needed, though not too large.
567    scroll_region_size = 8
568  end
569  if not self.app.config.prevent_edge_scrolling and
570      (x < scroll_region_size or y < scroll_region_size or
571       x >= self.app.config.width - scroll_region_size or
572       y >= self.app.config.height - scroll_region_size) then
573    local scroll_dx = 0
574    local scroll_dy = 0
575    local scroll_power = 7
576    if x < scroll_region_size then
577      scroll_dx = -scroll_power
578    elseif x >= self.app.config.width - scroll_region_size then
579      scroll_dx = scroll_power
580    end
581    if y < scroll_region_size then
582      scroll_dy = -scroll_power
583    elseif y >= self.app.config.height - scroll_region_size then
584      scroll_dy = scroll_power
585    end
586
587    if not self.tick_scroll_amount_mouse then
588      self.tick_scroll_amount_mouse = {x = scroll_dx, y = scroll_dy}
589    else
590      self.tick_scroll_amount_mouse.x = scroll_dx
591      self.tick_scroll_amount_mouse.y = scroll_dy
592    end
593  else
594    self.tick_scroll_amount_mouse = false
595  end
596
597  if Window.onMouseMove(self, x, y, dx, dy) then
598    repaint = true
599  end
600
601  self:updateTooltip()
602
603  local map = self.app.map
604  local wx, wy = self:ScreenToWorld(x, y)
605  wx = math.floor(wx)
606  wy = math.floor(wy)
607  if highlight_x then
608    --map.th:setCell(highlight_x, highlight_y, 4, 0)
609    highlight_x = nil
610  end
611  local map_width, map_height = map.th:size()
612  if 1 <= wx and wx <= map_width and 1 <= wy and wy <= map_height then
613    if map.th:getCellFlags(wx, wy).passable then
614      --map.th:setCell(wx, wy, 4, 24 + 8 * 256)
615      highlight_x = wx
616      highlight_y = wy
617    end
618  end
619
620  return repaint
621end
622
623function GameUI:onMouseUp(code, x, y)
624  if self.app.moviePlayer.playing then
625    return UI.onMouseUp(self, code, x, y)
626  end
627
628  local button = self.button_codes[code]
629  if button == "right" and not self.map_editor and highlight_x then
630    local window = self:getWindow(UIPatient)
631    local patient = (window and window.patient.is_debug and window.patient) or self.hospital:getDebugPatient()
632    if patient then
633      patient:walkTo(highlight_x, highlight_y)
634      patient:queueAction(IdleAction())
635    end
636  end
637
638  if self.edit_room then
639    if class.is(self.edit_room, Room) then
640      if button == "right" and self.cursor == self.waiting_cursor then
641        -- Still waiting for people to leave the room, abort editing it.
642        self:setEditRoom(false)
643      end
644    else -- No room chosen yet, but about to edit one.
645      if button == "left" then -- Take the clicked one.
646        local room = self.app.world:getRoom(self:ScreenToWorld(x, y))
647        if room then
648          if not room.crashed then
649            self:setCursor(self.waiting_cursor)
650            self.edit_room = room
651            room:tryToEdit()
652          else
653            if self.app.config.remove_destroyed_rooms then
654              local room_cost = room:calculateRemovalCost()
655              self:setEditRoom(false)
656              -- show confirmation dialog for removing the room
657              self:addWindow(UIConfirmDialog(self, false, _S.confirmation.remove_destroyed_room:format(room_cost),
658              --[[persistable:remove_destroyed_room_confirm_dialog]]function()
659                local world = room.world
660                UIEditRoom:removeRoom(false, room, world)
661                world:resetSideObjects()
662                world.rooms[room.id] = nil
663                self.hospital:spendMoney(room_cost, _S.transactions.remove_room)
664                end
665              ))
666            end
667          end
668        end
669      else -- right click, we don't want to edit a room after all.
670        self:setEditRoom(false)
671      end
672    end
673  end
674
675  -- During vaccination mode you can only interact with infected patients
676  local epidemic = self.hospital.epidemic
677  if epidemic and epidemic.vaccination_mode_active then
678    if button == "left" then
679      if self.cursor_entity then
680        -- Allow click behaviour for infected patients
681        if self.cursor_entity.infected then
682          self.cursor_entity:onClick(self,button)
683        end
684      end
685    elseif button == "right" then
686      --Right click turns vaccination mode off
687      local watch = TheApp.ui:getWindow(UIWatch)
688      watch:toggleVaccinationMode()
689    end
690  end
691
692  return UI.onMouseUp(self, code, x, y)
693end
694
695--! Process SDL_MULTIGESTURE events for zoom and map move functionality
696--!param numfingers (integer) number of touch points, received from the SDL event
697--!  This is still more info about param x.
698--!param dTheta (float) rotation in radians of the gesture from the SDL event
699--!param dDist (float) magnitude of pinch from the SDL event
700--!param x (float) normalised x value of the gesture
701--!param y (float) normalised y value of the gesture
702--!return (boolean) event processed indicator
703function GameUI:onMultiGesture(numfingers, dTheta, dDist, x, y)
704  -- only deal with 2 finger events for now
705  if numfingers == 2 then
706    -- calculate magnitude of pinch
707    local mag = math.abs(dDist)
708    if mag > multigesture_pinch_sensitivity_factor then
709      -- pinch action - constant needs to be tweaked
710      self.current_momentum.z = self.current_momentum.z + dDist * multigesture_pinch_amplification_factor
711      return true
712    else
713      -- scroll map
714      local normx = self.app.config.width * x
715      local normy = self.app.config.height * y
716
717      if self.multigesturemove.x == 0.0 then
718        self.multigesturemove.x = normx
719        self.multigesturemove.y = normy
720      else
721        local dx = normx - self.multigesturemove.x
722        local dy = normy - self.multigesturemove.y
723        self.current_momentum.x = self.current_momentum.x - dx
724        self.current_momentum.y = self.current_momentum.y - dy
725        self.multigesturemove.x = normx
726        self.multigesturemove.y = normy
727      end
728      return true
729    end
730  end
731  return false
732end
733
734function GameUI:onMouseWheel(x, y)
735  local inside_window = false
736  if self.windows then
737    for _, window in ipairs(self.windows) do
738      if window:hitTest(self.cursor_x - window.x, self.cursor_y - window.y) then
739        inside_window = true
740        break
741      end
742    end
743  end
744  if not inside_window then
745    -- Apply momentum to the zoom
746    if math.abs(self.current_momentum.z) < 12 then
747      self.current_momentum.z = self.current_momentum.z + y
748    end
749  end
750  return UI.onMouseWheel(self, x, y)
751end
752
753function GameUI:playAnnouncement(name, priority, played_callback, played_callback_delay)
754  self.announcer:playAnnouncement(name, priority, played_callback, played_callback_delay)
755end
756
757function GameUI:onTick()
758  local repaint = UI.onTick(self)
759  if not self.buttons_down.mouse_middle then
760    if math.abs(self.current_momentum.x) < 0.2 and math.abs(self.current_momentum.y) < 0.2 then
761      -- Stop scrolling
762      self.current_momentum.x = 0.0
763      self.current_momentum.y = 0.0
764    else
765      self.current_momentum.x = self.current_momentum.x * self.momentum
766      self.current_momentum.y = self.current_momentum.y * self.momentum
767      self:scrollMap(self.current_momentum.x, self.current_momentum.y)
768    end
769    if math.abs(self.current_momentum.z) < 0.2 then
770      self.current_momentum.z = 0.0
771    else
772      self.current_momentum.z = self.current_momentum.z * self.momentum
773      self.app.world:adjustZoom(self.current_momentum.z)
774    end
775    self.multigesturemove.x = 0.0
776    self.multigesturemove.y = 0.0
777  end
778  if self.tick_scroll_amount or self.tick_scroll_amount_mouse then
779    -- The scroll amount per tick gradually increases as the duration of the
780    -- scroll increases due to this multiplier.
781    local mult = self.tick_scroll_mult
782    mult = mult + 0.02
783    if mult > 2 then
784      mult = 2
785    end
786    self.tick_scroll_mult = mult
787
788    -- Combine the mouse scroll and keyboard scroll
789    local dx, dy = 0, 0
790    if self.tick_scroll_amount_mouse then
791      dx, dy = self.tick_scroll_amount_mouse.x, self.tick_scroll_amount_mouse.y
792      -- If the middle mouse button is down, then the world is being dragged,
793      -- and so the scroll direction due to the cursor being at the map edge
794      -- should be opposite to normal to make it feel more natural.
795      if self.buttons_down.mouse_middle then
796        dx, dy = -dx, -dy
797      end
798    end
799    if self.tick_scroll_amount then
800      dx = dx + self.tick_scroll_amount.x
801      dy = dy + self.tick_scroll_amount.y
802    end
803
804    -- Adjust scroll speed based on config value:
805    -- there is a separate config value for whether or not shift is held.
806    -- the speed is multiplied by 0.5 for consistency between the old and
807    -- new configuration. In the past scroll_speed applied only to shift
808    -- and defaulted to 2, where 1 was regular scroll speed. By
809    -- By multiplying by 0.5, we allow for setting slower than normal
810    -- scroll speeds, and ensure there is no behaviour change for players
811    -- who do not modify their config file.
812    if self.shift_scroll_speed_pressed then
813      mult = mult * self.app.config.shift_scroll_speed * 0.5
814    else
815      mult = mult * self.app.config.scroll_speed * 0.5
816    end
817
818    self:scrollMap(dx * mult, dy * mult)
819    repaint = true
820  else
821    self.tick_scroll_mult = 1
822  end
823  if self:onCursorWorldPositionChange() then
824    repaint = true
825  end
826
827  self.announcer:onTick()
828
829  return repaint
830end
831
832local abs, sqrt_5, floor = math.abs, math.sqrt(1 / 5), math.floor
833
834function GameUI:scrollMapTo(x, y)
835  local zoom = 2 * self.zoom_factor
836  local config = self.app.config
837  return self:scrollMap(x - self.screen_offset_x - config.width / zoom,
838                        y - self.screen_offset_y - config.height / zoom)
839end
840
841function GameUI.limitPointToDiamond(dx, dy, visible_diamond, do_limit)
842  -- If point outside visible diamond, then move point to the nearest position
843  -- on the edge of the diamond (NB: relies on diamond.w == 2 * diamond.h).
844  local rx = dx - visible_diamond.x
845  local ry = dy - visible_diamond.y
846  if abs(rx) + abs(ry) * 2 > visible_diamond.w then
847    if do_limit then
848      -- Determine the quadrant which the point lies in and accordingly set:
849      --  (vx, vy) : a unit vector perpendicular to the diamond edge in the quadrant
850      --  (p1x, p1y), (p2x, p2y) : the two diamond vertices in the quadrant
851      --  d : distance from the point to the line defined by the diamond edge (not the line segment itself)
852      local vx, vy, d
853      local p1x, p1y, p2x, p2y = 0, 0, 0, 0
854      if rx >= 0 and ry >= 0 then
855        p1x, p2y =  visible_diamond.w,  visible_diamond.h
856        vx, vy = sqrt_5, 2 * sqrt_5
857        d = (rx * vx + ry * vy) - (p1x * vx)
858      elseif rx >= 0 and ry < 0 then
859        p2x, p1y =  visible_diamond.w, -visible_diamond.h
860        vx, vy = sqrt_5, -2 * sqrt_5
861        d = (rx * vx + ry * vy) - (p2x * vx)
862      elseif rx < 0 and ry >= 0 then
863        p2x, p1y = -visible_diamond.w,  visible_diamond.h
864        vx, vy = -sqrt_5, 2 * sqrt_5
865        d = (rx * vx + ry * vy) - (p2x * vx)
866      else--if rx < 0 and ry < 0 then
867        p1x, p2y = -visible_diamond.w, -visible_diamond.h
868        vx, vy = -sqrt_5, -2 * sqrt_5
869        d = (rx * vx + ry * vy) - (p1x * vx)
870      end
871      -- In the unit vector parallel to the diamond edge, resolve the two vertices and
872      -- the point, and either move the point to the edge or to one of the two vertices.
873      -- NB: vx, vy, p1x, p1y, p2x, p2y are set such that p1 < p2.
874      local p1 = vx * p1y - vy * p1x
875      local p2 = vx * p2y - vy * p2x
876      local pd = vx * ry - vy * rx
877      if pd < p1 then
878        dx, dy = p1x + visible_diamond.x, p1y + visible_diamond.y
879      elseif pd > p2 then
880        dx, dy = p2x + visible_diamond.x, p2y + visible_diamond.y
881      else--if p1 <= pd and pd <= p2 then
882        dx, dy = dx - d * vx, dy - d * vy
883      end
884      return math.floor(dx), math.floor(dy), true
885    else
886      return dx, dy, false
887    end
888  end
889  return dx, dy, true
890end
891
892function GameUI:scrollMap(dx, dy)
893  dx = dx + self.screen_offset_x
894  dy = dy + self.screen_offset_y
895
896  dx, dy, self.in_visible_diamond = self.limitPointToDiamond(dx, dy,
897    self.visible_diamond, self.limit_to_visible_diamond)
898
899  self.screen_offset_x = floor(dx + 0.5)
900  self.screen_offset_y = floor(dy + 0.5)
901end
902
903--! Start shaking the screen, e.g. an earthquake effect
904--!param intensity (number) The magnitude of the effect, between 0 for no
905-- movement to 1 for significant shaking.
906function GameUI:beginShakeScreen(intensity)
907  self.shake_screen_intensity = intensity
908end
909
910--! Stop the screen from shaking after beginShakeScreen is called.
911function GameUI:endShakeScreen()
912  self.shake_screen_intensity = 0
913end
914
915function GameUI:limitCamera(mode)
916  self.limit_to_visible_diamond = mode
917  self:scrollMap(0, 0)
918end
919
920--! Applies the current setting for wall transparency to the map
921function GameUI:applyTransparency()
922  self.app.map.th:setWallDrawFlags(self.transparent_walls and 4 or 0)
923end
924
925--! Sets wall transparency to the specified parameter
926--!param mode (boolean) whether to enable or disable wall transparency
927function GameUI:setWallsTransparent(mode)
928  if mode ~= self.transparent_walls then
929    self.transparent_walls = mode
930    self:applyTransparency()
931  end
932end
933
934function UI:toggleAdviser()
935  self.app.config.adviser_disabled = not self.app.config.adviser_disabled
936  self.app:saveConfig()
937end
938
939function UI:togglePlaySounds()
940  self.app.config.play_sounds = not self.app.config.play_sounds
941  self.app.audio:playSoundEffects(self.app.config.play_sounds)
942  self.app:saveConfig()
943end
944
945function UI:togglePlayAnnouncements()
946  self.app.config.play_announcements = not self.app.config.play_announcements
947  self.app:saveConfig()
948end
949
950function UI:togglePlayMusic(item)
951  if not self.app.audio.background_music then
952    self.app.config.play_music = true
953    self.app.audio:playRandomBackgroundTrack() -- play
954  else
955    self.app.config.play_music = false
956    self.app.audio:stopBackgroundTrack() -- stop
957  end
958 -- self.app.config.play_music = not self.app.config.play_music
959  self.app:saveConfig()
960end
961
962local tutorial_phases
963local function make_tutorial_phases()
964tutorial_phases = {
965  {
966    -- 1) build reception
967    { text = _A.tutorial.build_reception,              -- 1
968      begin_callback = function() TheApp.ui:getWindow(UIBottomPanel):startButtonBlinking(3) end,
969      end_callback = function() TheApp.ui:getWindow(UIBottomPanel):stopButtonBlinking() end, },
970    { text = _A.tutorial.order_one_reception,          -- 2
971      begin_callback = function() TheApp.ui:getWindow(UIFurnishCorridor):startButtonBlinking(3) end,
972      end_callback = function() TheApp.ui:getWindow(UIFurnishCorridor):stopButtonBlinking(3) end, },
973    { text = _A.tutorial.accept_purchase,              -- 3
974      begin_callback = function() TheApp.ui:getWindow(UIFurnishCorridor):startButtonBlinking(2) end,
975      end_callback = function() TheApp.ui:getWindow(UIFurnishCorridor):stopButtonBlinking(2) end, },
976    _A.tutorial.rotate_and_place_reception,            -- 4
977    _A.tutorial.reception_invalid_position,            -- 5
978                                                       -- 6: object other than reception selected. currently no text for this phase.
979  },
980
981  {
982    -- 2) hire receptionist
983    { text = _A.tutorial.hire_receptionist,            -- 1
984      begin_callback = function() TheApp.ui:getWindow(UIBottomPanel):startButtonBlinking(5) end,
985      end_callback = function() TheApp.ui:getWindow(UIBottomPanel):stopButtonBlinking() end, },
986    { text = _A.tutorial.select_receptionists,         -- 2
987      begin_callback = function() TheApp.ui:getWindow(UIHireStaff):startButtonBlinking(4) end,
988      end_callback = function() TheApp.ui:getWindow(UIHireStaff):stopButtonBlinking() end, },
989    { text = _A.tutorial.next_receptionist,            -- 3
990      begin_callback = function() TheApp.ui:getWindow(UIHireStaff):startButtonBlinking(8) end,
991      end_callback = function() TheApp.ui:getWindow(UIHireStaff):stopButtonBlinking() end, },
992    { text = _A.tutorial.prev_receptionist,            -- 4
993      begin_callback = function() TheApp.ui:getWindow(UIHireStaff):startButtonBlinking(5) end,
994      end_callback = function() TheApp.ui:getWindow(UIHireStaff):stopButtonBlinking() end, },
995    { text = _A.tutorial.choose_receptionist,          -- 5
996      begin_callback = function() TheApp.ui:getWindow(UIHireStaff):startButtonBlinking(6) end,
997      end_callback = function() TheApp.ui:getWindow(UIHireStaff):stopButtonBlinking() end, },
998    _A.tutorial.place_receptionist,                    -- 6
999    _A.tutorial.receptionist_invalid_position,         -- 7
1000  },
1001
1002  {
1003    -- 3) build GP's office
1004    -- 3.1) room window
1005    { text = _A.tutorial.build_gps_office,             -- 1
1006      begin_callback = function() TheApp.ui:getWindow(UIBottomPanel):startButtonBlinking(2) end,
1007      end_callback = function() TheApp.ui:getWindow(UIBottomPanel):stopButtonBlinking() end, },
1008    { text = _A.tutorial.select_diagnosis_rooms,       -- 2
1009      begin_callback = function() TheApp.ui:getWindow(UIBuildRoom):startButtonBlinking(1) end,
1010      end_callback = function() TheApp.ui:getWindow(UIBuildRoom):stopButtonBlinking() end, },
1011    { text = _A.tutorial.click_gps_office,             -- 3
1012      begin_callback = function() TheApp.ui:getWindow(UIBuildRoom):startButtonBlinking(5) end,
1013      end_callback = function() TheApp.ui:getWindow(UIBuildRoom):stopButtonBlinking() end, },
1014
1015    -- 3.2) blueprint
1016    -- [11][58] was maybe planned to be used in this place, but is not needed.
1017    _A.tutorial.click_and_drag_to_build,               -- 4
1018    _A.tutorial.room_in_invalid_position,              -- 5
1019    _A.tutorial.room_too_small,                        -- 6
1020    _A.tutorial.room_too_small_and_invalid,            -- 7
1021    { text = _A.tutorial.room_big_enough,              -- 8
1022      begin_callback = function() TheApp.ui:getWindow(UIEditRoom):startButtonBlinking(4) end,
1023      end_callback = function() TheApp.ui:getWindow(UIEditRoom):stopButtonBlinking() end, },
1024
1025    -- 3.3) door and windows
1026    _A.tutorial.place_door,                            -- 9
1027    _A.tutorial.door_in_invalid_position,              -- 10
1028    { text = _A.tutorial.place_windows,                -- 11
1029      begin_callback = function() TheApp.ui:getWindow(UIEditRoom):startButtonBlinking(4) end,
1030      end_callback = function() TheApp.ui:getWindow(UIEditRoom):stopButtonBlinking() end, },
1031    { text = _A.tutorial.window_in_invalid_position,   -- 12
1032      begin_callback = function() TheApp.ui:getWindow(UIEditRoom):startButtonBlinking(4) end,
1033      end_callback = function() TheApp.ui:getWindow(UIEditRoom):stopButtonBlinking() end, },
1034
1035    -- 3.4) objects
1036    _A.tutorial.place_objects,                         -- 13
1037    _A.tutorial.object_in_invalid_position,            -- 14
1038    { text = _A.tutorial.confirm_room,                 -- 15
1039      begin_callback = function() TheApp.ui:getWindow(UIEditRoom):startButtonBlinking(4) end,
1040      end_callback = function() TheApp.ui:getWindow(UIEditRoom):stopButtonBlinking() end, },
1041    { text = _A.tutorial.information_window,           -- 16
1042      begin_callback = function() TheApp.ui:getWindow(UIInformation):startButtonBlinking(1) end,
1043      end_callback = function() TheApp.ui:getWindow(UIInformation):stopButtonBlinking() end, },
1044  },
1045
1046  {
1047    -- 4) hire doctor
1048    { text = _A.tutorial.hire_doctor,                  -- 1
1049      begin_callback = function() TheApp.ui:getWindow(UIBottomPanel):startButtonBlinking(5) end,
1050      end_callback = function() TheApp.ui:getWindow(UIBottomPanel):stopButtonBlinking() end, },
1051    { text = _A.tutorial.select_doctors,               -- 2
1052      begin_callback = function() TheApp.ui:getWindow(UIHireStaff):startButtonBlinking(1) end,
1053      end_callback = function() TheApp.ui:getWindow(UIHireStaff):stopButtonBlinking() end, },
1054    { text = _A.tutorial.choose_doctor,                -- 3
1055      begin_callback = function() TheApp.ui:getWindow(UIHireStaff):startButtonBlinking(6) end,
1056      end_callback = function() TheApp.ui:getWindow(UIHireStaff):stopButtonBlinking() end, },
1057    _A.tutorial.place_doctor,                          -- 4
1058    _A.tutorial.doctor_in_invalid_position,            -- 5
1059  },
1060  {
1061    -- 5) end of tutorial
1062    { begin_callback = function()
1063        -- The demo uses a single string for the post-tutorial info while
1064        -- the real game uses three.
1065        local texts = TheApp.using_demo_files and {
1066          {_S.introduction_texts["level15"]},
1067          {_S.introduction_texts["demo"]},
1068        } or {
1069          {_S.introduction_texts["level15"]},
1070          {_S.introduction_texts["level16"]},
1071          {_S.introduction_texts["level17"]},
1072          {_S.introduction_texts["level1"]},
1073        }
1074        TheApp.ui:addWindow(UIInformation(TheApp.ui, texts))
1075        TheApp.ui:addWindow(UIWatch(TheApp.ui, "initial_opening"))
1076      end,
1077    },
1078  },
1079}
1080end
1081tutorial_phases = setmetatable({}, {__index = function(t, k)
1082  make_tutorial_phases()
1083  return tutorial_phases[k]
1084end})
1085
1086-- Called to trigger step to another part of the tutorial.
1087-- chapter:    Individual parts of the tutorial. Step will only happen if it's the current chapter.
1088-- phase_from: Phase we need to be in for this step to happen. Multiple phases can be given here in an array.
1089-- phase_to:   Phase we want to step to or "next" to go to next chapter or "end" to end tutorial.
1090-- returns true if we changed phase, false if we didn't
1091function GameUI:tutorialStep(chapter, phase_from, phase_to, ...)
1092  if self.tutorial.chapter ~= chapter then
1093    return false
1094  end
1095  if type(phase_from) == "table" then
1096    local contains_current = false
1097    for _, phase in ipairs(phase_from) do
1098      if phase == self.tutorial.phase then
1099        contains_current = true
1100        break
1101      end
1102    end
1103    if not contains_current then return false end
1104  else
1105    if self.tutorial.phase ~= phase_from then return false end
1106  end
1107
1108  local old_phase = tutorial_phases[self.tutorial.chapter][self.tutorial.phase]
1109  if old_phase and old_phase.end_callback and type(old_phase.end_callback) == "function" then
1110    old_phase.end_callback(...)
1111  end
1112
1113  if phase_to == "end" then
1114    self.tutorial.chapter = 0
1115    self.tutorial.phase = 0
1116    return true
1117  elseif phase_to == "next" then
1118    self.tutorial.chapter = self.tutorial.chapter + 1
1119    self.tutorial.phase = 1
1120  else
1121    self.tutorial.phase = phase_to
1122  end
1123
1124  if TheApp.config.debug then print("Tutorial: Now in " .. self.tutorial.chapter .. ", " .. self.tutorial.phase) end
1125  local new_phase = tutorial_phases[self.tutorial.chapter][self.tutorial.phase]
1126  local str, callback
1127  if (type(new_phase) == "table" and type(new_phase.text) == "table") or not new_phase.text then
1128    str = new_phase.text
1129    callback = new_phase.begin_callback
1130  else
1131    str = new_phase
1132  end
1133  if str and str.text then
1134    self.adviser:say(str, true, true)
1135  else
1136    self.adviser.stay_up = nil
1137  end
1138  if callback then
1139    callback(...)
1140  end
1141  return true
1142end
1143
1144function GameUI:startTutorial(chapter)
1145  chapter = chapter or 1
1146  self.tutorial.chapter = chapter
1147  self.tutorial.phase = 0
1148
1149  self:tutorialStep(chapter, 0, 1)
1150end
1151
1152--! Converts centre of screen coordinates to world tile positions and stores the values for later recall
1153-- param index (integer) Position in recallpositions table
1154function GameUI:setMapRecallPosition(index)
1155  local cx, cy = self:ScreenToWorld(self.app.config.width / 2, self.app.config.height / 2)
1156  self.recallpositions[index] = {x = cx, y = cy, z = self.zoom_factor}
1157end
1158
1159--! Retrieves stored recall position and attempts to scroll to that position - will be limited to the bounds of the camera when zoomed out
1160-- param index (integer) Position in recallpositions table
1161function GameUI:recallMapPosition(index)
1162  if self.recallpositions[index] ~= nil then
1163    local sx, sy = self.app.map:WorldToScreen(self.recallpositions[index].x,  self.recallpositions[index].y)
1164    local dx, dy = self.app.map:ScreenToWorld(self.app.config.width / 2, self.app.config.height / 2)
1165    self:setZoom(self.recallpositions[index].z)
1166    self:scrollMapTo(sx + dx, sy + dy)
1167  end
1168end
1169
1170function GameUI:setEditRoom(enabled)
1171  -- TODO: Make the room the cursor is over flash
1172  if enabled then
1173    self:setCursor(self.edit_room_cursor)
1174    self.edit_room = true
1175  else
1176    -- If the actual editing hasn't started yet but is on its way,
1177    -- activate the room again.
1178    if class.is(self.edit_room, Room) and self.cursor == self.waiting_cursor then
1179      self.app.world:markRoomAsBuilt(self.edit_room)
1180    else
1181      -- If we are currently editing a room it may happen that we need to abort it.
1182      -- Also remove any dialog where the user is buying items.
1183      local item_window = self.app.ui:getWindow(UIFurnishCorridor)
1184      if item_window and item_window.edit_dialog then
1185        item_window:close()
1186      end
1187      local edit_window = self.app.ui:getWindow(UIEditRoom)
1188      if edit_window then
1189        edit_window:verifyOrAbortRoom()
1190      end
1191    end
1192    self:setCursor(self.default_cursor)
1193    self.edit_room = false
1194  end
1195end
1196
1197function GameUI:afterLoad(old, new)
1198  if old < 16 then
1199    self.zoom_factor = 1
1200  end
1201  if old < 23 then
1202    self.do_world_hit_test = not self:getWindow(UIPlaceObjects)
1203  end
1204  if old < 34 then
1205    self.adviser.queued_messages = {}
1206    self.adviser.phase = 0
1207    self.adviser.timer = nil
1208    self.adviser.frame = 1
1209    self.adviser.number_frames = 4
1210  end
1211  if old < 75 then
1212    self.current_momentum = { x = 0, y = 0 }
1213    self.momentum = self.app.config.scrolling_momentum
1214  end
1215  if old < 78 then
1216    self.current_momentum = { x = 0, y = 0, z = 0}
1217  end
1218  if old < 115 then
1219    self.shake_screen_intensity = 0
1220  end
1221  if old < 122 then
1222    self.multigesturemove = {x = 0.0, y = 0.0}
1223  end
1224  if old < 129 then
1225    self.recallpositions = {}
1226  end
1227  if old < 130 then
1228    self.ticks_since_last_announcement = nil -- cleanup
1229    self.announcer = Announcer(self.app)
1230  end
1231
1232  self.announcer.playing = false
1233
1234  return UI.afterLoad(self, old, new)
1235end
1236
1237function GameUI:showBriefing()
1238  local level = self.app.world.map.level_number
1239  local text = {_S.information.custom_game}
1240  if type(level) == "number" then
1241    text = {_S.introduction_texts[TheApp.using_demo_files and "demo" or "level" .. level]}
1242  elseif self.app.world.map.level_intro then
1243    text = {self.app.world.map.level_intro}
1244  end
1245  self:addWindow(UIInformation(self, text))
1246end
1247
1248--! Offers a confirmation window to quit the game and return to main menu
1249-- NB: overrides UI.quit, do NOT call it from here
1250function GameUI:quit()
1251  self:addWindow(UIConfirmDialog(self, false, _S.confirmation.quit, --[[persistable:gameui_confirm_quit]] function()
1252    self.app:loadMainMenu()
1253  end))
1254end
1255
1256function GameUI:showCheatsWindow()
1257  self:addWindow(UICheats(self))
1258end
1259
1260function GameUI:showMenuBar()
1261  self.menu_bar:appear()
1262end
1263