diff --git a/.luacheckrc b/.luacheckrc index 334f350dc..ec80df3a0 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -143,6 +143,7 @@ add_ignore("CorsixTH/Lua/app.lua", "212") add_ignore("CorsixTH/Lua/calls_dispatcher.lua", "212") add_ignore("CorsixTH/Lua/dialogs/bottom_panel.lua", "212") add_ignore("CorsixTH/Lua/dialogs/confirm_dialog.lua", "212") +add_ignore("CorsixTH/Lua/dialogs/edit_room.lua", "113") -- accessing hasBit and bitOr utility.lua functions add_ignore("CorsixTH/Lua/dialogs/edit_room.lua", "212") add_ignore("CorsixTH/Lua/dialogs/edit_room.lua", "542") add_ignore("CorsixTH/Lua/dialogs/fullscreen.lua", "212") @@ -223,6 +224,7 @@ add_ignore("CorsixTH/Lua/strings.lua", "212") add_ignore("CorsixTH/Lua/strings.lua", "122") add_ignore("CorsixTH/Lua/ui.lua", "111") -- _ is set in debug code add_ignore("CorsixTH/Lua/ui.lua", "212") +add_ignore("CorsixTH/Lua/utility.lua", "111") -- defining hasBit and bitOr add_ignore("CorsixTH/Lua/utility.lua", "121") add_ignore("CorsixTH/Lua/window.lua", "212") add_ignore("CorsixTH/Lua/window.lua", "542") diff --git a/CorsixTH/Lua/app.lua b/CorsixTH/Lua/app.lua index 58597806a..360a89543 100644 --- a/CorsixTH/Lua/app.lua +++ b/CorsixTH/Lua/app.lua @@ -28,7 +28,7 @@ local runDebugger = dofile "run_debugger" -- Increment each time a savegame break would occur -- and add compatibility code in afterLoad functions -local SAVEGAME_VERSION = 118 +local SAVEGAME_VERSION = 119 class "App" diff --git a/CorsixTH/Lua/dialogs/edit_room.lua b/CorsixTH/Lua/dialogs/edit_room.lua index 3ecf710ed..587161a71 100644 --- a/CorsixTH/Lua/dialogs/edit_room.lua +++ b/CorsixTH/Lua/dialogs/edit_room.lua @@ -711,7 +711,13 @@ function UIEditRoom:screenToWall(x, y) -- top corner local x_, _ = self.ui:WorldToScreen(cellx, celly) if x >= x_ then - return cellx + 1 + modifier, celly, "north" + -- correctly reflects (at least origin version) of TH. + -- Swing doors in top corner to the east, actually skip another tile + if swinging then + return cellx + 2 + modifier, celly, "north" + else + return cellx + 1 + modifier, celly, "north" + end else return cellx, celly + 1 + modifier, "west" end @@ -727,7 +733,11 @@ function UIEditRoom:screenToWall(x, y) -- left corner local _, y_ = self.ui:WorldToScreen(cellx, celly) if y >= y_ + 16 then - return cellx + 1, celly, "south" + if swinging and cellx <= rect.x + 2 then + return cellx + 1 + modifier, celly, "south" + else + return cellx + 1, celly, "south" + end else return cellx, celly - 1, "west" end @@ -735,7 +745,11 @@ function UIEditRoom:screenToWall(x, y) -- right corner local _, y_ = self.ui:WorldToScreen(cellx, celly) if y >= y_ + 16 then - return cellx, celly + 1, "east" + if swinging and celly <= rect.y + 2 then + return cellx, celly + 1 + modifier, "east" + else + return cellx, celly + 1, "east" + end else return cellx - 1, celly, "north" end @@ -752,19 +766,22 @@ function UIEditRoom:screenToWall(x, y) return rect.x, celly, "west" elseif (celly == rect.y - 1 or celly == rect.y) and rect.x <= cellx and cellx < rect.x + rect.w then -- north edge - if cellx == rect.x then - cellx = rect.x + 1 + -- correctly reflects (at least origin version) of TH. + -- Swing doors in top corner to the east, actually skip another tile + if swinging and cellx <= rect.x + 2 then + cellx = rect.x + 3 + elseif cellx == rect.x then + cellx = rect.x + 1 + modifier elseif cellx == rect.x + rect.w - 1 then cellx = rect.x + rect.w - 2 end - if swinging and cellx <= rect.x + 1 then - cellx = cellx + 1 - end return cellx, rect.y, "north" elseif (cellx == rect.x + rect.w or cellx == rect.x + rect.w - 1) and rect.y <= celly and celly < rect.y + rect.h then - -- east edge - if celly == rect.y then + -- east edge + if swinging and celly <= rect.y + 1 then + celly = rect.y + 2 + elseif celly == rect.y then celly = rect.y + 1 elseif celly == rect.y + rect.h - 1 then celly = rect.y + rect.h - 2 @@ -773,7 +790,9 @@ function UIEditRoom:screenToWall(x, y) elseif (celly == rect.y + rect.h or celly == rect.y + rect.h - 1) and rect.x <= cellx and cellx < rect.x + rect.w then -- south edge - if cellx == rect.x then + if swinging and cellx <= rect.x + 1 then + cellx = rect.x + 2 + elseif cellx == rect.x then cellx = rect.x + 1 elseif cellx == rect.x + rect.w - 1 then cellx = rect.x + rect.w - 2 @@ -1053,11 +1072,20 @@ function UIEditRoom:setBlueprintRect(x, y, w, h) rect.h = h end +--25 north wall, west +--26 north wall, east +--27 east wall, south +--28 east wall, north +--29 south wall, west +--30 south wall, east +--31 west wall, south +--32 west wall, north +-- single door blue print values matching TH local door_floor_blueprint_markers = { - north = 25, - east = 28, - south = 29, - west = 32, + north = 26, + east = 27, + south = 30, + west = 31 } local window_floor_blueprint_markers = { @@ -1071,8 +1099,12 @@ local window_floor_blueprint_markers = { --!param x (int) X tile position of the door. --!param y (int) Y tile position of the door. --!param wall (string) Name of the wall (either 'north' or 'west'). ---!param has_swingdoor Whether the room has a normal door (false) or a swing door (true) as entrance. ---!return whether the door can be placed at the given position and orientation. +--!param has_swingdoor (boolean) Whether the room has a normal door (false) or a swing door (true) as entrance. +--!return (int) bit flags indicating invalid tile position using 1 based power of 2 as this works with ipairs +--! values returned are the enumeration of +--! 4 = centre door in swing door or for single door the value can just be non-zero but uses the same bit of code +--! 2 (door section closer to top of screen) - smaller x or y +--! 8 (door section closer to bottom of screen) - larger x or y local function checkDoorWalls(x, y, wall, has_swingdoor) local th = TheApp.map.th @@ -1086,42 +1118,51 @@ local function checkDoorWalls(x, y, wall, has_swingdoor) dx = 1 dy = 0 end - + local invalid_tile = 0 if th:getCell(x, y, wall_num) % 0x100 ~= 0 then - return false + invalid_tile = 4 end -- If it is a swing door there are two more locations to check. if has_swingdoor then - if th:getCell(x - dx, y - dy, wall_num) % 0x100 ~= 0 or - th:getCell(x + dx, y + dy, wall_num) % 0x100 ~= 0 then - return false + if th:getCell(x - dx, y - dy, wall_num) % 0x100 ~= 0 then + invalid_tile = invalid_tile + 2 + end + if th:getCell(x + dx, y + dy, wall_num) % 0x100 ~= 0 then + invalid_tile = invalid_tile + 8 end end - return true + return invalid_tile end --! Check whether the given tile can function as a door entry/exit tile. --!param xpos (int) X position of the tile. --!param ypos (int) Y position of the tile. --!param player_id (int) Player id owning the hospital. ---!param flag_names (array) If set, array with two additional required properties. ---!return Whether the tile is considered to be valid. -local function validDoorTile(xpos, ypos, player_id, flag_names) +--!param world - reference to world object instance +--!return (boolean) whether the tile is considered to be valid. +local function validDoorTile(xpos, ypos, player_id, world) local th = TheApp.map.th - local tile_flags = th:getCellFlags(xpos, ypos) - if not (tile_flags.buildable or tile_flags.passable or tile_flags.owner == player_id) then return false end - if not flag_names then return true end - return tile_flags[flag_names[1]] and tile_flags[flag_names[2]] + -- check own it + if tile_flags.owner ~= player_id then return false end + -- any object will cause it to be blocked (ignore litter) + if tile_flags.thob ~= 0 and tile_flags.thob ~= 62 then return false end + -- check if its passable that no object footprint blocks it + if tile_flags.passable then return world:isTileExclusivelyPassable(xpos, ypos, 1) end + return true end -function UIEditRoom:setDoorBlueprint(orig_x, orig_y, orig_wall) - local x = orig_x - local y = orig_y - local wall = orig_wall - - -- Used to get the adjacent tiles when placing swing doors. +--! Calculate position offsets and door blueprint wall values +--! param x (int) doors blueprint x value +--! param y (int) doors blueprint y value +--! param wall (string) original wall orientation +--! return x (int) updated x value +--! return y (int) updated y value +--! return x_mod (int) offest value to apply to tile count to determine relative position +--! return y_mod (int) offset value to apply to tile count to determine relatitve position +--! return wall (string) wall orientation style (only 2 styles) +local function doorWallOffsetCalculations(x, y, wall) local x_mod local y_mod if wall == "south" then @@ -1137,11 +1178,20 @@ function UIEditRoom:setDoorBlueprint(orig_x, orig_y, orig_wall) else y_mod = 2 end + return x, y, x_mod, y_mod, wall +end + +function UIEditRoom:setDoorBlueprint(orig_x, orig_y, orig_wall) + local x, y, x_mod, y_mod, wall = doorWallOffsetCalculations(orig_x, orig_y, orig_wall) local map = TheApp.map.th if self.blueprint_door.anim then if self.room_type.swing_doors then if self.blueprint_door.anim[1] then + -- retrieve the old door position details to reset the blue print + local oldx, oldy + local _, _, oldx_mod, oldy_mod, _ = doorWallOffsetCalculations(self.blueprint_door.floor_x, + self.blueprint_door.floor_y, self.blueprint_door.wall) -- If we're dealing with swing doors the anim variable is actually a table with three -- identical "doors". for i, anim in ipairs(self.blueprint_door.anim) do @@ -1149,8 +1199,10 @@ function UIEditRoom:setDoorBlueprint(orig_x, orig_y, orig_wall) self.blueprint_door.old_flags[i]) anim:setTag(nil) self.blueprint_door.anim[i] = nil + oldx = oldx_mod and self.blueprint_door.floor_x + (i - oldx_mod) or self.blueprint_door.floor_x + oldy = oldy_mod and self.blueprint_door.floor_y + (i - oldy_mod) or self.blueprint_door.floor_y + map:setCell(oldx, oldy, 4, 24) end - map:setCell(self.blueprint_door.floor_x, self.blueprint_door.floor_y, 4, 24) end else self.blueprint_door.anim:setAnimation(self.anims, self.blueprint_door.old_anim, @@ -1202,49 +1254,59 @@ function UIEditRoom:setDoorBlueprint(orig_x, orig_y, orig_wall) flags = 0 y2 = y2 - 1 end - - self.blueprint_door.valid = checkDoorWalls(x, y, wall, self.room_type.swing_doors) - if self.blueprint_door.valid then - -- Ensure that the door isn't being built on top of an object - local flag_names - if wall == "west" then - flag_names = {"buildableNorth", "buildableSouth"} - else - flag_names = {"buildableWest", "buildableEast"} - end - local player_id = self.ui.hospital:getPlayerIndex() - - if not validDoorTile(x, y, player_id, flag_names) or - not validDoorTile(x2, y2, player_id, flag_names) then - self.blueprint_door.valid = false + local world = self.ui.app.world + -- invalid_tile used to select the individual blueprint that is blocked + local invalid_tile = checkDoorWalls(x, y, wall, self.room_type.swing_doors) + -- Ensure that the door isn't being built on top of an object + local player_id = self.ui.hospital:getPlayerIndex() + if not validDoorTile(x, y, player_id, world) or + not validDoorTile(x2, y2, player_id, world) then + invalid_tile = bitOr(invalid_tile, 4) + end + -- If we're making swing doors two more tiles need to be checked. + if self.room_type.swing_doors then + local dx = x_mod and 1 or 0 + local dy = y_mod and 1 or 0 + if not validDoorTile(x + dx, y + dy, player_id, world) or + not validDoorTile(x2 + dx, y2 + dy, player_id, world) then + invalid_tile = bitOr(invalid_tile, 8) end - -- If we're making swing doors two more tiles need to be checked. - if self.room_type.swing_doors then - local dx = x_mod and 1 or 0 - local dy = y_mod and 1 or 0 - if not validDoorTile(x + dx, y + dy, player_id, nil) or - not validDoorTile(x2 + dx, y2 + dy, player_id, nil) then - self.blueprint_door.valid = false - end - if not validDoorTile(x - dx, y - dy, player_id, nil) or - not validDoorTile(x2 - dx, y2 - dy, player_id, nil) then - self.blueprint_door.valid = false - end + if not validDoorTile(x - dx, y - dy, player_id, world) or + not validDoorTile(x2 - dx, y2 - dy, player_id, world) then + invalid_tile = bitOr(invalid_tile, 2) end end - if not self.blueprint_door.valid then - flags = flags + 16 -- Use red palette rather than normal palette - end + + self.blueprint_door.valid = (invalid_tile == 0) + if self.room_type.swing_doors then - for _, animation in ipairs(anim) do - animation:setAnimation(self.anims, 126, flags) + for i, animation in ipairs(anim) do + -- calculation here to flag blocked blueprint tiles on swing doors for each door tile + animation:setAnimation(self.anims, 126, flags + (hasBit(invalid_tile, i) and 1 or 0) * 16) end else - anim:setAnimation(self.anims, 126, flags) + anim:setAnimation(self.anims, 126, flags + (invalid_tile ~= 0 and 1 or 0) * 16) end - if self.blueprint_door.valid then + if self.room_type.swing_doors then + flags = door_floor_blueprint_markers[orig_wall] + local dirfix = orig_wall == "east" + flags = dirfix and flags + 1 or flags + for i = 1, 3 do + local x1 = x_mod and orig_x + i - x_mod or orig_x + local y1 = y_mod and orig_y + i - y_mod or orig_y + if (i == 2) then + map:setCell(x1, y1, 4, 24) + else + if dirfix then + map:setCell(x1, y1, 4, i < 2 and flags or flags - 1) + else + map:setCell(x1, y1, 4, i > 2 and flags or flags - 1) + end + end + end + else map:setCell(self.blueprint_door.floor_x, self.blueprint_door.floor_y, 4, - door_floor_blueprint_markers[orig_wall]) + door_floor_blueprint_markers[orig_wall]) end end diff --git a/CorsixTH/Lua/dialogs/place_objects.lua b/CorsixTH/Lua/dialogs/place_objects.lua index 67f5d7f1a..158024e1b 100644 --- a/CorsixTH/Lua/dialogs/place_objects.lua +++ b/CorsixTH/Lua/dialogs/place_objects.lua @@ -657,11 +657,18 @@ function UIPlaceObjects:setBlueprintCell(x, y) -- Check 3: The footprint tile should either be buildable or passable, is it?: if not tile.only_side and is_object_allowed then - is_object_allowed = world:isFootprintTileBuildableOrPassable(xpos, ypos, tile, object_footprint, flag, player_id) + is_object_allowed = world:isFootprintTileBuildableOrPassable(xpos, ypos, tile, object_footprint, flag, player_id) elseif is_object_allowed then is_object_allowed = map:getCellFlags(xpos, ypos, flags)[flag] and (player_id == 0 or flags.owner == player_id) end + -- ignore placed object tile if it is shareable + if not tile.shareable and is_object_allowed then + -- Check 4: only one object per tile allowed original TH + -- can build on litter and unoccupied tiles and only placeable if not on another objects passable footprint unless that too is a shareable tile + is_object_allowed = world:isTileExclusivelyPassable(xpos, ypos, 10) + end + -- Having checked if the tile is good set its blueprint appearance flag: if is_object_allowed then if not tile.invisible then diff --git a/CorsixTH/Lua/objects/bench.lua b/CorsixTH/Lua/objects/bench.lua index 8b1b044a8..c617a8b4a 100644 --- a/CorsixTH/Lua/objects/bench.lua +++ b/CorsixTH/Lua/objects/bench.lua @@ -126,19 +126,19 @@ object.usage_animations = { object.orientations = { north = { render_attach_position = { {0, 0}, {-1, 0}, {0, -1} }, - footprint = { {0, 0, complete_cell = true}, {0, -1, only_passable = true, invisible = true} }, + footprint = { {0, 0, complete_cell = true}, {0, -1, only_passable = true, invisible = true, shareable = true} }, use_position = "passable", }, east = { - footprint = { {0, 0, complete_cell = true}, {1, 0, only_passable = true, invisible = true} }, + footprint = { {0, 0, complete_cell = true}, {1, 0, only_passable = true, invisible = true, shareable = true} }, use_position = "passable", }, south = { - footprint = { {0, 0, complete_cell = true}, {0, 1, only_passable = true, invisible = true} }, + footprint = { {0, 0, complete_cell = true}, {0, 1, only_passable = true, invisible = true, shareable = true} }, use_position = "passable", }, west = { - footprint = { {0, 0, complete_cell = true}, {-1, 0, only_passable = true, invisible = true} }, + footprint = { {0, 0, complete_cell = true}, {-1, 0, only_passable = true, invisible = true, shareable = true} }, use_position = "passable", }, } @@ -194,4 +194,12 @@ function Bench:onDestroy() Object.onDestroy(self) end +--! This function is automatically called after loading a game and serves for compatibility. +function Bench:afterLoad(old, new) + if old < 119 then + self.footprint = object.orientations[self.direction].footprint + end + Object.afterLoad(self, old, new) +end + return object diff --git a/CorsixTH/Lua/objects/chair.lua b/CorsixTH/Lua/objects/chair.lua index 53f7540d7..dc20874cc 100644 --- a/CorsixTH/Lua/objects/chair.lua +++ b/CorsixTH/Lua/objects/chair.lua @@ -136,21 +136,38 @@ object.usage_animations = { } object.orientations = { north = { - footprint = { {0, 0, complete_cell = true}, {0, -1, only_passable = true} }, + footprint = { {0, 0, complete_cell = true}, {0, -1, only_passable = true, invisible = true, shareable = true} }, use_position = "passable", }, east = { - footprint = { {0, 0, complete_cell = true}, {1, 0, only_passable = true} }, + footprint = { {0, 0, complete_cell = true}, {1, 0, only_passable = true, invisible = true, shareable = true} }, use_position = "passable", }, south = { - footprint = { {0, 0, complete_cell = true}, {0, 1, only_passable = true} }, + footprint = { {0, 0, complete_cell = true}, {0, 1, only_passable = true, invisible = true, shareable = true} }, use_position = "passable", }, west = { - footprint = { {0, 0, complete_cell = true}, {-1, 0, only_passable = true} }, + footprint = { {0, 0, complete_cell = true}, {-1, 0, only_passable = true, invisible = true, shareable = true} }, use_position = "passable", }, } +class "Chair" (Object) + +---@type Chair +local Chair = _G["Chair"] + +function Chair:Chair(...) + self:Object(...) +end + +--! This function is automatically called after loading a game and serves for compatibility. +function Chair:afterLoad(old, new) + if old < 119 then + self.footprint = object.orientations[self.direction].footprint + end + Object.afterLoad(self, old, new) +end + return object diff --git a/CorsixTH/Lua/utility.lua b/CorsixTH/Lua/utility.lua index 3bedfaf1b..281c18fdb 100644 --- a/CorsixTH/Lua/utility.lua +++ b/CorsixTH/Lua/utility.lua @@ -293,3 +293,22 @@ function rangeMapLookup(number, buckets) end assert(false) -- Should never get here. end + +-- this is a pseudo bitwise OR operation +-- assumes value2 is always a power of 2 (limits carry errors in the addition) +-- mimics the logic of hasBit with the addition if bit not set +--!param value1 (int) value to check set bit of +--!param value2 (int) power of 2 value - bit enumeration +--!return (int) value1 and value2 'bitwise' or. +function bitOr(value1, value2) + return value1 % (value2 + value2) >= value2 and value1 or value1 + value2 +end + +--! Check bit is set +--!param value (int) value to check set bit of +--!param bit (int) 0-base index of bit to check +--!return (boolean) true if bit is set. +function hasBit(value, bit) + local p = 2 ^ bit + return value % (p + p) >= p +end diff --git a/CorsixTH/Lua/world.lua b/CorsixTH/Lua/world.lua index a1ab8fe6e..23e735529 100644 --- a/CorsixTH/Lua/world.lua +++ b/CorsixTH/Lua/world.lua @@ -2697,3 +2697,32 @@ function World:resetSideObjects() end end end + +--[[ When placing doors and objects the passable tiles need to be checked for overlapping +passable tiles. This presents problems with objects like Bench where the passable tile +is not for exclusive use of the Bench (another object can share that same tile) +the footprint.shareable differentiates shareable passable tiles, and exclusive use +passable tiles (the norm for most objects)]] +--!param x (int) x map tile position +--!param y (int) y map tile position +--!param distance (int) searchable distance for nearby objects +--!return (boolean) indicating if exclusively passable or not +function World:isTileExclusivelyPassable(x, y, distance) + for o in pairs(self:findAllObjectsNear(x, y, distance)) do + if o and o.footprint then + for _, footprint in pairs(o.footprint) do + if footprint[1] + o.tile_x == x and footprint[2] + o.tile_y == y and footprint.only_passable and not footprint.shareable then + return false + end + end + else + -- doors don't have a footprint but objects can't be built blocking them either + for _, footprint in pairs(o:getWalkableTiles()) do + if o.object_type and o.object_type.thob ~= 62 and footprint[1] == x and footprint[2] == y then + return false + end + end + end + end + return true +end