1--[[ Copyright (c) 2009 Peter "Corsix" Cawley 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 21local TH = require("TH") 22 23--! An `Entity` which occupies at least a single map tile and does not move. 24class "Object" (Entity) 25 26---@type Object 27local Object = _G["Object"] 28 29local orient_mirror = { 30 north = "west", 31 west = "north", 32 east = "south", 33 south = "east", 34} 35 36function Object:getDrawingLayer() 37 return 4 38end 39 40function Object:Object(hospital, object_type, x, y, direction, etc) 41 assert(class.is(hospital, Hospital), "First argument is not a Hospital instance.") 42 43 local th = TH.animation() 44 self:Entity(th) 45 46 if etc == "map object" then 47 if direction % 2 == 0 then 48 direction = "north" 49 else 50 direction = "west" 51 end 52 end 53 54 self.ticks = object_type.ticks 55 self.object_type = object_type 56 self.hospital = hospital 57 self.world = hospital.world 58 self.user = false 59 self.times_used = -1 -- Incremented in the call on the next line 60 self:updateDynamicInfo() 61 self:initOrientation(direction) 62 self:setTile(x, y) 63end 64 65--! Initializes the footprint, finds out what to draw and checks for 66-- split animations. 67--!param direction The orientation in which the object is facing. 68function Object:initOrientation(direction) 69 self.direction = direction 70 local object_type = self.object_type 71 local flags = self.init_anim_flags or 0 72 local anim = object_type.idle_animations[direction] 73 if not anim then 74 anim = object_type.idle_animations[orient_mirror[direction]] 75 flags = 1 76 end 77 local footprint = object_type.orientations 78 footprint = footprint and footprint[direction] 79 if footprint and footprint.early_list then 80 flags = flags + 1024 81 end 82 if footprint and footprint.list_bottom then 83 flags = flags + 2048 84 end 85 local rap = footprint and footprint.render_attach_position 86 if rap and rap[1] and type(rap[1]) == "table" then 87 self.split_anims = {self.th} 88 self.split_anim_positions = rap 89 self.th:setCrop(rap[1].column) 90 for i = 2, #rap do 91 local point = rap[i] 92 local th = TH.animation() 93 th:setCrop(point.column) 94 th:setHitTestResult(self) 95 th:setPosition(Map:WorldToScreen(1-point[1], 1-point[2])) 96 self.split_anims[i] = th 97 end 98 else 99 -- Make sure these variables aren't left behind. The object 100 -- might just have been moved and rotated. 101 self.split_anims = nil 102 self.split_anim_positions = nil 103 end 104 if footprint and footprint.animation_offset then 105 self:setPosition(unpack(footprint.animation_offset)) 106 end 107 footprint = footprint and footprint.footprint 108 if footprint then 109 self.footprint = footprint 110 else 111 self.footprint = nil 112 end 113 self:setAnimation(anim, flags) 114end 115 116--! Add methods to a class for creating and controlling a slave object 117function Object.slaveMixinClass(class_method_table) 118 local name = class.name(class_method_table) 119 local super = class.superclass(class_method_table) 120 local super_constructor = super[class.name(super)] 121 122 -- Constructor 123 class_method_table[name] = function(self, hospital, object_type, x, y, direction, ...) 124 super_constructor(self, hospital, object_type, x, y, direction, ...) 125 if object_type.slave_id then 126 local orientation = object_type.orientations 127 orientation = orientation and orientation[direction] 128 if orientation.slave_position then 129 x = x + orientation.slave_position[1] 130 y = y + orientation.slave_position[2] 131 end 132 self.slave = hospital.world:newObject(object_type.slave_id, x, y, direction, ...) 133 self.slave.master = self 134 end 135 end 136 137 -- Slave -> Master redirects 138 local function slave_to_master(method) 139 local super_method = super[method] 140 class_method_table[method] = function(self, ...) 141 local master = self.master 142 if master then 143 return master[method](master, ...) 144 else 145 return super_method(self, ...) 146 end 147 end 148 end 149 slave_to_master("onClick") 150 slave_to_master("updateDynamicInfo") 151 slave_to_master("getDynamicInfo") 152 153 -- Master -> Slave notifications 154 local function master_to_slave(method) 155 local super_method = super[method] 156 class_method_table[method] = function(self, ...) 157 local slave = self.slave 158 if slave then 159 slave[method](slave, ...) 160 end 161 return super_method(self, ...) 162 end 163 end 164 master_to_slave("initOrientation") 165 class_method_table.onDestroy = function(self, ...) 166 local slave = self.slave 167 if slave then 168 self.world:destroyEntity(slave) 169 end 170 return super.onDestroy(self, ...) 171 end 172 class_method_table.setTile = function(self, x, y) 173 if self.slave then 174 local dx, dy = 0, 0 175 local orientation = self.object_type.orientations 176 orientation = orientation and orientation[self.direction] 177 if orientation.slave_position then 178 dx = orientation.slave_position[1] 179 dy = orientation.slave_position[2] 180 end 181 if x then 182 self.slave:setTile(x + dx, y + dy) 183 else 184 self.slave:setTile(x, y) 185 end 186 end 187 return super.setTile(self, x, y) 188 end 189 190 return slave_to_master, master_to_slave 191end 192 193function Object:tick() 194 if self.split_anims then 195 if self.num_animation_ticks then 196 for i = 2, #self.split_anims do 197 local th = self.split_anims[i] 198 for _ = 1, self.num_animation_ticks do 199 th:tick() 200 end 201 end 202 else 203 for i = 2, #self.split_anims do 204 self.split_anims[i]:tick() 205 end 206 end 207 end 208 return Entity.tick(self) 209end 210 211function Object:setPosition(x, y) 212 if self.split_anims then 213 -- The given position is for the primary tile, so position non-primary 214 -- animations relative to the primary one. 215 local bx, by = unpack(self.split_anim_positions[1]) 216 for i, th in ipairs(self.split_anims) do 217 local point = self.split_anim_positions[i] 218 local dx, dy = Map:WorldToScreen(1-point[1]+bx, 1-point[2]+by) 219 th:setPosition(x + dx, y + dy) 220 end 221 else 222 self.th:setPosition(x, y) 223 end 224 return self 225end 226 227function Object:setAnimation(animation, flags) 228 if self.split_anims then 229 flags = (flags or 0) + DrawFlags.Crop 230 if self.permanent_flags then 231 flags = flags + self.permanent_flags 232 end 233 if animation ~= self.animation_idx or flags ~= self.animation_flags then 234 self.animation_idx = animation 235 self.animation_flags = flags 236 local anims = self.world.anims 237 for _, th in ipairs(self.split_anims) do 238 th:setAnimation(anims, animation, flags) 239 end 240 end 241 return self 242 else 243 return Entity.setAnimation(self, animation, flags) 244 end 245end 246 247--! Get the primary tile which the object is attached to for rendering 248--[[! For objects which attach to a single tile for rendering, this method will 249return the X and Y Lua world coordinates of that tile. For objects which split 250their rendering over multiple tiles, one of them is arbitrarily designated as 251the primary tile, and its coordinates are returned. 252]] 253function Object:getRenderAttachTile() 254 local x, y = self.tile_x, self.tile_y 255 local offset = self.object_type.orientations 256 if x and offset then 257 offset = offset[self.direction].render_attach_position 258 if self.split_anims then 259 offset = offset[1] 260 end 261 x = x + offset[1] 262 y = y + offset[2] 263 end 264 return x, y 265end 266 267function Object:updateDynamicInfo() 268 self.times_used = self.times_used + 1 269 local object = self.object_type 270 if object.dynamic_info then 271 self:setDynamicInfo("text", {object.name, "", _S.dynamic_info.object.times_used:format(self.times_used)}) 272 end 273end 274 275function Object:getSecondaryUsageTile() 276 local x, y = self.tile_x, self.tile_y 277 local offset = self.object_type.orientations 278 if offset then 279 offset = offset[self.direction].use_position_secondary 280 x = x + offset[1] 281 y = y + offset[2] 282 end 283 return x, y 284end 285 286-- This function returns a list of all "only_passable" tiles belonging to an object. 287-- It must be overridden by objects which do not have a footprint, but walkable tiles (e.g. doors of any kind) 288function Object:getWalkableTiles() 289 local tiles = {} 290 for _, xy in ipairs(self.footprint) do 291 if xy.only_passable then 292 tiles[#tiles + 1] = { self.tile_x + xy[1], self.tile_y + xy[2] } 293 end 294 end 295 return tiles 296end 297 298function Object:setTile(x, y) 299 local function coordinatesAreInFootprint(object_footprint, xpos, ypos) 300 for _, xy in ipairs(object_footprint) do 301 if xy[1] == xpos and xy[2] == ypos then 302 return true 303 end 304 end 305 return false 306 end 307 308 local function isEmpty(table) 309 return next(table) == nil 310 end 311 312 local function getComplementaryPassableFlag(passable_flag) 313 if passable_flag == "travelNorth" or passable_flag == "travelSouth" then 314 return passable_flag == "travelNorth" and "travelSouth" or "travelNorth" 315 else 316 return passable_flag == "travelEast" and "travelWest" or "travelEast" 317 end 318 end 319 320 local function setPassableFlags(passable_flag, xpos, ypos, next_x, next_y, value) 321 local flags1 = {} 322 flags1[passable_flag] = value 323 self.world.map.th:setCellFlags(xpos, ypos, flags1) 324 local flags2 = {} 325 flags2[getComplementaryPassableFlag(passable_flag)] = value 326 self.world.map.th:setCellFlags(next_x, next_y, flags2) 327 end 328 329 local direction_parameters = { 330 north = { x = 0, y = -1, buildable_flag = "buildableNorth", passable_flag = "travelNorth", needed_side = "need_north_side"}, 331 east = { x = 1, y = 0, buildable_flag = "buildableEast", passable_flag = "travelEast", needed_side = "need_east_side"}, 332 south = { x = 0, y = 1, buildable_flag = "buildableSouth", passable_flag = "travelSouth", needed_side = "need_south_side"}, 333 west = { x = -1, y = 0, buildable_flag = "buildableWest", passable_flag = "travelWest", needed_side = "need_west_side"} 334 } 335 336 local direction = self.direction 337 if self.object_type.thob == 50 and direction == "east" then 338 direction = "west" 339 end 340 341 if self.tile_x ~= nil then 342 self.world:removeObjectFromTile(self, self.tile_x, self.tile_y) 343 if self.footprint then 344 local map = self.world.map.th 345 for _, xy in ipairs(self.footprint) do 346 local xpos, ypos = self.tile_x + xy[1], self.tile_y + xy[2] 347 348 if xy.only_side then 349 if self.set_passable_flags then 350 self.set_passable_flags = nil 351 local par = direction_parameters[direction] 352 local passableFlag, next_tile_x, next_tile_y = par.passable_flag, xpos + par.x, ypos + par.y 353 setPassableFlags(passableFlag, xpos, ypos, next_tile_x, next_tile_y, true) 354 end 355 local flags_to_set= {} 356 flags_to_set[direction_parameters[direction].buildable_flag] = true 357 map:setCellFlags(xpos, ypos, flags_to_set) 358 else 359 local flags_to_set = {} 360 for _, value in pairs(direction_parameters) do 361 if coordinatesAreInFootprint(self.footprint, xy[1] + value.x, xy[2] + value.y) or 362 xy.complete_cell or xy[value.needed_side] then 363 flags_to_set[value.buildable_flag] = true 364 end 365 end 366 367 if not isEmpty(flags_to_set) then 368 map:setCellFlags(xpos, ypos, flags_to_set) 369 end 370 if not map:getCellFlags(xpos, ypos).passable then 371 map:setCellFlags(xpos, ypos, { 372 buildable = true, 373 passable = true, 374 }) 375 else 376 -- passable tiles can "belong" to multiple objects, so we have to check that 377 if not self.world:isTilePartOfNearbyObject(xpos, ypos, 10) then 378 -- assumption: no object defines a passable tile further than 10 tiles away from its origin 379 map:setCellFlags(xpos, ypos, { 380 buildable = true, 381 }) 382 end 383 end 384 end 385 end 386 end 387 end 388 389 local entity_map = self.world.entity_map 390 -- Remove the reference to the object at it's previous coordinates 391 if entity_map then 392 entity_map:removeEntity(self.tile_x,self.tile_y, self) 393 end 394 395 -- Update the objects coordinates 396 self.tile_x = x 397 self.tile_y = y 398 399 -- Update the entity map for the new position 400 if entity_map then 401 entity_map:addEntity(x,y,self) 402 end 403 404 if x then 405 self.th:setDrawingLayer(self:getDrawingLayer()) 406 self.th:setTile(self.world.map.th, self:getRenderAttachTile()) 407 self.world:addObjectToTile(self, x, y) 408 if self.footprint then 409 local map = self.world.map.th 410 local optional_found = false 411 local room = self.world:getRoom(x, y) 412 local roomId = room and room.id 413 local next_tile_x, next_tile_y = x,y 414 local passable_flag 415 416 for _, xy in ipairs(self.footprint) do 417 local change_flags = true 418 local flags_to_set = {} 419 local lx = x + xy[1] 420 local ly = y + xy[2] 421 local flags 422 423 if xy.optional then 424 if optional_found then 425 -- An optional tile has been accepted, we don't need anymore such tiles. 426 change_flags = false 427 else 428 flags = map:getCellFlags(lx, ly) 429 local is_object_allowed = true 430 if roomId and flags.roomId ~= roomId then 431 is_object_allowed = false 432 elseif xy.only_passable and not self.world.pathfinder:isReachableFromHospital(lx, ly) then 433 is_object_allowed = false 434 end 435 436 if is_object_allowed then 437 change_flags = true 438 optional_found = true 439 else 440 change_flags = false 441 end 442 end 443 end 444 445 flags = map:getCellFlags(lx, ly) 446 if xy.only_side then 447 local par = direction_parameters[direction] 448 flags_to_set[par["buildable_flag"]] = false 449 passable_flag, next_tile_x, next_tile_y = par["passable_flag"], x + par["x"], y + par["y"] 450 else 451 for _, value in pairs(direction_parameters) do 452 if coordinatesAreInFootprint(self.footprint, xy[1] + value["x"], xy[2] + value["y"]) or 453 xy.complete_cell or xy[value["needed_side"]] then 454 if map:getCellFlags(x, y, flags)[value["buildable_flag"]] == 0 then 455 change_flags = false 456 end 457 flags_to_set[value["buildable_flag"]] = false 458 end 459 end 460 end 461 462 if change_flags then 463 if not xy.only_side then 464 map:setCellFlags(lx, ly, { 465 buildable = false, 466 passable = not not xy.only_passable, 467 }) 468 end 469 if not isEmpty(flags_to_set) then 470 map:setCellFlags(lx, ly, flags_to_set) 471 end 472 if xy.only_side then 473 if map:getCellFlags(lx, ly)[passable_flag] == true then 474 self.set_passable_flags = true 475 setPassableFlags(passable_flag, lx, ly, next_tile_x, next_tile_y, false) 476 end 477 end 478 end 479 end 480 end 481 if self.split_anims then 482 local map = self.world.map.th 483 local pos = self.split_anim_positions 484 for i = 2, #self.split_anims do 485 self.split_anims[i]:setTile(map, x + pos[i][1], y + pos[i][2]) 486 end 487 end 488 else 489 self.th:setTile(nil) 490 if self.split_anims then 491 for i = 2, #self.split_anims do 492 self.split_anims[i]:setTile(nil) 493 end 494 end 495 end 496 self.world:clearCaches() 497 return self 498end 499 500-- Sets the user of this object to the given user. Note that if multiple_users_allowed 501-- is set to true for this object "user" will always be the last added user to this object. 502-- Please do not modify object.user directly. 503--!param user The user that is about to use this object. 504function Object:setUser(user) 505 assert(user, "setUser: Expected a user, got nil") -- It makes no sense to add a nil value 506 self.user = user 507 if self.object_type.multiple_users_allowed then 508 if not self.user_list then 509 self.user_list = {} 510 end 511 table.insert(self.user_list, user) 512 end 513 self.th:makeInvisible() 514 self:removeReservedUser(user) 515end 516 517-- Removes a user from the set of users if multiple_users_allowed is set to true for this 518-- object. Otherwise it sets "user" to nil. Please don't directly change the variable "user". 519function Object:removeUser(user) 520 if self.object_type.multiple_users_allowed then 521 if not user or not self.user_list then 522 -- No user specified, empty the list; or the list didn't exist 523 self.user_list = {} 524 end 525 local found = false 526 for i, users in ipairs(self.user_list) do 527 if users == user then 528 table.remove(self.user_list, i) 529 found = true 530 break 531 end 532 end 533 if not found then 534 self.world:gameLog("Warning: Could not find a humanoid to remove from the user list") 535 end 536 if #self.user_list == 0 then 537 -- No users left, make the object visible again. 538 self.user = nil 539 self.th:makeVisible() 540 end 541 else 542 self.user = nil 543 self.th:makeVisible() 544 end 545end 546 547-- Adds a reserved user for this object. If multiple_users_allowed is set there can be 548-- many users reserving the object. reserved_for will then be the user who most recently 549-- reserved this object. 550function Object:addReservedUser(user) 551 assert(user, "Expected a user, got nil") -- It makes no sense to add a nil value 552 if self.object_type.multiple_users_allowed then 553 if not self.reserved_for_list then 554 self.reserved_for_list = {} 555 end 556 table.insert(self.reserved_for_list, user) 557 else 558 assert(not self.reserved_for or user ~= self.reserved_for, "Object already reserved for another user") 559 end 560 self.reserved_for = user 561end 562 563-- If multiple_users_allowed is true: Removes the user specified from this object's list of reserved users. 564-- If the argument is nil it is assumed that the list should be emptied. 565-- Note that if there are many humanoids reserving this object reserved_for might still be set after a 566-- call to this function. 567-- Otherwise: sets reserved_for to nil. 568function Object:removeReservedUser(user) 569 if self.object_type.multiple_users_allowed then 570 -- No user specified, delete the whole list; or no list found, make it. 571 if not user or not self.reserved_for_list then 572 self.reserved_for_list = {} 573 end 574 local found = false 575 for i, users in ipairs(self.reserved_for_list) do 576 if users == user then 577 table.remove(self.reserved_for_list, i) 578 found = true 579 break 580 end 581 end 582 if not found then 583 self.world:gameLog("Warning: Could not find a humanoid to remove from the reserved user list") 584 end 585 if #self.reserved_for_list == 0 then 586 -- No users left, set reserved_for to nil. 587 self.reserved_for = nil 588 end 589 else 590 self.reserved_for = nil 591 end 592end 593 594-- Checks whether the object is reserved for the specified user. 595-- If the argument is nil a check for any reserved user is done. 596function Object:isReservedFor(user) 597 if not user then 598 if self.reserved_for_list then 599 return #self.reserved_for_list > 0 600 else 601 return not not self.reserved_for 602 end 603 end 604 if self.reserved_for == user then -- "Normal" use 605 return true 606 end 607 if self.object_type.multiple_users_allowed then 608 if not self.reserved_for_list then 609 self.reserved_for_list = {} 610 end 611 for _, users in ipairs(self.reserved_for_list) do 612 if users == user then 613 return true 614 end 615 end 616 end 617 return false 618end 619 620--[[ Called when the object is clicked 621!param ui (UI) The active ui. 622!param button (string) Which button was clicked. 623!param data (table) If some data should be retained after moving an object it is in this table. 624]] 625function Object:onClick(ui, button, data) 626 local window = ui:getWindow(UIEditRoom) 627 if button == "right" or (button == "left" and window and window.in_pickup_mode) then 628 -- This flag can be used if for example some things should only happen as long as the 629 -- object is not picked up. How lovely when it is so logical. :-) 630 local object_list = {{object = self.object_type, qty = 1, existing_object = self}} 631 local room = self:getRoom() 632 window = window and window.visible and window 633 local direction = self.direction 634 if (not room and window) or 635 (room and not (window and window.room == room) and not self.object_type.corridor_object) or 636 (not room and not self.object_type.corridor_object) then 637 return 638 end 639 640 local fullscreen = ui:getWindow(UIFullscreen) 641 if fullscreen then 642 fullscreen:close() 643 end 644 645 if self.object_type.class == "Plant" or self.object_type.class == "Machine" then 646 local taskType = "watering" 647 if self.object_type.class == "Machine" then 648 taskType = "repairing" 649 end 650 local index = self.hospital:getIndexOfTask(self.tile_x, self.tile_y, taskType) 651 if index ~= -1 then 652 self.hospital:removeHandymanTask(index, taskType) 653 end 654 end 655 656 self.picked_up = true 657 self.world:destroyEntity(self) 658 -- NB: the object has to be destroyed before updating/creating the window, 659 -- or the blueprint will be wrong 660 if not window then 661 window = UIPlaceObjects(ui, object_list, false) -- don't pay for 662 ui:addWindow(window) 663 else 664 window:stopPickupItems() 665 window:addObjects(object_list, false) -- don't pay for 666 window:selectObjectType(self.object_type) 667 window:checkEnableConfirm() -- since we removed an object from the room, the requirements may not be met anymore 668 end 669 window:setOrientation(direction) 670 self.orientation_before = self.direction 671 ui:playSound("pickup.wav") 672 end 673end 674 675function Object:eraseObject() 676 self.world.map.th:eraseObjectTypes(self.tile_x, self.tile_y) 677end 678 679function Object:resetAnimation() 680 self.world.map.th:setCellFlags(self.tile_x, self.tile_y, {thob = self.object_type.thob}) 681 self.th:setDrawingLayer(self:getDrawingLayer()) 682 self.th:setTile(self.world.map.th, self:getRenderAttachTile()) 683end 684 685function Object:onDestroy() 686 local room = self:getRoom() 687 if room then 688 room.objects[self] = nil 689 end 690 if self.user_list then 691 for _, user in ipairs(self.user_list) do 692 user:handleRemovedObject(self) 693 end 694 self.user_list = {} 695 elseif self.user then 696 self.user:handleRemovedObject(self) 697 end 698 self.user = nil 699 if self.reserved_for_list then 700 for _, reserver in ipairs(self.reserved_for_list) do 701 reserver:handleRemovedObject(self) 702 end 703 self.reserved_for_list = {} 704 elseif self.reserved_for then 705 self.reserved_for:handleRemovedObject(self) 706 end 707 self.reserved_for = nil 708 709 Entity.onDestroy(self) 710 711 -- Issue 1105 - rebuild wall travel<dir> and pathfinding on side object removal 712 if self.object_type.class == "SideObject" then 713 self.world.map.th:updatePathfinding() 714 self.world:resetSideObjects() 715 end 716end 717 718function Object:afterLoad(old, new) 719 if old < 52 then 720 self.hospital = self.world:getLocalPlayerHospital() 721 end 722 if old < 57 then 723 if self.footprint and self.direction then 724 local object_type = self.object_type 725 local footprint = object_type.orientations 726 footprint = footprint and footprint[self.direction] 727 if footprint and footprint.animation_offset then 728 self:setPosition(unpack(footprint.animation_offset)) 729 end 730 footprint = footprint and footprint.footprint 731 self.footprint = footprint 732 if object_type.class == "SideObject" then 733 local flags = {buildable = true} 734 self.world.map.th:setCellFlags(self.tile_x, self.tile_y, flags) 735 end 736 self:setTile(self.tile_x, self.tile_y) 737 end 738 end 739 return Entity.afterLoad(self, old, new) 740end 741 742local all_pathfind_dirs = {[0] = true, [1] = true, [2] = true, [3] = true} 743 744function Object.processTypeDefinition(object_type) 745 if object_type.id == "extinguisher" or object_type.id == "radiator" or 746 object_type.id == "plant" or object_type.id == "reception_desk" or 747 object_type.id == "bench" then 748 object_type.count_category = object_type.id 749 elseif object_type.id ~= "bin" and not object_type.corridor_object and 750 not object_type.id:find("door") then 751 object_type.count_category = "general" 752 end 753 if object_type.orientations then 754 for _, details in pairs(object_type.orientations) do 755 -- Set default values 756 if not details.animation_offset then 757 details.animation_offset = {0, 0} 758 end 759 if not details.render_attach_position then 760 details.render_attach_position = {0, 0} 761 end 762 -- Set the usage position 763 if details.use_position == "passable" then 764 -- "passable" => the *first* passable tile in the footprint list 765 for _, point in pairs(details.footprint) do 766 if point.only_passable then 767 details.use_position = {point[1], point[2]} 768 break 769 end 770 end 771 elseif not details.use_position then 772 details.use_position = {0, 0} 773 end 774 -- Set handyman repair tile 775 if object_type.default_strength and not details.handyman_position then 776 details.handyman_position = details.use_position 777 end 778 -- Find the nearest solid tile in the footprint to the usage position 779 local use_position = details.use_position 780 local solid_near_use_position 781 local solid_near_use_position_d = 10000 782 for _, point in pairs(details.footprint) do repeat 783 if point.only_passable then 784 break -- continue 785 end 786 local d = (point[1] - use_position[1])^2 + (point[2] - use_position[2])^2 787 if d >= solid_near_use_position_d then 788 break -- continue 789 end 790 solid_near_use_position = point 791 solid_near_use_position_d = d 792 until true end 793 if solid_near_use_position_d ~= 1 then 794 details.pathfind_allowed_dirs = all_pathfind_dirs 795 else 796 if use_position[1] < solid_near_use_position[1] then 797 details.pathfind_allowed_dirs = {[1] = true} 798 elseif use_position[1] > solid_near_use_position[1] then 799 details.pathfind_allowed_dirs = {[3] = true} 800 elseif use_position[2] < solid_near_use_position[2] then 801 details.pathfind_allowed_dirs = {[2] = true} 802 else 803 details.pathfind_allowed_dirs = {[0] = true} 804 end 805 end 806 -- Adjust the footprint to make this tile the origin 807 local solid_points = {} 808 if solid_near_use_position then 809 local x, y = unpack(solid_near_use_position) 810 for _, point in pairs(details.footprint) do 811 point[1] = point[1] - x 812 point[2] = point[2] - y 813 if not point.only_passable then 814 solid_points[point[1] * 100 + point[2]] = point 815 end 816 end 817 for _, key in ipairs({"use_position_secondary", 818 "finish_use_position", 819 "finish_use_position_secondary"}) do 820 if details[key] then 821 details[key][1] = details[key][1] - x 822 details[key][2] = details[key][2] - y 823 end 824 end 825 use_position[1] = use_position[1] - x 826 use_position[2] = use_position[2] - y 827 if details.slave_position then 828 details.slave_position[1] = details.slave_position[1] - x 829 details.slave_position[2] = details.slave_position[2] - y 830 end 831 local rx, ry = unpack(details.render_attach_position) 832 if type(rx) == "table" then 833 rx, ry = unpack(details.render_attach_position[1]) 834 for _, point in ipairs(details.render_attach_position) do 835 point.column = point[1] - point[2] 836 point[1] = point[1] - x 837 point[2] = point[2] - y 838 end 839 else 840 details.render_attach_position[1] = rx - x 841 details.render_attach_position[2] = ry - y 842 end 843 x, y = Map:WorldToScreen(rx + 1, ry + 1) 844 details.animation_offset[1] = details.animation_offset[1] - x 845 details.animation_offset[2] = details.animation_offset[2] - y 846 end 847 -- Find the region around the solid part of the footprint 848 local adjacent_set = {} 849 local adjacent_list = {} 850 details.adjacent_to_solid_footprint = adjacent_list 851 for _, point in pairs(solid_points) do 852 for _, delta in ipairs({{-1, 0}, {0, -1}, {0, 1}, {1, 0}}) do 853 local x = point[1] + delta[1] 854 local y = point[2] + delta[2] 855 local k2 = x * 100 + y 856 if not solid_points[k2] and not adjacent_set[k2] then 857 adjacent_set[k2] = {x, y} 858 adjacent_list[#adjacent_list + 1] = adjacent_set[k2] 859 end 860 end 861 end 862 end 863 end 864end 865 866--[[ Gets the state of an object 867 868! The state can be later used to set the state of this object. This is 869useful when we would destroy and create a new object that should represent 870the same object. 871 872!return (table) state 873]] 874function Object:getState() 875 return {times_used = self.times_used} 876end 877 878--[[ Sets the state of an object 879 880! This is a complement to a pair function. IT will use the generated state 881table to update it's state. 882!param state (table) table holding the state 883!return (void) 884]] 885function Object:setState(state) 886 if state then 887 self.times_used = state.times_used 888 end 889end 890