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